@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
298 lines (297 loc) • 10.8 kB
JavaScript
;
/**
* AgentShield API Delegation Verifier
*
* Queries delegations from AgentShield managed service API.
* Includes local caching to minimize network calls and maximize performance.
*
* Performance:
* - Fast path (cached): < 5ms
* - Slow path (API call): < 100ms
* - Cache TTL: 1 minute (configurable)
*
* API Endpoints:
* - POST /api/v1/delegations/verify - Verify delegation by agent DID + scopes
* - GET /api/v1/delegations/:id - Get delegation by ID
* - POST /api/v1/delegations - Create new delegation (admin only)
* - POST /api/v1/delegations/:id/revoke - Revoke delegation (admin only)
*
* Authentication: Bearer token via X-API-Key header
*
* Related: PHASE_1_XMCP_I_SERVER.md Ticket 1.3
* Related: AGENTSHIELD_DASHBOARD_PLAN.md Epic 2 (Public API)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentShieldAPIDelegationVerifier = void 0;
const delegation_1 = require("@kya-os/contracts/delegation");
const agentshield_api_1 = require("@kya-os/contracts/agentshield-api");
const delegation_verifier_1 = require("./delegation-verifier");
const mcp_i_core_1 = require("@kya-os/mcp-i-core");
const node_providers_1 = require("../providers/node-providers");
/**
* Simple in-memory cache (same as KV verifier)
*/
class DelegationCache {
cache = new Map();
maxSize;
constructor(maxSize = 1000) {
this.maxSize = maxSize;
}
get(key) {
const entry = this.cache.get(key);
if (!entry)
return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.data;
}
set(key, data, ttlMs) {
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();
}
}
// Use standard AgentShield API response types from contracts
/**
* AgentShield API Delegation Verifier
*
* Managed mode: Queries delegations from AgentShield dashboard API
*/
class AgentShieldAPIDelegationVerifier {
apiUrl;
apiKey;
cache;
cacheTtl;
debug;
accessControlService;
constructor(config) {
if (!config.agentshield?.apiUrl || !config.agentshield?.apiKey) {
throw new Error("AgentShieldAPIDelegationVerifier requires agentshield.apiUrl and agentshield.apiKey in config");
}
this.apiUrl = config.agentshield.apiUrl.replace(/\/$/, ""); // Remove trailing slash
this.apiKey = config.agentshield.apiKey;
this.cache = new DelegationCache();
this.cacheTtl = config.cacheTtl || 60_000; // Default 1 minute
this.debug = config.debug || false;
// Create AccessControlApiService instance
const fetchProvider = new node_providers_1.NodeFetchProvider();
this.accessControlService = new mcp_i_core_1.AccessControlApiService({
baseUrl: this.apiUrl,
apiKey: this.apiKey,
fetchProvider,
logger: this.debug ? (msg, data) => console.log(`[AgentShield] ${msg}`, data) : undefined,
});
}
/**
* Verify agent delegation via API
*/
async verify(agentDid, scopes, options) {
// Validate inputs
const validation = delegation_verifier_1.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
const scopesKey = this.buildScopesKey(scopes);
const cacheKey = `verify:${agentDid}:${scopesKey}`;
// Fast path: Check cache
if (!options?.skipCache) {
const cached = this.cache.get(cacheKey);
if (cached) {
if (this.debug) {
console.log(`[AgentShield] Cache HIT for ${agentDid} (${Date.now() - startTime}ms)`);
}
return { ...cached, cached: true };
}
}
// Slow path: API call
if (this.debug) {
console.log(`[AgentShield] Cache MISS for ${agentDid}, calling API...`);
}
try {
// Validate request input using contracts schema
const requestBody = agentshield_api_1.verifyDelegationRequestSchema.parse({
agent_did: agentDid,
scopes,
});
// Use AccessControlApiService for API call
const response = await this.accessControlService.verifyDelegation(requestBody);
// Extract data from wrapped response
const data = response.data;
// Build result from validated response
const result = {
valid: data.valid,
delegation: data.delegation,
reason: data.error ? data.error.message : data.reason,
cached: false,
};
// Cache result
const ttl = data.valid ? this.cacheTtl : this.cacheTtl / 2;
this.cache.set(cacheKey, result, ttl);
if (this.debug) {
console.log(`[AgentShield] Delegation ${data.valid ? "verified" : "rejected"} (${Date.now() - startTime}ms)`);
}
return result;
}
catch (error) {
console.error("[AgentShield] API call failed:", error);
// Return negative result on error (don't cache failures)
// Use standard error format
if (error instanceof agentshield_api_1.AgentShieldAPIError) {
return {
valid: false,
reason: error.message,
cached: false,
};
}
const runtimeError = {
error: "api_error",
message: error instanceof Error ? error.message : "Unknown error",
httpStatus: 500,
};
return {
valid: false,
reason: runtimeError.message,
cached: false,
};
}
}
/**
* Get delegation by ID via API
*/
async get(delegationId) {
// Fetch from API (no caching - cache is for verification results only)
try {
const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations/${delegationId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${this.apiKey}`,
},
});
if (!response.ok) {
if (response.status === 404)
return null;
throw new Error(`AgentShield API error: ${response.status}`);
}
const data = await response.json();
const parsed = delegation_1.DelegationRecordSchema.safeParse(data);
if (!parsed.success) {
console.error("[AgentShield] Invalid delegation in API response:", parsed.error);
return null;
}
return parsed.data;
}
catch (error) {
console.error("[AgentShield] Failed to get delegation:", error);
return null;
}
}
/**
* Store delegation (admin operation via API)
*
* Note: This is typically done via AgentShield dashboard UI,
* not by the bouncer directly. Included for completeness.
*/
async put(delegation) {
// Validate first
const parsed = delegation_1.DelegationRecordSchema.safeParse(delegation);
if (!parsed.success) {
throw new Error(`Invalid delegation record: ${parsed.error.message}`);
}
try {
const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify(delegation),
});
if (!response.ok) {
throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`);
}
// Invalidate cache
const delegationScopes = (0, delegation_verifier_1.extractScopes)(delegation);
const scopesKey = this.buildScopesKey(delegationScopes);
this.cache.delete(`delegation:${delegation.id}`);
this.cache.delete(`verify:${delegation.subjectDid}:${scopesKey}`);
if (this.debug) {
console.log(`[AgentShield] Stored delegation ${delegation.id}`);
}
}
catch (error) {
console.error("[AgentShield] Failed to store delegation:", error);
throw error;
}
}
/**
* Revoke delegation via API
*/
async revoke(delegationId, reason) {
try {
const response = await fetch(`${this.apiUrl}/api/v1/bouncer/delegations/${delegationId}/revoke`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ reason }),
});
if (!response.ok) {
throw new Error(`AgentShield API error: ${response.status} ${response.statusText}`);
}
// Invalidate cache
this.cache.delete(`delegation:${delegationId}`);
if (this.debug) {
console.log(`[AgentShield] Revoked delegation ${delegationId}`);
}
}
catch (error) {
console.error("[AgentShield] Failed to revoke delegation:", error);
throw error;
}
}
/**
* Close connections (cleanup)
*/
async close() {
this.cache.clear();
}
/**
* Build deterministic scopes key for caching
*/
buildScopesKey(scopes) {
const sorted = [...scopes].sort();
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;
}
return Math.abs(hash).toString(36);
}
}
exports.AgentShieldAPIDelegationVerifier = AgentShieldAPIDelegationVerifier;