Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(egress/record): rename capability #1572

Merged
merged 7 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/capabilities/src/usage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { capability, ok, Schema } from '@ucanto/validator'
import { and, equal, equalWith, SpaceDID } from './utils.js'
import { and, equal, equalWith, ProviderDID, SpaceDID } from './utils.js'

/**
* Capability can only be delegated (but not invoked) allowing audience to
Expand Down Expand Up @@ -46,8 +46,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. */
Expand Down
48 changes: 47 additions & 1 deletion packages/capabilities/test/capabilities/usage.test.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -200,4 +203,47 @@ 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()),
fforbeck marked this conversation as resolved.
Show resolved Hide resolved
})

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 })
})
})
4 changes: 4 additions & 0 deletions packages/capabilities/test/helpers/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ 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:dag.haus:freeway.com:test')
4 changes: 2 additions & 2 deletions packages/upload-api/src/usage/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ const record = async ({ capability, invocation }, context) => {
)
const consumerResponse = await context.provisionsStorage.getConsumer(
provider,
capability.with
capability.nb.space
)
if (consumerResponse.error) {
return consumerResponse
}
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.
Expand Down
14 changes: 9 additions & 5 deletions packages/upload-api/test/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
fforbeck marked this conversation as resolved.
Show resolved Hide resolved
fforbeck marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates a server for the given service.
Expand Down
16 changes: 10 additions & 6 deletions packages/w3up-client/src/capability/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,23 @@ 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]
* @param {API.Delegation[]} [options.proofs]
*/
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 */

if (!out.ok) {
throw new Error(`failed ${UsageCapabilities.record.can} invocation`, {
cause: out.error,
Expand Down Expand Up @@ -98,6 +100,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
Expand All @@ -109,14 +112,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),
Expand Down
176 changes: 157 additions & 19 deletions packages/w3up-client/test/capability/usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ 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: {
Expand Down Expand Up @@ -72,42 +75,177 @@ export const UsageClient = Test.withContext({
record: {
'should record egress': async (
assert,
{ connection, provisionsStorage }
{ id: w3, signer: w3Signer, connection, provisionsStorage }
) => {
const alice = new Client(await AgentData.create(), {
// 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
serviceConf: {
access: connection,
upload: connection,
},
with: w3.did(),
expiration: Infinity,
})

const space = await alice.createSpace('test')
const auth = await space.createAuthorization(alice)
await alice.addSpace(auth)
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: 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)

const record = await alice.capability.usage.record(space.did(), {
resource: resource.link(),
bytes: car.size,
servedAt: new Date().toISOString(),
// 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,
})

assert.ok(record)
// 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'
)
}
},
},
})
Expand Down
Loading