iso-ucan 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/src/capability.d.ts +0 -3
  2. package/dist/src/capability.d.ts.map +1 -1
  3. package/dist/src/delegation.d.ts +4 -1
  4. package/dist/src/delegation.d.ts.map +1 -1
  5. package/dist/src/envelope.d.ts +1 -1
  6. package/dist/src/envelope.d.ts.map +1 -1
  7. package/dist/src/invocation.d.ts.map +1 -1
  8. package/dist/src/rpc/client.d.ts +24 -0
  9. package/dist/src/rpc/client.d.ts.map +1 -0
  10. package/dist/src/rpc/command.d.ts +19 -0
  11. package/dist/src/rpc/command.d.ts.map +1 -0
  12. package/dist/src/rpc/index.d.ts +5 -0
  13. package/dist/src/rpc/index.d.ts.map +1 -0
  14. package/dist/src/rpc/receipt.d.ts +114 -0
  15. package/dist/src/rpc/receipt.d.ts.map +1 -0
  16. package/dist/src/rpc/server.d.ts +18 -0
  17. package/dist/src/rpc/server.d.ts.map +1 -0
  18. package/dist/src/rpc/types.d.ts +112 -0
  19. package/dist/src/rpc/types.d.ts.map +1 -0
  20. package/dist/src/store.d.ts +4 -2
  21. package/dist/src/store.d.ts.map +1 -1
  22. package/dist/src/types.d.ts +15 -3
  23. package/dist/src/types.d.ts.map +1 -1
  24. package/dist/src/utils.d.ts +19 -2
  25. package/dist/src/utils.d.ts.map +1 -1
  26. package/license +9 -0
  27. package/package.json +19 -26
  28. package/readme.md +185 -18
  29. package/src/capability.js +1 -6
  30. package/src/delegation.js +15 -13
  31. package/src/invocation.js +10 -6
  32. package/src/rpc/client.js +85 -0
  33. package/src/rpc/command.js +28 -0
  34. package/src/rpc/index.js +10 -0
  35. package/src/rpc/receipt.js +136 -0
  36. package/src/rpc/server.js +155 -0
  37. package/src/rpc/types.js +9 -0
  38. package/src/rpc/types.ts +153 -0
  39. package/src/store.js +41 -16
  40. package/src/types.ts +15 -6
  41. package/src/utils.js +39 -15
  42. package/dist/src/client.d.ts +0 -41
  43. package/dist/src/client.d.ts.map +0 -1
  44. package/dist/src/server.d.ts +0 -38
  45. package/dist/src/server.d.ts.map +0 -1
  46. package/src/client.js +0 -52
  47. package/src/server.js +0 -47
