diff --git a/Readme.md b/Readme.md index 85d113f5..c1f87fe0 100644 --- a/Readme.md +++ b/Readme.md @@ -36,7 +36,7 @@ import { capability, URI, Link, Failure } from '@ucanto/server' const Add = capability({ can: 'file/link', with: URI.match({ protocol: 'file:' }), - caveats: { link: Link }, + nb: { link: Link }, derives: (claimed, delegated) => // Can be derived if claimed capability path is contained in the delegated // capability path. @@ -47,7 +47,7 @@ const Add = capability({ const ensureTrailingDelimiter = uri => (uri.endsWith('/') ? uri : `${uri}/`) ``` -> Please note that the library guarantees that both `claimed` and `delegated` capabilties will have `{can: "file/link", with: string, uri: URL, caveats: { link?: CID }}` +> Please note that the library guarantees that both `claimed` and `delegated` capabilties will have `{can: "file/link", with: string nb: { link?: CID }}` > type inferred from the definition. > > We will explore more complicated cases later where a capability may be derived from a different capability or even a set. @@ -61,10 +61,10 @@ import { provide, Failure, MalformedCapability } from '@ucanto/server' const service = (context: { store: Map }) => { const add = provide(Add, ({ capability, invocation }) => { - store.set(capability.uri.href, capability.caveats.link) + store.set(capability.uri.href, capability.nb.link) return { with: capability.with, - link: capability.caveats.link, + link: capability.nb.link, } }) diff --git a/packages/client/test/client.spec.js b/packages/client/test/client.spec.js index 6d0eb35e..09ce87e4 100644 --- a/packages/client/test/client.spec.js +++ b/packages/client/test/client.spec.js @@ -10,7 +10,7 @@ import fetch from '@web-std/fetch' test('encode inovocation', async () => { /** @type {Client.ConnectionView} */ const connection = Client.connect({ - id: w3.principal, + id: w3, channel: HTTP.open({ url: new URL('about:blank'), fetch }), encoder: CAR, decoder: CBOR, @@ -26,7 +26,7 @@ test('encode inovocation', async () => { capability: { can: 'store/add', with: alice.did(), - link: car.cid, + nb: { link: car.cid }, }, proofs: [], }) @@ -49,8 +49,7 @@ test('encode inovocation', async () => { { can: 'store/add', with: alice.did(), - // @ts-ignore - link: car.cid, + nb: { link: car.cid }, }, ]) }) @@ -62,7 +61,7 @@ test('encode delegated invocation', async () => { /** @type {Client.ConnectionView} */ const connection = Client.connect({ - id: w3.principal, + id: w3, channel: HTTP.open({ url: new URL('about:blank'), fetch }), encoder: CAR, decoder: CBOR, @@ -85,7 +84,7 @@ test('encode delegated invocation', async () => { capability: { can: 'store/add', with: alice.did(), - link: car.cid, + nb: { link: car.cid }, }, proofs: [proof], }) @@ -112,7 +111,7 @@ test('encode delegated invocation', async () => { { can: 'store/add', with: alice.did(), - link: car.cid, + nb: { link: car.cid }, }, ]) @@ -140,12 +139,12 @@ test('encode delegated invocation', async () => { const service = Service.create() /** @type {Client.ConnectionView} */ const connection = Client.connect({ - id: w3.principal, + id: w3, channel: HTTP.open({ url: new URL('about:blank'), fetch: async (url, input) => { const invocations = await CAR.decode(input) - const promises = invocations.map((invocation) => { + const promises = invocations.map(invocation => { const [capabality] = invocation.capabilities switch (capabality.can) { case 'store/add': { @@ -187,7 +186,7 @@ test('execute', async () => { capability: { can: 'store/add', with: alice.did(), - link: car.cid, + nb: { link: car.cid }, }, proofs: [], }) @@ -198,7 +197,7 @@ test('execute', async () => { capability: { can: 'store/remove', with: alice.did(), - link: car.cid, + nb: { link: car.cid }, }, }) diff --git a/packages/client/test/fixtures.js b/packages/client/test/fixtures.js index 197b8d98..678e9516 100644 --- a/packages/client/test/fixtures.js +++ b/packages/client/test/fixtures.js @@ -1,18 +1,18 @@ -import { SigningPrincipal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = SigningPrincipal.parse( +export const alice = ed25519.parse( 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' ) /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = SigningPrincipal.parse( +export const bob = ed25519.parse( 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' ) /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = SigningPrincipal.parse( +export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) -export const service = SigningPrincipal.parse( +export const service = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/client/test/service.js b/packages/client/test/service.js index cac432bd..cb687a47 100644 --- a/packages/client/test/service.js +++ b/packages/client/test/service.js @@ -7,7 +7,7 @@ import { the } from './services/util.js' * @typedef {{ * can: "store/add" * with: API.DID - * link: API.Link + * nb: { link: API.Link } * }} Add * * @typedef {{ @@ -26,7 +26,7 @@ import { the } from './services/util.js' * @typedef {{ * can: "store/remove" * with: API.DID - * link: API.Link + * nb: { link: API.Link } * }} Remove */ @@ -58,20 +58,20 @@ class StorageService { // if (auth.ok) { const result = await this.storage.add( capability.with, - capability.link, + capability.nb.link, /** @type {any} */ (ucan).cid ) if (!result.error) { if (result.status === 'in-s3') { return { with: capability.with, - link: capability.link, + link: capability.nb.link, status: the('done'), } } else { return { with: capability.with, - link: capability.link, + link: capability.nb.link, status: the('upload'), url: 'http://localhost:9090/', } @@ -90,7 +90,7 @@ class StorageService { // if (access.ok) { const remove = await this.storage.remove( capability.with, - capability.link, + capability.nb.link, /** @type {any} */ (ucan).link ) if (remove?.error) { @@ -154,6 +154,6 @@ class Main { * @param {Partial} [config] * @returns {Service} */ -export const create = (config) => new Main(config) +export const create = config => new Main(config) export { Storage, Accounts } diff --git a/packages/core/package.json b/packages/core/package.json index 4061d8dd..ac695a53 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,7 @@ "dependencies": { "@ipld/car": "^4.1.5", "@ipld/dag-cbor": "^7.0.3", - "@ipld/dag-ucan": "3.0.0-beta", + "@ipld/dag-ucan": "^4.0.0-beta", "@ucanto/interface": "^1.0.0", "multiformats": "^9.8.1" }, diff --git a/packages/core/src/delegation.js b/packages/core/src/delegation.js index c7eadee9..0419efbe 100644 --- a/packages/core/src/delegation.js +++ b/packages/core/src/delegation.js @@ -15,7 +15,7 @@ export const isLink = * @param {API.Proof} proof * @return {proof is API.Delegation} */ -export const isDelegation = (proof) => !Link.isLink(proof) +export const isDelegation = proof => !Link.isLink(proof) /** * Represents UCAN chain view over the set of DAG UCAN nodes. You can think of @@ -73,14 +73,14 @@ export class Delegation { } /** - * @type {API.UCAN.Principal} + * @type {API.Principal} */ get issuer() { return this.data.issuer } /** - * @type {API.UCAN.Principal} + * @type {API.Principal} */ get audience() { return this.data.audience @@ -166,9 +166,8 @@ const decode = ({ bytes }) => { * not set it defaults to 30 seconds from now. Returns UCAN in primary - IPLD * representation. * - * @template {number} A * @template {API.Capabilities} C - * @param {API.DelegationOptions} data + * @param {API.DelegationOptions} data * @param {API.EncodeOptions} [options] * @returns {Promise>} */ @@ -230,7 +229,7 @@ const exportDAG = function* (root, blocks) { * @param {Iterable} dag * @returns {API.Delegation} */ -export const importDAG = (dag) => { +export const importDAG = dag => { /** @type {Array<[string, API.Block]>} */ let entries = [] for (const block of dag) { @@ -262,7 +261,7 @@ export const create = ({ root, blocks }) => new Delegation(root, blocks) /** * @param {API.Delegation} delegation */ -const proofs = (delegation) => { +const proofs = delegation => { /** @type {API.Proof[]} */ const proofs = [] const { root, blocks } = delegation diff --git a/packages/core/src/invocation.js b/packages/core/src/invocation.js index ed6bd1ee..07ec3994 100644 --- a/packages/core/src/invocation.js +++ b/packages/core/src/invocation.js @@ -6,7 +6,7 @@ import { delegate } from './delegation.js' * @param {API.InvocationOptions} options * @return {API.IssuedInvocationView} */ -export const invoke = (options) => new IssuedInvocation(options) +export const invoke = options => new IssuedInvocation(options) /** * @template {API.Capability} Capability diff --git a/packages/core/src/lib.js b/packages/core/src/lib.js index 97443272..598eaabe 100644 --- a/packages/core/src/lib.js +++ b/packages/core/src/lib.js @@ -10,3 +10,4 @@ export { decode as decodeLink, } from './link.js' export * as UCAN from '@ipld/dag-ucan' +export * as DID from '@ipld/dag-ucan/did' diff --git a/packages/core/test/fixtures.js b/packages/core/test/fixtures.js index 197b8d98..678e9516 100644 --- a/packages/core/test/fixtures.js +++ b/packages/core/test/fixtures.js @@ -1,18 +1,18 @@ -import { SigningPrincipal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = SigningPrincipal.parse( +export const alice = ed25519.parse( 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' ) /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = SigningPrincipal.parse( +export const bob = ed25519.parse( 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' ) /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = SigningPrincipal.parse( +export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) -export const service = SigningPrincipal.parse( +export const service = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/core/test/invocation.spec.js b/packages/core/test/invocation.spec.js index 1c8e491b..08c65f43 100644 --- a/packages/core/test/invocation.spec.js +++ b/packages/core/test/invocation.spec.js @@ -35,7 +35,7 @@ test('expired invocation', async () => { const expiration = UCAN.now() - 5 const invocation = invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'store/add', with: alice.did(), @@ -59,7 +59,7 @@ test('invocation with notBefore', async () => { const notBefore = UCAN.now() + 500 const invocation = invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'store/add', with: alice.did(), @@ -81,7 +81,7 @@ test('invocation with notBefore', async () => { test('invocation with nonce', async () => { const invocation = invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'store/add', with: alice.did(), @@ -103,7 +103,7 @@ test('invocation with nonce', async () => { test('invocation with facts', async () => { const invocation = invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'store/add', with: alice.did(), diff --git a/packages/core/test/lib.spec.js b/packages/core/test/lib.spec.js index d5e6597d..a6ae56fe 100644 --- a/packages/core/test/lib.spec.js +++ b/packages/core/test/lib.spec.js @@ -228,7 +228,7 @@ test('create delegation chain', async () => { }) } - const data = await UCAN.issue({ + const ucan = await UCAN.issue({ issuer: mallory, audience: service, capabilities: [ @@ -240,8 +240,8 @@ test('create delegation chain', async () => { proofs: [delegation.cid], }) - const { cid, bytes } = await UCAN.write(data) - const root = { cid, data, bytes } + const { cid, bytes } = await UCAN.write(ucan) + const root = { cid, data: ucan.model, bytes } { const invocation = Delegation.create({ @@ -346,7 +346,7 @@ test('import delegation', async () => { }) const replica = Delegation.import(original.export()) - assert.deepEqual(original, replica) + assert.deepEqual(replica, original) assert.equal(replica.issuer.did(), alice.did()) assert.equal(replica.audience.did(), bob.did()) @@ -404,7 +404,7 @@ test('issue chained delegation', async () => { return assert.fail('must be a delegation') } - assert.deepEqual(proof.bytes, delegation.bytes) + assert.deepEqual(delegation.bytes, proof.bytes) assert.deepEqual([...proof.export()], [proof.root]) assert.deepEqual([...delegation.export()], [proof.root]) diff --git a/packages/interface/package.json b/packages/interface/package.json index 5d05f8fe..6f7d89e9 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -23,7 +23,7 @@ "build": "tsc --build" }, "dependencies": { - "@ipld/dag-ucan": "3.0.0-beta", + "@ipld/dag-ucan": "^4.0.0-beta", "multiformats": "^9.8.1" }, "devDependencies": { diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index b1fb2331..affad7ef 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -5,7 +5,7 @@ import { Result, Failure, PrincipalParser, - SigningPrincipal, + Signer, URI, Await, IssuedInvocationView, @@ -132,26 +132,31 @@ export interface TheCapabilityParser> readonly can: M['value']['can'] create( - input: InferCreateOptions - ): Capability & - M['value']['caveats'] + input: InferCreateOptions + ): M['value'] invoke( - options: InvokeCapabilityOptions - ): IssuedInvocationView< - Capability & - M['value']['caveats'] - > + options: InferInvokeOptions + ): IssuedInvocationView } -export type InferCreateOptions = { - with: Resource - caveats?: {} -} & Optionalize<{ - with: R - caveats: InferCaveatParams -}> +export type InferInvokedCapability = C +// keyof C['nb'] extends never +// ? { can: C['can']; with: C['with']; nb?: never } +// : { can: C['can']; with: C['with']; nb: C['nb'] } + +export type InferCreateOptions = + // If capability has no NB we want to prevent passing it into + // .create funciton so we make `nb` as optional `never` type so + // it can not be satisfied + keyof C extends never ? { with: R; nb?: never } : { with: R; nb: C } +export type InferInvokeOptions< + R extends Resource, + C extends {} | undefined +> = UCANOptions & { issuer: Signer } & InferCreateOptions + +export type EmptyObject = { [key: string | number | symbol]: never } type Optionalize = InferRequried & InferOptional type InferOptional = { @@ -162,14 +167,6 @@ type InferRequried = { [K in keyof T as T[K] | undefined extends T[K] ? never : K]: T[K] } -export type InvokeCapabilityOptions< - R extends Resource, - C extends {} -> = UCANOptions & - InferCreateOptions & { - issuer: SigningPrincipal - } - export interface CapabilityParser extends View { /** * Defines capability that is either `this` or the the given `other`. This @@ -180,7 +177,6 @@ export interface CapabilityParser extends View { * other. */ or(other: MatchSelector): CapabilityParser - /** * Combines this capability and the other into a capability group. This allows * you to define right amplifications e.g `file/read+write` could be derived @@ -260,16 +256,13 @@ export type InferMatch = Members extends [] ? [M, ...InferMatch] : never -export interface ParsedCapability< +export type ParsedCapability< Can extends Ability = Ability, Resource extends URI = URI, C extends object = {} -> { - can: Can - with: Resource['href'] - uri: Resource - caveats: C -} +> = keyof C extends never + ? { can: Can; with: Resource; nb?: C } + : { can: Can; with: Resource; nb: C } export type InferCaveats = Optionalize<{ [K in keyof C]: C[K] extends Decoder ? T : never @@ -282,7 +275,8 @@ export interface Descriptor< > { can: A with: Decoder - caveats?: C + + nb?: C derives?: Derives< ParsedCapability>, diff --git a/packages/interface/src/lib.ts b/packages/interface/src/lib.ts index e877996d..b7251b7d 100644 --- a/packages/interface/src/lib.ts +++ b/packages/interface/src/lib.ts @@ -13,13 +13,11 @@ import { Phantom, Resource, Signature, + Principal, + Verifier, + Signer, } from '@ipld/dag-ucan' import * as UCAN from '@ipld/dag-ucan' -import type { - Principal, - PrincipalParser, - SigningPrincipal, -} from './principal.js' import { CanIssue, InvalidAudience, @@ -33,14 +31,13 @@ export type { MultibaseDecoder, MultibaseEncoder, } from 'multiformats/bases/interface' -export * from './principal.js' export * from './capability.js' export * from './transport.js' export type { Transport, Principal, - PrincipalParser, - SigningPrincipal, + Verifier, + Signer, Phantom, Tuple, DID, @@ -80,11 +77,8 @@ export interface UCANOptions { proofs?: Proof[] } -export interface DelegationOptions< - C extends Capabilities, - A extends number = number -> extends UCANOptions { - issuer: SigningPrincipal +export interface DelegationOptions extends UCANOptions { + issuer: Signer audience: Principal capabilities: C proofs?: Proof[] @@ -93,17 +87,17 @@ export interface DelegationOptions< export interface Delegation { readonly root: UCANBlock /** - * Map of all the IPLD blocks that where included with this delegation DAG. - * Usually this would be blocks corresponding to proofs, however it may - * also contain other blocks e.g. things that `capabilities` or `facts` may - * link. - * It is not guaranteed to include all the blocks of this DAG, as it represents - * a partial DAG of the delegation desired for transporting. - * - * Also note that map may contain blocks that are not part of this - * delegation DAG. That is because `Delegation` is usually constructed as - * view / selection over the CAR which may contain bunch of other blocks. - */ + * Map of all the IPLD blocks that where included with this delegation DAG. + * Usually this would be blocks corresponding to proofs, however it may + * also contain other blocks e.g. things that `capabilities` or `facts` may + * link. + * It is not guaranteed to include all the blocks of this DAG, as it represents + * a partial DAG of the delegation desired for transporting. + * + * Also note that map may contain blocks that are not part of this + * delegation DAG. That is because `Delegation` is usually constructed as + * view / selection over the CAR which may contain bunch of other blocks. + */ readonly blocks: Map readonly cid: UCANLink @@ -132,13 +126,13 @@ export interface Invocation export interface InvocationOptions extends UCANOptions { - issuer: SigningPrincipal + issuer: Signer capability: C } export interface IssuedInvocation extends DelegationOptions<[C]> { - readonly issuer: SigningPrincipal + readonly issuer: Signer readonly audience: Principal readonly capabilities: [C] @@ -382,11 +376,14 @@ export type Service = Record< export type Await = T | PromiseLike | Promise export type Protocol = `${Scheme}:` -export interface URI

extends URL { - protocol: P - href: `${P}${string}` -} -export type URIString

= `${URI['protocol']}${string}` & { - protocol?: Protocol +export type URI

= `${P}${string}` & + // ⚠️ Without phantom type TS does not seem to retain `P` type + // resulting in `${string}${string}` instead. + Phantom<{ + protocol: P + }> + +export interface PrincipalParser { + parse(did: UCAN.DID): UCAN.Verifier } diff --git a/packages/interface/src/principal.ts b/packages/interface/src/principal.ts deleted file mode 100644 index 347e44f4..00000000 --- a/packages/interface/src/principal.ts +++ /dev/null @@ -1,20 +0,0 @@ -export * as UCAN from '@ipld/dag-ucan' -import type * as UCAN from '@ipld/dag-ucan' - -export interface PrincipalParser { - parse(did: UCAN.DID): Principal -} - -export interface Principal - extends ArrayBufferView, - UCAN.Verifier { - bytes: Uint8Array -} - -export interface SigningPrincipal - extends Principal, - UCAN.Signer { - principal: Principal - bytes: Uint8Array - // secret: Uint8Array -} diff --git a/packages/interface/src/query.ts b/packages/interface/src/query.ts index fa819f3d..5f2b5ba5 100644 --- a/packages/interface/src/query.ts +++ b/packages/interface/src/query.ts @@ -10,7 +10,7 @@ import type { ConnectionView, Service, Principal, - SigningPrincipal, + Signer, Failure, } from './lib.js' @@ -137,7 +137,7 @@ type Store = { } declare var store: Store declare var channel: ConnectionView<{ store: Store }> -declare const alice: SigningPrincipal +declare const alice: Signer declare const bob: Principal declare const car: UCAN.Link diff --git a/packages/principal/package.json b/packages/principal/package.json index d641bc5c..4ce0c7a4 100644 --- a/packages/principal/package.json +++ b/packages/principal/package.json @@ -22,12 +22,12 @@ "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/**/*.spec.js", "test": "npm run test:node", - "coverage": "c8 --reporter=html mocha test/test-*.js && npm_config_yes=true npx st -d coverage -p 8080", + "coverage": "c8 --reporter=html mocha test/**/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", "typecheck": "tsc --build", "build": "tsc --build" }, "dependencies": { - "@ipld/dag-ucan": "3.0.0-beta", + "@ipld/dag-ucan": "^4.0.0-beta", "@noble/ed25519": "^1.7.0", "@ucanto/interface": "^1.0.0", "multiformats": "^9.8.1" @@ -50,15 +50,15 @@ "*": [ "dist/*" ], - "dist/src/lib.d.ts": [ - "dist/src/lib.d.ts" + "ed25519": [ + "dist/src/ed25519.d.ts" ] } }, "exports": { - ".": { - "types": "./dist/src/lib.d.ts", - "import": "./src/lib.js" + "./ed25519": { + "types": "./dist/src/ed25519.d.ts", + "import": "./src/ed25519.js" } }, "c8": { diff --git a/packages/principal/src/ed25519.js b/packages/principal/src/ed25519.js new file mode 100644 index 00000000..143f57d7 --- /dev/null +++ b/packages/principal/src/ed25519.js @@ -0,0 +1,3 @@ +export * from './ed25519/signer.js' +export * as Verifier from './ed25519/verifier.js' +export * as Signer from './ed25519/signer.js' diff --git a/packages/principal/src/ed25519/signer.js b/packages/principal/src/ed25519/signer.js new file mode 100644 index 00000000..83cbabd3 --- /dev/null +++ b/packages/principal/src/ed25519/signer.js @@ -0,0 +1,159 @@ +import * as ED25519 from '@noble/ed25519' +import { varint } from 'multiformats' +import * as API from '@ucanto/interface' +import * as Verifier from './verifier.js' +import { base64pad, base64url } from 'multiformats/bases/base64' +import * as Signature from '@ipld/dag-ucan/signature' + +export const code = 0x1300 +export const name = Verifier.name + +const PRIVATE_TAG_SIZE = varint.encodingLength(code) +const PUBLIC_TAG_SIZE = varint.encodingLength(Verifier.code) +const KEY_SIZE = 32 +const SIZE = PRIVATE_TAG_SIZE + KEY_SIZE + PUBLIC_TAG_SIZE + KEY_SIZE + +export const PUB_KEY_OFFSET = PRIVATE_TAG_SIZE + KEY_SIZE + +/** + * @typedef {API.Signer<"key", typeof Signature.EdDSA> & Uint8Array & { verifier: API.Verifier<"key", typeof Signature.EdDSA> }} Signer + */ + +/** + * Generates new issuer by generating underlying ED25519 keypair. + * @returns {Promise} + */ +export const generate = () => derive(ED25519.utils.randomPrivateKey()) + +/** + * Derives issuer from 32 byte long secret key. + * @param {Uint8Array} secret + * @returns {Promise} + */ +export const derive = async secret => { + if (secret.byteLength !== KEY_SIZE) { + throw new Error( + `Expected Uint8Array with byteLength of ${KEY_SIZE} instead not ${secret.byteLength}` + ) + } + + const publicKey = await ED25519.getPublicKey(secret) + const signer = new Ed25519Signer(SIZE) + + varint.encodeTo(code, signer, 0) + signer.set(secret, PRIVATE_TAG_SIZE) + + varint.encodeTo(Verifier.code, signer, PRIVATE_TAG_SIZE + KEY_SIZE) + signer.set(publicKey, PRIVATE_TAG_SIZE + KEY_SIZE + PUBLIC_TAG_SIZE) + + return signer +} + +/** + * @param {Uint8Array} bytes + * @returns {Signer} + */ +export const decode = bytes => { + if (bytes.byteLength !== SIZE) { + throw new Error( + `Expected Uint8Array with byteLength of ${SIZE} instead not ${bytes.byteLength}` + ) + } + + { + const [keyCode] = varint.decode(bytes) + if (keyCode !== code) { + throw new Error(`Given bytes must be a multiformat with ${code} tag`) + } + } + + { + const [code] = varint.decode(bytes.subarray(PUB_KEY_OFFSET)) + if (code !== Verifier.code) { + throw new Error( + `Given bytes must contain public key in multiformats with ${Verifier.code} tag` + ) + } + } + + return new Ed25519Signer(bytes) +} + +/** + * @param {Signer} signer + * @return {API.ByteView} + */ +export const encode = signer => signer + +/** + * @template {string} Prefix + * @param {Signer} signer + * @param {API.MultibaseEncoder} [encoder] + */ +export const format = (signer, encoder) => (encoder || base64pad).encode(signer) + +/** + * @template {string} Prefix + * @param {string} principal + * @param {API.MultibaseDecoder} [decoder] + * @returns {Signer} + */ +export const parse = (principal, decoder) => + decode((decoder || base64pad).decode(principal)) + +/** + * @implements {API.Signer<'key', typeof Signature.EdDSA>} + */ +class Ed25519Signer extends Uint8Array { + get verifier() { + const bytes = new Uint8Array(this.buffer, PRIVATE_TAG_SIZE + KEY_SIZE) + const verifier = Verifier.decode(bytes) + + Object.defineProperties(this, { + verifier: { + value: verifier, + }, + }) + + return verifier + } + + /** + * Raw public key without multiformat code. + */ + get secret() { + const secret = new Uint8Array(this.buffer, PRIVATE_TAG_SIZE, KEY_SIZE) + Object.defineProperties(this, { + secret: { + value: secret, + }, + }) + + return secret + } + + /** + * DID of this principal in `did:key` format. + */ + did() { + return this.verifier.did() + } + + /** + * @template T + * @param {API.ByteView} payload + * @returns {Promise>} + */ + async sign(payload) { + const raw = await ED25519.sign(payload, this.secret) + + return Signature.create(this.signatureCode, raw) + } + + get signatureAlgorithm() { + return 'EdDSA' + } + get signatureCode() { + return Signature.EdDSA + } +} diff --git a/packages/principal/src/principal.js b/packages/principal/src/ed25519/verifier.js similarity index 57% rename from packages/principal/src/principal.js rename to packages/principal/src/ed25519/verifier.js index 49dd2661..158eae13 100644 --- a/packages/principal/src/principal.js +++ b/packages/principal/src/ed25519/verifier.js @@ -2,27 +2,34 @@ import * as DID from '@ipld/dag-ucan/did' import * as ED25519 from '@noble/ed25519' import { varint } from 'multiformats' import * as API from '@ucanto/interface' +import * as Signature from '@ipld/dag-ucan/signature' +import { base58btc } from 'multiformats/bases/base58' export const code = 0xed +export const signatureCode = Signature.EdDSA export const name = 'Ed25519' const PUBLIC_TAG_SIZE = varint.encodingLength(code) const SIZE = 32 + PUBLIC_TAG_SIZE +/** + * @typedef {API.Verifier<"key", Signature.EdDSA> & Uint8Array} Verifier + */ + /** * Parses `did:key:` string as a VerifyingPrincipal. - * @param {API.DID|string} did - * @returns {API.Principal} + * + * @param {API.DID<"key">|string} did */ -export const parse = (did) => decode(DID.parse(did)) +export const parse = did => decode(DID.parse(did)) /** * Takes ed25519 public key tagged with `0xed` multiformat code and creates a * corresponding `Principal` that can be used to verify signatures. * * @param {Uint8Array} bytes - * @returns {API.Principal} + * @returns {Verifier} */ -export const decode = (bytes) => { +export const decode = bytes => { const [algorithm] = varint.decode(bytes) if (algorithm !== code) { throw new RangeError( @@ -33,47 +40,34 @@ export const decode = (bytes) => { `Expected Uint8Array with byteLength ${SIZE}, instead got Uint8Array with byteLength ${bytes.byteLength}` ) } else { - return new Principal(bytes.buffer, bytes.byteOffset) + return new Ed25519Principal( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength + ) } } /** * Formats given Principal into `did:key:` format. * - * @param {API.Principal} principal + * @param {API.Principal<"key">} principal */ -export const format = (principal) => DID.format(principal.bytes) +export const format = principal => DID.format(principal) /** * Encodes given Principal by tagging it's ed25519 public key with `0xed` * multiformat code. * - * @param {API.Principal} principal + * @param {API.Principal<"key">} principal */ -export const encode = (principal) => principal.bytes +export const encode = principal => DID.encode(principal) /** - * @implements {API.Principal} + * @implements {API.Verifier<"key", typeof Signature.EdDSA>} + * @implements {API.Principal<"key">} */ -class Principal { - /** - * @param {ArrayBuffer} buffer - * @param {number} [byteOffset] - */ - constructor(buffer, byteOffset = 0) { - /** @readonly */ - this.buffer = buffer - /** @readonly */ - this.byteOffset = byteOffset - /** @readonly */ - this.byteLength = SIZE - } - - get bytes() { - const bytes = new Uint8Array(this.buffer, this.byteOffset, this.byteLength) - Object.defineProperties(this, { bytes: { value: bytes } }) - return bytes - } +class Ed25519Principal extends Uint8Array { /** * Raw public key without a multiformat code. * @@ -90,18 +84,21 @@ class Principal { } /** * DID of the Principal in `did:key` format. - * @returns {API.DID} + * @returns {API.DID<"key">} */ did() { - return format(this) + return `did:key:${base58btc.encode(this)}` } /** * @template T * @param {API.ByteView} payload - * @param {API.Signature} signature - * @returns {Promise} + * @param {API.Signature} signature + * @returns {API.Await} */ verify(payload, signature) { - return ED25519.verify(signature, payload, this.publicKey) + return ( + signature.code === signatureCode && + ED25519.verify(signature.raw, payload, this.publicKey) + ) } } diff --git a/packages/principal/src/lib.js b/packages/principal/src/lib.js index 195bea86..ecee9f3c 100644 --- a/packages/principal/src/lib.js +++ b/packages/principal/src/lib.js @@ -1,3 +1 @@ -export * from './signing-principal.js' -export * as Principal from './principal.js' -export * as SigningPrincipal from './signing-principal.js' +export * as ed25519 from './ed25519.js' diff --git a/packages/principal/src/signing-principal.js b/packages/principal/src/signing-principal.js deleted file mode 100644 index 4fde4521..00000000 --- a/packages/principal/src/signing-principal.js +++ /dev/null @@ -1,170 +0,0 @@ -import * as ED25519 from '@noble/ed25519' -import { varint } from 'multiformats' -import * as API from '@ucanto/interface' -import * as Principal from './principal.js' -import { base64pad } from 'multiformats/bases/base64' - -export const code = 0x1300 -export const name = Principal.name - -const PRIVATE_TAG_SIZE = varint.encodingLength(code) -const PUBLIC_TAG_SIZE = varint.encodingLength(Principal.code) -const KEY_SIZE = 32 -const SIZE = PRIVATE_TAG_SIZE + KEY_SIZE + PUBLIC_TAG_SIZE + KEY_SIZE - -/** - * Generates new issuer by generating underlying ED25519 keypair. - * @returns {Promise>} - */ -export const generate = () => derive(ED25519.utils.randomPrivateKey()) - -/** - * Derives issuer from 32 byte long secret key. - * @param {Uint8Array} secret - * @returns {Promise>} - */ -export const derive = async (secret) => { - if (secret.byteLength !== KEY_SIZE) { - throw new Error( - `Expected Uint8Array with byteLength of ${KEY_SIZE} instead not ${secret.byteLength}` - ) - } - - const publicKey = await ED25519.getPublicKey(secret) - const bytes = new Uint8Array(SIZE) - - varint.encodeTo(code, bytes, 0) - bytes.set(secret, PRIVATE_TAG_SIZE) - - varint.encodeTo(Principal.code, bytes, PRIVATE_TAG_SIZE + KEY_SIZE) - bytes.set(publicKey, PRIVATE_TAG_SIZE + KEY_SIZE + PUBLIC_TAG_SIZE) - - return new SigningPrincipal(bytes) -} - -/** - * - * @param {Uint8Array} bytes - * @returns {SigningPrincipal} - */ -export const decode = (bytes) => { - if (bytes.byteLength !== SIZE) { - throw new Error( - `Expected Uint8Array with byteLength of ${SIZE} instead not ${bytes.byteLength}` - ) - } - - { - const [keyCode] = varint.decode(bytes) - if (keyCode !== code) { - throw new Error(`Given bytes must be a multiformat with ${code} tag`) - } - } - - { - const [code] = varint.decode(bytes.subarray(PRIVATE_TAG_SIZE + KEY_SIZE)) - if (code !== Principal.code) { - throw new Error( - `Given bytes must contain public key in multiformats with ${Principal.code} tag` - ) - } - } - - return new SigningPrincipal(bytes) -} - -/** - * @param {API.SigningPrincipal} agent - */ -export const encode = ({ bytes }) => bytes - -/** - * @template {string} Prefix - * @param {API.SigningPrincipal} agent - * @param {API.MultibaseEncoder} [encoder] - */ -export const format = ({ bytes }, encoder) => - (encoder || base64pad).encode(bytes) - -/** - * @template {string} Prefix - * @param {string} principal - * @param {API.MultibaseDecoder} [decoder] - * @returns {API.SigningPrincipal} - */ -export const parse = (principal, decoder) => - decode((decoder || base64pad).decode(principal)) - -/** - * @param {API.SigningPrincipal} principal - * @returns {API.DID} - */ -export const did = ({ principal }) => principal.did() - -/** - * @implements {API.SigningPrincipal} - */ -class SigningPrincipal { - /** - * @param {Uint8Array} bytes - */ - constructor(bytes) { - this.buffer = bytes.buffer - this.byteOffset = bytes.byteOffset - this.byteLength = SIZE - this.bytes = bytes - } - get principal() { - const bytes = new Uint8Array(this.buffer, PRIVATE_TAG_SIZE + KEY_SIZE) - const principal = Principal.decode(bytes) - - Object.defineProperties(this, { - principal: { - value: principal, - }, - }) - - return principal - } - - /** - * Raw public key without multiformat code. - */ - get secret() { - const secret = new Uint8Array(this.buffer, PRIVATE_TAG_SIZE, KEY_SIZE) - Object.defineProperties(this, { - secret: { - value: secret, - }, - }) - - return secret - } - - /** - * DID of this principal in `did:key` format. - * - * @returns {API.DID} - */ - did() { - return did(this) - } - - /** - * @template T - * @param {API.ByteView} payload - * @returns {Promise>} - */ - sign(payload) { - return ED25519.sign(payload, this.secret) - } - - /** - * @template T - * @param {API.ByteView} payload - * @param {API.Signature} signature - */ - verify(payload, signature) { - return this.principal.verify(payload, signature) - } -} diff --git a/packages/principal/test/lib.spec.js b/packages/principal/test/lib.spec.js index 232c150a..80a23a6e 100644 --- a/packages/principal/test/lib.spec.js +++ b/packages/principal/test/lib.spec.js @@ -1,10 +1,10 @@ -import * as Lib from '../src/lib.js' +import { ed25519 as Lib } from '../src/lib.js' import { assert } from 'chai' import { sha256 } from 'multiformats/hashes/sha2' import { varint } from 'multiformats' describe('signing principal', () => { - const { SigningPrincipal } = Lib + const { Signer } = Lib it('exports', () => { assert.equal(Lib.name, 'Ed25519') @@ -12,27 +12,26 @@ describe('signing principal', () => { assert.equal(typeof Lib.derive, 'function') assert.equal(typeof Lib.generate, 'function') - assert.equal(typeof Lib.Principal, 'object') - assert.equal(typeof Lib.SigningPrincipal, 'object') + assert.equal(typeof Lib.Verifier, 'object') + assert.equal(typeof Lib.Signer, 'object') }) it('generate', async () => { const signer = await Lib.generate() assert.ok(signer.did().startsWith('did:key')) - assert.equal(signer.did(), signer.principal.did()) - assert.ok(signer.bytes instanceof Uint8Array) - assert.ok(signer.buffer instanceof ArrayBuffer) + assert.ok(signer instanceof Uint8Array) const payload = await sha256.encode(new TextEncoder().encode('hello world')) const signature = await signer.sign(payload) + + const verifier = Lib.Verifier.parse(signer.did()) assert.ok( - await signer.verify(payload, signature), + await verifier.verify(payload, signature), 'signer can verify signature' ) - assert.ok( - await signer.principal.verify(payload, signature), - 'principal can verify signature' - ) + + assert.equal(signer.signatureAlgorithm, 'EdDSA') + assert.equal(signer.signatureCode, 0xd0ed) }) it('derive', async () => { @@ -58,46 +57,36 @@ describe('signing principal', () => { it('SigningPrincipal.decode', async () => { const signer = await Lib.generate() + const bytes = Signer.encode(signer) - assert.deepEqual(SigningPrincipal.decode(signer.bytes), signer) + assert.deepEqual(Signer.decode(signer), signer) - const invalid = new Uint8Array(signer.bytes) + const invalid = new Uint8Array(signer) varint.encodeTo(4, invalid, 0) - assert.throws( - () => SigningPrincipal.decode(invalid), - /must be a multiformat with/ - ) + assert.throws(() => Signer.decode(invalid), /must be a multiformat with/) assert.throws( - () => SigningPrincipal.decode(signer.bytes.slice(0, 32)), + () => Signer.decode(signer.slice(0, 32)), /Expected Uint8Array with byteLength/ ) - const malformed = new Uint8Array(signer.bytes) - varint.encodeTo(4, malformed, signer.principal.byteOffset) + const malformed = new Uint8Array(signer) + // @ts-ignore + varint.encodeTo(4, malformed, Signer.PUB_KEY_OFFSET) - assert.throws( - () => SigningPrincipal.decode(malformed), - /must contain public key/ - ) + assert.throws(() => Signer.decode(malformed), /must contain public key/) }) it('SigningPrincipal decode encode roundtrip', async () => { const signer = await Lib.generate() - assert.deepEqual( - SigningPrincipal.decode(SigningPrincipal.encode(signer)), - signer - ) + assert.deepEqual(Signer.decode(Signer.encode(signer)), signer) }) it('SigningPrincipal.format', async () => { const signer = await Lib.generate() - assert.deepEqual( - SigningPrincipal.parse(SigningPrincipal.format(signer)), - signer - ) + assert.deepEqual(Signer.parse(Signer.format(signer)), signer) }) it('SigningPrincipal.did', async () => { @@ -108,49 +97,52 @@ describe('signing principal', () => { }) describe('principal', () => { - const { Principal } = Lib + const { Verifier, Signer } = Lib it('exports', async () => { - assert.equal(Principal, await import('../src/principal.js')) - assert.equal(Principal.code, 0xed) - assert.equal(Principal.name, 'Ed25519') + assert.equal(Verifier, await import('../src/ed25519/verifier.js')) + assert.equal(Verifier.code, 0xed) + assert.equal(Verifier.name, 'Ed25519') }) - it('Principal.parse', async () => { + it('Verifier.parse', async () => { const signer = await Lib.generate() - const principal = Principal.parse(signer.did()) + const verifier = Verifier.parse(signer.did()) - assert.deepEqual(principal.bytes, signer.principal.bytes) - assert.equal(principal.did(), signer.did()) + assert.deepEqual( + new Uint8Array(signer.buffer, signer.byteOffset + Signer.PUB_KEY_OFFSET), + verifier + ) + assert.equal(verifier.did(), signer.did()) }) - it('Principal.decode', async () => { + it('Verifier.decode', async () => { const signer = await Lib.generate() - assert.deepEqual(Principal.decode(signer.principal.bytes), signer.principal) - assert.throws( - () => Principal.decode(signer.bytes), - /key algorithm with multicode/ + const verifier = new Uint8Array( + signer.buffer, + signer.byteOffset + Signer.PUB_KEY_OFFSET ) + assert.deepEqual(Verifier.decode(verifier), verifier) + assert.throws(() => Verifier.decode(signer), /key algorithm with multicode/) assert.throws( - () => Principal.decode(signer.principal.bytes.slice(0, 32)), + () => Verifier.decode(verifier.slice(0, 32)), /Expected Uint8Array with byteLength/ ) }) - it('Principal.format', async () => { - const principal = await Lib.generate() + it('Verifier.format', async () => { + const signer = await Lib.generate() + const verifier = Verifier.parse(signer.did()) - assert.deepEqual(Principal.format(principal.principal), principal.did()) + assert.deepEqual(Verifier.format(verifier), signer.did()) }) - it('Principal.encode', async () => { - const principal = await Lib.generate() + it('Verifier.encode', async () => { + const { verifier } = await Lib.generate() - assert.deepEqual( - [...Principal.encode(principal.principal)], - [...principal.principal.bytes] - ) + const bytes = Verifier.encode(verifier) + assert.deepEqual(Verifier.decode(bytes), verifier) }) }) diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index de28b56c..40287dad 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -24,14 +24,14 @@ export interface ProviderContext< C extends API.Caveats = API.Caveats > { capability: API.ParsedCapability> - invocation: API.Invocation & API.InferCaveats> + invocation: API.Invocation>> context: API.InvocationContext } export interface ProviderInput { capability: T - invocation: API.Invocation & T['caveats']> + invocation: API.Invocation> context: API.InvocationContext } diff --git a/packages/server/src/handler.js b/packages/server/src/handler.js index f2a9afed..87fa5952 100644 --- a/packages/server/src/handler.js +++ b/packages/server/src/handler.js @@ -2,17 +2,19 @@ import * as API from './api.js' import { access } from '@ucanto/validator' /** - * @template {API.ParsedCapability} T + * @template {API.Ability} A + * @template {API.URI} R + * @template {API.Caveats} C * @template {unknown} U - * @param {API.CapabilityParser>} capability - * @param {(input:API.ProviderInput) => API.Await} handler - * @returns {API.ServiceMethod & T['caveats'], Exclude, Exclude>>} + * @param {API.CapabilityParser>>>} capability + * @param {(input:API.ProviderInput>>) => API.Await} handler + * @returns {API.ServiceMethod>, Exclude, Exclude>>} */ export const provide = (capability, handler) => /** - * @param {API.Invocation & T['caveats']>} invocation + * @param {API.Invocation>>} invocation * @param {API.InvocationContext} options */ async (invocation, options) => { diff --git a/packages/server/src/lib.js b/packages/server/src/lib.js index 88dc08c9..f8130188 100644 --- a/packages/server/src/lib.js +++ b/packages/server/src/lib.js @@ -1,5 +1,4 @@ export * from './server.js' -export * from '@ucanto/principal' export * from '@ucanto/core' export { invoke } from '@ucanto/core' // @ts-ignore diff --git a/packages/server/src/server.js b/packages/server/src/server.js index 7b6cccc6..d377e1ee 100644 --- a/packages/server/src/server.js +++ b/packages/server/src/server.js @@ -1,6 +1,6 @@ import * as API from '@ucanto/interface' import { InvalidAudience } from '@ucanto/validator' -import { Principal } from '@ucanto/principal' +import { Verifier } from '@ucanto/principal/ed25519' export { capability, URI, @@ -16,7 +16,7 @@ export { * @param {API.Server} options * @returns {API.ServerView} */ -export const create = (options) => new Server(options) +export const create = options => new Server(options) /** * @template {Record} Service @@ -31,7 +31,7 @@ class Server { service, encoder, decoder, - principal = Principal, + principal = Verifier, canIssue = (capability, issuer) => capability.with === issuer || issuer === id.did(), ...rest diff --git a/packages/server/test/fixtures.js b/packages/server/test/fixtures.js index d553ae28..378348c1 100644 --- a/packages/server/test/fixtures.js +++ b/packages/server/test/fixtures.js @@ -1,19 +1,19 @@ -import { SigningPrincipal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = SigningPrincipal.parse( +export const alice = ed25519.parse( 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' ) /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = SigningPrincipal.parse( +export const bob = ed25519.parse( 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' ) /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = SigningPrincipal.parse( +export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) /** did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z */ -export const service = SigningPrincipal.parse( +export const service = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/server/test/handler.spec.js b/packages/server/test/handler.spec.js index ceb79d23..76985586 100644 --- a/packages/server/test/handler.spec.js +++ b/packages/server/test/handler.spec.js @@ -6,7 +6,7 @@ import * as API from '@ucanto/interface' import { alice, bob, mallory, service as w3 } from './fixtures.js' import { test, assert } from './test.js' import * as Access from './service/access.js' -import { Principal } from '@ucanto/principal' +import { Verifier } from '@ucanto/principal/ed25519' import { UnavailableProof } from '@ucanto/validator' const context = { @@ -19,17 +19,17 @@ const context = { */ canIssue: (capability, issuer) => capability.with === issuer || issuer == w3.did(), - principal: Principal, + principal: Verifier, /** * @param {API.UCANLink} link */ - resolve: (link) => new UnavailableProof(link), + resolve: link => new UnavailableProof(link), } test('invocation', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'identity/link', @@ -64,7 +64,7 @@ test('delegated invocation fail', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: proof.capabilities, proofs: [proof], }) @@ -72,8 +72,8 @@ test('delegated invocation fail', async () => { const result = await Access.link(invocation, context) assert.containSubset(result, { error: true, - name: 'UnknownDIDError', - did: 'mailto:alice@web.mail', + name: 'UnknownIDError', + id: 'mailto:alice@web.mail', }) }) @@ -91,7 +91,7 @@ test('delegated invocation fail', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: proof.capabilities, proofs: [proof], }) @@ -109,7 +109,7 @@ test('checks service id', async () => { }) const client = Client.connect({ - id: w3.principal, + id: w3, encoder: CAR, decoder: CBOR, channel: server, @@ -129,7 +129,7 @@ test('checks service id', async () => { { const invocation = Client.invoke({ issuer: bob, - audience: mallory.principal, + audience: mallory, capability: proof.capabilities[0], proofs: [proof], }) @@ -147,7 +147,7 @@ test('checks service id', async () => { { const invocation = Client.invoke({ issuer: bob, - audience: w3.principal, + audience: w3, capability: proof.capabilities[0], proofs: [proof], }) @@ -167,7 +167,7 @@ test('checks for single capability invocation', async () => { }) const client = Client.connect({ - id: w3.principal, + id: w3, encoder: CAR, decoder: CBOR, channel: server, @@ -186,7 +186,7 @@ test('checks for single capability invocation', async () => { const invocation = Client.invoke({ issuer: bob, - audience: w3.principal, + audience: w3, capability: proof.capabilities[0], proofs: [proof], }) diff --git a/packages/server/test/server.spec.js b/packages/server/test/server.spec.js index 736aeb6f..9def2523 100644 --- a/packages/server/test/server.spec.js +++ b/packages/server/test/server.spec.js @@ -9,22 +9,22 @@ import { test, assert } from './test.js' const storeAdd = Server.capability({ can: 'store/add', with: Server.URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Server.Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Server.Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -34,22 +34,22 @@ const storeAdd = Server.capability({ const storeRemove = Server.capability({ can: 'store/remove', with: Server.URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Server.Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Server.Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -72,7 +72,7 @@ test('encode delegated invocation', async () => { }) const connection = Client.connect({ - id: w3.principal, + id: w3, encoder: CAR, decoder: CBOR, channel: server, @@ -95,7 +95,9 @@ test('encode delegated invocation', async () => { capability: { can: 'store/add', with: alice.did(), - link: car.cid, + nb: { + link: car.cid, + }, }, proofs: [proof], }) @@ -106,7 +108,9 @@ test('encode delegated invocation', async () => { capability: { can: 'store/remove', with: alice.did(), - link: car.cid, + nb: { + link: car.cid, + }, }, }) @@ -153,7 +157,9 @@ test('encode delegated invocation', async () => { { can: 'store/remove', with: alice.did(), - link: car.cid, + nb: { + link: car.cid, + }, }, ]) }) @@ -167,7 +173,7 @@ test('unknown handler', async () => { }) const connection = Client.connect({ - id: w3.principal, + id: w3, encoder: CAR, decoder: CBOR, channel: server, @@ -235,7 +241,7 @@ test('execution error', async () => { }) const connection = Client.connect({ - id: w3.principal, + id: w3, encoder: CAR, decoder: CBOR, channel: server, diff --git a/packages/server/test/service/access.js b/packages/server/test/service/access.js index b31dc754..a245127b 100644 --- a/packages/server/test/service/access.js +++ b/packages/server/test/service/access.js @@ -8,9 +8,9 @@ const registerCapability = Server.capability({ can: 'identity/register', with: Server.URI.match({ protocol: 'mailto:' }), derives: (claimed, delegated) => - claimed.uri.href === delegated.uri.href || + claimed.with === delegated.with || new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ), }) @@ -18,9 +18,9 @@ const linkCapability = Server.capability({ can: 'identity/link', with: Server.URI, derives: (claimed, delegated) => - claimed.uri.href === delegated.uri.href || + claimed.with === delegated.with || new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ), }) @@ -28,15 +28,13 @@ const identifyCapability = Server.capability({ can: 'identity/identify', with: Server.URI, derives: (claimed, delegated) => - claimed.uri.href === delegated.uri.href || - delegated.uri.href === 'ucan:*' || - new Server.Failure( - `Can not derive ${claimed.uri.href} from ${claimed.uri.href}` - ), + claimed.with === delegated.with || + delegated.with === 'ucan:*' || + new Server.Failure(`Can not derive ${claimed.with} from ${claimed.with}`), }) /** - * @typedef {Map} Model + * @typedef {Map, {account:API.DID, proof:API.Link}>} Model * @type {Model} */ const state = new Map() @@ -47,7 +45,7 @@ export const register = provide( return associate( state, invocation.issuer.did(), - /** @type {API.DID} */ (capability.with), + capability.with, invocation.cid, true ) @@ -70,19 +68,19 @@ export const link = provide( export const identify = provide( identifyCapability, async function identify({ capability }) { - const did = /** @type {API.DID} */ (capability.uri.href) + const did = /** @type {API.DID} */ (capability.with) const account = resolve(state, did) - return account == null ? new UnknownDIDError(did) : account + return account == null ? new UnknownIDError(did) : account } ) /** * @param {Model} accounts * @param {API.DID} from - * @param {API.DID} to + * @param {API.DID|API.URI<"mailto:">} to * @param {API.Link} proof * @param {boolean} create - * @returns {API.SyncResult} + * @returns {API.SyncResult} */ const associate = (accounts, from, to, proof, create) => { const fromAccount = resolve(accounts, from) @@ -99,7 +97,7 @@ const associate = (accounts, from, to, proof, create) => { accounts.set(to, { account, proof }) accounts.set(from, { account, proof }) } else { - return new UnknownDIDError('Unknown did', to) + return new UnknownIDError('Unknown did', to) } } else if (toAccount) { accounts.set(from, { account: toAccount, proof }) @@ -120,7 +118,7 @@ const associate = (accounts, from, to, proof, create) => { * `did:ipld:bafy...hash` form. * * @param {Model} accounts - * @param {API.DID} member + * @param {API.DID|API.URI<"mailto:">} member * @returns {API.DID|null} */ const resolve = (accounts, member) => { @@ -137,30 +135,30 @@ const resolve = (accounts, member) => { } /** - * @implements {API.UnknownDIDError} + * @implements {API.UnknownIDError} */ -export class UnknownDIDError extends RangeError { +export class UnknownIDError extends RangeError { /** * @param {string} message - * @param {API.DID|null} [did] + * @param {API.DID|API.DID|API.URI<"mailto:">|null} [id] */ - constructor(message, did = null) { + constructor(message, id = null) { super(message) - this.did = did + this.id = id } get error() { return /** @type {true} */ (true) } - /** @type {"UnknownDIDError"} */ + /** @type {"UnknownIDError"} */ get name() { - return 'UnknownDIDError' + return 'UnknownIDError' } toJSON() { return { name: this.name, message: this.message, - did: this.did, + id: this.id, error: true, } } diff --git a/packages/server/test/service/api.ts b/packages/server/test/service/api.ts index 12ec0ca5..45fac374 100644 --- a/packages/server/test/service/api.ts +++ b/packages/server/test/service/api.ts @@ -23,7 +23,7 @@ export interface StorageProvider { group: DID, link: Link, proof: Link - ): Result + ): Result /** * * @param group - DID we received an invocation request from. @@ -34,7 +34,7 @@ export interface StorageProvider { group: DID, link: Link, proof: Link - ): Result + ): Result } export interface TokenStore { @@ -133,16 +133,16 @@ export interface AccessProvider { * Associates a DID with another DID in the system. If there is no account * associated with a `to` DID will produce an error. */ - link(member: DID, group: DID, proof: Link): Result + link(member: DID, group: DID, proof: Link): Result - unlink(member: DID, group: DID, proof: Link): Result + unlink(member: DID, group: DID, proof: Link): Result /** * Associates new child DID with an accound of the parent DID. If there is no * account associated with a parent it creates account with `parent` did first * and then associates child DID with it. */ - register(member: DID, group: DID, proof: Link): Result + register(member: DID, group: DID, proof: Link): Result /** * Resolves account DID associated with a given DID. Returns either account @@ -182,9 +182,9 @@ export interface DoesNotHasError extends RangeError { error: true } -export interface UnknownDIDError extends RangeError { - readonly name: 'UnknownDIDError' - did: DID | null +export interface UnknownIDError extends RangeError { + readonly name: 'UnknownIDError' + id: string | null error: true } diff --git a/packages/server/test/service/store.js b/packages/server/test/service/store.js index fa6c3802..9e70464d 100644 --- a/packages/server/test/service/store.js +++ b/packages/server/test/service/store.js @@ -8,22 +8,22 @@ import { service as issuer } from '../fixtures.js' const addCapability = Server.capability({ can: 'store/add', with: Server.URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Server.Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Server.Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -34,22 +34,22 @@ const addCapability = Server.capability({ const removeCapability = Server.capability({ can: 'store/remove', with: Server.URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Server.Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Server.Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Server.Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -60,8 +60,6 @@ const removeCapability = Server.capability({ /** @type {Map>} */ const state = new Map() -export const id = issuer.principal - export const add = provide(addCapability, async ({ capability, context }) => { const identify = await Client.delegate({ issuer, @@ -69,7 +67,7 @@ export const add = provide(addCapability, async ({ capability, context }) => { capabilities: [ { can: 'identity/identify', - with: /** @type {API.Resource} */ (capability.uri.href), + with: /** @type {API.Resource} */ (capability.with), }, ], }) @@ -79,8 +77,8 @@ export const add = provide(addCapability, async ({ capability, context }) => { return account } - const { link } = capability.caveats - const groupID = /** @type {API.DID} */ (capability.uri.href) + const { link } = capability.nb + const groupID = /** @type {API.DID} */ (capability.with) const links = state.get(groupID) || new Map() links.set(`${link}`, link) diff --git a/packages/transport/src/jwt.js b/packages/transport/src/jwt.js index be07c374..f4bdf254 100644 --- a/packages/transport/src/jwt.js +++ b/packages/transport/src/jwt.js @@ -15,7 +15,7 @@ const HEADERS = Object.freeze({ * @param {I} batch * @returns {Promise>} */ -export const encode = async (batch) => { +export const encode = async batch => { /** @type {Record} */ const headers = { ...HEADERS } /** @type {string[]} */ diff --git a/packages/transport/test/car.spec.js b/packages/transport/test/car.spec.js index 07ce96c9..200f4c1a 100644 --- a/packages/transport/test/car.spec.js +++ b/packages/transport/test/car.spec.js @@ -10,14 +10,14 @@ import { collect } from './util.js' test('encode / decode', async () => { const cid = parseLink( - 'bafyreigw75rhf7gf7eubwmrhovcrdu4mfy6pfbi4wgbzlfieq2wlfsza5i' + 'bafyreiaxnmoptsqiehdff2blpptvdbenxcz6xgrbojw5em36xovn2xea4y' ) const expiration = 1654298135 const request = await CAR.encode([ { issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -38,7 +38,7 @@ test('encode / decode', async () => { const expect = await Delegation.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -59,7 +59,7 @@ test('decode requires application/car contet type', async () => { const { body } = await CAR.encode([ { issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -88,7 +88,7 @@ test('accepts Content-Type as well', async () => { const request = await CAR.encode([ { issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -109,7 +109,7 @@ test('accepts Content-Type as well', async () => { const delegation = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -126,7 +126,7 @@ test('accepts Content-Type as well', async () => { test('delegated proofs', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -181,7 +181,7 @@ test('delegated proofs', async () => { test('omit proof', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', diff --git a/packages/transport/test/fixtures.js b/packages/transport/test/fixtures.js index 197b8d98..678e9516 100644 --- a/packages/transport/test/fixtures.js +++ b/packages/transport/test/fixtures.js @@ -1,18 +1,18 @@ -import { SigningPrincipal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = SigningPrincipal.parse( +export const alice = ed25519.parse( 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' ) /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = SigningPrincipal.parse( +export const bob = ed25519.parse( 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' ) /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = SigningPrincipal.parse( +export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) -export const service = SigningPrincipal.parse( +export const service = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/transport/test/jwt.spec.js b/packages/transport/test/jwt.spec.js index 82f5ad95..36b58e75 100644 --- a/packages/transport/test/jwt.spec.js +++ b/packages/transport/test/jwt.spec.js @@ -4,13 +4,14 @@ import { delegate, Delegation, UCAN } from '@ucanto/core' import * as UTF8 from '../src/utf8.js' import { alice, bob, mallory, service } from './fixtures.js' import * as API from '@ucanto/interface' +import { base64url } from 'multiformats/bases/base64' const NOW = 1654298135 const fixtures = { basic: { - cid: 'bafyreigw75rhf7gf7eubwmrhovcrdu4mfy6pfbi4wgbzlfieq2wlfsza5i', - jwt: 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOC4xIn0.eyJhdHQiOlt7ImNhbiI6InN0b3JlL2FkZCIsIndpdGgiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSJ9XSwiYXVkIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJleHAiOjE2NTQyOTgxMzUsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwicHJmIjpbXX0.xl_SgW5QrxffsbHslb1vSX7ZAV1JbjxF1rNIAEplNPHLreHtyC3OKqneOouWjO3mqqXAcrWAsnodrBgL50VWCA', + cid: 'bafyreiaxnmoptsqiehdff2blpptvdbenxcz6xgrbojw5em36xovn2xea4y', + jwt: 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4xIn0.eyJhdHQiOlt7ImNhbiI6InN0b3JlL2FkZCIsIndpdGgiOiJkaWQ6a2V5Ono2TWtrODliQzNKclZxS2llNzFZRWNjNU0xU01WeHVDZ054NnpMWjhTWUpzeEFMaSJ9XSwiYXVkIjoiZGlkOmtleTp6Nk1rZmZEWkNrQ1RXcmVnODg2OGZHMUZHRm9nY0pqNVg2UFk5M3BQY1dEbjlib2IiLCJleHAiOjE2NTQyOTgxMzUsImlzcyI6ImRpZDprZXk6ejZNa2s4OWJDM0pyVnFLaWU3MVlFY2M1TTFTTVZ4dUNnTng2ekxaOFNZSnN4QUxpIiwicHJmIjpbXX0.amtDCzx4xzI28w8M4gKCOBWuhREPPAh8cdoXfi4JDTMy5wxy-4VYYM4AC7lXufsgdiT6thaBtq3AAIv1P87lAA', }, } @@ -20,7 +21,7 @@ test('encode / decode', async () => { const request = await JWT.encode([ { issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -39,6 +40,7 @@ test('encode / decode', async () => { [`x-auth-${cid}`]: jwt, }, } + assert.deepEqual(request, expect) assert.deepEqual( @@ -46,7 +48,7 @@ test('encode / decode', async () => { [ await Delegation.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -80,7 +82,7 @@ test('decode requires application/json contet type', async () => { test('delegated proofs', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -131,7 +133,7 @@ test('delegated proofs', async () => { test('omit proof', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -182,7 +184,7 @@ test('omit proof', async () => { test('thorws on invalid heard', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -228,7 +230,7 @@ test('thorws on invalid heard', async () => { test('leaving out root throws', async () => { const proof = await delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', diff --git a/packages/validator/package.json b/packages/validator/package.json index e838f7e0..da0e1f83 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -20,7 +20,7 @@ "homepage": "https://github.com/web3-storage/ucanto", "scripts": { "test:web": "playwright-test test/**/*.spec.js --cov && nyc report", - "test:node": "c8 --check-coverage --branches 97 --functions 85 --lines 93 mocha test/**/*.spec.js", + "test:node": "c8 --check-coverage --branches 96 --functions 85 --lines 93 mocha test/**/*.spec.js", "test": "npm run test:node", "coverage": "c8 --reporter=html mocha test/**/*.spec.js && npm_config_yes=true npx st -d coverage -p 8080", "typecheck": "tsc --build", diff --git a/packages/validator/src/capability.js b/packages/validator/src/capability.js index b7e98900..97b8e1d0 100644 --- a/packages/validator/src/capability.js +++ b/packages/validator/src/capability.js @@ -16,7 +16,7 @@ import { invoke, delegate } from '@ucanto/core' * @param {API.Descriptor} descriptor * @returns {API.TheCapabilityParser>} */ -export const capability = (descriptor) => new Capability(descriptor) +export const capability = descriptor => new Capability(descriptor) /** * @template {API.Match} M @@ -114,12 +114,12 @@ class Capability extends Unit { } /** - * @param {API.InferCreateOptions>} options + * @param {API.InferCreateOptions>} options */ create(options) { const { descriptor, can } = this - const decoders = descriptor.caveats - const data = options.caveats || {} + const decoders = descriptor.nb + const data = /** @type {API.InferCaveats} */ (options.nb || {}) const resource = descriptor.with.decode(options.with) if (resource.error) { @@ -128,31 +128,41 @@ class Capability extends Unit { }) } - const caveats = /** @type {API.InferCaveats} */ ({}) + const capabality = + /** @type {API.ParsedCapability>} */ + ({ can, with: resource }) + for (const [name, decoder] of Object.entries(decoders || {})) { - const key = /** @type {keyof caveats & keyof data} */ (name) + const key = /** @type {keyof data & string} */ (name) const value = decoder.decode(data[key]) if (value?.error) { throw Object.assign( - new Error(`Invalid 'caveats.${key}' - ${value.message}`), + new Error(`Invalid 'nb.${key}' - ${value.message}`), { cause: value } ) } else { - const key = /** @type {keyof caveats} */ (name) - caveats[key] = /** @type {typeof caveats[key]} */ (value) + const nb = + capabality.nb || + (capabality.nb = /** @type {API.InferCaveats} */ ({})) + + const key = /** @type {keyof nb} */ (name) + nb[key] = /** @type {typeof nb[key]} */ (value) } } - return { ...caveats, can, with: resource.href } + return capabality } /** - * @param {API.InvokeCapabilityOptions>} data + * @param {API.InferInvokeOptions>} options */ - invoke({ with: with_, caveats, ...options }) { + invoke({ with: with_, nb, ...options }) { return invoke({ ...options, - capability: this.create({ with: with_, caveats }), + capability: this.create( + /** @type {API.InferCreateOptions>} */ + ({ with: with_, nb }) + ), }) } @@ -396,11 +406,9 @@ class Match { toString() { return JSON.stringify({ can: this.descriptor.can, - with: this.value.uri.href, - caveats: - Object.keys(this.value.caveats).length > 0 - ? this.value.caveats - : undefined, + with: this.value.with, + nb: + Object.keys(this.value.nb || {}).length > 0 ? this.value.nb : undefined, }) } } @@ -482,12 +490,10 @@ class DerivedMatch { errors: [ ...errors, ...direct.errors, - ...derived.errors.map((error) => new MatchError([error], this)), + ...derived.errors.map(error => new MatchError([error], this)), ], matches: [ - ...direct.matches.map( - (match) => new DerivedMatch(match, from, derives) - ), + ...direct.matches.map(match => new DerivedMatch(match, from, derives)), ...matches, ], } @@ -568,7 +574,7 @@ class AndMatch { return selectGroup(this, capabilities) } toString() { - return `[${this.matches.map((match) => match.toString()).join(', ')}]` + return `[${this.matches.map(match => match.toString()).join(', ')}]` } } @@ -582,9 +588,9 @@ class AndMatch { */ const parse = (self, source) => { - const { can, with: withDecoder, caveats: decoders } = self.descriptor - const { delegation, index } = source - const capability = /** @type {API.Capability & Record} */ ( + const { can, with: withDecoder, nb: decoders } = self.descriptor + const { delegation } = source + const capability = /** @type {API.Capability>} */ ( source.capability ) @@ -597,50 +603,42 @@ const parse = (self, source) => { return new MalformedCapability(capability, uri) } - const caveats = /** @type {API.InferCaveats} */ ({}) + const nb = /** @type {API.InferCaveats} */ ({}) if (decoders) { + /** @type {Partial>} */ + const caveats = capability.nb || {} for (const [name, decoder] of entries(decoders)) { - const key = /** @type {keyof capability & keyof caveats} */ (name) - const value = capability[key] - const result = decoder.decode(value) + const key = /** @type {keyof caveats & keyof nb} */ (name) + const result = decoder.decode(caveats[key]) if (result?.error) { return new MalformedCapability(capability, result) } else if (result != null) { - caveats[key] = /** @type {any} */ (result) + nb[key] = /** @type {any} */ (result) } } } - return new CapabilityView( - can, - /** @type {R['href']} */ (capability.with), - uri, - caveats, - delegation - ) + return new CapabilityView(can, capability.with, nb, delegation) } /** * @template {API.Ability} A * @template {API.URI} R * @template C - * @implements {API.ParsedCapability>} */ class CapabilityView { /** * @param {A} can - * @param {R['href']} with_ - * @param {R} uri - * @param {API.InferCaveats} caveats + * @param {R} with_ + * @param {API.InferCaveats} nb * @param {API.Delegation} delegation */ - constructor(can, with_, uri, caveats, delegation) { + constructor(can, with_, nb, delegation) { this.can = can this.with = with_ - this.uri = uri this.delegation = delegation - this.caveats = caveats + this.nb = nb } } @@ -696,7 +694,7 @@ const selectGroup = (self, capabilities) => { data.push(selected.matches) } - const matches = combine(data).map((group) => new AndMatch(group)) + const matches = combine(data).map(group => new AndMatch(group)) return { unknown: unknown || [], @@ -726,11 +724,13 @@ const derives = (claimed, delegated) => { ) } - for (const [name, value] of entries(delegated.caveats)) { - if (claimed.caveats[name] != value) { - return new Failure( - `${String(name)}: ${claimed.caveats[name]} violates ${value}` - ) + const caveats = delegated.nb || {} + const nb = claimed.nb || {} + const kv = entries(caveats) + + for (const [name, value] of kv) { + if (nb[name] != value) { + return new Failure(`${String(name)}: ${nb[name]} violates ${value}`) } } diff --git a/packages/validator/src/decoder/did.js b/packages/validator/src/decoder/did.js new file mode 100644 index 00000000..909a1ffb --- /dev/null +++ b/packages/validator/src/decoder/did.js @@ -0,0 +1,48 @@ +import * as API from '@ucanto/interface' +import { Failure } from '../error.js' + +/** + * @template {string} M + * @param {unknown} source + * @param {{method?: M}} options + * @return {API.Result & API.URI<"did:">, API.Failure>} + */ +export const decode = (source, { method } = {}) => { + const prefix = method ? `did:${method}:` : `did:` + if (typeof source != 'string') { + return new Failure( + `Expected a string but got ${ + source === null ? null : typeof source + } instead` + ) + } else if (!source.startsWith(prefix)) { + return new Failure(`Expected a ${prefix} but got "${source}" instead`) + } else { + return /** @type {API.DID} */ (source) + } +} + +/** + * @template {string} M + * @param {{method: M}} options + * @returns {API.Decoder & API.URI<"did:">, API.Failure>} + */ +export const match = options => ({ + decode: input => decode(input, options), +}) + +/** + * @template {string} M + * @param {{method?: M}} options + * @returns {API.Decoder & API.URI<"did:">), API.Failure>} + */ + +export const optional = options => ({ + decode: input => { + if (input === undefined) { + return undefined + } else { + return decode(input, options) + } + }, +}) diff --git a/packages/validator/src/decoder/link.js b/packages/validator/src/decoder/link.js index 77500e8b..8faa2ef1 100644 --- a/packages/validator/src/decoder/link.js +++ b/packages/validator/src/decoder/link.js @@ -65,10 +65,10 @@ export const match = options => ({ * @template {number} Code * @template {number} Alg * @template {1|0} Version - * @param {{code?:Code, algorithm?:Alg, version?:Version}} [options] + * @param {{code?:Code, algorithm?:Alg, version?:Version}} options * @returns {API.Decoder, API.Failure>} */ -export const optional = options => ({ +export const optional = (options = {}) => ({ decode: input => { if (input === undefined) { return undefined diff --git a/packages/validator/src/decoder/text.js b/packages/validator/src/decoder/text.js new file mode 100644 index 00000000..20659a1c --- /dev/null +++ b/packages/validator/src/decoder/text.js @@ -0,0 +1,46 @@ +import * as API from '@ucanto/interface' +import { Failure } from '../error.js' + +/** + * @param {unknown} input + * @param {{pattern?: RegExp}} options + * @return {API.Result} + */ +export const decode = (input, { pattern } = {}) => { + if (typeof input != 'string') { + return new Failure( + `Expected a string but got ${ + input === null ? null : typeof input + } instead` + ) + } else if (pattern && !pattern.test(input)) { + return new Failure( + `Expected to match ${pattern} but got "${input}" instead` + ) + } else { + return input + } +} + +/** + * @param {{pattern?: RegExp}} options + * @returns {API.Decoder} + */ +export const match = (options = {}) => ({ + decode: input => decode(input, options), +}) + +/** + * @param {{pattern?: RegExp}} options + * @returns {API.Decoder} + */ + +export const optional = (options = {}) => ({ + decode: input => { + if (input === undefined) { + return undefined + } else { + return decode(input, options) + } + }, +}) diff --git a/packages/validator/src/decoder/uri.js b/packages/validator/src/decoder/uri.js index b0c64844..9c24a546 100644 --- a/packages/validator/src/decoder/uri.js +++ b/packages/validator/src/decoder/uri.js @@ -2,14 +2,16 @@ import * as API from '@ucanto/interface' import { Failure } from '../error.js' /** - * @template {`${string}:`} Protocol + * @template {API.Protocol} P * @param {unknown} input - * @param {{protocol?: Protocol}} options - * @return {API.Result, API.Failure>} + * @param {{protocol?: P}} options + * @return {API.Result, API.Failure>} */ export const decode = (input, { protocol } = {}) => { if (typeof input !== 'string' && !(input instanceof URL)) { - return new Failure(`Expected URI but got ${typeof input}`) + return new Failure( + `Expected URI but got ${input === null ? 'null' : typeof input}` + ) } try { @@ -17,7 +19,7 @@ export const decode = (input, { protocol } = {}) => { if (protocol != null && url.protocol !== protocol) { return new Failure(`Expected ${protocol} URI instead got ${url.href}`) } else { - return /** @type {API.URI} */ (url) + return /** @type {API.URI

} */ (url.href) } } catch (_) { return new Failure(`Invalid URI`) @@ -25,27 +27,34 @@ export const decode = (input, { protocol } = {}) => { } /** - * @template {`${string}:`} Protocol - * @param {{protocol: Protocol}} options - * @returns {API.Decoder, API.Failure>} + * @template {{protocol: API.Protocol}} Options + * @param {Options} options + * @returns {API.Decoder, API.Failure>} */ export const match = options => ({ decode: input => decode(input, options), }) /** - * @template {`${string}:`} Protocol - * @typedef {`${Protocol}${string}`} URIString + * @template {{protocol: API.Protocol}} Options + * @param {Options} options + * @returns {API.Decoder, API.Failure>} */ -/** - * @template {string} Schema - * @param {{protocol?: API.Protocol}} [options] - * @returns {API.Decoder} - */ -export const string = options => ({ +export const optional = options => ({ decode: input => { - const result = decode(input, options) - return result.error ? result : result.href + if (input === undefined) { + return undefined + } else { + return decode(input, options) + } }, }) + +/** + * @template {API.Protocol} P + * @template {API.URI

} T + * @param {T} uri + * @return {T} + */ +export const from = uri => uri diff --git a/packages/validator/src/decoder/view.js b/packages/validator/src/decoder/view.js new file mode 100644 index 00000000..60d9d0b4 --- /dev/null +++ b/packages/validator/src/decoder/view.js @@ -0,0 +1,53 @@ +import * as API from '@ucanto/interface' +import { Failure } from '../error.js' + +/** + * @template T + * @template Options + * @implements {API.Decoder} + */ + +class Decoder { + /** + * @param {(input:unknown, options:Options) => API.Result} decodeWith + * @param {Options} options + * @param {boolean} optional + */ + constructor(decodeWith, options, optional = false) { + this.decodeWith = decodeWith + this.options = options + } + /** + * @param {unknown} input + */ + decode(input) { + return this.decodeWith(input, this.options) + } + /** + * @returns {API.Decoder} + */ + get optional() { + const optional = new OptionalDecoder(this.decodeWith, this.options) + Object.defineProperties(this, { optional: { value: optional } }) + return optional + } +} + +/** + * @template Options + * @template T + * @implements {API.Decoder} + * @extends {Decoder} + */ +class OptionalDecoder extends Decoder { + /** + * @param {unknown} input + */ + decode(input) { + if (input === undefined) { + return undefined + } else { + return this.decodeWith(input, this.options) + } + } +} diff --git a/packages/validator/src/lib.js b/packages/validator/src/lib.js index 3c2bc01a..e275eae7 100644 --- a/packages/validator/src/lib.js +++ b/packages/validator/src/lib.js @@ -18,13 +18,15 @@ export { capability } from './capability.js' export * as URI from './decoder/uri.js' export * as Link from './decoder/link.js' +export * as Text from './decoder/text.js' +export * as DID from './decoder/did.js' const empty = () => [] /** * @param {UCAN.Link} proof */ -const unavailable = (proof) => new UnavailableProof(proof) +const unavailable = proof => new UnavailableProof(proof) /** * @param {Required} config @@ -62,7 +64,7 @@ const resolveProofs = async (delegation, config) => { for (const [index, proof] of delegation.proofs.entries()) { if (!isDelegation(proof)) { promises.push( - new Promise(async (resolve) => { + new Promise(async resolve => { try { proofs[index] = await config.resolve(proof) } catch (error) { @@ -134,11 +136,11 @@ const resolveSources = async ({ delegation }, config) => { /** * @template {API.Ability} A * @template {API.URI} R + * @template {R} URI * @template {API.Caveats} C - * @template {API.ParsedCapability>} T - * @param {API.Invocation & API.InferCaveats>} invocation - * @param {API.ValidationOptions} config - * @returns {Promise, API.Unauthorized>>} + * @param {API.Invocation>>} invocation + * @param {API.ValidationOptions>>} config + * @returns {Promise>>, API.Unauthorized>>} */ export const access = async ( invocation, @@ -294,12 +296,12 @@ class InvalidClaim extends Failure { } describe() { const errors = [ - ...this.info.failedProofs.map((error) => li(error.message)), - ...this.info.delegationErrors.map((error) => li(error.message)), - ...this.info.invalidProofs.map((error) => li(error.message)), + ...this.info.failedProofs.map(error => li(error.message)), + ...this.info.delegationErrors.map(error => li(error.message)), + ...this.info.invalidProofs.map(error => li(error.message)), ] - const unknown = this.info.unknownCapaibilities.map((c) => + const unknown = this.info.unknownCapaibilities.map(c => li(JSON.stringify(c)) ) @@ -369,7 +371,7 @@ const MY = /my:(.*)/ * @param {string} uri * @returns {{did:API.DID, protocol:string}|null} */ -const parseAsURI = (uri) => { +const parseAsURI = uri => { const [, did, kind] = AS_PATTERN.exec(uri) || [] return did != null && kind != null ? { diff --git a/packages/validator/src/util.js b/packages/validator/src/util.js index 4abfff8c..efc740b4 100644 --- a/packages/validator/src/util.js +++ b/packages/validator/src/util.js @@ -3,15 +3,15 @@ * @param {T} value * @returns {T} */ -export const the = (value) => value +export const the = value => value /** * @template {{}} O * @param {O} object - * @returns {{ [K in keyof O]: [K, O[K]][] }[keyof O]} + * @returns {({ [K in keyof O]: [K, O[K]][] }[keyof O])|[[never, never]]} */ -export const entries = (object) => /** @type {any} */ (Object.entries(object)) +export const entries = object => /** @type {any} */ (Object.entries(object)) /** * @template T @@ -19,7 +19,7 @@ export const entries = (object) => /** @type {any} */ (Object.entries(object)) * @returns {T[][]} */ export const combine = ([first, ...rest]) => { - const results = first.map((value) => [value]) + const results = first.map(value => [value]) for (const values of rest) { const tuples = results.splice(0) for (const value of values) { diff --git a/packages/validator/test/capability.spec.js b/packages/validator/test/capability.spec.js index 8758d864..f6a0c013 100644 --- a/packages/validator/test/capability.spec.js +++ b/packages/validator/test/capability.spec.js @@ -8,7 +8,7 @@ import { test, assert } from './test.js' import { alice, bob, mallory, service as w3 } from './fixtures.js' /** - * @template {API.Capability[]} C + * @template {API.Capabilities} C * @param {C} capabilities * @param {object} delegation * @returns {API.Source[]} @@ -26,11 +26,11 @@ test('capability selects matches', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => { - if (claimed.uri.pathname.startsWith(delegated.uri.pathname)) { + if (claimed.with.startsWith(delegated.with)) { return true } else { return new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` + `'${claimed.with}' is not contained in '${delegated.with}'` ) } }, @@ -51,7 +51,7 @@ test('capability selects matches', () => { source: [d1[2]], value: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos' }, + with: 'file:///home/zAlice/photos', }, }, ], @@ -97,7 +97,7 @@ test('capability selects matches', () => { source: [d2[1]], value: { can: 'file/read', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, }, ], @@ -113,8 +113,8 @@ test('capability selects matches', () => { context: { value: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos' }, - caveats: {}, + with: 'file:///home/zAlice/photos', + nb: {}, }, }, causes: [ @@ -122,11 +122,11 @@ test('capability selects matches', () => { name: 'EscalatedCapability', claimed: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos' }, + with: 'file:///home/zAlice/photos', }, delegated: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos/public' }, + with: 'file:///home/zAlice/photos/public', }, cause: { message: `'file:///home/zAlice/photos' is not contained in 'file:///home/zAlice/photos/public'`, @@ -139,8 +139,8 @@ test('capability selects matches', () => { context: { value: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos' }, - caveats: {}, + with: 'file:///home/zAlice/photos', + nb: {}, }, }, causes: [ @@ -148,11 +148,11 @@ test('capability selects matches', () => { name: 'EscalatedCapability', claimed: { can: 'file/read', - uri: { href: 'file:///home/zAlice/photos' }, + with: 'file:///home/zAlice/photos', }, delegated: { can: 'file/read', - uri: { href: 'file:///home/zBob' }, + with: 'file:///home/zBob', }, cause: { message: `'file:///home/zAlice/photos' is not contained in 'file:///home/zBob'`, @@ -169,11 +169,11 @@ test('derived capability chain', () => { can: 'account/verify', with: URI.match({ protocol: 'mailto:' }), derives: (claimed, delegated) => { - if (claimed.uri.href.startsWith(delegated.uri.href)) { + if (claimed.with.startsWith(delegated.with)) { return true } else { return new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` + `'${claimed.with}' is not contained in '${delegated.with}'` ) } }, @@ -190,8 +190,8 @@ test('derived capability chain', () => { const c2 = delegated.can return ( - claimed.uri.href === delegated.uri.href || - new Failure(`'${claimed.uri.href}' != '${delegated.uri.href}'`) + claimed.with === delegated.with || + new Failure(`'${claimed.with}' != '${delegated.with}'`) ) }, }), @@ -202,8 +202,8 @@ test('derived capability chain', () => { const c2 = delegated.can return ( - claimed.uri.href === delegated.uri.href || - new Failure(`'${claimed.uri.href}' != '${delegated.uri.href}'`) + claimed.with === delegated.with || + new Failure(`'${claimed.with}' != '${delegated.with}'`) ) }, }) @@ -225,9 +225,7 @@ test('derived capability chain', () => { source: [d1[0]], value: { can: 'account/register', - uri: { - href: 'mailto:zAlice@web.mail', - }, + with: 'mailto:zAlice@web.mail', }, }, ], @@ -286,9 +284,7 @@ test('derived capability chain', () => { source: [d3[0]], value: { can: 'account/verify', - uri: { - href: 'mailto:zAlice@web.mail', - }, + with: 'mailto:zAlice@web.mail', }, }, ], @@ -316,7 +312,7 @@ test('derived capability chain', () => { context: { value: { can: 'account/register', - uri: { href: 'mailto:zAlice@web.mail' }, + with: 'mailto:zAlice@web.mail', }, }, causes: [ @@ -324,11 +320,11 @@ test('derived capability chain', () => { name: 'EscalatedCapability', claimed: { can: 'account/register', - uri: { href: 'mailto:zAlice@web.mail' }, + with: 'mailto:zAlice@web.mail', }, delegated: { can: 'account/verify', - uri: { href: 'mailto:bob@web.mail' }, + with: 'mailto:bob@web.mail', }, cause: { message: `'mailto:zAlice@web.mail' != 'mailto:bob@web.mail'`, @@ -355,7 +351,7 @@ test('derived capability chain', () => { { value: { can: 'account/register', - uri: { href: 'mailto:zAlice@web.mail' }, + with: 'mailto:zAlice@web.mail', }, }, ], @@ -389,7 +385,7 @@ test('derived capability chain', () => { source: [d6[0]], value: { can: 'account/verify', - uri: { href: 'mailto:zAlice@web.mail' }, + with: 'mailto:zAlice@web.mail', }, }, ], @@ -418,20 +414,16 @@ test('capability amplification', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.and(write).derive({ @@ -439,19 +431,19 @@ test('capability amplification', () => { can: 'file/read+write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || + claimed.with.startsWith(delegated.with) || new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` + `'${claimed.with}' is not contained in '${delegated.with}'` ), }), derives: (claimed, [read, write]) => { - if (!claimed.uri.pathname.startsWith(read.uri.pathname)) { + if (!claimed.with.startsWith(read.with)) { return new Failure( - `'${claimed.uri.href}' is not contained in '${read.uri.href}'` + `'${claimed.with}' is not contained in '${read.with}'` ) - } else if (!claimed.uri.pathname.startsWith(write.uri.pathname)) { + } else if (!claimed.with.startsWith(write.with)) { return new Failure( - `'${claimed.uri.href}' is not contained in '${write.uri.href}'` + `'${claimed.with}' is not contained in '${write.with}'` ) } else { return true @@ -492,7 +484,7 @@ test('capability amplification', () => { source: [d2[0]], value: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, }, ], @@ -516,7 +508,7 @@ test('capability amplification', () => { source: [d3[0]], value: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, }, ], @@ -542,7 +534,7 @@ test('capability amplification', () => { context: { value: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, }, causes: [ @@ -550,11 +542,11 @@ test('capability amplification', () => { name: 'EscalatedCapability', claimed: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, delegated: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public/photos' }, + with: 'file:///home/zAlice/public/photos', }, cause: { message: `'file:///home/zAlice/public' is not contained in 'file:///home/zAlice/public/photos'`, @@ -579,7 +571,7 @@ test('capability amplification', () => { source: [d5[0]], value: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, }, ], @@ -605,11 +597,11 @@ test('capability amplification', () => { value: [ { can: 'file/read', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, { can: 'file/write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, ], }, @@ -636,11 +628,11 @@ test('capability amplification', () => { value: [ { can: 'file/read', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, { can: 'file/write', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, ], }, @@ -667,7 +659,7 @@ test('capability amplification', () => { context: { value: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, }, causes: [ @@ -675,16 +667,16 @@ test('capability amplification', () => { name: 'EscalatedCapability', claimed: { can: 'file/read+write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, delegated: [ { can: 'file/read', - uri: { href: 'file:///home/zAlice/public/photos/' }, + with: 'file:///home/zAlice/public/photos/', }, { can: 'file/write', - uri: { href: 'file:///home/zAlice/public' }, + with: 'file:///home/zAlice/public', }, ], cause: { @@ -714,11 +706,11 @@ test('capability amplification', () => { value: [ { can: 'file/read', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, { can: 'file/write', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, ], }, @@ -727,11 +719,11 @@ test('capability amplification', () => { value: [ { can: 'file/read', - uri: { href: 'file:///home/' }, + with: 'file:///home/', }, { can: 'file/write', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, ], }, @@ -748,20 +740,16 @@ test('capability or combinator', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.or(write) @@ -781,14 +769,14 @@ test('capability or combinator', () => { source: [r], value: { can: 'file/read', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, }, { source: [w], value: { can: 'file/write', - uri: { href: 'file:///home/zAlice/' }, + with: 'file:///home/zAlice/', }, }, ], @@ -799,26 +787,26 @@ test('capability or combinator', () => { ) }) -test('parse with caveats', () => { +test('parse with nb', () => { const storeAdd = capability({ can: 'store/add', with: URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -827,7 +815,7 @@ test('parse with caveats', () => { }) const v1 = storeAdd.select( - delegate([{ can: 'store/add', with: 'did:key:zAlice', link: 5 }]) + delegate([{ can: 'store/add', with: 'did:key:zAlice', nb: { link: 5 } }]) ) assert.containSubset(v1, { @@ -842,7 +830,11 @@ test('parse with caveats', () => { causes: [ { name: 'MalformedCapability', - capability: { can: 'store/add', with: 'did:key:zAlice', link: 5 }, + capability: { + can: 'store/add', + with: 'did:key:zAlice', + nb: { link: 5 }, + }, cause: { message: 'Expected link to be a CID instead of 5', }, @@ -863,8 +855,8 @@ test('parse with caveats', () => { { value: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: {}, + with: 'did:key:zAlice', + nb: {}, }, }, ], @@ -877,9 +869,11 @@ test('parse with caveats', () => { { can: 'store/add', with: 'did:key:zAlice', - link: CID.parse( - 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' - ), + nb: { + link: CID.parse( + 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' + ), + }, }, ]) ) @@ -891,8 +885,8 @@ test('parse with caveats', () => { context: { value: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: {}, + with: 'did:key:zAlice', + nb: {}, }, }, causes: [ @@ -900,13 +894,13 @@ test('parse with caveats', () => { name: 'EscalatedCapability', claimed: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: {}, + with: 'did:key:zAlice', + nb: {}, }, delegated: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: { + with: 'did:key:zAlice', + nb: { link: CID.parse( 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' ), @@ -928,9 +922,11 @@ test('parse with caveats', () => { { can: 'store/add', with: 'did:key:zAlice', - link: CID.parse( - 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' - ), + nb: { + link: CID.parse( + 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' + ), + }, }, ]) ) @@ -941,9 +937,11 @@ test('parse with caveats', () => { { can: 'store/add', with: 'did:key:zAlice', - link: CID.parse( - 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' - ), + nb: { + link: CID.parse( + 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' + ), + }, }, ]) ) @@ -955,8 +953,8 @@ test('parse with caveats', () => { { value: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: { + with: 'did:key:zAlice', + nb: { link: CID.parse( 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' ), @@ -971,9 +969,11 @@ test('parse with caveats', () => { { can: 'store/add', with: 'did:key:zAlice', - link: CID.parse( - 'bafybeiepa5hmd3vg2i2unyzrhnxnthwi2aksunykhmcaykbl2jx2u77cny' - ), + nb: { + link: CID.parse( + 'bafybeiepa5hmd3vg2i2unyzrhnxnthwi2aksunykhmcaykbl2jx2u77cny' + ), + }, }, ]) ) @@ -985,8 +985,8 @@ test('parse with caveats', () => { context: { value: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: { + with: 'did:key:zAlice', + nb: { link: CID.parse( 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' ), @@ -998,8 +998,8 @@ test('parse with caveats', () => { name: 'EscalatedCapability', claimed: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: { + with: 'did:key:zAlice', + nb: { link: CID.parse( 'bafybeiabis2rrk6m3p7xghz42hi677ectmzqxsvz26icxxs7digddgpbr4' ), @@ -1007,8 +1007,8 @@ test('parse with caveats', () => { }, delegated: { can: 'store/add', - uri: { href: 'did:key:zAlice' }, - caveats: { + with: 'did:key:zAlice', + nb: { link: CID.parse( 'bafybeiepa5hmd3vg2i2unyzrhnxnthwi2aksunykhmcaykbl2jx2u77cny' ), @@ -1031,20 +1031,16 @@ test('and prune', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const readwrite = read.and(write) @@ -1054,7 +1050,7 @@ test('and prune', () => { { can: 'file/read', with: `file:///${alice.did()}/public` }, { can: 'file/write', with: `file:///${bob.did()}/@alice` }, ], - { issuer: alice.principal } + { issuer: alice } ) ) @@ -1072,12 +1068,12 @@ test('and prune', () => { const [match] = v1.matches const matchwrite = match.prune({ canIssue: (capabality, issuer) => - capabality.uri.href.startsWith(`file:///${issuer}`), + capabality.with.startsWith(`file:///${issuer}`), }) const v2 = matchwrite?.select( delegate([{ can: 'file/write', with: `file:///${bob.did()}/@alice` }], { - issuer: bob.principal, + issuer: bob.verifier, }) ) @@ -1091,7 +1087,7 @@ test('and prune', () => { const none = v2?.matches[0].prune({ canIssue: (capabality, issuer) => - capabality.uri.href.startsWith(`file:///${issuer}`), + capabality.with.startsWith(`file:///${issuer}`), }) assert.equal(none, null) @@ -1102,20 +1098,16 @@ test('toString methods', () => { can: 'file/read', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) const write = capability({ can: 'file/write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || - new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` - ), + claimed.with.startsWith(delegated.with) || + new Failure(`'${claimed.with}' is not contained in '${delegated.with}'`), }) assert.equal(read.toString(), '{"can":"file/read"}') @@ -1172,19 +1164,19 @@ test('toString methods', () => { can: 'file/read+write', with: URI.match({ protocol: 'file:' }), derives: (claimed, delegated) => - claimed.uri.pathname.startsWith(delegated.uri.pathname) || + claimed.with.startsWith(delegated.with) || new Failure( - `'${claimed.uri.href}' is not contained in '${delegated.uri.href}'` + `'${claimed.with}' is not contained in '${delegated.with}'` ), }), derives: (claimed, [read, write]) => { - if (!claimed.uri.pathname.startsWith(read.uri.pathname)) { + if (!claimed.with.startsWith(read.with)) { return new Failure( - `'${claimed.uri.href}' is not contained in '${read.uri.href}'` + `'${claimed.with}' is not contained in '${read.with}'` ) - } else if (!claimed.uri.pathname.startsWith(write.uri.pathname)) { + } else if (!claimed.with.startsWith(write.with)) { return new Failure( - `'${claimed.uri.href}' is not contained in '${write.uri.href}'` + `'${claimed.with}' is not contained in '${write.with}'` ) } else { return true @@ -1209,12 +1201,12 @@ test('toString methods', () => { } }) -test('capability create with caveats', () => { +test('capability create with nb', () => { const echo = capability({ can: 'test/echo', with: URI.match({ protocol: 'did:' }), - caveats: { - message: URI.string({ protocol: 'data:' }), + nb: { + message: URI.match({ protocol: 'data:' }), }, }) @@ -1222,9 +1214,10 @@ test('capability create with caveats', () => { echo.create({ // @ts-expect-error - not assignable to did: with: 'file://gozala/path', - caveats: { + nb: { message: 'data:hello', }, + bar: 1, }) }, /Invalid 'with' - Expected did: URI/) @@ -1233,24 +1226,26 @@ test('capability create with caveats', () => { echo.create({ with: alice.did(), }) - }, /Invalid 'caveats.message' - Expected URI but got undefined/) + }, /Invalid 'nb.message' - Expected URI but got undefined/) assert.throws(() => { echo.create({ with: alice.did(), - caveats: { + nb: { // @ts-expect-error message: 'echo:foo', }, }) - }, /Invalid 'caveats.message' - Expected data: URI instead got echo:foo/) + }, /Invalid 'nb.message' - Expected data: URI instead got echo:foo/) assert.deepEqual( - echo.create({ with: alice.did(), caveats: { message: 'data:hello' } }), + echo.create({ with: alice.did(), nb: { message: 'data:hello' } }), { can: 'test/echo', with: alice.did(), - message: 'data:hello', + nb: { + message: 'data:hello', + }, } ) @@ -1258,17 +1253,19 @@ test('capability create with caveats', () => { echo.create({ // @ts-expect-error - must be a string with: new URL(alice.did()), - caveats: { message: 'data:hello' }, + nb: { message: 'data:hello' }, }), { can: 'test/echo', with: alice.did(), - message: 'data:hello', + nb: { + message: 'data:hello', + }, } ) }) -test('capability create without caveats', () => { +test('capability create without nb', () => { const ping = capability({ can: 'test/ping', with: URI.match({ protocol: 'did:' }), @@ -1294,8 +1291,8 @@ test('capability create without caveats', () => { assert.deepEqual( ping.create({ with: alice.did(), - // @ts-expect-error - no caveats expected - caveats: { x: 1 }, + // @ts-expect-error - no nb expected + nb: { x: 1 }, }), { can: 'test/ping', @@ -1306,8 +1303,6 @@ test('capability create without caveats', () => { assert.deepEqual( ping.create({ with: alice.did(), - // @ts-expect-error - no caveats expected - caveats: {}, }), { can: 'test/ping', @@ -1316,7 +1311,7 @@ test('capability create without caveats', () => { ) }) -test('invoke capability (without caveats)', () => { +test('invoke capability (without nb)', () => { const ping = capability({ can: 'test/ping', with: URI.match({ protocol: 'did:' }), @@ -1325,21 +1320,30 @@ test('invoke capability (without caveats)', () => { assert.throws(() => { ping.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, // @ts-expect-error - not assignable to did: with: 'file://gozala/path', }) }, /Invalid 'with' - Expected did: URI/) + const a = invoke({ + issuer: alice, + audience: w3, + capability: { + can: 'test/ping', + with: alice.did(), + }, + }) + assert.deepEqual( ping.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, with: alice.did(), }), invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'test/ping', with: alice.did(), @@ -1350,14 +1354,14 @@ test('invoke capability (without caveats)', () => { assert.deepEqual( ping.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, with: alice.did(), - // @ts-expect-error - no caveats expected - caveats: { x: 1 }, + // @ts-expect-error - no nb expected + nb: { x: 1 }, }), invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'test/ping', with: alice.did(), @@ -1368,14 +1372,47 @@ test('invoke capability (without caveats)', () => { assert.deepEqual( ping.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, with: alice.did(), - // @ts-expect-error - no caveats expected - caveats: {}, + nb: undefined, }), invoke({ issuer: alice, - audience: w3.principal, + audience: w3, + capability: { + can: 'test/ping', + with: alice.did(), + }, + }) + ) + + assert.deepEqual( + ping.invoke({ + issuer: alice, + audience: w3, + with: alice.did(), + // @ts-expect-error - no nb expected + nb: {}, + }), + invoke({ + issuer: alice, + audience: w3, + capability: { + can: 'test/ping', + with: alice.did(), + }, + }) + ) + + assert.deepEqual( + ping.invoke({ + issuer: alice, + audience: w3, + with: alice.did(), + }), + invoke({ + issuer: alice, + audience: w3, capability: { can: 'test/ping', with: alice.did(), @@ -1384,22 +1421,22 @@ test('invoke capability (without caveats)', () => { ) }) -test('invoke capability (with caveats)', () => { +test('invoke capability (with nb)', () => { const echo = capability({ can: 'test/echo', with: URI.match({ protocol: 'did:' }), - caveats: { - message: URI.string({ protocol: 'data:' }), + nb: { + message: URI.match({ protocol: 'data:' }), }, }) assert.throws(() => { echo.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, // @ts-expect-error - not assignable to did: with: 'file://gozala/path', - caveats: { + nb: { message: 'data:hello', }, }) @@ -1409,35 +1446,37 @@ test('invoke capability (with caveats)', () => { // @ts-expect-error echo.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, with: alice.did(), }) - }, /Invalid 'caveats.message' - Expected URI but got undefined/) + }, /Invalid 'nb.message' - Expected URI but got undefined/) assert.throws(() => { echo.create({ with: alice.did(), - caveats: { + nb: { // @ts-expect-error message: 'echo:foo', }, }) - }, /Invalid 'caveats.message' - Expected data: URI instead got echo:foo/) + }, /Invalid 'nb.message' - Expected data: URI instead got echo:foo/) assert.deepEqual( echo.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, with: alice.did(), - caveats: { message: 'data:hello' }, + nb: { message: 'data:hello' }, }), invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'test/echo', with: alice.did(), - message: /** @type {'data:hello'} */ ('data:hello'), + nb: { + message: /** @type {'data:hello'} */ ('data:hello'), + }, }, }) ) @@ -1445,18 +1484,20 @@ test('invoke capability (with caveats)', () => { assert.deepEqual( echo.invoke({ issuer: alice, - audience: w3.principal, + audience: w3, // @ts-expect-error - must be a string with: new URL(alice.did()), - caveats: { message: 'data:hello' }, + nb: { message: 'data:hello' }, }), invoke({ issuer: alice, - audience: w3.principal, + audience: w3, capability: { can: 'test/echo', with: alice.did(), - message: /** @type {'data:hello'} */ ('data:hello'), + nb: { + message: /** @type {'data:hello'} */ ('data:hello'), + }, }, }) ) diff --git a/packages/validator/test/decoder.spec.js b/packages/validator/test/decoder.spec.js index a9098f06..924b2ebd 100644 --- a/packages/validator/test/decoder.spec.js +++ b/packages/validator/test/decoder.spec.js @@ -1,13 +1,13 @@ -import { URI, Link } from '../src/lib.js' +import { URI, Link, Text, DID } from '../src/lib.js' import { test, assert } from './test.js' import { CID } from 'multiformats' { - /** @type {[string, object][]} */ + /** @type {[string, string|{message:string}][]} */ const dataset = [ ['', { message: 'Invalid URI' }], - ['did:key:zAlice', { href: 'did:key:zAlice' }], - ['mailto:alice@mail.net', { href: 'mailto:alice@mail.net' }], + ['did:key:zAlice', 'did:key:zAlice'], + ['mailto:alice@mail.net', 'mailto:alice@mail.net'], ] for (const [input, expect] of dataset) { @@ -18,16 +18,18 @@ import { CID } from 'multiformats' } { - /** @type {[string, `${string}:`, {href?:string, message?:string}][]} */ + /** @type {[unknown, `${string}:`, {message:string}|string][]} */ const dataset = [ + [undefined, 'did:', { message: 'Expected URI but got undefined' }], + [null, 'did:', { message: 'Expected URI but got null' }], ['', 'did:', { message: 'Invalid URI' }], - ['did:key:zAlice', 'did:', { href: 'did:key:zAlice' }], + ['did:key:zAlice', 'did:', 'did:key:zAlice'], [ 'did:key:zAlice', 'mailto:', { message: 'Expected mailto: URI instead got did:key:zAlice' }, ], - ['mailto:alice@mail.net', 'mailto:', { href: 'mailto:alice@mail.net' }], + ['mailto:alice@mail.net', 'mailto:', 'mailto:alice@mail.net'], [ 'mailto:alice@mail.net', 'did:', @@ -40,10 +42,35 @@ import { CID } from 'multiformats' protocol, })}).decode(${JSON.stringify(input)})}}`, () => { assert.containSubset(URI.match({ protocol }).decode(input), expect) - assert.containSubset( - URI.string({ protocol }).decode(input), - expect.href || expect - ) + }) + } +} + +{ + /** @type {[unknown, `${string}:`, {message:string}|string|undefined][]} */ + const dataset = [ + [undefined, 'did:', undefined], + [null, 'did:', { message: 'Expected URI but got null' }], + ['', 'did:', { message: 'Invalid URI' }], + ['did:key:zAlice', 'did:', 'did:key:zAlice'], + [ + 'did:key:zAlice', + 'mailto:', + { message: 'Expected mailto: URI instead got did:key:zAlice' }, + ], + ['mailto:alice@mail.net', 'mailto:', 'mailto:alice@mail.net'], + [ + 'mailto:alice@mail.net', + 'did:', + { message: 'Expected did: URI instead got mailto:alice@mail.net' }, + ], + ] + + for (const [input, protocol, expect] of dataset) { + test(`URI.optional(${JSON.stringify({ + protocol, + })}).decode(${JSON.stringify(input)})}}`, () => { + assert.containSubset(URI.optional({ protocol }).decode(input), expect) }) } } @@ -126,3 +153,193 @@ import { CID } from 'multiformats' }) } } + +{ + /** @type {unknown[][]} */ + const dataset = [ + [undefined, { message: 'Expected a string but got undefined instead' }], + [null, { message: 'Expected a string but got null instead' }], + ['hello', 'hello'], + [ + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ] + + for (const [input, out] of dataset) { + test(`Text.decode(${input})`, () => { + assert.containSubset(Text.decode(input), out) + }) + } +} + +{ + /** @type {[{pattern:RegExp}, unknown, unknown][]} */ + const dataset = [ + [ + { pattern: /hello .*/ }, + undefined, + { message: 'Expected a string but got undefined instead' }, + ], + [ + { pattern: /hello .*/ }, + null, + { message: 'Expected a string but got null instead' }, + ], + [ + { pattern: /hello .*/ }, + 'hello', + { message: 'Expected to match /hello .*/ but got "hello" instead' }, + ], + [{ pattern: /hello .*/ }, 'hello world', 'hello world'], + [ + { pattern: /hello .*/ }, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ] + + for (const [options, input, out] of dataset) { + test(`Text.match({ pattern: ${options.pattern} }).decode(${input})`, () => { + assert.containSubset(Text.match(options).decode(input), out) + }) + } +} + +{ + /** @type {[{pattern?:RegExp}, unknown, unknown][]} */ + const dataset = [ + [{}, undefined, undefined], + [{}, null, { message: 'Expected a string but got null instead' }], + [{}, 'hello', 'hello'], + [ + {}, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + + [{ pattern: /hello .*/ }, undefined, undefined], + [ + { pattern: /hello .*/ }, + null, + { message: 'Expected a string but got null instead' }, + ], + [ + { pattern: /hello .*/ }, + 'hello', + { message: 'Expected to match /hello .*/ but got "hello" instead' }, + ], + [{ pattern: /hello .*/ }, 'hello world', 'hello world'], + [ + { pattern: /hello .*/ }, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ] + + for (const [options, input, out] of dataset) { + test(`Text.match({ pattern: ${options.pattern} }).decode(${input})`, () => { + assert.containSubset(Text.optional(options).decode(input), out) + }) + } +} + +{ + /** @type {unknown[][]} */ + const dataset = [ + [undefined, { message: 'Expected a string but got undefined instead' }], + [null, { message: 'Expected a string but got null instead' }], + ['hello', { message: 'Expected a did: but got "hello" instead' }], + [ + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ['did:echo:1', 'did:echo:1'], + ] + + for (const [input, out] of dataset) { + test(`DID.decode(${input})`, () => { + assert.containSubset(DID.decode(input), out) + }) + } +} + +{ + /** @type {[{method:string}, unknown, unknown][]} */ + const dataset = [ + [ + { method: 'echo' }, + undefined, + { message: 'Expected a string but got undefined instead' }, + ], + [ + { method: 'echo' }, + null, + { message: 'Expected a string but got null instead' }, + ], + [ + { method: 'echo' }, + 'hello', + { message: 'Expected a did:echo: but got "hello" instead' }, + ], + [{ method: 'echo' }, 'did:echo:hello', 'did:echo:hello'], + [ + { method: 'foo' }, + 'did:echo:hello', + { message: 'Expected a did:foo: but got "did:echo:hello" instead' }, + ], + [ + { method: 'echo' }, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ] + + for (const [options, input, out] of dataset) { + test(`DID.match({ method: ${options.method} }).decode(${input})`, () => { + assert.containSubset(DID.match(options).decode(input), out) + }) + } +} + +{ + /** @type {[{method?:string}, unknown, unknown][]} */ + const dataset = [ + [{}, undefined, undefined], + [{}, null, { message: 'Expected a string but got null instead' }], + [{}, 'did:echo:bar', 'did:echo:bar'], + [ + {}, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + + [{ method: 'echo' }, undefined, undefined], + [ + { method: 'echo' }, + null, + { message: 'Expected a string but got null instead' }, + ], + [ + { method: 'echo' }, + 'did:hello:world', + { message: 'Expected a did:echo: but got "did:hello:world" instead' }, + ], + [ + { method: 'echo' }, + 'hello world', + { message: 'Expected a did:echo: but got "hello world" instead' }, + ], + [ + { method: 'echo' }, + new String('hello'), + { message: 'Expected a string but got object instead' }, + ], + ] + + for (const [options, input, out] of dataset) { + test(`DID.optional({ method: "${options.method}" }).decode(${input})`, () => { + assert.containSubset(DID.optional(options).decode(input), out) + }) + } +} diff --git a/packages/validator/test/fixtures.js b/packages/validator/test/fixtures.js index 197b8d98..678e9516 100644 --- a/packages/validator/test/fixtures.js +++ b/packages/validator/test/fixtures.js @@ -1,18 +1,18 @@ -import { SigningPrincipal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' /** did:key:z6Mkqa4oY9Z5Pf5tUcjLHLUsDjKwMC95HGXdE1j22jkbhz6r */ -export const alice = SigningPrincipal.parse( +export const alice = ed25519.parse( 'MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=' ) /** did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob */ -export const bob = SigningPrincipal.parse( +export const bob = ed25519.parse( 'MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=' ) /** did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL */ -export const mallory = SigningPrincipal.parse( +export const mallory = ed25519.parse( 'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=' ) -export const service = SigningPrincipal.parse( +export const service = ed25519.parse( 'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=' ) diff --git a/packages/validator/test/lib.spec.js b/packages/validator/test/lib.spec.js index 066cfb37..992bc965 100644 --- a/packages/validator/test/lib.spec.js +++ b/packages/validator/test/lib.spec.js @@ -1,33 +1,35 @@ import { test, assert } from './test.js' import { access } from '../src/lib.js' -import { capability, URI, Link } from '../src/lib.js' +import { capability, URI, Text, Link, DID } from '../src/lib.js' import { Failure } from '../src/error.js' -import { Principal } from '@ucanto/principal' +import * as ed25519 from '@ucanto/principal/ed25519' import * as Client from '@ucanto/client' -import * as API from '@ucanto/interface' + import { alice, bob, mallory, service as w3 } from './fixtures.js' -import { UCAN } from '@ucanto/core' +import { UCAN, DID as Principal } from '@ucanto/core' import { UnavailableProof } from '../src/error.js' +import { equalWith, canDelegateURI, fail } from './util.js' +import * as API from './types.js' const storeAdd = capability({ can: 'store/add', with: URI.match({ protocol: 'did:' }), - caveats: { + nb: { link: Link.optional(), }, derives: (claimed, delegated) => { - if (claimed.uri.href !== delegated.uri.href) { + if (claimed.with !== delegated.with) { return new Failure( - `Expected 'with: "${delegated.uri.href}"' instead got '${claimed.uri.href}'` + `Expected 'with: "${delegated.with}"' instead got '${claimed.with}'` ) } else if ( - delegated.caveats.link && - `${delegated.caveats.link}` !== `${claimed.caveats.link}` + delegated.nb.link && + `${delegated.nb.link}` !== `${claimed.nb.link}` ) { return new Failure( `Link ${ - claimed.caveats.link == null ? '' : `${claimed.caveats.link} ` - }violates imposed ${delegated.caveats.link} constraint` + claimed.nb.link == null ? '' : `${claimed.nb.link} ` + }violates imposed ${delegated.nb.link} constraint` ) } else { return true @@ -37,7 +39,7 @@ const storeAdd = capability({ test('self-issued invocation', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -48,7 +50,7 @@ test('self-issued invocation', async () => { const result = await access(invocation, { capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -58,10 +60,10 @@ test('self-issued invocation', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.from(alice.did()), + audience: Principal.parse(bob.did()), proofs: [], }) }) @@ -70,7 +72,7 @@ test('expired invocation', async () => { const expiration = UCAN.now() - 5 const invocation = await Client.delegate({ issuer: alice, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -82,7 +84,7 @@ test('expired invocation', async () => { const result = await access(invocation, { capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -119,7 +121,7 @@ test('not vaid before invocation', async () => { const notBefore = UCAN.now() + 500 const invocation = await Client.delegate({ issuer: alice, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -131,7 +133,7 @@ test('not vaid before invocation', async () => { const result = await access(invocation, { capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -151,7 +153,7 @@ test('not vaid before invocation', async () => { test('invalid signature', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -164,7 +166,7 @@ test('invalid signature', async () => { const result = await access(invocation, { capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -183,7 +185,7 @@ test('invalid signature', async () => { test('unknown capability', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/write', @@ -196,7 +198,7 @@ test('unknown capability', async () => { const result = await access(invocation, { // @ts-ignore capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -215,7 +217,7 @@ test('unknown capability', async () => { test('delegated invocation', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -226,7 +228,7 @@ test('delegated invocation', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -238,7 +240,7 @@ test('delegated invocation', async () => { const result = await access(invocation, { capability: storeAdd, - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -248,19 +250,19 @@ test('delegated invocation', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, - issuer: bob.principal.bytes, - audience: w3.principal.bytes, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(w3.did()), proofs: [ { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), proofs: [], }, ], @@ -270,7 +272,7 @@ test('delegated invocation', async () => { test('invalid claim / no proofs', async () => { const invocation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -280,7 +282,7 @@ test('invalid claim / no proofs', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -297,7 +299,7 @@ test('invalid claim / no proofs', async () => { capability: { can: 'store/add', with: bob.did(), - caveats: {}, + nb: {}, }, }, }) @@ -307,7 +309,7 @@ test('invalid claim / expired', async () => { const expiration = UCAN.now() - 5 const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, expiration, capabilities: [ { @@ -319,13 +321,13 @@ test('invalid claim / expired', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: delegation.capabilities, proofs: [delegation], }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -343,7 +345,7 @@ test('invalid claim / expired', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, delegation: invocation, }, @@ -354,7 +356,7 @@ test('invalid claim / not valid before', async () => { const notBefore = UCAN.now() + 60 * 60 const proof = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, notBefore, capabilities: [ { @@ -366,13 +368,13 @@ test('invalid claim / not valid before', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: proof.capabilities, proofs: [proof], }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -390,7 +392,7 @@ test('invalid claim / not valid before', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, delegation: invocation, }, @@ -400,7 +402,7 @@ test('invalid claim / not valid before', async () => { test('invalid claim / invalid signature', async () => { const proof = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -413,13 +415,13 @@ test('invalid claim / invalid signature', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: proof.capabilities, proofs: [proof], }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -437,7 +439,7 @@ test('invalid claim / invalid signature', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, delegation: invocation, }, @@ -447,7 +449,7 @@ test('invalid claim / invalid signature', async () => { test('invalid claim / unknown capability', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/pin', @@ -458,7 +460,7 @@ test('invalid claim / unknown capability', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -469,7 +471,7 @@ test('invalid claim / unknown capability', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -493,7 +495,7 @@ test('invalid claim / malformed capability', async () => { const badDID = `bib:${alice.did().slice(4)}` const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -504,7 +506,7 @@ test('invalid claim / malformed capability', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -515,7 +517,7 @@ test('invalid claim / malformed capability', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -538,7 +540,7 @@ test('invalid claim / malformed capability', async () => { test('invalid claim / unavailable proof', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -549,7 +551,7 @@ test('invalid claim / unavailable proof', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -560,7 +562,7 @@ test('invalid claim / unavailable proof', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -582,7 +584,7 @@ test('invalid claim / unavailable proof', async () => { test('invalid claim / failed to resolve', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -593,7 +595,7 @@ test('invalid claim / failed to resolve', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -604,7 +606,7 @@ test('invalid claim / failed to resolve', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -630,7 +632,7 @@ test('invalid claim / failed to resolve', async () => { test('invalid claim / invalid audience', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -641,7 +643,7 @@ test('invalid claim / invalid audience', async () => { const invocation = await Client.delegate({ issuer: mallory, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -652,7 +654,7 @@ test('invalid claim / invalid audience', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -674,7 +676,7 @@ test('invalid claim / invalid audience', async () => { test('invalid claim / invalid claim', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -685,7 +687,7 @@ test('invalid claim / invalid claim', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -696,7 +698,7 @@ test('invalid claim / invalid claim', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -718,7 +720,7 @@ test('invalid claim / invalid claim', async () => { test('invalid claim / invalid sub delegation', async () => { const proof = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -729,7 +731,7 @@ test('invalid claim / invalid sub delegation', async () => { const delegation = await Client.delegate({ issuer: bob, - audience: mallory.principal, + audience: mallory, capabilities: [ { can: 'store/add', @@ -741,7 +743,7 @@ test('invalid claim / invalid sub delegation', async () => { const invocation = await Client.delegate({ issuer: mallory, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -752,7 +754,7 @@ test('invalid claim / invalid sub delegation', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, @@ -777,7 +779,7 @@ test('invalid claim / invalid sub delegation', async () => { test('delegate with my:*', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: '*', @@ -788,7 +790,7 @@ test('delegate with my:*', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -799,11 +801,11 @@ test('delegate with my:*', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, - my: (issuer) => { + my: issuer => { return [ { can: 'store/add', @@ -819,13 +821,13 @@ test('delegate with my:*', async () => { can: 'store/add', with: alice.did(), }, - issuer: bob.principal.bytes, - audience: w3.principal.bytes, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(w3.did()), proofs: [ { delegation, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), capability: { can: 'store/add', with: alice.did(), @@ -839,7 +841,7 @@ test('delegate with my:*', async () => { test('delegate with my:did', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: '*', @@ -850,7 +852,7 @@ test('delegate with my:did', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -861,11 +863,11 @@ test('delegate with my:did', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, - my: (issuer) => { + my: issuer => { return [ { can: 'store/add', @@ -881,13 +883,13 @@ test('delegate with my:did', async () => { can: 'store/add', with: alice.did(), }, - issuer: bob.principal.bytes, - audience: w3.principal.bytes, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(w3.did()), proofs: [ { delegation, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), capability: { can: 'store/add', with: alice.did(), @@ -901,7 +903,7 @@ test('delegate with my:did', async () => { test('delegate with as:*', async () => { const my = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: '*', @@ -912,7 +914,7 @@ test('delegate with as:*', async () => { const as = await Client.delegate({ issuer: bob, - audience: mallory.principal, + audience: mallory, capabilities: [ { can: '*', @@ -924,7 +926,7 @@ test('delegate with as:*', async () => { const invocation = await Client.delegate({ issuer: mallory, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -935,11 +937,11 @@ test('delegate with as:*', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, - my: (issuer) => { + my: issuer => { return [ { can: 'store/add', @@ -958,8 +960,8 @@ test('delegate with as:*', async () => { proofs: [ { delegation: as, - issuer: bob.principal.bytes, - audience: mallory.principal.bytes, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(mallory.did()), capability: { can: 'store/add', with: alice.did(), @@ -967,8 +969,8 @@ test('delegate with as:*', async () => { proofs: [ { delegation: my, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), capability: { can: 'store/add', with: alice.did(), @@ -988,7 +990,7 @@ test('delegate with as:did', async () => { const my = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: '*', @@ -999,7 +1001,7 @@ test('delegate with as:did', async () => { const as = await Client.delegate({ issuer: bob, - audience: mallory.principal, + audience: mallory, capabilities: [ { can: 'msg/send', @@ -1011,7 +1013,7 @@ test('delegate with as:did', async () => { const invocation = await Client.delegate({ issuer: mallory, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'msg/send', @@ -1022,14 +1024,14 @@ test('delegate with as:did', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return ( claim.with === issuer || (issuer === alice.did() && claim.can === 'msg/send') ) }, - my: (issuer) => { + my: issuer => { return [ { can: 'msg/send', @@ -1048,8 +1050,8 @@ test('delegate with as:did', async () => { proofs: [ { delegation: as, - issuer: bob.principal.bytes, - audience: mallory.principal.bytes, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(mallory.did()), capability: { can: 'msg/send', with: 'mailto:alice@web.mail', @@ -1057,8 +1059,8 @@ test('delegate with as:did', async () => { proofs: [ { delegation: my, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), capability: { can: 'msg/send', with: 'mailto:alice@web.mail', @@ -1073,7 +1075,7 @@ test('delegate with as:did', async () => { test('resolve proof', async () => { const delegation = await Client.delegate({ issuer: alice, - audience: bob.principal, + audience: bob, capabilities: [ { can: 'store/add', @@ -1084,7 +1086,7 @@ test('resolve proof', async () => { const invocation = await Client.delegate({ issuer: bob, - audience: w3.principal, + audience: w3, capabilities: [ { can: 'store/add', @@ -1095,11 +1097,11 @@ test('resolve proof', async () => { }) const result = await access(invocation, { - principal: Principal, + principal: ed25519.Verifier, canIssue: (claim, issuer) => { return claim.with === issuer }, - resolve: async (link) => { + resolve: async link => { if (link.toString() === delegation.cid.toString()) { return delegation } else { @@ -1113,13 +1115,13 @@ test('resolve proof', async () => { capability: { can: 'store/add', with: alice.did(), - caveats: {}, + nb: {}, }, proofs: [ { delegation, - issuer: alice.principal.bytes, - audience: bob.principal.bytes, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), capability: { can: 'store/add', with: alice.did(), @@ -1129,3 +1131,91 @@ test('resolve proof', async () => { ], }) }) + +test('execute capabilty', async () => { + const Voucher = capability({ + can: 'voucher/*', + with: DID.match({ method: 'key' }), + }) + + const Claim = Voucher.derive({ + to: capability({ + can: 'voucher/claim', + with: DID.match({ method: 'key' }), + nb: { + product: Link, + identity: URI.match({ protocol: 'mailto:' }), + service: DID, + }, + derives: (child, parent) => { + return ( + fail(equalWith(child, parent)) || + fail(canDelegateURI(child.nb.identity, parent.nb.identity)) || + fail( + canDelegateURI( + child.nb.product.toString(), + parent.nb.product.toString() + ) + ) || + fail(canDelegateURI(child.nb.service, parent.nb.service)) || + true + ) + }, + }), + derives: equalWith, + }) + + const claim = Claim.invoke({ + issuer: alice, + audience: w3, + with: alice.did(), + nb: { + identity: URI.from(`mailto:${alice.did}@web.mail`), + product: Link.parse('bafkqaaa'), + service: w3.did(), + }, + }) + + const Redeem = capability({ + can: 'voucher/redeem', + with: URI.match({ protocol: 'did:' }), + nb: { + product: Text, + identity: Text, + account: URI.match({ protocol: 'did:' }), + }, + }) + + const proof = Redeem.invoke({ + issuer: alice, + audience: w3, + with: alice.did(), + nb: { + product: 'test', + identity: 'whatever', + account: alice.did(), + }, + }) + + /** + * @param {API.ConnectionView} connection + * @param {API.Delegation<[API.VoucherRedeem]>} proof + */ + + const demo = async (connection, proof) => { + const redeem = Redeem.invoke({ + issuer: alice, + audience: w3, + with: alice.did(), + nb: { + product: 'test', + identity: proof.capabilities[0].nb.identity, + account: alice.did(), + }, + }) + + const r = await redeem.execute(connection) + + const result = await claim.execute(connection) + } +}) diff --git a/packages/interface/src/principal.js b/packages/validator/test/types.js similarity index 100% rename from packages/interface/src/principal.js rename to packages/validator/test/types.js diff --git a/packages/validator/test/types.ts b/packages/validator/test/types.ts new file mode 100644 index 00000000..a317ad92 --- /dev/null +++ b/packages/validator/test/types.ts @@ -0,0 +1,59 @@ +import type { + Capability, + DID, + Link, + ServiceMethod, + Failure, +} from '@ucanto/interface' + +export * from '@ucanto/interface' + +type AccountDID = DID<'key'> +type AgentDID = DID<'key'> + +// Voucher Protocol +export interface VoucherClaim + extends Capability< + 'voucher/claim', + AccountDID | AgentDID, + { + /** + * Product CID + */ + product: Link + + /** + * URI for an identity to be validated + */ + identity: `mailto:${string}` + + /** + * DID of the service they wish to redeem voucher with + */ + service: DID + } + > {} + +export interface VoucherRedeem + extends Capability< + 'voucher/redeem', + `did:${string}`, + { + product: string + identity: string + account: `did:${string}` + } + > { + nb: { + product: string + identity: string + account: `did:${string}` + } +} + +export interface Service { + voucher: { + claim: ServiceMethod + redeem: ServiceMethod + } +} diff --git a/packages/validator/test/util.js b/packages/validator/test/util.js new file mode 100644 index 00000000..f5a474ce --- /dev/null +++ b/packages/validator/test/util.js @@ -0,0 +1,41 @@ +import { Failure } from '../src/error.js' +import * as API from '@ucanto/interface' + +/** + * @param {API.Failure|true} value + */ +export const fail = value => (value === true ? undefined : value) + +/** + * Check URI can be delegated + * + * @param {string} child + * @param {string} parent + */ +export function canDelegateURI(child, parent) { + if (parent.endsWith('*')) { + return child.startsWith(parent.slice(0, -1)) + ? true + : new Failure(`${child} does not match ${parent}`) + } + + return child === parent + ? true + : new Failure(`${child} is different from ${parent}`) +} + +/** + * Checks that `with` on claimed capability is the same as `with` + * in delegated capability. Note this will ignore `can` field. + * + * @param {API.ParsedCapability} child + * @param {API.ParsedCapability} parent + */ +export function equalWith(child, parent) { + return ( + child.with === parent.with || + new Failure( + `Can not derive ${child.can} with ${child.with} from ${parent.with}` + ) + ) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 624c70c8..c1ee589e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: specifiers: '@ipld/car': ^4.1.5 '@ipld/dag-cbor': ^7.0.3 - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': ^4.0.0-beta '@types/chai': ^4.3.3 '@types/chai-subset': ^1.3.3 '@types/mocha': ^9.1.0 @@ -72,7 +72,7 @@ importers: dependencies: '@ipld/car': 4.1.5 '@ipld/dag-cbor': 7.0.3 - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': 4.0.0-beta '@ucanto/interface': link:../interface multiformats: 9.8.1 devDependencies: @@ -90,18 +90,18 @@ importers: packages/interface: specifiers: - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': ^4.0.0-beta multiformats: ^9.8.1 typescript: ^4.8.3 dependencies: - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': 4.0.0-beta multiformats: 9.8.1 devDependencies: typescript: 4.8.3 packages/principal: specifiers: - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': ^4.0.0-beta '@noble/ed25519': ^1.7.0 '@types/chai': ^4.3.3 '@types/mocha': ^9.1.0 @@ -114,7 +114,7 @@ importers: playwright-test: ^8.1.1 typescript: ^4.8.3 dependencies: - '@ipld/dag-ucan': 3.0.0-beta + '@ipld/dag-ucan': 4.0.0-beta '@noble/ed25519': 1.7.0 '@ucanto/interface': link:../interface multiformats: 9.8.1 @@ -478,8 +478,8 @@ packages: multiformats: 9.8.1 dev: false - /@ipld/dag-ucan/3.0.0-beta: - resolution: {integrity: sha512-WzKh4mDiUElslfI/cg9VjLNHsT+9r3XjwbDY/gck+1289sCU1Hywj1/9PMx0DMJoEEwd2OFy/bq5PX2IyQ20Cw==} + /@ipld/dag-ucan/4.0.0-beta: + resolution: {integrity: sha512-LNlTYutHn8fwJpempDFVKHpTG52hpz3QkHQJUkdh1yP79+cfpjGHP31R0lhUmYLouZb0d0r4wXKKJCrPnq7oVQ==} dependencies: '@ipld/dag-cbor': 7.0.3 '@ipld/dag-json': 8.0.11