Skip to content
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
6 changes: 5 additions & 1 deletion packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"types": "./dist/test/helpers/*.d.ts",
"import": "./test/helpers/*.js"
},
"./assert": {
"types": "./dist/assert.d.ts",
"import": "./dist/assert.js"
},
"./blob": {
"types": "./dist/blob/index.d.ts",
"import": "./dist/blob/index.js"
Expand Down Expand Up @@ -95,7 +99,7 @@
"@ucanto/transport": "catalog:",
"@ucanto/validator": "catalog:",
"@web3-storage/data-segment": "^5.2.0",
"uint8arrays": "^5.0.3"
"multiformats": "catalog:"
},
"devDependencies": {
"@arethetypeswrong/cli": "catalog:",
Expand Down
138 changes: 138 additions & 0 deletions packages/capabilities/src/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { capability, URI, Schema, ok } from '@ucanto/validator'
import { and, equal, equalLinkOrDigestContent, equalWith } from './utils.js'

const linkOrDigest = () =>
Schema.link().or(Schema.struct({ digest: Schema.bytes() }))

export const assert = capability({
can: 'assert/*',
with: URI.match({ protocol: 'did:' }),
})

/**
* Claims that a CID is available at a URL.
*/
export const location = capability({
can: 'assert/location',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
/** Blob CID or multihash */
content: linkOrDigest(),
location: Schema.array(URI),
range: Schema.struct({
offset: Schema.integer(),
length: Schema.integer().optional(),
}).optional(),
space: Schema.principal().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({}),
})

/**
* Claims that a CID includes the contents claimed in another CID.
*/
export const inclusion = capability({
can: 'assert/inclusion',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
/** CAR CID */
content: linkOrDigest(),
/** CARv2 index CID */
includes: Schema.link({ version: 1 }),
proof: Schema.link({ version: 1 }).optional(),
}),
})

/**
* Claims that a content graph can be found in blob(s) that are identified and
* indexed in the given index CID.
*/
export const index = capability({
can: 'assert/index',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
/** DAG root CID */
content: linkOrDigest(),
/**
* 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
*/
index: Schema.link({ version: 1 }),
}),
derives: (claimed, delegated) =>
and(equalWith(claimed, delegated)) ||
and(equal(claimed.nb.content, delegated.nb.content, 'content')) ||
and(equal(claimed.nb.index, delegated.nb.index, 'index')) ||
ok({}),
})

/**
* Claims that a CID's graph can be read from the blocks found in parts.
*/
export const partition = capability({
can: 'assert/partition',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
/** Content root CID */
content: linkOrDigest(),
/** CIDs CID */
blocks: Schema.link({ version: 1 }).optional(),
parts: Schema.array(Schema.link({ version: 1 })),
}),
})

/**
* Claims that a CID links to other CIDs.
*/
export const relation = capability({
can: 'assert/relation',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
content: linkOrDigest(),
/** CIDs this content links to directly. */
children: Schema.array(Schema.link()),
/** Parts this content and it's children can be read from. */
parts: Schema.array(
Schema.struct({
content: Schema.link({ version: 1 }),
/** CID of contents (CARv2 index) included in this part. */
includes: Schema.struct({
content: Schema.link({ version: 1 }),
/** CIDs of parts this index may be found in. */
parts: Schema.array(Schema.link({ version: 1 })).optional(),
}).optional(),
})
),
}),
})

/**
* Claim data is referred to by another CID and/or multihash.
* e.g CAR CID & CommP CID
*/
export const equals = capability({
can: 'assert/equals',
with: URI.match({ protocol: 'did:' }),
nb: Schema.struct({
content: linkOrDigest(),
equals: Schema.link(),
}),
derives: (claimed, delegated) =>
and(equalWith(claimed, delegated)) ||
and(equalLinkOrDigestContent(claimed, delegated)) ||
and(equal(claimed.nb.equals, delegated.nb.equals, 'equals')) ||
ok({}),
})
9 changes: 9 additions & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Assert from './assert.js'
import * as Provider from './provider.js'
import * as Space from './space.js'
import * as Top from './top.js'
Expand Down Expand Up @@ -27,6 +28,7 @@ import * as HTTP from './http.js'

