@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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @ixo/ucan@1.0.0 build /home/runner/actions-runner/_work/ixo-oracles-boilerplate/ixo-oracles-boilerplate/packages/ucan
2
+ > @ixo/ucan@1.0.1 build /home/runner/actions-runner/_work/ixo-oracles-boilerplate/ixo-oracles-boilerplate/packages/ucan
3
3
  > tsc
4
4
 
package/README.md CHANGED
@@ -1,189 +1,243 @@
1
1
  # @ixo/ucan
2
2
 
3
- UCAN (User Controlled Authorization Networks) implementation for IXO Services. This package wraps the battle-tested [ucanto](https://github.com/storacha/ucanto) library and provides IXO-specific extensions for DID resolution and MCP tool authorization.
3
+ A framework-agnostic UCAN (User Controlled Authorization Networks) implementation for any service. Built on top of the battle-tested [ucanto](https://github.com/storacha/ucanto) library and conforming to the [UCAN specification](https://github.com/ucan-wg/spec/).
4
+
5
+ ## What is UCAN?
6
+
7
+ UCAN is a decentralized authorization system using cryptographically signed tokens. Think of it as "JWT meets capabilities" - users can grant specific permissions to others, who can further delegate (but never escalate) those permissions.
8
+
9
+ **Key concepts:**
10
+ - **Capabilities**: What actions can be performed on which resources
11
+ - **Delegations**: Granting capabilities to others (can be chained)
12
+ - **Invocations**: Requests to use a capability
13
+ - **Attenuation**: Permissions can only be narrowed, never expanded
14
+
15
+ 📖 **[See the visual flow diagram →](./docs/FLOW.md)**
4
16
 
5
17
  ## Features
6
18
 
7
- - **did:ixo Resolution**: Resolve IXO DIDs to their public keys via the IXO blockchain indexer
8
- - **MCP Capability Definitions**: Pre-built capability schemas for MCP tool authorization
9
- - **Server & Client Helpers**: Easy setup for ucanto servers and clients with IXO defaults
10
- - **Replay Protection**: In-memory invocation store to prevent replay attacks
19
+ - 🔐 **Built on ucanto** - Battle-tested UCAN library from Storacha
20
+ - 🎯 **Generic Capabilities** - Define your own capabilities with custom schemas
21
+ - ⚙️ **Caveat Validation** - Enforce limits and restrictions on delegations
22
+ - 🌐 **Multi-DID Support** - `did:key` (native) + `did:ixo` (via blockchain indexer)
23
+ - 🚀 **Framework-Agnostic** - Works with Express, Fastify, Hono, NestJS, etc.
24
+ - 🛡️ **Replay Protection** - Built-in invocation store prevents replay attacks
11
25
 
12
26
  ## Installation
13
27
 
14
28
  ```bash
29
+ npm install @ixo/ucan
30
+ # or
15
31
  pnpm add @ixo/ucan
16
32
  ```
17
33
 
18
34
  ## Quick Start
19
35
 
20
- ### Server-Side (Oracle)
36
+ ### 1. Define a Capability
21
37
 
22
38
  ```typescript
23
- import { createIxoServer, MCPCall } from '@ixo/ucan';
24
- import { provide } from '@ucanto/server';
25
-
26
- // Create a handler for MCP tool calls
27
- const mcpCallHandler = provide(MCPCall, async ({ capability, invocation }) => {
28
- const { with: resourceUri } = capability;
39
+ import { defineCapability, Schema } from '@ixo/ucan';
40
+
41
+ const EmployeesRead = defineCapability({
42
+ can: 'employees/read',
43
+ protocol: 'myapp:',
44
+ nb: { limit: Schema.integer().optional() },
45
+ derives: (claimed, delegated) => {
46
+ const claimedLimit = claimed.nb?.limit ?? Infinity;
47
+ const delegatedLimit = delegated.nb?.limit ?? Infinity;
48
+
49
+ if (claimedLimit > delegatedLimit) {
50
+ return { error: new Error(`Limit ${claimedLimit} exceeds allowed ${delegatedLimit}`) };
51
+ }
52
+ return { ok: {} };
53
+ },
54
+ });
55
+ ```
29
56
 
30
- // Parse the resource URI to get server/tool info
31
- // Format: ixo:oracle:{oracleDid}:mcp/{serverName}/{toolName}
57
+ ### 2. Create a Validator (Server)
32
58
 
33
- // Execute the MCP tool...
34
- return { ok: { result: 'success' } };
59
+ ```typescript
60
+ import { createUCANValidator, createIxoDIDResolver } from '@ixo/ucan';
61
+
62
+ const validator = await createUCANValidator({
63
+ serverDid: 'did:ixo:ixo1abc...', // Your server's DID
64
+ rootIssuers: ['did:ixo:ixo1admin...'], // DIDs that can issue root capabilities
65
+ didResolver: createIxoDIDResolver({
66
+ indexerUrl: 'https://blocksync.ixo.earth/graphql',
67
+ }),
35
68
  });
69
+ ```
36
70
 
37
- // Create the server
38
- const { server, did } = createIxoServer({
39
- privateKey: process.env.ORACLE_PRIVATE_KEY!,
40
- rootIssuers: [process.env.ADMIN_DID!],
41
- indexerUrl: 'https://blocksync.ixo.earth/graphql',
42
- service: {
43
- mcp: { call: mcpCallHandler }
71
+ ### 3. Protect a Route
72
+
73
+ ```typescript
74
+ app.post('/employees', async (req, res) => {
75
+ const result = await validator.validate(
76
+ req.body.invocation, // Base64-encoded CAR
77
+ EmployeesRead,
78
+ 'myapp:company/acme'
79
+ );
80
+
81
+ if (!result.ok) {
82
+ return res.status(403).json({ error: result.error });
44
83
  }
45
- });
46
84
 
47
- console.log('Server DID:', did);
85
+ const limit = result.capability?.nb?.limit ?? 10;
86
+ res.json({ employees: getEmployees(limit) });
87
+ });
48
88
  ```
49
89
 
50
- ### Client-Side
90
+ ### 4. Create & Use a Delegation (Client)
51
91
 
52
92
  ```typescript
53
- import { createIxoClient, createMCPResourceURI } from '@ixo/ucan';
93
+ import { generateKeypair, createDelegation, createInvocation, serializeInvocation } from '@ixo/ucan';
94
+
95
+ // Generate a keypair for the user
96
+ const { signer, did } = await generateKeypair();
54
97
 
55
- // Create a client
56
- const client = createIxoClient({
57
- privateKey: myPrivateKey,
58
- serverDid: 'did:key:z6Mk...',
59
- endpoint: 'https://oracle.ixo.earth/ucan'
98
+ // Root creates a delegation for the user
99
+ const delegation = await createDelegation({
100
+ issuer: rootSigner,
101
+ audience: did,
102
+ capabilities: [{ can: 'employees/read', with: 'myapp:company/acme', nb: { limit: 50 } }],
103
+ expiration: Math.floor(Date.now() / 1000) + 3600, // 1 hour
60
104
  });
61
105
 
62
- // Invoke an MCP tool capability
63
- const result = await client.invoke({
64
- can: 'mcp/call',
65
- with: createMCPResourceURI('did:ixo:oracle123', 'postgres', 'query'),
66
- nb: { args: { sql: 'SELECT * FROM users' } }
106
+ // User creates an invocation
107
+ const invocation = await createInvocation({
108
+ issuer: signer,
109
+ audience: serverDid,
110
+ capability: { can: 'employees/read', with: 'myapp:company/acme', nb: { limit: 25 } },
111
+ proofs: [delegation],
67
112
  });
68
113
 
69
- // Or delegate capability to another user
70
- const delegation = await client.delegate({
71
- audience: 'did:ixo:alice',
72
- capabilities: [{
73
- can: 'mcp/call',
74
- with: createMCPResourceURI('did:ixo:oracle123', 'postgres', '*')
75
- }],
76
- expiration: Math.floor(Date.now() / 1000) + 86400 // 24 hours
114
+ // Serialize and send
115
+ const serialized = await serializeInvocation(invocation);
116
+ await fetch('/employees', {
117
+ method: 'POST',
118
+ body: JSON.stringify({ invocation: serialized }),
77
119
  });
78
120
  ```
79
121
 
80
- ## Capability URI Format
122
+ ## Documentation
81
123
 
82
- MCP capabilities use the following URI format:
124
+ | Document | Description |
125
+ |----------|-------------|
126
+ | **[Flow Diagram](./docs/FLOW.md)** | Visual explanation of UCAN delegation and invocation |
127
+ | **[Server Example](./docs/examples/SERVER.md)** | Complete Express server with protected routes |
128
+ | **[Client Example](./docs/examples/CLIENT.md)** | Frontend/client-side usage |
129
+ | **[Capabilities Guide](./docs/examples/CAPABILITIES.md)** | How to define custom capabilities with caveats |
83
130
 
84
- ```
85
- ixo:oracle:{oracleDid}:mcp/{serverName}/{toolName}
86
- ```
131
+ ## API Reference
87
132
 
88
- Examples:
89
- - `ixo:oracle:did:ixo:abc123:mcp/postgres/query` - Specific tool
90
- - `ixo:oracle:did:ixo:abc123:mcp/postgres/*` - All tools in server (wildcard)
91
- - `ixo:oracle:did:ixo:abc123:mcp/*` - All MCP tools (wildcard)
133
+ ### Capability Definition
92
134
 
93
- ## DID Resolution
135
+ ```typescript
136
+ defineCapability(options: DefineCapabilityOptions)
137
+ ```
94
138
 
95
- The package supports multiple DID methods:
139
+ Define a capability with optional caveat validation.
96
140
 
97
- - **did:key** - Handled natively by ucanto
98
- - **did:ixo** - Resolved via IXO blockchain indexer
141
+ | Option | Type | Description |
142
+ |--------|------|-------------|
143
+ | `can` | `string` | Action name (e.g., `'employees/read'`) |
144
+ | `protocol` | `string` | URI protocol (default: `'urn:'`) |
145
+ | `nb` | `object` | Schema for caveats using `Schema.*` |
146
+ | `derives` | `function` | Custom validation for attenuation |
99
147
 
100
- ```typescript
101
- import { createIxoDIDResolver, createCompositeDIDResolver } from '@ixo/ucan';
148
+ ### Validator
102
149
 
103
- const ixoResolver = createIxoDIDResolver({
104
- indexerUrl: 'https://blocksync.ixo.earth/graphql'
105
- });
106
-
107
- // Create a composite resolver for multiple DID methods
108
- const resolver = createCompositeDIDResolver([ixoResolver]);
150
+ ```typescript
151
+ createUCANValidator(options: CreateValidatorOptions): Promise<UCANValidator>
109
152
  ```
110
153
 
111
- ## Replay Protection
154
+ Create a framework-agnostic validator.
112
155
 
113
- The package includes an in-memory invocation store for replay protection:
156
+ | Option | Type | Description |
157
+ |--------|------|-------------|
158
+ | `serverDid` | `string` | Server's DID (any method supported) |
159
+ | `rootIssuers` | `string[]` | DIDs that can self-issue capabilities |
160
+ | `didResolver` | `DIDKeyResolver` | Resolver for non-`did:key` DIDs |
161
+ | `invocationStore` | `InvocationStore` | Custom store for replay protection |
114
162
 
115
- ```typescript
116
- import { InMemoryInvocationStore } from '@ixo/ucan';
163
+ ### Client Helpers
117
164
 
118
- const store = new InMemoryInvocationStore({
119
- defaultTtlMs: 24 * 60 * 60 * 1000, // 24 hours
120
- cleanupIntervalMs: 60 * 60 * 1000, // 1 hour
121
- });
165
+ | Function | Description |
166
+ |----------|-------------|
167
+ | `generateKeypair()` | Generate new Ed25519 keypair |
168
+ | `parseSigner(privateKey, did?)` | Parse private key into signer |
169
+ | `signerFromMnemonic(mnemonic, did?)` | Derive signer from BIP39 mnemonic |
170
+ | `createDelegation(options)` | Create a delegation |
171
+ | `createInvocation(options)` | Create an invocation |
172
+ | `serializeDelegation(delegation)` | Serialize to base64 CAR |
173
+ | `serializeInvocation(invocation)` | Serialize to base64 CAR |
174
+ | `parseDelegation(serialized)` | Parse from base64 CAR |
122
175
 
123
- // Check if invocation was already used
124
- if (await store.has(invocationCid)) {
125
- throw new Error('Replay attack detected');
126
- }
176
+ ### DID Resolution
127
177
 
128
- // Mark as used
129
- await store.add(invocationCid);
178
+ ```typescript
179
+ createIxoDIDResolver(config: IxoDIDResolverConfig): DIDKeyResolver
180
+ createCompositeDIDResolver(resolvers: DIDKeyResolver[]): DIDKeyResolver
130
181
  ```
131
182
 
132
- ## Integration with Oracle
183
+ ### Replay Protection
133
184
 
134
- See the oracle app's `src/ucan/` directory for a complete integration example:
185
+ ```typescript
186
+ new InMemoryInvocationStore(options?)
187
+ createInvocationStore(options?)
188
+ ```
135
189
 
136
- - `ucan.config.ts` - Configuration for MCP UCAN requirements
137
- - `ucan.service.ts` - NestJS service for validation
138
- - `ucan.module.ts` - NestJS module
190
+ ## DID Support
139
191
 
140
- ## Environment Variables
192
+ | DID Method | Support | Notes |
193
+ |------------|---------|-------|
194
+ | `did:key` | ✅ Native | Parsed directly from the identifier |
195
+ | `did:ixo` | ✅ Via resolver | Resolved via IXO blockchain indexer |
196
+ | `did:web` | 🔧 Extendable | Implement custom resolver |
141
197
 
142
- For the IXO DID resolver:
198
+ ## Environment Variables
143
199
 
144
200
  ```env
201
+ # For IXO DID resolution
145
202
  BLOCKSYNC_GRAPHQL_URL=https://blocksync.ixo.earth/graphql
146
203
  ```
147
204
 
148
- For UCAN configuration:
205
+ ## Advanced Usage
149
206
 
150
- ```env
151
- ORACLE_ENTITY_DID=did:ixo:your-oracle-did
152
- UCAN_ROOT_ISSUERS=did:ixo:admin1,did:ixo:admin2
153
- UCAN_PROTECTED_MCP_SERVERS=postgres,redis
207
+ ### Re-exported ucanto Packages
208
+
209
+ For advanced use cases, you can access the underlying ucanto packages:
210
+
211
+ ```typescript
212
+ import { UcantoServer, UcantoClient, UcantoValidator, ed25519 } from '@ixo/ucan';
154
213
  ```
155
214
 
156
- ## API Reference
215
+ ### Custom Invocation Store
157
216
 
158
- ### Types
217
+ Implement the `InvocationStore` interface for distributed deployments:
159
218
 
160
- - `IxoDID` - IXO DID type (`did:ixo:${string}`)
161
- - `KeyDID` - Key DID type (`did:key:${string}`)
162
- - `MCPResourceURI` - MCP resource URI type
163
- - `MCPUCANConfig` - Configuration for MCP UCAN requirements
164
- - `InvocationStore` - Interface for replay protection stores
165
- - `DIDKeyResolver` - DID resolver function type
219
+ ```typescript
220
+ interface InvocationStore {
221
+ has(cid: string): Promise<boolean>;
222
+ add(cid: string, ttlMs?: number): Promise<void>;
223
+ cleanup?(): Promise<void>;
224
+ }
225
+ ```
166
226
 
167
- ### Functions
227
+ ## Contributing
168
228
 
169
- - `createIxoServer(options)` - Create a ucanto server with IXO defaults
170
- - `createIxoClient(options)` - Create a ucanto client with IXO defaults
171
- - `createIxoDIDResolver(config)` - Create a did:ixo resolver
172
- - `createCompositeDIDResolver(resolvers)` - Combine multiple DID resolvers
173
- - `createMCPResourceURI(oracleDid, serverName, toolName)` - Build MCP resource URI
174
- - `parseMCPResourceURI(uri)` - Parse MCP resource URI into components
175
- - `createInvocationStore(options)` - Create an invocation store
229
+ See the [test script](./scripts/test-ucan.ts) for a complete example of the UCAN flow:
176
230
 
177
- ### Capabilities
231
+ ```bash
232
+ pnpm test:ucan
233
+ ```
178
234
 
179
- - `MCPCall` - Capability for calling MCP tools
235
+ ## License
180
236
 
181
- ## TODO
237
+ MIT
182
238
 
183
- - [ ] Add Redis implementation for distributed deployments
184
- - [ ] Add SQLite implementation for persistence
185
- - [ ] Add delegation management utilities
186
- - [ ] Add capability inspection utilities
187
- - [ ] Add support for capability revocation lists
188
- - [ ] Add support for time-based restrictions in caveats
239
+ ## Links
189
240
 
241
+ - [ucanto (underlying library)](https://github.com/storacha/ucanto)
242
+ - [UCAN Specification](https://github.com/ucan-wg/spec/)
243
+ - [IXO Network](https://www.ixo.world/)
package/docs/FLOW.md ADDED
@@ -0,0 +1,275 @@
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
+ - Server doesn't need external delegation store
188
+ - Validation is entirely local
189
+ - No network calls during validation (except DID resolution)
190
+
191
+ ## Replay Protection
192
+
193
+ Each invocation has a unique CID. The server stores used CIDs:
194
+
195
+ ```
196
+ ┌──────────────────────────────────────────────────────────────┐
197
+ │ REPLAY PROTECTION │
198
+ ├──────────────────────────────────────────────────────────────┤
199
+ │ │
200
+ │ First request: │
201
+ │ Invocation CID: bafy...abc │
202
+ │ → Not in store → PROCESS → Add to store ✅ │
203
+ │ │
204
+ │ Replay attempt: │
205
+ │ Invocation CID: bafy...abc (same!) │
206
+ │ → Already in store → REJECT ❌ │
207
+ │ │
208
+ └──────────────────────────────────────────────────────────────┘
209
+ ```
210
+
211
+ ## Code Example
212
+
213
+ ```typescript
214
+ import {
215
+ defineCapability,
216
+ createUCANValidator,
217
+ createDelegation,
218
+ createInvocation,
219
+ Schema
220
+ } from '@ixo/ucan';
221
+
222
+ // 1. Define capability with caveat
223
+ const EmployeesRead = defineCapability({
224
+ can: 'employees/read',
225
+ protocol: 'myapp:',
226
+ nb: { limit: Schema.integer().optional() },
227
+ derives: (claimed, delegated) => {
228
+ const claimedLimit = claimed.nb?.limit ?? Infinity;
229
+ const delegatedLimit = delegated.nb?.limit ?? Infinity;
230
+ if (claimedLimit > delegatedLimit) {
231
+ return { error: new Error('Limit exceeds delegation') };
232
+ }
233
+ return { ok: {} };
234
+ },
235
+ });
236
+
237
+ // 2. Root delegates to Alice with limit: 50
238
+ const rootToAlice = await createDelegation({
239
+ issuer: rootSigner,
240
+ audience: aliceDid,
241
+ capabilities: [{ can: 'employees/read', with: 'myapp:company', nb: { limit: 50 } }],
242
+ });
243
+
244
+ // 3. Alice re-delegates to Bob with limit: 25
245
+ const aliceToBob = await createDelegation({
246
+ issuer: aliceSigner,
247
+ audience: bobDid,
248
+ capabilities: [{ can: 'employees/read', with: 'myapp:company', nb: { limit: 25 } }],
249
+ proofs: [rootToAlice], // Include proof chain
250
+ });
251
+
252
+ // 4. Bob invokes with limit: 20
253
+ const invocation = await createInvocation({
254
+ issuer: bobSigner,
255
+ audience: serverDid,
256
+ capability: { can: 'employees/read', with: 'myapp:company', nb: { limit: 20 } },
257
+ proofs: [aliceToBob], // Includes entire chain
258
+ });
259
+
260
+ // 5. Server validates
261
+ const result = await validator.validate(serialized, EmployeesRead, 'myapp:company');
262
+ // result.ok === true, result.capability.nb.limit === 20
263
+ ```
264
+
265
+ ## Summary
266
+
267
+ | Concept | Description |
268
+ |---------|-------------|
269
+ | **Delegation** | Granting capabilities to others (signed by issuer) |
270
+ | **Attenuation** | Narrowing permissions when re-delegating |
271
+ | **Invocation** | Request to use a capability (signed by invoker) |
272
+ | **Proof Chain** | Delegations linked together, bundled in invocation |
273
+ | **Caveat** | Restrictions on capabilities (e.g., `limit`) |
274
+ | **Replay Protection** | CID-based tracking prevents reuse |
275
+