diff --git a/packages/capabilities/src/space.js b/packages/capabilities/src/space.js index 704e2cf66..54f48cb7c 100644 --- a/packages/capabilities/src/space.js +++ b/packages/capabilities/src/space.js @@ -63,3 +63,32 @@ export const allocate = capability({ } }, }) + +/** + * The capability grants permission for all content serve operations that fall under the "space/content/serve" namespace. + * It can be derived from any of the `space/*` capability that has matching `with`. + */ + +export const contentServe = capability({ + can: 'space/content/serve/*', + with: SpaceDID, + derives: equalWith, +}) + +/** + * Capability can be invoked by an agent to record egress data for a given resource. + * It can be derived from any of the `space/content/serve/*` capability that has matching `with`. + */ +export const egressRecord = capability({ + can: 'space/content/serve/egress/record', + with: SpaceDID, + nb: Schema.struct({ + /** CID of the resource that was served. */ + resource: Schema.link(), + /** Amount of bytes served. */ + bytes: Schema.integer().greaterThan(0), + /** Timestamp of the event in seconds after Unix epoch. */ + servedAt: Schema.integer().greaterThan(-1), + }), + derives: equalWith, +}) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 7704922e3..f3d48c6af 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -18,7 +18,7 @@ import { ProofData, uint64, } from '@web3-storage/data-segment' -import { space, info } from './space.js' +import * as SpaceCaps from './space.js' import * as provider from './provider.js' import { top } from './top.js' import * as BlobCaps from './blob.js' @@ -131,8 +131,14 @@ export type UsageReport = InferInvokedCapability export type UsageReportSuccess = Record export type UsageReportFailure = Ucanto.Failure -export type EgressRecord = InferInvokedCapability -export type EgressRecordSuccess = Unit +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 { @@ -276,8 +282,8 @@ export interface RateLimitListSuccess { export type RateLimitListFailure = Ucanto.Failure // Space -export type Space = InferInvokedCapability -export type SpaceInfo = InferInvokedCapability +export type Space = InferInvokedCapability +export type SpaceInfo = InferInvokedCapability // filecoin export interface DealMetadata { diff --git a/packages/capabilities/src/usage.js b/packages/capabilities/src/usage.js index bde471236..d80fb212d 100644 --- a/packages/capabilities/src/usage.js +++ b/packages/capabilities/src/usage.js @@ -40,20 +40,3 @@ export const report = capability({ ) }, }) - -/** - * Capability can be invoked by an agent to record usage data for a given resource. - */ -export const record = capability({ - can: 'usage/record', - with: SpaceDID, - nb: Schema.struct({ - /** CID of the resource that was served. */ - resource: Schema.link(), - /** Amount of bytes served. */ - bytes: Schema.integer().greaterThan(0), - /** Timestamp of the event in seconds after Unix epoch. */ - servedAt: Schema.integer().greaterThan(-1), - }), - derives: equalWith, -}) diff --git a/packages/capabilities/test/helpers/fixtures.js b/packages/capabilities/test/helpers/fixtures.js index a4ae2d1b4..504a17c16 100644 --- a/packages/capabilities/test/helpers/fixtures.js +++ b/packages/capabilities/test/helpers/fixtures.js @@ -36,3 +36,12 @@ export const service = Signer.parse( export const readmeCID = parseLink( 'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4' ) + +export const gateway = Signer.parse( + 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' // random key +).withDID('did:web:w3s.link') + +/** did:key:z6MktYxTNoCxrXhK9oS5PdzutujTJ5DaS3FWYxNpRTXwrH6h */ +export const space = Signer.parse( + 'MgCYBaaeyfAHFNt5+M07rY9pPLnmhyxvMEj5jdyAN0ajSlO0B0Xk2fW+t/EsB2nqWraDmB7N0NiTXKZaVBbOpCMtCktI=' // random key +) diff --git a/packages/upload-api/src/space.js b/packages/upload-api/src/space.js index a4a030a36..d45d6bd06 100644 --- a/packages/upload-api/src/space.js +++ b/packages/upload-api/src/space.js @@ -3,14 +3,16 @@ import * as Provider from '@ucanto/server' import * as API from './types.js' import { info } from './space/info.js' +import { provide as provideRecordEgress } from './space/record.js' import { createService as createBlobService } from './blob.js' import { createService as createIndexService } from './index.js' /** - * @param {API.SpaceServiceContext & API.BlobServiceContext & API.IndexServiceContext} ctx + * @param {API.SpaceServiceContext & API.BlobServiceContext & API.IndexServiceContext & API.UsageServiceContext} ctx */ export const createService = (ctx) => ({ info: Provider.provide(Space.info, (input) => info(input, ctx)), blob: createBlobService(ctx), index: createIndexService(ctx), + content: { serve: { egress: { record: provideRecordEgress(ctx) } } }, }) diff --git a/packages/upload-api/src/usage/record.js b/packages/upload-api/src/space/record.js similarity index 73% rename from packages/upload-api/src/usage/record.js rename to packages/upload-api/src/space/record.js index f1d5b56bd..616844d82 100644 --- a/packages/upload-api/src/usage/record.js +++ b/packages/upload-api/src/space/record.js @@ -1,17 +1,17 @@ import * as API from '../types.js' import * as Provider from '@ucanto/server' -import { Usage } from '@web3-storage/capabilities' +import { Space } from '@web3-storage/capabilities' -/** @param {API.UsageServiceContext} context */ +/** @param {API.SpaceServiceContext & API.UsageServiceContext} context */ export const provide = (context) => - Provider.provide(Usage.record, (input) => record(input, context)) + Provider.provide(Space.egressRecord, (input) => egressRecord(input, context)) /** - * @param {API.Input} input - * @param {API.UsageServiceContext} context + * @param {API.Input} input + * @param {API.SpaceServiceContext & API.UsageServiceContext} context * @returns {Promise>} */ -const record = async ({ capability, invocation }, context) => { +const egressRecord = async ({ capability, invocation }, context) => { const provider = /** @type {`did:web:${string}`} */ ( invocation.audience.did() ) diff --git a/packages/upload-api/src/usage.js b/packages/upload-api/src/usage.js index 8cea68010..2600acbf4 100644 --- a/packages/upload-api/src/usage.js +++ b/packages/upload-api/src/usage.js @@ -1,8 +1,6 @@ import { provide as provideReport } from './usage/report.js' -import { provide as provideRecord } from './usage/record.js' /** @param {import('./types.js').UsageServiceContext} context */ export const createService = (context) => ({ report: provideReport(context), - record: provideRecord(context), }) diff --git a/packages/upload-api/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index ee7a67175..91e6c766e 100644 --- a/packages/upload-api/test/helpers/utils.js +++ b/packages/upload-api/test/helpers/utils.js @@ -37,11 +37,15 @@ export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) -export const w3 = ed25519 - .parse( - 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' - ) - .withDID('did:web:test.web3.storage') +export const w3Signer = ed25519.parse( + 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' +) +export const w3 = w3Signer.withDID('did:web:test.web3.storage') + +export const gatewaySigner = ed25519.parse( + 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' +) +export const gateway = gatewaySigner.withDID('did:web:w3s.link') /** * Creates a server for the given service. diff --git a/packages/w3up-client/src/capability/space.js b/packages/w3up-client/src/capability/space.js index f37d31fae..9f7d9905b 100644 --- a/packages/w3up-client/src/capability/space.js +++ b/packages/w3up-client/src/capability/space.js @@ -1,4 +1,6 @@ import { Base } from '../base.js' +import { Space as SpaceCapabilities } from '@web3-storage/capabilities' +import * as API from '../types.js' /** * Client for interacting with the `space/*` capabilities. @@ -17,4 +19,71 @@ export class SpaceClient extends Base { async info(space, options) { return await this._agent.getSpaceInfo(space, options) } + + /** + * Record egress data for a served resource. + * It will execute the capability invocation to find the customer and then record the egress data for the resource. + * + * Required delegated capabilities: + * - `space/content/serve/egress/record` + * + * @param {object} egressData + * @param {import('../types.js').SpaceDID} egressData.space + * @param {API.UnknownLink} egressData.resource + * @param {number} egressData.bytes + * @param {string} egressData.servedAt + * @param {object} [options] + * @param {string} [options.nonce] + * @param {API.Delegation[]} [options.proofs] + * @returns {Promise} + */ + async egressRecord(egressData, options) { + const out = await egressRecord( + { agent: this.agent }, + { ...egressData }, + { ...options } + ) + + if (!out.ok) { + throw new Error( + `failed ${SpaceCapabilities.egressRecord.can} invocation`, + { + cause: out.error, + } + ) + } + + return /** @type {API.EgressRecordSuccess} */ (out.ok) + } +} + +/** + * Record egress data for a resource from a given space. + * + * @param {{agent: API.Agent}} client + * @param {object} egressData + * @param {API.SpaceDID} egressData.space + * @param {API.UnknownLink} egressData.resource + * @param {number} egressData.bytes + * @param {string} egressData.servedAt + * @param {object} options + * @param {string} [options.nonce] + * @param {API.Delegation[]} [options.proofs] + */ +export const egressRecord = async ( + { agent }, + { space, resource, bytes, servedAt }, + { nonce, proofs = [] } +) => { + const receipt = await agent.invokeAndExecute(SpaceCapabilities.egressRecord, { + with: space, + proofs, + nonce, + nb: { + resource, + bytes, + servedAt: Math.floor(new Date(servedAt).getTime() / 1000), + }, + }) + return receipt.out } diff --git a/packages/w3up-client/src/capability/usage.js b/packages/w3up-client/src/capability/usage.js index 5685dd772..19b53ba7c 100644 --- a/packages/w3up-client/src/capability/usage.js +++ b/packages/w3up-client/src/capability/usage.js @@ -31,37 +31,6 @@ export class UsageClient extends Base { return out.ok } - - /** - * Record egress data for a served resource. - * It will execute the capability invocation to find the customer and then record the egress data for the resource. - * - * Required delegated capabilities: - * - `usage/record` - * - * @param {import('../types.js').SpaceDID} space - * @param {object} egressData - * @param {API.UnknownLink} egressData.resource - * @param {number} egressData.bytes - * @param {string} egressData.servedAt - * @param {object} [options] - * @param {string} [options.nonce] - */ - async record(space, egressData, options) { - const out = await record( - { agent: this.agent }, - { space, ...egressData }, - { ...options } - ) - /* c8 ignore next 5 */ - if (!out.ok) { - throw new Error(`failed ${UsageCapabilities.record.can} invocation`, { - cause: out.error, - }) - } - - return out.ok - } } /** @@ -92,35 +61,3 @@ export const report = async ( }) return receipt.out } - -/** - * Record egress data for a resource from a given space. - * - * @param {{agent: API.Agent}} client - * @param {object} egressData - * @param {API.SpaceDID} egressData.space - * @param {API.UnknownLink} egressData.resource - * @param {number} egressData.bytes - * @param {string} egressData.servedAt - * @param {object} options - * @param {string} [options.nonce] - * @param {API.Delegation[]} [options.proofs] - * @returns {Promise>} - */ -export const record = async ( - { agent }, - { space, resource, bytes, servedAt }, - { nonce, proofs = [] } -) => { - const receipt = await agent.invokeAndExecute(UsageCapabilities.record, { - with: space, - proofs, - nonce, - nb: { - resource, - bytes, - servedAt: Math.floor(new Date(servedAt).getTime() / 1000), - }, - }) - return receipt.out -} diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index 8e9bcc4b1..b288b8afc 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -1,6 +1,9 @@ import { AgentData } from '@web3-storage/access/agent' import { Client } from '../../src/client.js' import * as Test from '../test.js' +import { Space } from '@web3-storage/capabilities' +import { gatewaySigner } from '../../../upload-api/test/helpers/utils.js' +import { randomCAR } from '../helpers/random.js' export const SpaceClient = Test.withContext({ info: { @@ -37,6 +40,544 @@ export const SpaceClient = Test.withContext({ assert.deepEqual(info.providers, [connection.id.did()]) }, }, + record: { + 'should record egress if the capability is derived from *': async ( + assert, + { id: w3, connection, provisionsStorage } + ) => { + const expiration = Date.now() + 1000 * 60 * 60 * 24 // 1 day from now + + // 1. Setup test space and allow Alice Agent to access it + const alice = new Client(await AgentData.create(), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + }) + const space = await alice.createSpace('test') + const auth = await alice.addSpace(await space.createAuthorization(alice)) + assert.ok(auth) + + await alice.setCurrentSpace(space.did()) + await provisionsStorage.put({ + // @ts-expect-error + provider: w3.did(), + account: alice.did(), + consumer: space.did(), + }) + + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: gatewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress + const egressRecordGatewayDelegation = await Space.egressRecord.delegate({ + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: await alice.proofs(), + }) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [egressRecordGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 4. freewayService claims the delegation + const freewayDelegations = await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => c.can === Space.egressRecord.can && c.with === space.did() + ) + ) + ) + + // 5. Create a random resource to record egress + const car = await randomCAR(128) + const resource = await alice.capability.store.add(car) + assert.ok(resource) + + // 6. freewayService invokes egress/record + try { + const egressData = { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + } + const egressRecord = await freewayService.capability.space.egressRecord( + egressData, + { + proofs: await freewayService.proofs(), + } + ) + assert.ok(egressRecord, 'egressRecord should be returned') + assert.equal( + egressRecord.space, + space.did(), + 'space should be the same' + ) + assert.equal( + egressRecord.resource.toString(), + resource.toString(), + 'resource should be the same' + ) + assert.equal(egressRecord.bytes, car.size, 'bytes should be the same') + assert.equal( + new Date(egressRecord.servedAt).getTime(), + Math.floor(new Date(egressData.servedAt).getTime() / 1000) * 1000, + 'servedAt should be the same' + ) + assert.ok(egressRecord.cause.toString(), 'cause should be a link') + } catch (error) { + // @ts-ignore + assert.fail(error.cause ? error.cause.message : error) + } + }, + 'should record egress if the capability is derived from space/*': async ( + assert, + { id: w3, connection, provisionsStorage } + ) => { + const expiration = Date.now() + 1000 * 60 * 60 * 24 // 1 day from now + + // 1. Setup test space and allow Alice Agent to access it + const alice = new Client(await AgentData.create(), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + }) + const space = await alice.createSpace('test') + const auth = await alice.addSpace(await space.createAuthorization(alice)) + assert.ok(auth) + + await alice.setCurrentSpace(space.did()) + await provisionsStorage.put({ + // @ts-expect-error + provider: w3.did(), + account: alice.did(), + consumer: space.did(), + }) + + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: gatewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress + const spaceAccessGatewayDelegation = await Space.top.delegate({ + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: await alice.proofs(), + }) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [spaceAccessGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 4. freewayService claims the delegation + const freewayDelegations = await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => c.can === Space.top.can && c.with === space.did() + ) + ) + ) + + // 5. Create a random resource to record egress + const car = await randomCAR(128) + const resource = await alice.capability.store.add(car) + assert.ok(resource) + + // 6. freewayService invokes egress/record + try { + const egressData = { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + } + const egressRecord = await freewayService.capability.space.egressRecord( + egressData, + { + proofs: await freewayService.proofs(), + } + ) + assert.ok(egressRecord, 'egressRecord should be returned') + assert.equal( + egressRecord.space, + space.did(), + 'space should be the same' + ) + assert.equal( + egressRecord.resource.toString(), + resource.toString(), + 'resource should be the same' + ) + assert.equal(egressRecord.bytes, car.size, 'bytes should be the same') + assert.equal( + new Date(egressRecord.servedAt).getTime(), + Math.floor(new Date(egressData.servedAt).getTime() / 1000) * 1000, + 'servedAt should be the same' + ) + assert.ok(egressRecord.cause.toString(), 'cause should be a link') + } catch (error) { + // @ts-ignore + assert.fail(error.cause ? error.cause.message : error) + } + }, + 'should record egress if the capability is derived from space/content/serve/*': + async (assert, { id: w3, connection, provisionsStorage }) => { + const expiration = Date.now() + 1000 * 60 * 60 * 24 // 1 day from now + + // 1. Setup test space and allow Alice Agent to access it + const alice = new Client(await AgentData.create(), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + }) + const space = await alice.createSpace('test') + const auth = await alice.addSpace( + await space.createAuthorization(alice) + ) + assert.ok(auth) + + await alice.setCurrentSpace(space.did()) + await provisionsStorage.put({ + // @ts-expect-error + provider: w3.did(), + account: alice.did(), + consumer: space.did(), + }) + + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: gatewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to serve content + const contentServeGatewayDelegation = await Space.contentServe.delegate( + { + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: await alice.proofs(), + } + ) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [contentServeGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 4. freewayService claims the delegation + const freewayDelegations = + await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => + c.can === Space.contentServe.can && c.with === space.did() + ) + ) + ) + + // 5. Create a random resource to record egress + const car = await randomCAR(128) + const resource = await alice.capability.store.add(car) + assert.ok(resource) + + // 6. freewayService invokes egress/record + try { + const egressData = { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + } + const egressRecord = + await freewayService.capability.space.egressRecord(egressData, { + proofs: await freewayService.proofs(), + }) + assert.ok(egressRecord, 'egressRecord should be returned') + assert.equal( + egressRecord.space, + space.did(), + 'space should be the same' + ) + assert.equal( + egressRecord.resource.toString(), + resource.toString(), + 'resource should be the same' + ) + assert.equal(egressRecord.bytes, car.size, 'bytes should be the same') + assert.equal( + new Date(egressRecord.servedAt).getTime(), + Math.floor(new Date(egressData.servedAt).getTime() / 1000) * 1000, + 'servedAt should be the same' + ) + assert.ok(egressRecord.cause.toString(), 'cause should be a link') + } catch (error) { + // @ts-ignore + assert.fail(error.cause ? error.cause.message : error) + } + }, + 'should record egress if the capability space/content/serve/egress/record is delegated': + async (assert, { id: w3, connection, provisionsStorage }) => { + const expiration = Date.now() + 1000 * 60 * 60 * 24 // 1 day from now + + // 1. Setup test space and allow Alice Agent to access it + const alice = new Client(await AgentData.create(), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + }) + const space = await alice.createSpace('test') + const auth = await alice.addSpace( + await space.createAuthorization(alice) + ) + assert.ok(auth) + + await alice.setCurrentSpace(space.did()) + await provisionsStorage.put({ + // @ts-expect-error + provider: w3.did(), + account: alice.did(), + consumer: space.did(), + }) + + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: gatewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress + const egressRecordGatewayDelegation = await Space.egressRecord.delegate( + { + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: await alice.proofs(), + } + ) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [egressRecordGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 4. freewayService claims the delegation + const freewayDelegations = + await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => + c.can === Space.egressRecord.can && c.with === space.did() + ) + ) + ) + + // 5. Create a random resource to record egress + const car = await randomCAR(128) + const resource = await alice.capability.store.add(car) + assert.ok(resource) + + // 6. freewayService invokes egress/record + try { + const egressData = { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + } + const egressRecord = + await freewayService.capability.space.egressRecord(egressData, { + proofs: await freewayService.proofs(), + }) + assert.ok(egressRecord, 'egressRecord should be returned') + assert.equal( + egressRecord.space, + space.did(), + 'space should be the same' + ) + assert.equal( + egressRecord.resource.toString(), + resource.toString(), + 'resource should be the same' + ) + assert.equal(egressRecord.bytes, car.size, 'bytes should be the same') + assert.equal( + new Date(egressRecord.servedAt).getTime(), + Math.floor(new Date(egressData.servedAt).getTime() / 1000) * 1000, + 'servedAt should be the same' + ) + assert.ok(egressRecord.cause.toString(), 'cause should be a link') + } catch (error) { + // @ts-ignore + assert.fail(error.cause ? error.cause.message : error) + } + }, + 'should fail to record egress if the capability was not delegated': async ( + assert, + { id: w3, connection, provisionsStorage } + ) => { + const expiration = Date.now() + 1000 * 60 * 60 * 24 // 1 day from now + + // 1. Setup test space and allow Alice Agent to access it + const alice = new Client(await AgentData.create(), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + }) + const space = await alice.createSpace('test') + const auth = await alice.addSpace(await space.createAuthorization(alice)) + assert.ok(auth) + + await alice.setCurrentSpace(space.did()) + await provisionsStorage.put({ + // @ts-expect-error + provider: w3.did(), + account: alice.did(), + consumer: space.did(), + }) + + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: gatewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress but without proofs + const egressRecordGatewayDelegation = await Space.egressRecord.delegate({ + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: [], // No proofs to test the error + }) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [egressRecordGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 4. freewayService claims the delegation + const freewayDelegations = await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => c.can === Space.egressRecord.can && c.with === space.did() + ) + ) + ) + + // 5. Create a random resource to record egress + const car = await randomCAR(128) + + // 6. FreewayService attempts to invoke egress/record without having the delegation + try { + await freewayService.capability.space.egressRecord( + { + space: space.did(), + resource: car.cid.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + }, + { proofs: [] } + ) + assert.fail('Expected an error due to missing delegation') + } catch (error) { + assert.equal( + // @ts-ignore + error.message, + `failed ${Space.egressRecord.can} invocation`, + 'error message should be the same' + ) + } + }, + }, }) Test.test({ SpaceClient }) diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index d24b76136..caadf5ede 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -2,7 +2,6 @@ import { AgentData } from '@web3-storage/access/agent' import { Client } from '../../src/client.js' import * as Test from '../test.js' import { receiptsEndpoint } from '../helpers/utils.js' -import { randomCAR } from '../helpers/random.js' export const UsageClient = Test.withContext({ report: { @@ -10,6 +9,7 @@ export const UsageClient = Test.withContext({ assert, { connection, provisionsStorage } ) => { + // 1. Setup alice account const alice = new Client(await AgentData.create(), { // @ts-ignore serviceConf: { @@ -69,47 +69,6 @@ export const UsageClient = Test.withContext({ assert.deepEqual(report, {}) }, }, - record: { - 'should record egress': async ( - assert, - { connection, provisionsStorage } - ) => { - const alice = new Client(await AgentData.create(), { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - }) - - const space = await alice.createSpace('test') - const auth = await space.createAuthorization(alice) - await alice.addSpace(auth) - - // Then we setup a billing for this account - await provisionsStorage.put({ - // @ts-expect-error - provider: connection.id.did(), - account: alice.agent.did(), - consumer: space.did(), - }) - - const car = await randomCAR(128) - const resource = car.cid - await alice.capability.upload.add(car.roots[0], [resource]) - - const result = await alice.capability.upload.get(car.roots[0]) - assert.ok(result) - - const record = await alice.capability.usage.record(space.did(), { - resource: resource.link(), - bytes: car.size, - servedAt: new Date().toISOString(), - }) - - assert.ok(record) - }, - }, }) Test.test({ UsageClient })