@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,419 @@
|
|
|
1
|
+
# Server Example
|
|
2
|
+
|
|
3
|
+
Complete Express.js server using `@ixo/ucan` for authorization.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install express @ixo/ucan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Full Example
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import express, { Request, Response } from 'express';
|
|
15
|
+
import {
|
|
16
|
+
createUCANValidator,
|
|
17
|
+
createIxoDIDResolver,
|
|
18
|
+
defineCapability,
|
|
19
|
+
Schema,
|
|
20
|
+
generateKeypair,
|
|
21
|
+
createDelegation,
|
|
22
|
+
serializeDelegation,
|
|
23
|
+
parseSigner,
|
|
24
|
+
SupportedDID,
|
|
25
|
+
} from '@ixo/ucan';
|
|
26
|
+
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(express.json());
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Configuration
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
// Server identity (use did:key for simplicity, or did:ixo for production)
|
|
35
|
+
const SERVER_DID = 'did:ixo:ixo1abc...'; // Your server's DID
|
|
36
|
+
const ROOT_DID = 'did:ixo:ixo1admin...'; // Admin who can delegate
|
|
37
|
+
const ROOT_PRIVATE_KEY = 'MgCY...'; // Admin's private key
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Define Capabilities
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Capability with caveat validation (limit)
|
|
45
|
+
*
|
|
46
|
+
* The `derives` function enforces attenuation:
|
|
47
|
+
* - A delegation with limit: 100 can create sub-delegations with limit ≤ 100
|
|
48
|
+
* - An invocation can only request up to the delegated limit
|
|
49
|
+
*/
|
|
50
|
+
const EmployeesRead = defineCapability({
|
|
51
|
+
can: 'employees/read',
|
|
52
|
+
protocol: 'myapp:',
|
|
53
|
+
nb: { limit: Schema.integer().optional() },
|
|
54
|
+
derives: (claimed, delegated) => {
|
|
55
|
+
const claimedLimit = claimed.nb?.limit ?? Infinity;
|
|
56
|
+
const delegatedLimit = delegated.nb?.limit ?? Infinity;
|
|
57
|
+
|
|
58
|
+
if (claimedLimit > delegatedLimit) {
|
|
59
|
+
return {
|
|
60
|
+
error: new Error(
|
|
61
|
+
`Cannot request limit=${claimedLimit}, delegation only allows limit=${delegatedLimit}`,
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { ok: {} };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Simple capability without caveats
|
|
71
|
+
*/
|
|
72
|
+
const EmployeesWrite = defineCapability({
|
|
73
|
+
can: 'employees/write',
|
|
74
|
+
protocol: 'myapp:',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Initialize Validator
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
let validator: Awaited<ReturnType<typeof createUCANValidator>>;
|
|
82
|
+
|
|
83
|
+
async function initializeServer() {
|
|
84
|
+
// Create DID resolver for did:ixo
|
|
85
|
+
const didResolver = createIxoDIDResolver({
|
|
86
|
+
indexerUrl: 'https://blocksync.ixo.earth/graphql',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Create validator (async to resolve non-did:key DIDs at startup)
|
|
90
|
+
validator = await createUCANValidator({
|
|
91
|
+
serverDid: SERVER_DID,
|
|
92
|
+
rootIssuers: [ROOT_DID],
|
|
93
|
+
didResolver,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log('Server DID:', SERVER_DID);
|
|
97
|
+
console.log('Root Issuer:', ROOT_DID);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Helper Functions
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
function buildResourceUri(serverDid: string): `myapp:${string}` {
|
|
105
|
+
return `myapp:${serverDid}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildEmployees(limit: number) {
|
|
109
|
+
return Array.from({ length: limit }, (_, i) => ({
|
|
110
|
+
id: i + 1,
|
|
111
|
+
name: `Employee ${i + 1}`,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// Routes
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Health check
|
|
121
|
+
*/
|
|
122
|
+
app.get('/health', (_req, res) => {
|
|
123
|
+
res.json({ status: 'ok' });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Server info and available capabilities
|
|
128
|
+
*/
|
|
129
|
+
app.get('/info', (_req, res) => {
|
|
130
|
+
res.json({
|
|
131
|
+
serverDid: SERVER_DID,
|
|
132
|
+
rootIssuers: [ROOT_DID],
|
|
133
|
+
capabilities: {
|
|
134
|
+
'employees/read': {
|
|
135
|
+
description: 'Read employee data',
|
|
136
|
+
caveats: { limit: 'Maximum number of employees to return' },
|
|
137
|
+
},
|
|
138
|
+
'employees/write': {
|
|
139
|
+
description: 'Write employee data',
|
|
140
|
+
caveats: null,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Protected route - requires valid UCAN invocation
|
|
148
|
+
*
|
|
149
|
+
* Send: POST /protected
|
|
150
|
+
* Body: { "invocation": "<base64 CAR>" }
|
|
151
|
+
*/
|
|
152
|
+
app.post('/protected', async (req: Request, res: Response) => {
|
|
153
|
+
const invocationBase64 = req.body?.invocation;
|
|
154
|
+
|
|
155
|
+
if (!invocationBase64) {
|
|
156
|
+
res.status(400).json({
|
|
157
|
+
error: 'Missing invocation in request body',
|
|
158
|
+
hint: 'Send { "invocation": "<base64 CAR>" }',
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate with capability (includes caveat validation!)
|
|
164
|
+
const result = await validator.validate(
|
|
165
|
+
invocationBase64,
|
|
166
|
+
EmployeesRead,
|
|
167
|
+
buildResourceUri(SERVER_DID),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
res.status(403).json({
|
|
172
|
+
error: 'Unauthorized',
|
|
173
|
+
details: result.error,
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Extract limit from validated capability
|
|
179
|
+
const limit = (result.capability?.nb?.limit as number) ?? 10;
|
|
180
|
+
|
|
181
|
+
res.json({
|
|
182
|
+
message: 'Access granted!',
|
|
183
|
+
invoker: result.invoker,
|
|
184
|
+
capability: result.capability,
|
|
185
|
+
employees: buildEmployees(limit),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create delegation for a user
|
|
191
|
+
*
|
|
192
|
+
* Send: POST /delegate
|
|
193
|
+
* Body: {
|
|
194
|
+
* "audience": "did:key:z6Mk...",
|
|
195
|
+
* "capabilities": [{ "can": "employees/read", "nb": { "limit": 50 } }],
|
|
196
|
+
* "expiration": 1735689600 // Optional, defaults to 24h
|
|
197
|
+
* }
|
|
198
|
+
*/
|
|
199
|
+
app.post('/delegate', async (req: Request, res: Response) => {
|
|
200
|
+
const { audience, capabilities, expiration } = req.body;
|
|
201
|
+
|
|
202
|
+
if (!audience) {
|
|
203
|
+
res.status(400).json({
|
|
204
|
+
error: 'Missing audience DID',
|
|
205
|
+
hint: 'Send { "audience": "did:key:..." }',
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!capabilities || !Array.isArray(capabilities)) {
|
|
211
|
+
res.status(400).json({
|
|
212
|
+
error: 'Missing or invalid capabilities',
|
|
213
|
+
hint: 'Send { "capabilities": [{ "can": "employees/read", "nb": { "limit": 50 } }] }',
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const rootSigner = parseSigner(ROOT_PRIVATE_KEY, ROOT_DID as SupportedDID);
|
|
220
|
+
|
|
221
|
+
// Build full capabilities with resource URI
|
|
222
|
+
const fullCapabilities = capabilities.map((cap: any) => ({
|
|
223
|
+
can: cap.can,
|
|
224
|
+
with: buildResourceUri(SERVER_DID),
|
|
225
|
+
nb: cap.nb,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
// Default expiration: 24 hours
|
|
229
|
+
const exp = expiration ?? Math.floor(Date.now() / 1000) + 86400;
|
|
230
|
+
|
|
231
|
+
const delegation = await createDelegation({
|
|
232
|
+
issuer: rootSigner,
|
|
233
|
+
audience,
|
|
234
|
+
capabilities: fullCapabilities as any,
|
|
235
|
+
expiration: exp,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const serialized = await serializeDelegation(delegation);
|
|
239
|
+
|
|
240
|
+
res.json({
|
|
241
|
+
success: true,
|
|
242
|
+
delegation: serialized,
|
|
243
|
+
details: {
|
|
244
|
+
cid: delegation.cid.toString(),
|
|
245
|
+
issuer: delegation.issuer.did(),
|
|
246
|
+
audience: delegation.audience.did(),
|
|
247
|
+
expiration: exp,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
} catch (error: any) {
|
|
251
|
+
res.status(500).json({
|
|
252
|
+
error: 'Failed to create delegation',
|
|
253
|
+
details: error.message,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// =============================================================================
|
|
259
|
+
// Start Server
|
|
260
|
+
// =============================================================================
|
|
261
|
+
|
|
262
|
+
initializeServer().then(() => {
|
|
263
|
+
const PORT = process.env.PORT || 3000;
|
|
264
|
+
app.listen(PORT, () => {
|
|
265
|
+
console.log(`Server listening on http://localhost:${PORT}`);
|
|
266
|
+
console.log('\nEndpoints:');
|
|
267
|
+
console.log(' GET /health - Health check');
|
|
268
|
+
console.log(' GET /info - Server info');
|
|
269
|
+
console.log(
|
|
270
|
+
' POST /protected - Protected endpoint (requires invocation)',
|
|
271
|
+
);
|
|
272
|
+
console.log(' POST /delegate - Create delegation for a user');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Testing with cURL
|
|
278
|
+
|
|
279
|
+
### 1. Get server info
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
curl http://localhost:3000/info
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 2. Create a delegation
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
curl -X POST http://localhost:3000/delegate \
|
|
289
|
+
-H "Content-Type: application/json" \
|
|
290
|
+
-d '{
|
|
291
|
+
"audience": "did:key:z6MkUserKey...",
|
|
292
|
+
"capabilities": [{ "can": "employees/read", "nb": { "limit": 50 } }]
|
|
293
|
+
}'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 3. Use the delegation (invocation)
|
|
297
|
+
|
|
298
|
+
The client needs to create and sign an invocation. See [CLIENT.md](./CLIENT.md) for how to do this.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
curl -X POST http://localhost:3000/protected \
|
|
302
|
+
-H "Content-Type: application/json" \
|
|
303
|
+
-d '{ "invocation": "<base64 CAR from client>" }'
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Validation Results
|
|
307
|
+
|
|
308
|
+
### Success Response
|
|
309
|
+
|
|
310
|
+
```json
|
|
311
|
+
{
|
|
312
|
+
"message": "Access granted!",
|
|
313
|
+
"invoker": "did:key:z6MkUserKey...",
|
|
314
|
+
"capability": {
|
|
315
|
+
"can": "employees/read",
|
|
316
|
+
"with": "myapp:did:ixo:ixo1abc...",
|
|
317
|
+
"nb": { "limit": 25 }
|
|
318
|
+
},
|
|
319
|
+
"employees": [...]
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Error Responses
|
|
324
|
+
|
|
325
|
+
**Missing invocation:**
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"error": "Missing invocation in request body",
|
|
330
|
+
"hint": "Send { \"invocation\": \"<base64 CAR>\" }"
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Invalid signature:**
|
|
335
|
+
|
|
336
|
+
```json
|
|
337
|
+
{
|
|
338
|
+
"error": "Unauthorized",
|
|
339
|
+
"details": { "code": "INVALID_SIGNATURE", "message": "..." }
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Caveat violation:**
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"error": "Unauthorized",
|
|
348
|
+
"details": {
|
|
349
|
+
"code": "CAVEAT_VIOLATION",
|
|
350
|
+
"message": "Cannot request limit=100, delegation only allows limit=50"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Replay attack:**
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"error": "Unauthorized",
|
|
360
|
+
"details": { "code": "REPLAY", "message": "Invocation has already been used" }
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Using with Other Frameworks
|
|
365
|
+
|
|
366
|
+
The validator is framework-agnostic. Here's how to use it with other frameworks:
|
|
367
|
+
|
|
368
|
+
### Fastify
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
fastify.post('/protected', async (request, reply) => {
|
|
372
|
+
const result = await validator.validate(
|
|
373
|
+
request.body.invocation,
|
|
374
|
+
EmployeesRead,
|
|
375
|
+
'myapp:server'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
if (!result.ok) {
|
|
379
|
+
return reply.code(403).send({ error: result.error });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { employees: [...] };
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Hono
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
app.post('/protected', async (c) => {
|
|
390
|
+
const { invocation } = await c.req.json();
|
|
391
|
+
const result = await validator.validate(invocation, EmployeesRead, 'myapp:server');
|
|
392
|
+
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
return c.json({ error: result.error }, 403);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return c.json({ employees: [...] });
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### NestJS
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
@Controller('employees')
|
|
405
|
+
export class EmployeesController {
|
|
406
|
+
constructor(private readonly ucanService: UCANService) {}
|
|
407
|
+
|
|
408
|
+
@Post()
|
|
409
|
+
async getEmployees(@Body('invocation') invocation: string) {
|
|
410
|
+
const result = await this.ucanService.validate(invocation, EmployeesRead);
|
|
411
|
+
|
|
412
|
+
if (!result.ok) {
|
|
413
|
+
throw new ForbiddenException(result.error);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { employees: [...] };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/ucan",
|
|
3
3
|
"description": "UCAN authorization for any service - built on ucanto",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -38,15 +38,13 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@cosmjs/crypto": "0.32.4",
|
|
40
40
|
"@cosmjs/encoding": "0.32.4",
|
|
41
|
-
"@types/jest": "^29.5.14",
|
|
42
41
|
"@types/node": "^22.10.5",
|
|
43
|
-
"jest": "^29.7.0",
|
|
44
|
-
"ts-jest": "^29.2.5",
|
|
45
42
|
"tsx": "^4.7.0",
|
|
46
43
|
"typescript": "^5.5.4",
|
|
47
|
-
"
|
|
48
|
-
"@ixo/
|
|
49
|
-
"@ixo/typescript-config": "
|
|
44
|
+
"vitest": "^3.2.4",
|
|
45
|
+
"@ixo/eslint-config": "2.0.0",
|
|
46
|
+
"@ixo/typescript-config": "1.0.0",
|
|
47
|
+
"@ixo/vitest-config": "1.0.0"
|
|
50
48
|
},
|
|
51
49
|
"dependencies": {
|
|
52
50
|
"@ucanto/client": "9.0.2",
|
|
@@ -72,7 +70,7 @@
|
|
|
72
70
|
},
|
|
73
71
|
"scripts": {
|
|
74
72
|
"build": "tsc",
|
|
75
|
-
"test": "
|
|
73
|
+
"test": "vitest run",
|
|
76
74
|
"test:ucan": "tsx scripts/test-ucan.ts"
|
|
77
75
|
}
|
|
78
76
|
}
|
package/scripts/test-ucan.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
/**
|
|
2
3
|
* UCAN Test Script
|
|
3
4
|
*
|
|
@@ -23,11 +24,8 @@ import {
|
|
|
23
24
|
// CONFIGURATION - Change this to test different scenarios
|
|
24
25
|
// ============================================================================
|
|
25
26
|
|
|
26
|
-
const ACTION:
|
|
27
|
-
|
|
28
|
-
| 'create-delegation'
|
|
29
|
-
| 'full-flow'
|
|
30
|
-
| 'validate' = 'full-flow';
|
|
27
|
+
const ACTION: 'generate-keys' | 'create-delegation' | 'full-flow' | 'validate' =
|
|
28
|
+
'full-flow';
|
|
31
29
|
|
|
32
30
|
// ============================================================================
|
|
33
31
|
// CAPABILITY DEFINITION
|
|
@@ -66,7 +64,9 @@ function log(title: string, data?: unknown) {
|
|
|
66
64
|
console.log(`│ ${title}`);
|
|
67
65
|
console.log('─'.repeat(70));
|
|
68
66
|
if (data !== undefined) {
|
|
69
|
-
console.log(
|
|
67
|
+
console.log(
|
|
68
|
+
typeof data === 'string' ? data : JSON.stringify(data, null, 2),
|
|
69
|
+
);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -97,11 +97,17 @@ async function generateKeys() {
|
|
|
97
97
|
const did = signer.did();
|
|
98
98
|
const privateKey = ed25519.Signer.format(signer);
|
|
99
99
|
|
|
100
|
-
console.log(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
console.log(
|
|
101
|
+
JSON.stringify(
|
|
102
|
+
{
|
|
103
|
+
did,
|
|
104
|
+
privateKey,
|
|
105
|
+
note: 'Save the privateKey securely! The DID is public.',
|
|
106
|
+
},
|
|
107
|
+
null,
|
|
108
|
+
2,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
105
111
|
|
|
106
112
|
return { signer, did, privateKey };
|
|
107
113
|
}
|
|
@@ -233,7 +239,7 @@ async function fullFlow() {
|
|
|
233
239
|
});
|
|
234
240
|
|
|
235
241
|
success(`Delegation created: ${aliceToBob.cid.toString().slice(0, 20)}...`);
|
|
236
|
-
info(
|
|
242
|
+
info("Bob can now read up to 25 employees (attenuated from Alice's 50)");
|
|
237
243
|
|
|
238
244
|
// ─────────────────────────────────────────────────────────────────────────
|
|
239
245
|
// STEP 4: Alice invokes with limit: 50 (should succeed)
|
|
@@ -261,10 +267,12 @@ async function fullFlow() {
|
|
|
261
267
|
);
|
|
262
268
|
|
|
263
269
|
if (aliceResult.ok) {
|
|
264
|
-
success(
|
|
270
|
+
success("Alice's invocation PASSED");
|
|
265
271
|
console.log(` Requested: ${aliceResult.capability?.nb?.limit} employees`);
|
|
266
272
|
} else {
|
|
267
|
-
fail(
|
|
273
|
+
fail(
|
|
274
|
+
`Alice's invocation failed unexpectedly: ${aliceResult.error?.message}`,
|
|
275
|
+
);
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -293,11 +301,11 @@ async function fullFlow() {
|
|
|
293
301
|
);
|
|
294
302
|
|
|
295
303
|
if (!bobBadResult.ok) {
|
|
296
|
-
success(
|
|
304
|
+
success("Bob's excessive request correctly REJECTED");
|
|
297
305
|
console.log(` Error: ${bobBadResult.error?.message}`);
|
|
298
306
|
console.log(` Code: ${bobBadResult.error?.code}`);
|
|
299
307
|
} else {
|
|
300
|
-
fail(
|
|
308
|
+
fail("Bob's excessive request should have been rejected!");
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -326,11 +334,15 @@ async function fullFlow() {
|
|
|
326
334
|
);
|
|
327
335
|
|
|
328
336
|
if (bobGoodResult.ok) {
|
|
329
|
-
success(
|
|
330
|
-
console.log(
|
|
337
|
+
success("Bob's valid request PASSED");
|
|
338
|
+
console.log(
|
|
339
|
+
` Requested: ${bobGoodResult.capability?.nb?.limit} employees`,
|
|
340
|
+
);
|
|
331
341
|
console.log(` Invoker: ${bobGoodResult.invoker?.slice(0, 40)}...`);
|
|
332
342
|
} else {
|
|
333
|
-
fail(
|
|
343
|
+
fail(
|
|
344
|
+
`Bob's valid request failed unexpectedly: ${bobGoodResult.error?.message}`,
|
|
345
|
+
);
|
|
334
346
|
}
|
|
335
347
|
|
|
336
348
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -23,7 +23,9 @@ export { Schema };
|
|
|
23
23
|
* Extracts the output type O from a Reader/Schema
|
|
24
24
|
* A Reader<O, I> has a read method that returns { ok: O } | { error: ... }
|
|
25
25
|
*/
|
|
26
|
-
type Infer<T> = T extends {
|
|
26
|
+
type Infer<T> = T extends {
|
|
27
|
+
read(input: unknown): { ok: infer O } | { error: unknown };
|
|
28
|
+
}
|
|
27
29
|
? O
|
|
28
30
|
: never;
|
|
29
31
|
|
|
@@ -66,8 +68,7 @@ type InferStruct<U extends Record<string, unknown>> = {
|
|
|
66
68
|
*/
|
|
67
69
|
export interface DefineCapabilityOptions<
|
|
68
70
|
// NBSchema is the schema SHAPE, e.g., { limit: Schema<number | undefined> }
|
|
69
|
-
|
|
70
|
-
NBSchema extends Record<string, any> = Record<string, never>,
|
|
71
|
+
NBSchema extends Record<string, unknown> = Record<string, never>,
|
|
71
72
|
> {
|
|
72
73
|
/**
|
|
73
74
|
* The action this capability authorizes
|
|
@@ -166,10 +167,10 @@ export interface DefineCapabilityOptions<
|
|
|
166
167
|
* });
|
|
167
168
|
* ```
|
|
168
169
|
*/
|
|
169
|
-
|
|
170
|
-
export function defineCapability<
|
|
171
|
-
|
|
172
|
-
) {
|
|
170
|
+
|
|
171
|
+
export function defineCapability<
|
|
172
|
+
NBSchema extends Record<string, unknown> = Record<string, never>,
|
|
173
|
+
>(options: DefineCapabilityOptions<NBSchema>) {
|
|
173
174
|
const protocol = (options.protocol ?? 'urn:') as `${string}:`;
|
|
174
175
|
const supportWildcards = options.supportWildcards ?? true;
|
|
175
176
|
|