Skip to content

Commit

Permalink
feat: introduce root/get and shard/get capabilities
Browse files Browse the repository at this point in the history
implementations of new capabilities proposed in storacha/specs#77
  • Loading branch information
travis committed Sep 11, 2023
1 parent 49cde70 commit 3905d08
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 1 deletion.
20 changes: 20 additions & 0 deletions packages/capabilities/src/root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { capability, struct, ok, Link } from '@ucanto/validator'
import { equalWith, and, equal, ProviderDID } from './utils.js'

/**
* Capability can be invoked by a provider to get information about an upload root CID.
*/
export const get = capability({
can: 'root/get',
with: ProviderDID,
nb: struct({
cid: Link,
}),
derives: (child, parent) => {
return (
and(equalWith(child, parent)) ||
and(equal(child.nb.cid, parent.nb.cid, 'cid')) ||
ok({})
)
},
})
21 changes: 21 additions & 0 deletions packages/capabilities/src/shard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { capability, struct, ok, Link } from '@ucanto/validator'
import { equalWith, and, equal, ProviderDID } from './utils.js'

/**
* Capability can be invoked by a provider to get information about the
* customer.
*/
export const get = capability({
can: 'shard/get',
with: ProviderDID,
nb: struct({
cid: Link,
}),
derives: (child, parent) => {
return (
and(equalWith(child, parent)) ||
and(equal(child.nb.cid, parent.nb.cid, 'cid')) ||
ok({})
)
},
})
5 changes: 4 additions & 1 deletion packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { fail, ok } from '@ucanto/validator'
import { DID, fail, ok } from '@ucanto/validator'
// eslint-disable-next-line no-unused-vars
import * as Types from '@ucanto/interface'

// e.g. did:web:web3.storage or did:web:staging.web3.storage
export const ProviderDID = DID.match({ method: 'web' })

/**
* Check URI can be delegated
*
Expand Down
100 changes: 100 additions & 0 deletions packages/capabilities/test/capabilities/root.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { delegate } from '@ucanto/core'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Root from '../../src/root.js'
import { service, alice, readmeCID } from '../helpers/fixtures.js'

describe('root/get', async function () {
const agent = alice
it('can be invoked by the service on the service', async function () {
const invocation = Root.get.invoke({
issuer: service,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
})
const result = await access(await invocation.delegate(), {
capability: Root.get,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail('error in self issue')
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'root/get')
assert.deepEqual(result.ok.capability.nb, {
cid: readmeCID,
})
}
})

it('can be invoked by an agent delegated permissions by the service', async function () {
const auth = Root.get.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
proofs: [
await delegate({
issuer: service,
audience: agent,
capabilities: [{ with: service.did(), can: 'root/get' }],
}),
],
})
const result = await access(await auth.delegate(), {
capability: Root.get,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail(
`error in self issue: ${JSON.stringify(result.error.message)}`
)
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'root/get')
assert.deepEqual(result.ok.capability.nb, {
cid: readmeCID,
})
}
})

it('fails without a delegation from the service delegation', async function () {
const agent = alice
const auth = Root.get.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
})

const result = await access(await auth.delegate(), {
capability: Root.get,
principal: Verifier,
authority: service,
})

assert.equal(result.error?.message.includes('not authorized'), true)
})

it('requires nb.cid', async function () {
assert.throws(() => {
Root.get.invoke({
issuer: alice,
audience: service,
with: service.did(),
// @ts-ignore
nb: {},
})
}, /Error: Invalid 'nb' - Object contains invalid field "cid"/)
})
})
100 changes: 100 additions & 0 deletions packages/capabilities/test/capabilities/shard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { delegate } from '@ucanto/core'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Shard from '../../src/shard.js'
import { service, alice, readmeCID } from '../helpers/fixtures.js'

describe('shard/get', function () {
const agent = alice
it('can be invoked by the service on the service', async function () {
const invocation = Shard.get.invoke({
issuer: service,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
})
const result = await access(await invocation.delegate(), {
capability: Shard.get,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail('error in self issue')
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'shard/get')
assert.deepEqual(result.ok.capability.nb, {
cid: readmeCID,
})
}
})

it('can be invoked by an agent delegated permissions by the service', async function () {
const auth = Shard.get.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
proofs: [
await delegate({
issuer: service,
audience: agent,
capabilities: [{ with: service.did(), can: 'shard/get' }],
}),
],
})
const result = await access(await auth.delegate(), {
capability: Shard.get,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail(
`error in self issue: ${JSON.stringify(result.error.message)}`
)
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'shard/get')
assert.deepEqual(result.ok.capability.nb, {
cid: readmeCID,
})
}
})

it('fails without a delegation from the service delegation', async function () {
const agent = alice
const auth = Shard.get.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
cid: readmeCID,
},
})

const result = await access(await auth.delegate(), {
capability: Shard.get,
principal: Verifier,
authority: service,
})

assert.equal(result.error?.message.includes('not authorized'), true)
})

it('requires nb.shard', async function () {
assert.throws(() => {
Shard.get.invoke({
issuer: alice,
audience: service,
with: service.did(),
// @ts-ignore
nb: {},
})
}, /Error: Invalid 'nb' - Object contains invalid field "shard"/)
})
})
5 changes: 5 additions & 0 deletions packages/capabilities/test/helpers/fixtures.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseLink } from '@ucanto/core'
import { Absentee } from '@ucanto/principal'
import { Signer } from '@ucanto/principal/ed25519'

Expand Down Expand Up @@ -31,3 +32,7 @@ export const malloryAccount = Absentee.from({
export const service = Signer.parse(
'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8='
).withDID('did:web:test.web3.storage')

export const readmeCID = parseLink(
'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4'
)

0 comments on commit 3905d08

Please sign in to comment.