@@ -0,0 +1,136 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { z } from 'zod/v4'
3
+
4
+ /**
5
+ * @import {z as zType} from 'zod/v4'
6
+ */
7
+
8
+ /**
9
+ * Schema for an Invocation CID.
10
+ *
11
+ * Accepts a {@link CID} instance (server-side construction), a CID string,
12
+ * or the dag-json `{ "/": "<cid>" }` form (which is what `CID.toJSON()`
13
+ * produces when serialized through `JSON.stringify`) and normalizes to a
14
+ * {@link CID}.
15
+ */
16
+ export const cidSchema = z.union([
17
+ z.instanceof(CID),
18
+ z
19
+ .union([z.string(), z.object({ '/': z.string() }).transform((v) => v['/'])])
20
+ .transform((value, ctx) => {
21
+ try {
22
+ return CID.parse(value)
23
+ } catch (error) {
24
+ ctx.addIssue({
25
+ code: 'custom',
26
+ message: `Invalid CID: ${
27
+ error instanceof Error ? error.message : String(error)
28
+ }`,
29
+ })
30
+ return z.NEVER
31
+ }
32
+ }),
33
+ ])
34
+
35
+ /**
36
+ * Schema for a successful receipt: `{ cid, result }`.
37
+ *
38
+ * @template {zType.ZodType} ResultSchema
39
+ * @param {ResultSchema} result Schema for the `result` payload.
40
+ * @returns {zType.ZodObject<{ cid: typeof cidSchema, result: ResultSchema }>}
41
+ */
42
+ export function receiptResult(result) {
43
+ return z.object({
44
+ cid: cidSchema,
45
+ result,
46
+ })
47
+ }
48
+
49
+ /**
50
+ * Schema for an error receipt: `{ cid, error: { code, message, data } }`.
51
+ *
52
+ * `code` and `message` are encoded as Zod literals so each error variant is
53
+ * distinguishable in the resulting union.
54
+ *
55
+ * @template {string} Code
56
+ * @template {string} Message
57
+ * @template {zType.ZodType} DataSchema
58
+ * @param {Code} code
59
+ * @param {Message} message
60
+ * @param {DataSchema} data Schema for the structured error `data` payload.
61
+ * @returns {zType.ZodObject<{
62
+ * cid: typeof cidSchema,
63
+ * error: zType.ZodObject<{
64
+ * code: zType.ZodLiteral<Code>,
65
+ * message: zType.ZodLiteral<Message>,
66
+ * data: DataSchema,
67
+ * }>,
68
+ * }>}
69
+ */
70
+ export function receiptError(code, message, data) {
71
+ return z.object({
72
+ cid: cidSchema,
73
+ error: z.object({
74
+ code: z.literal(code),
75
+ message: z.literal(message),
76
+ data,
77
+ }),
78
+ })
79
+ }
80
+
81
+ /**
82
+ * Generic schema for server-emitted internal errors.
83
+ *
84
+ * Unlike {@link receiptError} (which is part of the per-command receipt
85
+ * schema with literal `code`/`message`), this schema is emitted by the
86
+ * server for any error that happens outside of normal handler execution
87
+ * (invalid invocation, command not found, args validation failure, handler
88
+ * threw, etc.).
89
+ *
90
+ * `cid` is optional because some errors happen before the invocation can
91
+ * be decoded.
92
+ *
93
+ * Automatically included as a variant in every command receipt produced by
94
+ * {@link receipt}, so the client always knows how to validate and discriminate
95
+ * a server error from a normal receipt.
96
+ */
97
+ export const receiptServerError = z.object({
98
+ error: z.object({
99
+ code: z.literal('SERVER_ERROR'),
100
+ message: z.string(),
101
+ cid: cidSchema.optional(),
102
+ data: z.unknown().optional(),
103
+ }),
104
+ })
105
+
106
+ /**
107
+ * @typedef {zType.infer<typeof receiptServerError>} ReceiptServerError
108
+ */
109
+
110
+ /**
111
+ * Combine one {@link receiptResult} schema and zero or more
112
+ * {@link receiptError} schemas into a single discriminated receipt schema.
113
+ *
114
+ * The {@link receiptServerError} schema is always appended as the last
115
+ * variant of the union so every command receipt accepts a server-emitted
116
+ * internal error in addition to its declared result/error variants.
117
+ *
118
+ * The resulting schema validates one of:
119
+ * - `{ cid, result }`
120
+ * - `{ cid, error: { code, message, data } }` (one of the declared command errors)
121
+ * - `{ error: { code, message, cid?, data? } }` (a generic server error)
122
+ *
123
+ * Consumers discriminate via `'result' in receipt` / `'error' in receipt`,
124
+ * and within the error branch via the `code` field.
125
+ *
126
+ * @template {zType.ZodType} Result
127
+ * @template {zType.ZodType} Error
128
+ * @param {Result} result
129
+ * @param {...Error} errors
130
+ * @returns {zType.ZodUnion<readonly [Result, ...Error[], typeof receiptServerError]>}
131
+ */
132
+ export function receipt(result, ...errors) {
133
+ return /** @type {any} */ (
134
+ z.union(/** @type {any} */ ([result, ...errors, receiptServerError]))
135
+ )
136
+ }
@@ -0,0 +1,155 @@
1
+ import { u8 } from 'iso-base/utils'
2
+ import { Invocation } from '../invocation.js'
3
+
4
+ /**
5
+ * @import {
6
+ * CommandsRecord,
7
+ * DefineServerOptions,
8
+ * ServerRequestHandler,
9
+ * } from './types.js'
10
+ * @import {ReceiptServerError} from './receipt.js'
11
+ */
12
+
13
+ /**
14
+ * Build a {@link ReceiptServerError} payload.
15
+ *
16
+ * @param {string} message
17
+ * @param {{
18
+ * cid?: import('multiformats').CID,
19
+ * cmd?: string,
20
+ * issues?: readonly import('@standard-schema/spec').StandardSchemaV1.Issue[],
21
+ * }} [extra]
22
+ * @returns {ReceiptServerError}
23
+ */
24
+ function buildServerError(message, extra) {
25
+ /** @type {Record<string, unknown> | undefined} */
26
+ let data
27
+ if (extra?.cmd !== undefined || extra?.issues !== undefined) {
28
+ data = {}
29
+ if (extra.cmd !== undefined) data.cmd = extra.cmd
30
+ if (extra.issues !== undefined) data.issues = extra.issues
31
+ }
32
+
33
+ /** @type {ReceiptServerError['error']} */
34
+ const error = { code: 'SERVER_ERROR', message }
35
+ if (data !== undefined) error.data = data
36
+ if (extra?.cid !== undefined) error.cid = extra.cid
37
+ return { error }
38
+ }
39
+
40
+ /**
41
+ * Build a JSON {@link Response} for a server error.
42
+ *
43
+ * @param {number} status
44
+ * @param {string} message
45
+ * @param {Parameters<typeof buildServerError>[1]} [extra]
46
+ */
47
+ function serverErrorResponse(status, message, extra) {
48
+ return Response.json(buildServerError(message, extra), { status })
49
+ }
50
+
51
+ /**
52
+ * Define a type-safe server from a record of commands.
53
+ *
54
+ * The returned function accepts a standard `Request`, decodes its body as a
55
+ * UCAN {@link Invocation}, validates the args against the matching command's
56
+ * schema, and dispatches to the corresponding handler in `options.handlers`.
57
+ * The handler's return value is serialized to JSON.
58
+ *
59
+ * @template {CommandsRecord} Commands
60
+ * @param {Commands} commands
61
+ * @param {DefineServerOptions<Commands>} options
62
+ * @returns {ServerRequestHandler}
63
+ */
64
+ export function defineServer(commands, options) {
65
+ /** @type {Record<string, CommandsRecord[string]>} */
66
+ const commandMap = {}
67
+ for (const command of Object.values(commands)) {
68
+ commandMap[command.capability.cmd] = command
69
+ }
70
+
71
+ /** @type {Record<string, (opts: any) => unknown>} */
72
+ const handlers = options.handlers
73
+
74
+ return async (request) => {
75
+ /** @type {Invocation | undefined} */
76
+ let invocation
77
+
78
+ try {
79
+ const bytes = u8(await request.arrayBuffer())
80
+
81
+ invocation = await Invocation.from({
82
+ bytes,
83
+ audience: options.signer,
84
+ verifierResolver: options.verifierResolver,
85
+ resolveProof: options.store.resolveProof.bind(options.store),
86
+ })
87
+ } catch (error) {
88
+ return serverErrorResponse(
89
+ 400,
90
+ `Invalid invocation: ${
91
+ error instanceof Error ? error.message : String(error)
92
+ }`
93
+ )
94
+ }
95
+
96
+ const cid = invocation.cid
97
+ const cmd = invocation.payload.cmd
98
+ const command = commandMap[cmd]
99
+ if (!command) {
100
+ return serverErrorResponse(404, `Command not found: ${cmd}`, {
101
+ cid,
102
+ cmd,
103
+ })
104
+ }
105
+
106
+ const handler = handlers[cmd]
107
+ if (!handler) {
108
+ return serverErrorResponse(404, `Handler not found: ${cmd}`, {
109
+ cid,
110
+ cmd,
111
+ })
112
+ }
113
+
114
+ const validated = await command.capability.validate(
115
+ /** @type {any} */ (invocation.payload.args)
116
+ )
117
+ if (validated.issues) {
118
+ return serverErrorResponse(400, `Invalid args for ${cmd}`, {
119
+ cid,
120
+ cmd,
121
+ issues: validated.issues,
122
+ })
123
+ }
124
+
125
+ /** @type {unknown} */
126
+ let receipt
127
+ try {
128
+ receipt = await handler({
129
+ args: validated.value,
130
+ invocation,
131
+ signer: options.signer,
132
+ store: options.store,
133
+ })
134
+ } catch (error) {
135
+ return serverErrorResponse(
136
+ 500,
137
+ error instanceof Error ? error.message : String(error),
138
+ { cid, cmd }
139
+ )
140
+ }
141
+
142
+ const validatedReceipt = await command.receipt['~standard'].validate(
143
+ /** @type {any} */ (receipt)
144
+ )
145
+ if (validatedReceipt.issues) {
146
+ return serverErrorResponse(500, `Invalid receipt for ${cmd}`, {
147
+ cid,
148
+ cmd,
149
+ issues: validatedReceipt.issues,
150
+ })
151
+ }
152
+
153
+ return Response.json(receipt)
154
+ }
155
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Runtime stub for the type-only `./types.ts` module so that
3
+ * `export * from './types.js'` in `index.js` resolves under plain Node
4
+ * (e.g. when consumers run `.ts` files via `--experimental-strip-types`).
5
+ *
6
+ * All real declarations live in `./types.ts`; TypeScript's bundler
7
+ * resolution prefers the `.ts` file at type-check time.
8
+ */
9
+ export {}
@@ -0,0 +1,153 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
2
+ import type { ISigner } from 'iso-signatures/types'
3
+ import type { Capability } from '../capability.js'
4
+ import type { Invocation } from '../invocation.js'
5
+ import type { Store } from '../store.js'
6
+ import type { ClientOptions, Promisable, VerifierResolver } from '../types.js'
7
+
8
+ export interface CommandOptions<
9
+ Args extends StandardSchemaV1,
10
+ Cmd extends string = string,
11
+ ReceiptSchema extends StandardSchemaV1 = StandardSchemaV1,
12
+ > {
13
+ cmd: Cmd
14
+ args: Args
15
+ receipt: ReceiptSchema
16
+ }
17
+
18
+ export interface CommandOutput<
19
+ Args extends StandardSchemaV1,
20
+ Cmd extends string = string,
21
+ ReceiptSchema extends StandardSchemaV1 = StandardSchemaV1,
22
+ > {
23
+ capability: Capability<Args, Cmd>
24
+ receipt: ReceiptSchema
25
+ }
26
+
27
+ export type AnyCommand = CommandOutput<
28
+ StandardSchemaV1,
29
+ string,
30
+ StandardSchemaV1
31
+ >
32
+
33
+ export type CommandsRecord = Record<string, AnyCommand>
34
+
35
+ /**
36
+ * Union of `{cmd, args, receipt}` shapes derived from a commands record.
37
+ * Used to discriminate `request` calls by `cmd`.
38
+ *
39
+ * `args` is the schema's input type (what the caller provides) and `receipt`
40
+ * is the schema's output type (what the caller receives back).
41
+ */
42
+ export type CommandUnion<Commands extends CommandsRecord> = {
43
+ [K in keyof Commands]: Commands[K] extends CommandOutput<
44
+ infer Args,
45
+ infer Cmd,
46
+ infer ReceiptSchema
47
+ >
48
+ ? {
49
+ cmd: Cmd
50
+ args: StandardSchemaV1.InferInput<Args>
51
+ receipt: StandardSchemaV1.InferOutput<ReceiptSchema>
52
+ }
53
+ : never
54
+ }[keyof Commands]
55
+
56
+ /**
57
+ * Server-side counterpart to {@link CommandUnion}: `args` is the schema's
58
+ * output (validated value passed to the handler) and `receipt` is the
59
+ * schema's input — the full receipt object, including `cid`, that the
60
+ * handler is responsible for constructing (typically using `invocation.cid`).
61
+ */
62
+ export type ServerCommandUnion<Commands extends CommandsRecord> = {
63
+ [K in keyof Commands]: Commands[K] extends CommandOutput<
64
+ infer Args,
65
+ infer Cmd,
66
+ infer ReceiptSchema
67
+ >
68
+ ? {
69
+ cmd: Cmd
70
+ args: StandardSchemaV1.InferOutput<Args>
71
+ receipt: StandardSchemaV1.InferInput<ReceiptSchema>
72
+ }
73
+ : never
74
+ }[keyof Commands]
75
+
76
+ /**
77
+ * Options for {@link defineClient}.
78
+ *
79
+ * Same as {@link ClientOptions} but the `capabilities` are derived from the
80
+ * commands record passed as the first argument.
81
+ */
82
+ export type DefineClientOptions = Omit<ClientOptions, 'capabilities'>
83
+
84
+ /**
85
+ * Options for {@link CommandClient.request}.
86
+ */
87
+ export type RequestOptions<
88
+ Commands extends CommandsRecord,
89
+ Cmd extends CommandUnion<Commands>['cmd'] = CommandUnion<Commands>['cmd'],
90
+ > = {
91
+ cmd: Cmd
92
+ args: Extract<CommandUnion<Commands>, { cmd: Cmd }>['args']
93
+ }
94
+
95
+ /**
96
+ * Type-safe client built from a record of commands.
97
+ */
98
+ export interface CommandClient<Commands extends CommandsRecord> {
99
+ request<Cmd extends CommandUnion<Commands>['cmd']>(
100
+ options: RequestOptions<Commands, Cmd>
101
+ ): Promise<Extract<CommandUnion<Commands>, { cmd: Cmd }>['receipt']>
102
+ invoke<Cmd extends CommandUnion<Commands>['cmd']>(
103
+ options: RequestOptions<Commands, Cmd>
104
+ ): Promise<Invocation>
105
+ }
106
+
107
+ /**
108
+ * Options passed to a server handler.
109
+ *
110
+ * `args` is the validated/decoded args from the command's schema.
111
+ */
112
+ export interface ServerHandlerOptions<Args> {
113
+ args: Args
114
+ invocation: Invocation
115
+ signer: ISigner
116
+ store: Store
117
+ }
118
+
119
+ /**
120
+ * Map of server handlers keyed by `cmd`, derived from a commands record.
121
+ *
122
+ * Each handler receives args typed as the command's args schema output and
123
+ * must return a value matching the command's receipt schema input.
124
+ */
125
+ export type ServerHandlers<Commands extends CommandsRecord> = {
126
+ [Cmd in ServerCommandUnion<Commands>['cmd']]: (
127
+ options: ServerHandlerOptions<
128
+ Extract<ServerCommandUnion<Commands>, { cmd: Cmd }>['args']
129
+ >
130
+ ) => Promisable<
131
+ Extract<ServerCommandUnion<Commands>, { cmd: Cmd }>['receipt']
132
+ >
133
+ }
134
+
135
+ /**
136
+ * Options for {@link defineServer}.
137
+ */
138
+ export interface DefineServerOptions<Commands extends CommandsRecord> {
139
+ handlers: ServerHandlers<Commands>
140
+ /**
141
+ * The server's signer. Used as the audience when decoding the incoming
142
+ * invocation and passed through to handlers as `signer` so they can issue
143
+ * receipts or further invocations.
144
+ */
145
+ signer: ISigner
146
+ store: Store
147
+ verifierResolver: VerifierResolver
148
+ }
149
+
150
+ /**
151
+ * Handler for an incoming HTTP request, returned by {@link defineServer}.
152
+ */
153
+ export type ServerRequestHandler = (request: Request) => Promise<Response>
package/src/store.js CHANGED
@@ -128,14 +128,28 @@ export class Store {
128
128
  }
129
129
 
130
130
  /**
131
- * Resolve a single chain of proofs ending with a root (subject === issuer).
132
- * Returns the first such chain found (depth-first).
131
+ * Resolve the first such chain found (depth-first), ordered from root to leaf.
132
+ *
133
+ * Cycles are detected via a per-path visited set of delegation CIDs so that
134
+ * self-delegations or other loops can be skipped without recursing forever.
133
135
  *
134
136
  * @param {import('./types.js').StoreProofsOptions} options
135
137
  * @returns {Promise<Delegation[]>}
136
138
  */
137
139
  async chain({ aud, sub, cmd, args }) {
138
- // console.log('🚀 ~ aud', aud, 'sub', sub, 'cmd', cmd)
140
+ const path = await this.#chain({ aud, sub, cmd, args }, new Set())
141
+ return path ? path.reverse() : []
142
+ }
143
+
144
+ /**
145
+ * Internal depth-first search for a delegation chain. Returns the path in
146
+ * leaf-to-root order, or `null` if no path exists.
147
+ *
148
+ * @param {import('./types.js').StoreProofsOptions} options
149
+ * @param {Set<string>} visited - delegation CIDs already on the current path
150
+ * @returns {Promise<Delegation[] | null>}
151
+ */
152
+ async #chain({ aud, sub, cmd, args }, visited) {
139
153
  const parents = parentCmds(cmd)
140
154
 
141
155
  const sources = merge(
@@ -147,28 +161,39 @@ export class Store {
147
161
  if (!parents.includes(proof.cmd)) continue
148
162
  if (!validate(args, proof.pol)) continue
149
163
 
164
+ const cidStr = proof.cid.toString()
165
+ if (visited.has(cidStr)) continue
166
+
167
+ const issDid = didParse(proof.iss).did
168
+
150
169
  // If root, return this proof as the end of the path
151
- if (proof.sub === didParse(proof.iss).did) {
152
- // console.log('🚀 ~ found root:', proof.iss, proof.sub, proof.cmd)
170
+ if (proof.sub === issDid) {
153
171
  return [proof]
154
172
  }
155
173
 
156
- // Otherwise, go deeper and prepend this proof if a path is found
157
- // console.log('🚀 ~ trying deeper:', proof.iss, proof.sub, proof.cmd)
158
- const nextPath = await this.chain({
159
- aud: didParse(proof.iss).did,
160
- sub: proof.sub ? proof.sub : sub,
161
- cmd: proof.cmd,
162
- args,
163
- })
174
+ // Skip non-root self-delegations (iss === aud). They never add
175
+ // capabilities a chain wouldn't already have through the predecessor
176
+ // delegation, and following them only lengthens the resulting chain.
177
+ if (issDid === proof.aud) continue
178
+
179
+ visited.add(cidStr)
180
+ const nextPath = await this.#chain(
181
+ {
182
+ aud: issDid,
183
+ sub: proof.sub ? proof.sub : sub,
184
+ cmd: proof.cmd,
185
+ args,
186
+ },
187
+ visited
188
+ )
189
+ visited.delete(cidStr)
190
+
164
191
  if (nextPath?.length) {
165
192
  return [proof, ...nextPath]
166
193
  }
167
- // console.log('🚀 ~ no path found')
168
194
  }
169
195
 
170
- // No path found
171
- return []
196
+ return null
172
197
  }
173
198
  }
