Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@
"test:unit:only": "npm run build:debug && mocha --experimental-vm-modules"
},
"dependencies": {
"@ipld/dag-json": "^10.2.5",
"@ipld/dag-pb": "^4.1.5",
"@microlabs/otel-cf-workers": "^1.0.0-rc.48",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-base": "^1.27.0",
"@storacha/capabilities": "^1.10.0",
"@storacha/indexing-service-client": "^2.6.6",
"@storacha/indexing-service-client": "^2.6.9",
"@types/node": "^24.10.1",
"@ucanto/client": "^9.0.2",
"@ucanto/core": "^10.4.5",
Expand All @@ -50,14 +51,15 @@
"@ucanto/server": "^11.0.3",
"@ucanto/transport": "^9.2.1",
"@ucanto/validator": "^10.0.1",
"@web3-storage/blob-fetcher": "^4.2.5",
"@web3-storage/blob-fetcher": "^4.2.6",
"@web3-storage/gateway-lib": "^5.2.2",
"dagula": "^8.0.0",
"http-range-parse": "^1.0.0",
"lnmap": "^2.0.0",
"multiformats": "^13.0.1"
},
"devDependencies": {
"@ipld/dag-ucan": "^3.4.5",
"@storacha/blob-index": "^1.2.4",
"@storacha/cli": "^1.6.2",
"@storacha/client": "^1.8.2",
Expand Down
91 changes: 91 additions & 0 deletions scripts/mk-validator-proof.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Create a "ucan/attest" delegation allowing the gateway to validate
* attestations issued by the upload-service.
*
* This generates the GATEWAY_VALIDATOR_PROOF environment variable value.
*
* Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>
*
* Example (staging):
* node scripts/mk-validator-proof.js \
* did:web:staging.up.storacha.network \
* MgCZT5J+...your-key-here... \
* did:web:staging.w3s.link
*
* Example (production):
* node scripts/mk-validator-proof.js \
* did:web:up.storacha.network \
* MgCZT5J+...your-key-here... \
* did:web:w3s.link
*/
import * as DID from '@ipld/dag-ucan/did'
import { CAR, delegate } from '@ucanto/core'
import * as ed25519 from '@ucanto/principal/ed25519'
import { base64 } from 'multiformats/bases/base64'
import { identity } from 'multiformats/hashes/identity'
import * as Link from 'multiformats/link'

// CORRECT DIRECTION (staging):
// - issuer should be did:web:staging.up.storacha.network (upload-service)
// - audience should be did:web:staging.w3s.link (gateway)
// - can should be 'ucan/attest'
// - with should be issuer.did() (i.e. did:web:staging.up.storacha.network)
// The private key must be the upload-service private key. This makes the
// gateway trust attestations issued by the upload-service.

const uploadServiceDIDWeb = process.argv[2]
const uploadServicePrivateKey = process.argv[3]
const gatewayDIDWeb = process.argv[4]

if (!uploadServiceDIDWeb || !uploadServicePrivateKey || !gatewayDIDWeb) {
console.error('Error: Missing required arguments')
console.error('Usage: node scripts/mk-validator-proof.js <upload-service-did-web> <upload-service-private-key> <gateway-did-web>')
console.error('')
console.error('Example (staging):')
console.error(' node scripts/mk-validator-proof.js \\')
console.error(' did:web:staging.up.storacha.network \\')
console.error(' MgCZT5J+...your-key-here... \\')
console.error(' did:web:staging.w3s.link')
process.exit(1)
}

console.log(`Upload Service DID: ${uploadServiceDIDWeb}`)
console.log(`Upload Service Private Key: ${uploadServicePrivateKey.slice(0, 7)}...${uploadServicePrivateKey.slice(-7)}`)
console.log(`Gateway DID: ${gatewayDIDWeb}`)
console.log('')

const issuer = ed25519
.parse(uploadServicePrivateKey)
.withDID(DID.parse(uploadServiceDIDWeb).did())
const audience = DID.parse(gatewayDIDWeb)

// Note: variable names are confusing - "uploadService" is actually the issuer (gateway in our case)
// and "gateway" is actually the audience (upload service in our case)
// The 'with' should be the issuer's DID per colleague's instructions
const delegation = await delegate({
issuer,
audience,
capabilities: [{ can: 'ucan/attest', with: issuer.did() }],
expiration: Infinity
})

console.log('✅ Delegation created:')
console.log(` Issuer: ${issuer.did()}`)
console.log(` Audience: ${audience.did()}`)
console.log(` Capability: ucan/attest with ${issuer.did()}`)
console.log('')

const res = await delegation.archive()
if (res.error) {
console.error('❌ Error archiving delegation:', res.error)
throw res.error
}

const proof = Link.create(CAR.code, identity.digest(res.ok)).toString(base64)

console.log('✅ Validator proof generated successfully!')
console.log('')
console.log('Add this to your environment variables:')
console.log('')
console.log('GATEWAY_VALIDATOR_PROOF=' + proof)
console.log('')
1 change: 1 addition & 0 deletions src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface Environment
HONEYCOMB_API_KEY: string
FF_TELEMETRY_ENABLED: string
TELEMETRY_RATIO: string
GATEWAY_VALIDATOR_PROOF?: string
}
17 changes: 12 additions & 5 deletions src/middleware/withAuthorizedSpace.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Verifier } from '@ucanto/principal'
import { ok, access, fail, Unauthorized } from '@ucanto/validator'
import { resolveDIDKey, getValidatorProofs } from '../server/index.js'
import { HttpError } from '@web3-storage/gateway-lib/util'
import * as serve from '../capabilities/serve.js'
import { SpaceDID } from '@storacha/capabilities/utils'
Expand Down Expand Up @@ -51,9 +52,9 @@ function extractSpaceDID (space) {
* @import * as Ucanto from '@ucanto/interface'
* @import { IpfsUrlContext, Middleware } from '@web3-storage/gateway-lib'
* @import { LocatorContext } from './withLocator.types.js'
* @import { AuthTokenContext } from './withAuthToken.types.js'
* @import { AuthTokenContext, Environment } from './withAuthToken.types.js'
* @import { SpaceContext } from './withAuthorizedSpace.types.js'
* @import { DelegationsStorageContext } from './withDelegationsStorage.types.js'
* @import { DelegationsStorageContext, DelegationsStorageEnvironment } from './withDelegationsStorage.types.js'
* @import { GatewayIdentityContext } from './withGatewayIdentity.types.js'
* @import { DelegationProofsContext } from './withAuthorizedSpace.types.js'
*/
Expand Down Expand Up @@ -95,6 +96,7 @@ export function withAuthorizedSpace (handler) {
ctx.authToken === null

if (shouldServeLegacy) {
console.log('[withAuthorizedSpace] Using legacy path (no space)')
return handler(request, env, ctx)
}

Expand All @@ -117,7 +119,8 @@ export function withAuthorizedSpace (handler) {
// First space to successfully authorize is the one we'll use.
const { space: selectedSpace, delegationProofs } = await Promise.any(
spaces.map(async (space) => {
const result = await authorize(SpaceDID.from(space.toString()), ctx)
// @ts-ignore
const result = await authorize(SpaceDID.from(space), ctx, env)
if (result.error) {
throw result.error
}
Expand Down Expand Up @@ -168,9 +171,10 @@ export function withAuthorizedSpace (handler) {
*
* @param {import('@storacha/capabilities/types').SpaceDID} space
* @param {AuthTokenContext & DelegationsStorageContext & GatewayIdentityContext} ctx
* @param {import('./withRateLimit.types.js').Environment} env
* @returns {Promise<Ucanto.Result<{space: import('@storacha/capabilities/types').SpaceDID, delegationProofs: Ucanto.Delegation[]}, Ucanto.Failure>>}
*/
const authorize = async (space, ctx) => {
const authorize = async (space, ctx, env) => {
// Look up delegations that might authorize us to serve the content.
const relevantDelegationsResult = await ctx.delegationsStorage.find(space)
if (relevantDelegationsResult.error) {
Expand All @@ -197,11 +201,14 @@ const authorize = async (space, ctx) => {
})
.delegate()

// Validate the invocation.
// Load validator proofs and validate the invocation
const validatorProofs = await getValidatorProofs(env)
const accessResult = await access(invocation, {
capability: serve.transportHttp,
authority: ctx.gatewayIdentity,
principal: Verifier,
proofs: validatorProofs,
resolveDIDKey,
validateAuthorization: () => ok({})
})

Expand Down
3 changes: 2 additions & 1 deletion src/middleware/withDelegationsStorage.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SpaceDID } from '@storacha/capabilities/types'
export interface DelegationsStorageEnvironment extends MiddlewareEnvironment {
CONTENT_SERVE_DELEGATIONS_STORE: KVNamespace
FF_DELEGATIONS_STORAGE_ENABLED: string
GATEWAY_VALIDATOR_PROOF?: string
}

export interface DelegationsStorageContext
Expand Down Expand Up @@ -35,6 +36,6 @@ export interface DelegationsStorage {
*/
store: (
space: SpaceDID,
delegation: Ucanto.Delegation<Ucanto.Capabilities>
delegation: Ucanto.Delegation<Ucanto.Capabilities>,
) => Promise<Ucanto.Result<Ucanto.Unit, StoreOperationFailed | Ucanto.Failure>>
}
6 changes: 5 additions & 1 deletion src/middleware/withGatewayIdentity.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export function withGatewayIdentity (handler) {
const gatewayIdentity = gatewaySigner.withDID(
Schema.DID.from(env.GATEWAY_SERVICE_DID)
)
return handler(req, env, { ...ctx, gatewaySigner, gatewayIdentity })
return handler(req, env, {
...ctx,
gatewaySigner,
gatewayIdentity
})
}
}
2 changes: 1 addition & 1 deletion src/middleware/withGatewayIdentity.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ export interface Environment extends MiddlewareEnvironment {
}

export interface GatewayIdentityContext extends MiddlewareContext {
gatewaySigner: EdSigner
gatewaySigner: Ucanto.Signer
gatewayIdentity: Ucanto.Signer
}
1 change: 1 addition & 0 deletions src/middleware/withRateLimit.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Environment extends MiddlewareEnvironment {
RATE_LIMITER: RateLimit
AUTH_TOKEN_METADATA: KVNamespace
FF_RATE_LIMITER_ENABLED: string
GATEWAY_VALIDATOR_PROOF?: string
}

export interface TokenMetadata {
Expand Down
5 changes: 3 additions & 2 deletions src/middleware/withUcanInvocationHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export function withUcanInvocationHandler (handler) {
return handler(request, env, ctx)
}

const service = ctx.service ?? createService(ctx)
const server = ctx.server ?? createServer(ctx, service)
const service =
ctx.service ?? (await createService(ctx, env))
const server = ctx.server ?? (await createServer(ctx, service, env))

const { headers, body, status } = await server.request({
body: new Uint8Array(await request.arrayBuffer()),
Expand Down
1 change: 1 addition & 0 deletions src/middleware/withUcanInvocationHandler.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DelegationsStorageContext } from './withDelegationsStorage.types.js'
import { Service } from '../server/api.types.js'
import * as Server from '@ucanto/server'
export interface Environment extends MiddlewareEnvironment {
GATEWAY_VALIDATOR_PROOF?: string
}

export interface Context<T = unknown, U = unknown>
Expand Down
Loading