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(capability): add space to location assertion #84

Merged
merged 5 commits into from
Jan 30, 2025
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
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
}
Loading