From 5212b1af03880fc7eb055ca0339003119b0cec60 Mon Sep 17 00:00:00 2001 From: Hannah Howard Date: Wed, 29 Jan 2025 18:15:24 -0800 Subject: [PATCH] feat(capability): add space to location assertion (#84) # Goals Enable parsing and usage of assertions defined here with responses from the indexer service. # Implementation Add Space DID as an optional field for the AssertLocation # For Discussion *Why ship a change here?* This library contains the best JS definitions of our various assertions. Apart from the missing Space DID, it's the SAME as the assertions defined in go-capabilities. Currently, JS fetching logic for the indexer does NOT understand assertions, and moreover, the locator for the indexing service in the blob-fetcher uses a non-standard approach of using a parsing validator called 'zod' to parse only location claims. We should maintain full definitions for our various assertions across languages and those definitions should maintain parity. Ultimately, they shouldn't live here. Maybe @storacha/capabilities -- i.e. move them to w3up. But that's a problem for another day. --- package-lock.json | 31 +++++------ packages/core/package.json | 3 +- packages/core/src/capability/assert.js | 6 ++- packages/core/src/client/api.ts | 72 ++++++-------------------- packages/core/src/client/index.js | 50 ++++++++++++------ 5 files changed, 70 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index a07cad9..df8f360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8031,21 +8031,21 @@ } }, "node_modules/@ucanto/core": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@ucanto/core/-/core-10.0.1.tgz", - "integrity": "sha512-1BfUaJu0/c9Rl/WdZSDbScJJLsPsPe1g4ynl5kubUj3xDD/lyp/Q12PQVQ2X7hDiWwkpwmxCkRMkOxwc70iNKQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@ucanto/core/-/core-10.1.1.tgz", + "integrity": "sha512-Dypn1hlWvP4kFLuM98U02c8anFuT8hrR3uVi7YZ3wTyBeKOhl/0ggGBR06eyXLw6EL15Yp7dR3Mlce9jTEPwlA==", "dependencies": { "@ipld/car": "^5.1.0", "@ipld/dag-cbor": "^9.0.0", "@ipld/dag-ucan": "^3.4.0", - "@ucanto/interface": "^10.0.1", + "@ucanto/interface": "^10.0.2", "multiformats": "^11.0.2" } }, "node_modules/@ucanto/interface": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@ucanto/interface/-/interface-10.0.1.tgz", - "integrity": "sha512-+Vr/N4mLsdynV9/bqtdFiq7WsUf3265/Qx2aHJmPtXo9/QvWKthJtpe0g8U4NWkWpVfqIFvyAO2db6D9zWQfQw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@ucanto/interface/-/interface-10.0.2.tgz", + "integrity": "sha512-0n1H6ChvC1moQl2lnGMdSN/ThfCiJ99VdyTtfiu/380vcf3U3Sb8soIrAWE9mM9KysNZUWfJBB0ahj5vganeoA==", "dependencies": { "@ipld/dag-ucan": "^3.4.0", "multiformats": "^11.0.2" @@ -8066,14 +8066,14 @@ } }, "node_modules/@ucanto/server": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@ucanto/server/-/server-10.0.0.tgz", - "integrity": "sha512-JMDMT3tFRE0S1cdtx/Hhh7v9FizV6IS0fPrh6pcli7AzKvXVy8Xu6EQ/66Fax4AQM2tkGxNNxjj2wHM7P4CqAg==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@ucanto/server/-/server-10.0.2.tgz", + "integrity": "sha512-UNO4MAVXnMFP13JgcO3bZcfbW6FS4agZJJGozlo9FxNcUorfrRNTJ+uSmUStszjO+uHYzpIi0dPNRge2Fmm38Q==", "dependencies": { - "@ucanto/core": "^10.0.0", - "@ucanto/interface": "^10.0.0", - "@ucanto/principal": "^9.0.0", - "@ucanto/validator": "^9.0.1" + "@ucanto/core": "^10.1.1", + "@ucanto/interface": "^10.0.2", + "@ucanto/principal": "^9.0.1", + "@ucanto/validator": "^9.0.2" } }, "node_modules/@ucanto/transport": { @@ -18580,8 +18580,9 @@ "license": "Apache-2.0 OR MIT", "dependencies": { "@ucanto/client": "^9.0.1", + "@ucanto/core": "^10.1.1", "@ucanto/interface": "^10.0.0", - "@ucanto/server": "^10.0.0", + "@ucanto/server": "^10.0.2", "@ucanto/transport": "^9.1.1", "carstream": "^2.0.0", "multiformats": "^13.1.0" diff --git a/packages/core/package.json b/packages/core/package.json index 2497ba5..8d923d6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -111,8 +111,9 @@ }, "dependencies": { "@ucanto/client": "^9.0.1", + "@ucanto/core": "^10.1.1", "@ucanto/interface": "^10.0.0", - "@ucanto/server": "^10.0.0", + "@ucanto/server": "^10.0.2", "@ucanto/transport": "^9.1.1", "carstream": "^2.0.0", "multiformats": "^13.1.0" diff --git a/packages/core/src/capability/assert.js b/packages/core/src/capability/assert.js index 4d09e62..0f8acd7 100644 --- a/packages/core/src/capability/assert.js +++ b/packages/core/src/capability/assert.js @@ -21,7 +21,8 @@ export const location = capability({ range: Schema.struct({ offset: Schema.integer(), length: Schema.integer().optional() - }).optional() + }).optional(), + space: Schema.didBytes().optional() }), derives: (claimed, delegated) => ( and(equalWith(claimed, delegated)) || @@ -29,6 +30,7 @@ export const location = capability({ and(equal(claimed.nb.location, delegated.nb.location, 'location')) || and(equal(claimed.nb.range?.offset, delegated.nb.range?.offset, 'offset')) || and(equal(claimed.nb.range?.length, delegated.nb.range?.length, 'length')) || + and(equal(claimed.nb.space, delegated.nb.space, 'space')) || ok({}) ) }) @@ -57,7 +59,7 @@ export const index = capability({ with: URI.match({ protocol: 'did:' }), nb: Schema.struct({ /** DAG root CID */ - content: Schema.link(), + content: linkOrDigest(), /** * Link to a Content Archive that contains the index. * e.g. `index/sharded/dag@0.1` diff --git a/packages/core/src/client/api.ts b/packages/core/src/client/api.ts index 4485b90..a30d602 100644 --- a/packages/core/src/client/api.ts +++ b/packages/core/src/client/api.ts @@ -1,88 +1,51 @@ -import { Link, URI, UnknownLink, Block, MultihashDigest } from '@ucanto/client' +import { Delegation, Capability, Ability, Resource, Caveats } from '@ucanto/client' import * as Assert from '../capability/assert.js' +import { AssertEquals, AssertInclusion, AssertIndex, AssertLocation, AssertPartition, AssertRelation } from '../capability/api.js' + +type InferCaveats = C extends Capability ? NB : never + +type InferContent = C extends { content: infer T} ? T : never /** A verifiable claim about data. */ export interface ContentClaim { /** Subject of the claim e.g. CAR, DAG root etc. */ - readonly content: MultihashDigest + readonly content: InferContent> /** Discriminator for different types of claims. */ readonly type: T /** - * Returns an iterable of all IPLD blocks that are included in this claim. - */ - export (): IterableIterator - /** - * Writes the UCAN `Delegation` chain for this claim into a content addressed - * archive (CAR) buffer and returns it. + * Returns the underlying delegation this is based on */ - archive (): Promise + delegation() : Delegation } /** A claim not known to this library. */ export interface UnknownClaim extends ContentClaim<'unknown'> {} /** A claim that a CID is available at a URL. */ -export interface LocationClaim extends ContentClaim { - readonly location: URI[] - readonly range?: ByteRange +export interface LocationClaim extends ContentClaim, Readonly> { } /** A claim that a CID's graph can be read from the blocks found in parts. */ -export interface PartitionClaim extends ContentClaim { - /** CIDs CID - the hash of the binary sorted links in the set. */ - readonly blocks?: Link - /** List of archives (CAR CIDs) containing the blocks. */ - readonly parts: Link[] +export interface PartitionClaim extends ContentClaim, Readonly> { } /** A claim that a CID includes the contents claimed in another CID. */ -export interface InclusionClaim extends ContentClaim { - /** e.g. CARv2 Index CID or Sub-Deal CID (CommP) */ - readonly includes: Link - /** Zero-knowledge proof */ - readonly proof?: Link +export interface InclusionClaim extends ContentClaim, Readonly> { } /** * A claim that a content graph can be found in blob(s) that are identified and * indexed in the given index CID. */ -export interface IndexClaim extends ContentClaim { - /** - * Link to a Content Archive that contains the index. - * e.g. `index/sharded/dag@0.1` - * @see https://github.com/storacha/specs/blob/main/w3-index.md - */ - readonly index: Link +export interface IndexClaim extends ContentClaim, Readonly> { } /** A claim that a CID links to other CIDs. */ -export interface RelationClaim extends ContentClaim { - /** CIDs of blocks this content directly links to. */ - readonly children: UnknownLink[] - /** List of archives (CAR CIDs) containing the blocks. */ - readonly parts: RelationPart[] -} - -/** Part this content and it's children can be read from. */ -export interface RelationPart { - /** Part CID. */ - content: Link - /** CID of contents (CARv2 index) included in this part. */ - includes?: RelationPartInclusion -} - -export interface RelationPartInclusion { - /** Inclusion CID (CARv2 index) */ - content: Link - /** CIDs of parts this index may be found in. */ - parts?: Link[] +export interface RelationClaim extends ContentClaim, Readonly> { } /** A claim that the same data is referred to by another CID and/or multihash */ -export interface EqualsClaim extends ContentClaim { - /** A CID that is equivalent to the content CID e.g the Piece CID for that CAR CID */ - readonly equals: UnknownLink +export interface EqualsClaim extends ContentClaim, Readonly> { } /** Types of claim that are known to this library. */ @@ -103,8 +66,3 @@ export type Claim = | RelationClaim | EqualsClaim | UnknownClaim - -export interface ByteRange { - readonly offset: number - readonly length?: number -} diff --git a/packages/core/src/client/index.js b/packages/core/src/client/index.js index fb7b5b4..d28da48 100644 --- a/packages/core/src/client/index.js +++ b/packages/core/src/client/index.js @@ -3,11 +3,11 @@ import { extract as extractDelegation } from '@ucanto/core/delegation' import { connect, invoke, delegate } from '@ucanto/client' import { CAR, HTTP } from '@ucanto/transport' import { sha256 } from 'multiformats/hashes/sha2' -import { decode as decodeDigest } from 'multiformats/hashes/digest' import { equals } from 'multiformats/bytes' import { base58btc } from 'multiformats/bases/base58' import { CARReaderStream } from 'carstream/reader' import * as Assert from '../capability/assert.js' +import { decode as decodeDigest } from 'multiformats/hashes/digest' export const serviceURL = new URL('https://claims.web3.storage') @@ -23,22 +23,21 @@ export const connection = connect({ export { connect, invoke, delegate, CAR, HTTP } -const assertCapNames = [ - Assert.location.can, - Assert.partition.can, - Assert.inclusion.can, - Assert.index.can, - Assert.relation.can, - Assert.equals.can -] +const assertCapMap = { + [Assert.location.can]: Assert.location, + [Assert.partition.can]: Assert.partition, + [Assert.inclusion.can]: Assert.inclusion, + [Assert.index.can]: Assert.index, + [Assert.relation.can]: Assert.relation, + [Assert.equals.can]: Assert.equals +} /** * @param {import('@ucanto/interface').Capability} cap * @returns {cap is import('../server/api.js').AnyAssertCap} */ const isAssertCap = cap => - // @ts-expect-error - assertCapNames.includes(cap.can) && + Object.keys(assertCapMap).includes(cap.can) && 'nb' in cap && typeof cap.nb === 'object' && 'content' in cap.nb @@ -52,17 +51,25 @@ export const decode = async bytes => { if (delegation.error) { throw new Error('failed to decode claim', { cause: delegation.error }) } - const cap = delegation.ok.capabilities[0] + return decodeDelegation(delegation.ok) +} + +/** + * @param {import('@ucanto/interface').Delegation} delegation + * @returns {Promise} + */ +export const decodeDelegation = async delegation => { + const cap = delegation.capabilities[0] if (!isAssertCap(cap)) { throw new Error('invalid claim') } // @ts-expect-error + const parsedCap = assertCapMap[cap.can].create({ with: cap.with, nb: cap.nb }) + // @ts-expect-error return { - ...cap.nb, - content: 'digest' in cap.nb.content ? decodeDigest(cap.nb.content.digest) : cap.nb.content.multihash, - type: cap.can, - export: () => delegation.ok.export(), - archive: async () => bytes + ...parsedCap.nb, + type: parsedCap.can, + delegation: () => delegation } } @@ -120,3 +127,12 @@ export const read = async (content, options) => { return claims } + +/** + * + * @param {import('./api.js').Claim} claim + * @returns + */ +export const contentMultihash = (claim) => { + return 'digest' in claim.content ? decodeDigest(claim.content.digest) : claim.content.multihash +}