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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "iso-ucan",
3
3
  "type": "module",
4
- "version": "0.4.2",
4
+ "version": "0.5.1",
5
5
  "description": "Isomorphic UCAN",
6
6
  "author": "Hugo Dias <hugomrdias@gmail.com> (hugodias.me)",
7
7
  "license": "MIT",
@@ -44,17 +44,13 @@
44
44
  "types": "./dist/src/varsig.d.ts",
45
45
  "default": "./src/varsig.js"
46
46
  },
47
- "./server": {
48
- "types": "./dist/src/server.d.ts",
49
- "default": "./src/server.js"
50
- },
51
47
  "./policy": {
52
48
  "types": "./dist/src/policy.d.ts",
53
49
  "default": "./src/policy.js"
54
50
  },
55
- "./client": {
56
- "types": "./dist/src/client.d.ts",
57
- "default": "./src/client.js"
51
+ "./rpc": {
52
+ "types": "./dist/src/rpc/index.d.ts",
53
+ "default": "./src/rpc/index.js"
58
54
  },
59
55
  "./types": {
60
56
  "types": "./dist/src/types.d.ts"
@@ -82,14 +78,11 @@
82
78
  "store": [
83
79
  "dist/src/store"
84
80
  ],
85
- "server": [
86
- "dist/src/server"
87
- ],
88
81
  "policy": [
89
82
  "dist/src/policy"
90
83
  ],
91
- "client": [
92
- "dist/src/client"
84
+ "rpc": [
85
+ "dist/src/rpc/index"
93
86
  ],
94
87
  "types": [
95
88
  "dist/src/types"
@@ -102,24 +95,24 @@
102
95
  "dist/src/**/*.d.ts.map"
103
96
  ],
104
97
  "dependencies": {
105
- "@ipld/dag-cbor": "^9.2.5",
106
- "@standard-schema/spec": "^1.0.0",
98
+ "@ipld/dag-cbor": "^10.0.2",
99
+ "@standard-schema/spec": "^1.1.0",
107
100
  "@standard-schema/utils": "^0.3.0",
108
101
  "iobuffer": "^6.0.1",
109
- "iso-kv": "^3.1.0",
110
- "iso-web": "^1.4.2",
111
- "it-merge": "^3.0.12",
112
- "multiformats": "^13.4.1",
113
- "zod": "^4.1.12",
114
- "iso-did": "^2.1.1",
115
- "iso-base": "^4.2.1",
116
- "iso-signatures": "^0.5.1"
102
+ "iso-base": "^4.4.0",
103
+ "iso-did": "^3.0.0",
104
+ "iso-kv": "^3.2.0",
105
+ "iso-signatures": "^0.5.2",
106
+ "iso-web": "^4.0.0",
107
+ "it-merge": "^3.0.15",
108
+ "multiformats": "^14.0.5",
109
+ "zod": "^4.6.5"
117
110
  },
118
111
  "devDependencies": {
119
- "@types/node": "^24.10.1",
112
+ "@types/node": "^26.6.2",
120
113
  "delay": "^7.0.0",
121
- "playwright-test": "^14.1.12",
122
- "typescript": "5.9.3"
114
+ "playwright-test": "^15.0.1",
115
+ "typescript": "^6.0.3"
123
116
  },
124
117
  "publishConfig": {
125
118
  "provenance": true
package/readme.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # iso-ucan [![NPM Version](https://img.shields.io/npm/v/iso-ucan.svg)](https://www.npmjs.com/package/iso-ucan) [![License](https://img.shields.io/npm/l/iso-ucan.svg)](https://github.com/hugomrdias/iso-repo/blob/main/license) [![iso-ucan](https://github.com/hugomrdias/iso-repo/actions/workflows/iso-ucan.yml/badge.svg)](https://github.com/hugomrdias/iso-repo/actions/workflows/iso-ucan.yml)
2
2
 
3
- > Isomorphic UCAN
3
+ > Isomorphic UCAN primitives for delegations, invocations, proof storage, and
4
+ > typed RPC.
5
+
6
+ ## Overview
7
+
8
+ UCAN is a local-first authorization model where permissions are
9
+ cryptographically signed, delegated, and verified without relying on a central
10
+ authority. See [ucan.xyz](https://ucan.xyz/) for more background.
11
+
12
+ `iso-ucan` provides:
13
+
14
+ - Typed capabilities backed by schema validation.
15
+ - Delegation and invocation builders for UCAN authorization flows.
16
+ - Proof storage with pluggable `iso-kv` drivers.
17
+ - Signer and verifier support through `iso-signatures`.
18
+ - EIP-191 wallet signing support through `iso-signatures` `EIP191Signer`.
19
+ - Filsnap-powered UCAN signature insights in MetaMask signature popups.
20
+ - A typed RPC layer for request/response APIs built on UCAN invocations.
4
21
 
5
22
  ## Install
6
23
 
@@ -43,39 +60,189 @@ const invoker = await EdDSASigner.generate()
43
60
  const nowInSeconds = Math.floor(Date.now() / 1000)
44
61
 
45
62
  const ownerDelegation = await AccountCap.delegate({
46
- iss: owner,
47
- aud: bob,
48
- sub: owner,
49
- pol: [],
50
- exp: nowInSeconds + 1000,
63
+ iss: owner,
64
+ aud: bob,
65
+ sub: owner,
66
+ pol: [],
67
+ exp: nowInSeconds + 1000,
51
68
  })
52
69
 
53
70
  await store.set(ownerDelegation)
54
71
 
55
72
  const bobDelegation = await AccountCap.delegate({
56
- iss: bob,
57
- aud: invoker,
58
- sub: owner,
59
- pol: [],
60
- exp: nowInSeconds + 1000,
73
+ iss: bob,
74
+ aud: invoker,
75
+ sub: owner,
76
+ pol: [],
77
+ exp: nowInSeconds + 1000,
61
78
  })
62
79
 
63
80
  await store.set(bobDelegation)
64
81
 
65
82
  const invocation = await AccountCreateCap.invoke({
66
- iss: invoker,
67
- sub: owner,
68
- args: {
83
+ iss: invoker,
84
+ sub: owner,
85
+ args: {
69
86
  type: 'account',
70
87
  properties: {
71
- name: 'John Doe',
88
+ name: 'John Doe',
72
89
  },
73
- },
74
- store,
75
- exp: nowInSeconds + 1000,
90
+ },
91
+ store,
92
+ exp: nowInSeconds + 1000,
76
93
  })
77
94
  ```
78
95
 
96
+ ## EIP-191 wallet signing
97
+
98
+ `iso-ucan` can use EIP-191 wallet signatures through `EIP191Signer` from
99
+ `iso-signatures`. That lets UCAN delegations and invocations be issued by an
100
+ Ethereum account exposed through an EIP-1193 provider, such as an injected
101
+ MetaMask wallet, while verification is handled by the matching EIP-191 verifier
102
+ from `iso-signatures`.
103
+
104
+ [`examples/eip191`](../../examples/eip191) shows this flow in a browser web
105
+ app. It uses `wagmi` to connect the wallet, wraps the provider in
106
+ `EIP191Signer`, then creates and verifies UCAN delegations and invocations
107
+ directly in the browser.
108
+
109
+ The example uses `filsnap-adapter` to install or enable
110
+ [Filsnap](https://github.com/filecoin-project/filsnap), the Filecoin Wallet
111
+ MetaMask Snap. Filsnap supports UCAN signature insights for EIP-191 signers, so
112
+ when the app asks MetaMask to sign a UCAN delegation, the MetaMask signature
113
+ popup can show a readable summary of the issuer, audience, subject, command,
114
+ expiration, and raw payload.
115
+
116
+ ## RPC
117
+
118
+ `iso-ucan/rpc` is a small request/response layer on top of UCAN
119
+ invocations. You declare a protocol once as a record of commands
120
+ (`defineCommand`), then build matching client and server pairs from it
121
+ (`defineClient` / `defineServer`). Every command's success and error shapes
122
+ are described by a single receipt schema (`receipt`, `receiptResult`,
123
+ `receiptError`) which is shared by both ends, so the call sites are fully
124
+ type-safe and discriminated unions just work on the client.
125
+
126
+ Internal server failures (invalid invocation, unknown command, handler
127
+ threw, …) are surfaced as a generic `SERVER_ERROR` variant that
128
+ `receipt(...)` adds implicitly, so clients can always handle them with one
129
+ narrow.
130
+
131
+ ### Define a shared protocol
132
+
133
+ ```ts
134
+ import {
135
+ defineCommand,
136
+ receipt,
137
+ receiptError,
138
+ receiptResult,
139
+ } from 'iso-ucan/rpc'
140
+ import { z } from 'zod'
141
+
142
+ const TodoSchema = z.object({
143
+ id: z.string(),
144
+ text: z.string(),
145
+ done: z.boolean(),
146
+ })
147
+
148
+ export const Protocol = {
149
+ TodoList: defineCommand({
150
+ cmd: '/todo/list',
151
+ args: z.object({}),
152
+ receipt: receipt(receiptResult(z.object({ todos: z.array(TodoSchema) }))),
153
+ }),
154
+ TodoAdd: defineCommand({
155
+ cmd: '/todo/add',
156
+ args: z.object({ text: z.string().min(1) }),
157
+ receipt: receipt(receiptResult(z.object({ todo: TodoSchema }))),
158
+ }),
159
+ TodoComplete: defineCommand({
160
+ cmd: '/todo/complete',
161
+ args: z.object({ id: z.string() }),
162
+ receipt: receipt(
163
+ receiptResult(z.object({ todo: TodoSchema })),
164
+ receiptError('NOT_FOUND', 'Todo not found.', z.object({ id: z.string() }))
165
+ ),
166
+ }),
167
+ } as const
168
+ ```
169
+
170
+ ### Server (e.g. Hono + `@hono/node-server`)
171
+
172
+ ```ts
173
+ import { serve } from '@hono/node-server'
174
+ import { Hono } from 'hono'
175
+ import { MemoryDriver } from 'iso-kv/drivers/memory.js'
176
+ import { defineServer, type ServerHandlers } from 'iso-ucan/rpc'
177
+ import { Store } from 'iso-ucan/store'
178
+ import { Protocol } from './protocol.ts'
179
+
180
+ const store = new Store(new MemoryDriver())
181
+ // ...seed `store` with the delegations that authorise your clients...
182
+
183
+ const handlers: ServerHandlers<typeof Protocol> = {
184
+ '/todo/list': ({ invocation }) => ({
185
+ cid: invocation.cid,
186
+ result: { todos: [/* ... */] },
187
+ }),
188
+ '/todo/add': ({ args, invocation }) => ({
189
+ cid: invocation.cid,
190
+ result: { todo: { id: '1', text: args.text, done: false } },
191
+ }),
192
+ '/todo/complete': ({ args, invocation }) => {
193
+ // return either { cid, result: ... } or { cid, error: { code, message, data } }
194
+ return {
195
+ cid: invocation.cid,
196
+ error: {
197
+ code: 'NOT_FOUND',
198
+ message: 'Todo not found.',
199
+ data: { id: args.id },
200
+ },
201
+ }
202
+ },
203
+ }
204
+
205
+ const rpc = defineServer(Protocol, {
206
+ signer: serverSigner,
207
+ store,
208
+ verifierResolver,
209
+ handlers,
210
+ })
211
+
212
+ const app = new Hono()
213
+ app.post('/rpc', (c) => rpc(c.req.raw))
214
+ serve({ fetch: app.fetch, port: 3000 })
215
+ ```
216
+
217
+ ### Client
218
+
219
+ ```ts
220
+ import { defineClient } from 'iso-ucan/rpc'
221
+
222
+ const client = defineClient(Protocol, {
223
+ url: 'http://localhost:3000/rpc',
224
+ issuer: cliSigner,
225
+ audience: serverSigner.didObject,
226
+ store,
227
+ verifierResolver,
228
+ })
229
+
230
+ // `args` and the returned receipt are typed from the protocol entry for `cmd`.
231
+ const r = await client.request({ cmd: '/todo/complete', args: { id: '42' } })
232
+
233
+ if ('result' in r) {
234
+ console.log(r.result.todo)
235
+ } else if (r.error.code === 'NOT_FOUND') {
236
+ console.error(r.error.message, r.error.data) // data: { id: string }
237
+ } else {
238
+ // r.error.code === 'SERVER_ERROR' — always present in every receipt union.
239
+ console.error('server error:', r.error.message)
240
+ }
241
+ ```
242
+
243
+ A complete runnable example (Hono server + a small CLI client) lives in
244
+ [`examples/rpc-todo`](../../examples/rpc-todo).
245
+
79
246
  ## License
80
247
 
81
248
  MIT © [Hugo Dias](http://hugodias.me)
package/src/capability.js CHANGED
@@ -19,9 +19,6 @@ export class Capability {
19
19
  constructor(options) {
20
20
  this.schema = options.schema
21
21
  this.cmd = options.cmd
22
- this.isRevoked = options.isRevoked
23
- this.didResolver = options.didResolver
24
- this.verifierResolver = options.verifierResolver
25
22
  }
26
23
 
27
24
  /**
@@ -64,14 +61,12 @@ export class Capability {
64
61
  args: result.value,
65
62
  })
66
63
  }
64
+
67
65
  return await Invocation.create({
68
66
  ...options,
69
67
  args: result.value,
70
68
  cmd: this.cmd,
71
69
  prf: proofs,
72
- verifierResolver: this.verifierResolver,
73
- didResolver: this.didResolver,
74
- isRevoked: this.isRevoked,
75
70
  })
76
71
  }
77
72
 
package/src/delegation.js CHANGED
@@ -1,4 +1,4 @@
1
- import { base64pad } from 'iso-base/rfc4648'
1
+ import { base64 } from 'iso-base/rfc4648'
2
2
  import { parse as didParse } from 'iso-did'
3
3
  import { randomBytes } from 'iso-web/crypto'
4
4
  import * as Envelope from './envelope.js'
@@ -84,7 +84,7 @@ export class Delegation {
84
84
  )
85
85
  }
86
86
 
87
- assertStructure(envelope.payload)
87
+ assertStructure(envelope.payload, options.now)
88
88
 
89
89
  await verifySignature(envelope, verifierResolver, didResolveOptions)
90
90
 
@@ -98,9 +98,10 @@ export class Delegation {
98
98
  * Create a delegation from a base64 encoded string
99
99
  *
100
100
  * @param {string} token
101
+ * @param { {now?: number}} [options]
101
102
  */
102
- static async fromString(token) {
103
- const bytes = base64pad.decode(token)
103
+ static async fromString(token, options = {}) {
104
+ const bytes = base64.decode(token)
104
105
  const envelope = /** @type {typeof Envelope.decode<"dlg">} */ (
105
106
  Envelope.decode
106
107
  )({ envelope: bytes })
@@ -111,7 +112,7 @@ export class Delegation {
111
112
  )
112
113
  }
113
114
 
114
- assertStructure(envelope.payload)
115
+ assertStructure(envelope.payload, options.now)
115
116
  const _cid = await cid(envelope)
116
117
  return new Delegation(envelope, bytes, _cid)
117
118
  }
@@ -143,7 +144,7 @@ export class Delegation {
143
144
  payload.meta = options.meta
144
145
  }
145
146
 
146
- assertStructure(payload)
147
+ assertStructure(payload, options.now)
147
148
 
148
149
  const { signature, signaturePayload } = await Envelope.sign({
149
150
  spec: 'dlg',
@@ -174,8 +175,8 @@ export class Delegation {
174
175
  * @param {DelegationValidateOptions} options
175
176
  */
176
177
  async validate(options) {
177
- assertStructure(this.envelope.payload)
178
- assertNotBefore(this.envelope.payload.nbf)
178
+ assertStructure(this.envelope.payload, options.now)
179
+ assertNotBefore(this.envelope.payload.nbf, options.now)
179
180
  await verifySignature(
180
181
  this.envelope,
181
182
  options.verifierResolver,
@@ -187,7 +188,7 @@ export class Delegation {
187
188
  }
188
189
 
189
190
  toString() {
190
- return base64pad.encode(this.bytes)
191
+ return base64.encode(this.bytes)
191
192
  }
192
193
 
193
194
  toJSON() {
@@ -203,10 +204,10 @@ export class Delegation {
203
204
  pol: this.pol,
204
205
  exp: this.exp,
205
206
  nbf: this.nbf,
206
- nonce: base64pad.encode(this.nonce),
207
+ nonce: base64.encode(this.nonce),
207
208
  meta: this.meta,
208
209
  },
209
- signature: base64pad.encode(this.envelope.signature),
210
+ signature: base64.encode(this.envelope.signature),
210
211
  alg: this.envelope.alg,
211
212
  enc: this.envelope.enc,
212
213
  spec: this.envelope.spec,
@@ -219,8 +220,9 @@ export class Delegation {
219
220
  /**
220
221
  *
221
222
  * @param {import('./types.js').DelegationPayload} payload
223
+ * @param {number} [now]
222
224
  */
223
- function assertStructure(payload) {
225
+ function assertStructure(payload, now) {
224
226
  didParse(payload.iss)
225
227
  didParse(payload.aud)
226
228
 
@@ -229,7 +231,7 @@ function assertStructure(payload) {
229
231
  }
230
232
 
231
233
  assertIsValidCommand(payload.cmd)
232
- assertExpiration(payload.exp)
234
+ assertExpiration(payload.exp, now)
233
235
  assertNonce(payload.nonce)
234
236
 
235
237
  // assertPolicy(payload.pol)
package/src/invocation.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  assertMeta,
10
10
  assertNonce,
11
11
  cid,
12
+ commandCovers,
12
13
  expOrTtl,
13
14
  verifySignature,
14
15
  } from './utils.js'
@@ -65,6 +66,7 @@ export class Invocation {
65
66
  }
66
67
  // Audience or subject must match receiver
67
68
  if (
69
+ audience &&
68
70
  envelope.payload.aud !== audience.did &&
69
71
  envelope.payload.sub !== audience.did
70
72
  ) {
@@ -192,14 +194,15 @@ export function assertProofs(payload, proofs) {
192
194
  throw new Error("UCAN Invocation couldn't resolve all proofs CIDs")
193
195
  }
194
196
 
195
- const lastProof = proofs[proofs.length - 1]
196
- const issuerDid = didParse(lastProof.envelope.payload.iss).did
197
- if (issuerDid !== lastProof.envelope.payload.sub) {
197
+ const rootProof = proofs[0]
198
+ const issuerDid = didParse(rootProof.iss).did
199
+ if (issuerDid !== rootProof.sub) {
198
200
  throw new Error('UCAN Invocation root proof is not self-signed')
199
201
  }
200
202
 
201
203
  for (let index = 0; index < proofs.length; index++) {
202
204
  const current = proofs[index].envelope.payload
205
+
203
206
  const currentAudience = didParse(current.aud)
204
207
  const currentSubject = didParse(current.sub ?? payload.sub)
205
208
 
@@ -210,12 +213,13 @@ export function assertProofs(payload, proofs) {
210
213
  }
211
214
 
212
215
  const nextProof = proofs[index + 1]
216
+
213
217
  /** @type {import("./types.js").Payload} */
214
218
  let next
215
219
  if (nextProof) {
216
220
  next = nextProof.envelope.payload
217
221
  } else {
218
- next = payload
222
+ next = payload // checks against the invocation payload
219
223
  }
220
224
 
221
225
  const nextIssuer = didParse(next.iss)
@@ -232,9 +236,9 @@ export function assertProofs(payload, proofs) {
232
236
  )
233
237
  }
234
238
 
235
- if (!next.cmd.startsWith(current.cmd)) {
239
+ if (!commandCovers(current.cmd, next.cmd)) {
236
240
  throw new Error(
237
- `UCAN Invocation command mismatch, expected ${current.cmd} to be a broader than ${next.cmd}`
241
+ `UCAN Invocation command mismatch, expected ${current.cmd} to cover ${next.cmd}`
238
242
  )
239
243
  }
240
244
  }
@@ -0,0 +1,85 @@
1
+ import { SchemaError } from '@standard-schema/utils'
2
+
3
+ /**
4
+ * @import {
5
+ * CommandsRecord,
6
+ * DefineClientOptions,
7
+ * CommandClient,
8
+ * } from './types.js'
9
+ */
10
+
11
+ /**
12
+ * Define a type-safe client from a record of commands.
13
+ *
14
+ * The returned client exposes a `request(cmd, args)` method whose `cmd`
15
+ * parameter is constrained to the union of `cmd` values declared by
16
+ * `commands`, and whose `args` parameter is typed accordingly.
17
+ *
18
+ * @template {CommandsRecord} Commands
19
+ * @param {Commands} commands
20
+ * @param {DefineClientOptions} options
21
+ * @returns {CommandClient<Commands>}
22
+ */
23
+ export function defineClient(commands, options) {
24
+ /** @type {Record<string, CommandsRecord[string]>} */
25
+ const commandMap = {}
26
+ for (const command of Object.values(commands)) {
27
+ commandMap[command.capability.cmd] = command
28
+ }
29
+
30
+ return {
31
+ request: async ({ cmd, args }) => {
32
+ const command = commandMap[cmd]
33
+ if (!command) {
34
+ throw new Error(`Command not found: ${cmd}`)
35
+ }
36
+
37
+ const inv = await command.capability.invoke({
38
+ args,
39
+ iss: options.issuer,
40
+ sub: options.audience.did,
41
+ store: options.store,
42
+ exp: null,
43
+ verifierResolver: options.verifierResolver,
44
+ })
45
+
46
+ const response = await fetch(options.url, {
47
+ method: 'POST',
48
+ body: /** @type {BufferSource} */ (inv.bytes),
49
+ })
50
+
51
+ const data = await response.json()
52
+ if (
53
+ typeof data !== 'object' ||
54
+ data === null ||
55
+ (!('result' in data) && !('error' in data))
56
+ ) {
57
+ throw new Error(
58
+ 'Invalid receipt: response must include a `result` or `error` field'
59
+ )
60
+ }
61
+
62
+ const validated = await command.receipt['~standard'].validate(data)
63
+ if (validated.issues) {
64
+ throw new SchemaError(validated.issues)
65
+ }
66
+ return validated.value
67
+ },
68
+
69
+ invoke: async ({ cmd, args }) => {
70
+ const command = commandMap[cmd]
71
+ if (!command) {
72
+ throw new Error(`Command not found: ${cmd}`)
73
+ }
74
+
75
+ return await command.capability.invoke({
76
+ args,
77
+ iss: options.issuer,
78
+ sub: options.audience.did,
79
+ store: options.store,
80
+ exp: null,
81
+ verifierResolver: options.verifierResolver,
82
+ })
83
+ },
84
+ }
85
+ }
@@ -0,0 +1,28 @@
1
+ import { Capability } from '../capability.js'
2
+
3
+ /**
4
+ * @import {
5
+ * CommandOptions,
6
+ * CommandOutput,
7
+ * } from './types.js'
8
+ * @import {StandardSchemaV1} from '@standard-schema/spec'
9
+ */
10
+
11
+ /**
12
+ * @template {StandardSchemaV1} Args
13
+ * @template {string} Cmd
14
+ * @template {StandardSchemaV1} ReceiptSchema
15
+ * @param {CommandOptions<Args, Cmd, ReceiptSchema>} options
16
+ * @returns {CommandOutput<Args, Cmd, ReceiptSchema>}
17
+ */
18
+ export function defineCommand(options) {
19
+ return {
20
+ capability: /** @type {Capability<Args, Cmd>} */ (
21
+ Capability.from({
22
+ schema: options.args,
23
+ cmd: options.cmd,
24
+ })
25
+ ),
26
+ receipt: options.receipt,
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ export { defineClient } from './client.js'
2
+ export { defineCommand } from './command.js'
3
+ export {
4
+ cidSchema,
5
+ receipt,
6
+ receiptError,
7
+ receiptResult,
8
+ receiptServerError,
9
+ } from './receipt.js'
10
+ export { defineServer } from './server.js'