Skip to content

Commit

Permalink
feat(capabilities)!: add index/add capability (#1410)
Browse files Browse the repository at this point in the history
Adds `index/add` capability.

refs https://github.com/w3s-project/specs/blob/main/w3-index.md

BREAKING CHANGE: `BlobMultihash` type in `@web3-storage/capabilities`
renamed to `Multihash`.

---------

Co-authored-by: Vasco Santos <[email protected]>
  • Loading branch information
Alan Shaw and vasco-santos authored Apr 26, 2024
1 parent 2441ee9 commit 1b71b89
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 11 deletions.
4 changes: 4 additions & 0 deletions packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"types": "./dist/src/filecoin/dealer.d.ts",
"import": "./src/filecoin/dealer.js"
},
"./index": {
"types": "./dist/src/index/index.d.ts",
"import": "./src/index/index.js"
},
"./web3.storage/blob": {
"types": "./dist/src/web3.storage/blob.d.ts",
"import": "./src/web3.storage/blob.js"
Expand Down
1 change: 1 addition & 0 deletions packages/capabilities/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import * as Filecoin from '@web3-storage/capabilities/filecoin'
import * as Aggregator from '@web3-storage/capabilities/filecoin/aggregator'
import * as DealTracker from '@web3-storage/capabilities/filecoin/deal-tracker'
import * as Dealer from '@web3-storage/capabilities/filecoin/dealer'
import * as Index from '@web3-storage/capabilities/index'

// This package has a "main" entrypoint but we recommend the usage of the specific imports above
```
Expand Down
6 changes: 5 additions & 1 deletion packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as Storefront from './filecoin/storefront.js'
import * as Aggregator from './filecoin/aggregator.js'
import * as Dealer from './filecoin/dealer.js'
import * as DealTracker from './filecoin/deal-tracker.js'
import * as Index from './index/index.js'
import * as UCAN from './ucan.js'
import * as Plan from './plan.js'
import * as Usage from './usage.js'
Expand All @@ -37,6 +38,7 @@ export {
RateLimit,
Subscription,
Filecoin,
Index,
Storefront,
Aggregator,
Dealer,
Expand All @@ -50,7 +52,7 @@ export {
HTTP,
}

/** @type {import('./types.js').AbilitiesArray} */
/** @type {import('./types.js').ServiceAbility[]} */
export const abilitiesAsStrings = [
Top.top.can,
Provider.add.can,
Expand Down Expand Up @@ -101,4 +103,6 @@ export const abilitiesAsStrings = [
W3sBlob.allocate.can,
W3sBlob.accept.can,
HTTP.put.can,
Index.index.can,
Index.add.can,
]
52 changes: 52 additions & 0 deletions packages/capabilities/src/index/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Index Capabilities.
*
* W3 Indexing protocol allows authorized agents to submit verifiable claims
* about content-addressed data to be published on the InterPlanetary Network
* Indexer (IPNI), making it publicly queryable.
*
* These can be imported directly with:
* ```js
* import * as Index from '@web3-storage/capabilities/index'
* ```
*
* @module
*/
import { CAR } from '@ucanto/core'
import { capability, Schema, ok } from '@ucanto/validator'
import { equalWith, SpaceDID, and, equal } from '../utils.js'

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

/**
* `index/add` capability allows an agent to submit verifiable claims
* about content-addressed data to be published on the InterPlanetary Network
* Indexer (IPNI), making it publicly queryable.
*/
export const add = capability({
can: 'index/add',
/** DID of the space where indexed data is stored. */
with: SpaceDID,
nb: Schema.struct({
/** Content Archive (CAR) containing the `Index`. */
index: Schema.link({ code: CAR.code }),
}),
derives: (claimed, delegated) =>
and(equalWith(claimed, delegated)) ||
and(equal(claimed.nb.index, delegated.nb.index, 'index')) ||
ok({}),
})

// ⚠️ We export imports here so they are not omitted in generated typedefs
// @see https://github.com/microsoft/TypeScript/issues/51548
export { Schema }
41 changes: 38 additions & 3 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as StorefrontCaps from './filecoin/storefront.js'
import * as AggregatorCaps from './filecoin/aggregator.js'
import * as DealTrackerCaps from './filecoin/deal-tracker.js'
import * as DealerCaps from './filecoin/dealer.js'
import * as IndexCaps from './index/index.js'
import * as AdminCaps from './admin.js'
import * as UCANCaps from './ucan.js'
import * as PlanCaps from './plan.js'
Expand All @@ -53,6 +54,8 @@ export interface UCANAwait<Selector extends string = string, Task = unknown> {
*/
export type CARLink = Link<unknown, typeof CAR.codec.code>

export type Multihash = Uint8Array

export type AccountDID = DID<'mailto'>
export type SpaceDID = DID<'key'>

Expand Down Expand Up @@ -449,6 +452,37 @@ export type UploadGetFailure = UploadNotFound | Ucanto.Failure
// HTTP
export type HTTPPut = InferInvokedCapability<typeof HTTPCaps.put>

// Index
export type Index = InferInvokedCapability<typeof IndexCaps.index>
export type IndexAdd = InferInvokedCapability<typeof IndexCaps.add>

export type IndexAddSuccess = Unit

export type IndexAddFailure =
| UnknownFormat
| ShardNotFound
| SliceNotFound
| Failure

/** The index is not in a format understood by the service. */
export interface UnknownFormat extends Failure {
name: 'UnknownFormat'
}

/** A shard referenced by the index is not stored in the referenced space. */
export interface ShardNotFound extends Failure {
name: 'ShardNotFound'
/** Multihash digest of the shard that could not be found. */
digest: Multihash
}

/** A slice referenced by the index was not found in the specified shard. */
export interface SliceNotFound extends Failure {
name: 'SliceNotFound'
/** Multihash digest of the slice that could not be found. */
digest: Multihash
}

// Blob
export type Blob = InferInvokedCapability<typeof BlobCaps.blob>
export type BlobAdd = InferInvokedCapability<typeof BlobCaps.add>
Expand All @@ -458,9 +492,8 @@ export type ServiceBlob = InferInvokedCapability<typeof W3sBlobCaps.blob>
export type BlobAllocate = InferInvokedCapability<typeof W3sBlobCaps.allocate>
export type BlobAccept = InferInvokedCapability<typeof W3sBlobCaps.accept>

export type BlobMultihash = Uint8Array
export interface BlobModel {
digest: BlobMultihash
digest: Multihash
size: number
}

Expand Down Expand Up @@ -841,7 +874,9 @@ export type ServiceAbilityArray = [
ServiceBlob['can'],
BlobAllocate['can'],
BlobAccept['can'],
HTTPPut['can']
HTTPPut['can'],
Index['can'],
IndexAdd['can']
]

/**
Expand Down
155 changes: 155 additions & 0 deletions packages/capabilities/test/capabilities/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal'
import * as Index from '../../src/index/index.js'
import * as Capability from '../../src/top.js'
import {
alice,
service as w3,
mallory as account,
bob,
} from '../helpers/fixtures.js'
import { createCarCid, validateAuthorization } from '../helpers/utils.js'

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

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

describe('index capabilities', function () {
it('index/add can be derived from *', async () => {
const add = Index.add.invoke({
issuer: alice,
audience: w3,
with: account.did(),
nb: {
index: await createCarCid('test'),
},
proofs: [await top()],
})

const result = await access(await add.delegate(), {
capability: Index.add,
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, 'index/add')
assert.deepEqual(result.ok.capability.nb, {
index: await createCarCid('test'),
})
})

it('index/add can be derived from index/*', async () => {
const add = Index.add.invoke({
issuer: alice,
audience: w3,
with: account.did(),
nb: {
index: await createCarCid('test'),
},
proofs: [await index()],
})

const result = await access(await add.delegate(), {
capability: Index.add,
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, 'index/add')
assert.deepEqual(result.ok.capability.nb, {
index: await createCarCid('test'),
})
})

it('index/add can be derived from index/* derived from *', async () => {
const index = await Index.index.delegate({
issuer: alice,
audience: bob,
with: account.did(),
proofs: [await top()],
})

const add = Index.add.invoke({
issuer: bob,
audience: w3,
with: account.did(),
nb: {
index: await createCarCid('test'),
},
proofs: [index],
})

const result = await access(await add.delegate(), {
capability: Index.add,
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, 'index/add')
assert.deepEqual(result.ok.capability.nb, {
index: await createCarCid('test'),
})
})

it('index/add should fail when escalating index constraint', async () => {
const delegation = await Index.add.delegate({
issuer: alice,
audience: bob,
with: account.did(),
nb: {
index: await createCarCid('test'),
},
proofs: [await top()],
})

const add = Index.add.invoke({
issuer: bob,
audience: w3,
with: account.did(),
nb: {
index: await createCarCid('test2'),
},
proofs: [delegation],
})

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

assert.ok(result.error)
assert(result.error.message.includes('violates imposed index constraint'))
})
})
14 changes: 7 additions & 7 deletions packages/upload-api/src/types/blob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
DID,
} from '@ucanto/interface'
import {
BlobMultihash,
Multihash,
BlobListItem,
BlobRemoveSuccess,
} from '@web3-storage/capabilities/types'
Expand All @@ -19,11 +19,11 @@ export type TasksStorage = Storage<UnknownLink, Invocation>
export interface AllocationsStorage {
get: (
space: DID,
blobMultihash: BlobMultihash
blobMultihash: Multihash
) => Promise<Result<BlobGetOutput, Failure>>
exists: (
space: DID,
blobMultihash: BlobMultihash
blobMultihash: Multihash
) => Promise<Result<boolean, Failure>>
/** Inserts an item in the table if it does not already exist. */
insert: (
Expand All @@ -36,7 +36,7 @@ export interface AllocationsStorage {
/** Removes an item from the table, returning zero on size if non existent. */
remove: (
space: DID,
digest: BlobMultihash
digest: Multihash
) => Promise<Result<BlobRemoveSuccess, Failure>>
}

Expand All @@ -46,7 +46,7 @@ export interface ListOptions {
}

export interface BlobModel {
digest: BlobMultihash
digest: Multihash
size: number
}

Expand All @@ -64,9 +64,9 @@ export interface BlobGetOutput {
}

export interface BlobsStorage {
has: (content: BlobMultihash) => Promise<Result<boolean, Failure>>
has: (content: Multihash) => Promise<Result<boolean, Failure>>
createUploadUrl: (
content: BlobMultihash,
content: Multihash,
size: number,
/**
* The number of seconds before the presigned URL expires
Expand Down

0 comments on commit 1b71b89

Please sign in to comment.