@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/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 |