@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
package/docs/FLOW.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# UCAN Flow: Delegation, Attenuation & Invocation
|
|
2
|
+
|
|
3
|
+
This document explains how UCAN authorization works with visual diagrams.
|
|
4
|
+
|
|
5
|
+
## The Players
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
👑 ROOT 👩 ALICE 👤 BOB
|
|
9
|
+
───────────────── ────────────────── ─────────────────
|
|
10
|
+
Resource Owner Gets limit: 50 Gets limit: 25
|
|
11
|
+
Full authority Can delegate further Can only use, not expand
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The Flow
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ UCAN DELEGATION CHAIN │
|
|
19
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
20
|
+
|
|
21
|
+
┌─────────────────────────────┐
|
|
22
|
+
│ ROOT │
|
|
23
|
+
│ (Resource Owner) │
|
|
24
|
+
│ can: employees/read │
|
|
25
|
+
│ with: myapp:company │
|
|
26
|
+
│ limit: ∞ (unlimited) │
|
|
27
|
+
└─────────────┬───────────────┘
|
|
28
|
+
│
|
|
29
|
+
│ DELEGATES (limit: 50)
|
|
30
|
+
▼
|
|
31
|
+
┌─────────────────────────────┐
|
|
32
|
+
│ ALICE │
|
|
33
|
+
│ (Team Lead) │
|
|
34
|
+
│ can: employees/read │
|
|
35
|
+
│ with: myapp:company │
|
|
36
|
+
│ limit: 50 │
|
|
37
|
+
└─────────────┬───────────────┘
|
|
38
|
+
│
|
|
39
|
+
│ RE-DELEGATES (limit: 25)
|
|
40
|
+
│ ← Attenuated! (narrower)
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────────────┐
|
|
43
|
+
│ BOB │
|
|
44
|
+
│ (Employee) │
|
|
45
|
+
│ can: employees/read │
|
|
46
|
+
│ with: myapp:company │
|
|
47
|
+
│ limit: 25 │
|
|
48
|
+
└─────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Delegation Structure
|
|
52
|
+
|
|
53
|
+
When Root delegates to Alice:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌────────────────────────────────────────────┐
|
|
57
|
+
│ DELEGATION #1 │
|
|
58
|
+
│ ────────────────────────────────────── │
|
|
59
|
+
│ issuer: did:key:root │
|
|
60
|
+
│ audience: did:key:alice │
|
|
61
|
+
│ can: "employees/read" │
|
|
62
|
+
│ with: "myapp:company" │
|
|
63
|
+
│ nb: { limit: 50 } │
|
|
64
|
+
│ expires: 2025-12-31 │
|
|
65
|
+
│ proofs: [] ← Root needs no proof │
|
|
66
|
+
│ ────────────────────────────────────── │
|
|
67
|
+
│ signature: <Root's signature> │
|
|
68
|
+
└────────────────────────────────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When Alice re-delegates to Bob:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌────────────────────────────────────────────┐
|
|
75
|
+
│ DELEGATION #2 │
|
|
76
|
+
│ ────────────────────────────────────── │
|
|
77
|
+
│ issuer: did:key:alice │
|
|
78
|
+
│ audience: did:key:bob │
|
|
79
|
+
│ can: "employees/read" │
|
|
80
|
+
│ with: "myapp:company" │
|
|
81
|
+
│ nb: { limit: 25 } ← NARROWED! │
|
|
82
|
+
│ expires: 2025-06-30 ← SHORTER! │
|
|
83
|
+
│ proofs: [DELEGATION #1] ← Chain │
|
|
84
|
+
│ ────────────────────────────────────── │
|
|
85
|
+
│ signature: <Alice's signature> │
|
|
86
|
+
└────────────────────────────────────────────┘
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Invocation & Validation
|
|
90
|
+
|
|
91
|
+
When Bob wants to use his capability:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌────────────────────────────────────────────┐
|
|
95
|
+
│ INVOCATION (Bob's Request) │
|
|
96
|
+
│ ────────────────────────────────────── │
|
|
97
|
+
│ issuer: did:key:bob │
|
|
98
|
+
│ audience: did:key:server │
|
|
99
|
+
│ can: "employees/read" │
|
|
100
|
+
│ with: "myapp:company" │
|
|
101
|
+
│ nb: { limit: 20 } ← Request │
|
|
102
|
+
│ proofs: [DELEGATION #2] │
|
|
103
|
+
│ ────────────────────────────────────── │
|
|
104
|
+
│ signature: <Bob's signature> │
|
|
105
|
+
└────────────────────────────────────────────┘
|
|
106
|
+
│
|
|
107
|
+
│ Sent to Server
|
|
108
|
+
▼
|
|
109
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
110
|
+
│ SERVER VALIDATION │
|
|
111
|
+
├─────────────────────────────────────────────────────────────┤
|
|
112
|
+
│ 1. ✅ Verify Bob's signature on invocation │
|
|
113
|
+
│ 2. ✅ Check invocation audience = server DID │
|
|
114
|
+
│ 3. ✅ Extract DELEGATION #2 from proofs │
|
|
115
|
+
│ 4. ✅ Verify Alice's signature on DELEGATION #2 │
|
|
116
|
+
│ 5. ✅ Check DELEGATION #2.audience = Bob (invoker) │
|
|
117
|
+
│ 6. ✅ Extract DELEGATION #1 from DELEGATION #2.proofs │
|
|
118
|
+
│ 7. ✅ Verify Root's signature on DELEGATION #1 │
|
|
119
|
+
│ 8. ✅ Check DELEGATION #1.audience = Alice │
|
|
120
|
+
│ 9. ✅ Root is trusted root issuer │
|
|
121
|
+
│ 10. ✅ Caveat check: 20 ≤ 25 (Bob's limit) │
|
|
122
|
+
│ 11. ✅ Caveat check: 25 ≤ 50 (Alice's limit) │
|
|
123
|
+
│ 12. ✅ CID not in replay store │
|
|
124
|
+
│ 13. ✅ Not expired │
|
|
125
|
+
├─────────────────────────────────────────────────────────────┤
|
|
126
|
+
│ ACCESS GRANTED ✅ │
|
|
127
|
+
└─────────────────────────────────────────────────────────────┘
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Attenuation Rules
|
|
131
|
+
|
|
132
|
+
**Key Principle**: You can only delegate ≤ what you have.
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
136
|
+
│ ATTENUATION RULES │
|
|
137
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
138
|
+
│ │
|
|
139
|
+
│ ✅ ALLOWED (Narrowing): │
|
|
140
|
+
│ • limit: 50 → limit: 25 (smaller) │
|
|
141
|
+
│ • expires: Dec → expires: June (shorter) │
|
|
142
|
+
│ • with: "myapp:*" → with: "myapp:company" (more specific) │
|
|
143
|
+
│ │
|
|
144
|
+
│ ❌ FORBIDDEN (Escalation): │
|
|
145
|
+
│ • limit: 25 → limit: 50 (larger) │
|
|
146
|
+
│ • expires: June → expires: Dec (longer) │
|
|
147
|
+
│ • with: "myapp:company" → with: "myapp:*" (broader) │
|
|
148
|
+
│ │
|
|
149
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## What Each Party Can Do
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
156
|
+
│ Action │ Root │ Alice │ Bob │
|
|
157
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
158
|
+
│ Read 100 employees │ ✅ │ ❌ │ ❌ │
|
|
159
|
+
│ Read 50 employees │ ✅ │ ✅ │ ❌ │
|
|
160
|
+
│ Read 25 employees │ ✅ │ ✅ │ ✅ │
|
|
161
|
+
│ Delegate limit: 50 │ ✅ │ ✅ │ ❌ │
|
|
162
|
+
│ Delegate limit: 25 │ ✅ │ ✅ │ ✅ │
|
|
163
|
+
│ Delegate limit: 10 │ ✅ │ ✅ │ ✅ │
|
|
164
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Self-Contained Invocations
|
|
168
|
+
|
|
169
|
+
Invocations bundle their entire proof chain:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
173
|
+
│ INVOCATION PAYLOAD (self-contained) │
|
|
174
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
175
|
+
│ │
|
|
176
|
+
│ Bob's Invocation │
|
|
177
|
+
│ └── proofs: [ DELEGATION #2 ] │
|
|
178
|
+
│ └── Alice's Delegation to Bob │
|
|
179
|
+
│ └── proofs: [ DELEGATION #1 ] │
|
|
180
|
+
│ └── Root's Delegation to Alice │
|
|
181
|
+
│ └── proofs: [] (root!) │
|
|
182
|
+
│ │
|
|
183
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Benefits:**
|
|
187
|
+
|
|
188
|
+
- Server doesn't need external delegation store
|
|
189
|
+
- Validation is entirely local
|
|
190
|
+
- No network calls during validation (except DID resolution)
|
|
191
|
+
|
|
192
|
+
## Replay Protection
|
|
193
|
+
|
|
194
|
+
Each invocation has a unique CID. The server stores used CIDs:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
198
|
+
│ REPLAY PROTECTION │
|
|
199
|
+
├──────────────────────────────────────────────────────────────┤
|
|
200
|
+
│ │
|
|
201
|
+
│ First request: │
|
|
202
|
+
│ Invocation CID: bafy...abc │
|
|
203
|
+
│ → Not in store → PROCESS → Add to store ✅ │
|
|
204
|
+
│ │
|
|
205
|
+
│ Replay attempt: │
|
|
206
|
+
│ Invocation CID: bafy...abc (same!) │
|
|
207
|
+
│ → Already in store → REJECT ❌ │
|
|
208
|
+
│ │
|
|
209
|
+
└──────────────────────────────────────────────────────────────┘
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Code Example
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import {
|
|
216
|
+
defineCapability,
|
|
217
|
+
createUCANValidator,
|
|
218
|
+
createDelegation,
|
|
219
|
+
createInvocation,
|
|
220
|
+
Schema,
|
|
221
|
+
} from '@ixo/ucan';
|
|
222
|
+
|
|
223
|
+
// 1. Define capability with caveat
|
|
224
|
+
const EmployeesRead = defineCapability({
|
|
225
|
+
can: 'employees/read',
|
|
226
|
+
protocol: 'myapp:',
|
|
227
|
+
nb: { limit: Schema.integer().optional() },
|
|
228
|
+
derives: (claimed, delegated) => {
|
|
229
|
+
const claimedLimit = claimed.nb?.limit ?? Infinity;
|
|
230
|
+
const delegatedLimit = delegated.nb?.limit ?? Infinity;
|
|
231
|
+
if (claimedLimit > delegatedLimit) {
|
|
232
|
+
return { error: new Error('Limit exceeds delegation') };
|
|
233
|
+
}
|
|
234
|
+
return { ok: {} };
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 2. Root delegates to Alice with limit: 50
|
|
239
|
+
const rootToAlice = await createDelegation({
|
|
240
|
+
issuer: rootSigner,
|
|
241
|
+
audience: aliceDid,
|
|
242
|
+
capabilities: [
|
|
243
|
+
{ can: 'employees/read', with: 'myapp:company', nb: { limit: 50 } },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 3. Alice re-delegates to Bob with limit: 25
|
|
248
|
+
const aliceToBob = await createDelegation({
|
|
249
|
+
issuer: aliceSigner,
|
|
250
|
+
audience: bobDid,
|
|
251
|
+
capabilities: [
|
|
252
|
+
{ can: 'employees/read', with: 'myapp:company', nb: { limit: 25 } },
|
|
253
|
+
],
|
|
254
|
+
proofs: [rootToAlice], // Include proof chain
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 4. Bob invokes with limit: 20
|
|
258
|
+
const invocation = await createInvocation({
|
|
259
|
+
issuer: bobSigner,
|
|
260
|
+
audience: serverDid,
|
|
261
|
+
capability: {
|
|
262
|
+
can: 'employees/read',
|
|
263
|
+
with: 'myapp:company',
|
|
264
|
+
nb: { limit: 20 },
|
|
265
|
+
},
|
|
266
|
+
proofs: [aliceToBob], // Includes entire chain
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// 5. Server validates
|
|
270
|
+
const result = await validator.validate(
|
|
271
|
+
serialized,
|
|
272
|
+
EmployeesRead,
|
|
273
|
+
'myapp:company',
|
|
274
|
+
);
|
|
275
|
+
// result.ok === true, result.capability.nb.limit === 20
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Summary
|
|
279
|
+
|
|
280
|
+
| Concept | Description |
|
|
281
|
+
| --------------------- | -------------------------------------------------- |
|
|
282
|
+
| **Delegation** | Granting capabilities to others (signed by issuer) |
|
|
283
|
+
| **Attenuation** | Narrowing permissions when re-delegating |
|
|
284
|
+
| **Invocation** | Request to use a capability (signed by invoker) |
|
|
285
|
+
| **Proof Chain** | Delegations linked together, bundled in invocation |
|
|
286
|
+
| **Caveat** | Restrictions on capabilities (e.g., `limit`) |
|
|
287
|
+
| **Replay Protection** | CID-based tracking prevents reuse |
|