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

feat: add usage/report capability #1079

Merged
merged 5 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/access-client/src/agent-use-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export async function authorizeAndWait(access, email, opts = {}) {
{ can: 'upload/*' },
{ can: 'ucan/*' },
{ can: 'plan/*' },
{ can: 'usage/*' },
{ can: 'w3up/*' },
]
)
Expand Down
4 changes: 4 additions & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Dealer from './filecoin/dealer.js'
import * as DealTracker from './filecoin/deal-tracker.js'
import * as UCAN from './ucan.js'
import * as Plan from './plan.js'
import * as Usage from './usage.js'

export {
Access,
Expand All @@ -40,6 +41,7 @@ export {
Admin,
UCAN,
Plan,
Usage,
}

/** @type {import('./types.js').AbilitiesArray} */
Expand Down Expand Up @@ -80,4 +82,6 @@ export const abilitiesAsStrings = [
Admin.upload.inspect.can,
Admin.store.inspect.can,
Plan.get.can,
Usage.usage.can,
Usage.report.can,
]
42 changes: 41 additions & 1 deletion packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as DealerCaps from './filecoin/dealer.js'
import * as AdminCaps from './admin.js'
import * as UCANCaps from './ucan.js'
import * as PlanCaps from './plan.js'
import * as UsageCaps from './usage.js'

export type ISO8601Date = string

Expand Down Expand Up @@ -104,6 +105,43 @@ export interface DelegationNotFound extends Ucanto.Failure {

export type AccessConfirm = InferInvokedCapability<typeof AccessCaps.confirm>

// Usage

export type Usage = InferInvokedCapability<typeof UsageCaps.usage>
export type UsageReport = InferInvokedCapability<typeof UsageCaps.report>
export type UsageReportSuccess = Record<ProviderDID, UsageData>
export type UsageReportFailure = Ucanto.Failure

export interface UsageData {
/** Space the report concerns. */
alanshaw marked this conversation as resolved.
Show resolved Hide resolved
provider: ProviderDID
/** Space the report concerns. */
space: SpaceDID
/** Period the report applies to. */
period: {
/** ISO datetime the report begins from (inclusive). */
from: ISO8601Date
/** ISO datetime the report ends at (inclusive). */
to: ISO8601Date
}
/** Observed space size for the period. */
size: {
/** Size at the beginning of the report period. */
initial: number
/** Size at the end of the report period. */
final: number
}
/** Events that caused the size to change during the period. */
events: Array<{
/** CID of the invoked task that caused the size to change. */
cause: Link
/** Number of bytes that were added or removed. */
delta: number
/** ISO datetime that the receipt was issued for the change. */
receiptAt: ISO8601Date
}>
}

// Provider
export type ProviderAdd = InferInvokedCapability<typeof provider.add>
// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down Expand Up @@ -580,5 +618,7 @@ export type AbilitiesArray = [
Admin['can'],
AdminUploadInspect['can'],
AdminStoreInspect['can'],
PlanGet['can']
PlanGet['can'],
Usage['can'],
UsageReport['can']
]
42 changes: 42 additions & 0 deletions packages/capabilities/src/usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { capability, ok, Schema } from '@ucanto/validator'
import { and, equal, equalWith, SpaceDID } from './utils.js'

/**
* Capability can only be delegated (but not invoked) allowing audience to
* derived any `usage/` prefixed capability for the (memory) space identified
* by DID in the `with` field.
alanshaw marked this conversation as resolved.
Show resolved Hide resolved
*/
export const usage = capability({
can: 'usage/*',
/** DID of the (memory) space where usage is derived. */
with: SpaceDID,
derives: equalWith,
})

/**
* Capability can be invoked by an agent to retrieve usage data for a space in
* a given period.
*/
export const report = capability({
can: 'usage/report',
with: SpaceDID,
nb: Schema.struct({
/** Period to retrieve events between. */
period: Schema.struct({
/** Time in seconds after Unix epoch (inclusive). */
from: Schema.integer().greaterThan(-1),
/** Time in seconds after Unix epoch (exclusive). */
to: Schema.integer().greaterThan(-1),
}),
}),
derives: (child, parent) => {
return (
and(equalWith(child, parent)) ||
and(
equal(child.nb.period?.from, parent.nb.period?.from, 'period.from')
) ||
and(equal(child.nb.period?.to, parent.nb.period?.to, 'period.to')) ||
ok({})
)
},
})
203 changes: 203 additions & 0 deletions packages/capabilities/test/capabilities/usage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal'
import * as Usage from '../../src/usage.js'
import * as Capability from '../../src/top.js'
import {
alice,
service as w3,
mallory as account,
bob,
} from '../helpers/fixtures.js'
import { validateAuthorization } from '../helpers/utils.js'

const top = async () =>
Capability.top.delegate({
issuer: account,
audience: alice,
with: account.did(),
})

const usage = async () =>
Usage.usage.delegate({
issuer: account,
audience: alice,
with: account.did(),
proofs: [await top()],
})

describe('usage capabilities', function () {
it('usage/report can be derived from *', async () => {
const period = { from: 0, to: 1 }
const report = Usage.report.invoke({
issuer: alice,
audience: w3,
with: account.did(),
nb: { period },
proofs: [await top()],
})

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 })
})

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()],
})

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 })
})

