@kya-os/mcp-i 1.4.0 → 1.5.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/dist/runtime/auth-handshake.d.ts +144 -0
- package/dist/runtime/auth-handshake.js +254 -0
- package/dist/runtime/delegation-verifier-agentshield.d.ts +64 -0
- package/dist/runtime/delegation-verifier-agentshield.js +301 -0
- package/dist/runtime/delegation-verifier-kv.d.ts +51 -0
- package/dist/runtime/delegation-verifier-kv.js +280 -0
- package/dist/runtime/delegation-verifier-memory.d.ts +50 -0
- package/dist/runtime/delegation-verifier-memory.js +128 -0
- package/dist/runtime/delegation-verifier.d.ts +133 -0
- package/dist/runtime/delegation-verifier.js +107 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +24 -1
- package/dist/runtime/mcpi-runtime.d.ts +31 -1
- package/dist/runtime/mcpi-runtime.js +46 -1
- package/dist/runtime/proof-batch-queue.d.ts +117 -0
- package/dist/runtime/proof-batch-queue.js +257 -0
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AgentShield API Delegation Verifier
|
|
4
|
+
*
|
|
5
|
+
* Queries delegations from AgentShield managed service API.
|
|
6
|
+
* Includes local caching to minimize network calls and maximize performance.
|
|
7
|
+
*
|
|
8
|
+
* Performance:
|
|
9
|
+
* - Fast path (cached): < 5ms
|
|
10
|
+
* - Slow path (API call): < 100ms
|
|
11
|
+
* - Cache TTL: 1 minute (configurable)
|
|
12
|
+
*
|
|
13
|
+
* API Endpoints:
|
|
14
|
+
* - POST /api/v1/delegations/verify - Verify delegation by agent DID + scopes
|
|
15
|
+
* - GET /api/v1/delegations/:id - Get delegation by ID
|
|
16
|
+
* - POST /api/v1/delegations - Create new delegation (admin only)
|
|
17
|
+
* - POST /api/v1/delegations/:id/revoke - Revoke delegation (admin only)
|
|
18
|
+
*
|
|
19
|
+
* Authentication: Bearer token via X-API-Key header
|
|
20
|
+
*
|
|
21
|
+
* Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.3
|
|
22
|
+
* Related: AGENTSHIELD_DASHBOARD_PLAN.md Epic 2 (Public API)
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.AgentShieldAPIDelegationVerifier = void 0;
|
|
26
|
+
const delegation_1 = require("@kya-os/contracts/delegation");
|
|
27
|
+
const delegation_verifier_1 = require("./delegation-verifier");
|
|
28
|
+
/**
|
|
29
|
+
* Simple in-memory cache (same as KV verifier)
|
|
30
|
+
*/
|
|
31
|
+
class DelegationCache {
|
|
32
|
+
cache = new Map();
|
|
33
|
+
maxSize;
|
|
34
|
+
constructor(maxSize = 1000) {
|
|
35
|
+
this.maxSize = maxSize;
|
|
36
|
+
}
|
|
37
|
+
get(key) {
|
|
38
|
+
const entry = this.cache.get(key);
|
|
39
|
+
if (!entry)
|
|
40
|
+
return null;
|
|
41
|
+
if (Date.now() > entry.expiresAt) {
|
|
42
|
+
this.cache.delete(key);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return entry.data;
|
|
46
|
+
}
|
|
47
|
+
set(key, data, ttlMs) {
|
|
48
|
+
if (this.cache.size >= this.maxSize) {
|
|
49
|
+
const firstKey = this.cache.keys().next().value;
|
|
50
|
+
if (firstKey)
|
|
51
|
+
this.cache.delete(firstKey);
|
|
52
|
+
}
|
|
53
|
+
this.cache.set(key, {
|
|
54
|
+
data,
|
|
55
|
+
expiresAt: Date.now() + ttlMs,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
delete(key) {
|
|
59
|
+
this.cache.delete(key);
|
|
60
|
+
}
|
|
61
|
+
clear() {
|
|
62
|
+
this.cache.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* AgentShield API Delegation Verifier
|
|
67
|
+
*
|
|
68
|
+
* Managed mode: Queries delegations from AgentShield dashboard API
|
|
69
|
+
*/
|
|
70
|
+
class AgentShieldAPIDelegationVerifier {
|
|
71
|
+
apiUrl;
|
|
72
|
+
apiKey;
|
|
73
|
+
cache;
|
|
74
|
+
cacheTtl;
|
|
75
|
+
debug;
|
|
76
|
+
constructor(config) {
|
|
77
|
+
if (!config.agentshield?.apiUrl || !config.agentshield?.apiKey) {
|
|
78
|
+
throw new Error('AgentShieldAPIDelegationVerifier requires agentshield.apiUrl and agentshield.apiKey in config');
|
|
79
|
+
}
|
|
80
|
+
this.apiUrl = config.agentshield.apiUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
81
|
+
this.apiKey = config.agentshield.apiKey;
|
|
82
|
+
this.cache = new DelegationCache();
|
|
83
|
+
this.cacheTtl = config.cacheTtl || 60_000; // Default 1 minute
|
|
84
|
+
this.debug = config.debug || false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Verify agent delegation via API
|
|
88
|
+
*/
|
|
89
|
+
async verify(agentDid, scopes, options) {
|
|
90
|
+
const startTime = Date.now();
|
|
91
|
+
// Build cache key
|
|
92
|
+
const scopesKey = this.buildScopesKey(scopes);
|
|
93
|
+
const cacheKey = `verify:${agentDid}:${scopesKey}`;
|
|
94
|
+
// Fast path: Check cache
|
|
95
|
+
if (!options?.skipCache) {
|
|
96
|
+
const cached = this.cache.get(cacheKey);
|
|
97
|
+
if (cached) {
|
|
98
|
+
if (this.debug) {
|
|
99
|
+
console.log(`[AgentShield] Cache HIT for ${agentDid} (${Date.now() - startTime}ms)`);
|
|
100
|
+
}
|
|
101
|
+
return { ...cached, cached: true };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Slow path: API call
|
|
105
|
+
if (this.debug) {
|
|
106
|
+
console.log(`[AgentShield] Cache MISS for ${agentDid}, calling API...`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(`${this.apiUrl}/api/v1/delegations/verify`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'X-API-Key': this.apiKey,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
agentDid,
|
|
117
|
+
scopes,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
// Handle API errors
|
|
122
|
+
if (response.status === 404) {
|
|
123
|
+
const result = {
|
|
124
|
+
valid: false,
|
|
125
|
+
reason: 'No delegation found',
|
|
126
|
+
cached: false,
|
|
127
|
+
};
|
|
128
|
+
this.cache.set(cacheKey, result, this.cacheTtl / 2);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
if (response.status === 401 || response.status === 403) {
|
|
132
|
+
throw new Error(`AgentShield API authentication failed: ${response.status}`);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`);
|
|
135
|
+
}
|
|
136
|
+
const data = await response.json();
|
|
137
|
+
// Validate response
|
|
138
|
+
if (data.delegation) {
|
|
139
|
+
const parsed = delegation_1.DelegationRecordSchema.safeParse(data.delegation);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
console.error('[AgentShield] Invalid delegation in API response:', parsed.error);
|
|
142
|
+
data.valid = false;
|
|
143
|
+
data.reason = 'Invalid delegation format';
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Re-validate locally (trust but verify)
|
|
147
|
+
const validation = (0, delegation_verifier_1.validateDelegation)(parsed.data);
|
|
148
|
+
if (!validation.valid) {
|
|
149
|
+
data.valid = false;
|
|
150
|
+
data.reason = validation.reason;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const result = {
|
|
155
|
+
valid: data.valid,
|
|
156
|
+
delegation: data.delegation,
|
|
157
|
+
reason: data.reason,
|
|
158
|
+
cached: false,
|
|
159
|
+
};
|
|
160
|
+
// Cache result
|
|
161
|
+
const ttl = data.valid ? this.cacheTtl : this.cacheTtl / 2;
|
|
162
|
+
this.cache.set(cacheKey, result, ttl);
|
|
163
|
+
if (this.debug) {
|
|
164
|
+
console.log(`[AgentShield] Delegation ${data.valid ? 'verified' : 'rejected'} (${Date.now() - startTime}ms)`);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error('[AgentShield] API call failed:', error);
|
|
170
|
+
// Return negative result on error (don't cache failures)
|
|
171
|
+
return {
|
|
172
|
+
valid: false,
|
|
173
|
+
reason: `API error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
174
|
+
cached: false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get delegation by ID via API
|
|
180
|
+
*/
|
|
181
|
+
async get(delegationId) {
|
|
182
|
+
const cacheKey = `delegation:${delegationId}`;
|
|
183
|
+
// Check cache
|
|
184
|
+
const cached = this.cache.get(cacheKey);
|
|
185
|
+
if (cached)
|
|
186
|
+
return cached;
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch(`${this.apiUrl}/api/v1/delegations/${delegationId}`, {
|
|
189
|
+
method: 'GET',
|
|
190
|
+
headers: {
|
|
191
|
+
'X-API-Key': this.apiKey,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
if (response.status === 404)
|
|
196
|
+
return null;
|
|
197
|
+
throw new Error(`AgentShield API error: ${response.status}`);
|
|
198
|
+
}
|
|
199
|
+
const data = await response.json();
|
|
200
|
+
const parsed = delegation_1.DelegationRecordSchema.safeParse(data);
|
|
201
|
+
if (!parsed.success) {
|
|
202
|
+
console.error('[AgentShield] Invalid delegation in API response:', parsed.error);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const delegation = parsed.data;
|
|
206
|
+
// Cache it
|
|
207
|
+
this.cache.set(cacheKey, delegation, this.cacheTtl);
|
|
208
|
+
return delegation;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error('[AgentShield] Failed to get delegation:', error);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Store delegation (admin operation via API)
|
|
217
|
+
*
|
|
218
|
+
* Note: This is typically done via AgentShield dashboard UI,
|
|
219
|
+
* not by the bouncer directly. Included for completeness.
|
|
220
|
+
*/
|
|
221
|
+
async put(delegation) {
|
|
222
|
+
// Validate first
|
|
223
|
+
const parsed = delegation_1.DelegationRecordSchema.safeParse(delegation);
|
|
224
|
+
if (!parsed.success) {
|
|
225
|
+
throw new Error(`Invalid delegation record: ${parsed.error.message}`);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(`${this.apiUrl}/api/v1/delegations`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
'X-API-Key': this.apiKey,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(delegation),
|
|
235
|
+
});
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`);
|
|
238
|
+
}
|
|
239
|
+
// Invalidate cache
|
|
240
|
+
const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation);
|
|
241
|
+
const scopesKey = this.buildScopesKey(delegationScopes);
|
|
242
|
+
this.cache.delete(`delegation:${delegation.id}`);
|
|
243
|
+
this.cache.delete(`verify:${delegation.subjectDid}:${scopesKey}`);
|
|
244
|
+
if (this.debug) {
|
|
245
|
+
console.log(`[AgentShield] Stored delegation ${delegation.id}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.error('[AgentShield] Failed to store delegation:', error);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Revoke delegation via API
|
|
255
|
+
*/
|
|
256
|
+
async revoke(delegationId, reason) {
|
|
257
|
+
try {
|
|
258
|
+
const response = await fetch(`${this.apiUrl}/api/v1/delegations/${delegationId}/revoke`, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
'X-API-Key': this.apiKey,
|
|
263
|
+
},
|
|
264
|
+
body: JSON.stringify({ reason }),
|
|
265
|
+
});
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`);
|
|
268
|
+
}
|
|
269
|
+
// Invalidate cache
|
|
270
|
+
this.cache.delete(`delegation:${delegationId}`);
|
|
271
|
+
if (this.debug) {
|
|
272
|
+
console.log(`[AgentShield] Revoked delegation ${delegationId}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.error('[AgentShield] Failed to revoke delegation:', error);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Close connections (cleanup)
|
|
282
|
+
*/
|
|
283
|
+
async close() {
|
|
284
|
+
this.cache.clear();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Build deterministic scopes key for caching
|
|
288
|
+
*/
|
|
289
|
+
buildScopesKey(scopes) {
|
|
290
|
+
const sorted = [...scopes].sort();
|
|
291
|
+
const str = sorted.join(',');
|
|
292
|
+
let hash = 0;
|
|
293
|
+
for (let i = 0; i < str.length; i++) {
|
|
294
|
+
const char = str.charCodeAt(i);
|
|
295
|
+
hash = (hash << 5) - hash + char;
|
|
296
|
+
hash = hash & hash;
|
|
297
|
+
}
|
|
298
|
+
return Math.abs(hash).toString(36);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
exports.AgentShieldAPIDelegationVerifier = AgentShieldAPIDelegationVerifier;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare KV Delegation Verifier
|
|
3
|
+
*
|
|
4
|
+
* Stores delegations in Cloudflare Workers KV for high-performance,
|
|
5
|
+
* edge-cached delegation verification.
|
|
6
|
+
*
|
|
7
|
+
* Performance:
|
|
8
|
+
* - Fast path (cached): < 5ms
|
|
9
|
+
* - Slow path (KV read): < 50ms
|
|
10
|
+
* - Cache TTL: 1 minute (configurable)
|
|
11
|
+
*
|
|
12
|
+
* Key Structure:
|
|
13
|
+
* - `delegation:{delegationId}` - Full delegation record
|
|
14
|
+
* - `agent:{agentDid}:scopes:{hash}` - Delegation lookup by agent+scopes
|
|
15
|
+
*
|
|
16
|
+
* Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.2
|
|
17
|
+
*/
|
|
18
|
+
import { DelegationRecord } from '@kya-os/contracts/delegation';
|
|
19
|
+
import { DelegationVerifier, DelegationVerifierConfig, VerifyDelegationResult, VerifyDelegationOptions } from './delegation-verifier';
|
|
20
|
+
/**
|
|
21
|
+
* Cloudflare KV Delegation Verifier
|
|
22
|
+
*
|
|
23
|
+
* Self-hosted mode: Stores delegations in local Cloudflare KV namespace
|
|
24
|
+
*/
|
|
25
|
+
export declare class CloudflareKVDelegationVerifier implements DelegationVerifier {
|
|
26
|
+
private kv;
|
|
27
|
+
private cache;
|
|
28
|
+
private cacheTtl;
|
|
29
|
+
private debug;
|
|
30
|
+
constructor(config: DelegationVerifierConfig);
|
|
31
|
+
/**
|
|
32
|
+
* Verify agent delegation
|
|
33
|
+
*/
|
|
34
|
+
verify(agentDid: string, scopes: string[], options?: VerifyDelegationOptions): Promise<VerifyDelegationResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Get delegation by ID
|
|
37
|
+
*/
|
|
38
|
+
get(delegationId: string): Promise<DelegationRecord | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Store delegation record
|
|
41
|
+
*/
|
|
42
|
+
put(delegation: DelegationRecord): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Revoke delegation
|
|
45
|
+
*/
|
|
46
|
+
revoke(delegationId: string, reason?: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Build deterministic scopes key for caching/lookup
|
|
49
|
+
*/
|
|
50
|
+
private buildScopesKey;
|
|
51
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cloudflare KV Delegation Verifier
|
|
4
|
+
*
|
|
5
|
+
* Stores delegations in Cloudflare Workers KV for high-performance,
|
|
6
|
+
* edge-cached delegation verification.
|
|
7
|
+
*
|
|
8
|
+
* Performance:
|
|
9
|
+
* - Fast path (cached): < 5ms
|
|
10
|
+
* - Slow path (KV read): < 50ms
|
|
11
|
+
* - Cache TTL: 1 minute (configurable)
|
|
12
|
+
*
|
|
13
|
+
* Key Structure:
|
|
14
|
+
* - `delegation:{delegationId}` - Full delegation record
|
|
15
|
+
* - `agent:{agentDid}:scopes:{hash}` - Delegation lookup by agent+scopes
|
|
16
|
+
*
|
|
17
|
+
* Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.2
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.CloudflareKVDelegationVerifier = void 0;
|
|
21
|
+
const delegation_1 = require("@kya-os/contracts/delegation");
|
|
22
|
+
const delegation_verifier_1 = require("./delegation-verifier");
|
|
23
|
+
/**
|
|
24
|
+
* Simple in-memory LRU cache for delegation lookups
|
|
25
|
+
*/
|
|
26
|
+
class DelegationCache {
|
|
27
|
+
cache = new Map();
|
|
28
|
+
maxSize;
|
|
29
|
+
constructor(maxSize = 1000) {
|
|
30
|
+
this.maxSize = maxSize;
|
|
31
|
+
}
|
|
32
|
+
get(key) {
|
|
33
|
+
const entry = this.cache.get(key);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return null;
|
|
36
|
+
// Check expiration
|
|
37
|
+
if (Date.now() > entry.expiresAt) {
|
|
38
|
+
this.cache.delete(key);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return entry.data;
|
|
42
|
+
}
|
|
43
|
+
set(key, data, ttlMs) {
|
|
44
|
+
// Simple LRU: if cache is full, delete oldest entry
|
|
45
|
+
if (this.cache.size >= this.maxSize) {
|
|
46
|
+
const firstKey = this.cache.keys().next().value;
|
|
47
|
+
if (firstKey)
|
|
48
|
+
this.cache.delete(firstKey);
|
|
49
|
+
}
|
|
50
|
+
this.cache.set(key, {
|
|
51
|
+
data,
|
|
52
|
+
expiresAt: Date.now() + ttlMs,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
delete(key) {
|
|
56
|
+
this.cache.delete(key);
|
|
57
|
+
}
|
|
58
|
+
clear() {
|
|
59
|
+
this.cache.clear();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cloudflare KV Delegation Verifier
|
|
64
|
+
*
|
|
65
|
+
* Self-hosted mode: Stores delegations in local Cloudflare KV namespace
|
|
66
|
+
*/
|
|
67
|
+
class CloudflareKVDelegationVerifier {
|
|
68
|
+
kv; // KVNamespace from @cloudflare/workers-types
|
|
69
|
+
cache;
|
|
70
|
+
cacheTtl;
|
|
71
|
+
debug;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
if (!config.kvNamespace) {
|
|
74
|
+
throw new Error('CloudflareKVDelegationVerifier requires kvNamespace in config');
|
|
75
|
+
}
|
|
76
|
+
this.kv = config.kvNamespace;
|
|
77
|
+
this.cache = new DelegationCache();
|
|
78
|
+
this.cacheTtl = config.cacheTtl || 60_000; // Default 1 minute
|
|
79
|
+
this.debug = config.debug || false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Verify agent delegation
|
|
83
|
+
*/
|
|
84
|
+
async verify(agentDid, scopes, options) {
|
|
85
|
+
const startTime = Date.now();
|
|
86
|
+
// Build cache key from agent DID + sorted scopes
|
|
87
|
+
const scopesKey = this.buildScopesKey(scopes);
|
|
88
|
+
const cacheKey = `verify:${agentDid}:${scopesKey}`;
|
|
89
|
+
// Fast path: Check cache first (unless skipCache is true)
|
|
90
|
+
if (!options?.skipCache) {
|
|
91
|
+
const cached = this.cache.get(cacheKey);
|
|
92
|
+
if (cached) {
|
|
93
|
+
if (this.debug) {
|
|
94
|
+
console.log(`[KV] Cache HIT for ${agentDid} (${Date.now() - startTime}ms)`);
|
|
95
|
+
}
|
|
96
|
+
return { ...cached, cached: true };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Slow path: Lookup in KV
|
|
100
|
+
if (this.debug) {
|
|
101
|
+
console.log(`[KV] Cache MISS for ${agentDid}, querying KV...`);
|
|
102
|
+
}
|
|
103
|
+
// List all delegations for this agent (to support subset scope matching)
|
|
104
|
+
const listResult = await this.kv.list({ prefix: `agent:${agentDid}:scopes:` });
|
|
105
|
+
if (!listResult.keys || listResult.keys.length === 0) {
|
|
106
|
+
const result = {
|
|
107
|
+
valid: false,
|
|
108
|
+
reason: 'No delegation found for agent',
|
|
109
|
+
cached: false,
|
|
110
|
+
};
|
|
111
|
+
// Cache negative result (shorter TTL)
|
|
112
|
+
this.cache.set(cacheKey, result, this.cacheTtl / 2);
|
|
113
|
+
if (this.debug) {
|
|
114
|
+
console.log(`[KV] No delegation found (${Date.now() - startTime}ms)`);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
// Try each delegation to find one that matches the requested scopes
|
|
119
|
+
let matchingDelegation = null;
|
|
120
|
+
for (const key of listResult.keys) {
|
|
121
|
+
const delegationId = await this.kv.get(key.name, 'text');
|
|
122
|
+
if (!delegationId)
|
|
123
|
+
continue;
|
|
124
|
+
const delegation = await this.get(delegationId);
|
|
125
|
+
if (!delegation)
|
|
126
|
+
continue;
|
|
127
|
+
// Check if this delegation has the required scopes
|
|
128
|
+
const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation);
|
|
129
|
+
const scopesMatch = (0, delegation_verifier_1.checkScopes)(delegationScopes, scopes);
|
|
130
|
+
if (scopesMatch) {
|
|
131
|
+
// Found a matching delegation!
|
|
132
|
+
matchingDelegation = delegation;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!matchingDelegation) {
|
|
137
|
+
const result = {
|
|
138
|
+
valid: false,
|
|
139
|
+
reason: 'No delegation found with required scopes',
|
|
140
|
+
cached: false,
|
|
141
|
+
};
|
|
142
|
+
this.cache.set(cacheKey, result, this.cacheTtl / 2);
|
|
143
|
+
if (this.debug) {
|
|
144
|
+
console.log(`[KV] No matching delegation found (${Date.now() - startTime}ms)`);
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
const delegation = matchingDelegation;
|
|
149
|
+
// Validate delegation
|
|
150
|
+
const validation = (0, delegation_verifier_1.validateDelegation)(delegation);
|
|
151
|
+
if (!validation.valid) {
|
|
152
|
+
const result = {
|
|
153
|
+
valid: false,
|
|
154
|
+
delegation,
|
|
155
|
+
reason: validation.reason,
|
|
156
|
+
cached: false,
|
|
157
|
+
};
|
|
158
|
+
this.cache.set(cacheKey, result, this.cacheTtl / 2);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
// Check scopes
|
|
162
|
+
const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation);
|
|
163
|
+
const scopesMatch = (0, delegation_verifier_1.checkScopes)(delegationScopes, scopes);
|
|
164
|
+
if (!scopesMatch) {
|
|
165
|
+
const result = {
|
|
166
|
+
valid: false,
|
|
167
|
+
delegation,
|
|
168
|
+
reason: 'Insufficient scopes',
|
|
169
|
+
cached: false,
|
|
170
|
+
};
|
|
171
|
+
this.cache.set(cacheKey, result, this.cacheTtl / 2);
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
// Success!
|
|
175
|
+
const result = {
|
|
176
|
+
valid: true,
|
|
177
|
+
delegation,
|
|
178
|
+
cached: false,
|
|
179
|
+
};
|
|
180
|
+
// Cache positive result
|
|
181
|
+
this.cache.set(cacheKey, result, this.cacheTtl);
|
|
182
|
+
if (this.debug) {
|
|
183
|
+
console.log(`[KV] Delegation verified (${Date.now() - startTime}ms)`);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get delegation by ID
|
|
189
|
+
*/
|
|
190
|
+
async get(delegationId) {
|
|
191
|
+
const cacheKey = `delegation:${delegationId}`;
|
|
192
|
+
// Check cache
|
|
193
|
+
const cached = this.cache.get(cacheKey);
|
|
194
|
+
if (cached)
|
|
195
|
+
return cached;
|
|
196
|
+
// Fetch from KV
|
|
197
|
+
const kvKey = `delegation:${delegationId}`;
|
|
198
|
+
const raw = await this.kv.get(kvKey, 'text');
|
|
199
|
+
if (!raw)
|
|
200
|
+
return null;
|
|
201
|
+
try {
|
|
202
|
+
const data = JSON.parse(raw);
|
|
203
|
+
const parsed = delegation_1.DelegationRecordSchema.safeParse(data);
|
|
204
|
+
if (!parsed.success) {
|
|
205
|
+
console.error('[KV] Invalid delegation record:', parsed.error);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const delegation = parsed.data;
|
|
209
|
+
// Cache it
|
|
210
|
+
this.cache.set(cacheKey, delegation, this.cacheTtl);
|
|
211
|
+
return delegation;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
console.error('[KV] Failed to parse delegation:', error);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Store delegation record
|
|
220
|
+
*/
|
|
221
|
+
async put(delegation) {
|
|
222
|
+
// Validate first
|
|
223
|
+
const parsed = delegation_1.DelegationRecordSchema.safeParse(delegation);
|
|
224
|
+
if (!parsed.success) {
|
|
225
|
+
throw new Error(`Invalid delegation record: ${parsed.error.message}`);
|
|
226
|
+
}
|
|
227
|
+
const validated = parsed.data;
|
|
228
|
+
// Store full delegation record
|
|
229
|
+
const kvKey = `delegation:${validated.id}`;
|
|
230
|
+
await this.kv.put(kvKey, JSON.stringify(validated));
|
|
231
|
+
// Create lookup key for agent + scopes
|
|
232
|
+
const delegationScopes = (0, delegation_verifier_1.extractScopes)(validated);
|
|
233
|
+
const scopesKey = this.buildScopesKey(delegationScopes);
|
|
234
|
+
const lookupKey = `agent:${validated.subjectDid}:scopes:${scopesKey}`;
|
|
235
|
+
await this.kv.put(lookupKey, validated.id);
|
|
236
|
+
// Invalidate cache
|
|
237
|
+
this.cache.delete(`delegation:${validated.id}`);
|
|
238
|
+
this.cache.delete(`verify:${validated.subjectDid}:${scopesKey}`);
|
|
239
|
+
if (this.debug) {
|
|
240
|
+
console.log(`[KV] Stored delegation ${validated.id}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Revoke delegation
|
|
245
|
+
*/
|
|
246
|
+
async revoke(delegationId, reason) {
|
|
247
|
+
const delegation = await this.get(delegationId);
|
|
248
|
+
if (!delegation) {
|
|
249
|
+
throw new Error(`Delegation not found: ${delegationId}`);
|
|
250
|
+
}
|
|
251
|
+
// Update status
|
|
252
|
+
delegation.status = 'revoked';
|
|
253
|
+
delegation.revokedAt = Date.now();
|
|
254
|
+
if (reason) {
|
|
255
|
+
delegation.revokedReason = reason;
|
|
256
|
+
}
|
|
257
|
+
// Store updated record
|
|
258
|
+
await this.put(delegation);
|
|
259
|
+
if (this.debug) {
|
|
260
|
+
console.log(`[KV] Revoked delegation ${delegationId}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Build deterministic scopes key for caching/lookup
|
|
265
|
+
*/
|
|
266
|
+
buildScopesKey(scopes) {
|
|
267
|
+
// Sort scopes for deterministic key
|
|
268
|
+
const sorted = [...scopes].sort();
|
|
269
|
+
// Simple hash (for production, consider crypto.subtle.digest)
|
|
270
|
+
const str = sorted.join(',');
|
|
271
|
+
let hash = 0;
|
|
272
|
+
for (let i = 0; i < str.length; i++) {
|
|
273
|
+
const char = str.charCodeAt(i);
|
|
274
|
+
hash = (hash << 5) - hash + char;
|
|
275
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
276
|
+
}
|
|
277
|
+
return Math.abs(hash).toString(36);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.CloudflareKVDelegationVerifier = CloudflareKVDelegationVerifier;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Delegation Verifier
|
|
3
|
+
*
|
|
4
|
+
* Simple in-memory delegation storage for testing and development.
|
|
5
|
+
* NOT suitable for production - delegations lost on restart.
|
|
6
|
+
*
|
|
7
|
+
* Use for:
|
|
8
|
+
* - Unit tests
|
|
9
|
+
* - Local development
|
|
10
|
+
* - Integration tests
|
|
11
|
+
*
|
|
12
|
+
* Related: PHASE_1_XMCP_I_SERVER.md Testing section
|
|
13
|
+
*/
|
|
14
|
+
import { DelegationRecord } from '@kya-os/contracts/delegation';
|
|
15
|
+
import { DelegationVerifier, DelegationVerifierConfig, VerifyDelegationResult, VerifyDelegationOptions } from './delegation-verifier';
|
|
16
|
+
/**
|
|
17
|
+
* Memory Delegation Verifier
|
|
18
|
+
*
|
|
19
|
+
* Simple in-memory storage (for testing only)
|
|
20
|
+
*/
|
|
21
|
+
export declare class MemoryDelegationVerifier implements DelegationVerifier {
|
|
22
|
+
private delegations;
|
|
23
|
+
private agentIndex;
|
|
24
|
+
private debug;
|
|
25
|
+
constructor(config: DelegationVerifierConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Verify agent delegation
|
|
28
|
+
*/
|
|
29
|
+
verify(agentDid: string, scopes: string[], _options?: VerifyDelegationOptions): Promise<VerifyDelegationResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Get delegation by ID
|
|
32
|
+
*/
|
|
33
|
+
get(delegationId: string): Promise<DelegationRecord | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Store delegation
|
|
36
|
+
*/
|
|
37
|
+
put(delegation: DelegationRecord): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Revoke delegation
|
|
40
|
+
*/
|
|
41
|
+
revoke(delegationId: string, reason?: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Clear all delegations (for testing)
|
|
44
|
+
*/
|
|
45
|
+
clear(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Get all delegations (for testing/debugging)
|
|
48
|
+
*/
|
|
49
|
+
getAll(): DelegationRecord[];
|
|
50
|
+
}
|