Skip to content

Commit

Permalink
feat: Use Indexing Service when feature flag is present (#132)
Browse files Browse the repository at this point in the history
storacha/project-tracking#140

Depends on storacha/blob-fetcher#19 and
storacha/upload-service#57

When `ff=indexing-service` is given in the gateway URL, Freeway will use
the Indexing Service to locate blobs.
  • Loading branch information
Peeja authored Dec 3, 2024
1 parent c822465 commit fa3f480
Show file tree
Hide file tree
Showing 15 changed files with 2,235 additions and 1,115 deletions.
2,924 changes: 1,955 additions & 969 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@
"@microlabs/otel-cf-workers": "^1.0.0-rc.48",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-base": "^1.27.0",
"@storacha/indexing-service-client": "^2.0.0",
"@ucanto/client": "^9.0.1",
"@ucanto/principal": "^9.0.1",
"@ucanto/transport": "^9.1.1",
"@web3-storage/blob-fetcher": "^2.3.1",
"@web3-storage/blob-fetcher": "^2.4.3",
"@web3-storage/capabilities": "^17.4.1",
"@web3-storage/gateway-lib": "^5.1.2",
"dagula": "^8.0.0",
Expand All @@ -51,17 +52,18 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20231218.0",
"@storacha/cli": "^1.0.1",
"@storacha/client": "^1.0.5",
"@types/chai": "^5.0.0",
"@types/mocha": "^10.0.9",
"@types/node-fetch": "^2.6.11",
"@types/sinon": "^17.0.3",
"@web3-storage/content-claims": "^5.0.0",
"@web3-storage/public-bucket": "^1.1.0",
"@web3-storage/upload-client": "^16.1.1",
"@web3-storage/w3cli": "^7.8.2",
"carstream": "^2.1.0",
"chai": "^5.1.1",
"esbuild": "^0.18.20",
"esbuild": "^0.24.0",
"files-from-path": "^0.2.6",
"miniflare": "^3.20240909.5",
"mocha": "^10.7.3",
Expand All @@ -70,7 +72,7 @@
"standard": "^17.1.0",
"tree-kill": "^1.2.2",
"typescript": "^5.6.3",
"wrangler": "^3.86.1"
"wrangler": "^3.90.0"
},
"standard": {
"ignore": [
Expand Down
4 changes: 2 additions & 2 deletions scripts/delegate-serve.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare module '@web3-storage/w3cli/lib.js' {
import { Client } from '@web3-storage/w3up-client'
declare module '@storacha/cli/lib.js' {
import { Client } from '@storacha/client'
export declare function getClient(): Promise<Client>
}
176 changes: 120 additions & 56 deletions scripts/delegate-serve.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,137 @@
import sade from 'sade'
import { getClient } from '@web3-storage/w3cli/lib.js'
import { Schema } from '@ucanto/core'
import { getClient } from '@storacha/cli/lib.js'
import { Space } from '@web3-storage/capabilities'
import * as serve from '../src/capabilities/serve.js'

const cli = sade('delegate-serve.js [space] [token] [accountDID] [gatewayDID]')
const MailtoDID =
/** @type {import('@ucanto/validator').StringSchema<`did:mailto:${string}:${string}`, unknown>} */ (
Schema.text({ pattern: /^did:mailto:.*:.*$/ })
)

cli
.option('--space', 'The space DID to delegate. If not provided, a new space will be created.')
.option('--token', 'The auth token to use. If not provided, the delegation will not be authenticated.')
sade('delegate-serve.js [space]')
.option(
'--token',
'The auth token to use. If not provided, the delegation will not be authenticated.'
)
.option('--accountDID', 'The account DID to use when creating a new space.')
.option('--gatewayDID', 'The gateway DID to use when delegating the space/content/serve capability. Defaults to did:web:staging.w3s.link.')
.option(
'--gatewayDID',
'The gateway DID to use when delegating the space/content/serve capability. Defaults to did:web:staging.w3s.link.'
)
.describe(
`Delegates ${Space.contentServe.can} to the Gateway for a test space generated by the script, with an optional auth token. Outputs a base64url string suitable for the stub_delegation query parameter. Pipe the output to pbcopy or similar for the quickest workflow.`
`Delegates ${Space.contentServe.can} to the Gateway for a test space generated by the script, with an optional auth token. Outputs a base64url string suitable for the stub_delegation query parameter.`
)
.action(async (space, token, accountDID, gatewayDID, options) => {
const { space: spaceOption, token: tokenOption, accountDID: accountDIDOption, gatewayDID: gatewayDIDOption } = options
space = spaceOption || undefined
token = tokenOption || undefined
accountDID = accountDIDOption || undefined
gatewayDID = gatewayDIDOption || 'did:web:staging.w3s.link'
const client = await getClient()
.action(
/**
* @param {string} [space]
* @param {object} [options]
* @param {string} [options.token]
* @param {string} [options.accountDID]
* @param {string} [options.gatewayDID]
*/
async (
space,
{ token, accountDID, gatewayDID = 'did:web:staging.w3s.link' } = {}
) => {
const client = await getClient()

space ??= await createSpace(client, accountDID)

let spaceDID
let proofs = []
if (!space) {
const provider = /** @type {`did:web:${string}`} */ (client.defaultProvider())
const account = client.accounts()[accountDID]
const newSpace = await client.agent.createSpace('test')
const provision = await account.provision(newSpace.did(), { provider })
if (provision.error) throw provision.error
await newSpace.save()
const authProof = await newSpace.createAuthorization(client.agent)
proofs = [authProof]
spaceDID = newSpace.did()
} else {
client.addSpace(space)
spaceDID = space
proofs = client.proofs([
if (!Schema.did({}).is(space)) {
throw new Error(`Invalid space DID: ${space}`)
}

const proofs = client.proofs([
{
can: Space.contentServe.can,
with: spaceDID
with: space
}
])
}

/** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */
const gatewayIdentity = {
did: () => gatewayDID
}
if (proofs.length === 0) {
throw new Error(
`No proofs found. Are you authorized to ${serve.star.can} ${space}?`
)
}

if (!Schema.did({}).is(gatewayDID)) {
throw new Error(`Invalid gateway DID: ${gatewayDID}`)
}

// @ts-expect-error - The client still needs to be updated to support the capability type
const delegation = await client.createDelegation(gatewayIdentity, [Space.contentServe.can], {
expiration: Infinity,
proofs
})
const gatewayIdentity = {
did: () => gatewayDID
}

await client.capability.access.delegate({
delegations: [delegation]
})
// NOTE: This type assertion is wrong. It's a hack to let us use this
// ability. `client.createDelegation` currently only accepts abilities it
// knows about. That should probably be expanded, but this little script
// isn't going to be the reason to go change that, as it involves updating
// multiple packages.
const ability = /** @type {"*"} */ (Space.contentServe.can)

client.setCurrentSpace(space)
const delegation = await client.createDelegation(
gatewayIdentity,
[ability],
{
expiration: Infinity,
proofs
}
)

await client.capability.access.delegate({
delegations: [delegation]
})

const carResult = await delegation.archive()
if (carResult.error) throw carResult.error
const base64Url = Buffer.from(carResult.ok).toString('base64url')
process.stdout.write(
`Agent Proofs: ${proofs
.flatMap((p) => p.capabilities)
.map((c) => `${c.can} with ${c.with}`)
.join('\n')}\n`
)
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(
`Delegation: ${delegation.capabilities
.map((c) => `${c.can} with ${c.with}`)
.join('\n')}\n`
)
process.stdout.write(
`Stubs: stub_space=${space}&stub_delegation=${base64Url}&authToken=${
token ?? ''
}\n`
)
}
)
.parse(process.argv)

const carResult = await delegation.archive()
if (carResult.error) throw carResult.error
const base64Url = Buffer.from(carResult.ok).toString('base64url')
process.stdout.write(`Agent Proofs: ${proofs.flatMap(p => p.capabilities).map(c => `${c.can} with ${c.with}`).join('\n')}\n`)
process.stdout.write(`Issuer: ${client.agent.issuer.did()}\n`)
process.stdout.write(`Audience: ${gatewayIdentity.did()}\n`)
process.stdout.write(`Space: ${spaceDID}\n`)
process.stdout.write(`Token: ${token ?? 'none'}\n`)
process.stdout.write(`Delegation: ${delegation.capabilities.map(c => `${c.can} with ${c.with}`).join('\n')}\n`)
process.stdout.write(`Stubs: stub_space=${spaceDID}&stub_delegation=${base64Url}&authToken=${token ?? ''}\n`)
})
/**
* @param {import('@storacha/client').Client} client
* @param {string} [accountDID]
*/
async function createSpace (client, accountDID) {
const provider = client.defaultProvider()
if (!Schema.did({ method: 'web' }).is(provider)) {
throw new Error(`Invalid provider DID: ${provider}`)
}
if (!accountDID) {
throw new Error('Must provide an account DID to create a space')
}

cli.parse(process.argv)
if (!MailtoDID.is(accountDID)) {
throw new Error(`Invalid account DID: ${accountDID}`)
}
const account = client.accounts()[accountDID]
const newSpace = await client.agent.createSpace('test')
const provision = await account.provision(newSpace.did(), { provider })
if (provision.error) throw provision.error
await newSpace.save()
await newSpace.createAuthorization(client.agent)
return newSpace.did()
}
45 changes: 15 additions & 30 deletions src/middleware/withAuthorizedSpace.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import * as serve from '../capabilities/serve.js'

/**
* @import * as Ucanto from '@ucanto/interface'
* @import { Locator } from '@web3-storage/blob-fetcher'
* @import { IpfsUrlContext, Middleware } from '@web3-storage/gateway-lib'
* @import { LocatorContext } from './withLocator.types.js'
* @import { AuthTokenContext } from './withAuthToken.types.js'
* @import { SpaceContext, DelegationsStorageContext } from './withAuthorizedSpace.types.js'
* @import { SpaceContext, DelegationsStorageContext, DelegationProofsContext } from './withAuthorizedSpace.types.js'
* @import { GatewayIdentityContext } from './withGatewayIdentity.types.js'
*/

/**
Expand All @@ -21,9 +21,8 @@ import * as serve from '../capabilities/serve.js'
* @throws {Error} If the locator fails in any other way.
* @type {(
* Middleware<
* LocatorContext & IpfsUrlContext & AuthTokenContext & DelegationsStorageContext & SpaceContext,
* LocatorContext & IpfsUrlContext & AuthTokenContext & DelegationsStorageContext,
* {}
* LocatorContext & IpfsUrlContext & AuthTokenContext & GatewayIdentityContext & DelegationProofsContext & DelegationsStorageContext & SpaceContext,
* LocatorContext & IpfsUrlContext & AuthTokenContext & GatewayIdentityContext & DelegationProofsContext & DelegationsStorageContext
* >
* )}
*/
Expand Down Expand Up @@ -68,14 +67,23 @@ export function withAuthorizedSpace (handler) {
...ctx,
space: selectedSpace,
delegationProofs,
locator: spaceScopedLocator(locator, selectedSpace)
locator: locator.scopeToSpaces([selectedSpace])
})
} catch (error) {
// If all Spaces failed to authorize, throw the first error.
if (
error instanceof AggregateError &&
error.errors.every((e) => e instanceof Unauthorized)
) {
if (env.DEBUG === 'true') {
console.log(
[
'Authorization Failures:',
...error.errors.map((e) => e.message)
].join('\n\n')
)
}

throw new HttpError('Not Found', { status: 404, cause: error })
} else {
throw error
Expand All @@ -90,7 +98,7 @@ export function withAuthorizedSpace (handler) {
* {@link DelegationsStorageContext.delegationsStorage}.
*
* @param {Ucanto.DID} space
* @param {AuthTokenContext & DelegationsStorageContext} ctx
* @param {AuthTokenContext & DelegationsStorageContext & GatewayIdentityContext} ctx
* @returns {Promise<Ucanto.Result<{space: Ucanto.DID, delegationProofs: Ucanto.Delegation[]}, Ucanto.Failure>>}
*/
const authorize = async (space, ctx) => {
Expand Down Expand Up @@ -134,26 +142,3 @@ const authorize = async (space, ctx) => {
}
}
}

/**
* Wraps a {@link Locator} and locates content only from a specific Space.
*
* @param {Locator} locator
* @param {Ucanto.DID} space
* @returns {Locator}
*/
const spaceScopedLocator = (locator, space) => ({
locate: async (digest) => {
const locateResult = await locator.locate(digest)
if (locateResult.error) {
return locateResult
} else {
return {
ok: {
...locateResult.ok,
site: locateResult.ok.site.filter((site) => site.space === space)
}
}
}
}
})
8 changes: 4 additions & 4 deletions src/middleware/withAuthorizedSpace.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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,
GatewayIdentityContext {
export interface DelegationsStorageContext extends MiddlewareContext {
delegationsStorage: DelegationsStorage
}

export interface DelegationProofsContext extends MiddlewareContext {
/**
* The delegation proofs to use for the egress record
* The proofs must be valid for the space and the owner of the space
Expand Down
32 changes: 4 additions & 28 deletions src/middleware/withDelegationStubs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Delegation } from '@ucanto/core'
import { Delegation, Schema } from '@ucanto/core'

/**
* @import * as Ucanto from '@ucanto/interface'
* @import {
* Middleware,
* Context as MiddlewareContext
Expand All @@ -22,7 +21,7 @@ import { Delegation } from '@ucanto/core'
*
* @type {(
* Middleware<
* MiddlewareContext & LocatorContext & DelegationsStorageContext,
* MiddlewareContext & LocatorContext & GatewayIdentityContext & DelegationsStorageContext,
* MiddlewareContext & LocatorContext & GatewayIdentityContext,
* {}
* >
Expand Down Expand Up @@ -50,32 +49,9 @@ export const withDelegationStubs = (handler) => async (request, env, ctx) => {
return handler(request, env, {
...ctx,
delegationsStorage: { find: async () => ({ ok: stubDelegations }) },
delegationProofs: [], // Delegation proofs are set by withAuthorizedSpace handler
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
}))
}
}
}
}
stubSpace && Schema.did({ method: 'key' }).is(stubSpace)
? ctx.locator.scopeToSpaces([stubSpace])
: ctx.locator
})
}

/**
* True if the given string is a `key:` DID.
*
* @param {string} did
* @returns {did is Ucanto.DIDKey}
*/
const isDIDKey = (did) => did.startsWith('did:key:')
Loading

0 comments on commit fa3f480

Please sign in to comment.