export {
Access,
Assert,
Provider,
Space,
Top,
Expand Down Expand Up @@ -57,6 +59,13 @@ export {
/** @type {import('./types.js').ServiceAbility[]} */
export const abilitiesAsStrings = [
Top.top.can,
Assert.assert.can,
Assert.equals.can,
Assert.inclusion.can,
Assert.index.can,
Assert.location.can,
Assert.partition.can,
Assert.relation.can,
Provider.add.can,
Space.space.can,
Space.info.can,
Expand Down
6 changes: 3 additions & 3 deletions packages/capabilities/src/space/blob.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* @module
*/
import { equals as SpaceBlobCapabilities } from 'uint8arrays/equals'
import { equals } from 'multiformats/bytes'
import { capability, Schema, fail, ok } from '@ucanto/validator'
import {
equalBlob,
Expand Down Expand Up @@ -101,7 +101,7 @@ export const remove = capability({
)
} else if (
delegated.nb.digest &&
!SpaceBlobCapabilities(delegated.nb.digest, claimed.nb.digest)
!equals(delegated.nb.digest, claimed.nb.digest)
) {
return fail(
`Link ${
Expand Down Expand Up @@ -167,7 +167,7 @@ export const get = capability({
)
} else if (
delegated.nb.digest &&
!SpaceBlobCapabilities(delegated.nb.digest, claimed.nb.digest)
!equals(delegated.nb.digest, claimed.nb.digest)
) {
return fail(
`Link ${
Expand Down
22 changes: 22 additions & 0 deletions packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ProofData,
uint64,
} from '@web3-storage/data-segment'
import * as AssertCaps from './assert.js'
import * as SpaceCaps from './space.js'
import * as provider from './provider.js'
import { top } from './top.js'
Expand Down Expand Up @@ -126,6 +127,20 @@ export interface DelegationNotFound extends Ucanto.Failure {

export type AccessConfirm = InferInvokedCapability<typeof AccessCaps.confirm>

// Assert

export type Assert = InferInvokedCapability<typeof AssertCaps.assert>
export type AssertEquals = InferInvokedCapability<typeof AssertCaps.equals>
export type AssertInclusion = InferInvokedCapability<
typeof AssertCaps.inclusion
>
export type AssertIndex = InferInvokedCapability<typeof AssertCaps.index>
export type AssertLocation = InferInvokedCapability<typeof AssertCaps.location>
export type AssertPartition = InferInvokedCapability<
typeof AssertCaps.partition
>
export type AssertRelation = InferInvokedCapability<typeof AssertCaps.relation>

// Usage

export type Usage = InferInvokedCapability<typeof UsageCaps.usage>
Expand Down Expand Up @@ -985,6 +1000,13 @@ export type ServiceAbility = TupleToUnion<ServiceAbilityArray>

export type ServiceAbilityArray = [
Top['can'],
Assert['can'],
AssertEquals['can'],
AssertInclusion['can'],
AssertIndex['can'],
AssertLocation['can'],
AssertPartition['can'],
AssertRelation['can'],
ProviderAdd['can'],
Space['can'],
SpaceInfo['can'],
Expand Down
41 changes: 39 additions & 2 deletions packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as API from '@ucanto/interface'
import { DID, Schema, fail, ok } from '@ucanto/validator'
import { equals } from 'uint8arrays/equals'
import { equals } from 'multiformats/bytes'
import { base58btc } from 'multiformats/bases/base58'

// e.g. did:web:storacha.network or did:web:staging.storacha.network
export const ProviderDID = DID.match({ method: 'web' })
Expand Down Expand Up @@ -59,7 +60,7 @@ export function equal(child, parent, constraint) {
return ok({})
} else {
return fail(
`Constrain violation: ${child} violates imposed ${constraint} constraint ${parent}`
`Constraint violation: ${child} violates imposed ${constraint} constraint ${parent}`
)
}
}
Expand Down Expand Up @@ -89,6 +90,42 @@ export const equalLink = (claimed, delegated) => {
}
}

/** @param {API.UnknownLink | { digest: Uint8Array }} linkOrDigest */
const toDigestBytes = (linkOrDigest) =>
'multihash' in linkOrDigest
? linkOrDigest.multihash.bytes
: linkOrDigest.digest

/**
* @template {API.ParsedCapability<API.Ability, API.URI, { content?: API.UnknownLink | { digest: Uint8Array } }>} T
* @param {T} claimed
* @param {T} delegated
* @returns {API.Result<{}, API.Failure>}
*/
export const equalLinkOrDigestContent = (claimed, delegated) => {
if (delegated.nb.content) {
const delegatedBytes = toDigestBytes(delegated.nb.content)
if (!claimed.nb.content) {
return fail(
`Constraint violation: undefined violates imposed content constraint ${base58btc.encode(
delegatedBytes
)}`
)
}
const claimedBytes = toDigestBytes(claimed.nb.content)
if (!equals(claimedBytes, delegatedBytes)) {
return fail(
`Constraint violation: ${base58btc.encode(
claimedBytes
)} violates imposed content constraint ${base58btc.encode(
delegatedBytes
)}`
)
}
}
return ok({})
}

/**
* @template {API.ParsedCapability<API.Ability, API.URI<'did:'>, {blob: { digest: Uint8Array, size: number }}>} T
* @param {T} claimed
Expand Down
Loading