Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@
"types": "./dist/space/blob.d.ts",
"import": "./dist/space/blob.js"
},
"./space/replica": {
"types": "./dist/space/replica/index.d.ts",
"import": "./dist/space/replica/index.js"
},
"./web3.storage/blob": {
"types": "./dist/web3.storage/blob.d.ts",
"import": "./dist/web3.storage/blob.js"
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 @@ -19,6 +19,7 @@ import * as Aggregator from './filecoin/aggregator.js'
import * as Dealer from './filecoin/dealer.js'
import * as DealTracker from './filecoin/deal-tracker.js'
import * as SpaceIndex from './space/index.js'
import * as SpaceReplica from './space/replica/index.js'
import * as UCAN from './ucan.js'
import * as Plan from './plan.js'
import * as Usage from './usage.js'
Expand All @@ -44,6 +45,7 @@ export {
Subscription,
Filecoin,
SpaceIndex,
SpaceReplica,
Storefront,
Aggregator,
Dealer,
Expand Down Expand Up @@ -127,4 +129,6 @@ export const abilitiesAsStrings = [
HTTP.put.can,
SpaceIndex.index.can,
SpaceIndex.add.can,
SpaceReplica.replica.can,
SpaceReplica.list.can,
]
71 changes: 71 additions & 0 deletions packages/capabilities/src/space/replica/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Space Replica Capabilities.
*
* An extension to the space protocol that allows agents to manage and query
* replica information for blobs stored in a space.
*
* These can be imported directly with:
*
* ```js
* import * as SpaceReplica from '@storacha/capabilities/space/replica'
* ```
*
* @module
*/
import { capability, Schema, ok, fail } from '@ucanto/validator'
import { equals } from 'multiformats/bytes'
import { equalWith, SpaceDID } from '../../utils.js'

/**
* Capability can only be delegated (but not invoked) allowing audience to
* derive any `space/replica/` prefixed capability for the space identified
* by DID in the `with` field.
*/
export const replica = capability({
can: 'space/replica/*',
/** DID of the space where replica information is stored. */
with: SpaceDID,
derives: equalWith,
})

/**
* The `space/replica/list` capability allows an agent to list current replicas
* for a given blob in the space identified by DID in the `with` field.
*/
export const list = capability({
can: 'space/replica/list',
/** DID of the space where replica information is stored. */
with: SpaceDID,
nb: Schema.struct({
/** Blob to list replicas for. */
blob: Schema.bytes(),
/**
* A pointer that can be moved back and forth on the list.
* It can be used to paginate a list for instance.
*/
cursor: Schema.string().optional(),
/**
* Maximum number of items per page.
*/
size: Schema.integer().optional(),
}),
derives: (claimed, delegated) => {
if (claimed.with !== delegated.with) {
return fail(
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
)
} else if (
delegated.nb.blob &&
!equals(delegated.nb.blob, claimed.nb.blob)
) {
return fail(
`Blob digest ${claimed.nb.blob ? `${claimed.nb.blob}` : ''} violates imposed ${delegated.nb.blob} constraint.`
)
}
return ok({})
},
})

// ⚠️ We export imports here so they are not omitted in generated typedefs
// @see https://github.com/microsoft/TypeScript/issues/51548
export { Schema }
115 changes: 115 additions & 0 deletions packages/capabilities/test/capabilities/space/replica/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { strict as assert } from 'node:assert'
import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal'
import * as SpaceReplica from '../../../../src/space/replica/index.js'
import * as Capability from '../../../../src/top.js'
import {
alice,
service as storageNode,
mallory as account,
} from '../../../helpers/fixtures.js'
import {
createCar,
validateAuthorization,
} from '../../../helpers/utils.js'

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

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

describe('space replica capabilities', function () {
it('space/replica/list can be derived from *', async () => {
const car = await createCar('test')

const list = SpaceReplica.list.invoke({
issuer: alice,
audience: storageNode,
with: account.did(),
nb: {
blob: car.cid.multihash.bytes,
},
proofs: [await top()],
})

const result = await access(await list.delegate(), {
capability: SpaceReplica.list,
principal: Verifier,
authority: storageNode,
validateAuthorization,
})

assert.ok(result.ok)
assert.deepEqual(result.ok.audience.did(), storageNode.did())
assert.equal(result.ok.capability.can, SpaceReplica.list.can)
})

it('space/replica/list can be derived from space/replica/*', async () => {
const car = await createCar('test')

const list = SpaceReplica.list.invoke({
issuer: alice,
audience: storageNode,
with: account.did(),
nb: {
blob: car.cid.multihash.bytes,
},
proofs: [await replica()],
})

const result = await access(await list.delegate(), {
capability: SpaceReplica.list,
principal: Verifier,
authority: storageNode,
validateAuthorization,
})

assert.ok(result.ok)
assert.deepEqual(result.ok.audience.did(), storageNode.did())
assert.equal(result.ok.capability.can, SpaceReplica.list.can)
})

it('space/replica/list should fail when escalating space constraint', async () => {
const car1 = await createCar('test1')
const car2 = await createCar('test2')

const list = await SpaceReplica.list.delegate({
issuer: account,
audience: alice,
with: account.did(),
nb: {
blob: car1.cid.multihash.bytes,
},
proofs: [await top()],
})

const invalidList = SpaceReplica.list.invoke({
issuer: alice,
audience: storageNode,
with: account.did(),
nb: {
blob: car2.cid.multihash.bytes,
},
})

const result = await access(await invalidList.delegate(), {
capability: SpaceReplica.list,
principal: Verifier,
authority: storageNode,
validateAuthorization,
proofs: [list],
})

assert.ok(!result.ok)
})
})