@ixo/ucan 1.0.0 → 1.1.0
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/.turbo/turbo-build.log +1 -1
- package/README.md +215 -117
- package/dist/capabilities/capability.d.ts +2 -2
- package/dist/capabilities/capability.d.ts.map +1 -1
- package/dist/capabilities/capability.js.map +1 -1
- package/dist/client/create-client.d.ts +1 -0
- package/dist/client/create-client.d.ts.map +1 -1
- package/dist/client/create-client.js +6 -3
- package/dist/client/create-client.js.map +1 -1
- package/dist/did/ixo-resolver.d.ts.map +1 -1
- package/dist/did/ixo-resolver.js.map +1 -1
- package/dist/store/memory.d.ts.map +1 -1
- package/dist/store/memory.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/validator/validator.d.ts +3 -1
- package/dist/validator/validator.d.ts.map +1 -1
- package/dist/validator/validator.js +25 -0
- package/dist/validator/validator.js.map +1 -1
- package/docs/FLOW.md +287 -0
- package/docs/examples/CLIENT.md +418 -0
- package/docs/examples/SERVER.md +419 -0
- package/package.json +6 -8
- package/scripts/test-ucan.ts +31 -19
- package/src/capabilities/capability.ts +8 -7
- package/src/client/create-client.ts +29 -11
- package/src/did/ixo-resolver.ts +4 -6
- package/src/did/utils.ts +0 -1
- package/src/store/memory.ts +4 -2
- package/src/validator/validator.test.ts +611 -0
- package/src/validator/validator.ts +67 -7
- package/tsconfig.json +1 -1
- package/vitest.config.ts +2 -0
- package/.eslintrc.js +0 -9
- package/.prettierignore +0 -3
- package/.prettierrc.js +0 -4
- package/CHANGELOG.md +0 -0
- package/jest.config.js +0 -3
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import * as Client from '@ucanto/client';
|
|
3
|
+
import { ed25519 } from '@ucanto/principal';
|
|
4
|
+
import { createUCANValidator } from './validator.js';
|
|
5
|
+
import { defineCapability, Schema } from '../capabilities/capability.js';
|
|
6
|
+
import {
|
|
7
|
+
createDelegation,
|
|
8
|
+
createInvocation,
|
|
9
|
+
serializeInvocation,
|
|
10
|
+
type Capability,
|
|
11
|
+
} from '../client/create-client.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper: generate an ed25519 keypair
|
|
15
|
+
*/
|
|
16
|
+
async function keygen() {
|
|
17
|
+
const signer = await ed25519.Signer.generate();
|
|
18
|
+
return { signer, did: signer.did() };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Simple capability without caveats
|
|
23
|
+
*/
|
|
24
|
+
const TestRead = defineCapability({
|
|
25
|
+
can: 'test/read',
|
|
26
|
+
protocol: 'ixo:',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Capability with limit caveat
|
|
31
|
+
*/
|
|
32
|
+
const EmployeesRead = defineCapability({
|
|
33
|
+
can: 'employees/read',
|
|
34
|
+
protocol: 'myapp:',
|
|
35
|
+
nb: { limit: Schema.integer().optional() },
|
|
36
|
+
derives: (claimed, delegated) => {
|
|
37
|
+
const claimedLimit = claimed.nb?.limit ?? Infinity;
|
|
38
|
+
const delegatedLimit = delegated.nb?.limit ?? Infinity;
|
|
39
|
+
if (claimedLimit > delegatedLimit) {
|
|
40
|
+
return {
|
|
41
|
+
error: new Error(
|
|
42
|
+
`Cannot request limit=${claimedLimit}, delegation only allows limit=${delegatedLimit}`,
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ok: {} };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('UCAN Validator', () => {
|
|
51
|
+
describe('proofChain', () => {
|
|
52
|
+
it('should return single-element chain for direct root invocation', async () => {
|
|
53
|
+
const server = await keygen();
|
|
54
|
+
const root = await keygen();
|
|
55
|
+
|
|
56
|
+
const validator = await createUCANValidator({
|
|
57
|
+
serverDid: server.did,
|
|
58
|
+
rootIssuers: [root.did],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const invocation = Client.invoke({
|
|
62
|
+
issuer: root.signer,
|
|
63
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
64
|
+
capability: {
|
|
65
|
+
can: 'test/read' as const,
|
|
66
|
+
with: `ixo:resource:123` as const,
|
|
67
|
+
},
|
|
68
|
+
proofs: [],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const serialized = await serializeInvocation(invocation);
|
|
72
|
+
const result = await validator.validate(
|
|
73
|
+
serialized,
|
|
74
|
+
TestRead,
|
|
75
|
+
'ixo:resource:123',
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(result.ok).toBe(true);
|
|
79
|
+
expect(result.proofChain).toEqual([root.did]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return two-element chain for root -> user delegation', async () => {
|
|
83
|
+
const server = await keygen();
|
|
84
|
+
const root = await keygen();
|
|
85
|
+
const user = await keygen();
|
|
86
|
+
|
|
87
|
+
const validator = await createUCANValidator({
|
|
88
|
+
serverDid: server.did,
|
|
89
|
+
rootIssuers: [root.did],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const delegation = await Client.delegate({
|
|
93
|
+
issuer: root.signer,
|
|
94
|
+
audience: user.signer,
|
|
95
|
+
capabilities: [
|
|
96
|
+
{
|
|
97
|
+
can: 'test/read' as const,
|
|
98
|
+
with: 'ixo:resource:123' as const,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const invocation = Client.invoke({
|
|
104
|
+
issuer: user.signer,
|
|
105
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
106
|
+
capability: {
|
|
107
|
+
can: 'test/read' as const,
|
|
108
|
+
with: 'ixo:resource:123' as const,
|
|
109
|
+
},
|
|
110
|
+
proofs: [delegation],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const serialized = await serializeInvocation(invocation);
|
|
114
|
+
const result = await validator.validate(
|
|
115
|
+
serialized,
|
|
116
|
+
TestRead,
|
|
117
|
+
'ixo:resource:123',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(result.ok).toBe(true);
|
|
121
|
+
expect(result.proofChain).toEqual([root.did, user.did]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return three-element chain for root -> alice -> bob', async () => {
|
|
125
|
+
const server = await keygen();
|
|
126
|
+
const root = await keygen();
|
|
127
|
+
const alice = await keygen();
|
|
128
|
+
const bob = await keygen();
|
|
129
|
+
|
|
130
|
+
const validator = await createUCANValidator({
|
|
131
|
+
serverDid: server.did,
|
|
132
|
+
rootIssuers: [root.did],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const rootToAlice = await Client.delegate({
|
|
136
|
+
issuer: root.signer,
|
|
137
|
+
audience: alice.signer,
|
|
138
|
+
capabilities: [
|
|
139
|
+
{
|
|
140
|
+
can: 'test/read' as const,
|
|
141
|
+
with: 'ixo:resource:123' as const,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const aliceToBob = await Client.delegate({
|
|
147
|
+
issuer: alice.signer,
|
|
148
|
+
audience: bob.signer,
|
|
149
|
+
capabilities: [
|
|
150
|
+
{
|
|
151
|
+
can: 'test/read' as const,
|
|
152
|
+
with: 'ixo:resource:123' as const,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
proofs: [rootToAlice],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const invocation = Client.invoke({
|
|
159
|
+
issuer: bob.signer,
|
|
160
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
161
|
+
capability: {
|
|
162
|
+
can: 'test/read' as const,
|
|
163
|
+
with: 'ixo:resource:123' as const,
|
|
164
|
+
},
|
|
165
|
+
proofs: [aliceToBob],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const serialized = await serializeInvocation(invocation);
|
|
169
|
+
const result = await validator.validate(
|
|
170
|
+
serialized,
|
|
171
|
+
TestRead,
|
|
172
|
+
'ixo:resource:123',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(result.ok).toBe(true);
|
|
176
|
+
expect(result.proofChain).toEqual([root.did, alice.did, bob.did]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('expiration', () => {
|
|
181
|
+
it('should return undefined expiration when no expiration is set (Infinity)', async () => {
|
|
182
|
+
const server = await keygen();
|
|
183
|
+
const root = await keygen();
|
|
184
|
+
const user = await keygen();
|
|
185
|
+
|
|
186
|
+
const validator = await createUCANValidator({
|
|
187
|
+
serverDid: server.did,
|
|
188
|
+
rootIssuers: [root.did],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Using createDelegation/createInvocation which default to Infinity
|
|
192
|
+
const delegation = await createDelegation({
|
|
193
|
+
issuer: root.signer,
|
|
194
|
+
audience: user.did,
|
|
195
|
+
capabilities: [
|
|
196
|
+
{
|
|
197
|
+
can: 'test/read' as Capability['can'],
|
|
198
|
+
with: 'ixo:resource:123' as Capability['with'],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const invocation = await createInvocation({
|
|
204
|
+
issuer: user.signer,
|
|
205
|
+
audience: server.did,
|
|
206
|
+
capability: {
|
|
207
|
+
can: 'test/read' as Capability['can'],
|
|
208
|
+
with: 'ixo:resource:123' as Capability['with'],
|
|
209
|
+
},
|
|
210
|
+
proofs: [delegation],
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const serialized = await serializeInvocation(invocation);
|
|
214
|
+
const result = await validator.validate(
|
|
215
|
+
serialized,
|
|
216
|
+
TestRead,
|
|
217
|
+
'ixo:resource:123',
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(result.ok).toBe(true);
|
|
221
|
+
// No expiration set → defaults to Infinity → filtered out
|
|
222
|
+
expect(result.expiration).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return delegation expiration when set', async () => {
|
|
226
|
+
const server = await keygen();
|
|
227
|
+
const root = await keygen();
|
|
228
|
+
const user = await keygen();
|
|
229
|
+
|
|
230
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
|
|
231
|
+
|
|
232
|
+
const validator = await createUCANValidator({
|
|
233
|
+
serverDid: server.did,
|
|
234
|
+
rootIssuers: [root.did],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const delegation = await Client.delegate({
|
|
238
|
+
issuer: root.signer,
|
|
239
|
+
audience: user.signer,
|
|
240
|
+
capabilities: [
|
|
241
|
+
{
|
|
242
|
+
can: 'test/read' as const,
|
|
243
|
+
with: 'ixo:resource:123' as const,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
expiration: futureExp,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const invocation = Client.invoke({
|
|
250
|
+
issuer: user.signer,
|
|
251
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
252
|
+
capability: {
|
|
253
|
+
can: 'test/read' as const,
|
|
254
|
+
with: 'ixo:resource:123' as const,
|
|
255
|
+
},
|
|
256
|
+
proofs: [delegation],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const serialized = await serializeInvocation(invocation);
|
|
260
|
+
const result = await validator.validate(
|
|
261
|
+
serialized,
|
|
262
|
+
TestRead,
|
|
263
|
+
'ixo:resource:123',
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(result.ok).toBe(true);
|
|
267
|
+
expect(result.expiration).toBeDefined();
|
|
268
|
+
expect(result.expiration).toBeLessThanOrEqual(futureExp);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should return earliest expiration across the chain', async () => {
|
|
272
|
+
const server = await keygen();
|
|
273
|
+
const root = await keygen();
|
|
274
|
+
const alice = await keygen();
|
|
275
|
+
const bob = await keygen();
|
|
276
|
+
|
|
277
|
+
const laterExp = Math.floor(Date.now() / 1000) + 7200; // 2 hours
|
|
278
|
+
const earlierExp = Math.floor(Date.now() / 1000) + 3600; // 1 hour
|
|
279
|
+
|
|
280
|
+
const validator = await createUCANValidator({
|
|
281
|
+
serverDid: server.did,
|
|
282
|
+
rootIssuers: [root.did],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Root -> Alice with later expiration
|
|
286
|
+
const rootToAlice = await Client.delegate({
|
|
287
|
+
issuer: root.signer,
|
|
288
|
+
audience: alice.signer,
|
|
289
|
+
capabilities: [
|
|
290
|
+
{
|
|
291
|
+
can: 'test/read' as const,
|
|
292
|
+
with: 'ixo:resource:123' as const,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
expiration: laterExp,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Alice -> Bob with earlier expiration
|
|
299
|
+
const aliceToBob = await Client.delegate({
|
|
300
|
+
issuer: alice.signer,
|
|
301
|
+
audience: bob.signer,
|
|
302
|
+
capabilities: [
|
|
303
|
+
{
|
|
304
|
+
can: 'test/read' as const,
|
|
305
|
+
with: 'ixo:resource:123' as const,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
expiration: earlierExp,
|
|
309
|
+
proofs: [rootToAlice],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const invocation = Client.invoke({
|
|
313
|
+
issuer: bob.signer,
|
|
314
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
315
|
+
capability: {
|
|
316
|
+
can: 'test/read' as const,
|
|
317
|
+
with: 'ixo:resource:123' as const,
|
|
318
|
+
},
|
|
319
|
+
proofs: [aliceToBob],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const serialized = await serializeInvocation(invocation);
|
|
323
|
+
const result = await validator.validate(
|
|
324
|
+
serialized,
|
|
325
|
+
TestRead,
|
|
326
|
+
'ixo:resource:123',
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(result.ok).toBe(true);
|
|
330
|
+
expect(result.expiration).toBeDefined();
|
|
331
|
+
// Should be the earlier expiration (alice->bob's 1 hour, not root->alice's 2 hours)
|
|
332
|
+
expect(result.expiration).toBeLessThanOrEqual(earlierExp);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('validation failures', () => {
|
|
337
|
+
it('should reject malformed base64 input', async () => {
|
|
338
|
+
const server = await keygen();
|
|
339
|
+
const root = await keygen();
|
|
340
|
+
|
|
341
|
+
const validator = await createUCANValidator({
|
|
342
|
+
serverDid: server.did,
|
|
343
|
+
rootIssuers: [root.did],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const result = await validator.validate(
|
|
347
|
+
'not-valid-base64!!!',
|
|
348
|
+
TestRead,
|
|
349
|
+
'ixo:resource:123',
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
expect(result.ok).toBe(false);
|
|
353
|
+
expect(result.error?.code).toBe('INVALID_FORMAT');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should reject invocation with wrong audience', async () => {
|
|
357
|
+
const server = await keygen();
|
|
358
|
+
const wrongServer = await keygen();
|
|
359
|
+
const root = await keygen();
|
|
360
|
+
|
|
361
|
+
const validator = await createUCANValidator({
|
|
362
|
+
serverDid: server.did,
|
|
363
|
+
rootIssuers: [root.did],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Invocation addressed to wrong server
|
|
367
|
+
const invocation = Client.invoke({
|
|
368
|
+
issuer: root.signer,
|
|
369
|
+
audience: ed25519.Verifier.parse(wrongServer.did),
|
|
370
|
+
capability: {
|
|
371
|
+
can: 'test/read' as const,
|
|
372
|
+
with: 'ixo:resource:123' as const,
|
|
373
|
+
},
|
|
374
|
+
proofs: [],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const serialized = await serializeInvocation(invocation);
|
|
378
|
+
const result = await validator.validate(
|
|
379
|
+
serialized,
|
|
380
|
+
TestRead,
|
|
381
|
+
'ixo:resource:123',
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
expect(result.ok).toBe(false);
|
|
385
|
+
expect(result.error?.code).toBe('UNAUTHORIZED');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should reject invocation with untrusted root', async () => {
|
|
389
|
+
const server = await keygen();
|
|
390
|
+
const trustedRoot = await keygen();
|
|
391
|
+
const untrustedRoot = await keygen();
|
|
392
|
+
const user = await keygen();
|
|
393
|
+
|
|
394
|
+
const validator = await createUCANValidator({
|
|
395
|
+
serverDid: server.did,
|
|
396
|
+
rootIssuers: [trustedRoot.did], // Only trustedRoot is trusted
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Delegation from untrusted root
|
|
400
|
+
const delegation = await Client.delegate({
|
|
401
|
+
issuer: untrustedRoot.signer,
|
|
402
|
+
audience: user.signer,
|
|
403
|
+
capabilities: [
|
|
404
|
+
{
|
|
405
|
+
can: 'test/read' as const,
|
|
406
|
+
with: 'ixo:resource:123' as const,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const invocation = Client.invoke({
|
|
412
|
+
issuer: user.signer,
|
|
413
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
414
|
+
capability: {
|
|
415
|
+
can: 'test/read' as const,
|
|
416
|
+
with: 'ixo:resource:123' as const,
|
|
417
|
+
},
|
|
418
|
+
proofs: [delegation],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const serialized = await serializeInvocation(invocation);
|
|
422
|
+
const result = await validator.validate(
|
|
423
|
+
serialized,
|
|
424
|
+
TestRead,
|
|
425
|
+
'ixo:resource:123',
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(result.ok).toBe(false);
|
|
429
|
+
expect(result.error?.code).toBe('UNAUTHORIZED');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should reject invocation with mismatched resource', async () => {
|
|
433
|
+
const server = await keygen();
|
|
434
|
+
const root = await keygen();
|
|
435
|
+
const user = await keygen();
|
|
436
|
+
|
|
437
|
+
const validator = await createUCANValidator({
|
|
438
|
+
serverDid: server.did,
|
|
439
|
+
rootIssuers: [root.did],
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const delegation = await Client.delegate({
|
|
443
|
+
issuer: root.signer,
|
|
444
|
+
audience: user.signer,
|
|
445
|
+
capabilities: [
|
|
446
|
+
{
|
|
447
|
+
can: 'test/read' as const,
|
|
448
|
+
with: 'ixo:resource:123' as const,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const invocation = Client.invoke({
|
|
454
|
+
issuer: user.signer,
|
|
455
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
456
|
+
capability: {
|
|
457
|
+
can: 'test/read' as const,
|
|
458
|
+
with: 'ixo:resource:123' as const,
|
|
459
|
+
},
|
|
460
|
+
proofs: [delegation],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const serialized = await serializeInvocation(invocation);
|
|
464
|
+
// Validate against a different resource than what was delegated
|
|
465
|
+
const result = await validator.validate(
|
|
466
|
+
serialized,
|
|
467
|
+
TestRead,
|
|
468
|
+
'ixo:resource:999',
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(result.ok).toBe(false);
|
|
472
|
+
expect(result.error?.code).toBe('UNAUTHORIZED');
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
describe('caveat validation', () => {
|
|
477
|
+
it('should pass when caveats are within bounds', async () => {
|
|
478
|
+
const server = await keygen();
|
|
479
|
+
const root = await keygen();
|
|
480
|
+
const user = await keygen();
|
|
481
|
+
|
|
482
|
+
const resource = `myapp:${server.did}` as const;
|
|
483
|
+
|
|
484
|
+
const validator = await createUCANValidator({
|
|
485
|
+
serverDid: server.did,
|
|
486
|
+
rootIssuers: [root.did],
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const delegation = await Client.delegate({
|
|
490
|
+
issuer: root.signer,
|
|
491
|
+
audience: user.signer,
|
|
492
|
+
capabilities: [
|
|
493
|
+
{
|
|
494
|
+
can: 'employees/read' as const,
|
|
495
|
+
with: resource,
|
|
496
|
+
nb: { limit: 50 },
|
|
497
|
+
},
|
|
498
|
+
],
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const invocation = Client.invoke({
|
|
502
|
+
issuer: user.signer,
|
|
503
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
504
|
+
capability: {
|
|
505
|
+
can: 'employees/read' as const,
|
|
506
|
+
with: resource,
|
|
507
|
+
nb: { limit: 25 },
|
|
508
|
+
},
|
|
509
|
+
proofs: [delegation],
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const serialized = await serializeInvocation(invocation);
|
|
513
|
+
const result = await validator.validate(
|
|
514
|
+
serialized,
|
|
515
|
+
EmployeesRead,
|
|
516
|
+
resource,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(result.ok).toBe(true);
|
|
520
|
+
expect(result.capability?.nb?.limit).toBe(25);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should reject when caveats exceed delegated bounds', async () => {
|
|
524
|
+
const server = await keygen();
|
|
525
|
+
const root = await keygen();
|
|
526
|
+
const user = await keygen();
|
|
527
|
+
|
|
528
|
+
const resource = `myapp:${server.did}` as const;
|
|
529
|
+
|
|
530
|
+
const validator = await createUCANValidator({
|
|
531
|
+
serverDid: server.did,
|
|
532
|
+
rootIssuers: [root.did],
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const delegation = await Client.delegate({
|
|
536
|
+
issuer: root.signer,
|
|
537
|
+
audience: user.signer,
|
|
538
|
+
capabilities: [
|
|
539
|
+
{
|
|
540
|
+
can: 'employees/read' as const,
|
|
541
|
+
with: resource,
|
|
542
|
+
nb: { limit: 25 },
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// User tries to exceed their limit
|
|
548
|
+
const invocation = Client.invoke({
|
|
549
|
+
issuer: user.signer,
|
|
550
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
551
|
+
capability: {
|
|
552
|
+
can: 'employees/read' as const,
|
|
553
|
+
with: resource,
|
|
554
|
+
nb: { limit: 100 },
|
|
555
|
+
},
|
|
556
|
+
proofs: [delegation],
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const serialized = await serializeInvocation(invocation);
|
|
560
|
+
const result = await validator.validate(
|
|
561
|
+
serialized,
|
|
562
|
+
EmployeesRead,
|
|
563
|
+
resource,
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
expect(result.ok).toBe(false);
|
|
567
|
+
expect(result.error?.code).toBe('CAVEAT_VIOLATION');
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('replay protection', () => {
|
|
572
|
+
it('should reject replayed invocations', async () => {
|
|
573
|
+
const server = await keygen();
|
|
574
|
+
const root = await keygen();
|
|
575
|
+
|
|
576
|
+
const validator = await createUCANValidator({
|
|
577
|
+
serverDid: server.did,
|
|
578
|
+
rootIssuers: [root.did],
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const invocation = Client.invoke({
|
|
582
|
+
issuer: root.signer,
|
|
583
|
+
audience: ed25519.Verifier.parse(server.did),
|
|
584
|
+
capability: {
|
|
585
|
+
can: 'test/read' as const,
|
|
586
|
+
with: 'ixo:resource:123' as const,
|
|
587
|
+
},
|
|
588
|
+
proofs: [],
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const serialized = await serializeInvocation(invocation);
|
|
592
|
+
|
|
593
|
+
// First validation should pass
|
|
594
|
+
const result1 = await validator.validate(
|
|
595
|
+
serialized,
|
|
596
|
+
TestRead,
|
|
597
|
+
'ixo:resource:123',
|
|
598
|
+
);
|
|
599
|
+
expect(result1.ok).toBe(true);
|
|
600
|
+
|
|
601
|
+
// Second validation (replay) should fail
|
|
602
|
+
const result2 = await validator.validate(
|
|
603
|
+
serialized,
|
|
604
|
+
TestRead,
|
|
605
|
+
'ixo:resource:123',
|
|
606
|
+
);
|
|
607
|
+
expect(result2.ok).toBe(false);
|
|
608
|
+
expect(result2.error?.code).toBe('REPLAY');
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|