From a0bbd9e02b1b02c42317e4cff4d8ae0100eff4cb Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 31 Oct 2024 15:45:34 -0300 Subject: [PATCH 1/7] fix(usage/record): add provider as the resource --- packages/capabilities/src/usage.js | 8 ++- .../test/capabilities/usage.test.js | 68 +++++++++++++++++++ .../capabilities/test/helpers/fixtures.js | 4 ++ packages/upload-api/src/usage/record.js | 4 +- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/packages/capabilities/src/usage.js b/packages/capabilities/src/usage.js index bde471236..857a00ab7 100644 --- a/packages/capabilities/src/usage.js +++ b/packages/capabilities/src/usage.js @@ -1,6 +1,8 @@ -import { capability, ok, Schema } from '@ucanto/validator' +import { DID, capability, ok, Schema } from '@ucanto/validator' import { and, equal, equalWith, SpaceDID } from './utils.js' +export const ProviderDID = DID.match({ method: 'web' }) + /** * Capability can only be delegated (but not invoked) allowing audience to * be derived any `usage/` prefixed capability for the (memory) space identified @@ -46,8 +48,10 @@ export const report = capability({ */ export const record = capability({ can: 'usage/record', - with: SpaceDID, + with: ProviderDID, nb: Schema.struct({ + /** DID of the space where the resource is served from. */ + space: SpaceDID, /** CID of the resource that was served. */ resource: Schema.link(), /** Amount of bytes served. */ diff --git a/packages/capabilities/test/capabilities/usage.test.js b/packages/capabilities/test/capabilities/usage.test.js index 3d2d330dc..22d9cf3dd 100644 --- a/packages/capabilities/test/capabilities/usage.test.js +++ b/packages/capabilities/test/capabilities/usage.test.js @@ -8,6 +8,9 @@ import { service as w3, mallory as account, bob, + gateway, + readmeCID, + mallory, } from '../helpers/fixtures.js' import { validateAuthorization } from '../helpers/utils.js' @@ -200,4 +203,69 @@ describe('usage capabilities', function () { }) }, /Expected value of type integer instead got 6\.6/) }) + + it('usage/record should fail to be derived from *', async () => { + const data = { + space: mallory.did(), + resource: readmeCID, + bytes: 100, + servedAt: 1714204800, + } + const record = Usage.record.invoke({ + issuer: alice, + audience: w3, + with: gateway.did(), + nb: { ...data }, + proofs: [await top()], + }) + + const result = await access(await record.delegate(), { + capability: Usage.record, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error, 'Expected an error but none was found') + }) + + it('usage/record can be derived from usage/record', async () => { + const data = { + space: mallory.did(), + resource: readmeCID, + bytes: 100, + servedAt: 1714204800, + } + + const usageRecordDelegationProof = await Usage.record.delegate({ + issuer: alice, + audience: bob, + with: gateway.did(), + nb: { ...data }, + proofs: [await top()], + }) + + const record = Usage.record.invoke({ + issuer: bob, + audience: w3, + with: gateway.did(), + nb: { ...data }, + proofs: [usageRecordDelegationProof], + }) + + const result = await access(await record.delegate(), { + capability: Usage.record, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + if (result.error) { + assert.fail(result.error.message) + } + + assert.deepEqual(result.ok.audience.did(), w3.did()) + assert.equal(result.ok.capability.can, 'usage/record') + assert.deepEqual(result.ok.capability.nb, { data }) + }) }) diff --git a/packages/capabilities/test/helpers/fixtures.js b/packages/capabilities/test/helpers/fixtures.js index a4ae2d1b4..51698835b 100644 --- a/packages/capabilities/test/helpers/fixtures.js +++ b/packages/capabilities/test/helpers/fixtures.js @@ -36,3 +36,7 @@ export const service = Signer.parse( export const readmeCID = parseLink( 'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4' ) + +export const gateway = Absentee.from({ + id: 'did:web:freeway.storacha.network:gateway', +}) diff --git a/packages/upload-api/src/usage/record.js b/packages/upload-api/src/usage/record.js index f1d5b56bd..eae6bf323 100644 --- a/packages/upload-api/src/usage/record.js +++ b/packages/upload-api/src/usage/record.js @@ -17,7 +17,7 @@ const record = async ({ capability, invocation }, context) => { ) const consumerResponse = await context.provisionsStorage.getConsumer( provider, - capability.with + capability.nb.space ) if (consumerResponse.error) { return consumerResponse @@ -25,7 +25,7 @@ const record = async ({ capability, invocation }, context) => { const consumer = consumerResponse.ok const res = await context.usageStorage.record( // The space which contains the resource that was served. - capability.with, + capability.nb.space, // The customer that is being billed for the egress traffic. consumer.customer, // CID of the resource that was served. From 4549961b3b79f0823bb43e4b30819be77dd873e6 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Thu, 31 Oct 2024 21:44:45 -0300 Subject: [PATCH 2/7] fix: fixed test to resolve did key --- packages/capabilities/src/usage.js | 6 +-- .../test/capabilities/usage.test.js | 50 ++++++------------- .../capabilities/test/helpers/fixtures.js | 8 +-- packages/w3up-client/src/capability/usage.js | 13 +++-- .../w3up-client/test/capability/usage.test.js | 19 ++++--- 5 files changed, 41 insertions(+), 55 deletions(-) diff --git a/packages/capabilities/src/usage.js b/packages/capabilities/src/usage.js index 857a00ab7..093b990d0 100644 --- a/packages/capabilities/src/usage.js +++ b/packages/capabilities/src/usage.js @@ -1,7 +1,5 @@ -import { DID, capability, ok, Schema } from '@ucanto/validator' -import { and, equal, equalWith, SpaceDID } from './utils.js' - -export const ProviderDID = DID.match({ method: 'web' }) +import { capability, DID, ok, Schema } from '@ucanto/validator' +import { and, equal, equalWith, ProviderDID, SpaceDID } from './utils.js' /** * Capability can only be delegated (but not invoked) allowing audience to diff --git a/packages/capabilities/test/capabilities/usage.test.js b/packages/capabilities/test/capabilities/usage.test.js index 22d9cf3dd..692f0f586 100644 --- a/packages/capabilities/test/capabilities/usage.test.js +++ b/packages/capabilities/test/capabilities/usage.test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import { access } from '@ucanto/validator' +import { access, Schema } from '@ucanto/validator' import { Verifier } from '@ucanto/principal' import * as Usage from '../../src/usage.js' import * as Capability from '../../src/top.js' @@ -204,32 +204,7 @@ describe('usage capabilities', function () { }, /Expected value of type integer instead got 6\.6/) }) - it('usage/record should fail to be derived from *', async () => { - const data = { - space: mallory.did(), - resource: readmeCID, - bytes: 100, - servedAt: 1714204800, - } - const record = Usage.record.invoke({ - issuer: alice, - audience: w3, - with: gateway.did(), - nb: { ...data }, - proofs: [await top()], - }) - - const result = await access(await record.delegate(), { - capability: Usage.record, - principal: Verifier, - authority: w3, - validateAuthorization, - }) - - assert.ok(result.error, 'Expected an error but none was found') - }) - - it('usage/record can be derived from usage/record', async () => { + it('should delegate and invoke usage/record', async () => { const data = { space: mallory.did(), resource: readmeCID, @@ -237,27 +212,30 @@ describe('usage capabilities', function () { servedAt: 1714204800, } + // W3 delegates ability to record usage to Gateway const usageRecordDelegationProof = await Usage.record.delegate({ - issuer: alice, - audience: bob, - with: gateway.did(), - nb: { ...data }, - proofs: [await top()], + issuer: w3, + audience: gateway, + with: w3.did(), + expiration: Infinity, }) - const record = Usage.record.invoke({ - issuer: bob, + // Gateway invokes usage/record and indicates the w3 as the audience + const recordInvocation = Usage.record.invoke({ + issuer: gateway, audience: w3, with: gateway.did(), nb: { ...data }, proofs: [usageRecordDelegationProof], }) - const result = await access(await record.delegate(), { + // W3 validates the delegation from Gateway to itself + const result = await access(await recordInvocation.delegate(), { capability: Usage.record, principal: Verifier, authority: w3, validateAuthorization, + resolveDIDKey: () => Schema.ok(gateway.toDIDKey()), }) if (result.error) { @@ -266,6 +244,6 @@ describe('usage capabilities', function () { assert.deepEqual(result.ok.audience.did(), w3.did()) assert.equal(result.ok.capability.can, 'usage/record') - assert.deepEqual(result.ok.capability.nb, { data }) + assert.deepEqual(result.ok.capability.nb, { ...data }) }) }) diff --git a/packages/capabilities/test/helpers/fixtures.js b/packages/capabilities/test/helpers/fixtures.js index 51698835b..347aaaab7 100644 --- a/packages/capabilities/test/helpers/fixtures.js +++ b/packages/capabilities/test/helpers/fixtures.js @@ -1,4 +1,4 @@ -import { parseLink } from '@ucanto/core' +import { DID, parseLink } from '@ucanto/core' import { Absentee } from '@ucanto/principal' import { Signer } from '@ucanto/principal/ed25519' @@ -37,6 +37,6 @@ export const readmeCID = parseLink( 'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4' ) -export const gateway = Absentee.from({ - id: 'did:web:freeway.storacha.network:gateway', -}) +export const gateway = Signer.parse( + 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' // random key +).withDID('did:web:dag.haus:freeway.com:test') diff --git a/packages/w3up-client/src/capability/usage.js b/packages/w3up-client/src/capability/usage.js index 5685dd772..9288e169e 100644 --- a/packages/w3up-client/src/capability/usage.js +++ b/packages/w3up-client/src/capability/usage.js @@ -39,18 +39,19 @@ export class UsageClient extends Base { * Required delegated capabilities: * - `usage/record` * - * @param {import('../types.js').SpaceDID} space * @param {object} egressData + * @param {import('../types.js').SpaceDID} egressData.space * @param {API.UnknownLink} egressData.resource * @param {number} egressData.bytes * @param {string} egressData.servedAt + * @param {API.ProviderDID} provider * @param {object} [options] * @param {string} [options.nonce] */ - async record(space, egressData, options) { + async record(egressData, provider, options) { const out = await record( { agent: this.agent }, - { space, ...egressData }, + { provider, ...egressData }, { ...options } ) /* c8 ignore next 5 */ @@ -98,6 +99,7 @@ export const report = async ( * * @param {{agent: API.Agent}} client * @param {object} egressData + * @param {API.ProviderDID} egressData.provider * @param {API.SpaceDID} egressData.space * @param {API.UnknownLink} egressData.resource * @param {number} egressData.bytes @@ -109,14 +111,15 @@ export const report = async ( */ export const record = async ( { agent }, - { space, resource, bytes, servedAt }, + { provider, space, resource, bytes, servedAt }, { nonce, proofs = [] } ) => { const receipt = await agent.invokeAndExecute(UsageCapabilities.record, { - with: space, + with: provider, proofs, nonce, nb: { + space, resource, bytes, servedAt: Math.floor(new Date(servedAt).getTime() / 1000), diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index d24b76136..f741e5194 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -3,6 +3,7 @@ import { Client } from '../../src/client.js' import * as Test from '../test.js' import { receiptsEndpoint } from '../helpers/utils.js' import { randomCAR } from '../helpers/random.js' +import { Absentee } from '@ucanto/principal' export const UsageClient = Test.withContext({ report: { @@ -74,6 +75,9 @@ export const UsageClient = Test.withContext({ assert, { connection, provisionsStorage } ) => { + const gateway = Absentee.from({ + id: 'did:web:freeway.storacha.network', + }) const alice = new Client(await AgentData.create(), { // @ts-ignore serviceConf: { @@ -85,7 +89,6 @@ export const UsageClient = Test.withContext({ 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 @@ -101,11 +104,15 @@ export const UsageClient = Test.withContext({ 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(), - }) + const record = await alice.capability.usage.record( + { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + }, + gateway.did() + ) assert.ok(record) }, From 092906f8d61d402f410c071dcf05e2cf9490b784 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 1 Nov 2024 11:13:06 -0300 Subject: [PATCH 3/7] wip: fixing tests --- packages/capabilities/src/usage.js | 2 +- .../capabilities/test/helpers/fixtures.js | 2 +- packages/upload-api/test/helpers/utils.js | 14 ++- packages/w3up-client/src/capability/usage.js | 1 + .../w3up-client/test/capability/usage.test.js | 95 +++++++++++++++---- 5 files changed, 87 insertions(+), 27 deletions(-) diff --git a/packages/capabilities/src/usage.js b/packages/capabilities/src/usage.js index 093b990d0..810aecb47 100644 --- a/packages/capabilities/src/usage.js +++ b/packages/capabilities/src/usage.js @@ -1,4 +1,4 @@ -import { capability, DID, ok, Schema } from '@ucanto/validator' +import { capability, ok, Schema } from '@ucanto/validator' import { and, equal, equalWith, ProviderDID, SpaceDID } from './utils.js' /** diff --git a/packages/capabilities/test/helpers/fixtures.js b/packages/capabilities/test/helpers/fixtures.js index 347aaaab7..6203ed661 100644 --- a/packages/capabilities/test/helpers/fixtures.js +++ b/packages/capabilities/test/helpers/fixtures.js @@ -1,4 +1,4 @@ -import { DID, parseLink } from '@ucanto/core' +import { parseLink } from '@ucanto/core' import { Absentee } from '@ucanto/principal' import { Signer } from '@ucanto/principal/ed25519' diff --git a/packages/upload-api/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index ee7a67175..0f198f264 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 freewaySigner = ed25519.parse( + 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' +) +export const freeway = freewaySigner.withDID('did:web:freeway.web3.storage') /** * Creates a server for the given service. diff --git a/packages/w3up-client/src/capability/usage.js b/packages/w3up-client/src/capability/usage.js index 9288e169e..144b63799 100644 --- a/packages/w3up-client/src/capability/usage.js +++ b/packages/w3up-client/src/capability/usage.js @@ -47,6 +47,7 @@ export class UsageClient extends Base { * @param {API.ProviderDID} provider * @param {object} [options] * @param {string} [options.nonce] + * @param {API.Delegation[]} [options.proofs] */ async record(egressData, provider, options) { const out = await record( diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index f741e5194..b2c3a0728 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -3,7 +3,20 @@ import { Client } from '../../src/client.js' import * as Test from '../test.js' import { receiptsEndpoint } from '../helpers/utils.js' import { randomCAR } from '../helpers/random.js' -import { Absentee } from '@ucanto/principal' +import { + freeway, + freewaySigner, + mallory, + w3, + w3Signer, +} from '../../../upload-api/test/helpers/utils.js' +import { + alice, + gateway, +} from '@web3-storage/capabilities/test/helpers/fixtures' +import { Usage } from '@web3-storage/capabilities' +import { claim } from '../../src/capability/access.js' +import { Signer } from '@ucanto/principal/ed25519' export const UsageClient = Test.withContext({ report: { @@ -75,43 +88,85 @@ export const UsageClient = Test.withContext({ assert, { connection, provisionsStorage } ) => { - const gateway = Absentee.from({ - id: 'did:web:freeway.storacha.network', - }) - const alice = new Client(await AgentData.create(), { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - }) + // Creates a new agent using w3Signer as the principal + const w3Service = new Client( + await AgentData.create({ + principal: w3Signer, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + const space = await w3Service.createSpace('test') + const auth = await space.createAuthorization(w3Service) + await w3Service.addSpace(auth) - 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(), + account: w3Service.agent.did(), consumer: space.did(), }) + // Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: freewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // Random resource to record egress 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) + // w3Service delegates ability to record usage to freewayService + const recordEgress = await Usage.record.delegate({ + issuer: w3Service.agent.issuer, + audience: freewayService, + with: w3.did(), + expiration: Infinity, + }) + + const delegationResult = await w3Service.capability.access.delegate({ + delegations: [recordEgress], + }) + assert.ok(delegationResult.ok) + + // freewayService claims the delegation + const delegations = await freewayService.capability.access.claim() + assert.ok(delegations.length > 0) + assert.ok( + delegations.some( + (d) => + d.audience.did() === recordEgress.audience.did() && + d.issuer.did() === recordEgress.issuer.did() && + d.capabilities.some((c) => c.can === Usage.record.can) + ) + ) - const record = await alice.capability.usage.record( + // freewayService invokes usage/record and indicates the w3 as the provider + const record = await freewayService.capability.usage.record( { space: space.did(), resource: resource.link(), bytes: car.size, servedAt: new Date().toISOString(), }, - gateway.did() + w3.did(), + { proofs: delegations } ) assert.ok(record) From 3c2866dd443d38df22b4978fae01bbe2e3e10282 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Fri, 1 Nov 2024 15:14:44 -0300 Subject: [PATCH 4/7] fix: usage/record tests Signed-off-by: Felipe Forbeck --- packages/w3up-client/src/capability/usage.js | 2 +- .../w3up-client/test/capability/usage.test.js | 108 +++++++++++++++--- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/packages/w3up-client/src/capability/usage.js b/packages/w3up-client/src/capability/usage.js index 144b63799..e98b5f2e8 100644 --- a/packages/w3up-client/src/capability/usage.js +++ b/packages/w3up-client/src/capability/usage.js @@ -55,7 +55,7 @@ export class UsageClient extends Base { { provider, ...egressData }, { ...options } ) - /* c8 ignore next 5 */ + if (!out.ok) { throw new Error(`failed ${UsageCapabilities.record.can} invocation`, { cause: out.error, diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index b2c3a0728..936913327 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -3,19 +3,8 @@ import { Client } from '../../src/client.js' import * as Test from '../test.js' import { receiptsEndpoint } from '../helpers/utils.js' import { randomCAR } from '../helpers/random.js' -import { - freeway, - freewaySigner, - mallory, - w3, - w3Signer, -} from '../../../upload-api/test/helpers/utils.js' -import { - alice, - gateway, -} from '@web3-storage/capabilities/test/helpers/fixtures' +import { freewaySigner } from '../../../upload-api/test/helpers/utils.js' import { Usage } from '@web3-storage/capabilities' -import { claim } from '../../src/capability/access.js' import { Signer } from '@ucanto/principal/ed25519' export const UsageClient = Test.withContext({ @@ -86,12 +75,13 @@ export const UsageClient = Test.withContext({ record: { 'should record egress': async ( assert, - { connection, provisionsStorage } + { id: w3, signer: w3Signer, connection, provisionsStorage } ) => { // Creates a new agent using w3Signer as the principal const w3Service = new Client( await AgentData.create({ - principal: w3Signer, + // @ts-ignore + principal: w3, }), { // @ts-ignore @@ -105,6 +95,7 @@ export const UsageClient = Test.withContext({ const space = await w3Service.createSpace('test') const auth = await space.createAuthorization(w3Service) await w3Service.addSpace(auth) + await w3Service.setCurrentSpace(space.did()) // Then we setup a billing for this account await provisionsStorage.put({ @@ -135,7 +126,8 @@ export const UsageClient = Test.withContext({ // w3Service delegates ability to record usage to freewayService const recordEgress = await Usage.record.delegate({ issuer: w3Service.agent.issuer, - audience: freewayService, + audience: freewaySigner, + // @ts-ignore with: w3.did(), expiration: Infinity, }) @@ -165,12 +157,96 @@ export const UsageClient = Test.withContext({ bytes: car.size, servedAt: new Date().toISOString(), }, - w3.did(), + // @ts-ignore + w3.did(), // did:web:string { proofs: delegations } ) assert.ok(record) }, + 'should fail to record egress if the capability was not delegated': async ( + assert, + { id: w3, connection, provisionsStorage } + ) => { + // Creates a new agent using w3Signer as the principal + const w3Service = new Client( + await AgentData.create({ + // @ts-ignore + principal: w3, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + const space = await w3Service.createSpace('test') + const auth = await space.createAuthorization(w3Service) + await w3Service.addSpace(auth) + await w3Service.setCurrentSpace(space.did()) + + // Then we setup a billing for this account + await provisionsStorage.put({ + // @ts-expect-error + provider: connection.id.did(), + account: w3Service.agent.did(), + consumer: space.did(), + }) + + // Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: freewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // Random resource to record egress + const car = await randomCAR(128) + const resource = car.cid + + // w3Service creates a delegation to a random service + const recordEgress = await Usage.record.delegate({ + issuer: w3Service.agent.issuer, + audience: await Signer.generate(), + // @ts-ignore + with: w3.did(), + expiration: Infinity, + }) + + // FreewayService attempts to invoke usage/record without performing the delegation + try { + await freewayService.capability.usage.record( + { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + }, + // @ts-ignore + w3.did(), // did:web:string + { proofs: [recordEgress] } + ) + assert.fail('Expected an error due to missing delegation') + } catch (error) { + assert.ok( + // @ts-ignore + error.cause.message.startsWith( + 'Claim {"can":"usage/record"} is not authorized\n - Capability {"can":"usage/record","with":"did:web:test.web3.storage",' + ), + 'Error was thrown as expected' + ) + } + }, }, }) From 810a2bf98cabc21a675d4b31afcad32c1f3b08d2 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Mon, 4 Nov 2024 17:07:17 -0300 Subject: [PATCH 5/7] wip: rename usage/record capability --- packages/capabilities/src/space.js | 31 +++ packages/capabilities/src/types.ts | 8 +- packages/capabilities/src/usage.js | 21 +- .../test/capabilities/space.test.js | 132 +++++++++++++ .../test/capabilities/usage.test.js | 48 +---- .../capabilities/test/helpers/fixtures.js | 7 +- packages/upload-api/src/space.js | 4 +- .../upload-api/src/{usage => space}/record.js | 16 +- packages/upload-api/src/usage.js | 2 - packages/w3up-client/src/capability/space.js | 69 +++++++ packages/w3up-client/src/capability/usage.js | 67 ------- .../w3up-client/test/capability/space.test.js | 183 ++++++++++++++++++ .../w3up-client/test/capability/usage.test.js | 181 +---------------- 13 files changed, 439 insertions(+), 330 deletions(-) create mode 100644 packages/capabilities/test/capabilities/space.test.js rename packages/upload-api/src/{usage => space}/record.js (70%) diff --git a/packages/capabilities/src/space.js b/packages/capabilities/src/space.js index 704e2cf66..b9db95e58 100644 --- a/packages/capabilities/src/space.js +++ b/packages/capabilities/src/space.js @@ -63,3 +63,34 @@ export const allocate = capability({ } }, }) + +/** + * The capability grants permission for all operations or actions that fall under the "space/content" namespace. + * It can be derived from any of the `space/*` capability that has matching `with`. + */ +export const spaceContent = capability({ + can: 'space/content/*', + 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/*` capability that has matching `with`. + */ +export const recordEgress = spaceContent.derive({ + to: capability({ + can: 'space/content/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, + }), + derives: equalWith, +}) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 7704922e3..e2ff87044 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,7 +131,7 @@ export type UsageReport = InferInvokedCapability export type UsageReportSuccess = Record export type UsageReportFailure = Ucanto.Failure -export type EgressRecord = InferInvokedCapability +export type EgressRecord = InferInvokedCapability export type EgressRecordSuccess = Unit export type EgressRecordFailure = ConsumerNotFound | Ucanto.Failure @@ -276,8 +276,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 810aecb47..d80fb212d 100644 --- a/packages/capabilities/src/usage.js +++ b/packages/capabilities/src/usage.js @@ -1,5 +1,5 @@ import { capability, ok, Schema } from '@ucanto/validator' -import { and, equal, equalWith, ProviderDID, SpaceDID } from './utils.js' +import { and, equal, equalWith, SpaceDID } from './utils.js' /** * Capability can only be delegated (but not invoked) allowing audience to @@ -40,22 +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: ProviderDID, - nb: Schema.struct({ - /** DID of the space where the resource is served from. */ - space: SpaceDID, - /** 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/capabilities/space.test.js b/packages/capabilities/test/capabilities/space.test.js new file mode 100644 index 000000000..95c6974dc --- /dev/null +++ b/packages/capabilities/test/capabilities/space.test.js @@ -0,0 +1,132 @@ +import assert from 'assert' +import { access, DIDResolutionError, Schema } from '@ucanto/validator' +import { Verifier } from '@ucanto/principal' +import { + alice, + service as w3, + gateway, + readmeCID, + mallory as agent, + space, +} from '../helpers/fixtures.js' +import { validateAuthorization } from '../helpers/utils.js' +import { Access, Space } from '../../src/index.js' + +// const top = async () => +// Space.top.delegate({ +// issuer: account, +// audience: alice, +// with: account.did(), +// }) + +describe.skip('space capabilities', function () { + const resolveDIDKey = ( + /* @ts-ignore */ + k + ) => { + const didKey = [w3, gateway, alice] + .find((signer) => signer.did() === k) + ?.toDIDKey() + if (didKey) { + return Schema.ok(didKey) + } else { + return { error: new DIDResolutionError(k) } + } + } + + it('should delegate and invoke space/content/serve/egress/record', async () => { + const data = { + space: space.did(), + resource: readmeCID, + bytes: 100, + servedAt: 1714204800, + } + const auth = Access.authorize.invoke({ + issuer: agent, + audience: alice, + with: agent.did(), + nb: { + iss: 'did:mailto:web3.storage:test', + att: [{ can: '*' }], + }, + }) + + const resultA = await access(await auth.delegate(), { + capability: Access.authorize, + principal: Verifier, + authority: alice, + validateAuthorization, + }) + assert.ok(resultA.ok) + + // Agent delegates to Alice the ability to record egress + const aliceEgressRecordProof = await Space.recordEgress.delegate({ + issuer: agent, + audience: alice, + with: data.space, + expiration: Date.now() + 10e9, + }) + assert.ok(aliceEgressRecordProof) + + // Alice delegates to the Gateway the ability to record egress + const gatewayEgressRecordProof = await Space.recordEgress.delegate({ + issuer: alice, + audience: gateway, + with: data.space, + expiration: Date.now() + 10e9, + }) + + // Gateway invokes egress/record with the proof + const recordInvocation = Space.recordEgress.invoke({ + issuer: gateway, + audience: w3, + with: data.space, + nb: { ...data }, + proofs: [gatewayEgressRecordProof], + }) + + // W3 validates the delegation from Alice to Gateway + const delegation = await recordInvocation.delegate() + const result = await access(delegation, { + capability: Space.recordEgress, + principal: Verifier, + authority: w3, + validateAuthorization, + resolveDIDKey, + }) + + if (result.error) { + assert.fail(result.error.message) + } + + assert.deepEqual(result.ok.audience.did(), gateway.did()) + assert.equal(result.ok.capability.can, 'space/content/serve/egress/record') + assert.deepEqual(result.ok.capability.nb, { ...data }) + }) + + // it('usage/report can be derived from usage/*', async () => { + // const period = { from: 2, to: 3 } + // const report = Usage.report.invoke({ + // issuer: alice, + // audience: w3, + // with: account.did(), + // nb: { period }, + // proofs: [await Usage.usage()], + // }) + + // const result = await access(await report.delegate(), { + // capability: Usage.report, + // principal: Verifier, + // authority: w3, + // validateAuthorization, + // }) + + // if (result.error) { + // assert.fail(result.error.message) + // } + + // assert.deepEqual(result.ok.audience.did(), w3.did()) + // assert.equal(result.ok.capability.can, 'usage/report') + // assert.deepEqual(result.ok.capability.nb, { period }) + // }) +}) diff --git a/packages/capabilities/test/capabilities/usage.test.js b/packages/capabilities/test/capabilities/usage.test.js index 692f0f586..3d2d330dc 100644 --- a/packages/capabilities/test/capabilities/usage.test.js +++ b/packages/capabilities/test/capabilities/usage.test.js @@ -1,5 +1,5 @@ import assert from 'assert' -import { access, Schema } from '@ucanto/validator' +import { access } from '@ucanto/validator' import { Verifier } from '@ucanto/principal' import * as Usage from '../../src/usage.js' import * as Capability from '../../src/top.js' @@ -8,9 +8,6 @@ import { service as w3, mallory as account, bob, - gateway, - readmeCID, - mallory, } from '../helpers/fixtures.js' import { validateAuthorization } from '../helpers/utils.js' @@ -203,47 +200,4 @@ describe('usage capabilities', function () { }) }, /Expected value of type integer instead got 6\.6/) }) - - it('should delegate and invoke usage/record', async () => { - const data = { - space: mallory.did(), - resource: readmeCID, - bytes: 100, - servedAt: 1714204800, - } - - // W3 delegates ability to record usage to Gateway - const usageRecordDelegationProof = await Usage.record.delegate({ - issuer: w3, - audience: gateway, - with: w3.did(), - expiration: Infinity, - }) - - // Gateway invokes usage/record and indicates the w3 as the audience - const recordInvocation = Usage.record.invoke({ - issuer: gateway, - audience: w3, - with: gateway.did(), - nb: { ...data }, - proofs: [usageRecordDelegationProof], - }) - - // W3 validates the delegation from Gateway to itself - const result = await access(await recordInvocation.delegate(), { - capability: Usage.record, - principal: Verifier, - authority: w3, - validateAuthorization, - resolveDIDKey: () => Schema.ok(gateway.toDIDKey()), - }) - - if (result.error) { - assert.fail(result.error.message) - } - - assert.deepEqual(result.ok.audience.did(), w3.did()) - assert.equal(result.ok.capability.can, 'usage/record') - assert.deepEqual(result.ok.capability.nb, { ...data }) - }) }) diff --git a/packages/capabilities/test/helpers/fixtures.js b/packages/capabilities/test/helpers/fixtures.js index 6203ed661..504a17c16 100644 --- a/packages/capabilities/test/helpers/fixtures.js +++ b/packages/capabilities/test/helpers/fixtures.js @@ -39,4 +39,9 @@ export const readmeCID = parseLink( export const gateway = Signer.parse( 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' // random key -).withDID('did:web:dag.haus:freeway.com:test') +).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..c906e7294 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: { egress: { record: provideRecordEgress(ctx) } }, }) diff --git a/packages/upload-api/src/usage/record.js b/packages/upload-api/src/space/record.js similarity index 70% rename from packages/upload-api/src/usage/record.js rename to packages/upload-api/src/space/record.js index eae6bf323..a569df12f 100644 --- a/packages/upload-api/src/usage/record.js +++ b/packages/upload-api/src/space/record.js @@ -1,23 +1,23 @@ 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.recordEgress, (input) => recordEgress(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 recordEgress = async ({ capability, invocation }, context) => { const provider = /** @type {`did:web:${string}`} */ ( invocation.audience.did() ) const consumerResponse = await context.provisionsStorage.getConsumer( provider, - capability.nb.space + capability.with ) if (consumerResponse.error) { return consumerResponse @@ -25,7 +25,7 @@ const record = async ({ capability, invocation }, context) => { const consumer = consumerResponse.ok const res = await context.usageStorage.record( // The space which contains the resource that was served. - capability.nb.space, + capability.with, // The customer that is being billed for the egress traffic. consumer.customer, // CID of the resource that was served. 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/w3up-client/src/capability/space.js b/packages/w3up-client/src/capability/space.js index f37d31fae..df53b4dee 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] + */ + async recordEgress(egressData, options) { + const out = await recordEgress( + { agent: this.agent }, + { ...egressData }, + { ...options } + ) + + if (!out.ok) { + throw new Error( + `failed ${SpaceCapabilities.recordEgress.can} invocation`, + { + cause: out.error, + } + ) + } + + return 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] + * @returns {Promise>} + */ +export const recordEgress = async ( + { agent }, + { space, resource, bytes, servedAt }, + { nonce, proofs = [] } +) => { + const receipt = await agent.invokeAndExecute(SpaceCapabilities.recordEgress, { + 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 e98b5f2e8..19b53ba7c 100644 --- a/packages/w3up-client/src/capability/usage.js +++ b/packages/w3up-client/src/capability/usage.js @@ -31,39 +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 {object} egressData - * @param {import('../types.js').SpaceDID} egressData.space - * @param {API.UnknownLink} egressData.resource - * @param {number} egressData.bytes - * @param {string} egressData.servedAt - * @param {API.ProviderDID} provider - * @param {object} [options] - * @param {string} [options.nonce] - * @param {API.Delegation[]} [options.proofs] - */ - async record(egressData, provider, options) { - const out = await record( - { agent: this.agent }, - { provider, ...egressData }, - { ...options } - ) - - if (!out.ok) { - throw new Error(`failed ${UsageCapabilities.record.can} invocation`, { - cause: out.error, - }) - } - - return out.ok - } } /** @@ -94,37 +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.ProviderDID} egressData.provider - * @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 }, - { provider, space, resource, bytes, servedAt }, - { nonce, proofs = [] } -) => { - const receipt = await agent.invokeAndExecute(UsageCapabilities.record, { - with: provider, - proofs, - nonce, - nb: { - space, - 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..7d2fb6927 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 { freewaySigner } from '../../../upload-api/test/helpers/utils.js' +import { randomCAR } from '../helpers/random.js' export const SpaceClient = Test.withContext({ info: { @@ -37,6 +40,186 @@ export const SpaceClient = Test.withContext({ assert.deepEqual(info.providers, [connection.id.did()]) }, }, + record: { + 'should record egress': 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. Allow Alice Agent to record egress + const recordEgressAuth = await space.createAuthorization(alice, { + access: { [Space.recordEgress.can]: {} }, + expiration: expiration, + }) + await alice.addProof(recordEgressAuth) + const proofs = await alice.proofs() + assert.ok(proofs.length >= 2) + + // 3. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: freewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 4. Alice delegates to the Gateway the ability to record egress + const recordEgressGatewayDelegation = await Space.recordEgress.delegate({ + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + }) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [recordEgressGatewayDelegation], + }) + assert.ok(resultDelegation2.ok) + + // 5. freewayService claims the delegation + const freewayDelegations = await freewayService.capability.access.claim() + assert.ok(freewayDelegations.length > 0) + assert.ok( + freewayDelegations.some( + (d) => + d.audience.did() === recordEgressGatewayDelegation.audience.did() && + d.issuer.did() === recordEgressGatewayDelegation.issuer.did() && + d.capabilities.some((c) => c.can === Space.recordEgress.can) + ) + ) + + // 6. Create a random resource to record egress + const car = await randomCAR(128) + const resource = await alice.capability.store.add(car) + assert.ok(resource) + + // 7. freewayService invokes egress/record + try { + const log = await freewayService.capability.space.recordEgress( + { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + }, + { proofs: [recordEgressGatewayDelegation] } + ) + + assert.deepEqual(log, {}) + } catch (error) { + // @ts-ignore + assert.fail(error.cause.message || error) + } + }, + // 'should fail to record egress if the capability was not delegated': async ( + // assert, + // { id: w3, connection, provisionsStorage } + // ) => { + // // Creates a new agent using w3Signer as the principal + // const w3Service = new Client( + // await AgentData.create({ + // // @ts-ignore + // principal: w3, + // }), + // { + // // @ts-ignore + // serviceConf: { + // access: connection, + // upload: connection, + // }, + // } + // ) + + // const space = await w3Service.createSpace('test') + // const auth = await space.createAuthorization(w3Service) + // await w3Service.addSpace(auth) + // await w3Service.setCurrentSpace(space.did()) + + // // Then we setup a billing for this account + // await provisionsStorage.put({ + // // @ts-expect-error + // provider: connection.id.did(), + // account: w3Service.agent.did(), + // consumer: space.did(), + // }) + + // // Creates a new agent using freewaySigner as the principal + // const freewayService = new Client( + // await AgentData.create({ + // principal: freewaySigner, + // }), + // { + // // @ts-ignore + // serviceConf: { + // access: connection, + // upload: connection, + // }, + // } + // ) + + // // Random resource to record egress + // const car = await randomCAR(128) + // const resource = car.cid + + // // w3Service creates a delegation to a random service + // const recordEgress = await Space.record.delegate({ + // issuer: w3Service.agent.issuer, + // audience: await ed25519.Signer.generate(), + // // @ts-ignore + // with: w3.did(), + // expiration: Infinity, + // }) + + // // FreewayService attempts to invoke egress/record without performing the delegation + // try { + // await freewayService.capability.space.record( + // { + // space: space.did(), + // resource: resource.link(), + // bytes: car.size, + // servedAt: new Date().toISOString(), + // }, + // { proofs: [recordEgress] } + // ) + // assert.fail('Expected an error due to missing delegation') + // } catch (error) { + // assert.ok( + // // @ts-ignore + // error.cause.message.startsWith( + // 'Claim {"can":"usage/record"} is not authorized\n - Capability {"can":"usage/record","with":"did:web:test.web3.storage",' + // ), + // 'Error was thrown as expected' + // ) + // } + // }, + }, }) Test.test({ SpaceClient }) diff --git a/packages/w3up-client/test/capability/usage.test.js b/packages/w3up-client/test/capability/usage.test.js index 936913327..caadf5ede 100644 --- a/packages/w3up-client/test/capability/usage.test.js +++ b/packages/w3up-client/test/capability/usage.test.js @@ -2,10 +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' -import { freewaySigner } from '../../../upload-api/test/helpers/utils.js' -import { Usage } from '@web3-storage/capabilities' -import { Signer } from '@ucanto/principal/ed25519' export const UsageClient = Test.withContext({ report: { @@ -13,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: { @@ -72,182 +69,6 @@ export const UsageClient = Test.withContext({ assert.deepEqual(report, {}) }, }, - record: { - 'should record egress': async ( - assert, - { id: w3, signer: w3Signer, connection, provisionsStorage } - ) => { - // Creates a new agent using w3Signer as the principal - const w3Service = new Client( - await AgentData.create({ - // @ts-ignore - principal: w3, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - } - ) - - const space = await w3Service.createSpace('test') - const auth = await space.createAuthorization(w3Service) - await w3Service.addSpace(auth) - await w3Service.setCurrentSpace(space.did()) - - // Then we setup a billing for this account - await provisionsStorage.put({ - // @ts-expect-error - provider: connection.id.did(), - account: w3Service.agent.did(), - consumer: space.did(), - }) - - // Creates a new agent using freewaySigner as the principal - const freewayService = new Client( - await AgentData.create({ - principal: freewaySigner, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - } - ) - - // Random resource to record egress - const car = await randomCAR(128) - const resource = car.cid - - // w3Service delegates ability to record usage to freewayService - const recordEgress = await Usage.record.delegate({ - issuer: w3Service.agent.issuer, - audience: freewaySigner, - // @ts-ignore - with: w3.did(), - expiration: Infinity, - }) - - const delegationResult = await w3Service.capability.access.delegate({ - delegations: [recordEgress], - }) - assert.ok(delegationResult.ok) - - // freewayService claims the delegation - const delegations = await freewayService.capability.access.claim() - assert.ok(delegations.length > 0) - assert.ok( - delegations.some( - (d) => - d.audience.did() === recordEgress.audience.did() && - d.issuer.did() === recordEgress.issuer.did() && - d.capabilities.some((c) => c.can === Usage.record.can) - ) - ) - - // freewayService invokes usage/record and indicates the w3 as the provider - const record = await freewayService.capability.usage.record( - { - space: space.did(), - resource: resource.link(), - bytes: car.size, - servedAt: new Date().toISOString(), - }, - // @ts-ignore - w3.did(), // did:web:string - { proofs: delegations } - ) - - assert.ok(record) - }, - 'should fail to record egress if the capability was not delegated': async ( - assert, - { id: w3, connection, provisionsStorage } - ) => { - // Creates a new agent using w3Signer as the principal - const w3Service = new Client( - await AgentData.create({ - // @ts-ignore - principal: w3, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - } - ) - - const space = await w3Service.createSpace('test') - const auth = await space.createAuthorization(w3Service) - await w3Service.addSpace(auth) - await w3Service.setCurrentSpace(space.did()) - - // Then we setup a billing for this account - await provisionsStorage.put({ - // @ts-expect-error - provider: connection.id.did(), - account: w3Service.agent.did(), - consumer: space.did(), - }) - - // Creates a new agent using freewaySigner as the principal - const freewayService = new Client( - await AgentData.create({ - principal: freewaySigner, - }), - { - // @ts-ignore - serviceConf: { - access: connection, - upload: connection, - }, - } - ) - - // Random resource to record egress - const car = await randomCAR(128) - const resource = car.cid - - // w3Service creates a delegation to a random service - const recordEgress = await Usage.record.delegate({ - issuer: w3Service.agent.issuer, - audience: await Signer.generate(), - // @ts-ignore - with: w3.did(), - expiration: Infinity, - }) - - // FreewayService attempts to invoke usage/record without performing the delegation - try { - await freewayService.capability.usage.record( - { - space: space.did(), - resource: resource.link(), - bytes: car.size, - servedAt: new Date().toISOString(), - }, - // @ts-ignore - w3.did(), // did:web:string - { proofs: [recordEgress] } - ) - assert.fail('Expected an error due to missing delegation') - } catch (error) { - assert.ok( - // @ts-ignore - error.cause.message.startsWith( - 'Claim {"can":"usage/record"} is not authorized\n - Capability {"can":"usage/record","with":"did:web:test.web3.storage",' - ), - 'Error was thrown as expected' - ) - } - }, - }, }) Test.test({ UsageClient }) From abcbb87d555836efaf729db06cce3ef6e08dcb34 Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Tue, 5 Nov 2024 13:19:46 -0300 Subject: [PATCH 6/7] fix(usage/record) - rename capability --- packages/capabilities/src/space.js | 32 +- packages/capabilities/src/types.ts | 8 +- .../test/capabilities/space.test.js | 132 ---- packages/upload-api/src/space.js | 2 +- packages/w3up-client/src/capability/space.js | 4 +- .../w3up-client/test/capability/space.test.js | 564 ++++++++++++++---- 6 files changed, 486 insertions(+), 256 deletions(-) delete mode 100644 packages/capabilities/test/capabilities/space.test.js diff --git a/packages/capabilities/src/space.js b/packages/capabilities/src/space.js index b9db95e58..546d947f9 100644 --- a/packages/capabilities/src/space.js +++ b/packages/capabilities/src/space.js @@ -65,32 +65,30 @@ export const allocate = capability({ }) /** - * The capability grants permission for all operations or actions that fall under the "space/content" namespace. + * 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 spaceContent = capability({ - can: 'space/content/*', + +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/*` capability that has matching `with`. + * It can be derived from any of the `space/content/serve/*` capability that has matching `with`. */ -export const recordEgress = spaceContent.derive({ - to: capability({ - can: 'space/content/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, +export const recordEgress = 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 e2ff87044..499b2ccca 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -132,7 +132,13 @@ export type UsageReportSuccess = Record export type UsageReportFailure = Ucanto.Failure export type EgressRecord = InferInvokedCapability -export type EgressRecordSuccess = Unit +export type EgressRecordSuccess = { + space: SpaceDID + resource: UnknownLink + bytes: number + servedAt: ISO8601Date + cause: UnknownLink +} export type EgressRecordFailure = ConsumerNotFound | Ucanto.Failure export interface UsageData { diff --git a/packages/capabilities/test/capabilities/space.test.js b/packages/capabilities/test/capabilities/space.test.js deleted file mode 100644 index 95c6974dc..000000000 --- a/packages/capabilities/test/capabilities/space.test.js +++ /dev/null @@ -1,132 +0,0 @@ -import assert from 'assert' -import { access, DIDResolutionError, Schema } from '@ucanto/validator' -import { Verifier } from '@ucanto/principal' -import { - alice, - service as w3, - gateway, - readmeCID, - mallory as agent, - space, -} from '../helpers/fixtures.js' -import { validateAuthorization } from '../helpers/utils.js' -import { Access, Space } from '../../src/index.js' - -// const top = async () => -// Space.top.delegate({ -// issuer: account, -// audience: alice, -// with: account.did(), -// }) - -describe.skip('space capabilities', function () { - const resolveDIDKey = ( - /* @ts-ignore */ - k - ) => { - const didKey = [w3, gateway, alice] - .find((signer) => signer.did() === k) - ?.toDIDKey() - if (didKey) { - return Schema.ok(didKey) - } else { - return { error: new DIDResolutionError(k) } - } - } - - it('should delegate and invoke space/content/serve/egress/record', async () => { - const data = { - space: space.did(), - resource: readmeCID, - bytes: 100, - servedAt: 1714204800, - } - const auth = Access.authorize.invoke({ - issuer: agent, - audience: alice, - with: agent.did(), - nb: { - iss: 'did:mailto:web3.storage:test', - att: [{ can: '*' }], - }, - }) - - const resultA = await access(await auth.delegate(), { - capability: Access.authorize, - principal: Verifier, - authority: alice, - validateAuthorization, - }) - assert.ok(resultA.ok) - - // Agent delegates to Alice the ability to record egress - const aliceEgressRecordProof = await Space.recordEgress.delegate({ - issuer: agent, - audience: alice, - with: data.space, - expiration: Date.now() + 10e9, - }) - assert.ok(aliceEgressRecordProof) - - // Alice delegates to the Gateway the ability to record egress - const gatewayEgressRecordProof = await Space.recordEgress.delegate({ - issuer: alice, - audience: gateway, - with: data.space, - expiration: Date.now() + 10e9, - }) - - // Gateway invokes egress/record with the proof - const recordInvocation = Space.recordEgress.invoke({ - issuer: gateway, - audience: w3, - with: data.space, - nb: { ...data }, - proofs: [gatewayEgressRecordProof], - }) - - // W3 validates the delegation from Alice to Gateway - const delegation = await recordInvocation.delegate() - const result = await access(delegation, { - capability: Space.recordEgress, - principal: Verifier, - authority: w3, - validateAuthorization, - resolveDIDKey, - }) - - if (result.error) { - assert.fail(result.error.message) - } - - assert.deepEqual(result.ok.audience.did(), gateway.did()) - assert.equal(result.ok.capability.can, 'space/content/serve/egress/record') - assert.deepEqual(result.ok.capability.nb, { ...data }) - }) - - // it('usage/report can be derived from usage/*', async () => { - // const period = { from: 2, to: 3 } - // const report = Usage.report.invoke({ - // issuer: alice, - // audience: w3, - // with: account.did(), - // nb: { period }, - // proofs: [await Usage.usage()], - // }) - - // const result = await access(await report.delegate(), { - // capability: Usage.report, - // principal: Verifier, - // authority: w3, - // validateAuthorization, - // }) - - // if (result.error) { - // assert.fail(result.error.message) - // } - - // assert.deepEqual(result.ok.audience.did(), w3.did()) - // assert.equal(result.ok.capability.can, 'usage/report') - // assert.deepEqual(result.ok.capability.nb, { period }) - // }) -}) diff --git a/packages/upload-api/src/space.js b/packages/upload-api/src/space.js index c906e7294..d45d6bd06 100644 --- a/packages/upload-api/src/space.js +++ b/packages/upload-api/src/space.js @@ -14,5 +14,5 @@ export const createService = (ctx) => ({ info: Provider.provide(Space.info, (input) => info(input, ctx)), blob: createBlobService(ctx), index: createIndexService(ctx), - content: { egress: { record: provideRecordEgress(ctx) } }, + content: { serve: { egress: { record: provideRecordEgress(ctx) } } }, }) diff --git a/packages/w3up-client/src/capability/space.js b/packages/w3up-client/src/capability/space.js index df53b4dee..93e6749ec 100644 --- a/packages/w3up-client/src/capability/space.js +++ b/packages/w3up-client/src/capability/space.js @@ -35,6 +35,7 @@ export class SpaceClient extends Base { * @param {object} [options] * @param {string} [options.nonce] * @param {API.Delegation[]} [options.proofs] + * @returns {Promise} */ async recordEgress(egressData, options) { const out = await recordEgress( @@ -52,7 +53,7 @@ export class SpaceClient extends Base { ) } - return out.ok + return /** @type {API.EgressRecordSuccess} */ (out.ok) } } @@ -68,7 +69,6 @@ export class SpaceClient extends Base { * @param {object} options * @param {string} [options.nonce] * @param {API.Delegation[]} [options.proofs] - * @returns {Promise>} */ export const recordEgress = async ( { agent }, diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index 7d2fb6927..ef8cd7748 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -41,7 +41,7 @@ export const SpaceClient = Test.withContext({ }, }, record: { - 'should record egress': async ( + 'should record egress if the capability is derived from *': async ( assert, { id: w3, connection, provisionsStorage } ) => { @@ -67,16 +67,117 @@ export const SpaceClient = Test.withContext({ consumer: space.did(), }) - // 2. Allow Alice Agent to record egress - const recordEgressAuth = await space.createAuthorization(alice, { - access: { [Space.recordEgress.can]: {} }, + // 2. Creates a new agent using freewaySigner as the principal + const freewayService = new Client( + await AgentData.create({ + principal: freewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress + const recordEgressGatewayDelegation = await Space.recordEgress.delegate({ + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), expiration: expiration, + proofs: await alice.proofs(), + }) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [recordEgressGatewayDelegation], }) - await alice.addProof(recordEgressAuth) - const proofs = await alice.proofs() - assert.ok(proofs.length >= 2) + assert.ok(resultDelegation2.ok) - // 3. Creates a new agent using freewaySigner as the principal + // 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.recordEgress.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.recordEgress( + 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: freewaySigner, @@ -90,135 +191,392 @@ export const SpaceClient = Test.withContext({ } ) - // 4. Alice delegates to the Gateway the ability to record egress - const recordEgressGatewayDelegation = await Space.recordEgress.delegate({ + // 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: [recordEgressGatewayDelegation], + delegations: [spaceAccessGatewayDelegation], }) assert.ok(resultDelegation2.ok) - // 5. freewayService claims the delegation + // 4. freewayService claims the delegation const freewayDelegations = await freewayService.capability.access.claim() assert.ok(freewayDelegations.length > 0) assert.ok( freewayDelegations.some( (d) => - d.audience.did() === recordEgressGatewayDelegation.audience.did() && - d.issuer.did() === recordEgressGatewayDelegation.issuer.did() && - d.capabilities.some((c) => c.can === Space.recordEgress.can) + d.issuer.did() === alice.did() && + d.audience.did() === freewayService.did() && + d.capabilities.some( + (c) => c.can === Space.top.can && c.with === space.did() + ) ) ) - // 6. Create a random resource to record egress + // 5. Create a random resource to record egress const car = await randomCAR(128) const resource = await alice.capability.store.add(car) assert.ok(resource) - // 7. freewayService invokes egress/record + // 6. freewayService invokes egress/record try { - const log = await freewayService.capability.space.recordEgress( + const egressData = { + space: space.did(), + resource: resource.link(), + bytes: car.size, + servedAt: new Date().toISOString(), + } + const egressRecord = await freewayService.capability.space.recordEgress( + 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: freewaySigner, + }), + { + // @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.recordEgress(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: freewaySigner, + }), { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, + }, + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress + const recordEgressGatewayDelegation = await Space.recordEgress.delegate( + { + issuer: alice.agent.issuer, + audience: freewayService, + with: space.did(), + expiration: expiration, + proofs: await alice.proofs(), + } + ) + + const resultDelegation2 = await alice.capability.access.delegate({ + delegations: [recordEgressGatewayDelegation], + }) + 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.recordEgress.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.recordEgress(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: freewaySigner, + }), + { + // @ts-ignore + serviceConf: { + access: connection, + upload: connection, }, - { proofs: [recordEgressGatewayDelegation] } + } + ) + + // 3. Alice delegates to the Gateway the ability to record egress but without proofs + const recordEgressGatewayDelegation = await Space.recordEgress.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: [recordEgressGatewayDelegation], + }) + 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.recordEgress.can && c.with === space.did() + ) ) + ) - assert.deepEqual(log, {}) + // 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.recordEgress( + { + 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) { - // @ts-ignore - assert.fail(error.cause.message || error) + assert.equal( + // @ts-ignore + error.message, + `failed ${Space.recordEgress.can} invocation`, + 'error message should be the same' + ) } }, - // 'should fail to record egress if the capability was not delegated': async ( - // assert, - // { id: w3, connection, provisionsStorage } - // ) => { - // // Creates a new agent using w3Signer as the principal - // const w3Service = new Client( - // await AgentData.create({ - // // @ts-ignore - // principal: w3, - // }), - // { - // // @ts-ignore - // serviceConf: { - // access: connection, - // upload: connection, - // }, - // } - // ) - - // const space = await w3Service.createSpace('test') - // const auth = await space.createAuthorization(w3Service) - // await w3Service.addSpace(auth) - // await w3Service.setCurrentSpace(space.did()) - - // // Then we setup a billing for this account - // await provisionsStorage.put({ - // // @ts-expect-error - // provider: connection.id.did(), - // account: w3Service.agent.did(), - // consumer: space.did(), - // }) - - // // Creates a new agent using freewaySigner as the principal - // const freewayService = new Client( - // await AgentData.create({ - // principal: freewaySigner, - // }), - // { - // // @ts-ignore - // serviceConf: { - // access: connection, - // upload: connection, - // }, - // } - // ) - - // // Random resource to record egress - // const car = await randomCAR(128) - // const resource = car.cid - - // // w3Service creates a delegation to a random service - // const recordEgress = await Space.record.delegate({ - // issuer: w3Service.agent.issuer, - // audience: await ed25519.Signer.generate(), - // // @ts-ignore - // with: w3.did(), - // expiration: Infinity, - // }) - - // // FreewayService attempts to invoke egress/record without performing the delegation - // try { - // await freewayService.capability.space.record( - // { - // space: space.did(), - // resource: resource.link(), - // bytes: car.size, - // servedAt: new Date().toISOString(), - // }, - // { proofs: [recordEgress] } - // ) - // assert.fail('Expected an error due to missing delegation') - // } catch (error) { - // assert.ok( - // // @ts-ignore - // error.cause.message.startsWith( - // 'Claim {"can":"usage/record"} is not authorized\n - Capability {"can":"usage/record","with":"did:web:test.web3.storage",' - // ), - // 'Error was thrown as expected' - // ) - // } - // }, }, }) From aca951795099a5022b5b40f33fe495753f2ba73e Mon Sep 17 00:00:00 2001 From: Felipe Forbeck Date: Tue, 5 Nov 2024 16:09:13 -0300 Subject: [PATCH 7/7] applied suggested changes --- packages/capabilities/src/space.js | 2 +- packages/capabilities/src/types.ts | 2 +- packages/upload-api/src/space/record.js | 6 +-- packages/upload-api/test/helpers/utils.js | 4 +- packages/w3up-client/src/capability/space.js | 10 ++--- .../w3up-client/test/capability/space.test.js | 42 +++++++++---------- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/capabilities/src/space.js b/packages/capabilities/src/space.js index 546d947f9..54f48cb7c 100644 --- a/packages/capabilities/src/space.js +++ b/packages/capabilities/src/space.js @@ -79,7 +79,7 @@ export const contentServe = capability({ * 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 recordEgress = capability({ +export const egressRecord = capability({ can: 'space/content/serve/egress/record', with: SpaceDID, nb: Schema.struct({ diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 499b2ccca..f3d48c6af 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -131,7 +131,7 @@ export type UsageReport = InferInvokedCapability export type UsageReportSuccess = Record export type UsageReportFailure = Ucanto.Failure -export type EgressRecord = InferInvokedCapability +export type EgressRecord = InferInvokedCapability export type EgressRecordSuccess = { space: SpaceDID resource: UnknownLink diff --git a/packages/upload-api/src/space/record.js b/packages/upload-api/src/space/record.js index a569df12f..616844d82 100644 --- a/packages/upload-api/src/space/record.js +++ b/packages/upload-api/src/space/record.js @@ -4,14 +4,14 @@ import { Space } from '@web3-storage/capabilities' /** @param {API.SpaceServiceContext & API.UsageServiceContext} context */ export const provide = (context) => - Provider.provide(Space.recordEgress, (input) => recordEgress(input, context)) + Provider.provide(Space.egressRecord, (input) => egressRecord(input, context)) /** - * @param {API.Input} input + * @param {API.Input} input * @param {API.SpaceServiceContext & API.UsageServiceContext} context * @returns {Promise>} */ -const recordEgress = 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/test/helpers/utils.js b/packages/upload-api/test/helpers/utils.js index 0f198f264..91e6c766e 100644 --- a/packages/upload-api/test/helpers/utils.js +++ b/packages/upload-api/test/helpers/utils.js @@ -42,10 +42,10 @@ export const w3Signer = ed25519.parse( ) export const w3 = w3Signer.withDID('did:web:test.web3.storage') -export const freewaySigner = ed25519.parse( +export const gatewaySigner = ed25519.parse( 'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' ) -export const freeway = freewaySigner.withDID('did:web:freeway.web3.storage') +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 93e6749ec..9f7d9905b 100644 --- a/packages/w3up-client/src/capability/space.js +++ b/packages/w3up-client/src/capability/space.js @@ -37,8 +37,8 @@ export class SpaceClient extends Base { * @param {API.Delegation[]} [options.proofs] * @returns {Promise} */ - async recordEgress(egressData, options) { - const out = await recordEgress( + async egressRecord(egressData, options) { + const out = await egressRecord( { agent: this.agent }, { ...egressData }, { ...options } @@ -46,7 +46,7 @@ export class SpaceClient extends Base { if (!out.ok) { throw new Error( - `failed ${SpaceCapabilities.recordEgress.can} invocation`, + `failed ${SpaceCapabilities.egressRecord.can} invocation`, { cause: out.error, } @@ -70,12 +70,12 @@ export class SpaceClient extends Base { * @param {string} [options.nonce] * @param {API.Delegation[]} [options.proofs] */ -export const recordEgress = async ( +export const egressRecord = async ( { agent }, { space, resource, bytes, servedAt }, { nonce, proofs = [] } ) => { - const receipt = await agent.invokeAndExecute(SpaceCapabilities.recordEgress, { + const receipt = await agent.invokeAndExecute(SpaceCapabilities.egressRecord, { with: space, proofs, nonce, diff --git a/packages/w3up-client/test/capability/space.test.js b/packages/w3up-client/test/capability/space.test.js index ef8cd7748..b288b8afc 100644 --- a/packages/w3up-client/test/capability/space.test.js +++ b/packages/w3up-client/test/capability/space.test.js @@ -2,7 +2,7 @@ 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 { freewaySigner } from '../../../upload-api/test/helpers/utils.js' +import { gatewaySigner } from '../../../upload-api/test/helpers/utils.js' import { randomCAR } from '../helpers/random.js' export const SpaceClient = Test.withContext({ @@ -70,7 +70,7 @@ export const SpaceClient = Test.withContext({ // 2. Creates a new agent using freewaySigner as the principal const freewayService = new Client( await AgentData.create({ - principal: freewaySigner, + principal: gatewaySigner, }), { // @ts-ignore @@ -82,7 +82,7 @@ export const SpaceClient = Test.withContext({ ) // 3. Alice delegates to the Gateway the ability to record egress - const recordEgressGatewayDelegation = await Space.recordEgress.delegate({ + const egressRecordGatewayDelegation = await Space.egressRecord.delegate({ issuer: alice.agent.issuer, audience: freewayService, with: space.did(), @@ -91,7 +91,7 @@ export const SpaceClient = Test.withContext({ }) const resultDelegation2 = await alice.capability.access.delegate({ - delegations: [recordEgressGatewayDelegation], + delegations: [egressRecordGatewayDelegation], }) assert.ok(resultDelegation2.ok) @@ -104,7 +104,7 @@ export const SpaceClient = Test.withContext({ d.issuer.did() === alice.did() && d.audience.did() === freewayService.did() && d.capabilities.some( - (c) => c.can === Space.recordEgress.can && c.with === space.did() + (c) => c.can === Space.egressRecord.can && c.with === space.did() ) ) ) @@ -122,7 +122,7 @@ export const SpaceClient = Test.withContext({ bytes: car.size, servedAt: new Date().toISOString(), } - const egressRecord = await freewayService.capability.space.recordEgress( + const egressRecord = await freewayService.capability.space.egressRecord( egressData, { proofs: await freewayService.proofs(), @@ -180,7 +180,7 @@ export const SpaceClient = Test.withContext({ // 2. Creates a new agent using freewaySigner as the principal const freewayService = new Client( await AgentData.create({ - principal: freewaySigner, + principal: gatewaySigner, }), { // @ts-ignore @@ -232,7 +232,7 @@ export const SpaceClient = Test.withContext({ bytes: car.size, servedAt: new Date().toISOString(), } - const egressRecord = await freewayService.capability.space.recordEgress( + const egressRecord = await freewayService.capability.space.egressRecord( egressData, { proofs: await freewayService.proofs(), @@ -290,7 +290,7 @@ export const SpaceClient = Test.withContext({ // 2. Creates a new agent using freewaySigner as the principal const freewayService = new Client( await AgentData.create({ - principal: freewaySigner, + principal: gatewaySigner, }), { // @ts-ignore @@ -347,7 +347,7 @@ export const SpaceClient = Test.withContext({ servedAt: new Date().toISOString(), } const egressRecord = - await freewayService.capability.space.recordEgress(egressData, { + await freewayService.capability.space.egressRecord(egressData, { proofs: await freewayService.proofs(), }) assert.ok(egressRecord, 'egressRecord should be returned') @@ -402,7 +402,7 @@ export const SpaceClient = Test.withContext({ // 2. Creates a new agent using freewaySigner as the principal const freewayService = new Client( await AgentData.create({ - principal: freewaySigner, + principal: gatewaySigner, }), { // @ts-ignore @@ -414,7 +414,7 @@ export const SpaceClient = Test.withContext({ ) // 3. Alice delegates to the Gateway the ability to record egress - const recordEgressGatewayDelegation = await Space.recordEgress.delegate( + const egressRecordGatewayDelegation = await Space.egressRecord.delegate( { issuer: alice.agent.issuer, audience: freewayService, @@ -425,7 +425,7 @@ export const SpaceClient = Test.withContext({ ) const resultDelegation2 = await alice.capability.access.delegate({ - delegations: [recordEgressGatewayDelegation], + delegations: [egressRecordGatewayDelegation], }) assert.ok(resultDelegation2.ok) @@ -440,7 +440,7 @@ export const SpaceClient = Test.withContext({ d.audience.did() === freewayService.did() && d.capabilities.some( (c) => - c.can === Space.recordEgress.can && c.with === space.did() + c.can === Space.egressRecord.can && c.with === space.did() ) ) ) @@ -459,7 +459,7 @@ export const SpaceClient = Test.withContext({ servedAt: new Date().toISOString(), } const egressRecord = - await freewayService.capability.space.recordEgress(egressData, { + await freewayService.capability.space.egressRecord(egressData, { proofs: await freewayService.proofs(), }) assert.ok(egressRecord, 'egressRecord should be returned') @@ -514,7 +514,7 @@ export const SpaceClient = Test.withContext({ // 2. Creates a new agent using freewaySigner as the principal const freewayService = new Client( await AgentData.create({ - principal: freewaySigner, + principal: gatewaySigner, }), { // @ts-ignore @@ -526,7 +526,7 @@ export const SpaceClient = Test.withContext({ ) // 3. Alice delegates to the Gateway the ability to record egress but without proofs - const recordEgressGatewayDelegation = await Space.recordEgress.delegate({ + const egressRecordGatewayDelegation = await Space.egressRecord.delegate({ issuer: alice.agent.issuer, audience: freewayService, with: space.did(), @@ -535,7 +535,7 @@ export const SpaceClient = Test.withContext({ }) const resultDelegation2 = await alice.capability.access.delegate({ - delegations: [recordEgressGatewayDelegation], + delegations: [egressRecordGatewayDelegation], }) assert.ok(resultDelegation2.ok) @@ -548,7 +548,7 @@ export const SpaceClient = Test.withContext({ d.issuer.did() === alice.did() && d.audience.did() === freewayService.did() && d.capabilities.some( - (c) => c.can === Space.recordEgress.can && c.with === space.did() + (c) => c.can === Space.egressRecord.can && c.with === space.did() ) ) ) @@ -558,7 +558,7 @@ export const SpaceClient = Test.withContext({ // 6. FreewayService attempts to invoke egress/record without having the delegation try { - await freewayService.capability.space.recordEgress( + await freewayService.capability.space.egressRecord( { space: space.did(), resource: car.cid.link(), @@ -572,7 +572,7 @@ export const SpaceClient = Test.withContext({ assert.equal( // @ts-ignore error.message, - `failed ${Space.recordEgress.can} invocation`, + `failed ${Space.egressRecord.can} invocation`, 'error message should be the same' ) }