it('usage/report can be derived from usage/* derived from *', async () => {
const period = { from: 3, to: 4 }
const usage = await Usage.report.delegate({
issuer: alice,
audience: bob,
with: account.did(),
proofs: [await top()],
})

const report = Usage.report.invoke({
issuer: bob,
audience: w3,
with: account.did(),
nb: { period },
proofs: [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 })
})

it('usage/report sholud fail when escalating period constraint', async () => {
const period = { from: 5, to: 6 }
const delegation = await Usage.report.delegate({
issuer: alice,
audience: bob,
with: account.did(),
nb: { period },
proofs: [await top()],
})

{
const report = Usage.report.invoke({
issuer: bob,
audience: w3,
with: account.did(),
nb: { period: { from: period.from + 1, to: period.to } },
proofs: [delegation],
})

const result = await access(await report.delegate(), {
capability: Usage.report,
principal: Verifier,
authority: w3,
validateAuthorization,
})

assert.ok(result.error)
assert(
result.error.message.includes(
`${period.from + 1} violates imposed period.from constraint ${
period.from
}`
)
)
}

{
const report = Usage.report.invoke({
issuer: bob,
audience: w3,
with: account.did(),
nb: { period: { from: period.from, to: period.to + 1 } },
proofs: [delegation],
})

const result = await access(await report.delegate(), {
capability: Usage.report,
principal: Verifier,
authority: w3,
validateAuthorization,
})

assert.ok(result.error)
assert(
result.error.message.includes(
`${period.to + 1} violates imposed period.to constraint ${period.to}`
)
)
}
})

it('usage/report period from must be an int', async () => {
const period = { from: 5.5, to: 6 }
const proofs = [await top()]
assert.throws(() => {
Usage.report.invoke({
issuer: alice,
audience: w3,
with: account.did(),
nb: { period },
proofs,
})
}, /Expected value of type integer instead got 5\.5/)
})

it('usage/report period to must be an int', async () => {
const period = { from: 5, to: 6.6 }
const proofs = [await top()]
assert.throws(() => {
Usage.report.invoke({
issuer: alice,
audience: w3,
with: account.did(),
nb: { period },
proofs,
})
}, /Expected value of type integer instead got 6\.6/)
})
})
5 changes: 3 additions & 2 deletions packages/did-mailto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@
"test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules -n experimental-fetch --watch-files src,test"
},
"devDependencies": {
"@web3-storage/eslint-config-w3up": "workspace:^",
"@types/assert": "^1.5.6",
"@types/mocha": "^10.0.1",
"mocha": "^10.2.0"
"@web3-storage/eslint-config-w3up": "workspace:^",
"mocha": "^10.2.0",
"typescript": "5.2.2"
},
"eslintConfig": {
"extends": [
Expand Down
3 changes: 2 additions & 1 deletion packages/filecoin-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@
"@web3-storage/filecoin-client": "workspace:^",
"mocha": "^10.2.0",
"multiformats": "^12.1.2",
"one-webcrypto": "git://github.com/web3-storage/one-webcrypto",
"p-wait-for": "^5.0.2",
"one-webcrypto": "git://github.com/web3-storage/one-webcrypto"
"typescript": "5.2.2"
},
"eslintConfig": {
"extends": [
Expand Down
5 changes: 3 additions & 2 deletions packages/upload-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@
"@types/mocha": "^10.0.1",
"@ucanto/core": "^9.0.0",
"@web-std/blob": "^3.0.5",
"@web3-storage/sigv4": "^1.0.2",
"@web3-storage/eslint-config-w3up": "workspace:^",
"@web3-storage/sigv4": "^1.0.2",
"is-subset": "^0.1.1",
"mocha": "^10.2.0",
"one-webcrypto": "git://github.com/web3-storage/one-webcrypto"
"one-webcrypto": "git://github.com/web3-storage/one-webcrypto",
"typescript": "5.2.2"
},
"eslintConfig": {
"extends": [
Expand Down
Loading
Loading