From 7d94924f6994ee9f40163f14d3759212226ef8f8 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 May 2025 15:40:52 +0100 Subject: [PATCH 1/4] feat: add assert/* capabilities --- packages/capabilities/package.json | 6 +- packages/capabilities/src/assert.js | 133 ++++++ packages/capabilities/src/index.js | 9 + packages/capabilities/src/space/blob.js | 6 +- packages/capabilities/src/types.ts | 18 + packages/capabilities/src/utils.js | 31 +- .../test/capabilities/assert.test.js | 437 ++++++++++++++++++ .../test/capabilities/provider.test.js | 2 +- pnpm-lock.yaml | 125 ++--- 9 files changed, 675 insertions(+), 92 deletions(-) create mode 100644 packages/capabilities/src/assert.js create mode 100644 packages/capabilities/test/capabilities/assert.test.js diff --git a/packages/capabilities/package.json b/packages/capabilities/package.json index 901631f9e..6f69ce774 100644 --- a/packages/capabilities/package.json +++ b/packages/capabilities/package.json @@ -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" @@ -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:", diff --git a/packages/capabilities/src/assert.js b/packages/capabilities/src/assert.js new file mode 100644 index 000000000..f3e2d5098 --- /dev/null +++ b/packages/capabilities/src/assert.js @@ -0,0 +1,133 @@ +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({}) + ) +}) diff --git a/packages/capabilities/src/index.js b/packages/capabilities/src/index.js index dbbe77296..b2d36a3ae 100644 --- a/packages/capabilities/src/index.js +++ b/packages/capabilities/src/index.js @@ -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' @@ -27,6 +28,7 @@ import * as HTTP from './http.js' export { Access, + Assert, Provider, Space, Top, @@ -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, diff --git a/packages/capabilities/src/space/blob.js b/packages/capabilities/src/space/blob.js index 24ba98a56..cda518468 100644 --- a/packages/capabilities/src/space/blob.js +++ b/packages/capabilities/src/space/blob.js @@ -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, @@ -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 ${ @@ -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 ${ diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 01be04f3e..11a110ead 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -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' @@ -126,6 +127,16 @@ export interface DelegationNotFound extends Ucanto.Failure { export type AccessConfirm = InferInvokedCapability +// Assert + +export type Assert = InferInvokedCapability +export type AssertEquals = InferInvokedCapability +export type AssertInclusion = InferInvokedCapability +export type AssertIndex = InferInvokedCapability +export type AssertLocation = InferInvokedCapability +export type AssertPartition = InferInvokedCapability +export type AssertRelation = InferInvokedCapability + // Usage export type Usage = InferInvokedCapability @@ -985,6 +996,13 @@ export type ServiceAbility = TupleToUnion 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'], diff --git a/packages/capabilities/src/utils.js b/packages/capabilities/src/utils.js index 76a1b7aef..a9dfe8e0d 100644 --- a/packages/capabilities/src/utils.js +++ b/packages/capabilities/src/utils.js @@ -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' }) @@ -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}` ) } } @@ -89,6 +90,32 @@ 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} 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, {blob: { digest: Uint8Array, size: number }}>} T * @param {T} claimed diff --git a/packages/capabilities/test/capabilities/assert.test.js b/packages/capabilities/test/capabilities/assert.test.js new file mode 100644 index 000000000..5d7b6bcf8 --- /dev/null +++ b/packages/capabilities/test/capabilities/assert.test.js @@ -0,0 +1,437 @@ +import assert from 'assert' +import { access } from '@ucanto/validator' +import { Verifier } from '@ucanto/principal' +import * as Assert from '../../src/assert.js' +import * as Top 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 () => + Top.top.delegate({ + issuer: account, + audience: alice, + with: account.did(), + }) + +const assertTop = async () => + Assert.assert.delegate({ + issuer: account, + audience: alice, + with: account.did(), + proofs: [await top()], + }) + +describe('assert capabilities', function () { + it('assert/equals can be derived from *', async () => { + const equals = Assert.equals.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }, + proofs: [await top()], + }) + + const result = await access(await equals.delegate(), { + capability: Assert.equals, + 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, 'assert/equals') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }) + }) + + it('assert/equals can be derived from assert/*', async () => { + const equals = Assert.equals.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }, + proofs: [await assertTop()], + }) + + const result = await access(await equals.delegate(), { + capability: Assert.equals, + 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, 'assert/equals') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }) + }) + + it('assert/equals can be derived from assert/* derived from *', async () => { + const assertTop = await Assert.assert.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + proofs: [await top()], + }) + + const equals = Assert.equals.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }, + proofs: [assertTop], + }) + + const result = await access(await equals.delegate(), { + capability: Assert.equals, + 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, 'assert/equals') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }) + }) + + it('assert/equals should fail when escalating content constraint', async () => { + const delegation = await Assert.equals.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }, + proofs: [await top()], + }) + + const equals = Assert.equals.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test2'), + equals: await createCarCid('equivalent'), + }, + proofs: [delegation], + }) + + const result = await access(await equals.delegate(), { + capability: Assert.equals, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed content constraint')) + }) + + it('assert/equals should fail when escalating equals constraint', async () => { + const delegation = await Assert.equals.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent'), + }, + proofs: [await top()], + }) + + const equals = Assert.equals.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + equals: await createCarCid('equivalent2'), + }, + proofs: [delegation], + }) + + const result = await access(await equals.delegate(), { + capability: Assert.equals, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed equals constraint')) + }) + + it('assert/location can be derived from *', async () => { + const site = Assert.location.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + }, + proofs: [await top()], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + 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, 'assert/location') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + location: ['http://localhost/'], + }) + }) + + it('assert/location can be derived from assert/*', async () => { + const site = Assert.location.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + }, + proofs: [await assertTop()], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + 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, 'assert/location') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + location: ['http://localhost/'], + }) + }) + + it('assert/location can be derived from assert/* derived from *', async () => { + const assertTop = await Assert.assert.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + proofs: [await top()], + }) + + const site = Assert.location.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + }, + proofs: [assertTop], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + 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, 'assert/location') + assert.deepEqual(result.ok.capability.nb, { + content: await createCarCid('test'), + location: ['http://localhost/'], + }) + }) + + it('assert/location should fail when escalating content constraint', async () => { + const delegation = await Assert.location.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + }, + proofs: [await top()], + }) + + const site = Assert.location.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test2'), + location: ['http://localhost/'], + }, + proofs: [delegation], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed content constraint')) + }) + + it('assert/location should fail when escalating location constraint', async () => { + const delegation = await Assert.location.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + }, + proofs: [await top()], + }) + + const site = Assert.location.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost:3000/'], + }, + proofs: [delegation], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed location constraint')) + }) + + it('assert/location should fail when escalating range offset constraint', async () => { + const delegation = await Assert.location.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + range: { offset: 123, length: 456 } + }, + proofs: [await top()], + }) + + const site = Assert.location.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + range: { offset: 120, length: 456 } + }, + proofs: [delegation], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed offset constraint')) + }) + + it('assert/location should fail when escalating range length constraint', async () => { + const delegation = await Assert.location.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + range: { offset: 123, length: 456 } + }, + proofs: [await top()], + }) + + const site = Assert.location.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCarCid('test'), + location: ['http://localhost/'], + range: { offset: 123, length: 457 } + }, + proofs: [delegation], + }) + + const result = await access(await site.delegate(), { + capability: Assert.location, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed length constraint')) + }) +}) diff --git a/packages/capabilities/test/capabilities/provider.test.js b/packages/capabilities/test/capabilities/provider.test.js index 7b50fd8a8..69ef9d17d 100644 --- a/packages/capabilities/test/capabilities/provider.test.js +++ b/packages/capabilities/test/capabilities/provider.test.js @@ -326,7 +326,7 @@ describe('provider/add', function () { validateAuthorization, }) - assert.equal(result.error?.message.includes('Constrain violation'), true) + assert.equal(result.error?.message.includes('Constraint violation'), true) }) it('can not change delegated provider', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f154fa03..715d43729 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -535,9 +535,9 @@ importers: '@web3-storage/data-segment': specifier: ^5.2.0 version: 5.3.0 - uint8arrays: - specifier: ^5.0.3 - version: 5.1.0 + multiformats: + specifier: 'catalog:' + version: 13.3.3 devDependencies: '@arethetypeswrong/cli': specifier: 'catalog:' @@ -2629,21 +2629,12 @@ packages: resolution: {integrity: sha512-hUMFbDQ/nZN+1TLMi6iMO1QFz9RSV8yGG8S42WFPFma1d7VSNE0eMdJUmwjmtav22/iQkzHMmu6oTSfAvRGS8g==} engines: {node: '>=16'} - '@emnapi/core@1.3.1': - resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} - '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} - '@emnapi/runtime@1.3.1': - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} - '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - '@emnapi/wasi-threads@1.0.1': - resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} - '@emnapi/wasi-threads@1.0.2': resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} @@ -5977,6 +5968,7 @@ packages: '@walletconnect/ethereum-provider@2.9.2': resolution: {integrity: sha512-eO1dkhZffV1g7vpG19XUJTw09M/bwGUwwhy1mJ3AOPbOSbMPvwiCuRz2Kbtm1g9B0Jv15Dl+TvJ9vTgYF8zoZg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' peerDependencies: '@walletconnect/modal': '>=2' peerDependenciesMeta: @@ -6032,7 +6024,7 @@ packages: '@walletconnect/sign-client@2.9.2': resolution: {integrity: sha512-anRwnXKlR08lYllFMEarS01hp1gr6Q9XUgvacr749hoaC/AwGVlxYFdM8+MyYr3ozlA+2i599kjbK/mAebqdXg==} - deprecated: Reliability and performance greatly improved - please see https://github.com/WalletConnect/walletconnect-monorepo/releases + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -8499,14 +8491,6 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.4.4: resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} peerDependencies: @@ -12603,10 +12587,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -14536,34 +14516,18 @@ snapshots: dependencies: '@edge-runtime/primitives': 4.0.5 - '@emnapi/core@1.3.1': - dependencies: - '@emnapi/wasi-threads': 1.0.1 - tslib: 2.8.1 - '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.3.1': - dependencies: - tslib: 2.8.1 '@emnapi/runtime@1.4.3': dependencies: tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.0.1': - dependencies: - tslib: 2.8.1 '@emnapi/wasi-threads@1.0.2': dependencies: tslib: 2.8.1 - optional: true '@es-joy/jsdoccomment@0.36.1': dependencies: @@ -15458,7 +15422,7 @@ snapshots: '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.4.3 optional: true '@img/sharp-win32-ia32@0.33.5': @@ -16247,7 +16211,7 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.6.3 + semver: 7.7.1 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -16620,8 +16584,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@emnapi/core': 1.3.1 - '@emnapi/runtime': 1.3.1 + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@0.2.9': @@ -16706,7 +16670,7 @@ snapshots: '@npmcli/fs@4.0.0': dependencies: - semver: 7.6.3 + semver: 7.7.1 '@npmcli/redact@3.1.1': {} @@ -16769,9 +16733,9 @@ snapshots: npm-package-arg: 11.0.1 npm-run-path: 4.0.1 ora: 5.3.0 - semver: 7.6.3 + semver: 7.7.1 source-map-support: 0.5.19 - tinyglobby: 0.2.12 + tinyglobby: 0.2.13 ts-node: 10.9.1(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.6.3) tsconfig-paths: 4.2.0 tslib: 2.8.1 @@ -16812,9 +16776,9 @@ snapshots: npm-package-arg: 11.0.1 npm-run-path: 4.0.1 ora: 5.3.0 - semver: 7.6.3 + semver: 7.7.1 source-map-support: 0.5.19 - tinyglobby: 0.2.12 + tinyglobby: 0.2.13 ts-node: 10.9.1(@swc/core@1.11.11(@swc/helpers@0.5.15))(@types/node@22.13.10)(typescript@5.8.3) tsconfig-paths: 4.2.0 tslib: 2.8.1 @@ -17348,7 +17312,7 @@ snapshots: '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color @@ -17464,7 +17428,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.13.1 require-in-the-middle: 7.5.2 - semver: 7.6.3 + semver: 7.7.1 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -17476,7 +17440,7 @@ snapshots: '@types/shimmer': 1.2.0 import-in-the-middle: 1.13.1 require-in-the-middle: 7.5.2 - semver: 7.6.3 + semver: 7.7.1 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -18776,7 +18740,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.3 + semver: 7.7.1 tsutils: 3.21.0(typescript@4.9.5) optionalDependencies: typescript: 4.9.5 @@ -18791,7 +18755,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 1.4.3(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -18806,7 +18770,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 2.0.1(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: @@ -18820,7 +18784,7 @@ snapshots: fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.1 ts-api-utils: 2.0.1(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -18836,7 +18800,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) eslint: 8.57.1 eslint-scope: 5.1.1 - semver: 7.6.3 + semver: 7.7.1 transitivePeerDependencies: - supports-color - typescript @@ -20487,7 +20451,7 @@ snapshots: builtins@5.1.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 bundle-name@3.0.0: dependencies: @@ -21092,7 +21056,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.1) postcss-modules-values: 4.0.0(postcss@8.5.1) postcss-value-parser: 4.2.0 - semver: 7.6.3 + semver: 7.7.1 optionalDependencies: '@rspack/core': 1.3.6(@swc/helpers@0.5.15) webpack: 5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)) @@ -21366,7 +21330,7 @@ snapshots: require-package-name: 2.0.1 resolve: 1.22.10 resolve-from: 5.0.0 - semver: 7.6.3 + semver: 7.7.1 yargs: 16.2.0 transitivePeerDependencies: - supports-color @@ -22025,7 +21989,7 @@ snapshots: eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) @@ -22096,7 +22060,7 @@ snapshots: tinyglobby: 0.2.13 unrs-resolver: 1.7.0 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -22140,7 +22104,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -22206,7 +22170,7 @@ snapshots: escape-string-regexp: 4.0.0 eslint: 8.57.1 esquery: 1.6.0 - semver: 7.6.3 + semver: 7.7.1 spdx-expression-parse: 3.0.1 transitivePeerDependencies: - supports-color @@ -22257,7 +22221,7 @@ snapshots: is-core-module: 2.16.1 minimatch: 3.1.2 resolve: 1.22.10 - semver: 7.6.3 + semver: 7.7.1 eslint-plugin-next-on-pages@1.6.3(eslint@8.57.1): dependencies: @@ -22318,7 +22282,7 @@ snapshots: regexp-tree: 0.1.27 regjsparser: 0.9.1 safe-regex: 2.1.1 - semver: 7.6.3 + semver: 7.7.1 strip-indent: 3.0.0 eslint-scope@5.1.1: @@ -22634,10 +22598,6 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.4.3(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.4.4(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -22768,7 +22728,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.6.3 + semver: 7.7.1 tapable: 2.2.1 typescript: 5.8.3 webpack: 5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)) @@ -24562,7 +24522,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.6.3 + semver: 7.7.1 make-error@1.3.6: {} @@ -25104,7 +25064,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.6.3 + semver: 7.7.1 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -25115,7 +25075,7 @@ snapshots: dependencies: hosted-git-info: 7.0.2 proc-log: 3.0.0 - semver: 7.6.3 + semver: 7.7.1 validate-npm-package-name: 5.0.1 npm-package-arg@12.0.2: @@ -25196,7 +25156,7 @@ snapshots: open: 8.4.2 ora: 5.3.0 resolve.exports: 2.0.3 - semver: 7.6.3 + semver: 7.7.1 string-width: 4.2.3 tar-stream: 2.2.0 tmp: 0.2.3 @@ -25473,7 +25433,7 @@ snapshots: ky: 1.7.4 registry-auth-token: 5.0.3 registry-url: 6.0.1 - semver: 7.6.3 + semver: 7.7.1 package-manager-manager@0.2.0: dependencies: @@ -25804,7 +25764,7 @@ snapshots: cosmiconfig: 7.1.0 klona: 2.0.6 postcss: 8.5.1 - semver: 7.6.3 + semver: 7.7.1 webpack: 5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)) postcss-merge-longhand@6.0.5(postcss@8.5.1): @@ -26686,7 +26646,7 @@ snapshots: dependencies: color: 4.2.3 detect-libc: 2.0.4 - semver: 7.6.3 + semver: 7.7.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -27423,11 +27383,6 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.12: - dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -27507,7 +27462,7 @@ snapshots: chalk: 4.1.2 enhanced-resolve: 5.18.1 micromatch: 4.0.8 - semver: 7.6.3 + semver: 7.7.1 source-map: 0.7.4 typescript: 5.8.3 webpack: 5.99.6(@swc/core@1.11.11(@swc/helpers@0.5.15)) From ff77a1431181cca833e680714ad855532d82c22c Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 May 2025 16:04:51 +0100 Subject: [PATCH 2/4] chore: appease linter --- packages/capabilities/src/assert.js | 1 + packages/upload-api/src/test/handlers/ucan.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/capabilities/src/assert.js b/packages/capabilities/src/assert.js index f3e2d5098..0b9b24334 100644 --- a/packages/capabilities/src/assert.js +++ b/packages/capabilities/src/assert.js @@ -63,6 +63,7 @@ export const index = capability({ /** * 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 }) diff --git a/packages/upload-api/src/test/handlers/ucan.js b/packages/upload-api/src/test/handlers/ucan.js index 8c2b5a96d..da30fe73f 100644 --- a/packages/upload-api/src/test/handlers/ucan.js +++ b/packages/upload-api/src/test/handlers/ucan.js @@ -358,7 +358,7 @@ export const test = { }) .execute(context.connection) - assert.ok(String(revoke.out.error?.message).match(/Constrain violation/)) + assert.ok(String(revoke.out.error?.message).match(/Constraint violation/)) }, 'ucan/conclude writes a receipt for unknown tasks': async ( assert, From 3475319f1d6d53eca02bfeba01c11dfda2c21ea7 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 May 2025 16:11:30 +0100 Subject: [PATCH 3/4] test: add assert/index tests --- .../test/capabilities/assert.test.js | 174 +++++++++++++++++- 1 file changed, 173 insertions(+), 1 deletion(-) diff --git a/packages/capabilities/test/capabilities/assert.test.js b/packages/capabilities/test/capabilities/assert.test.js index 5d7b6bcf8..524e20b0a 100644 --- a/packages/capabilities/test/capabilities/assert.test.js +++ b/packages/capabilities/test/capabilities/assert.test.js @@ -9,7 +9,11 @@ import { mallory as account, bob, } from '../helpers/fixtures.js' -import { createCarCid, validateAuthorization } from '../helpers/utils.js' +import { + createCborCid, + createCarCid, + validateAuthorization, +} from '../helpers/utils.js' const top = async () => Top.top.delegate({ @@ -434,4 +438,172 @@ describe('assert capabilities', function () { assert.ok(result.error) assert(result.error.message.includes('violates imposed length constraint')) }) + + it('assert/index can be derived from *', async () => { + const index = Assert.index.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [await top()], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index can be derived from assert/*', async () => { + const index = Assert.index.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [await assertTop()], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index can be derived from assert/* derived from *', async () => { + const assertTop = await Assert.assert.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + proofs: [await top()], + }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [assertTop], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index should fail when escalating content constraint', async () => { + const delegation = await Assert.index.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [await top()], + }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test2'), + index: await createCarCid('index'), + }, + proofs: [delegation], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed content constraint')) + }) + + it('assert/index should fail when escalating index constraint', async () => { + const delegation = await Assert.index.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [await top()], + }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index2'), + }, + proofs: [delegation], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed index constraint')) + }) }) From 082a54d20640e943ce40612d86e745b153d4e178 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 May 2025 16:47:45 +0100 Subject: [PATCH 4/4] chore: appease linter --- packages/capabilities/src/assert.js | 64 ++-- packages/capabilities/src/types.ts | 8 +- packages/capabilities/src/utils.js | 14 +- .../test/capabilities/assert.test.js | 330 +++++++++--------- 4 files changed, 218 insertions(+), 198 deletions(-) diff --git a/packages/capabilities/src/assert.js b/packages/capabilities/src/assert.js index 0b9b24334..0ddb08961 100644 --- a/packages/capabilities/src/assert.js +++ b/packages/capabilities/src/assert.js @@ -1,11 +1,12 @@ 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() })) +const linkOrDigest = () => + Schema.link().or(Schema.struct({ digest: Schema.bytes() })) export const assert = capability({ can: 'assert/*', - with: URI.match({ protocol: 'did:' }) + with: URI.match({ protocol: 'did:' }), }) /** @@ -20,19 +21,22 @@ export const location = capability({ location: Schema.array(URI), range: Schema.struct({ offset: Schema.integer(), - length: Schema.integer().optional() + length: Schema.integer().optional(), }).optional(), - space: Schema.principal().optional() + space: Schema.principal().optional(), }), - derives: (claimed, delegated) => ( + 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.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({}) - ) + ok({}), }) /** @@ -46,8 +50,8 @@ export const inclusion = capability({ content: linkOrDigest(), /** CARv2 index CID */ includes: Schema.link({ version: 1 }), - proof: Schema.link({ version: 1 }).optional() - }) + proof: Schema.link({ version: 1 }).optional(), + }), }) /** @@ -66,14 +70,13 @@ export const index = capability({ * * @see https://github.com/storacha/specs/blob/main/w3-index.md */ - index: Schema.link({ version: 1 }) + index: Schema.link({ version: 1 }), }), - derives: (claimed, delegated) => ( + 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({}) - ) + ok({}), }) /** @@ -87,8 +90,8 @@ export const partition = capability({ content: linkOrDigest(), /** CIDs CID */ blocks: Schema.link({ version: 1 }).optional(), - parts: Schema.array(Schema.link({ version: 1 })) - }) + parts: Schema.array(Schema.link({ version: 1 })), + }), }) /** @@ -102,16 +105,18 @@ export const relation = capability({ /** 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({ + parts: Schema.array( + 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() - })) - }) + /** 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(), + }) + ), + }), }) /** @@ -123,12 +128,11 @@ export const equals = capability({ with: URI.match({ protocol: 'did:' }), nb: Schema.struct({ content: linkOrDigest(), - equals: Schema.link() + equals: Schema.link(), }), - derives: (claimed, delegated) => ( + derives: (claimed, delegated) => and(equalWith(claimed, delegated)) || and(equalLinkOrDigestContent(claimed, delegated)) || and(equal(claimed.nb.equals, delegated.nb.equals, 'equals')) || - ok({}) - ) + ok({}), }) diff --git a/packages/capabilities/src/types.ts b/packages/capabilities/src/types.ts index 11a110ead..b0f29b254 100644 --- a/packages/capabilities/src/types.ts +++ b/packages/capabilities/src/types.ts @@ -131,10 +131,14 @@ export type AccessConfirm = InferInvokedCapability export type Assert = InferInvokedCapability export type AssertEquals = InferInvokedCapability -export type AssertInclusion = InferInvokedCapability +export type AssertInclusion = InferInvokedCapability< + typeof AssertCaps.inclusion +> export type AssertIndex = InferInvokedCapability export type AssertLocation = InferInvokedCapability -export type AssertPartition = InferInvokedCapability +export type AssertPartition = InferInvokedCapability< + typeof AssertCaps.partition +> export type AssertRelation = InferInvokedCapability // Usage diff --git a/packages/capabilities/src/utils.js b/packages/capabilities/src/utils.js index a9dfe8e0d..796fb22e3 100644 --- a/packages/capabilities/src/utils.js +++ b/packages/capabilities/src/utils.js @@ -106,11 +106,21 @@ 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)}`) + 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 fail( + `Constraint violation: ${base58btc.encode( + claimedBytes + )} violates imposed content constraint ${base58btc.encode( + delegatedBytes + )}` + ) } } return ok({}) diff --git a/packages/capabilities/test/capabilities/assert.test.js b/packages/capabilities/test/capabilities/assert.test.js index 524e20b0a..8b6c08802 100644 --- a/packages/capabilities/test/capabilities/assert.test.js +++ b/packages/capabilities/test/capabilities/assert.test.js @@ -364,7 +364,9 @@ describe('assert capabilities', function () { }) assert.ok(result.error) - assert(result.error.message.includes('violates imposed location constraint')) + assert( + result.error.message.includes('violates imposed location constraint') + ) }) it('assert/location should fail when escalating range offset constraint', async () => { @@ -375,7 +377,7 @@ describe('assert capabilities', function () { nb: { content: await createCarCid('test'), location: ['http://localhost/'], - range: { offset: 123, length: 456 } + range: { offset: 123, length: 456 }, }, proofs: [await top()], }) @@ -387,7 +389,7 @@ describe('assert capabilities', function () { nb: { content: await createCarCid('test'), location: ['http://localhost/'], - range: { offset: 120, length: 456 } + range: { offset: 120, length: 456 }, }, proofs: [delegation], }) @@ -411,7 +413,7 @@ describe('assert capabilities', function () { nb: { content: await createCarCid('test'), location: ['http://localhost/'], - range: { offset: 123, length: 456 } + range: { offset: 123, length: 456 }, }, proofs: [await top()], }) @@ -423,7 +425,7 @@ describe('assert capabilities', function () { nb: { content: await createCarCid('test'), location: ['http://localhost/'], - range: { offset: 123, length: 457 } + range: { offset: 123, length: 457 }, }, proofs: [delegation], }) @@ -440,170 +442,170 @@ describe('assert capabilities', function () { }) it('assert/index can be derived from *', async () => { - const index = Assert.index.invoke({ - issuer: alice, - audience: w3, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index'), - }, - proofs: [await top()], - }) - - const result = await access(await index.delegate(), { - capability: Assert.index, - 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, 'assert/index') - assert.deepEqual(result.ok.capability.nb, { + const index = Assert.index.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [await top()], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index can be derived from assert/*', async () => { + const index = Assert.index.invoke({ + issuer: alice, + audience: w3, + with: account.did(), + nb: { content: await createCborCid('test'), index: await createCarCid('index'), - }) - }) - - it('assert/index can be derived from assert/*', async () => { - const index = Assert.index.invoke({ - issuer: alice, - audience: w3, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index'), - }, - proofs: [await assertTop()], - }) - - const result = await access(await index.delegate(), { - capability: Assert.index, - 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, 'assert/index') - assert.deepEqual(result.ok.capability.nb, { + }, + proofs: [await assertTop()], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index can be derived from assert/* derived from *', async () => { + const assertTop = await Assert.assert.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + proofs: [await top()], + }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index'), + }, + proofs: [assertTop], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + 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, 'assert/index') + assert.deepEqual(result.ok.capability.nb, { + content: await createCborCid('test'), + index: await createCarCid('index'), + }) + }) + + it('assert/index should fail when escalating content constraint', async () => { + const delegation = await Assert.index.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { content: await createCborCid('test'), index: await createCarCid('index'), - }) - }) - - it('assert/index can be derived from assert/* derived from *', async () => { - const assertTop = await Assert.assert.delegate({ - issuer: alice, - audience: bob, - with: account.did(), - proofs: [await top()], - }) - - const index = Assert.index.invoke({ - issuer: bob, - audience: w3, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index'), - }, - proofs: [assertTop], - }) - - const result = await access(await index.delegate(), { - capability: Assert.index, - 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, 'assert/index') - assert.deepEqual(result.ok.capability.nb, { + }, + proofs: [await top()], + }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test2'), + index: await createCarCid('index'), + }, + proofs: [delegation], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed content constraint')) + }) + + it('assert/index should fail when escalating index constraint', async () => { + const delegation = await Assert.index.delegate({ + issuer: alice, + audience: bob, + with: account.did(), + nb: { content: await createCborCid('test'), index: await createCarCid('index'), - }) - }) - - it('assert/index should fail when escalating content constraint', async () => { - const delegation = await Assert.index.delegate({ - issuer: alice, - audience: bob, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index'), - }, - proofs: [await top()], - }) - - const index = Assert.index.invoke({ - issuer: bob, - audience: w3, - with: account.did(), - nb: { - content: await createCborCid('test2'), - index: await createCarCid('index'), - }, - proofs: [delegation], - }) - - const result = await access(await index.delegate(), { - capability: Assert.index, - principal: Verifier, - authority: w3, - validateAuthorization, - }) - - assert.ok(result.error) - assert(result.error.message.includes('violates imposed content constraint')) - }) - - it('assert/index should fail when escalating index constraint', async () => { - const delegation = await Assert.index.delegate({ - issuer: alice, - audience: bob, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index'), - }, - proofs: [await top()], - }) - - const index = Assert.index.invoke({ - issuer: bob, - audience: w3, - with: account.did(), - nb: { - content: await createCborCid('test'), - index: await createCarCid('index2'), - }, - proofs: [delegation], - }) - - const result = await access(await index.delegate(), { - capability: Assert.index, - principal: Verifier, - authority: w3, - validateAuthorization, - }) - - assert.ok(result.error) - assert(result.error.message.includes('violates imposed index constraint')) + }, + proofs: [await top()], }) + + const index = Assert.index.invoke({ + issuer: bob, + audience: w3, + with: account.did(), + nb: { + content: await createCborCid('test'), + index: await createCarCid('index2'), + }, + proofs: [delegation], + }) + + const result = await access(await index.delegate(), { + capability: Assert.index, + principal: Verifier, + authority: w3, + validateAuthorization, + }) + + assert.ok(result.error) + assert(result.error.message.includes('violates imposed index constraint')) + }) })