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.
- package/dist/src/capability.d.ts +0 -3
- package/dist/src/capability.d.ts.map +1 -1
- package/dist/src/delegation.d.ts +4 -1
- package/dist/src/delegation.d.ts.map +1 -1
- package/dist/src/envelope.d.ts +1 -1
- package/dist/src/envelope.d.ts.map +1 -1
- package/dist/src/invocation.d.ts.map +1 -1
- package/dist/src/rpc/client.d.ts +24 -0
- package/dist/src/rpc/client.d.ts.map +1 -0
- package/dist/src/rpc/command.d.ts +19 -0
- package/dist/src/rpc/command.d.ts.map +1 -0
- package/dist/src/rpc/index.d.ts +5 -0
- package/dist/src/rpc/index.d.ts.map +1 -0
- package/dist/src/rpc/receipt.d.ts +114 -0
- package/dist/src/rpc/receipt.d.ts.map +1 -0
- package/dist/src/rpc/server.d.ts +18 -0
- package/dist/src/rpc/server.d.ts.map +1 -0
- package/dist/src/rpc/types.d.ts +112 -0
- package/dist/src/rpc/types.d.ts.map +1 -0
- package/dist/src/store.d.ts +4 -2
- package/dist/src/store.d.ts.map +1 -1
- package/dist/src/types.d.ts +15 -3
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +19 -2
- package/dist/src/utils.d.ts.map +1 -1
- package/license +9 -0
- package/package.json +19 -26
- package/readme.md +185 -18
- package/src/capability.js +1 -6
- package/src/delegation.js +15 -13
- package/src/invocation.js +10 -6
- package/src/rpc/client.js +85 -0
- package/src/rpc/command.js +28 -0
- package/src/rpc/index.js +10 -0
- package/src/rpc/receipt.js +136 -0
- package/src/rpc/server.js +155 -0
- package/src/rpc/types.js +9 -0
- package/src/rpc/types.ts +153 -0
- package/src/store.js +41 -16
- package/src/types.ts +15 -6
- package/src/utils.js +39 -15
- package/dist/src/client.d.ts +0 -41
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/server.d.ts +0 -38
- package/dist/src/server.d.ts.map +0 -1
- package/src/client.js +0 -52
- 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
|
+
"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
|
-
"./
|
|
56
|
-
"types": "./dist/src/
|
|
57
|
-
"default": "./src/
|
|
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
|
-
"
|
|
92
|
-
"dist/src/
|
|
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": "^
|
|
106
|
-
"@standard-schema/spec": "^1.
|
|
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-
|
|
110
|
-
"iso-
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
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": "^
|
|
112
|
+
"@types/node": "^26.6.2",
|
|
120
113
|
"delay": "^7.0.0",
|
|
121
|
-
"playwright-test": "^
|
|
122
|
-
"typescript": "
|
|
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 [](https://www.npmjs.com/package/iso-ucan) [](https://github.com/hugomrdias/iso-repo/blob/main/license) [](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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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:
|
|
207
|
+
nonce: base64.encode(this.nonce),
|
|
207
208
|
meta: this.meta,
|
|
208
209
|
},
|
|
209
|
-
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
|
|
196
|
-
const issuerDid = didParse(
|
|
197
|
-
if (issuerDid !==
|
|
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 (!
|
|
239
|
+
if (!commandCovers(current.cmd, next.cmd)) {
|
|
236
240
|
throw new Error(
|
|
237
|
-
`UCAN Invocation command mismatch, expected ${current.cmd} to
|
|
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
|
+
}
|
package/src/rpc/index.js
ADDED