From 0a98e8783b5b0bd6c81b940a6a8118f58a6a62be Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Tue, 26 Nov 2024 11:36:35 -0300 Subject: [PATCH 01/11] feat: authorize gateway --- packages/capabilities/src/types.ts | 24 +++++----- packages/w3up-client/src/client.js | 71 ++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index f3d48c6af..354665924 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -131,16 +131,6 @@ export type UsageReport = InferInvokedCapability export type UsageReportSuccess = Record export type UsageReportFailure = Ucanto.Failure -export type EgressRecord = InferInvokedCapability -export type EgressRecordSuccess = { - space: SpaceDID - resource: UnknownLink - bytes: number - servedAt: ISO8601Date - cause: UnknownLink -} -export type EgressRecordFailure = ConsumerNotFound | Ucanto.Failure - export interface UsageData { /** Provider the report concerns, e.g. `did:web:web3.storage` */ provider: ProviderDID @@ -284,6 +274,18 @@ export type RateLimitListFailure = Ucanto.Failure // Space export type Space = InferInvokedCapability export type SpaceInfo = InferInvokedCapability +export type SpaceContentServe = InferInvokedCapability< + typeof SpaceCaps.contentServe +> +export type EgressRecord = InferInvokedCapability +export type EgressRecordSuccess = { + space: SpaceDID + resource: UnknownLink + bytes: number + servedAt: ISO8601Date + cause: UnknownLink +} +export type EgressRecordFailure = ConsumerNotFound | Ucanto.Failure // filecoin export interface DealMetadata { @@ -895,6 +897,8 @@ export type ServiceAbilityArray = [ ProviderAdd['can'], Space['can'], SpaceInfo['can'], + SpaceContentServe['can'], + EgressRecord['can'], Upload['can'], UploadAdd['can'], UploadGet['can'], diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index aebdd9d23..956cf2732 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -9,6 +9,7 @@ import { Index as IndexCapabilities, Upload as UploadCapabilities, Filecoin as FilecoinCapabilities, + Space as SpaceCapabilities, } from '@web3-storage/capabilities' import * as DIDMailto from '@web3-storage/did-mailto' import { Base } from './base.js' @@ -253,6 +254,9 @@ export class Client extends Base { * * @typedef {object} CreateOptions * @property {Account.Account} [account] + * @property {boolean} [unauthorizeGateway] - If true, the gateway will not be authorized to serve content from the space. + * @property {`did:web:${string}`} [gateway] - The gateway to be authorized to serve content from the space. If not provided, the default did:web:w3s.link gateway will be authorized. + * @property {number} [gatewayExpiration] - The time in seconds to expire the gateway authorization. * * @param {string} name * @param {CreateOptions} options @@ -291,9 +295,76 @@ export class Client extends Base { ) } } + + if (!options.unauthorizeGateway) { + await this.authorizeGateway(space, { + gateway: options.gateway, + expiration: options.gatewayExpiration, + }) + } + return space } + /** + * Authorizes a gateway to serve content from the provided space and record egress events. + * Delegates the following capabilities to the gateway: + * - `space/content/serve/*` + * + * @param {import('./types.js').OwnedSpace} space - The space to authorize the gateway for. + * @param {object} [options] - Options for the authorization. + * @param {`did:web:${string}`} [options.gateway] - The Web DID of the gateway to authorize. If not provided, the default `did:web:w3s.link` gateway will be authorized. + * @param {number} [options.expiration] - The time in seconds to expire the authorization. + * @returns {Promise} + */ + async authorizeGateway(space, options = {}) { + const currentSpace = this.currentSpace() + try { + if (currentSpace && currentSpace.did() !== space.did()) { + // Set the current space to the space we are authorizing the gateway for + await this.setCurrentSpace(space.did()) + } + + // Authorize the agent to access the space + const authProof = await space.createAuthorization(this.agent) + + // Create a delegation for the Gateway to serve content from the space + const delegation = await this.createDelegation( + /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ + { + did: () => options.gateway ?? `did:web:w3s.link`, + }, + [SpaceCapabilities.contentServe.can], + { + expiration: options.expiration ?? Infinity, + proofs: [authProof], + } + ) + + // Authorize the gateway to serve content from the space + const result = await this.capability.access.delegate({ + delegations: [delegation], + }) + + if (result.error) { + throw new Error( + `failed to authorize gateway: ${result.error.message}`, + { + cause: result.error, + } + ) + } + + // TODO: save the delegation into the DelegationsStore + // delegation + } finally { + // Reset the current space to the original space + if (currentSpace && currentSpace.did() !== space.did()) { + await this.setCurrentSpace(currentSpace.did()) + } + } + } + /** * Share an existing space with another Storacha account via email address delegation. * Delegates access to the space to the specified email account with the following permissions: From d2735dc3c8bbaac931743b3f658baa7578193c8f Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Wed, 27 Nov 2024 09:39:52 -0300 Subject: [PATCH 02/11] fix tests --- packages/w3up-client/src/client.js | 37 +++++++--------------- packages/w3up-client/test/client.test.js | 40 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 956cf2732..f906cc434 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -254,9 +254,6 @@ export class Client extends Base { * * @typedef {object} CreateOptions * @property {Account.Account} [account] - * @property {boolean} [unauthorizeGateway] - If true, the gateway will not be authorized to serve content from the space. - * @property {`did:web:${string}`} [gateway] - The gateway to be authorized to serve content from the space. If not provided, the default did:web:w3s.link gateway will be authorized. - * @property {number} [gatewayExpiration] - The time in seconds to expire the gateway authorization. * * @param {string} name * @param {CreateOptions} options @@ -296,13 +293,6 @@ export class Client extends Base { } } - if (!options.unauthorizeGateway) { - await this.authorizeGateway(space, { - gateway: options.gateway, - expiration: options.gatewayExpiration, - }) - } - return space } @@ -315,25 +305,23 @@ export class Client extends Base { * @param {object} [options] - Options for the authorization. * @param {`did:web:${string}`} [options.gateway] - The Web DID of the gateway to authorize. If not provided, the default `did:web:w3s.link` gateway will be authorized. * @param {number} [options.expiration] - The time in seconds to expire the authorization. - * @returns {Promise} + * @returns {Promise>} Resolves with the AgentDelegation instance once the gateway is successfully authorized. */ async authorizeGateway(space, options = {}) { const currentSpace = this.currentSpace() try { - if (currentSpace && currentSpace.did() !== space.did()) { - // Set the current space to the space we are authorizing the gateway for - await this.setCurrentSpace(space.did()) - } + // Set the current space to the space we are authorizing the gateway for + await this.setCurrentSpace(space.did()) - // Authorize the agent to access the space const authProof = await space.createAuthorization(this.agent) - // Create a delegation for the Gateway to serve content from the space + /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ + const gateway = { + did: () => options.gateway ?? `did:web:w3s.link`, + } + const delegation = await this.createDelegation( - /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ - { - did: () => options.gateway ?? `did:web:w3s.link`, - }, + gateway, [SpaceCapabilities.contentServe.can], { expiration: options.expiration ?? Infinity, @@ -341,11 +329,11 @@ export class Client extends Base { } ) - // Authorize the gateway to serve content from the space const result = await this.capability.access.delegate({ delegations: [delegation], }) + /* c8 ignore next 8 - can't mock error */ if (result.error) { throw new Error( `failed to authorize gateway: ${result.error.message}`, @@ -356,10 +344,9 @@ export class Client extends Base { } // TODO: save the delegation into the DelegationsStore - // delegation + return delegation } finally { - // Reset the current space to the original space - if (currentSpace && currentSpace.did() !== space.did()) { + if (currentSpace) { await this.setCurrentSpace(currentSpace.did()) } } diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 7869aa2f0..df137dddf 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -15,6 +15,7 @@ import { receiptsEndpoint } from './helpers/utils.js' import { Absentee } from '@ucanto/principal' import { DIDMailto } from '../src/capability/access.js' import { confirmConfirmationUrl } from '../../upload-api/test/helpers/utils.js' +import * as SpaceCapability from '@web3-storage/capabilities/space' /** @type {Test.Suite} */ export const testClient = { @@ -528,6 +529,45 @@ export const testClient = { ) }, }), + authorizeGateway: Test.withContext({ + 'should authorize a gateway to serve content from a space': async ( + assert, + { client, mail, grantAccess } + ) => { + // Step 1: Create a client for Alice and login + const aliceEmail = 'alice@web.mail' + const aliceLogin = client.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Alice creates a space + const spaceA = await client.createSpace('authorize-gateway-space', { + account: aliceAccount, + }) + assert.ok(spaceA) + await client.setCurrentSpace(spaceA.did()) + + // Step 3: Authorize the gateway to serve content from the space + const delegation = await client.authorizeGateway(spaceA, { + gateway: 'did:web:staging.w3s.link', + expiration: Infinity, + }) + assert.ok(delegation) + + // Step 4: Find the delegation for the default gateway + assert.equal(delegation.audience.did(), 'did:web:staging.w3s.link') + assert.ok( + delegation.capabilities.some( + // @ts-expect-error + (c) => + c.can === SpaceCapability.contentServe.can && + c.with === spaceA.did() + ) + ) + }, + }), proofs: { 'should get proofs': async (assert) => { const alice = new Client(await AgentData.create()) From 52453702fb05f43afe22d789489488512ba895e4 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 28 Nov 2024 16:33:45 -0300 Subject: [PATCH 03/11] implemented reviewer suggestions --- packages/w3up-client/src/client.js | 53 ++++++++++++++---------- packages/w3up-client/src/types.ts | 13 ++++++ packages/w3up-client/test/client.test.js | 15 ++++--- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index f906cc434..2b1caad1e 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -5,6 +5,7 @@ import { Receipt, } from '@web3-storage/upload-client' import { + Access as AccessCapabilities, Blob as BlobCapabilities, Index as IndexCapabilities, Upload as UploadCapabilities, @@ -297,53 +298,61 @@ export class Client extends Base { } /** - * Authorizes a gateway to serve content from the provided space and record egress events. - * Delegates the following capabilities to the gateway: + * Authorizes an audience to serve content from the provided space and record egress events. + * It also publishes the delegation to the content serve service. + * Delegates the following capabilities to the audience: * - `space/content/serve/*` * - * @param {import('./types.js').OwnedSpace} space - The space to authorize the gateway for. - * @param {object} [options] - Options for the authorization. - * @param {`did:web:${string}`} [options.gateway] - The Web DID of the gateway to authorize. If not provided, the default `did:web:w3s.link` gateway will be authorized. - * @param {number} [options.expiration] - The time in seconds to expire the authorization. - * @returns {Promise>} Resolves with the AgentDelegation instance once the gateway is successfully authorized. + * @param {import('./types.js').OwnedSpace} space - The space to authorize the audience for. + * @param {object} options - Options for the authorization. + * @param {`did:${string}:${string}`} options.audience - The Web DID of the audience (gateway or peer) to authorize. + * @param {import('./types.js').ConnectionView} options.connection - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @param {number} [options.expiration] - The time at which the delegation expires in seconds from unix epoch. */ - async authorizeGateway(space, options = {}) { + async authorizeContentServe(space, options) { const currentSpace = this.currentSpace() try { // Set the current space to the space we are authorizing the gateway for await this.setCurrentSpace(space.did()) - const authProof = await space.createAuthorization(this.agent) - /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ - const gateway = { - did: () => options.gateway ?? `did:web:w3s.link`, + const audience = { + did: () => options.audience, } + // Create the delegation const delegation = await this.createDelegation( - gateway, + audience, [SpaceCapabilities.contentServe.can], { expiration: options.expiration ?? Infinity, - proofs: [authProof], } ) - const result = await this.capability.access.delegate({ - delegations: [delegation], - }) + // Publish the delegation to the content serve service + const verificationResult = await AccessCapabilities.delegate + .invoke({ + issuer: this._agent.issuer, + audience, + with: space.did(), + proofs: [delegation], + nb: { + delegations: { + [delegation.cid.toString()]: delegation.cid, + }, + }, + }) + .execute(options.connection) - /* c8 ignore next 8 - can't mock error */ - if (result.error) { + if (verificationResult.out.error) { throw new Error( - `failed to authorize gateway: ${result.error.message}`, + `failed to publish delegation for audience ${options.audience} to the content serve service: ${verificationResult.out.error.message}`, { - cause: result.error, + cause: verificationResult.out.error, } ) } - // TODO: save the delegation into the DelegationsStore return delegation } finally { if (currentSpace) { diff --git a/packages/w3up-client/src/types.ts b/packages/w3up-client/src/types.ts index 553c55e93..64882a515 100644 --- a/packages/w3up-client/src/types.ts +++ b/packages/w3up-client/src/types.ts @@ -1,5 +1,8 @@ import { type Driver } from '@web3-storage/access/drivers/types' import { + AccessDelegate, + AccessDelegateFailure, + AccessDelegateSuccess, type Service as AccessService, type AgentDataExport, } from '@web3-storage/access/types' @@ -11,6 +14,7 @@ import type { Ability, Resource, Unit, + ServiceMethod, } from '@ucanto/interface' import { type Client } from './client.js' import { StorefrontService } from '@web3-storage/filecoin-client/storefront' @@ -36,6 +40,15 @@ export interface ServiceConf { filecoin: ConnectionView } +export interface ContentServeService { + access: { + delegate: ServiceMethod< + AccessDelegate, + AccessDelegateSuccess, + AccessDelegateFailure + > + } +} export interface ClientFactoryOptions { /** * A storage driver that persists exported agent data. diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index df137dddf..6496b7dc7 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -14,7 +14,11 @@ import * as Test from './test.js' import { receiptsEndpoint } from './helpers/utils.js' import { Absentee } from '@ucanto/principal' import { DIDMailto } from '../src/capability/access.js' -import { confirmConfirmationUrl } from '../../upload-api/test/helpers/utils.js' +import { + confirmConfirmationUrl, + w3, + w3Signer, +} from '../../upload-api/test/helpers/utils.js' import * as SpaceCapability from '@web3-storage/capabilities/space' /** @type {Test.Suite} */ @@ -532,7 +536,7 @@ export const testClient = { authorizeGateway: Test.withContext({ 'should authorize a gateway to serve content from a space': async ( assert, - { client, mail, grantAccess } + { client, mail, grantAccess, connection } ) => { // Step 1: Create a client for Alice and login const aliceEmail = 'alice@web.mail' @@ -550,9 +554,9 @@ export const testClient = { await client.setCurrentSpace(spaceA.did()) // Step 3: Authorize the gateway to serve content from the space - const delegation = await client.authorizeGateway(spaceA, { - gateway: 'did:web:staging.w3s.link', - expiration: Infinity, + const delegation = await client.authorizeContentServe(spaceA, { + audience: w3.did(), + connection: connection, }) assert.ok(delegation) @@ -560,7 +564,6 @@ export const testClient = { assert.equal(delegation.audience.did(), 'did:web:staging.w3s.link') assert.ok( delegation.capabilities.some( - // @ts-expect-error (c) => c.can === SpaceCapability.contentServe.can && c.with === spaceA.did() From a9c36e92232ba73c7e0b9883ec863cdb9ec16913 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 28 Nov 2024 21:12:15 -0300 Subject: [PATCH 04/11] create space and authorize content serve flow --- packages/w3up-client/src/client.js | 45 ++++++++++++++++++++---- packages/w3up-client/test/client.test.js | 5 +-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 2b1caad1e..35025b632 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -30,6 +30,7 @@ import { FilecoinClient } from './capability/filecoin.js' import { CouponAPI } from './coupon.js' export * as Access from './capability/access.js' import * as Result from './result.js' +import { remove } from '@web3-storage/capabilities/store' export { AccessClient, @@ -252,15 +253,22 @@ export class Client extends Base { * If an account is not provided, the space is created without any delegation and is not saved, hence it is a temporary space. * When an account is provided in the options argument, then it creates a delegated recovery account * by provisioning the space, saving it and then delegating access to the recovery account. + * In addition, it authorizes the listed Content Serve Services to serve content from the created space. + * It is done by delegating the `space/content/serve/*` capability to the Content Serve Service. + * User can skip the Content Serve authorization by setting the `skipContentServeAuthorization` option to `true`. * - * @typedef {object} CreateOptions - * @property {Account.Account} [account] + * @typedef {object} SpaceCreateOptions + * @property {boolean} [skipContentServeAuthorization] - Whether to skip the Content Serve authorization. + * @property {`did:${string}:${string}`[]} [authorizeContentServeServices] - The DID Key or DID Web of the Content Serve Service to authorize to serve content from the created space. + * @property {import('./types.js').ConnectionView} [authorizeContentServeServices.connection] - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @property {Account.Account} [account] - The account configured as the recovery account for the space. + * @property {string} [name] - The name of the space to create. * - * @param {string} name - * @param {CreateOptions} options + * @param {string} name - The name of the space to create. + * @param {SpaceCreateOptions} options - Options for the space creation. * @returns {Promise} The created space owned by the agent. */ - async createSpace(name, options = {}) { + async createSpace(name, options) { const space = await this._agent.createSpace(name) const account = options.account @@ -294,6 +302,31 @@ export class Client extends Base { } } + // Authorize the listed Content Serve Services to serve content from the created space + if (options.skipContentServeAuthorization !== true) { + if ( + !options.authorizeContentServeServices || + options.authorizeContentServeServices.length === 0 + ) { + throw new Error( + 'failed to authorize Content Serve Services: missing option' + ) + } + + if (!options.connection) { + throw new Error( + 'failed to authorize Content Serve Services: missing option' + ) + } + + for (const service of options.authorizeContentServeServices) { + await this.authorizeContentServe(space, { + audience: service, + connection: options.connection, + }) + } + } + return space } @@ -353,7 +386,7 @@ export class Client extends Base { ) } - return delegation + return { ok: { ...verificationResult.out.ok, delegation } } } finally { if (currentSpace) { await this.setCurrentSpace(currentSpace.did()) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 6496b7dc7..a92b9d727 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -554,11 +554,12 @@ export const testClient = { await client.setCurrentSpace(spaceA.did()) // Step 3: Authorize the gateway to serve content from the space - const delegation = await client.authorizeContentServe(spaceA, { + const delegationResult = await client.authorizeContentServe(spaceA, { audience: w3.did(), connection: connection, }) - assert.ok(delegation) + assert.ok(delegationResult.ok) + const { delegation } = delegationResult.ok // Step 4: Find the delegation for the default gateway assert.equal(delegation.audience.did(), 'did:web:staging.w3s.link') From 022061ef555733b8c8ffa8aa72b6b85ee45e68dc Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 29 Nov 2024 09:02:44 -0300 Subject: [PATCH 05/11] fix broken tests due to changes in createSpace func --- packages/upload-api/test/helpers/utils.js | 1 + packages/w3up-client/src/client.js | 10 ++-- .../test/capability/access.test.js | 7 ++- .../w3up-client/test/capability/blob.test.js | 16 ++++-- .../w3up-client/test/capability/space.test.js | 24 ++++++-- .../w3up-client/test/capability/store.test.js | 16 ++++-- .../w3up-client/test/capability/usage.test.js | 8 ++- packages/w3up-client/test/client.test.js | 57 ++++++++++++++----- 8 files changed, 101 insertions(+), 38 deletions(-) diff --git a/packages/upload-api/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index 91e6c766e..d4c31c125 100644 --- a/packages/upload-api/test/helpers/utils.js +++ b/packages/upload-api/test/helpers/utils.js @@ -37,6 +37,7 @@ export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) +/** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */ export const w3Signer = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 35025b632..85053b1c3 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -258,7 +258,7 @@ export class Client extends Base { * User can skip the Content Serve authorization by setting the `skipContentServeAuthorization` option to `true`. * * @typedef {object} SpaceCreateOptions - * @property {boolean} [skipContentServeAuthorization] - Whether to skip the Content Serve authorization. + * @property {boolean} [skipContentServeAuthorization] - Whether to skip the Content Serve authorization. It means that the content of the space will not be served by any Content Serve Service. * @property {`did:${string}:${string}`[]} [authorizeContentServeServices] - The DID Key or DID Web of the Content Serve Service to authorize to serve content from the created space. * @property {import('./types.js').ConnectionView} [authorizeContentServeServices.connection] - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. * @property {Account.Account} [account] - The account configured as the recovery account for the space. @@ -271,6 +271,9 @@ export class Client extends Base { async createSpace(name, options) { const space = await this._agent.createSpace(name) + // Save the space to authorize the client to use the space + await space.save() + const account = options.account if (account) { // Provision the account with the space @@ -282,9 +285,6 @@ export class Client extends Base { ) } - // Save the space to authorize the client to use the space - await space.save() - // Create a recovery for the account const recovery = await space.createRecovery(account.did()) @@ -379,7 +379,7 @@ export class Client extends Base { if (verificationResult.out.error) { throw new Error( - `failed to publish delegation for audience ${options.audience} to the content serve service: ${verificationResult.out.error.message}`, + `failed to publish delegation for audience ${options.audience}: ${verificationResult.out.error.message}`, { cause: verificationResult.out.error, } diff --git a/packages/w3up-client/test/capability/access.test.js b/packages/w3up-client/test/capability/access.test.js index 61702a9ff..938bb05b7 100644 --- a/packages/w3up-client/test/capability/access.test.js +++ b/packages/w3up-client/test/capability/access.test.js @@ -20,7 +20,7 @@ export const AccessClient = Test.withContext({ }, 'should delegate and then claim': async ( assert, - { connection, provisionsStorage } + { id: w3, connection, provisionsStorage } ) => { const alice = new Client(await AgentData.create(), { // @ts-ignore @@ -29,7 +29,10 @@ export const AccessClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('upload-test') + + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/capability/blob.test.js b/packages/w3up-client/test/capability/blob.test.js index 209d21307..7e5f9481e 100644 --- a/packages/w3up-client/test/capability/blob.test.js +++ b/packages/w3up-client/test/capability/blob.test.js @@ -19,7 +19,9 @@ export const BlobClient = Test.withContext({ receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -56,7 +58,9 @@ export const BlobClient = Test.withContext({ receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -94,7 +98,9 @@ export const BlobClient = Test.withContext({ receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -126,7 +132,9 @@ export const BlobClient = Test.withContext({ receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index d25f447db..dd07ae227 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -19,7 +19,9 @@ export const SpaceClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice, { access: { 'space/info': {} }, expiration: Infinity, @@ -55,7 +57,9 @@ export const SpaceClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) @@ -165,7 +169,9 @@ export const SpaceClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) @@ -273,7 +279,9 @@ export const SpaceClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await alice.addSpace( await space.createAuthorization(alice) ) @@ -385,7 +393,9 @@ export const SpaceClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await alice.addSpace( await space.createAuthorization(alice) ) @@ -499,7 +509,9 @@ export const SpaceClient = Test.withContext({ upload: connection, }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) diff --git a/packages/w3up-client/test/capability/store.test.js b/packages/w3up-client/test/capability/store.test.js index 602a46602..c0ac10b3f 100644 --- a/packages/w3up-client/test/capability/store.test.js +++ b/packages/w3up-client/test/capability/store.test.js @@ -16,7 +16,9 @@ export const StoreClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -51,7 +53,9 @@ export const StoreClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -87,7 +91,9 @@ export const StoreClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -121,7 +127,9 @@ export const StoreClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index caadf5ede..d04b8fffb 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -18,7 +18,9 @@ export const UsageClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -60,7 +62,9 @@ export const UsageClient = Test.withContext({ }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index a92b9d727..73a3fc638 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -17,7 +17,6 @@ import { DIDMailto } from '../src/capability/access.js' import { confirmConfirmationUrl, w3, - w3Signer, } from '../../upload-api/test/helpers/utils.js' import * as SpaceCapability from '@web3-storage/capabilities/space' @@ -43,7 +42,9 @@ export const testClient = { receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('upload-test') + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -120,7 +121,9 @@ export const testClient = { receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('upload-dir-test') + const space = await alice.createSpace('upload-dir-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -165,7 +168,9 @@ export const testClient = { receiptsEndpoint: new URL(receiptsEndpoint), }) - const space = await alice.createSpace('car-space') + const space = await alice.createSpace('car-space', { + skipContentServeAuthorization: true, + }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -226,7 +231,9 @@ export const testClient = { const current0 = alice.currentSpace() assert.equal(current0, undefined) - const space = await alice.createSpace('new-space') + const space = await alice.createSpace('new-space', { + skipContentServeAuthorization: true, + }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -240,7 +247,9 @@ export const testClient = { const alice = new Client(await AgentData.create()) const name = `space-${Date.now()}` - const space = await alice.createSpace(name) + const space = await alice.createSpace(name, { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -254,7 +263,9 @@ export const testClient = { const alice = new Client(await AgentData.create()) const bob = new Client(await AgentData.create()) - const space = await alice.createSpace('new-space') + const space = await alice.createSpace('new-space', { + skipContentServeAuthorization: true, + }) await alice.addSpace( await space.createAuthorization(alice, { access: { '*': {} }, @@ -577,7 +588,9 @@ export const testClient = { const alice = new Client(await AgentData.create()) const bob = new Client(await AgentData.create()) - const space = await alice.createSpace('proof-space') + const space = await alice.createSpace('proof-space', { + skipContentServeAuthorization: true, + }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -595,7 +608,9 @@ export const testClient = { const alice = new Client(await AgentData.create()) const bob = new Client(await AgentData.create()) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) const name = `delegation-${Date.now()}` @@ -631,7 +646,9 @@ export const testClient = { }, }) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) await alice.addSpace( await space.createAuthorization(alice, { access: { '*': {} }, @@ -653,7 +670,9 @@ export const testClient = { const alice = new Client(await AgentData.create()) const bob = new Client(await AgentData.create()) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) const name = `delegation-${Date.now()}` @@ -704,7 +723,9 @@ export const testClient = { }) // setup space - const space = await alice.createSpace('upload-test') + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -753,7 +774,9 @@ export const testClient = { }) // setup space - const space = await alice.createSpace('upload-test') + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -805,7 +828,9 @@ export const testClient = { }) // setup space - const space = await alice.createSpace('upload-test') + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -829,7 +854,9 @@ export const testClient = { }) // setup space - const space = await alice.createSpace('upload-test') + const space = await alice.createSpace('upload-test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) From 64b08d1a2331e32b31b6ec9f314eb1261c7d8529 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 29 Nov 2024 10:43:41 -0300 Subject: [PATCH 06/11] fix authorizeContentServe test --- packages/filecoin-api/test/context/service.js | 11 +++++ packages/upload-api/test/helpers/utils.js | 1 + packages/w3up-client/src/client.js | 9 ++-- packages/w3up-client/test/client.test.js | 42 +++++++++++++----- packages/w3up-client/test/mocks/service.js | 43 +++++++++++++++++++ 5 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 packages/w3up-client/test/mocks/service.js diff --git a/packages/filecoin-api/test/context/service.js b/packages/filecoin-api/test/context/service.js index 37de7be45..44b0635dd 100644 --- a/packages/filecoin-api/test/context/service.js +++ b/packages/filecoin-api/test/context/service.js @@ -13,6 +13,7 @@ import * as API from '../../src/types.js' import { validateAuthorization } from '../utils.js' import { mockService } from './mocks.js' +import { Access } from '@web3-storage/capabilities' export { getStoreImplementations } from './store-implementations.js' export { getQueueImplementations } from './queue-implementations.js' @@ -240,6 +241,16 @@ export function getMockService() { } ), }, + access: { + delegate: Server.provide( + Access.delegate, + async ({ capability, invocation }) => { + return { + ok: {}, + } + } + ), + }, }) } diff --git a/packages/upload-api/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index d4c31c125..e95be29e6 100644 --- a/packages/upload-api/test/helpers/utils.js +++ b/packages/upload-api/test/helpers/utils.js @@ -43,6 +43,7 @@ export const w3Signer = ed25519.parse( ) export const w3 = w3Signer.withDID('did:web:test.web3.storage') +/** did:key:z6MkuKJgV8DKxiAF5oaUcT8ckg8kZUoBe6yavSLnHn5ZgyAP */ export const gatewaySigner = ed25519.parse( 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' ) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 85053b1c3..e5e0e43b5 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -345,7 +345,7 @@ export class Client extends Base { async authorizeContentServe(space, options) { const currentSpace = this.currentSpace() try { - // Set the current space to the space we are authorizing the gateway for + // Set the current space to the space we are authorizing the gateway for, otherwise the delegation will fail await this.setCurrentSpace(space.did()) /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ @@ -353,7 +353,7 @@ export class Client extends Base { did: () => options.audience, } - // Create the delegation + // Grant the audience the ability to serve content from the space, it includes existing proofs automatically const delegation = await this.createDelegation( audience, [SpaceCapabilities.contentServe.can], @@ -363,12 +363,15 @@ export class Client extends Base { ) // Publish the delegation to the content serve service + const accessProofs = this.proofs([ + { can: AccessCapabilities.access.can, with: space.did() }, + ]) const verificationResult = await AccessCapabilities.delegate .invoke({ issuer: this._agent.issuer, audience, with: space.did(), - proofs: [delegation], + proofs: [...accessProofs, delegation], nb: { delegations: { [delegation.cid.toString()]: delegation.cid, diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 73a3fc638..56c184080 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -15,10 +15,13 @@ import { receiptsEndpoint } from './helpers/utils.js' import { Absentee } from '@ucanto/principal' import { DIDMailto } from '../src/capability/access.js' import { + alice, confirmConfirmationUrl, - w3, + gateway, + gatewaySigner, } from '../../upload-api/test/helpers/utils.js' import * as SpaceCapability from '@web3-storage/capabilities/space' +import { getConnection, getMockService } from './mocks/service.js' /** @type {Test.Suite} */ export const testClient = { @@ -547,33 +550,52 @@ export const testClient = { authorizeGateway: Test.withContext({ 'should authorize a gateway to serve content from a space': async ( assert, - { client, mail, grantAccess, connection } + { mail, grantAccess, connection } ) => { // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + const aliceEmail = 'alice@web.mail' - const aliceLogin = client.login(aliceEmail) + const aliceLogin = aliceClient.login(aliceEmail) const message = await mail.take() assert.deepEqual(message.to, aliceEmail) await grantAccess(message) const aliceAccount = await aliceLogin // Step 2: Alice creates a space - const spaceA = await client.createSpace('authorize-gateway-space', { + const spaceA = await aliceClient.createSpace('authorize-gateway-space', { account: aliceAccount, + skipContentServeAuthorization: true, }) assert.ok(spaceA) - await client.setCurrentSpace(spaceA.did()) - // Step 3: Authorize the gateway to serve content from the space - const delegationResult = await client.authorizeContentServe(spaceA, { - audience: w3.did(), - connection: connection, + const gatewayService = getMockService() + const gatewayConnection = getConnection( + gateway, + gatewayService + ).connection + + // Step 3: Alice authorizes the gateway to serve content from the space + const delegationResult = await aliceClient.authorizeContentServe(spaceA, { + audience: gateway.did(), + connection: gatewayConnection, }) assert.ok(delegationResult.ok) const { delegation } = delegationResult.ok // Step 4: Find the delegation for the default gateway - assert.equal(delegation.audience.did(), 'did:web:staging.w3s.link') + assert.equal(delegation.audience.did(), gateway.did()) assert.ok( delegation.capabilities.some( (c) => diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js new file mode 100644 index 000000000..761e0666e --- /dev/null +++ b/packages/w3up-client/test/mocks/service.js @@ -0,0 +1,43 @@ +import * as Client from '@ucanto/client' +import * as Server from '@ucanto/server' +import * as CAR from '@ucanto/transport/car' + +import * as AccessCaps from '@web3-storage/capabilities' + +/** + * Mocked Gateway/Content Serve service + */ +export function getMockService() { + return { + access: { + delegate: Server.provide( + AccessCaps.Access.delegate, + async ({ capability, invocation }) => { + return { + ok: {}, + } + } + ), + }, + } +} + +/** + * @param {any} service + * @param {any} id + */ +export function getConnection(id, service) { + const server = Server.create({ + id: id, + service, + codec: CAR.inbound, + validateAuthorization: () => ({ ok: {} }), + }) + const connection = Client.connect({ + id: id, + codec: CAR.outbound, + channel: server, + }) + + return { connection } +} From 7a395a9eec4a98d2e1b8936ff4c7c334612a52fd Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 29 Nov 2024 13:36:29 -0300 Subject: [PATCH 07/11] fix and add tests --- packages/filecoin-api/test/context/service.js | 11 - packages/w3up-client/src/client.js | 12 +- packages/w3up-client/test/account.test.js | 22 +- .../test/capability/filecoin.test.js | 8 +- .../w3up-client/test/capability/index.test.js | 4 +- .../test/capability/subscription.test.js | 4 +- .../test/capability/upload.test.js | 16 +- packages/w3up-client/test/client.test.js | 301 ++++++++++++++---- packages/w3up-client/test/coupon.test.js | 4 +- packages/w3up-client/test/mocks/service.js | 17 +- packages/w3up-client/test/space.test.js | 4 +- 11 files changed, 306 insertions(+), 97 deletions(-) diff --git a/packages/filecoin-api/test/context/service.js b/packages/filecoin-api/test/context/service.js index 44b0635dd..37de7be45 100644 --- a/packages/filecoin-api/test/context/service.js +++ b/packages/filecoin-api/test/context/service.js @@ -13,7 +13,6 @@ import * as API from '../../src/types.js' import { validateAuthorization } from '../utils.js' import { mockService } from './mocks.js' -import { Access } from '@web3-storage/capabilities' export { getStoreImplementations } from './store-implementations.js' export { getQueueImplementations } from './queue-implementations.js' @@ -241,16 +240,6 @@ export function getMockService() { } ), }, - access: { - delegate: Server.provide( - Access.delegate, - async ({ capability, invocation }) => { - return { - ok: {}, - } - } - ), - }, }) } diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index e5e0e43b5..982a4ece2 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -30,7 +30,6 @@ import { FilecoinClient } from './capability/filecoin.js' import { CouponAPI } from './coupon.js' export * as Access from './capability/access.js' import * as Result from './result.js' -import { remove } from '@web3-storage/capabilities/store' export { AccessClient, @@ -260,7 +259,7 @@ export class Client extends Base { * @typedef {object} SpaceCreateOptions * @property {boolean} [skipContentServeAuthorization] - Whether to skip the Content Serve authorization. It means that the content of the space will not be served by any Content Serve Service. * @property {`did:${string}:${string}`[]} [authorizeContentServeServices] - The DID Key or DID Web of the Content Serve Service to authorize to serve content from the created space. - * @property {import('./types.js').ConnectionView} [authorizeContentServeServices.connection] - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @property {import('./types.js').ConnectionView} [connection] - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. * @property {Account.Account} [account] - The account configured as the recovery account for the space. * @property {string} [name] - The name of the space to create. * @@ -269,10 +268,8 @@ export class Client extends Base { * @returns {Promise} The created space owned by the agent. */ async createSpace(name, options) { - const space = await this._agent.createSpace(name) - // Save the space to authorize the client to use the space - await space.save() + const space = await this._agent.createSpace(name) const account = options.account if (account) { @@ -285,6 +282,9 @@ export class Client extends Base { ) } + // Save the space to authorize the client to use the space + await space.save() + // Create a recovery for the account const recovery = await space.createRecovery(account.did()) @@ -380,6 +380,7 @@ export class Client extends Base { }) .execute(options.connection) + /* c8 ignore next 8 - can't mock this error */ if (verificationResult.out.error) { throw new Error( `failed to publish delegation for audience ${options.audience}: ${verificationResult.out.error.message}`, @@ -388,7 +389,6 @@ export class Client extends Base { } ) } - return { ok: { ...verificationResult.out.ok, delegation } } } finally { if (currentSpace) { diff --git a/packages/w3up-client/test/account.test.js b/packages/w3up-client/test/account.test.js index 573907b6c..0278c527b 100644 --- a/packages/w3up-client/test/account.test.js +++ b/packages/w3up-client/test/account.test.js @@ -111,7 +111,9 @@ export const testAccount = Test.withContext({ assert, { client, mail, grantAccess } ) => { - const space = await client.createSpace('test') + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) const mnemonic = space.toMnemonic() const { signer } = await Space.fromMnemonic(mnemonic, { name: 'import' }) assert.deepEqual( @@ -147,7 +149,9 @@ export const testAccount = Test.withContext({ 'multi device workflow': async (asserts, { connect, mail, grantAccess }) => { const laptop = await connect() - const space = await laptop.createSpace('main') + const space = await laptop.createSpace('main', { + skipContentServeAuthorization: true, + }) // want to provision space ? const email = 'alice@web.mail' @@ -183,7 +187,9 @@ export const testAccount = Test.withContext({ asserts.deepEqual(result.did, space.did()) }, 'setup recovery': async (assert, { client, mail, grantAccess }) => { - const space = await client.createSpace('test') + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) const email = 'alice@web.mail' const login = Account.login(client, email) @@ -280,7 +286,9 @@ export const testAccount = Test.withContext({ assert, { client, mail, grantAccess } ) => { - const space = await client.createSpace('test') + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) const email = 'alice@web.mail' const login = Account.login(client, email) @@ -299,8 +307,10 @@ export const testAccount = Test.withContext({ assert.equal(typeof subs.results[0].subscription, 'string') }, - 'space.save': async (assert, { client, mail, grantAccess }) => { - const space = await client.createSpace('test') + 'space.save': async (assert, { client }) => { + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) assert.deepEqual(client.spaces(), []) const result = await space.save() diff --git a/packages/w3up-client/test/capability/filecoin.test.js b/packages/w3up-client/test/capability/filecoin.test.js index f2640bece..c8795171a 100644 --- a/packages/w3up-client/test/capability/filecoin.test.js +++ b/packages/w3up-client/test/capability/filecoin.test.js @@ -5,7 +5,9 @@ import * as Test from '../test.js' export const FilecoinClient = Test.withContext({ offer: { 'should send an offer': async (assert, { client: alice }) => { - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -36,7 +38,9 @@ export const FilecoinClient = Test.withContext({ throw new Error('could not compute proof') } - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/capability/index.test.js b/packages/w3up-client/test/capability/index.test.js index 40196272c..3dbd4f4bf 100644 --- a/packages/w3up-client/test/capability/index.test.js +++ b/packages/w3up-client/test/capability/index.test.js @@ -14,7 +14,9 @@ export const IndexClient = Test.withContext({ ) => { const car = await randomCAR(128) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/capability/subscription.test.js b/packages/w3up-client/test/capability/subscription.test.js index c4d036712..67fa48277 100644 --- a/packages/w3up-client/test/capability/subscription.test.js +++ b/packages/w3up-client/test/capability/subscription.test.js @@ -8,7 +8,9 @@ export const SubscriptionClient = Test.withContext({ assert, { client, connection, service, plansStorage, grantAccess, mail } ) => { - const space = await client.createSpace('test') + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) const email = 'alice@web.mail' const login = Account.login(client, email) const message = await mail.take() diff --git a/packages/w3up-client/test/capability/upload.test.js b/packages/w3up-client/test/capability/upload.test.js index 573221908..efbf5252e 100644 --- a/packages/w3up-client/test/capability/upload.test.js +++ b/packages/w3up-client/test/capability/upload.test.js @@ -9,7 +9,9 @@ export const UploadClient = Test.withContext({ ) => { const car = await randomCAR(128) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -38,7 +40,9 @@ export const UploadClient = Test.withContext({ ) => { const car = await randomCAR(128) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -77,7 +81,9 @@ export const UploadClient = Test.withContext({ ) => { const car = await randomCAR(128) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) @@ -109,7 +115,9 @@ export const UploadClient = Test.withContext({ ) => { const car = await randomCAR(128) - const space = await alice.createSpace('test') + const space = await alice.createSpace('test', { + skipContentServeAuthorization: true, + }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) await alice.setCurrentSpace(space.did()) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 56c184080..42cee03e4 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -1,5 +1,6 @@ import assert from 'assert' import { parseLink } from '@ucanto/server' +import * as Server from '@ucanto/server' import { Agent, AgentData, @@ -18,10 +19,9 @@ import { alice, confirmConfirmationUrl, gateway, - gatewaySigner, } from '../../upload-api/test/helpers/utils.js' import * as SpaceCapability from '@web3-storage/capabilities/space' -import { getConnection, getMockService } from './mocks/service.js' +import { getConnection, getContentServeMockService } from './mocks/service.js' /** @type {Test.Suite} */ export const testClient = { @@ -303,6 +303,7 @@ export const testClient = { // Step 2: Alice creates a space with her account as the recovery account const space = await client.createSpace('recovery-space-test', { account: aliceAccount, // The account is the recovery account + skipContentServeAuthorization: true, }) assert.ok(space) @@ -334,7 +335,9 @@ export const testClient = { await aliceLogin // Step 2: Alice creates a space without providing a recovery account - const space = await client.createSpace('no-recovery-space-test') + const space = await client.createSpace('no-recovery-space-test', { + skipContentServeAuthorization: true, + }) assert.ok(space) // Step 3: Attempt to access the space from a new device @@ -437,6 +440,7 @@ export const testClient = { // Step 2: Alice creates a space const space = await aliceClient.createSpace('share-space-test', { account: aliceAccount, + skipContentServeAuthorization: true, }) assert.ok(space) @@ -489,6 +493,7 @@ export const testClient = { 'share-space-delegate-fail-test', { account: aliceAccount, + skipContentServeAuthorization: true, } ) assert.ok(space) @@ -525,12 +530,14 @@ export const testClient = { // Step 2: Alice creates a space const spaceA = await client.createSpace('test-space-a', { account: aliceAccount, + skipContentServeAuthorization: true, }) assert.ok(spaceA) // Step 3: Alice creates another space to share with a friend const spaceB = await client.createSpace('test-space-b', { account: aliceAccount, + skipContentServeAuthorization: true, }) assert.ok(spaceB) @@ -548,62 +555,248 @@ export const testClient = { }, }), authorizeGateway: Test.withContext({ - 'should authorize a gateway to serve content from a space': async ( - assert, - { mail, grantAccess, connection } - ) => { - // Step 1: Create a client for Alice and login - const aliceClient = new Client( - await AgentData.create({ - principal: alice, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, + 'should explicitly authorize a gateway to serve content from a space': + async (assert, { mail, grantAccess, connection }) => { + // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + const aliceEmail = 'alice@web.mail' + const aliceLogin = aliceClient.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Alice creates a space + const spaceA = await aliceClient.createSpace( + 'authorize-gateway-space', + { + account: aliceAccount, + skipContentServeAuthorization: true, + } + ) + assert.ok(spaceA) + + const gatewayService = getContentServeMockService() + const gatewayConnection = getConnection( + gateway, + gatewayService + ).connection + + // Step 3: Alice authorizes the gateway to serve content from the space + const delegationResult = await aliceClient.authorizeContentServe( + spaceA, + { + audience: gateway.did(), + connection: gatewayConnection, + } + ) + assert.ok(delegationResult.ok) + const { delegation } = delegationResult.ok + + // Step 4: Find the delegation for the default gateway + assert.equal(delegation.audience.did(), gateway.did()) + assert.ok( + delegation.capabilities.some( + (c) => + c.can === SpaceCapability.contentServe.can && + c.with === spaceA.did() + ) + ) + }, + 'should automatically authorize a gateway to serve content from a space when the space is created': + async (assert, { mail, grantAccess, connection }) => { + // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + const aliceEmail = 'alice@web.mail' + const aliceLogin = aliceClient.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Alice creates a space + const gatewayService = getContentServeMockService() + const gatewayConnection = getConnection( + gateway, + gatewayService + ).connection + + try { + const spaceA = await aliceClient.createSpace( + 'authorize-gateway-space', + { + account: aliceAccount, + authorizeContentServeServices: [gateway.did()], + connection: gatewayConnection, + } + ) + assert.ok(spaceA, 'should create the space') + } catch (error) { + assert.fail(error, 'should not throw when creating the space') } - ) + }, + 'should throw when the content serve authorization fails due to missing connection configuration': + async (assert, { mail, grantAccess, connection }) => { + // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) - const aliceEmail = 'alice@web.mail' - const aliceLogin = aliceClient.login(aliceEmail) - const message = await mail.take() - assert.deepEqual(message.to, aliceEmail) - await grantAccess(message) - const aliceAccount = await aliceLogin + const aliceEmail = 'alice@web.mail' + const aliceLogin = aliceClient.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin - // Step 2: Alice creates a space - const spaceA = await aliceClient.createSpace('authorize-gateway-space', { - account: aliceAccount, - skipContentServeAuthorization: true, - }) - assert.ok(spaceA) + try { + const spaceA = await aliceClient.createSpace( + 'authorize-gateway-space', + { + account: aliceAccount, + authorizeContentServeServices: [gateway.did()], + } + ) + assert.fail(spaceA, 'should not create the space') + } catch (error) { + assert.match( + // @ts-expect-error + error.message, + /missing option/, + 'should throw when creating the space' + ) + } + }, + 'should throw when the content serve authorization fails due to missing service configuration': + async (assert, { mail, grantAccess, connection }) => { + // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) - const gatewayService = getMockService() - const gatewayConnection = getConnection( - gateway, - gatewayService - ).connection - - // Step 3: Alice authorizes the gateway to serve content from the space - const delegationResult = await aliceClient.authorizeContentServe(spaceA, { - audience: gateway.did(), - connection: gatewayConnection, - }) - assert.ok(delegationResult.ok) - const { delegation } = delegationResult.ok - - // Step 4: Find the delegation for the default gateway - assert.equal(delegation.audience.did(), gateway.did()) - assert.ok( - delegation.capabilities.some( - (c) => - c.can === SpaceCapability.contentServe.can && - c.with === spaceA.did() + const aliceEmail = 'alice@web.mail' + const aliceLogin = aliceClient.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + const gatewayService = getContentServeMockService() + const gatewayConnection = getConnection( + gateway, + gatewayService + ).connection + try { + const spaceA = await aliceClient.createSpace( + 'authorize-gateway-space', + { + account: aliceAccount, + authorizeContentServeServices: [], // No services to authorize + connection: gatewayConnection, + } + ) + assert.fail(spaceA, 'should not create the space') + } catch (error) { + assert.match( + // @ts-expect-error + error.message, + /missing option/, + 'should throw when creating the space' + ) + } + }, + 'should throw when content serve service can not process the invocation': + async (assert, { mail, grantAccess, connection }) => { + // Step 1: Create a client for Alice and login + const aliceClient = new Client( + await AgentData.create({ + principal: alice, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } ) - ) - }, + + const aliceEmail = 'alice@web.mail' + const aliceLogin = aliceClient.login(aliceEmail) + const message = await mail.take() + assert.deepEqual(message.to, aliceEmail) + await grantAccess(message) + const aliceAccount = await aliceLogin + + // Step 2: Alice creates a space + const gatewayService = getContentServeMockService({ + error: Server.fail( + 'Content serve service can not process the invocation' + ).error, + }) + const gatewayConnection = getConnection( + gateway, + gatewayService + ).connection + + try { + await aliceClient.createSpace('authorize-gateway-space', { + account: aliceAccount, + authorizeContentServeServices: [gateway.did()], + connection: gatewayConnection, + }) + assert.fail('should not create the space') + } catch (error) { + assert.match( + // @ts-expect-error + error.message, + /failed to publish delegation for audience/, + 'should throw when publishing the delegation' + ) + } + }, }), proofs: { 'should get proofs': async (assert) => { diff --git a/packages/w3up-client/test/coupon.test.js b/packages/w3up-client/test/coupon.test.js index faee5f227..c44aa7fce 100644 --- a/packages/w3up-client/test/coupon.test.js +++ b/packages/w3up-client/test/coupon.test.js @@ -37,7 +37,9 @@ export const testCoupon = Test.withContext({ const access = await alice.coupon.redeem(archive) // creates a space and provision it with redeemed coupon - const space = await alice.createSpace('home') + const space = await alice.createSpace('home', { + skipContentServeAuthorization: true, + }) const result = await space.provision(access) await space.save() diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js index 761e0666e..3e86a3266 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -1,28 +1,25 @@ import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' import * as CAR from '@ucanto/transport/car' - import * as AccessCaps from '@web3-storage/capabilities' /** * Mocked Gateway/Content Serve service + * @param {{ ok: any } | { error: Server.API.Failure }} result */ -export function getMockService() { +export function getContentServeMockService(result = { ok: {} }) { return { access: { - delegate: Server.provide( - AccessCaps.Access.delegate, - async ({ capability, invocation }) => { - return { - ok: {}, - } - } - ), + delegate: Server.provide(AccessCaps.Access.delegate, async () => { + return result + }), }, } } /** + * Generic function to create connection to any type of mock service with any type of signer id. + * * @param {any} service * @param {any} id */ diff --git a/packages/w3up-client/test/space.test.js b/packages/w3up-client/test/space.test.js index a6a0d1281..838b534ec 100644 --- a/packages/w3up-client/test/space.test.js +++ b/packages/w3up-client/test/space.test.js @@ -24,7 +24,9 @@ export const testSpace = Test.withContext({ }, 'should get usage': async (assert, { client, grantAccess, mail }) => { - const space = await client.createSpace('test') + const space = await client.createSpace('test', { + skipContentServeAuthorization: true, + }) const email = 'alice@web.mail' const login = Account.login(client, email) From 988ea2d0674a90445e16392dac0e0a803fed073a Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 29 Nov 2024 13:50:01 -0300 Subject: [PATCH 08/11] lint fix --- packages/filecoin-api/src/aggregator/events.js | 5 ++++- packages/upload-api/src/access/claim.js | 2 ++ packages/upload-api/src/lib.js | 2 +- packages/w3up-client/test/mocks/service.js | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/filecoin-api/src/aggregator/events.js b/packages/filecoin-api/src/aggregator/events.js index 11e054731..7c2f82a53 100644 --- a/packages/filecoin-api/src/aggregator/events.js +++ b/packages/filecoin-api/src/aggregator/events.js @@ -229,7 +229,10 @@ export const handleAggregateInsertToPieceAcceptQueue = async ( // TODO: Batch per a maximum to queue const results = await map( pieces, - /** @returns {Promise>} */ + /** + * @param piece + * @returns {Promise>} + */ async piece => { const inclusionProof = aggregateBuilder.resolveProof(piece.link) if (inclusionProof.error) return inclusionProof diff --git a/packages/upload-api/src/access/claim.js b/packages/upload-api/src/access/claim.js index 6f5e2d513..ebd936630 100644 --- a/packages/upload-api/src/access/claim.js +++ b/packages/upload-api/src/access/claim.js @@ -13,6 +13,7 @@ export const provide = (ctx) => /** * Checks if the given Principal is an Account. + * * @param {API.Principal} principal * @returns {principal is API.Principal>} */ @@ -20,6 +21,7 @@ const isAccount = (principal) => principal.did().startsWith('did:mailto:') /** * Returns true when the delegation has a `ucan:*` capability. + * * @param {API.Delegation} delegation * @returns boolean */ diff --git a/packages/upload-api/src/lib.js b/packages/upload-api/src/lib.js index 1d866b6ab..efebc517a 100644 --- a/packages/upload-api/src/lib.js +++ b/packages/upload-api/src/lib.js @@ -147,8 +147,8 @@ export const execute = async (agent, input) => { * a receipt it will return receipt without running invocation. * * @template {Record} S - * @param {Types.Invocation} invocation * @param {Agent} agent + * @param {Types.Invocation} invocation */ export const run = async (agent, invocation) => { const cached = await agent.context.agentStore.receipts.get(invocation.link()) diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js index 3e86a3266..62ee0c37d 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -5,6 +5,7 @@ import * as AccessCaps from '@web3-storage/capabilities' /** * Mocked Gateway/Content Serve service + * * @param {{ ok: any } | { error: Server.API.Failure }} result */ export function getContentServeMockService(result = { ok: {} }) { @@ -20,8 +21,8 @@ export function getContentServeMockService(result = { ok: {} }) { /** * Generic function to create connection to any type of mock service with any type of signer id. * - * @param {any} service * @param {any} id + * @param {any} service */ export function getConnection(id, service) { const server = Server.create({ From 5f414b0eb2847c124802278c1af0c307d0ca8072 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 2 Dec 2024 10:46:17 -0300 Subject: [PATCH 09/11] reviewer suggestions implemented --- packages/w3up-client/src/client.js | 59 +++++----- packages/w3up-client/test/account.test.js | 10 +- .../test/capability/access.test.js | 2 +- .../w3up-client/test/capability/blob.test.js | 8 +- .../test/capability/filecoin.test.js | 4 +- .../w3up-client/test/capability/index.test.js | 2 +- .../w3up-client/test/capability/space.test.js | 12 +- .../w3up-client/test/capability/store.test.js | 8 +- .../test/capability/subscription.test.js | 2 +- .../test/capability/upload.test.js | 8 +- .../w3up-client/test/capability/usage.test.js | 4 +- packages/w3up-client/test/client.test.js | 104 +++++------------- packages/w3up-client/test/coupon.test.js | 2 +- packages/w3up-client/test/space.test.js | 2 +- 14 files changed, 83 insertions(+), 144 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index 982a4ece2..b331449a3 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -248,20 +248,20 @@ export class Client extends Base { } /** - * Create a new space with a given name. + * Creates a new space with a given name. * If an account is not provided, the space is created without any delegation and is not saved, hence it is a temporary space. * When an account is provided in the options argument, then it creates a delegated recovery account * by provisioning the space, saving it and then delegating access to the recovery account. - * In addition, it authorizes the listed Content Serve Services to serve content from the created space. - * It is done by delegating the `space/content/serve/*` capability to the Content Serve Service. - * User can skip the Content Serve authorization by setting the `skipContentServeAuthorization` option to `true`. + * In addition, it authorizes the listed Gateway Services to serve content from the created space. + * It is done by delegating the `space/content/serve/*` capability to the Gateway Service. + * User can skip the Gateway authorization by setting the `skipGatewayAuthorization` option to `true`. + * + * @typedef {import('./types.js').ConnectionView} ConnectionView * * @typedef {object} SpaceCreateOptions - * @property {boolean} [skipContentServeAuthorization] - Whether to skip the Content Serve authorization. It means that the content of the space will not be served by any Content Serve Service. - * @property {`did:${string}:${string}`[]} [authorizeContentServeServices] - The DID Key or DID Web of the Content Serve Service to authorize to serve content from the created space. - * @property {import('./types.js').ConnectionView} [connection] - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. * @property {Account.Account} [account] - The account configured as the recovery account for the space. - * @property {string} [name] - The name of the space to create. + * @property {Array} [authorizeGatewayServices] - The DID Key or DID Web of the Gateway to authorize to serve content from the created space. + * @property {boolean} [skipGatewayAuthorization] - Whether to skip the Gateway authorization. It means that the content of the space will not be served by any Gateway. * * @param {string} name - The name of the space to create. * @param {SpaceCreateOptions} options - Options for the space creation. @@ -289,41 +289,32 @@ export class Client extends Base { const recovery = await space.createRecovery(account.did()) // Delegate space access to the recovery - const result = await this.capability.access.delegate({ + const delegationResult = await this.capability.access.delegate({ space: space.did(), delegations: [recovery], }) - if (result.error) { + if (delegationResult.error) { throw new Error( - `failed to authorize recovery account: ${result.error.message}`, - { cause: result.error } + `failed to authorize recovery account: ${delegationResult.error.message}`, + { cause: delegationResult.error } ) } } - // Authorize the listed Content Serve Services to serve content from the created space - if (options.skipContentServeAuthorization !== true) { + // Authorize the listed Gateway Services to serve content from the created space + if (options.skipGatewayAuthorization !== true) { if ( - !options.authorizeContentServeServices || - options.authorizeContentServeServices.length === 0 + !options.authorizeGatewayServices || + options.authorizeGatewayServices.length === 0 ) { throw new Error( - 'failed to authorize Content Serve Services: missing option' - ) - } - - if (!options.connection) { - throw new Error( - 'failed to authorize Content Serve Services: missing option' + 'failed to authorize Gateway Services: missing option' ) } - for (const service of options.authorizeContentServeServices) { - await this.authorizeContentServe(space, { - audience: service, - connection: options.connection, - }) + for (const serviceConnection of options.authorizeGatewayServices) { + await this.authorizeContentServe(space, serviceConnection) } } @@ -337,12 +328,12 @@ export class Client extends Base { * - `space/content/serve/*` * * @param {import('./types.js').OwnedSpace} space - The space to authorize the audience for. - * @param {object} options - Options for the authorization. - * @param {`did:${string}:${string}`} options.audience - The Web DID of the audience (gateway or peer) to authorize. - * @param {import('./types.js').ConnectionView} options.connection - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @param {import('./types.js').ConnectionView} connection - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @param {object} [options] - Options for the content serve authorization invocation. + * @param {`did:${string}:${string}`} [options.audience] - The Web DID of the audience (gateway or peer) to authorize. * @param {number} [options.expiration] - The time at which the delegation expires in seconds from unix epoch. */ - async authorizeContentServe(space, options) { + async authorizeContentServe(space, connection, options = {}) { const currentSpace = this.currentSpace() try { // Set the current space to the space we are authorizing the gateway for, otherwise the delegation will fail @@ -350,7 +341,7 @@ export class Client extends Base { /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ const audience = { - did: () => options.audience, + did: () => options.audience ?? connection.id.did(), } // Grant the audience the ability to serve content from the space, it includes existing proofs automatically @@ -378,7 +369,7 @@ export class Client extends Base { }, }, }) - .execute(options.connection) + .execute(connection) /* c8 ignore next 8 - can't mock this error */ if (verificationResult.out.error) { diff --git a/packages/w3up-client/test/account.test.js b/packages/w3up-client/test/account.test.js index 0278c527b..154e43e5b 100644 --- a/packages/w3up-client/test/account.test.js +++ b/packages/w3up-client/test/account.test.js @@ -112,7 +112,7 @@ export const testAccount = Test.withContext({ { client, mail, grantAccess } ) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const mnemonic = space.toMnemonic() const { signer } = await Space.fromMnemonic(mnemonic, { name: 'import' }) @@ -150,7 +150,7 @@ export const testAccount = Test.withContext({ 'multi device workflow': async (asserts, { connect, mail, grantAccess }) => { const laptop = await connect() const space = await laptop.createSpace('main', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) // want to provision space ? @@ -188,7 +188,7 @@ export const testAccount = Test.withContext({ }, 'setup recovery': async (assert, { client, mail, grantAccess }) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const email = 'alice@web.mail' @@ -287,7 +287,7 @@ export const testAccount = Test.withContext({ { client, mail, grantAccess } ) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const email = 'alice@web.mail' @@ -309,7 +309,7 @@ export const testAccount = Test.withContext({ 'space.save': async (assert, { client }) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.deepEqual(client.spaces(), []) diff --git a/packages/w3up-client/test/capability/access.test.js b/packages/w3up-client/test/capability/access.test.js index 938bb05b7..baf6add3d 100644 --- a/packages/w3up-client/test/capability/access.test.js +++ b/packages/w3up-client/test/capability/access.test.js @@ -31,7 +31,7 @@ export const AccessClient = Test.withContext({ }) const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/blob.test.js b/packages/w3up-client/test/capability/blob.test.js index 7e5f9481e..6c0d21470 100644 --- a/packages/w3up-client/test/capability/blob.test.js +++ b/packages/w3up-client/test/capability/blob.test.js @@ -20,7 +20,7 @@ export const BlobClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -59,7 +59,7 @@ export const BlobClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -99,7 +99,7 @@ export const BlobClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -133,7 +133,7 @@ export const BlobClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/filecoin.test.js b/packages/w3up-client/test/capability/filecoin.test.js index c8795171a..7d51d4c78 100644 --- a/packages/w3up-client/test/capability/filecoin.test.js +++ b/packages/w3up-client/test/capability/filecoin.test.js @@ -6,7 +6,7 @@ export const FilecoinClient = Test.withContext({ offer: { 'should send an offer': async (assert, { client: alice }) => { const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -39,7 +39,7 @@ export const FilecoinClient = Test.withContext({ } const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/index.test.js b/packages/w3up-client/test/capability/index.test.js index 3dbd4f4bf..1e4bdc0a4 100644 --- a/packages/w3up-client/test/capability/index.test.js +++ b/packages/w3up-client/test/capability/index.test.js @@ -15,7 +15,7 @@ export const IndexClient = Test.withContext({ const car = await randomCAR(128) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index dd07ae227..3b71e3798 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -20,7 +20,7 @@ export const SpaceClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice, { access: { 'space/info': {} }, @@ -58,7 +58,7 @@ export const SpaceClient = Test.withContext({ }, }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) @@ -170,7 +170,7 @@ export const SpaceClient = Test.withContext({ }, }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) @@ -280,7 +280,7 @@ export const SpaceClient = Test.withContext({ }, }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await alice.addSpace( await space.createAuthorization(alice) @@ -394,7 +394,7 @@ export const SpaceClient = Test.withContext({ }, }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await alice.addSpace( await space.createAuthorization(alice) @@ -510,7 +510,7 @@ export const SpaceClient = Test.withContext({ }, }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await alice.addSpace(await space.createAuthorization(alice)) assert.ok(auth) diff --git a/packages/w3up-client/test/capability/store.test.js b/packages/w3up-client/test/capability/store.test.js index c0ac10b3f..a58ba0f9e 100644 --- a/packages/w3up-client/test/capability/store.test.js +++ b/packages/w3up-client/test/capability/store.test.js @@ -17,7 +17,7 @@ export const StoreClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -54,7 +54,7 @@ export const StoreClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -92,7 +92,7 @@ export const StoreClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -128,7 +128,7 @@ export const StoreClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/subscription.test.js b/packages/w3up-client/test/capability/subscription.test.js index 67fa48277..82860a127 100644 --- a/packages/w3up-client/test/capability/subscription.test.js +++ b/packages/w3up-client/test/capability/subscription.test.js @@ -9,7 +9,7 @@ export const SubscriptionClient = Test.withContext({ { client, connection, service, plansStorage, grantAccess, mail } ) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const email = 'alice@web.mail' const login = Account.login(client, email) diff --git a/packages/w3up-client/test/capability/upload.test.js b/packages/w3up-client/test/capability/upload.test.js index efbf5252e..649d91ce0 100644 --- a/packages/w3up-client/test/capability/upload.test.js +++ b/packages/w3up-client/test/capability/upload.test.js @@ -10,7 +10,7 @@ export const UploadClient = Test.withContext({ const car = await randomCAR(128) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -41,7 +41,7 @@ export const UploadClient = Test.withContext({ const car = await randomCAR(128) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -82,7 +82,7 @@ export const UploadClient = Test.withContext({ const car = await randomCAR(128) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -116,7 +116,7 @@ export const UploadClient = Test.withContext({ const car = await randomCAR(128) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index d04b8fffb..45a2d9182 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -19,7 +19,7 @@ export const UsageClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -63,7 +63,7 @@ export const UsageClient = Test.withContext({ }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 42cee03e4..8b2285fda 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -46,7 +46,7 @@ export const testClient = { }) const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -125,7 +125,7 @@ export const testClient = { }) const space = await alice.createSpace('upload-dir-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -172,7 +172,7 @@ export const testClient = { }) const space = await alice.createSpace('car-space', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -235,7 +235,7 @@ export const testClient = { assert.equal(current0, undefined) const space = await alice.createSpace('new-space', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -251,7 +251,7 @@ export const testClient = { const name = `space-${Date.now()}` const space = await alice.createSpace(name, { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -267,7 +267,7 @@ export const testClient = { const bob = new Client(await AgentData.create()) const space = await alice.createSpace('new-space', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace( await space.createAuthorization(alice, { @@ -303,7 +303,7 @@ export const testClient = { // Step 2: Alice creates a space with her account as the recovery account const space = await client.createSpace('recovery-space-test', { account: aliceAccount, // The account is the recovery account - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.ok(space) @@ -336,7 +336,7 @@ export const testClient = { // Step 2: Alice creates a space without providing a recovery account const space = await client.createSpace('no-recovery-space-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.ok(space) @@ -440,7 +440,7 @@ export const testClient = { // Step 2: Alice creates a space const space = await aliceClient.createSpace('share-space-test', { account: aliceAccount, - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.ok(space) @@ -493,7 +493,7 @@ export const testClient = { 'share-space-delegate-fail-test', { account: aliceAccount, - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, } ) assert.ok(space) @@ -530,14 +530,14 @@ export const testClient = { // Step 2: Alice creates a space const spaceA = await client.createSpace('test-space-a', { account: aliceAccount, - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.ok(spaceA) // Step 3: Alice creates another space to share with a friend const spaceB = await client.createSpace('test-space-b', { account: aliceAccount, - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) assert.ok(spaceB) @@ -583,7 +583,7 @@ export const testClient = { 'authorize-gateway-space', { account: aliceAccount, - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, } ) assert.ok(spaceA) @@ -597,10 +597,7 @@ export const testClient = { // Step 3: Alice authorizes the gateway to serve content from the space const delegationResult = await aliceClient.authorizeContentServe( spaceA, - { - audience: gateway.did(), - connection: gatewayConnection, - } + gatewayConnection ) assert.ok(delegationResult.ok) const { delegation } = delegationResult.ok @@ -650,8 +647,7 @@ export const testClient = { 'authorize-gateway-space', { account: aliceAccount, - authorizeContentServeServices: [gateway.did()], - connection: gatewayConnection, + authorizeGatewayServices: [gatewayConnection], } ) assert.ok(spaceA, 'should create the space') @@ -659,47 +655,6 @@ export const testClient = { assert.fail(error, 'should not throw when creating the space') } }, - 'should throw when the content serve authorization fails due to missing connection configuration': - async (assert, { mail, grantAccess, connection }) => { - // Step 1: Create a client for Alice and login - const aliceClient = new Client( - await AgentData.create({ - principal: alice, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - } - ) - - const aliceEmail = 'alice@web.mail' - const aliceLogin = aliceClient.login(aliceEmail) - const message = await mail.take() - assert.deepEqual(message.to, aliceEmail) - await grantAccess(message) - const aliceAccount = await aliceLogin - - try { - const spaceA = await aliceClient.createSpace( - 'authorize-gateway-space', - { - account: aliceAccount, - authorizeContentServeServices: [gateway.did()], - } - ) - assert.fail(spaceA, 'should not create the space') - } catch (error) { - assert.match( - // @ts-expect-error - error.message, - /missing option/, - 'should throw when creating the space' - ) - } - }, 'should throw when the content serve authorization fails due to missing service configuration': async (assert, { mail, grantAccess, connection }) => { // Step 1: Create a client for Alice and login @@ -723,18 +678,12 @@ export const testClient = { await grantAccess(message) const aliceAccount = await aliceLogin - const gatewayService = getContentServeMockService() - const gatewayConnection = getConnection( - gateway, - gatewayService - ).connection try { const spaceA = await aliceClient.createSpace( 'authorize-gateway-space', { account: aliceAccount, - authorizeContentServeServices: [], // No services to authorize - connection: gatewayConnection, + authorizeGatewayServices: [], // No services to authorize } ) assert.fail(spaceA, 'should not create the space') @@ -742,7 +691,7 @@ export const testClient = { assert.match( // @ts-expect-error error.message, - /missing option/, + /missing option/, 'should throw when creating the space' ) } @@ -784,8 +733,7 @@ export const testClient = { try { await aliceClient.createSpace('authorize-gateway-space', { account: aliceAccount, - authorizeContentServeServices: [gateway.did()], - connection: gatewayConnection, + authorizeGatewayServices: [gatewayConnection], }) assert.fail('should not create the space') } catch (error) { @@ -804,7 +752,7 @@ export const testClient = { const bob = new Client(await AgentData.create()) const space = await alice.createSpace('proof-space', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -824,7 +772,7 @@ export const testClient = { const bob = new Client(await AgentData.create()) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -862,7 +810,7 @@ export const testClient = { }) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace( await space.createAuthorization(alice, { @@ -886,7 +834,7 @@ export const testClient = { const bob = new Client(await AgentData.create()) const space = await alice.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) await alice.addSpace(await space.createAuthorization(alice)) await alice.setCurrentSpace(space.did()) @@ -939,7 +887,7 @@ export const testClient = { // setup space const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -990,7 +938,7 @@ export const testClient = { // setup space const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -1044,7 +992,7 @@ export const testClient = { // setup space const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) @@ -1070,7 +1018,7 @@ export const testClient = { // setup space const space = await alice.createSpace('upload-test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const auth = await space.createAuthorization(alice) await alice.addSpace(auth) diff --git a/packages/w3up-client/test/coupon.test.js b/packages/w3up-client/test/coupon.test.js index c44aa7fce..d75c9ee27 100644 --- a/packages/w3up-client/test/coupon.test.js +++ b/packages/w3up-client/test/coupon.test.js @@ -38,7 +38,7 @@ export const testCoupon = Test.withContext({ // creates a space and provision it with redeemed coupon const space = await alice.createSpace('home', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const result = await space.provision(access) await space.save() diff --git a/packages/w3up-client/test/space.test.js b/packages/w3up-client/test/space.test.js index 838b534ec..65c7f54b0 100644 --- a/packages/w3up-client/test/space.test.js +++ b/packages/w3up-client/test/space.test.js @@ -25,7 +25,7 @@ export const testSpace = Test.withContext({ 'should get usage': async (assert, { client, grantAccess, mail }) => { const space = await client.createSpace('test', { - skipContentServeAuthorization: true, + skipGatewayAuthorization: true, }) const email = 'alice@web.mail' From d85023d9d12ea52794a96f88d195041371a5bf23 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 2 Dec 2024 13:23:11 -0300 Subject: [PATCH 10/11] export authorizeContentServe --- packages/w3up-client/src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/w3up-client/src/index.js b/packages/w3up-client/src/index.js index 5d38b5363..dbc59a80e 100644 --- a/packages/w3up-client/src/index.js +++ b/packages/w3up-client/src/index.js @@ -44,4 +44,7 @@ export async function create(options = {}) { return new Client(data, options) } +export const authorizeContentServe = + Client.prototype.authorizeContentServe.bind(Client.prototype) + export { Client } From 8ab9792ae2f7ac5c277803894977547f428e8207 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 2 Dec 2024 15:11:52 -0300 Subject: [PATCH 11/11] fix authorizeContentServe export --- packages/w3up-client/src/client.js | 142 +++++++++++---------- packages/w3up-client/src/index.js | 4 +- packages/w3up-client/test/client.test.js | 5 +- packages/w3up-client/test/mocks/service.js | 6 +- 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/packages/w3up-client/src/client.js b/packages/w3up-client/src/client.js index b331449a3..096dcdbce 100644 --- a/packages/w3up-client/src/client.js +++ b/packages/w3up-client/src/client.js @@ -314,80 +314,13 @@ export class Client extends Base { } for (const serviceConnection of options.authorizeGatewayServices) { - await this.authorizeContentServe(space, serviceConnection) + await authorizeContentServe(this, space, serviceConnection) } } return space } - /** - * Authorizes an audience to serve content from the provided space and record egress events. - * It also publishes the delegation to the content serve service. - * Delegates the following capabilities to the audience: - * - `space/content/serve/*` - * - * @param {import('./types.js').OwnedSpace} space - The space to authorize the audience for. - * @param {import('./types.js').ConnectionView} connection - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. - * @param {object} [options] - Options for the content serve authorization invocation. - * @param {`did:${string}:${string}`} [options.audience] - The Web DID of the audience (gateway or peer) to authorize. - * @param {number} [options.expiration] - The time at which the delegation expires in seconds from unix epoch. - */ - async authorizeContentServe(space, connection, options = {}) { - const currentSpace = this.currentSpace() - try { - // Set the current space to the space we are authorizing the gateway for, otherwise the delegation will fail - await this.setCurrentSpace(space.did()) - - /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ - const audience = { - did: () => options.audience ?? connection.id.did(), - } - - // Grant the audience the ability to serve content from the space, it includes existing proofs automatically - const delegation = await this.createDelegation( - audience, - [SpaceCapabilities.contentServe.can], - { - expiration: options.expiration ?? Infinity, - } - ) - - // Publish the delegation to the content serve service - const accessProofs = this.proofs([ - { can: AccessCapabilities.access.can, with: space.did() }, - ]) - const verificationResult = await AccessCapabilities.delegate - .invoke({ - issuer: this._agent.issuer, - audience, - with: space.did(), - proofs: [...accessProofs, delegation], - nb: { - delegations: { - [delegation.cid.toString()]: delegation.cid, - }, - }, - }) - .execute(connection) - - /* c8 ignore next 8 - can't mock this error */ - if (verificationResult.out.error) { - throw new Error( - `failed to publish delegation for audience ${options.audience}: ${verificationResult.out.error.message}`, - { - cause: verificationResult.out.error, - } - ) - } - return { ok: { ...verificationResult.out.ok, delegation } } - } finally { - if (currentSpace) { - await this.setCurrentSpace(currentSpace.did()) - } - } - } - /** * Share an existing space with another Storacha account via email address delegation. * Delegates access to the space to the specified email account with the following permissions: @@ -619,3 +552,76 @@ export class Client extends Base { await this.capability.upload.remove(contentCID) } } + +/** + * Authorizes an audience to serve content from the provided space and record egress events. + * It also publishes the delegation to the content serve service. + * Delegates the following capabilities to the audience: + * - `space/content/serve/*` + * + * @param {Client} client - The w3up client instance. + * @param {import('./types.js').OwnedSpace} space - The space to authorize the audience for. + * @param {import('./types.js').ConnectionView} connection - The connection to the Content Serve Service that will handle, validate, and store the access/delegate UCAN invocation. + * @param {object} [options] - Options for the content serve authorization invocation. + * @param {`did:${string}:${string}`} [options.audience] - The Web DID of the audience (gateway or peer) to authorize. + * @param {number} [options.expiration] - The time at which the delegation expires in seconds from unix epoch. + */ +export const authorizeContentServe = async ( + client, + space, + connection, + options = {} +) => { + const currentSpace = client.currentSpace() + try { + // Set the current space to the space we are authorizing the gateway for, otherwise the delegation will fail + await client.setCurrentSpace(space.did()) + + /** @type {import('@ucanto/client').Principal<`did:${string}:${string}`>} */ + const audience = { + did: () => options.audience ?? connection.id.did(), + } + + // Grant the audience the ability to serve content from the space, it includes existing proofs automatically + const delegation = await client.createDelegation( + audience, + [SpaceCapabilities.contentServe.can], + { + expiration: options.expiration ?? Infinity, + } + ) + + // Publish the delegation to the content serve service + const accessProofs = client.proofs([ + { can: AccessCapabilities.access.can, with: space.did() }, + ]) + const verificationResult = await AccessCapabilities.delegate + .invoke({ + issuer: client.agent.issuer, + audience, + with: space.did(), + proofs: [...accessProofs, delegation], + nb: { + delegations: { + [delegation.cid.toString()]: delegation.cid, + }, + }, + }) + .execute(connection) + + /* c8 ignore next 8 - can't mock this error */ + if (verificationResult.out.error) { + throw new Error( + `failed to publish delegation for audience ${options.audience}: ${verificationResult.out.error.message}`, + { + cause: verificationResult.out.error, + } + ) + } + return { ok: { ...verificationResult.out.ok, delegation } } + } finally { + if (currentSpace) { + await client.setCurrentSpace(currentSpace.did()) + } + } +} diff --git a/packages/w3up-client/src/index.js b/packages/w3up-client/src/index.js index dbc59a80e..da1bcbb14 100644 --- a/packages/w3up-client/src/index.js +++ b/packages/w3up-client/src/index.js @@ -12,6 +12,7 @@ import { Client } from './client.js' export * as Result from './result.js' export * as Account from './account.js' export * from './ability.js' +export { authorizeContentServe } from './client.js' /** * Create a new w3up client. @@ -44,7 +45,4 @@ export async function create(options = {}) { return new Client(data, options) } -export const authorizeContentServe = - Client.prototype.authorizeContentServe.bind(Client.prototype) - export { Client } diff --git a/packages/w3up-client/test/client.test.js b/packages/w3up-client/test/client.test.js index 8b2285fda..b64463bf4 100644 --- a/packages/w3up-client/test/client.test.js +++ b/packages/w3up-client/test/client.test.js @@ -10,7 +10,7 @@ import { import { randomBytes, randomCAR } from './helpers/random.js' import { toCAR } from './helpers/car.js' import { File } from './helpers/shims.js' -import { Client } from '../src/client.js' +import { authorizeContentServe, Client } from '../src/client.js' import * as Test from './test.js' import { receiptsEndpoint } from './helpers/utils.js' import { Absentee } from '@ucanto/principal' @@ -595,7 +595,8 @@ export const testClient = { ).connection // Step 3: Alice authorizes the gateway to serve content from the space - const delegationResult = await aliceClient.authorizeContentServe( + const delegationResult = await authorizeContentServe( + aliceClient, spaceA, gatewayConnection ) diff --git a/packages/w3up-client/test/mocks/service.js b/packages/w3up-client/test/mocks/service.js index 62ee0c37d..4b8805fde 100644 --- a/packages/w3up-client/test/mocks/service.js +++ b/packages/w3up-client/test/mocks/service.js @@ -1,5 +1,6 @@ import * as Client from '@ucanto/client' import * as Server from '@ucanto/server' +import { HTTP } from '@ucanto/transport' import * as CAR from '@ucanto/transport/car' import * as AccessCaps from '@web3-storage/capabilities' @@ -23,8 +24,9 @@ export function getContentServeMockService(result = { ok: {} }) { * * @param {any} id * @param {any} service + * @param {string | undefined} [url] */ -export function getConnection(id, service) { +export function getConnection(id, service, url = undefined) { const server = Server.create({ id: id, service, @@ -34,7 +36,7 @@ export function getConnection(id, service) { const connection = Client.connect({ id: id, codec: CAR.outbound, - channel: server, + channel: url ? HTTP.open({ url: new URL(url) }) : server, }) return { connection }