Skip to content

Commit

Permalink
fix(egress/record): rename capability (#1572)
Browse files Browse the repository at this point in the history
## Update to `usage/record` Capability

### Overview

This PR restructures the `usage/record` capability, moving it under the
`Space` namespace instead of `Usage`. As part of this change, the
`usage/record` definition has been renamed to
`space/content/serve/egress/record`, and a new top-level capability,
`space/content/serve/*`, has been introduced.

### Key Changes

- **Namespace Update**: The `usage/record` capability now resides under
the `Space` namespace.
- **New Naming Convention**:
- `space/content/serve/egress/record`: This capability records egress
for all served data.
- `space/content/serve/*`: New top-level capability, representing
general serve actions within the `Space.contentServe` namespace.

---------

Signed-off-by: Felipe Forbeck <felipe.forbeck@gmail.com>
  • Loading branch information
fforbeck authored Nov 5, 2024
1 parent b43804c commit d28691c
Show file tree
Hide file tree
Showing 12 changed files with 678 additions and 141 deletions.
29 changes: 29 additions & 0 deletions packages/capabilities/src/space.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,32 @@ export const allocate = capability({
}
},
})

/**
* The capability grants permission for all content serve operations that fall under the "space/content/serve" namespace.
* It can be derived from any of the `space/*` capability that has matching `with`.
*/

export const contentServe = capability({
can: 'space/content/serve/*',
with: SpaceDID,
derives: equalWith,
})

/**
* Capability can be invoked by an agent to record egress data for a given resource.
* It can be derived from any of the `space/content/serve/*` capability that has matching `with`.
*/
export const egressRecord = capability({
can: 'space/content/serve/egress/record',
with: SpaceDID,
nb: Schema.struct({
/** CID of the resource that was served. */
resource: Schema.link(),
/** Amount of bytes served. */
bytes: Schema.integer().greaterThan(0),
/** Timestamp of the event in seconds after Unix epoch. */
servedAt: Schema.integer().greaterThan(-1),
}),
derives: equalWith,
})
16 changes: 11 additions & 5 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -131,8 +131,14 @@ export type UsageReport = InferInvokedCapability<typeof UsageCaps.report>
export type UsageReportSuccess = Record<ProviderDID, UsageData>
export type UsageReportFailure = Ucanto.Failure

export type EgressRecord = InferInvokedCapability<typeof UsageCaps.record>
export type EgressRecordSuccess = Unit
export type EgressRecord = InferInvokedCapability<typeof SpaceCaps.egressRecord>
export type EgressRecordSuccess = {
space: SpaceDID
resource: UnknownLink
bytes: number
servedAt: ISO8601Date
cause: UnknownLink
}
export type EgressRecordFailure = ConsumerNotFound | Ucanto.Failure

export interface UsageData {
Expand Down Expand Up @@ -276,8 +282,8 @@ export interface RateLimitListSuccess {
export type RateLimitListFailure = Ucanto.Failure

// Space
export type Space = InferInvokedCapability<typeof space>
export type SpaceInfo = InferInvokedCapability<typeof info>
export type Space = InferInvokedCapability<typeof SpaceCaps.space>
export type SpaceInfo = InferInvokedCapability<typeof SpaceCaps.info>

// filecoin
export interface DealMetadata {
Expand Down
17 changes: 0 additions & 17 deletions packages/capabilities/src/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,3 @@ export const report = capability({
)
},
})

/**
* Capability can be invoked by an agent to record usage data for a given resource.
*/
export const record = capability({
can: 'usage/record',
with: SpaceDID,
nb: Schema.struct({
/** CID of the resource that was served. */
resource: Schema.link(),
/** Amount of bytes served. */
bytes: Schema.integer().greaterThan(0),
/** Timestamp of the event in seconds after Unix epoch. */
servedAt: Schema.integer().greaterThan(-1),
}),
derives: equalWith,
})
9 changes: 9 additions & 0 deletions packages/capabilities/test/helpers/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,12 @@ export const service = Signer.parse(
export const readmeCID = parseLink(
'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4'
)

export const gateway = Signer.parse(
'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg=' // random key
).withDID('did:web:w3s.link')

/** did:key:z6MktYxTNoCxrXhK9oS5PdzutujTJ5DaS3FWYxNpRTXwrH6h */
export const space = Signer.parse(
'MgCYBaaeyfAHFNt5+M07rY9pPLnmhyxvMEj5jdyAN0ajSlO0B0Xk2fW+t/EsB2nqWraDmB7N0NiTXKZaVBbOpCMtCktI=' // random key
)
4 changes: 3 additions & 1 deletion packages/upload-api/src/space.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import * as Provider from '@ucanto/server'
import * as API from './types.js'

import { info } from './space/info.js'
import { provide as provideRecordEgress } from './space/record.js'
import { createService as createBlobService } from './blob.js'
import { createService as createIndexService } from './index.js'

/**
* @param {API.SpaceServiceContext & API.BlobServiceContext & API.IndexServiceContext} ctx
* @param {API.SpaceServiceContext & API.BlobServiceContext & API.IndexServiceContext & API.UsageServiceContext} ctx
*/
export const createService = (ctx) => ({
info: Provider.provide(Space.info, (input) => info(input, ctx)),
blob: createBlobService(ctx),
index: createIndexService(ctx),
content: { serve: { egress: { record: provideRecordEgress(ctx) } } },
})
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import * as API from '../types.js'
import * as Provider from '@ucanto/server'
import { Usage } from '@web3-storage/capabilities'
import { Space } from '@web3-storage/capabilities'

/** @param {API.UsageServiceContext} context */
/** @param {API.SpaceServiceContext & API.UsageServiceContext} context */
export const provide = (context) =>
Provider.provide(Usage.record, (input) => record(input, context))
Provider.provide(Space.egressRecord, (input) => egressRecord(input, context))

/**
* @param {API.Input<Usage.record>} input
* @param {API.UsageServiceContext} context
* @param {API.Input<Space.egressRecord>} input
* @param {API.SpaceServiceContext & API.UsageServiceContext} context
* @returns {Promise<API.Result<API.EgressRecordSuccess, API.EgressRecordFailure>>}
*/
const record = async ({ capability, invocation }, context) => {
const egressRecord = async ({ capability, invocation }, context) => {
const provider = /** @type {`did:web:${string}`} */ (
invocation.audience.did()
)
Expand Down
2 changes: 0 additions & 2 deletions packages/upload-api/src/usage.js
Original file line number Diff line number Diff line change
@@ -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),
})
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 gatewaySigner = ed25519.parse(
'MgCaNpGXCEX0+BxxE4SjSStrxU9Ru/Im+HGNQ/JJx3lDoI+0B3NWjWW3G8OzjbazZjanjM3kgfcZbvpyxv20jHtmcTtg='
)
export const gateway = gatewaySigner.withDID('did:web:w3s.link')

/**
* Creates a server for the given service.
Expand Down
69 changes: 69 additions & 0 deletions packages/w3up-client/src/capability/space.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,4 +19,71 @@ export class SpaceClient extends Base {
async info(space, options) {
return await this._agent.getSpaceInfo(space, options)
}

/**
* Record egress data for a served resource.
* It will execute the capability invocation to find the customer and then record the egress data for the resource.
*
* Required delegated capabilities:
* - `space/content/serve/egress/record`
*
* @param {object} egressData
* @param {import('../types.js').SpaceDID} egressData.space
* @param {API.UnknownLink} egressData.resource
* @param {number} egressData.bytes
* @param {string} egressData.servedAt
* @param {object} [options]
* @param {string} [options.nonce]
* @param {API.Delegation[]} [options.proofs]
* @returns {Promise<API.EgressRecordSuccess>}
*/
async egressRecord(egressData, options) {
const out = await egressRecord(
{ agent: this.agent },
{ ...egressData },
{ ...options }
)

if (!out.ok) {
throw new Error(
`failed ${SpaceCapabilities.egressRecord.can} invocation`,
{
cause: out.error,
}
)
}

return /** @type {API.EgressRecordSuccess} */ (out.ok)
}
}

