Skip to content

Commit

Permalink
feat(capability): add space to location assertion (#84)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
hannahhoward authored Jan 30, 2025
1 parent d72d27f commit 5212b1a
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 92 deletions.
31 changes: 16 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/capability/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ 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)) ||
and(equalLinkOrDigestContent(claimed, delegated)) ||
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({})
)
})
Expand Down Expand Up @@ -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/[email protected]`
Expand Down
72 changes: 15 additions & 57 deletions packages/core/src/client/api.ts
Original file line number Diff line number Diff line change
@@ -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> = C extends Capability<Ability, Resource, infer NB> ? NB : never

type InferContent<C extends Caveats> = C extends { content: infer T} ? T : never

/** A verifiable claim about data. */
export interface ContentClaim<T extends string> {
/** Subject of the claim e.g. CAR, DAG root etc. */
readonly content: MultihashDigest
readonly content: InferContent<InferCaveats<AssertLocation | AssertPartition | AssertInclusion | AssertIndex | AssertEquals | AssertRelation>>
/** Discriminator for different types of claims. */
readonly type: T
/**
* Returns an iterable of all IPLD blocks that are included in this claim.
*/
export (): IterableIterator<Block>
/**
* 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<Uint8Array>
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<typeof Assert.location.can> {
readonly location: URI[]
readonly range?: ByteRange
export interface LocationClaim extends ContentClaim<typeof Assert.location.can>, Readonly<InferCaveats<AssertLocation>> {
}

/** A claim that a CID's graph can be read from the blocks found in parts. */
export interface PartitionClaim extends ContentClaim<typeof Assert.partition.can> {
/** 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<typeof Assert.partition.can>, Readonly<InferCaveats<AssertPartition>> {
}

/** A claim that a CID includes the contents claimed in another CID. */
export interface InclusionClaim extends ContentClaim<typeof Assert.inclusion.can> {
/** e.g. CARv2 Index CID or Sub-Deal CID (CommP) */
readonly includes: Link
/** Zero-knowledge proof */
readonly proof?: Link
export interface InclusionClaim extends ContentClaim<typeof Assert.inclusion.can>, Readonly<InferCaveats<AssertInclusion>> {
}

/**
* 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<typeof Assert.index.can> {
/**
* Link to a Content Archive that contains the index.
* e.g. `index/sharded/[email protected]`
* @see https://github.com/storacha/specs/blob/main/w3-index.md
*/
readonly index: Link
export interface IndexClaim extends ContentClaim<typeof Assert.index.can>, Readonly<InferCaveats<AssertIndex>> {
}

/** A claim that a CID links to other CIDs. */
export interface RelationClaim extends ContentClaim<typeof Assert.relation.can> {
/** 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<typeof Assert.relation.can>, Readonly<InferCaveats<AssertRelation>> {
}

/** A claim that the same data is referred to by another CID and/or multihash */
export interface EqualsClaim extends ContentClaim<typeof Assert.equals.can> {
/** 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<typeof Assert.equals.can>, Readonly<InferCaveats<AssertEquals>> {
}

/** Types of claim that are known to this library. */
Expand All @@ -103,8 +66,3 @@ export type Claim =
| RelationClaim
| EqualsClaim
| UnknownClaim

export interface ByteRange {
readonly offset: number
readonly length?: number
}
50 changes: 33 additions & 17 deletions packages/core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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
Expand All @@ -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<import('./api.js').Claim>}
*/
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
}
}

Expand Down Expand Up @@ -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
}

0 comments on commit 5212b1a

Please sign in to comment.