@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.
@@ -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
+