@ixo/ucan 1.0.0 → 1.0.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/.turbo/turbo-build.log +1 -1
- package/README.md +171 -117
- package/docs/FLOW.md +275 -0
- package/docs/examples/CLIENT.md +407 -0
- package/docs/examples/SERVER.md +414 -0
- package/package.json +3 -3
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# Client Example
|
|
2
|
+
|
|
3
|
+
How to use `@ixo/ucan` on the client-side (browser or Node.js) to create delegations and invocations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ixo/ucan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Key Management
|
|
12
|
+
|
|
13
|
+
### Generate a New Keypair
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { generateKeypair } from '@ixo/ucan';
|
|
17
|
+
|
|
18
|
+
const { signer, did, privateKey } = await generateKeypair();
|
|
19
|
+
|
|
20
|
+
console.log('DID:', did); // did:key:z6Mk...
|
|
21
|
+
console.log('Private Key:', privateKey); // MgCY... (save securely!)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Parse an Existing Private Key
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { parseSigner } from '@ixo/ucan';
|
|
28
|
+
|
|
29
|
+
const signer = parseSigner('MgCY...'); // Your stored private key
|
|
30
|
+
console.log('DID:', signer.did());
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Derive from a BIP39 Mnemonic
|
|
34
|
+
|
|
35
|
+
Useful for deriving keys from an existing wallet mnemonic:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { signerFromMnemonic } from '@ixo/ucan';
|
|
39
|
+
|
|
40
|
+
const { signer, did, privateKey } = await signerFromMnemonic(
|
|
41
|
+
'word1 word2 word3 ...', // 12-24 word mnemonic
|
|
42
|
+
'did:ixo:ixo1abc...' // Optional: override DID (e.g., for did:ixo)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log('DID:', did);
|
|
46
|
+
console.log('Private Key:', privateKey); // Can be used with parseSigner()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Working with Delegations
|
|
50
|
+
|
|
51
|
+
### Create a Delegation
|
|
52
|
+
|
|
53
|
+
Grant capabilities to another user:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createDelegation, parseSigner, serializeDelegation } from '@ixo/ucan';
|
|
57
|
+
|
|
58
|
+
// Your signer (whoever is granting the delegation)
|
|
59
|
+
const issuerSigner = parseSigner('MgCY...');
|
|
60
|
+
|
|
61
|
+
// Create delegation
|
|
62
|
+
const delegation = await createDelegation({
|
|
63
|
+
issuer: issuerSigner,
|
|
64
|
+
audience: 'did:key:z6MkRecipient...', // Who receives the capability
|
|
65
|
+
capabilities: [
|
|
66
|
+
{
|
|
67
|
+
can: 'employees/read',
|
|
68
|
+
with: 'myapp:company/acme',
|
|
69
|
+
nb: { limit: 50 }, // Caveat: max 50 employees
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Serialize for storage/transport
|
|
76
|
+
const serialized = await serializeDelegation(delegation);
|
|
77
|
+
console.log('Delegation (base64):', serialized);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Parse a Delegation
|
|
81
|
+
|
|
82
|
+
Load a previously serialized delegation:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { parseDelegation } from '@ixo/ucan';
|
|
86
|
+
|
|
87
|
+
const delegation = await parseDelegation(serializedBase64);
|
|
88
|
+
|
|
89
|
+
console.log('CID:', delegation.cid.toString());
|
|
90
|
+
console.log('Issuer:', delegation.issuer.did());
|
|
91
|
+
console.log('Audience:', delegation.audience.did());
|
|
92
|
+
console.log('Capabilities:', delegation.capabilities);
|
|
93
|
+
console.log('Expiration:', new Date(delegation.expiration * 1000));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Re-Delegate (Chain Delegations)
|
|
97
|
+
|
|
98
|
+
Pass your delegation to someone else with equal or narrower permissions:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { createDelegation, parseDelegation, parseSigner } from '@ixo/ucan';
|
|
102
|
+
|
|
103
|
+
// Your delegation (received from someone above you in the chain)
|
|
104
|
+
const myDelegation = await parseDelegation(mySerializedDelegation);
|
|
105
|
+
|
|
106
|
+
// Your signer
|
|
107
|
+
const mySigner = parseSigner('MgCY...');
|
|
108
|
+
|
|
109
|
+
// Re-delegate to someone else (with narrower permissions!)
|
|
110
|
+
const subDelegation = await createDelegation({
|
|
111
|
+
issuer: mySigner,
|
|
112
|
+
audience: 'did:key:z6MkSubordinate...',
|
|
113
|
+
capabilities: [
|
|
114
|
+
{
|
|
115
|
+
can: 'employees/read',
|
|
116
|
+
with: 'myapp:company/acme',
|
|
117
|
+
nb: { limit: 25 }, // ⬅️ Narrower than my limit of 50
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
expiration: Math.floor(Date.now() / 1000) + 1800, // Shorter: 30 min
|
|
121
|
+
proofs: [myDelegation], // Include proof chain
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Creating Invocations
|
|
126
|
+
|
|
127
|
+
### Create and Send an Invocation
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { createInvocation, serializeInvocation, parseDelegation, parseSigner } from '@ixo/ucan';
|
|
131
|
+
|
|
132
|
+
// Load your delegation
|
|
133
|
+
const delegation = await parseDelegation(mySerializedDelegation);
|
|
134
|
+
|
|
135
|
+
// Your signer
|
|
136
|
+
const signer = parseSigner('MgCY...');
|
|
137
|
+
|
|
138
|
+
// Server's DID (from /info endpoint or known)
|
|
139
|
+
const serverDid = 'did:ixo:ixo1server...';
|
|
140
|
+
|
|
141
|
+
// Create invocation
|
|
142
|
+
const invocation = await createInvocation({
|
|
143
|
+
issuer: signer,
|
|
144
|
+
audience: serverDid,
|
|
145
|
+
capability: {
|
|
146
|
+
can: 'employees/read',
|
|
147
|
+
with: 'myapp:company/acme',
|
|
148
|
+
nb: { limit: 25 }, // Must be ≤ delegated limit
|
|
149
|
+
},
|
|
150
|
+
proofs: [delegation], // Include delegation chain
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Serialize
|
|
154
|
+
const serialized = await serializeInvocation(invocation);
|
|
155
|
+
|
|
156
|
+
// Send to server
|
|
157
|
+
const response = await fetch('http://server/protected', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({ invocation: serialized }),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const result = await response.json();
|
|
164
|
+
console.log('Result:', result);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Complete React Example
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { useState, useCallback } from 'react';
|
|
171
|
+
import {
|
|
172
|
+
parseDelegation,
|
|
173
|
+
createInvocation,
|
|
174
|
+
serializeInvocation,
|
|
175
|
+
signerFromMnemonic,
|
|
176
|
+
} from '@ixo/ucan';
|
|
177
|
+
|
|
178
|
+
function ProtectedDataComponent() {
|
|
179
|
+
const [employees, setEmployees] = useState([]);
|
|
180
|
+
const [error, setError] = useState(null);
|
|
181
|
+
const [loading, setLoading] = useState(false);
|
|
182
|
+
|
|
183
|
+
// These would come from your app's state/storage
|
|
184
|
+
const userMnemonic = '...'; // User's mnemonic (from wallet)
|
|
185
|
+
const userDid = 'did:ixo:ixo1user...';
|
|
186
|
+
const delegationBase64 = '...'; // Stored delegation
|
|
187
|
+
const serverDid = 'did:ixo:ixo1server...';
|
|
188
|
+
|
|
189
|
+
const fetchEmployees = useCallback(async (limit: number) => {
|
|
190
|
+
setLoading(true);
|
|
191
|
+
setError(null);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// 1. Get user's signer from mnemonic
|
|
195
|
+
const { signer } = await signerFromMnemonic(userMnemonic, userDid);
|
|
196
|
+
|
|
197
|
+
// 2. Parse the delegation
|
|
198
|
+
const delegation = await parseDelegation(delegationBase64);
|
|
199
|
+
|
|
200
|
+
// 3. Create invocation with requested limit
|
|
201
|
+
const invocation = await createInvocation({
|
|
202
|
+
issuer: signer,
|
|
203
|
+
audience: serverDid,
|
|
204
|
+
capability: {
|
|
205
|
+
can: 'employees/read',
|
|
206
|
+
with: `myapp:${serverDid}`,
|
|
207
|
+
nb: { limit },
|
|
208
|
+
},
|
|
209
|
+
proofs: [delegation],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 4. Serialize and send
|
|
213
|
+
const serialized = await serializeInvocation(invocation);
|
|
214
|
+
|
|
215
|
+
const response = await fetch('/api/protected', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: { 'Content-Type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({ invocation: serialized }),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const err = await response.json();
|
|
223
|
+
throw new Error(err.details?.message || err.error || 'Request failed');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
setEmployees(data.employees);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
setError(err.message);
|
|
230
|
+
} finally {
|
|
231
|
+
setLoading(false);
|
|
232
|
+
}
|
|
233
|
+
}, [userMnemonic, userDid, delegationBase64, serverDid]);
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div>
|
|
237
|
+
<h2>Protected Data</h2>
|
|
238
|
+
|
|
239
|
+
<div>
|
|
240
|
+
<button onClick={() => fetchEmployees(10)} disabled={loading}>
|
|
241
|
+
Fetch 10 Employees
|
|
242
|
+
</button>
|
|
243
|
+
<button onClick={() => fetchEmployees(25)} disabled={loading}>
|
|
244
|
+
Fetch 25 Employees
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
{loading && <p>Loading...</p>}
|
|
249
|
+
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
|
250
|
+
|
|
251
|
+
{employees.length > 0 && (
|
|
252
|
+
<ul>
|
|
253
|
+
{employees.map((emp) => (
|
|
254
|
+
<li key={emp.id}>{emp.name}</li>
|
|
255
|
+
))}
|
|
256
|
+
</ul>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## IXO Client Pattern (did:ixo + ED Mnemonic)
|
|
264
|
+
|
|
265
|
+
IXO users have a `did:ixo` identity on-chain and use a separate ED25519 mnemonic for UCAN signing. The key pattern is using `signerFromMnemonic` with the `did:ixo` to ensure the signer's identity matches.
|
|
266
|
+
|
|
267
|
+
### Using signerFromMnemonic with did:ixo
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import {
|
|
271
|
+
signerFromMnemonic,
|
|
272
|
+
parseDelegation,
|
|
273
|
+
createInvocation,
|
|
274
|
+
serializeInvocation,
|
|
275
|
+
SupportedDID,
|
|
276
|
+
} from '@ixo/ucan';
|
|
277
|
+
|
|
278
|
+
// User's ED25519 signing mnemonic (stored/retrieved separately)
|
|
279
|
+
const edSigningMnemonic = 'word1 word2 word3 ...';
|
|
280
|
+
|
|
281
|
+
// User's on-chain DID
|
|
282
|
+
const userDid = 'did:ixo:ixo1abc123...';
|
|
283
|
+
|
|
284
|
+
// Derive signer with did:ixo identity (NOT the default did:key)
|
|
285
|
+
const { signer } = await signerFromMnemonic(
|
|
286
|
+
edSigningMnemonic,
|
|
287
|
+
userDid as SupportedDID // ⬅️ This wraps the signer with did:ixo
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
console.log(signer.did()); // "did:ixo:ixo1abc123..." (not did:key!)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Complete Invocation Example
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import {
|
|
297
|
+
signerFromMnemonic,
|
|
298
|
+
parseDelegation,
|
|
299
|
+
createInvocation,
|
|
300
|
+
serializeInvocation,
|
|
301
|
+
SupportedDID,
|
|
302
|
+
} from '@ixo/ucan';
|
|
303
|
+
|
|
304
|
+
async function invokeWithIxoDid(
|
|
305
|
+
edSigningMnemonic: string, // User's ED mnemonic
|
|
306
|
+
userDid: string, // User's did:ixo
|
|
307
|
+
delegationBase64: string, // Stored delegation
|
|
308
|
+
serverDid: string, // Server's DID
|
|
309
|
+
requestedLimit: number
|
|
310
|
+
) {
|
|
311
|
+
// 1. Derive signer with did:ixo identity
|
|
312
|
+
const { signer } = await signerFromMnemonic(
|
|
313
|
+
edSigningMnemonic,
|
|
314
|
+
userDid as SupportedDID
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// 2. Parse the delegation for proof
|
|
318
|
+
const delegation = await parseDelegation(delegationBase64);
|
|
319
|
+
|
|
320
|
+
// 3. Create invocation
|
|
321
|
+
const invocation = await createInvocation({
|
|
322
|
+
issuer: signer,
|
|
323
|
+
audience: serverDid,
|
|
324
|
+
capability: {
|
|
325
|
+
can: 'employees/read',
|
|
326
|
+
with: `myapp:${serverDid}`,
|
|
327
|
+
nb: { limit: requestedLimit },
|
|
328
|
+
},
|
|
329
|
+
proofs: [delegation],
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// 4. Serialize and send
|
|
333
|
+
const serialized = await serializeInvocation(invocation);
|
|
334
|
+
|
|
335
|
+
const response = await fetch('http://server/protected', {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({ invocation: serialized }),
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return response.json();
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Key Points
|
|
346
|
+
|
|
347
|
+
1. **Always pass `did:ixo` to `signerFromMnemonic`** - Without it, the signer defaults to `did:key` which won't match your delegation audience.
|
|
348
|
+
|
|
349
|
+
2. **The ED mnemonic is separate from wallet mnemonic** - IXO uses a dedicated ED25519 mnemonic for UCAN signing, not the main wallet mnemonic.
|
|
350
|
+
|
|
351
|
+
3. **Cast to `SupportedDID`** - TypeScript requires `userDid as SupportedDID` for type safety.
|
|
352
|
+
|
|
353
|
+
## Storing Delegations
|
|
354
|
+
|
|
355
|
+
Delegations should be stored securely. Options include:
|
|
356
|
+
|
|
357
|
+
### Browser LocalStorage (development only)
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// Store
|
|
361
|
+
localStorage.setItem('ucan-delegation', serializedDelegation);
|
|
362
|
+
|
|
363
|
+
// Load
|
|
364
|
+
const delegation = await parseDelegation(localStorage.getItem('ucan-delegation'));
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Error Handling
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
try {
|
|
371
|
+
const result = await validator.validate(invocation, capability, resource);
|
|
372
|
+
|
|
373
|
+
if (!result.ok) {
|
|
374
|
+
switch (result.error?.code) {
|
|
375
|
+
case 'INVALID_FORMAT':
|
|
376
|
+
// Malformed invocation
|
|
377
|
+
break;
|
|
378
|
+
case 'INVALID_SIGNATURE':
|
|
379
|
+
// Bad signature
|
|
380
|
+
break;
|
|
381
|
+
case 'UNAUTHORIZED':
|
|
382
|
+
// No valid delegation chain
|
|
383
|
+
break;
|
|
384
|
+
case 'CAVEAT_VIOLATION':
|
|
385
|
+
// Exceeded limits
|
|
386
|
+
break;
|
|
387
|
+
case 'REPLAY':
|
|
388
|
+
// Already used
|
|
389
|
+
break;
|
|
390
|
+
case 'EXPIRED':
|
|
391
|
+
// Delegation expired
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error('Unexpected error:', err);
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Tips
|
|
401
|
+
|
|
402
|
+
1. **Store private keys securely** - Never expose private keys in client code or logs
|
|
403
|
+
2. **Handle expiration** - Check delegation expiration before using; refresh if needed
|
|
404
|
+
3. **Use appropriate limits** - Don't request more than you need
|
|
405
|
+
4. **Bundle proofs** - Always include the full delegation chain in invocations
|
|
406
|
+
5. **Verify server DID** - Make sure `audience` matches the actual server DID
|
|
407
|
+
|