Skip to content

Commit 0d033c5

Browse files
committed
wip
1 parent 7483d3d commit 0d033c5

13 files changed

+312
-69
lines changed

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"main": "src/index.js",
2323
"types": "dist/src/index.d.ts",
2424
"scripts": {
25-
"build": "esbuild --bundle src/index.js --format=esm --external:node:buffer --external:node:events --external:node:async_hooks --sourcemap --minify --outfile=dist/worker.mjs && npm run build:tsc",
26-
"build:debug": "esbuild --bundle src/index.js --format=esm --external:node:buffer --external:node:events --external:node:async_hooks --outfile=dist/worker.mjs",
25+
"build": "esbuild --bundle src/index.js --format=esm --external:node:buffer --external:node:events --external:node:async_hooks --external:cloudflare:workers --sourcemap --minify --outfile=dist/worker.mjs && npm run build:tsc",
26+
"build:debug": "esbuild --bundle src/index.js --format=esm --external:node:buffer --external:node:events --external:node:async_hooks --external:cloudflare:workers --outfile=dist/worker.mjs",
2727
"build:tsc": "tsc --build",
2828
"dev": "npm run build:debug && miniflare dist/worker.mjs --watch --debug -m --r2-persist --global-async-io --global-timers",
2929
"lint": "standard",
@@ -57,6 +57,7 @@
5757
"multiformats": "^13.0.1"
5858
},
5959
"devDependencies": {
60+
"@ipld/dag-ucan": "^3.4.5",
6061
"@storacha/cli": "^1.6.2",
6162
"@storacha/client": "^1.8.2",
6263
"@types/chai": "^5.0.0",
@@ -81,5 +82,6 @@
8182
"ignore": [
8283
"*.ts"
8384
]
84-
}
85+
},
86+
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
8587
}

scripts/mk-validator-proof.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Create a "ucan/attest" delegation allowing the gateway to validate
3+
* attestations issued by the upload-service.
4+
*
5+
* This generates the GATEWAY_VALIDATOR_PROOF environment variable value.
6+
*
7+
* Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>
8+
*
9+
* Example (staging):
10+
* node scripts/mk-validator-proof.js \
11+
* did:web:staging.up.storacha.network \
12+
* MgCZT5J+...your-key-here... \
13+
* did:web:staging.w3s.link
14+
*
15+
* Example (production):
16+
* node scripts/mk-validator-proof.js \
17+
* did:web:up.storacha.network \
18+
* MgCZT5J+...your-key-here... \
19+
* did:web:w3s.link
20+
*/
21+
import * as DID from '@ipld/dag-ucan/did'
22+
import { CAR, delegate } from '@ucanto/core'
23+
import * as ed25519 from '@ucanto/principal/ed25519'
24+
import { base64 } from 'multiformats/bases/base64'
25+
import { identity } from 'multiformats/hashes/identity'
26+
import * as Link from 'multiformats/link'
27+
28+
// CORRECT DIRECTION (staging):
29+
// - issuer should be did:web:staging.up.storacha.network (upload-service)
30+
// - audience should be did:web:staging.w3s.link (gateway)
31+
// - can should be 'ucan/attest'
32+
// - with should be issuer.did() (i.e. did:web:staging.up.storacha.network)
33+
// The private key must be the upload-service private key. This makes the
34+
// gateway trust attestations issued by the upload-service.
35+
36+
37+
const uploadServiceDIDWeb = process.argv[2]
38+
const uploadServicePrivateKey = process.argv[3]
39+
const gatewayDIDWeb = process.argv[4]
40+
41+
if (!uploadServiceDIDWeb || !uploadServicePrivateKey || !gatewayDIDWeb) {
42+
console.error('Error: Missing required arguments')
43+
console.error('Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>')
44+
console.error('')
45+
console.error('Example (staging):')
46+
console.error(' node scripts/mk-validator-proof.js \\')
47+
console.error(' did:web:staging.up.storacha.network \\')
48+
console.error(' MgCZT5J+...your-key-here... \\')
49+
console.error(' did:web:staging.w3s.link')
50+
process.exit(1)
51+
}
52+
53+
console.log(`Upload Service DID: ${uploadServiceDIDWeb}`)
54+
console.log(`Upload Service Private Key: ${uploadServicePrivateKey.slice(0, 7)}...${uploadServicePrivateKey.slice(-7)}`)
55+
console.log(`Gateway DID: ${gatewayDIDWeb}`)
56+
console.log('')
57+
58+
const issuer = ed25519
59+
.parse(uploadServicePrivateKey)
60+
.withDID(DID.parse(uploadServiceDIDWeb).did())
61+
const audience = DID.parse(gatewayDIDWeb)
62+
63+
// Note: variable names are confusing - "uploadService" is actually the issuer (gateway in our case)
64+
// and "gateway" is actually the audience (upload service in our case)
65+
// The 'with' should be the issuer's DID per colleague's instructions
66+
const delegation = await delegate({
67+
issuer: issuer,
68+
audience: audience,
69+
capabilities: [{ can: 'ucan/attest', with: issuer.did() }],
70+
expiration: Infinity
71+
})
72+
73+
console.log('✅ Delegation created:')
74+
console.log(` Issuer: ${issuer.did()}`)
75+
console.log(` Audience: ${audience.did()}`)
76+
console.log(` Capability: ucan/attest with ${issuer.did()}`)
77+
console.log('')
78+
79+
const res = await delegation.archive()
80+
if (res.error) {
81+
console.error('❌ Error archiving delegation:', res.error)
82+
throw res.error
83+
}
84+
85+
const proof = Link.create(CAR.code, identity.digest(res.ok)).toString(base64)
86+
87+
console.log('✅ Validator proof generated successfully!')
88+
console.log('')
89+
console.log('Add this to your environment variables:')
90+
console.log('')
91+
console.log('GATEWAY_VALIDATOR_PROOF=' + proof)
92+
console.log('')