174
199
 
package/src/types.ts CHANGED
@@ -13,6 +13,7 @@ import type { Policy } from './types/policy'
13
13
  export type { StandardSchemaV1 } from '@standard-schema/spec'
14
14
  export type { DID, DIDURL, DIDURLObject } from 'iso-did/types'
15
15
  export type { ISigner } from 'iso-signatures/types'
16
+ export * from './rpc/types'
16
17
  export * from './types/envelope'
17
18
  export * from './types/policy'
18
19
  export * from './types/varsig'
@@ -63,12 +64,22 @@ export interface DelegationOptions<Schema extends StandardSchemaV1> {
63
64
  nonce?: Uint8Array
64
65
  cmd: string
65
66
  meta?: CborObject
67
+ /**
68
+ * The current time in seconds. Mostly used for testing.
69
+ * @default Math.floor(Date.now() / 1000)
70
+ */
71
+ now?: number
66
72
  }
67
73
 
68
74
  export interface DelegationValidateOptions {
69
75
  isRevoked?: IsRevoked
70
76
  didResolver?: DidResolver
71
77
  verifierResolver: VerifierResolver
78
+ /**
79
+ * The current time in seconds. Mostly used for testing.
80
+ * @default Math.floor(Date.now() / 1000)
81
+ */
82
+ now?: number
72
83
  }