/**
* Record egress data for a resource from a given space.
*
* @param {{agent: API.Agent}} client
* @param {object} egressData
* @param {API.SpaceDID} egressData.space
* @param {API.UnknownLink} egressData.resource
* @param {number} egressData.bytes
* @param {string} egressData.servedAt
* @param {object} options
* @param {string} [options.nonce]
* @param {API.Delegation[]} [options.proofs]
*/
export const egressRecord = async (
{ agent },
{ space, resource, bytes, servedAt },
{ nonce, proofs = [] }
) => {
const receipt = await agent.invokeAndExecute(SpaceCapabilities.egressRecord, {
with: space,
proofs,
nonce,
nb: {
resource,
bytes,
servedAt: Math.floor(new Date(servedAt).getTime() / 1000),
},
})
return receipt.out
}
63 changes: 0 additions & 63 deletions packages/w3up-client/src/capability/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,37 +31,6 @@ export class UsageClient extends Base {

return out.ok
}

/**
* Record egress data for a served resource.
* It will execute the capability invocation to find the customer and then record the egress data for the resource.
*
* Required delegated capabilities:
* - `usage/record`
*
* @param {import('../types.js').SpaceDID} space
* @param {object} egressData
* @param {API.UnknownLink} egressData.resource
* @param {number} egressData.bytes
* @param {string} egressData.servedAt
* @param {object} [options]
* @param {string} [options.nonce]
*/
async record(space, egressData, options) {
const out = await record(
{ agent: this.agent },
{ space, ...egressData },
{ ...options }
)
/* c8 ignore next 5 */
if (!out.ok) {
throw new Error(`failed ${UsageCapabilities.record.can} invocation`, {
cause: out.error,
})
}

return out.ok
}
}

/**
Expand Down Expand Up @@ -92,35 +61,3 @@ export const report = async (
})
return receipt.out
}

/**
* Record egress data for a resource from a given space.
*
* @param {{agent: API.Agent}} client
* @param {object} egressData
* @param {API.SpaceDID} egressData.space
* @param {API.UnknownLink} egressData.resource
* @param {number} egressData.bytes
* @param {string} egressData.servedAt
* @param {object} options
* @param {string} [options.nonce]
* @param {API.Delegation[]} [options.proofs]
* @returns {Promise<API.Result<API.Unit, API.EgressRecordFailure>>}
*/
export const record = async (
{ agent },
{ space, resource, bytes, servedAt },
{ nonce, proofs = [] }
) => {
const receipt = await agent.invokeAndExecute(UsageCapabilities.record, {
with: space,
proofs,
nonce,
nb: {
resource,
bytes,
servedAt: Math.floor(new Date(servedAt).getTime() / 1000),
},
})
return receipt.out
}
Loading

0 comments on commit d28691c

Please sign in to comment.