UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

282 lines (281 loc) 10 kB
"use strict"; /** * Cloudflare KV Delegation Verifier * * Stores delegations in Cloudflare Workers KV for high-performance, * edge-cached delegation verification. * * Performance: * - Fast path (cached): < 5ms * - Slow path (KV read): < 50ms * - Cache TTL: 1 minute (configurable) * * Key Structure: * - `delegation:{delegationId}` - Full delegation record * - `agent:{agentDid}:scopes:{hash}` - Delegation lookup by agent+scopes * * Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.2 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudflareKVDelegationVerifier = void 0; const delegation_1 = require("@kya-os/contracts/delegation"); const delegation_verifier_1 = require("./delegation-verifier"); const delegation_verifier_2 = require("./delegation-verifier"); /** * Simple in-memory LRU cache for delegation lookups */ class DelegationCache { cache = new Map(); maxSize; constructor(maxSize = 1000) { this.maxSize = maxSize; } get(key) { const entry = this.cache.get(key); if (!entry) return null; // Check expiration if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry.data; } set(key, data, ttlMs) { // Simple LRU: if cache is full, delete oldest entry if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey) this.cache.delete(firstKey); } this.cache.set(key, { data, expiresAt: Date.now() + ttlMs, }); } delete(key) { this.cache.delete(key); } clear() { this.cache.clear(); } } /** * Cloudflare KV Delegation Verifier * * Self-hosted mode: Stores delegations in local Cloudflare KV namespace */ class CloudflareKVDelegationVerifier { kv; cache; cacheTtl; debug; constructor(config) { if (!config.kvNamespace) { throw new Error('CloudflareKVDelegationVerifier requires kvNamespace in config'); } this.kv = config.kvNamespace; this.cache = new DelegationCache(); this.cacheTtl = config.cacheTtl || 60_000; // Default 1 minute this.debug = config.debug || false; } /** * Verify agent delegation */ async verify(agentDid, scopes, options) { // Validate inputs const validation = delegation_verifier_2.VerifyDelegationInputSchema.safeParse({ agentDid, scopes, options }); if (!validation.success) { return { valid: false, reason: `Invalid request: ${validation.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`, }; } const startTime = Date.now(); // Build cache key from agent DID + sorted scopes const scopesKey = this.buildScopesKey(scopes); const cacheKey = `verify:${agentDid}:${scopesKey}`; // Fast path: Check cache first (unless skipCache is true) if (!options?.skipCache) { const cached = this.cache.get(cacheKey); if (cached) { if (this.debug) { console.log(`[KV] Cache HIT for ${agentDid} (${Date.now() - startTime}ms)`); } return { ...cached, cached: true }; } } // Slow path: Lookup in KV if (this.debug) { console.log(`[KV] Cache MISS for ${agentDid}, querying KV...`); } // List all delegations for this agent (to support subset scope matching) const listResult = await this.kv.list({ prefix: `agent:${agentDid}:scopes:` }); if (!listResult.keys || listResult.keys.length === 0) { const result = { valid: false, reason: 'No delegation found for agent', cached: false, }; // Cache negative result (shorter TTL) this.cache.set(cacheKey, result, this.cacheTtl / 2); if (this.debug) { console.log(`[KV] No delegation found (${Date.now() - startTime}ms)`); } return result; } // Try each delegation to find one that matches the requested scopes let matchingDelegation = null; for (const key of listResult.keys) { const delegationId = await this.kv.get(key.name); if (!delegationId) continue; const delegation = await this.get(delegationId); if (!delegation) continue; // Check if this delegation has the required scopes const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation); const scopesMatch = (0, delegation_verifier_1.checkScopes)(delegationScopes, scopes); if (scopesMatch) { // Found a matching delegation! matchingDelegation = delegation; break; } } if (!matchingDelegation) { const result = { valid: false, reason: 'No delegation found with required scopes', cached: false, }; this.cache.set(cacheKey, result, this.cacheTtl / 2); if (this.debug) { console.log(`[KV] No matching delegation found (${Date.now() - startTime}ms)`); } return result; } const delegation = matchingDelegation; // Validate delegation const delegationValidation = (0, delegation_verifier_1.validateDelegation)(delegation); if (!delegationValidation.valid) { const result = { valid: false, delegation, reason: delegationValidation.reason, cached: false, }; this.cache.set(cacheKey, result, this.cacheTtl / 2); return result; } // Check scopes const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation); const scopesMatch = (0, delegation_verifier_1.checkScopes)(delegationScopes, scopes); if (!scopesMatch) { const result = { valid: false, delegation, reason: 'Insufficient scopes', cached: false, }; this.cache.set(cacheKey, result, this.cacheTtl / 2); return result; } // Success! const result = { valid: true, delegation, cached: false, }; // Cache positive result this.cache.set(cacheKey, result, this.cacheTtl); if (this.debug) { console.log(`[KV] Delegation verified (${Date.now() - startTime}ms)`); } return result; } /** * Get delegation by ID */ async get(delegationId) { // Fetch from KV (no caching - cache is for verification results only) const kvKey = `delegation:${delegationId}`; const raw = await this.kv.get(kvKey); if (!raw) return null; try { const data = JSON.parse(raw); const parsed = delegation_1.DelegationRecordSchema.safeParse(data); if (!parsed.success) { console.error('[KV] Invalid delegation record:', parsed.error); return null; } return parsed.data; } catch (error) { console.error('[KV] Failed to parse delegation:', error); return null; } } /** * Store delegation record */ async put(delegation) { // Validate first const parsed = delegation_1.DelegationRecordSchema.safeParse(delegation); if (!parsed.success) { throw new Error(`Invalid delegation record: ${parsed.error.message}`); } const validated = parsed.data; // Store full delegation record const kvKey = `delegation:${validated.id}`; await this.kv.put(kvKey, JSON.stringify(validated)); // Create lookup key for agent + scopes const delegationScopes = (0, delegation_verifier_1.extractScopes)(validated); const scopesKey = this.buildScopesKey(delegationScopes); const lookupKey = `agent:${validated.subjectDid}:scopes:${scopesKey}`; await this.kv.put(lookupKey, validated.id); // Invalidate cache this.cache.delete(`delegation:${validated.id}`); this.cache.delete(`verify:${validated.subjectDid}:${scopesKey}`); if (this.debug) { console.log(`[KV] Stored delegation ${validated.id}`); } } /** * Revoke delegation */ async revoke(delegationId, reason) { const delegation = await this.get(delegationId); if (!delegation) { throw new Error(`Delegation not found: ${delegationId}`); } // Update status delegation.status = 'revoked'; delegation.revokedAt = Date.now(); if (reason) { delegation.revokedReason = reason; } // Store updated record await this.put(delegation); if (this.debug) { console.log(`[KV] Revoked delegation ${delegationId}`); } } /** * Build deterministic scopes key for caching/lookup */ buildScopesKey(scopes) { // Sort scopes for deterministic key const sorted = [...scopes].sort(); // Simple hash (for production, consider crypto.subtle.digest) const str = sorted.join(','); let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(36); } } exports.CloudflareKVDelegationVerifier = CloudflareKVDelegationVerifier;