73
84
 
74
85
  export interface DelegationFromOptions extends DelegationValidateOptions {
@@ -113,7 +124,7 @@ export interface InvocationOptions extends DelegationValidateOptions {
113
124
 
114
125
  export interface InvocationFromOptions extends DelegationValidateOptions {
115
126
  bytes: Uint8Array
116
- audience: VerifiableDID
127
+ audience?: VerifiableDID
117
128
  resolveProof: ResolveProof
118
129
  }
119
130
 
@@ -127,7 +138,7 @@ export interface InvocationFromOptions extends DelegationValidateOptions {
127
138
  export interface CapabilityOptions<
128
139
  Schema extends StandardSchemaV1,
129
140
  Cmd extends string = string,
130
- > extends DelegationValidateOptions {
141
+ > {
131
142
  schema: Schema
132
143
  cmd: Cmd
133
144
  }
@@ -147,10 +158,7 @@ export interface CapabilityDelegateOptions<Schema extends StandardSchemaV1>
147
158
  * Invoke a capability
148
159
  */
149
160
  export interface CapabilityInvokeOptions<Schema extends StandardSchemaV1>
150
- extends Omit<
151
- InvocationOptions,
152
- 'cmd' | 'args' | 'prf' | 'verifierResolver' | 'didResolver' | 'isRevoked'
153
- > {
161
+ extends Omit<InvocationOptions, 'cmd' | 'args' | 'prf'> {
154
162
  args: StandardSchemaV1.InferOutput<Schema>
155
163
  /**
156
164
  * Store to resolve proofs from
@@ -267,4 +275,5 @@ export type ClientOptions = {
267
275
  audience: DIDURLObject
268
276
  store: Store
269
277
  capabilities: Capability<StandardSchemaV1, string>[]
278
+ verifierResolver: VerifierResolver
270
279
  }
package/src/utils.js CHANGED
@@ -42,7 +42,7 @@ export function nowInSeconds() {
42
42
  export async function cid(envelope) {
43
43
  const bytes = Envelope.encode({
44
44
  signature: envelope.signature,
45
- // @ts-ignore
45
+ // @ts-expect-error
46
46
  signaturePayload: signaturePayload(envelope),
47
47
  })
48
48
  const hash = await sha256.digest(bytes)
@@ -91,9 +91,9 @@ export function isSigAndDidCompatible(did, sigType) {
91
91
  * Validate the expiration of a UCAN
92
92
  *
93
93
  * @param {number | null} exp
94
+ * @param {number} [now]
94
95
  */
95
- export function assertExpiration(exp) {
96
- const now = nowInSeconds()
96
+ export function assertExpiration(exp, now = nowInSeconds()) {
97
97
  if (exp !== null && !Number.isSafeInteger(exp)) {
98
98
  throw new TypeError(
99
99
  `UCAN expiration must be null or a safe integer. Received: ${exp}`
@@ -111,9 +111,10 @@ export function assertExpiration(exp) {
111
111
  * Validate the not before time of a UCAN
112
112
  *
113
113
  * @param {number} [nbf]
114
+ * @param {number} [now]
114
115
  */
115
- export function assertNotBefore(nbf) {
116
- if (nbf && nbf > nowInSeconds()) {
116
+ export function assertNotBefore(nbf, now = nowInSeconds()) {
117
+ if (nbf && nbf > now) {
117
118
  throw new Error('UCAN not valid yet')
118
119
  }
119
120
  }
@@ -149,20 +150,43 @@ export async function verifySignature(
149
150
  didResolver
150
151
  ) {
151
152
  const issuer = await validateIssuerAndSignature(envelope, didResolver)
152
- const isVerified = await signatureVerifierResolver.verify({
153
- signature: envelope.signature,
154
- message: dagCbor.encode(signaturePayload(envelope)),
155
- did: issuer,
156
- type: envelope.alg,
157
- })
158
-
159
- if (!isVerified) {
160
- throw new Error('UCAN signature verification failed')
153
+ try {
154
+ const isVerified = await signatureVerifierResolver.verify({
155
+ signature: envelope.signature,
156
+ message: dagCbor.encode(signaturePayload(envelope)),
157
+ did: issuer,
158
+ type: envelope.alg,
159
+ })
160
+
161
+ if (!isVerified) {
162
+ throw new Error('UCAN signature verification failed')
163
+ }
164
+ } catch (error) {
165
+ throw new Error('UCAN signature verification failed', { cause: error })
161
166
  }
162
167
 
163
168
  return true
164
169
  }
165
170
 
171
+ /**
172
+ * Check if a command is covered by (equal to or a segment-wise child of) another command.
173
+ *
174
+ * Commands are compared by path segment, not by string prefix: `/crypto` covers
175
+ * `/crypto` and `/crypto/sign` but not `/cryptocurrency`. The command `/` covers
176
+ * every command.
177
+ *
178
+ * Both commands are expected to be valid per {@link assertIsValidCommand}, so
179
+ * `${parent}/` is always a real segment boundary.
180
+ *
181
+ * @param {string} parent - The broader command, e.g. from a delegation.
182
+ * @param {string} child - The command that must be attenuated from `parent`.
183
+ * @returns {boolean}
184
+ */
185
+ export function commandCovers(parent, child) {
186
+ if (parent === '/') return child.startsWith('/')
187
+ return child === parent || child.startsWith(`${parent}/`)
188
+ }
189
+
166
190
  /**
167
191
  * Asserts that a UCAN command string is syntactically valid.
168
192
  * If the command is invalid, it throws a descriptive error.
@@ -333,7 +357,7 @@ export const selector = z.union([
333
357
  export const statement =
334
358
  /** @type {typeof z.lazy<z.ZodType<import('../src/types.js').Statement<unknown>>>} */ (
335
359
  z.lazy
336
- // @ts-ignore
360
+ // @ts-expect-error
337
361
  )(() => {
338
362
  return z.union([
339
363
  z.tuple([