Skip to content

Commit

Permalink
add proofs and identity to the ctx
Browse files Browse the repository at this point in the history
  • Loading branch information
fforbeck committed Nov 7, 2024
1 parent 5136925 commit f83c689
Show file tree
Hide file tree
Showing 20 changed files with 205 additions and 129 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"standard": "^17.1.0",
"tree-kill": "^1.2.2",
"typescript": "^5.6.3",
"wrangler": "^3.84.1"
"wrangler": "^3.85.0"
},
"standard": {
"ignore": [
Expand Down
14 changes: 10 additions & 4 deletions scripts/delegate-serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ const cli = sade('delegate-serve.js <space> [token]')

cli
.describe(
`Delegates ${serve.star.can} to the Gateway for <space>, with an optional token. Outputs a base64url string suitable for the stub_delegation query parameter. Pipe the output to pbcopy or similar for the quickest workflow.`
`Delegates ${serve.star.can} to the Gateway for <space>, with an optional token. Outputs a base64url string suitable for the stub_delegation query parameter. Pipe the output to pbcopy or similar for the quickest workflow. If the GATEWAY_PRINCIPAL_KEY environment variable is not set, a new key pair will be generated.`
)
.action(async (space, token) => {
const client = await getClient()
const signer =
process.env.GATEWAY_PRINCIPAL_KEY
? ed25519.Signer.parse(process.env.GATEWAY_PRINCIPAL_KEY)
: await ed25519.Signer.generate()

const gatewayIdentity = (await ed25519.Signer.generate()).withDID(
'did:web:w3s.link'
)
const gatewayIdentity = signer.withDID('did:web:w3s.link')

const delegation = await serve.star.delegate({
issuer: client.agent.issuer,
Expand All @@ -32,6 +34,10 @@ cli

const carResult = await delegation.archive()
if (carResult.error) throw carResult.error
process.stdout.write(`Issuer: ${client.agent.issuer.did()}\n`)
process.stdout.write(`Audience: ${gatewayIdentity.did()}\n`)
process.stdout.write(`Space: ${space}\n`)
process.stdout.write(`Token: ${token ?? 'none'}\n`)
process.stdout.write(Buffer.from(carResult.ok).toString('base64url'))
})

Expand Down
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
} from './middleware/index.js'
import { instrument } from '@microlabs/otel-cf-workers'
import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { withEgressClient } from './middleware/withEgressClient.js'
import { withGatewayIdentity } from './middleware/withGatewayIdentity.js'

/**
* @import {
Expand Down Expand Up @@ -57,12 +59,14 @@ const handler = {
createWithHttpMethod('GET', 'HEAD'),
withAuthToken,
withLocator,
withGatewayIdentity,
withDelegationStubs,

// Rate-limit requests
withRateLimit,

// Track egress bytes
// Track Egress
withEgressClient,
withEgressTracker,

// Fetch data
Expand Down
3 changes: 3 additions & 0 deletions src/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export { withVersionHeader } from './withVersionHeader.js'
export { withAuthorizedSpace } from './withAuthorizedSpace.js'
export { withLocator } from './withLocator.js'
export { withEgressTracker } from './withEgressTracker.js'
export { withEgressClient } from './withEgressClient.js'
export { withDelegationStubs } from './withDelegationStubs.js'

export const GATEWAY_DID = 'did:web:w3s.link'
32 changes: 21 additions & 11 deletions src/middleware/withAuthorizedSpace.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Verifier } from '@ucanto/principal'
import { ok, access, Unauthorized } from '@ucanto/validator'
import { HttpError } from '@web3-storage/gateway-lib/util'
import * as serve from '../capabilities/serve.js'
import { Schema } from '@ucanto/client'

/**
* @import * as Ucanto from '@ucanto/interface'
Expand All @@ -27,7 +28,7 @@ import * as serve from '../capabilities/serve.js'
* >
* )}
*/
export function withAuthorizedSpace (handler) {
export function withAuthorizedSpace(handler) {
return async (request, env, ctx) => {
const { locator, dataCid } = ctx
const locRes = await locator.locate(dataCid.multihash)
Expand Down Expand Up @@ -57,17 +58,19 @@ export function withAuthorizedSpace (handler) {

try {
// First space to successfully authorize is the one we'll use.
const space = await Promise.any(
const { space: selectedSpace, delegationProofs } = await Promise.any(
spaces.map(async (space) => {
const result = await authorize(space, ctx)
if (result.error) throw result.error
return space
return result.ok
})
)
debugger
return handler(request, env, {
...ctx,
space,
locator: spaceScopedLocator(locator, space)
space: selectedSpace,
delegationProofs,
locator: spaceScopedLocator(locator, selectedSpace)
})
} catch (error) {
// If all Spaces failed to authorize, throw the first error.
Expand All @@ -90,7 +93,7 @@ export function withAuthorizedSpace (handler) {
*
* @param {Ucanto.DID} space
* @param {AuthTokenContext & DelegationsStorageContext} ctx
* @returns {Promise<Ucanto.Result<{}, Ucanto.Failure>>}
* @returns {Promise<Ucanto.Result<{space: Ucanto.DID, delegationProofs: Ucanto.Delegation[]}, Ucanto.Failure>>}
*/
const authorize = async (space, ctx) => {
// Look up delegations that might authorize us to serve the content.
Expand All @@ -101,7 +104,7 @@ const authorize = async (space, ctx) => {
})

if (relevantDelegationsResult.error) return relevantDelegationsResult

// Create an invocation of the serve capability.
const invocation = await serve.transportHttp
.invoke({
Expand All @@ -111,23 +114,30 @@ const authorize = async (space, ctx) => {
nb: {
token: ctx.authToken
},
proofs: relevantDelegationsResult.ok
proofs: relevantDelegationsResult.ok,
})
.delegate()

// Validate the invocation.
debugger
const accessResult = await access(invocation, {
capability: serve.transportHttp,
authority: ctx.gatewayIdentity,
principal: Verifier,
validateAuthorization: () => ok({})
validateAuthorization: () => ok({}),
resolveDIDKey: () => Schema.ok(ctx.gatewayIdentity.toDIDKey()),
})

debugger
if (accessResult.error) {
return accessResult
}

return { ok: {} }
return {
ok: {
space,
delegationProofs: relevantDelegationsResult.ok
}
}
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/middleware/withAuthorizedSpace.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as Ucanto from '@ucanto/interface'
import { Context as MiddlewareContext } from '@web3-storage/gateway-lib'
import { GatewayIdentityContext as GatewayIdentityContext } from './withGatewayIdentity.types.js'

export interface DelegationsStorageContext extends MiddlewareContext {
/** The identity of the gateway itself */
gatewayIdentity: Ucanto.Signer
export interface DelegationsStorageContext
extends MiddlewareContext,
GatewayIdentityContext {
delegationsStorage: DelegationsStorage
delegationProofs?: Ucanto.Delegation[]
}

export interface SpaceContext extends MiddlewareContext {
Expand Down
31 changes: 13 additions & 18 deletions src/middleware/withDelegationStubs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import * as ed25519 from '@ucanto/principal/ed25519'
import { Delegation } from '@ucanto/core'

const GATEWAY_DID = 'did:web:w3s.link'

/**
* @import * as Ucanto from '@ucanto/interface'
* @import {
Expand All @@ -11,6 +8,7 @@ const GATEWAY_DID = 'did:web:w3s.link'
* } from '@web3-storage/gateway-lib'
* @import { DelegationsStorageContext } from './withAuthorizedSpace.types.js'
* @import { LocatorContext } from './withLocator.types.js'
* @import { GatewayIdentityContext } from './withGatewayIdentity.types.js'
*/

/**
Expand All @@ -25,7 +23,7 @@ const GATEWAY_DID = 'did:web:w3s.link'
* @type {(
* Middleware<
* MiddlewareContext & LocatorContext & DelegationsStorageContext,
* MiddlewareContext & LocatorContext,
* MiddlewareContext & LocatorContext & GatewayIdentityContext,
* {}
* >
* )}
Expand All @@ -52,26 +50,23 @@ export const withDelegationStubs = (handler) => async (request, env, ctx) => {
return handler(request, env, {
...ctx,
delegationsStorage: { find: async () => ({ ok: stubDelegations }) },
// NOTE: It doesn't actually matter right now what key the `gatewayIdentity`
// uses, because we don't need anyone else to execute its invocations.
gatewayIdentity: (await ed25519.Signer.generate()).withDID(GATEWAY_DID),
locator:
stubSpace && isDIDKey(stubSpace)
? {
locate: async (digest, options) => {
const locateResult = await ctx.locator.locate(digest, options)
if (locateResult.error) return locateResult
return {
ok: {
...locateResult.ok,
site: locateResult.ok.site.map((site) => ({
...site,
space: stubSpace
}))
}
locate: async (digest, options) => {
const locateResult = await ctx.locator.locate(digest, options)
if (locateResult.error) return locateResult
return {
ok: {
...locateResult.ok,
site: locateResult.ok.site.map((site) => ({
...site,
space: stubSpace
}))
}
}
}
}
: ctx.locator
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@ import { Space } from '@web3-storage/capabilities'

/**
* @import { Middleware } from '@web3-storage/gateway-lib'
* @typedef {import('./withUcantoClient.types.ts').UcantoClientContext} UcantoClientContext
* @typedef {import('./withUcantoClient.types.ts').Environment} Environment
* @typedef {import('./withEgressClient.types.js').EgressClientContext} EgressClientContext
* @typedef {import('./withEgressClient.types.js').Environment} Environment
*/

/**
* The UCantoClient handler exposes the methods to invoke capabilities on the Upload API.
* The EgressClient handler exposes the methods to invoke capabilities on the Upload API.
*
* @type {Middleware<UcantoClientContext, UcantoClientContext, Environment>}
* @type {Middleware<EgressClientContext, EgressClientContext, Environment>}
*/
export function withUcantoClient (handler) {
export function withEgressClient(handler) {
return async (req, env, ctx) => {
const ucantoClient = await create(env)
const egressClient = await create(env, ctx)

return handler(req, env, { ...ctx, ucantoClient })
return handler(req, env, { ...ctx, egressClient })
}
}

/**
* Creates a UCantoClient instance with the given environment and establishes a connection to the UCanto Server.
* Creates a EgressClient instance with the given environment and establishes a connection to the UCanto Server.
*
* @param {Environment} env
* @returns {Promise<import('./withUcantoClient.types.ts').UCantoClient>}
* @param {import('./withEgressClient.types.js').EgressClientContext} ctx
* @returns {Promise<import('./withEgressClient.types.js').EgressClient>}
*/
async function create (env) {
const service = Verifier.parse(env.SERVICE_ID)
const principal = Signer.parse(env.SIGNER_PRINCIPAL_KEY)
const { connection } = await connect(env.UPLOAD_API_URL, principal)
async function create(env, ctx) {
const principalSigner = ctx.gatewaySigner
const { connection } = await connect(env.UPLOAD_API_URL, principalSigner)

return {
/**
Expand All @@ -46,19 +46,8 @@ async function create (env) {
* @returns {Promise<void>}
*/
record: async (space, resource, bytes, servedAt) =>
egressRecord(space, resource, bytes, servedAt, principal, service, connection),
egressRecord(space, resource, bytes, servedAt, connection, ctx),

/**
* TODO: implement this function
*
* @param {string} authToken
* @returns {Promise<import('./withUcantoClient.types.ts').TokenMetadata | undefined>}
*/
getTokenMetadata: async (authToken) => {
// TODO I think this needs to check the content claims service (?) for any claims relevant to this token
// TODO do we have a plan for this? need to ask Hannah if the indexing service covers this?
return undefined
}
}
}

Expand All @@ -69,7 +58,7 @@ async function create (env) {
* @param {import('@ucanto/principal/ed25519').EdSigner} principal
*
*/
async function connect (serverUrl, principal) {
async function connect(serverUrl, principal) {
const connection = await UCantoClient.connect({
id: principal,
codec: CAR.outbound,
Expand All @@ -80,32 +69,33 @@ async function connect (serverUrl, principal) {
}

/**
* Records the egress bytes in the UCanto Server by invoking the `Usage.record` capability.
* Records the egress bytes in the UCanto Server by invoking the `Space.egressRecord` capability.
*
* @param {import('@ucanto/principal/ed25519').DIDKey} space - The Space DID where the content was served
* @param {import('@ucanto/principal/ed25519').UnknownLink} resource - The link to the resource that was served
* @param {number} bytes - The number of bytes served
* @param {Date} servedAt - The timestamp of when the content was served
* @param {import('@ucanto/principal/ed25519').EdSigner} principal - The principal signer
* @param {Signer.Verifier} service - The service verifier
* @param {any} connection - The connection to execute the command
* @param {import('./withEgressClient.types.js').EgressClientContext} ctx - The egress client context
* @returns {Promise<void>}
*/
async function egressRecord (space, resource, bytes, servedAt, principal, service, connection) {
async function egressRecord(space, resource, bytes, servedAt, connection, ctx) {
debugger
const res = await Space.egressRecord
.invoke({
issuer: principal,
audience: service,
issuer: ctx.gatewayIdentity,
audience: ctx.gatewayIdentity, // TODO should it be the upload service DID?
with: SpaceDID.from(space),
nb: {
resource,
bytes,
servedAt: Math.floor(servedAt.getTime() / 1000)
}
},
proofs: ctx.delegationProofs ? ctx.delegationProofs : []
})
.execute(connection)

if (res.out.error) {
console.error('Failed to record egress', res.out.error)
console.error(`Failed to record egress for space ${space}`, res.out.error)
}
}
Loading

0 comments on commit f83c689

Please sign in to comment.