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(capabilities)!: add index/add capability #1410

Merged
merged 3 commits into from
Apr 26, 2024
Merged
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 @@ -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
}
Comment on lines +479 to +484
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a question, are we going to validate this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have talked previously about sampling a few in storacha/specs#85


// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change, I think this is imported somewhere else IIRC. Either should be kept, or need to be flagged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps is just in upload-api, but honestly I would keep this or signal a Breaking Change

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
Loading