@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
package/.turbo/turbo-build.log
CHANGED
package/README.md
CHANGED
|
@@ -1,189 +1,243 @@
|
|
|
1
1
|
# @ixo/ucan
|
|
2
2
|
|
|
3
|
-
UCAN (User Controlled Authorization Networks) implementation for
|
|
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
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
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
|
-
###
|
|
36
|
+
### 1. Define a Capability
|
|
21
37
|
|
|
22
38
|
```typescript
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
// Format: ixo:oracle:{oracleDid}:mcp/{serverName}/{toolName}
|
|
57
|
+
### 2. Create a Validator (Server)
|
|
32
58
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
85
|
+
const limit = result.capability?.nb?.limit ?? 10;
|
|
86
|
+
res.json({ employees: getEmployees(limit) });
|
|
87
|
+
});
|
|
48
88
|
```
|
|
49
89
|
|
|
50
|
-
### Client
|
|
90
|
+
### 4. Create & Use a Delegation (Client)
|
|
51
91
|
|
|
52
92
|
```typescript
|
|
53
|
-
import {
|
|
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
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
122
|
+
## Documentation
|
|
81
123
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
defineCapability(options: DefineCapabilityOptions)
|
|
137
|
+
```
|
|
94
138
|
|
|
95
|
-
|
|
139
|
+
Define a capability with optional caveat validation.
|
|
96
140
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
import { createIxoDIDResolver, createCompositeDIDResolver } from '@ixo/ucan';
|
|
148
|
+
### Validator
|
|
102
149
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
154
|
+
Create a framework-agnostic validator.
|
|
112
155
|
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
import { InMemoryInvocationStore } from '@ixo/ucan';
|
|
163
|
+
### Client Helpers
|
|
117
164
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
if (await store.has(invocationCid)) {
|
|
125
|
-
throw new Error('Replay attack detected');
|
|
126
|
-
}
|
|
176
|
+
### DID Resolution
|
|
127
177
|
|
|
128
|
-
|
|
129
|
-
|
|
178
|
+
```typescript
|
|
179
|
+
createIxoDIDResolver(config: IxoDIDResolverConfig): DIDKeyResolver
|
|
180
|
+
createCompositeDIDResolver(resolvers: DIDKeyResolver[]): DIDKeyResolver
|
|
130
181
|
```
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
### Replay Protection
|
|
133
184
|
|
|
134
|
-
|
|
185
|
+
```typescript
|
|
186
|
+
new InMemoryInvocationStore(options?)
|
|
187
|
+
createInvocationStore(options?)
|
|
188
|
+
```
|
|
135
189
|
|
|
136
|
-
|
|
137
|
-
- `ucan.service.ts` - NestJS service for validation
|
|
138
|
-
- `ucan.module.ts` - NestJS module
|
|
190
|
+
## DID Support
|
|
139
191
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
## Advanced Usage
|
|
149
206
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
215
|
+
### Custom Invocation Store
|
|
157
216
|
|
|
158
|
-
|
|
217
|
+
Implement the `InvocationStore` interface for distributed deployments:
|
|
159
218
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
227
|
+
## Contributing
|
|
168
228
|
|
|
169
|
-
|
|
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
|
-
|
|
231
|
+
```bash
|
|
232
|
+
pnpm test:ucan
|
|
233
|
+
```
|
|
178
234
|
|
|
179
|
-
|
|
235
|
+
## License
|
|
180
236
|
|
|
181
|
-
|
|
237
|
+
MIT
|
|
182
238
|
|
|
183
|
-
|
|
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
|
+
|