src/bindings.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export interface Environment
2626
HONEYCOMB_API_KEY: string
2727
FF_TELEMETRY_ENABLED: string
2828
TELEMETRY_RATIO: string
29+
GATEWAY_VALIDATOR_PROOF?: string
2930
}

src/middleware/withAuthorizedSpace.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { Verifier } from '@ucanto/principal'
22
import { ok, access, Unauthorized } from '@ucanto/validator'
3+
import { resolveDIDKey, getValidatorProofs } from '../server/index.js'
34
import { HttpError } from '@web3-storage/gateway-lib/util'
45
import * as serve from '../capabilities/serve.js'
56
import { SpaceDID } from '@storacha/capabilities/utils'
7+
68

79
/**
810
* @import * as Ucanto from '@ucanto/interface'
911
* @import { IpfsUrlContext, Middleware } from '@web3-storage/gateway-lib'
1012
* @import { LocatorContext } from './withLocator.types.js'
11-
* @import { AuthTokenContext } from './withAuthToken.types.js'
13+
* @import { AuthTokenContext, Environment } from './withAuthToken.types.js'
1214
* @import { SpaceContext } from './withAuthorizedSpace.types.js'
13-
* @import { DelegationsStorageContext } from './withDelegationsStorage.types.js'
15+
* @import { DelegationsStorageContext, DelegationsStorageEnvironment } from './withDelegationsStorage.types.js'
1416
* @import { GatewayIdentityContext } from './withGatewayIdentity.types.js'
1517
* @import { DelegationProofsContext } from './withAuthorizedSpace.types.js'
1618
*/
@@ -61,7 +63,8 @@ export function withAuthorizedSpace (handler) {
6163
// First space to successfully authorize is the one we'll use.
6264
const { space: selectedSpace, delegationProofs } = await Promise.any(
6365
spaces.map(async (space) => {
64-
const result = await authorize(SpaceDID.from(space), ctx)
66+
// @ts-ignore
67+
const result = await authorize(SpaceDID.from(space), ctx, env)
6568
if (result.error) throw result.error
6669
return result.ok
6770
})
@@ -102,9 +105,10 @@ export function withAuthorizedSpace (handler) {
102105
*
103106
* @param {import('@storacha/capabilities/types').SpaceDID} space
104107
* @param {AuthTokenContext & DelegationsStorageContext & GatewayIdentityContext} ctx
108+
* @param {import('./withRateLimit.types.js').Environment} env
105109
* @returns {Promise<Ucanto.Result<{space: import('@storacha/capabilities/types').SpaceDID, delegationProofs: Ucanto.Delegation[]}, Ucanto.Failure>>}
106110
*/
107-
const authorize = async (space, ctx) => {
111+
const authorize = async (space, ctx, env) => {
108112
// Look up delegations that might authorize us to serve the content.
109113
const relevantDelegationsResult = await ctx.delegationsStorage.find(space)
110114
if (relevantDelegationsResult.error) return relevantDelegationsResult
@@ -121,12 +125,15 @@ const authorize = async (space, ctx) => {
121125
proofs: delegationProofs
122126
})
123127
.delegate()
124-
125-
// Validate the invocation.
128+
129+
// Load validator proofs and validate the invocation
130+
const validatorProofs = await getValidatorProofs(env)
126131
const accessResult = await access(invocation, {
127132
capability: serve.transportHttp,
128133
authority: ctx.gatewayIdentity,
129134
principal: Verifier,
135+
proofs: validatorProofs,
136+
resolveDIDKey,
130137
validateAuthorization: () => ok({})
131138
})
132139
if (accessResult.error) {

src/middleware/withDelegationsStorage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ function createStorage (env) {
8888
try {
8989
await env.CONTENT_SERVE_DELEGATIONS_STORE.put(
9090
`${space}:${delegation.cid.toString()}`,
91-
value.ok.buffer,
91+
/** @type {ArrayBuffer} */ (value.ok.buffer),
9292
options
9393
)
9494
return ok({})

src/middleware/withDelegationsStorage.types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SpaceDID } from '@storacha/capabilities/types'
77
export interface DelegationsStorageEnvironment extends MiddlewareEnvironment {
88
CONTENT_SERVE_DELEGATIONS_STORE: KVNamespace
99
FF_DELEGATIONS_STORAGE_ENABLED: string
10+
GATEWAY_VALIDATOR_PROOF?: string
1011
}
1112

1213
export interface DelegationsStorageContext
@@ -35,6 +36,6 @@ export interface DelegationsStorage {
3536
*/
3637
store: (
3738
space: SpaceDID,
38-
delegation: Ucanto.Delegation<Ucanto.Capabilities>
39+
delegation: Ucanto.Delegation<Ucanto.Capabilities>,
3940
) => Promise<Ucanto.Result<Ucanto.Unit, StoreOperationFailed | Ucanto.Failure>>
4041
}

src/middleware/withRateLimit.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Environment extends MiddlewareEnvironment {
66
RATE_LIMITER: RateLimit
77
AUTH_TOKEN_METADATA: KVNamespace
88
FF_RATE_LIMITER_ENABLED: string
9+
GATEWAY_VALIDATOR_PROOF?: string
910
}
1011

1112
export interface TokenMetadata {

src/middleware/withUcanInvocationHandler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export function withUcanInvocationHandler (handler) {
2222
return handler(request, env, ctx)
2323
}
2424

25-
const service = ctx.service ?? createService(ctx)
26-
const server = ctx.server ?? createServer(ctx, service)
25+
const service = ctx.service ?? await createService(ctx, env)
26+
const server = ctx.server ?? await createServer(ctx, service, env)
2727

2828
const { headers, body, status } = await server.request({
2929
body: new Uint8Array(await request.arrayBuffer()),

src/middleware/withUcanInvocationHandler.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DelegationsStorageContext } from './withDelegationsStorage.types.js'
44
import { Service } from '../server/api.types.js'
55
import * as Server from '@ucanto/server'
66
export interface Environment extends MiddlewareEnvironment {
7+
GATEWAY_VALIDATOR_PROOF?: string
78
}
89

910
export interface Context<T = unknown, U = unknown>

src/server/index.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,93 @@
11
import * as Server from '@ucanto/server'
22
import * as CAR from '@ucanto/transport/car'
3+
import { DIDResolutionError } from '@ucanto/validator'
4+
import * as Proof from '@storacha/client/proof'
5+
6+
/**
7+
* Known did:web to did:key mappings for signature verification
8+
* @type {Record<`did:${string}:${string}`, `did:key:${string}`>}
9+
*/
10+
const knownWebDIDs = {
11+
// Production
12+
'did:web:up.storacha.network': 'did:key:z6MkqdncRZ1wj8zxCTDUQ8CRT8NQWd63T7mZRvZUX8B7XDFi',
13+
'did:web:web3.storage': 'did:key:z6MkqdncRZ1wj8zxCTDUQ8CRT8NQWd63T7mZRvZUX8B7XDFi',
14+
'did:web:w3s.link': 'did:key:z6Mkha3NLZ38QiZXsUHKRHecoumtha3LnbYEL21kXYBFXvo5',
15+
16+
// Staging
17+
'did:web:staging.up.storacha.network': 'did:key:z6MkhcbEpJpEvNVDd3n5RurquVdqs5dPU16JDU5VZTDtFgnn',
18+
'did:web:staging.web3.storage': 'did:key:z6MkhcbEpJpEvNVDd3n5RurquVdqs5dPU16JDU5VZTDtFgnn',
19+
'did:web:staging.w3s.link': 'did:key:z6MkqK1d4thaCEXSGZ6EchJw3tDPhQriwynWDuR55ayATMNf',
20+
}
21+
22+
/**
23+
* Resolves did:web DIDs to their corresponding did:key DIDs
24+
* @param {import('@ucanto/interface').DID} did
25+
*/
26+
export const resolveDIDKey = async (did) => {
27+
if (knownWebDIDs[did]) {
28+
const didKey = /** @type {`did:key:${string}`} */ (knownWebDIDs[did])
29+
return Server.ok([didKey]) // Return array of did:keys
30+
}
31+
return Server.error(new DIDResolutionError(did))
32+
}
33+
34+
/**
35+
* @type {import('@ucanto/interface').Delegation[]}
36+
*/
37+
let cachedValidatorProofs
38+
39+
/**
40+
* Loads validator proofs from environment variable.
41+
* These proofs allow the gateway to validate ucan/attest delegations.
42+
*
43+
* @param {{ GATEWAY_VALIDATOR_PROOF?: string }} env
44+
* @returns {Promise<import('@ucanto/interface').Delegation[]>}
45+
*/
46+
export const getValidatorProofs = async (env) => {
47+
if (cachedValidatorProofs) {
48+
return cachedValidatorProofs
49+
}
50+
cachedValidatorProofs = []
51+
if (env.GATEWAY_VALIDATOR_PROOF) {
52+
try {
53+
const proof = await Proof.parse(env.GATEWAY_VALIDATOR_PROOF)
54+
const delegation = /** @type {import('@ucanto/interface').Delegation} */ (proof)
55+
console.log(`Gateway validator proof loaded: [issuer: ${delegation.issuer.did()}, audience: ${delegation.audience.did()}]`)
56+
cachedValidatorProofs = [delegation]
57+
} catch (error) {
58+
console.error('Failed to parse GATEWAY_VALIDATOR_PROOF:', error)
59+
}
60+
}
61+
return cachedValidatorProofs
62+
}
363

464
/**
565
* Creates a UCAN server.
666
*
767
* @template T
868
* @param {import('../middleware/withUcanInvocationHandler.types.js').Context} ctx
969
* @param {import('./api.types.js').Service<T>} service
70+
* @param {{ GATEWAY_VALIDATOR_PROOF?: string }} env
1071
*/
11-
export function createServer (ctx, service) {
72+
export async function createServer (ctx, service, env) {
73+
const proofs = await getValidatorProofs(env)
74+
console.log('Server validator proofs loaded:', proofs.length)
75+
if (proofs.length > 0) {
76+
console.log('First proof details:', {
77+
issuer: proofs[0].issuer.did(),
78+
audience: proofs[0].audience.did(),
79+
capabilities: proofs[0].capabilities.map(c => ({ can: c.can, with: c.with })),
80+
cid: proofs[0].cid.toString()
81+
})
82+
}
1283
return Server.create({
1384
id: ctx.gatewaySigner,
1485
codec: CAR.inbound,
1586
service,
1687
catch: err => console.error(err),
1788
// TODO: wire into revocations
18-
validateAuthorization: () => ({ ok: {} })
89+
validateAuthorization: () => ({ ok: {} }),
90+
resolveDIDKey,
91+
proofs,
1992
})
2093
}

0 commit comments

Comments
 (0)