@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
282 lines (281 loc) • 10 kB
JavaScript
"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;