@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.
@@ -0,0 +1,418 @@
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 {
131
+ createInvocation,
132
+ serializeInvocation,
133
+ parseDelegation,
134
+ parseSigner,
135
+ } from '@ixo/ucan';
136
+
137
+ // Load your delegation
138
+ const delegation = await parseDelegation(mySerializedDelegation);
139
+
140
+ // Your signer
141
+ const signer = parseSigner('MgCY...');
142
+
143
+ // Server's DID (from /info endpoint or known)
144
+ const serverDid = 'did:ixo:ixo1server...';
145
+
146
+ // Create invocation
147
+ const invocation = await createInvocation({
148
+ issuer: signer,
149
+ audience: serverDid,
150
+ capability: {
151
+ can: 'employees/read',
152
+ with: 'myapp:company/acme',
153
+ nb: { limit: 25 }, // Must be ≤ delegated limit
154
+ },
155
+ proofs: [delegation], // Include delegation chain
156
+ });
157
+
158
+ // Serialize
159
+ const serialized = await serializeInvocation(invocation);
160
+
161
+ // Send to server
162
+ const response = await fetch('http://server/protected', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ invocation: serialized }),
166
+ });
167
+
168
+ const result = await response.json();
169
+ console.log('Result:', result);
170
+ ```
171
+
172
+ ## Complete React Example
173
+
174
+ ```tsx
175
+ import { useState, useCallback } from 'react';
176
+ import {
177
+ parseDelegation,
178
+ createInvocation,
179
+ serializeInvocation,
180
+ signerFromMnemonic,
181
+ } from '@ixo/ucan';
182
+
183
+ function ProtectedDataComponent() {
184
+ const [employees, setEmployees] = useState([]);
185
+ const [error, setError] = useState(null);
186
+ const [loading, setLoading] = useState(false);
187
+
188
+ // These would come from your app's state/storage
189
+ const userMnemonic = '...'; // User's mnemonic (from wallet)
190
+ const userDid = 'did:ixo:ixo1user...';
191
+ const delegationBase64 = '...'; // Stored delegation
192
+ const serverDid = 'did:ixo:ixo1server...';
193
+
194
+ const fetchEmployees = useCallback(
195
+ async (limit: number) => {
196
+ setLoading(true);
197
+ setError(null);
198
+
199
+ try {
200
+ // 1. Get user's signer from mnemonic
201
+ const { signer } = await signerFromMnemonic(userMnemonic, userDid);
202
+
203
+ // 2. Parse the delegation
204
+ const delegation = await parseDelegation(delegationBase64);
205
+
206
+ // 3. Create invocation with requested limit
207
+ const invocation = await createInvocation({
208
+ issuer: signer,
209
+ audience: serverDid,
210
+ capability: {
211
+ can: 'employees/read',
212
+ with: `myapp:${serverDid}`,
213
+ nb: { limit },
214
+ },
215
+ proofs: [delegation],
216
+ });
217
+
218
+ // 4. Serialize and send
219
+ const serialized = await serializeInvocation(invocation);
220
+
221
+ const response = await fetch('/api/protected', {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify({ invocation: serialized }),
225
+ });
226
+
227
+ if (!response.ok) {
228
+ const err = await response.json();
229
+ throw new Error(
230
+ err.details?.message || err.error || 'Request failed',
231
+ );
232
+ }
233
+
234
+ const data = await response.json();
235
+ setEmployees(data.employees);
236
+ } catch (err) {
237
+ setError(err.message);
238
+ } finally {
239
+ setLoading(false);
240
+ }
241
+ },
242
+ [userMnemonic, userDid, delegationBase64, serverDid],
243
+ );
244
+
245
+ return (
246
+ <div>
247
+ <h2>Protected Data</h2>
248
+
249
+ <div>
250
+ <button onClick={() => fetchEmployees(10)} disabled={loading}>
251
+ Fetch 10 Employees
252
+ </button>
253
+ <button onClick={() => fetchEmployees(25)} disabled={loading}>
254
+ Fetch 25 Employees
255
+ </button>
256
+ </div>
257
+
258
+ {loading && <p>Loading...</p>}
259
+ {error && <p style={{ color: 'red' }}>Error: {error}</p>}
260
+
261
+ {employees.length > 0 && (
262
+ <ul>
263
+ {employees.map((emp) => (
264
+ <li key={emp.id}>{emp.name}</li>
265
+ ))}
266
+ </ul>
267
+ )}
268
+ </div>
269
+ );
270
+ }
271
+ ```
272
+
273
+ ## IXO Client Pattern (did:ixo + ED Mnemonic)
274
+
275
+ 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.
276
+
277
+ ### Using signerFromMnemonic with did:ixo
278
+
279
+ ```typescript
280
+ import {
281
+ signerFromMnemonic,
282
+ parseDelegation,
283
+ createInvocation,
284
+ serializeInvocation,
285
+ SupportedDID,
286
+ } from '@ixo/ucan';
287
+
288
+ // User's ED25519 signing mnemonic (stored/retrieved separately)
289
+ const edSigningMnemonic = 'word1 word2 word3 ...';
290
+
291
+ // User's on-chain DID
292
+ const userDid = 'did:ixo:ixo1abc123...';
293
+
294
+ // Derive signer with did:ixo identity (NOT the default did:key)
295
+ const { signer } = await signerFromMnemonic(
296
+ edSigningMnemonic,
297
+ userDid as SupportedDID, // ⬅️ This wraps the signer with did:ixo
298
+ );
299
+
300
+ console.log(signer.did()); // "did:ixo:ixo1abc123..." (not did:key!)
301
+ ```
302
+
303
+ ### Complete Invocation Example
304
+
305
+ ```typescript
306
+ import {
307
+ signerFromMnemonic,
308
+ parseDelegation,
309
+ createInvocation,
310
+ serializeInvocation,
311
+ SupportedDID,
312
+ } from '@ixo/ucan';
313
+
314
+ async function invokeWithIxoDid(
315
+ edSigningMnemonic: string, // User's ED mnemonic
316
+ userDid: string, // User's did:ixo
317
+ delegationBase64: string, // Stored delegation
318
+ serverDid: string, // Server's DID
319
+ requestedLimit: number,
320
+ ) {
321
+ // 1. Derive signer with did:ixo identity
322
+ const { signer } = await signerFromMnemonic(
323
+ edSigningMnemonic,
324
+ userDid as SupportedDID,
325
+ );
326
+
327
+ // 2. Parse the delegation for proof
328
+ const delegation = await parseDelegation(delegationBase64);
329
+
330
+ // 3. Create invocation
331
+ const invocation = await createInvocation({
332
+ issuer: signer,
333
+ audience: serverDid,
334
+ capability: {
335
+ can: 'employees/read',
336
+ with: `myapp:${serverDid}`,
337
+ nb: { limit: requestedLimit },
338
+ },
339
+ proofs: [delegation],
340
+ });
341
+
342
+ // 4. Serialize and send
343
+ const serialized = await serializeInvocation(invocation);
344
+
345
+ const response = await fetch('http://server/protected', {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/json' },
348
+ body: JSON.stringify({ invocation: serialized }),
349
+ });
350
+
351
+ return response.json();
352
+ }
353
+ ```
354
+
355
+ ### Key Points
356
+
357
+ 1. **Always pass `did:ixo` to `signerFromMnemonic`** - Without it, the signer defaults to `did:key` which won't match your delegation audience.
358
+
359
+ 2. **The ED mnemonic is separate from wallet mnemonic** - IXO uses a dedicated ED25519 mnemonic for UCAN signing, not the main wallet mnemonic.
360
+
361
+ 3. **Cast to `SupportedDID`** - TypeScript requires `userDid as SupportedDID` for type safety.
362
+
363
+ ## Storing Delegations
364
+
365
+ Delegations should be stored securely. Options include:
366
+
367
+ ### Browser LocalStorage (development only)
368
+
369
+ ```typescript
370
+ // Store
371
+ localStorage.setItem('ucan-delegation', serializedDelegation);
372
+
373
+ // Load
374
+ const delegation = await parseDelegation(
375
+ localStorage.getItem('ucan-delegation'),
376
+ );
377
+ ```
378
+
379
+ ## Error Handling
380
+
381
+ ```typescript
382
+ try {
383
+ const result = await validator.validate(invocation, capability, resource);
384
+
385
+ if (!result.ok) {
386
+ switch (result.error?.code) {
387
+ case 'INVALID_FORMAT':
388
+ // Malformed invocation
389
+ break;
390
+ case 'INVALID_SIGNATURE':
391
+ // Bad signature
392
+ break;
393
+ case 'UNAUTHORIZED':
394
+ // No valid delegation chain
395
+ break;
396
+ case 'CAVEAT_VIOLATION':
397
+ // Exceeded limits
398
+ break;
399
+ case 'REPLAY':
400
+ // Already used
401
+ break;
402
+ case 'EXPIRED':
403
+ // Delegation expired
404
+ break;
405
+ }
406
+ }
407
+ } catch (err) {
408
+ console.error('Unexpected error:', err);
409
+ }
410
+ ```
411
+
412
+ ## Tips
413
+
414
+ 1. **Store private keys securely** - Never expose private keys in client code or logs
415
+ 2. **Handle expiration** - Check delegation expiration before using; refresh if needed
416
+ 3. **Use appropriate limits** - Don't request more than you need
417
+ 4. **Bundle proofs** - Always include the full delegation chain in invocations
418
+ 5. **Verify server DID** - Make sure `audience` matches the actual server DID