@kya-os/mcp-i-core 1.7.0 → 1.8.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.
Files changed (121) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance/capability-verifier-adapter.d.ts +66 -0
  3. package/dist/conformance/capability-verifier-adapter.js +61 -0
  4. package/dist/conformance/index.d.ts +10 -0
  5. package/dist/conformance/index.js +19 -0
  6. package/dist/conformance/validate-level2.d.ts +203 -0
  7. package/dist/conformance/validate-level2.js +294 -0
  8. package/dist/conformance/vc-jwt-verifier.d.ts +55 -0
  9. package/dist/conformance/vc-jwt-verifier.js +147 -0
  10. package/dist/delegation/did-web-resolver.d.ts +60 -0
  11. package/dist/delegation/did-web-resolver.js +129 -0
  12. package/dist/delegation/outbound-proof.d.ts +3 -2
  13. package/dist/delegation/outbound-proof.js +3 -2
  14. package/dist/delegation/utils.d.ts +3 -1
  15. package/dist/index.d.ts +9 -63
  16. package/dist/index.js +22 -161
  17. package/dist/utils/base58.d.ts +4 -28
  18. package/dist/utils/base58.js +18 -98
  19. package/dist/utils/base64.d.ts +4 -29
  20. package/dist/utils/base64.js +18 -167
  21. package/dist/utils/did-helpers.d.ts +4 -165
  22. package/dist/utils/did-helpers.js +18 -217
  23. package/package.json +5 -7
  24. package/dist/auth/handshake.d.ts +0 -119
  25. package/dist/auth/handshake.js +0 -250
  26. package/dist/auth/index.d.ts +0 -6
  27. package/dist/auth/index.js +0 -11
  28. package/dist/auth/types.d.ts +0 -46
  29. package/dist/auth/types.js +0 -10
  30. package/dist/cache/oauth-config-cache.d.ts +0 -69
  31. package/dist/cache/oauth-config-cache.js +0 -76
  32. package/dist/cache/tool-protection-cache.d.ts +0 -89
  33. package/dist/cache/tool-protection-cache.js +0 -118
  34. package/dist/compliance/index.d.ts +0 -8
  35. package/dist/compliance/index.js +0 -24
  36. package/dist/compliance/schema-registry.d.ts +0 -50
  37. package/dist/compliance/schema-registry.js +0 -438
  38. package/dist/compliance/schema-verifier.d.ts +0 -110
  39. package/dist/compliance/schema-verifier.js +0 -518
  40. package/dist/config/remote-config.d.ts +0 -173
  41. package/dist/config/remote-config.js +0 -253
  42. package/dist/config.d.ts +0 -182
  43. package/dist/config.js +0 -148
  44. package/dist/identity/idp-token-resolver.d.ts +0 -69
  45. package/dist/identity/idp-token-resolver.js +0 -136
  46. package/dist/identity/idp-token-storage.interface.d.ts +0 -80
  47. package/dist/identity/idp-token-storage.interface.js +0 -14
  48. package/dist/identity/user-did-manager.d.ts +0 -228
  49. package/dist/identity/user-did-manager.js +0 -356
  50. package/dist/logging/index.d.ts +0 -5
  51. package/dist/logging/index.js +0 -10
  52. package/dist/logging/logger.d.ts +0 -33
  53. package/dist/logging/logger.js +0 -104
  54. package/dist/proof/generator.d.ts +0 -109
  55. package/dist/proof/generator.js +0 -236
  56. package/dist/proof/index.d.ts +0 -5
  57. package/dist/proof/index.js +0 -11
  58. package/dist/providers/base.d.ts +0 -95
  59. package/dist/providers/base.js +0 -47
  60. package/dist/providers/memory.d.ts +0 -42
  61. package/dist/providers/memory.js +0 -121
  62. package/dist/runtime/audit-logger.d.ts +0 -37
  63. package/dist/runtime/audit-logger.js +0 -9
  64. package/dist/runtime/base.d.ts +0 -375
  65. package/dist/runtime/base.js +0 -1491
  66. package/dist/runtime/ext-apps-constants.d.ts +0 -14
  67. package/dist/runtime/ext-apps-constants.js +0 -17
  68. package/dist/services/access-control.service.d.ts +0 -138
  69. package/dist/services/access-control.service.js +0 -721
  70. package/dist/services/authorization/authorization-registry.d.ts +0 -29
  71. package/dist/services/authorization/authorization-registry.js +0 -58
  72. package/dist/services/authorization/types.d.ts +0 -53
  73. package/dist/services/authorization/types.js +0 -10
  74. package/dist/services/batch-delegation.service.d.ts +0 -53
  75. package/dist/services/batch-delegation.service.js +0 -95
  76. package/dist/services/crypto.service.d.ts +0 -69
  77. package/dist/services/crypto.service.js +0 -225
  78. package/dist/services/delegation-error-formatter.d.ts +0 -124
  79. package/dist/services/delegation-error-formatter.js +0 -214
  80. package/dist/services/errors.d.ts +0 -49
  81. package/dist/services/errors.js +0 -66
  82. package/dist/services/index.d.ts +0 -9
  83. package/dist/services/index.js +0 -15
  84. package/dist/services/oauth-config.service.d.ts +0 -53
  85. package/dist/services/oauth-config.service.js +0 -141
  86. package/dist/services/oauth-provider-registry.d.ts +0 -88
  87. package/dist/services/oauth-provider-registry.js +0 -128
  88. package/dist/services/oauth-service.d.ts +0 -122
  89. package/dist/services/oauth-service.js +0 -442
  90. package/dist/services/oauth-token-retrieval.service.d.ts +0 -49
  91. package/dist/services/oauth-token-retrieval.service.js +0 -150
  92. package/dist/services/policy.service.d.ts +0 -130
  93. package/dist/services/policy.service.js +0 -399
  94. package/dist/services/proof-verifier.d.ts +0 -98
  95. package/dist/services/proof-verifier.js +0 -319
  96. package/dist/services/provider-resolver.d.ts +0 -89
  97. package/dist/services/provider-resolver.js +0 -196
  98. package/dist/services/provider-validator.d.ts +0 -55
  99. package/dist/services/provider-validator.js +0 -135
  100. package/dist/services/session-registration.service.d.ts +0 -80
  101. package/dist/services/session-registration.service.js +0 -174
  102. package/dist/services/storage.service.d.ts +0 -116
  103. package/dist/services/storage.service.js +0 -411
  104. package/dist/services/tool-context-builder.d.ts +0 -74
  105. package/dist/services/tool-context-builder.js +0 -215
  106. package/dist/services/tool-protection.service.d.ts +0 -193
  107. package/dist/services/tool-protection.service.js +0 -736
  108. package/dist/session/index.d.ts +0 -5
  109. package/dist/session/index.js +0 -11
  110. package/dist/session/manager.d.ts +0 -113
  111. package/dist/session/manager.js +0 -273
  112. package/dist/types/oauth-required-error.d.ts +0 -46
  113. package/dist/types/oauth-required-error.js +0 -42
  114. package/dist/types/tool-protection.d.ts +0 -144
  115. package/dist/types/tool-protection.js +0 -48
  116. package/dist/utils/cors.d.ts +0 -44
  117. package/dist/utils/cors.js +0 -71
  118. package/dist/utils/index.d.ts +0 -8
  119. package/dist/utils/index.js +0 -24
  120. package/dist/utils/storage-keys.d.ts +0 -120
  121. package/dist/utils/storage-keys.js +0 -217
@@ -1,1491 +0,0 @@
1
- "use strict";
2
- /**
3
- * MCPIRuntimeBase - Provider-based runtime
4
- *
5
- * Core runtime that accepts providers for all platform-specific operations.
6
- * This enables the same runtime logic to work across Node.js, Cloudflare Workers,
7
- * and other platforms.
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.MCPIRuntimeBase = void 0;
11
- const tool_protection_js_1 = require("../types/tool-protection.js");
12
- const crypto_service_js_1 = require("../services/crypto.service.js");
13
- const access_control_service_js_1 = require("../services/access-control.service.js");
14
- const agentshield_api_1 = require("@kya-os/contracts/agentshield-api");
15
- const user_did_manager_1 = require("../identity/user-did-manager");
16
- const isRecord = (value) => typeof value === "object" && value !== null;
17
- class MCPIRuntimeBase {
18
- crypto;
19
- clock;
20
- fetch;
21
- storage;
22
- nonceCache;
23
- identity;
24
- config;
25
- cachedIdentity;
26
- sessions = new Map();
27
- lastProof;
28
- userDidManager;
29
- interceptedCalls = new Map(); // Store intercepted tool calls by resume token
30
- cryptoService;
31
- proofVerifier; // Optional ProofVerifier (injected by subclasses)
32
- accessControlService; // Optional AccessControlApiService (injected by subclasses)
33
- /**
34
- * Cached flag indicating whether @modelcontextprotocol/ext-apps is available.
35
- * Set by subclasses after a successful dynamic import (avoids require() vs ESM issues).
36
- * When undefined, falls back to synchronous require() probe.
37
- */
38
- _extAppsAvailable;
39
- constructor(config) {
40
- this.config = config;
41
- this.crypto = config.cryptoProvider;
42
- this.clock = config.clockProvider;
43
- this.fetch = config.fetchProvider;
44
- this.storage = config.storageProvider;
45
- this.nonceCache = config.nonceCacheProvider;
46
- this.identity = config.identityProvider;
47
- // Initialize CryptoService for JWS verification
48
- this.cryptoService = new crypto_service_js_1.CryptoService(this.crypto);
49
- }
50
- /**
51
- * Initialize the runtime
52
- */
53
- async initialize() {
54
- // Load or generate identity
55
- this.cachedIdentity = await this.identity.getIdentity();
56
- // Initialize user DID manager if identity config enables it
57
- if (this.config.identity?.generateUserDids) {
58
- this.userDidManager = new user_did_manager_1.UserDidManager({
59
- crypto: this.crypto,
60
- storage: this.config.identity?.userDidStorage
61
- ? {
62
- get: async (key) => await this.storage.get(`userDid:${key}`),
63
- set: async (key, value, ttl) => {
64
- await this.storage.set(`userDid:${key}`, value);
65
- },
66
- delete: async (key) => await this.storage.delete(`userDid:${key}`),
67
- }
68
- : undefined,
69
- useDidWeb: this.config.identity?.userDidStorage === "persistent",
70
- });
71
- }
72
- // Initialize nonce cache if it has an initialize method
73
- const nonceCacheWithInitialize = this.nonceCache;
74
- if (typeof nonceCacheWithInitialize.initialize === "function") {
75
- await nonceCacheWithInitialize.initialize();
76
- }
77
- // Log initialization if audit is enabled
78
- if (this.config.audit?.enabled) {
79
- this.logAudit("runtime_initialized", {
80
- did: this.cachedIdentity.did,
81
- environment: this.config.environment || "development",
82
- userDidGeneration: this.config.identity?.generateUserDids
83
- ? "enabled"
84
- : "disabled",
85
- });
86
- }
87
- }
88
- /**
89
- * Get the current agent identity
90
- */
91
- async getIdentity() {
92
- if (!this.cachedIdentity) {
93
- this.cachedIdentity = await this.identity.getIdentity();
94
- }
95
- return this.cachedIdentity;
96
- }
97
- /**
98
- * Handle MCP handshake request
99
- *
100
- * Phase 5: Anonymous Sessions Until OAuth
101
- * - Sessions start anonymous (no userDid) unless OAuth identity provided
102
- * - User DID is resolved via AgentShield after OAuth completes
103
- * - Eliminates DID fragmentation (same user = same DID across sessions)
104
- *
105
- * @param request - Handshake request object (may include oauthIdentity for persistent user DID lookup)
106
- * @returns Handshake response with session ID and agent DID
107
- */
108
- async handleHandshake(request) {
109
- const identity = await this.getIdentity();
110
- const timestamp = this.clock.now();
111
- const sessionId = await this.generateSessionId();
112
- // Phase 5: Try to resolve user DID from existing OAuth mapping
113
- // Sessions start anonymous - no ephemeral generation
114
- let userDid;
115
- if (this.userDidManager) {
116
- try {
117
- const oauthIdentity = request.oauthIdentity;
118
- const resolvedDid = await this.userDidManager.getOrCreateUserDid(sessionId, oauthIdentity);
119
- // Convert null to undefined for session storage
120
- userDid = resolvedDid ?? undefined;
121
- if (this.config.audit?.enabled) {
122
- if (userDid) {
123
- console.error("[MCP-I] Resolved existing user DID for session:", {
124
- userDid: userDid.substring(0, 20) + "...",
125
- hasOAuth: !!oauthIdentity,
126
- provider: oauthIdentity?.provider,
127
- });
128
- }
129
- else {
130
- console.error("[MCP-I] Session started anonymous (no userDid):", {
131
- sessionId: sessionId.substring(0, 8) + "...",
132
- hasOAuth: !!oauthIdentity,
133
- });
134
- }
135
- }
136
- }
137
- catch (error) {
138
- console.warn("[MCP-I] Failed to resolve user DID:", error);
139
- // Continue without user DID - session is anonymous
140
- }
141
- }
142
- // Extract client info if available
143
- const normalizeString = (value) => {
144
- if (typeof value !== "string") {
145
- return undefined;
146
- }
147
- const trimmed = value.trim();
148
- return trimmed.length > 0 ? trimmed : undefined;
149
- };
150
- const protocolVersion = normalizeString(request.clientProtocolVersion);
151
- const clientCapabilities = request.clientCapabilities;
152
- const requestClientInfo = request.clientInfo;
153
- const shouldPersistClientInfo = requestClientInfo ||
154
- typeof protocolVersion === "string" ||
155
- typeof clientCapabilities !== "undefined";
156
- const clientInfo = shouldPersistClientInfo
157
- ? {
158
- name: normalizeString(requestClientInfo?.name) ?? "unknown",
159
- title: normalizeString(requestClientInfo?.title),
160
- version: normalizeString(requestClientInfo?.version),
161
- platform: normalizeString(requestClientInfo?.platform),
162
- vendor: normalizeString(requestClientInfo?.vendor),
163
- persistentId: normalizeString(requestClientInfo?.persistentId),
164
- clientId: normalizeString(requestClientInfo?.clientId) ?? crypto.randomUUID(),
165
- protocolVersion,
166
- capabilities: clientCapabilities,
167
- }
168
- : undefined;
169
- // Create session with Phase 5 identity state
170
- const session = {
171
- id: sessionId,
172
- clientDid: request.clientDid || userDid, // Use provided clientDid or generated userDid
173
- userDid: userDid, // Store user DID (may be undefined for anonymous sessions)
174
- agentDid: request.agentDid, // ✅ FIXED: Only agent DID, no fallback
175
- serverDid: identity.did, // ✅ NEW: Server's DID (for clarity)
176
- audience: request.audience,
177
- createdAt: timestamp,
178
- expiresAt: this.clock.calculateExpiry((this.config.session?.ttlMinutes || 30) * 60),
179
- clientInfo, // Store client information
180
- // Phase 5: Identity state tracking
181
- identityState: userDid ? "authenticated" : "anonymous",
182
- oauthIdentity: request.oauthIdentity ?? undefined,
183
- };
184
- this.sessions.set(sessionId, session);
185
- // Create handshake response
186
- const response = {
187
- sessionId,
188
- agentDid: identity.did,
189
- timestamp,
190
- capabilities: ["identity", "proof", "audit"],
191
- ...(userDid && { userDid }), // Include user DID in response if generated
192
- };
193
- // Sign the response
194
- const signature = await this.signData(response);
195
- return {
196
- ...response,
197
- signature,
198
- };
199
- }
200
- /**
201
- * Update session identity after OAuth resolution (Phase 5)
202
- *
203
- * Called after AgentShield identity/resolve returns a persistent user DID.
204
- * Updates the session to authenticated state with the resolved DID.
205
- *
206
- * @param sessionId - MCP session ID
207
- * @param userDid - Persistent user DID from AgentShield
208
- * @param oauthIdentity - OAuth identity information
209
- * @throws Error if session not found
210
- */
211
- async updateSessionIdentity(sessionId, userDid, oauthIdentity) {
212
- const session = this.sessions.get(sessionId);
213
- if (!session) {
214
- throw new Error(`Session not found: ${sessionId}`);
215
- }
216
- // Update session with resolved identity
217
- session.userDid = userDid;
218
- session.identityState = "authenticated";
219
- if (oauthIdentity) {
220
- session.oauthIdentity = oauthIdentity;
221
- }
222
- // Update the sessions map
223
- this.sessions.set(sessionId, session);
224
- // Also update UserDidManager cache if available
225
- if (this.userDidManager) {
226
- await this.userDidManager.setUserDidForSession(sessionId, userDid, oauthIdentity);
227
- }
228
- if (this.config.audit?.enabled) {
229
- console.error("[MCP-I] Session identity updated (Phase 5):", {
230
- sessionId: sessionId.substring(0, 8) + "...",
231
- userDid: userDid.substring(0, 20) + "...",
232
- provider: oauthIdentity?.provider,
233
- identityState: "authenticated",
234
- });
235
- }
236
- }
237
- /**
238
- * Get session by ID
239
- */
240
- getSession(sessionId) {
241
- return this.sessions.get(sessionId);
242
- }
243
- /**
244
- * Extract the correct provider for consent URL from tool protection config
245
- *
246
- * For password auth tools, the provider is in authorization.provider.
247
- * For OAuth auth tools, the provider is in oauthProvider (legacy) or authorization.provider.
248
- *
249
- * Note: Not all authorization types have a provider field (e.g., 'none', 'mdl', 'webauthn').
250
- * Only 'oauth', 'oauth2', 'password', and 'idv' types have provider.
251
- *
252
- * @param protection - Tool protection configuration
253
- * @returns Provider name to use in consent URL, or undefined
254
- */
255
- getConsentProvider(protection) {
256
- // Check authorization field - only some types have provider
257
- if (protection.authorization) {
258
- const auth = protection.authorization;
259
- // Types with provider: oauth, oauth2, password, idv
260
- if (auth.type === "oauth" ||
261
- auth.type === "oauth2" ||
262
- auth.type === "password" ||
263
- auth.type === "idv") {
264
- // Return auth.provider if set, otherwise fall through to legacy oauthProvider
265
- if (auth.provider) {
266
- return auth.provider;
267
- }
268
- }
269
- // Other types don't have provider (none, mdl, verifiable_credential, webauthn, siwe)
270
- // Fall through to legacy oauthProvider
271
- }
272
- // Fall back to legacy oauthProvider (deprecated)
273
- return protection.oauthProvider;
274
- }
275
- /**
276
- * Process tool call with automatic proof generation
277
- * Returns clean result only - proof is stored for out-of-band retrieval
278
- *
279
- * @param toolName - Name of the tool being called
280
- * @param args - Tool arguments
281
- * @param handler - Tool execution handler
282
- * @param session - Session context (expected fields: id, audience, nonce?, delegationToken?, consentProof?)
283
- */
284
- async processToolCall(toolName, args, handler, session) {
285
- // Check tool protection (delegation requirement)
286
- if (this.config.toolProtectionService) {
287
- // Get agent identity to check protection
288
- const identity = await this.getIdentity();
289
- if (this.config.audit?.enabled) {
290
- console.error("[MCP-I] Checking tool protection:", {
291
- tool: toolName,
292
- agentDid: identity.did.slice(0, 20) + "...",
293
- hasDelegation: !!(session?.delegationToken || session?.consentProof),
294
- });
295
- }
296
- const protection = await this.config.toolProtectionService.checkToolProtection(toolName, identity.did);
297
- if (protection) {
298
- // Tool requires delegation
299
- const hasDelegation = session?.delegationToken || session?.consentProof;
300
- if (!hasDelegation) {
301
- // No delegation provided - intercept the tool call and store context
302
- // Store intercepted tool call context for resumption
303
- const interceptedCall = {
304
- toolName,
305
- args,
306
- sessionId: session?.id || "unknown",
307
- timestamp: this.clock.now(),
308
- expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
309
- };
310
- // Generate resume token
311
- const resumeToken = await this.generateResumeToken(interceptedCall);
312
- // Build consent URL with resume token
313
- // Note: projectId is not available in base class - subclasses should override buildConsentUrl
314
- // Pass oauthProvider to ensure correct auth method is selected (e.g., "credentials" vs "github")
315
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
316
- this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
317
- );
318
- // Create error with intercepted call context and pre-generated resume token
319
- const error = new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
320
- // Store intercepted call for resumption
321
- this.interceptedCalls.set(resumeToken, interceptedCall);
322
- // Clean up expired intercepted calls periodically
323
- this.cleanupExpiredInterceptedCalls();
324
- if (this.config.audit?.enabled) {
325
- console.warn("[MCP-I] BLOCKED: Tool requires delegation but none provided", {
326
- tool: toolName,
327
- requiredScopes: protection.requiredScopes,
328
- agentDid: identity.did.slice(0, 20) + "...",
329
- resumeToken: error.resumeToken,
330
- consentUrl,
331
- });
332
- }
333
- // Try MCP Apps inline consent UI before falling back to error
334
- // Client capabilities are stored under session.clientInfo.capabilities (set during handleHandshake)
335
- const clientInfo = session?.clientInfo;
336
- const clientCaps = isRecord(clientInfo?.capabilities)
337
- ? clientInfo.capabilities
338
- : undefined;
339
- const uiResult = this.buildConsentUIResult(toolName, protection.requiredScopes, session, resumeToken, session?.projectId || undefined, session?.serverOrigin || undefined, clientCaps);
340
- if (uiResult) {
341
- return uiResult;
342
- }
343
- throw error;
344
- }
345
- // Delegation provided - verify it with AccessControlApiService
346
- const delegationToken = session?.delegationToken;
347
- const consentProof = session?.consentProof;
348
- if (!this.accessControlService) {
349
- // Access control service not available - log warning but allow execution
350
- // This enables graceful degradation when service is not configured
351
- if (this.config.audit?.enabled) {
352
- console.warn("[MCP-I] ⚠️ Delegation token provided but AccessControlApiService not configured - skipping verification", {
353
- tool: toolName,
354
- agentDid: identity.did.slice(0, 20) + "...",
355
- hasDelegationToken: !!delegationToken,
356
- hasConsentProof: !!consentProof,
357
- });
358
- }
359
- }
360
- else {
361
- // Verify delegation token with AccessControlApiService
362
- try {
363
- if (this.config.audit?.enabled) {
364
- console.error("[MCP-I] 🔐 Verifying delegation token with AccessControlApiService", {
365
- tool: toolName,
366
- agentDid: identity.did.slice(0, 20) + "...",
367
- hasDelegationToken: !!delegationToken,
368
- hasConsentProof: !!consentProof,
369
- requiredScopes: protection.requiredScopes,
370
- });
371
- }
372
- // Build verification request
373
- const verifyRequest = {
374
- agent_did: identity.did,
375
- scopes: protection.requiredScopes,
376
- };
377
- // ✅ CRITICAL: Include user_did for user-scoped delegation verification
378
- // This prevents cross-user delegation leakage (Priority 3 fallback issue)
379
- // Without user_did, AgentShield cannot validate user isolation
380
- if (session?.userDid) {
381
- verifyRequest.user_did = session.userDid;
382
- if (this.config.audit?.enabled) {
383
- console.error("[MCP-I] 🔐 Including user_did in verification request", {
384
- userDid: session.userDid.slice(0, 20) + "...",
385
- });
386
- }
387
- }
388
- // Add delegation token if available (preferred over consent proof)
389
- if (delegationToken) {
390
- verifyRequest.delegation_token = delegationToken;
391
- }
392
- else if (consentProof) {
393
- // Consent proof is a JWT credential - use as credential_jwt
394
- verifyRequest.credential_jwt = consentProof;
395
- }
396
- // Add optional timestamp for verification
397
- verifyRequest.timestamp = this.clock.now();
398
- // Add client info from session if available
399
- if (session?.clientDid || session?.clientId) {
400
- verifyRequest.client_info = {
401
- origin: session?.serverOrigin,
402
- user_agent: session?.userAgent,
403
- };
404
- }
405
- // Perform verification
406
- const verificationResult = await this.accessControlService.verifyDelegation(verifyRequest, {
407
- delegationToken: delegationToken || undefined,
408
- credentialJwt: consentProof || undefined,
409
- });
410
- // Check verification result
411
- if (!verificationResult.data.valid) {
412
- // Delegation verification failed
413
- const reason = verificationResult.data.reason ||
414
- "Delegation token invalid or expired";
415
- const errorDetails = verificationResult.data.error;
416
- if (this.config.audit?.enabled) {
417
- console.error("[MCP-I] ❌ Delegation verification FAILED", {
418
- tool: toolName,
419
- agentDid: identity.did.slice(0, 20) + "...",
420
- reason,
421
- errorCode: errorDetails?.code,
422
- errorMessage: errorDetails?.message,
423
- requiredScopes: protection.requiredScopes,
424
- });
425
- }
426
- // Throw DelegationRequiredError to trigger consent flow
427
- const interceptedCall = {
428
- toolName,
429
- args,
430
- sessionId: session?.id || "unknown",
431
- timestamp: this.clock.now(),
432
- expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
433
- };
434
- const resumeToken = await this.generateResumeToken(interceptedCall);
435
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
436
- this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
437
- );
438
- this.interceptedCalls.set(resumeToken, interceptedCall);
439
- this.cleanupExpiredInterceptedCalls();
440
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
441
- }
442
- // ✅ SECURITY: Validate user_identifier matches session userDid
443
- // This ensures delegations are user-specific and prevents user isolation bypass
444
- const credential = verificationResult.data.credential;
445
- const delegationUserIdentifier = credential?.user_identifier;
446
- // Also check for user_id field (AgentShield may return DID or ID directly)
447
- const delegationUserId = credential?.user_id;
448
- const sessionUserDid = session?.userDid;
449
- if (delegationUserIdentifier && sessionUserDid) {
450
- // Check if identifiers match (direct match or via user_id field)
451
- const identifiersMatch = delegationUserIdentifier === sessionUserDid ||
452
- delegationUserId === sessionUserDid;
453
- // Special case: If delegationUserIdentifier is NOT a DID (e.g., email),
454
- // but AgentShield verified the delegation, trust it.
455
- // AgentShield internally links email → DID, so the verification is valid.
456
- // Only fail if BOTH are DIDs and they don't match.
457
- const isDelegationIdentifierDid = delegationUserIdentifier.startsWith("did:");
458
- const shouldEnforceMatch = isDelegationIdentifierDid || delegationUserId;
459
- if (shouldEnforceMatch && !identifiersMatch) {
460
- // User identifier mismatch - potential security issue
461
- const securityError = `Delegation user_identifier mismatch: delegation has "${delegationUserIdentifier.substring(0, 20)}..." but session has "${sessionUserDid.substring(0, 20)}..."`;
462
- if (this.config.audit?.enabled) {
463
- console.error("[MCP-I] 🔒 SECURITY: User identifier validation FAILED", {
464
- tool: toolName,
465
- agentDid: identity.did.slice(0, 20) + "...",
466
- delegationUserIdentifier: delegationUserIdentifier.substring(0, 20) + "...",
467
- delegationUserId: delegationUserId
468
- ? delegationUserId.substring(0, 20) + "..."
469
- : "N/A",
470
- sessionUserDid: sessionUserDid.substring(0, 20) + "...",
471
- sessionId: session?.id?.substring(0, 20) + "...",
472
- reason: "user_identifier_mismatch",
473
- severity: "high",
474
- });
475
- }
476
- // Throw DelegationRequiredError to force re-authentication
477
- const interceptedCall = {
478
- toolName,
479
- args,
480
- sessionId: session?.id || "unknown",
481
- timestamp: this.clock.now(),
482
- expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
483
- };
484
- const resumeToken = await this.generateResumeToken(interceptedCall);
485
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
486
- this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
487
- );
488
- this.interceptedCalls.set(resumeToken, interceptedCall);
489
- this.cleanupExpiredInterceptedCalls();
490
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
491
- }
492
- // User identifier validation passed (direct match or trusted AgentShield verification)
493
- if (this.config.audit?.enabled) {
494
- console.error("[MCP-I] ✅ User identifier validation PASSED", {
495
- tool: toolName,
496
- agentDid: identity.did.slice(0, 20) + "...",
497
- userDid: sessionUserDid.substring(0, 20) + "...",
498
- sessionId: session?.id?.substring(0, 20) + "...",
499
- matchType: identifiersMatch
500
- ? "direct"
501
- : "trusted_agentshield_verification",
502
- delegationIdentifierFormat: isDelegationIdentifierDid
503
- ? "did"
504
- : "email_or_other",
505
- });
506
- }
507
- }
508
- else if (delegationUserIdentifier && !sessionUserDid) {
509
- // Delegation has user_identifier but session doesn't - log warning
510
- if (this.config.audit?.enabled) {
511
- console.warn("[MCP-I] ⚠️ Delegation has user_identifier but session missing userDid", {
512
- tool: toolName,
513
- agentDid: identity.did.slice(0, 20) + "...",
514
- delegationUserIdentifier: delegationUserIdentifier.substring(0, 20) + "...",
515
- sessionId: session?.id?.substring(0, 20) + "...",
516
- });
517
- }
518
- }
519
- // ✅ SECURITY: Validate authorization method matches tool requirements
520
- // This prevents stale delegations from working after tool auth method changes
521
- // NOTE: Authorization validation only applies when credential is present (AgentShield API verifier).
522
- // KV/Memory verifiers don't return credential data, so we skip auth method validation for them.
523
- const toolAuth = protection.authorization;
524
- const delegationAuth = credential?.authorization;
525
- // Only validate when:
526
- // 1. Tool has authorization requirement defined
527
- // 2. Credential is present (AgentShield API verifier)
528
- if (toolAuth && credential) {
529
- // If tool requires authorization but credential doesn't have auth data, reject
530
- // This prevents bypassing auth checks with incomplete credential data
531
- if (!delegationAuth) {
532
- if (this.config.audit?.enabled) {
533
- console.error("[MCP-I] ❌ Tool requires authorization but credential missing auth data", {
534
- tool: toolName,
535
- agentDid: identity.did.slice(0, 20) + "...",
536
- toolAuth,
537
- note: "Credential exists but missing authorization metadata - rejecting for security",
538
- });
539
- }
540
- // Throw DelegationRequiredError to force re-authorization
541
- const interceptedCall = {
542
- toolName,
543
- args,
544
- sessionId: session?.id || "unknown",
545
- timestamp: this.clock.now(),
546
- expiresAt: this.clock.calculateExpiry(1800),
547
- };
548
- const resumeToken = await this.generateResumeToken(interceptedCall);
549
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
550
- this.interceptedCalls.set(resumeToken, interceptedCall);
551
- this.cleanupExpiredInterceptedCalls();
552
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
553
- }
554
- // Both tool and delegation have authorization - compare them
555
- if (!(0, access_control_service_js_1.authorizationMatches)(delegationAuth, toolAuth)) {
556
- const toolAuthRecord = toolAuth;
557
- const toolAuthProvider = typeof toolAuthRecord.provider === "string"
558
- ? `:${toolAuthRecord.provider}`
559
- : "";
560
- const toolAuthCredentialType = typeof toolAuthRecord.credentialType === "string"
561
- ? `:${toolAuthRecord.credentialType}`
562
- : "";
563
- const authMismatchReason = `Authorization method mismatch: delegation has ${delegationAuth.type}${delegationAuth.provider ? `:${delegationAuth.provider}` : ""}${delegationAuth.credentialType ? `:${delegationAuth.credentialType}` : ""} but tool requires ${toolAuth.type}${toolAuthProvider}${toolAuthCredentialType}`;
564
- if (this.config.audit?.enabled) {
565
- console.error("[MCP-I] ❌ Authorization method validation FAILED", {
566
- tool: toolName,
567
- agentDid: identity.did.slice(0, 20) + "...",
568
- reason: authMismatchReason,
569
- delegationAuth,
570
- toolAuth,
571
- });
572
- }
573
- // Throw DelegationRequiredError to force re-authorization with correct method
574
- const interceptedCall = {
575
- toolName,
576
- args,
577
- sessionId: session?.id || "unknown",
578
- timestamp: this.clock.now(),
579
- expiresAt: this.clock.calculateExpiry(1800),
580
- };
581
- const resumeToken = await this.generateResumeToken(interceptedCall);
582
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, this.getConsentProvider(protection));
583
- this.interceptedCalls.set(resumeToken, interceptedCall);
584
- this.cleanupExpiredInterceptedCalls();
585
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
586
- }
587
- // Authorization method validation passed
588
- if (this.config.audit?.enabled) {
589
- console.error("[MCP-I] ✅ Authorization method validation PASSED", {
590
- tool: toolName,
591
- agentDid: identity.did.slice(0, 20) + "...",
592
- authType: delegationAuth.type,
593
- provider: delegationAuth.provider || "N/A",
594
- });
595
- }
596
- }
597
- // If credential is missing entirely (KV/Memory verifiers), skip validation for backward compatibility
598
- // Verification succeeded
599
- if (this.config.audit?.enabled) {
600
- console.error("[MCP-I] ✅ Delegation verification SUCCEEDED", {
601
- tool: toolName,
602
- agentDid: identity.did.slice(0, 20) + "...",
603
- delegationId: verificationResult.data.delegation_id,
604
- credentialScopes: verificationResult.data.credential?.scopes,
605
- requiredScopes: protection.requiredScopes,
606
- });
607
- }
608
- }
609
- catch (error) {
610
- // Handle verification errors
611
- if (error instanceof tool_protection_js_1.DelegationRequiredError) {
612
- // Re-throw DelegationRequiredError as-is (already handled above)
613
- throw error;
614
- }
615
- // Handle AgentShieldAPIError (network errors, API errors, etc.)
616
- if (error instanceof agentshield_api_1.AgentShieldAPIError) {
617
- if (this.config.audit?.enabled) {
618
- console.error("[MCP-I] ❌ Delegation verification error (API failure)", {
619
- tool: toolName,
620
- agentDid: identity.did.slice(0, 20) + "...",
621
- errorCode: error.code,
622
- errorMessage: error.message,
623
- errorDetails: error.details,
624
- });
625
- }
626
- // On API errors, fail securely by requiring delegation
627
- // This prevents unauthorized access when verification service is unavailable
628
- const interceptedCall = {
629
- toolName,
630
- args,
631
- sessionId: session?.id || "unknown",
632
- timestamp: this.clock.now(),
633
- expiresAt: this.clock.calculateExpiry(1800),
634
- };
635
- const resumeToken = await this.generateResumeToken(interceptedCall);
636
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
637
- this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
638
- );
639
- this.interceptedCalls.set(resumeToken, interceptedCall);
640
- this.cleanupExpiredInterceptedCalls();
641
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
642
- }
643
- // Unexpected error - log and fail securely
644
- if (this.config.audit?.enabled) {
645
- console.error("[MCP-I] ❌ Unexpected error during delegation verification", {
646
- tool: toolName,
647
- agentDid: identity.did.slice(0, 20) + "...",
648
- error: error instanceof Error ? error.message : String(error),
649
- errorStack: error instanceof Error ? error.stack : undefined,
650
- });
651
- }
652
- // Fail securely - require delegation on unexpected errors
653
- const interceptedCall = {
654
- toolName,
655
- args,
656
- sessionId: session?.id || "unknown",
657
- timestamp: this.clock.now(),
658
- expiresAt: this.clock.calculateExpiry(1800),
659
- };
660
- const resumeToken = await this.generateResumeToken(interceptedCall);
661
- const consentUrl = this.buildConsentUrl(toolName, protection.requiredScopes, session, resumeToken, undefined, // projectId - handled by subclass override
662
- this.getConsentProvider(protection) // Provider from tool config (supports both password and oauth auth)
663
- );
664
- this.interceptedCalls.set(resumeToken, interceptedCall);
665
- this.cleanupExpiredInterceptedCalls();
666
- throw new tool_protection_js_1.DelegationRequiredError(toolName, protection.requiredScopes, consentUrl, interceptedCall, resumeToken);
667
- }
668
- }
669
- }
670
- else {
671
- // No protection required - tool can be executed freely
672
- if (this.config.audit?.enabled) {
673
- console.error("[MCP-I] Tool protection check passed (no delegation required)", {
674
- tool: toolName,
675
- agentDid: identity.did.slice(0, 20) + "...",
676
- reason: "Tool not configured to require delegation",
677
- });
678
- }
679
- }
680
- }
681
- // Execute the tool
682
- const result = await handler(args);
683
- // Create proof
684
- const proof = await this.createProof(result, session);
685
- // Store proof for out-of-band retrieval
686
- this.lastProof = proof;
687
- // Log if audit is enabled
688
- if (this.config.audit?.enabled) {
689
- this.logAudit("tool_executed", {
690
- tool: toolName,
691
- sessionId: session?.id,
692
- timestamp: this.clock.now(),
693
- });
694
- }
695
- // Return clean result only (no proof in LLM context)
696
- return result;
697
- }
698
- /**
699
- * Resume a tool call after authorization
700
- *
701
- * @param resumeToken - Token from DelegationRequiredError
702
- * @param handler - Tool execution handler
703
- * @param delegationToken - Delegation token from authorization
704
- * @returns Tool execution result
705
- */
706
- async resumeToolCall(resumeToken, handler, delegationToken) {
707
- const interceptedCall = this.interceptedCalls.get(resumeToken);
708
- if (!interceptedCall) {
709
- throw new Error(`Invalid or expired resume token: ${resumeToken}`);
710
- }
711
- // Check if call has expired
712
- if (this.clock.hasExpired(interceptedCall.expiresAt)) {
713
- this.interceptedCalls.delete(resumeToken);
714
- throw new Error(`Resume token expired: ${resumeToken}`);
715
- }
716
- // Get session for the intercepted call
717
- const session = this.sessions.get(interceptedCall.sessionId);
718
- if (!session) {
719
- throw new Error(`Session not found for intercepted call: ${interceptedCall.sessionId}`);
720
- }
721
- // Add delegation token to session
722
- const enhancedSession = {
723
- ...session,
724
- delegationToken,
725
- };
726
- // Resume the tool call with delegation
727
- const result = await this.processToolCall(interceptedCall.toolName, interceptedCall.args, handler, enhancedSession);
728
- // Clean up intercepted call after successful resumption to prevent memory leak
729
- this.interceptedCalls.delete(resumeToken);
730
- return result;
731
- }
732
- /**
733
- * Generate a resume token for intercepted tool call
734
- *
735
- * Uses cryptographically secure random bytes for token generation.
736
- * The token is used as an opaque lookup key in the interceptedCalls Map.
737
- */
738
- async generateResumeToken(_call) {
739
- const bytes = await this.crypto.randomBytes(16);
740
- return `resume_${this.bytesToHex(bytes)}`;
741
- }
742
- /**
743
- * Clean up expired intercepted calls
744
- */
745
- cleanupExpiredInterceptedCalls() {
746
- const now = this.clock.now();
747
- for (const [token, call] of this.interceptedCalls.entries()) {
748
- if (this.clock.hasExpired(call.expiresAt)) {
749
- this.interceptedCalls.delete(token);
750
- }
751
- }
752
- }
753
- /**
754
- * Build consent URL for AgentShield delegation flow
755
- *
756
- * Note: Parameter names use snake_case for AgentShield API compatibility.
757
- * This is documented in docs/API_PARITY_GUIDE.md under "Field Naming Conventions".
758
- *
759
- * AgentShield API requires snake_case in URL parameters:
760
- * - session_id (not sessionId)
761
- * - agent_did (not agentDid)
762
- * - resume_token (not resumeToken)
763
- * - agent_name (human-readable agent name for dashboard display)
764
- *
765
- * @param toolName - Tool that requires delegation
766
- * @param scopes - Required scopes for the tool
767
- * @param session - Current session context
768
- * @param resumeToken - Token to resume after delegation
769
- * @param projectId - Project ID for AgentShield API
770
- * @param provider - Provider name (e.g., "github", "credentials") to select specific auth method
771
- * @returns Full consent URL with snake_case parameters
772
- */
773
- /**
774
- * MCP Apps UI consent resource URI.
775
- * Single shared resource for all consent flows - tool-specific data
776
- * is passed via structuredContent in the tool result.
777
- */
778
- static CONSENT_UI_RESOURCE_URI = "ui://mcpi-consent/authorize";
779
- /**
780
- * Check if the connected MCP client supports MCP Apps UI rendering.
781
- * Returns false if @modelcontextprotocol/ext-apps is not installed
782
- * or the client didn't advertise the io.modelcontextprotocol/ui capability.
783
- *
784
- * Override in subclasses to provide client capabilities from the MCP session.
785
- */
786
- hasUICapability(clientCapabilities) {
787
- if (!clientCapabilities) {
788
- console.error("[MCP-I] hasUICapability: no clientCapabilities provided");
789
- return false;
790
- }
791
- try {
792
- // eslint-disable-next-line @typescript-eslint/no-require-imports
793
- const { getUiCapability, RESOURCE_MIME_TYPE } = require("@modelcontextprotocol/ext-apps/server");
794
- const uiCap = getUiCapability(clientCapabilities);
795
- const hasUI = uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE) ?? false;
796
- console.error("[MCP-I] hasUICapability check:", {
797
- hasUI,
798
- uiCap: uiCap ? JSON.stringify(uiCap) : "null",
799
- capabilityKeys: Object.keys(clientCapabilities),
800
- });
801
- return hasUI;
802
- }
803
- catch {
804
- // ext-apps not installed - graceful degradation
805
- console.error("[MCP-I] hasUICapability: ext-apps not available");
806
- return false;
807
- }
808
- }
809
- /**
810
- * Mark the ext-apps SDK as available.
811
- * Called by subclasses after a successful dynamic import() of the SDK,
812
- * which avoids the CJS require() vs ESM incompatibility that causes
813
- * the synchronous probe to fail in Cloudflare Workers.
814
- */
815
- setExtAppsAvailable(available) {
816
- this._extAppsAvailable = available;
817
- }
818
- /**
819
- * Check if the MCP Apps inline consent UI is enabled.
820
- * Requires BOTH conditions:
821
- * 1. The SDK is available (set via setExtAppsAvailable(true) after dynamic import)
822
- * 2. The config flag is enabled (config.inlineConsent === true OR MCPI_INLINE_CONSENT env var)
823
- * This allows merging the feature while keeping it off by default.
824
- */
825
- isExtAppsAvailable() {
826
- return this._extAppsAvailable === true && this.config.inlineConsent === true;
827
- }
828
- /**
829
- * Build an MCP Apps-compatible tool result that renders an inline consent UI.
830
- * Returns null when the host doesn't support MCP Apps or the SDK is unavailable,
831
- * allowing callers to fall back to DelegationRequiredError.
832
- *
833
- * @param toolName - Tool requiring delegation
834
- * @param scopes - Required scopes
835
- * @param session - Current session context
836
- * @param resumeToken - Token to resume after delegation
837
- * @param projectId - AgentShield project ID
838
- * @param serverUrl - Server URL for consent approval POST
839
- * @param clientCapabilities - MCP client capabilities from the session
840
- */
841
- buildConsentUIResult(toolName, scopes, session, resumeToken, projectId, serverUrl, clientCapabilities, authMode = "consent-only", provider) {
842
- // Per the MCP Apps spec, always return UI metadata when the SDK is available.
843
- // Clients that support MCP Apps will render the inline iframe;
844
- // clients that don't will use the text content as fallback.
845
- // The capability check is logged but not gating.
846
- this.hasUICapability(clientCapabilities);
847
- if (!this.isExtAppsAvailable())
848
- return null;
849
- // Build fallback URL for graceful degradation within the UI itself
850
- const consentUrl = this.buildConsentUrl(toolName, scopes, session, resumeToken, projectId);
851
- return {
852
- content: [
853
- {
854
- type: "text",
855
- text: `Authorization required for tool "${toolName}". Please approve in the consent form below, or open this link to authorize in your browser: ${consentUrl}`,
856
- },
857
- ],
858
- structuredContent: {
859
- type: "consent_required",
860
- authMode,
861
- tool: toolName,
862
- scopes,
863
- agentDid: session?.agentDid || "",
864
- agentName: session?.agentName || "",
865
- sessionId: session?.id || "",
866
- projectId: projectId || "",
867
- resumeToken: resumeToken || "",
868
- serverUrl: serverUrl || "",
869
- consentUrl,
870
- ...(provider ? { provider } : {}),
871
- },
872
- _meta: {
873
- ui: {
874
- resourceUri: MCPIRuntimeBase.CONSENT_UI_RESOURCE_URI,
875
- },
876
- // Legacy flat format for backward compatibility with older clients
877
- "ui/resourceUri": MCPIRuntimeBase.CONSENT_UI_RESOURCE_URI,
878
- },
879
- };
880
- }
881
- buildConsentUrl(toolName, scopes, session, resumeToken, projectId, provider) {
882
- // Default implementation - override in subclasses
883
- // This URL should point to AgentShield's consent page
884
- // Parameter names use snake_case for AgentShield API compatibility
885
- const params = new URLSearchParams({
886
- tool: toolName,
887
- scopes: scopes.join(","),
888
- session_id: session?.id || "",
889
- agent_did: session?.agentDid || "",
890
- });
891
- // Add agent_name for AgentShield dashboard display
892
- // Priority: session.agentName > "Unknown Agent"
893
- const agentName = session?.agentName;
894
- if (agentName) {
895
- params.set("agent_name", agentName);
896
- }
897
- // Add project_id if provided (required for AgentShield consent endpoint)
898
- if (projectId) {
899
- params.set("project_id", projectId);
900
- }
901
- // Add resume token if provided
902
- if (resumeToken) {
903
- params.set("resume_token", resumeToken);
904
- }
905
- // Add provider if specified (allows selecting specific auth method like "credentials" or "github")
906
- // This is critical when multiple providers are configured for a project
907
- if (provider) {
908
- params.set("provider", provider);
909
- }
910
- // Use AgentShield consent endpoint
911
- return `https://kya.vouched.id/bouncer/consent?${params.toString()}`;
912
- }
913
- /**
914
- * Issue a new nonce and register it in the cache
915
- * Use this to get a nonce for the session context before calling processToolCall
916
- */
917
- async issueNonce(sessionId) {
918
- const nonce = await this.generateNonce();
919
- // Get session to extract agentDid for agent-scoped nonce caching
920
- const session = this.sessions.get(sessionId);
921
- const agentDid = session?.agentDid || (await this.getIdentity()).did;
922
- await this.nonceCache.add(nonce, 300, // 5 minute expiry in seconds
923
- agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
924
- );
925
- return nonce;
926
- }
927
- /**
928
- * Create cryptographic proof for data
929
- */
930
- async createProof(data, session) {
931
- const identity = await this.getIdentity();
932
- const timestamp = this.clock.now();
933
- // Use nonce from session if provided, otherwise generate new one
934
- let nonce;
935
- if (session?.nonce) {
936
- nonce = session.nonce;
937
- }
938
- else {
939
- nonce = await this.generateNonce();
940
- // Add nonce to cache to prevent replay (agent-scoped to prevent cross-agent replay attacks)
941
- const agentDid = session?.agentDid || identity.did;
942
- await this.nonceCache.add(nonce, 300, // 5 minute expiry in seconds
943
- agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
944
- );
945
- }
946
- const proofData = {
947
- data,
948
- timestamp,
949
- nonce,
950
- did: identity.did,
951
- sessionId: session?.id,
952
- audience: session?.audience,
953
- };
954
- const signature = await this.signData(proofData);
955
- return {
956
- timestamp,
957
- nonce,
958
- did: identity.did,
959
- signature,
960
- algorithm: "Ed25519",
961
- sessionId: session?.id,
962
- audience: session?.audience,
963
- };
964
- }
965
- /**
966
- * Verify a proof
967
- *
968
- * Supports both old format (data, proof) and new DetachedProof format.
969
- * When DetachedProof format is used, ProofVerifier is used if available.
970
- *
971
- * @param dataOrProof - Either raw data (old format) or DetachedProof (new format)
972
- * @param proofOrSession - Either proof object (old format) or session context (new format)
973
- * @returns true if proof is valid, false otherwise
974
- */
975
- async verifyProof(dataOrProof, proofOrSession) {
976
- // Check if first argument is DetachedProof format
977
- if (dataOrProof &&
978
- typeof dataOrProof === "object" &&
979
- "jws" in dataOrProof &&
980
- "meta" in dataOrProof) {
981
- // New DetachedProof format
982
- const detachedProof = dataOrProof;
983
- const session = isRecord(proofOrSession)
984
- ? proofOrSession
985
- : undefined;
986
- // Use ProofVerifier if available
987
- if (this.proofVerifier) {
988
- try {
989
- // Resolve DID to get public key
990
- const didDoc = await this.fetch.resolveDID(detachedProof.meta.did);
991
- const publicKeyJwk = this.extractPublicKeyJwk(didDoc, detachedProof.meta.kid);
992
- if (!publicKeyJwk) {
993
- console.error("[MCPIRuntimeBase] Failed to extract public key JWK");
994
- return false;
995
- }
996
- // Verify proof using ProofVerifier
997
- const result = await this.proofVerifier.verifyProof(detachedProof, publicKeyJwk);
998
- if (result.valid && session) {
999
- // Store canonical payload in session for detached JWS consumers
1000
- const canonicalPayload = this.proofVerifier.buildCanonicalPayload(detachedProof.meta);
1001
- session.canonicalPayload = canonicalPayload;
1002
- }
1003
- return result.valid;
1004
- }
1005
- catch (error) {
1006
- console.error("[MCPIRuntimeBase] Proof verification failed:", error);
1007
- return false;
1008
- }
1009
- }
1010
- else {
1011
- // Fallback to old verification if ProofVerifier not available
1012
- console.warn("[MCPIRuntimeBase] ProofVerifier not available, using fallback verification");
1013
- return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
1014
- }
1015
- }
1016
- else {
1017
- // Old format (data, proof)
1018
- return this.verifyProofLegacy(dataOrProof, isRecord(proofOrSession) ? proofOrSession : {});
1019
- }
1020
- }
1021
- /**
1022
- * Legacy proof verification (backward compatibility)
1023
- * @internal
1024
- */
1025
- async verifyProofLegacy(data, proof) {
1026
- try {
1027
- const nonce = typeof proof.nonce === "string" ? proof.nonce : undefined;
1028
- const did = typeof proof.did === "string" ? proof.did : undefined;
1029
- const timestamp = typeof proof.timestamp === "number" ? proof.timestamp : undefined;
1030
- const signature = typeof proof.signature === "string" ? proof.signature : undefined;
1031
- const sessionId = typeof proof.sessionId === "string" ? proof.sessionId : undefined;
1032
- if (!nonce || !did || !timestamp || !signature) {
1033
- return false;
1034
- }
1035
- // Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
1036
- if (await this.nonceCache.has(nonce, did)) {
1037
- return false;
1038
- }
1039
- // Check timestamp is within skew
1040
- if (!this.clock.isWithinSkew(timestamp, this.config.session?.timestampSkewSeconds || 300)) {
1041
- return false;
1042
- }
1043
- // Resolve DID to get public key
1044
- const didDoc = await this.fetch.resolveDID(did);
1045
- const publicKey = this.extractPublicKey(didDoc);
1046
- // Verify signature
1047
- const proofData = {
1048
- data,
1049
- timestamp,
1050
- nonce,
1051
- did,
1052
- sessionId,
1053
- };
1054
- const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
1055
- const signatureBytes = this.base64ToBytes(signature);
1056
- const isValid = await this.crypto.verify(dataBytes, signatureBytes, publicKey);
1057
- // If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
1058
- if (isValid) {
1059
- // Pass TTL in seconds, not absolute timestamp
1060
- const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
1061
- await this.nonceCache.add(nonce, ttlSeconds, did);
1062
- }
1063
- return isValid;
1064
- }
1065
- catch (error) {
1066
- console.error("Proof verification failed:", error);
1067
- return false;
1068
- }
1069
- }
1070
- /**
1071
- * Verify a JWS proof (full compact JWS format: header.payload.signature)
1072
- *
1073
- * This method provides full cryptographic signature verification using CryptoService.
1074
- * Use this when you have a DetachedProof with JWS instead of raw signature.
1075
- *
1076
- * @param jws - Full compact JWS string (header.payload.signature)
1077
- * @param publicKeyJwk - Ed25519 public key in JWK format
1078
- * @param detachedPayload - Optional detached payload for detached JWS format
1079
- * @returns true if signature is valid, false otherwise
1080
- */
1081
- async verifyProofJWS(jws, publicKeyJwk, detachedPayload) {
1082
- if (!this.cryptoService) {
1083
- console.error("[MCPIRuntimeBase] CryptoService not initialized");
1084
- return false;
1085
- }
1086
- try {
1087
- const options = detachedPayload !== undefined ? { detachedPayload } : undefined;
1088
- return await this.cryptoService.verifyJWS(jws, publicKeyJwk, options);
1089
- }
1090
- catch (error) {
1091
- console.error("[MCPIRuntimeBase] JWS verification failed:", error);
1092
- return false;
1093
- }
1094
- }
1095
- /**
1096
- * Get current session
1097
- */
1098
- async getCurrentSession() {
1099
- // Find non-expired session
1100
- for (const [id, session] of this.sessions) {
1101
- if (!this.clock.hasExpired(session.expiresAt)) {
1102
- return session;
1103
- }
1104
- }
1105
- return null;
1106
- }
1107
- /**
1108
- * Get the last generated proof for out-of-band transport
1109
- */
1110
- getLastProof() {
1111
- return this.lastProof;
1112
- }
1113
- /**
1114
- * Create well-known handler for identity verification
1115
- */
1116
- createWellKnownHandler(config) {
1117
- return async (path) => {
1118
- const identity = await this.getIdentity();
1119
- if (path === "/.well-known/did.json") {
1120
- const didDocument = this.createDIDDocument(identity);
1121
- return {
1122
- status: 200,
1123
- headers: {
1124
- "Content-Type": "application/did+json",
1125
- "Cache-Control": this.config.environment === "production"
1126
- ? "public, max-age=300"
1127
- : "no-store",
1128
- },
1129
- body: JSON.stringify(didDocument, null, 2),
1130
- };
1131
- }
1132
- if (path === "/.well-known/agent.json") {
1133
- const agentDocument = {
1134
- id: identity.did,
1135
- capabilities: {
1136
- "mcp-i": ["handshake", "signing", "verification"],
1137
- },
1138
- };
1139
- if (config?.serviceName || config?.serviceEndpoint) {
1140
- agentDocument.metadata = {
1141
- ...(config?.serviceName && { name: config.serviceName }),
1142
- ...(config?.serviceEndpoint && {
1143
- serviceEndpoint: config.serviceEndpoint,
1144
- }),
1145
- };
1146
- }
1147
- return {
1148
- status: 200,
1149
- headers: {
1150
- "Content-Type": "application/json",
1151
- "Cache-Control": this.config.environment === "production"
1152
- ? "public, max-age=300"
1153
- : "no-store",
1154
- },
1155
- body: JSON.stringify(agentDocument, null, 2),
1156
- };
1157
- }
1158
- if (path === "/.well-known/mcp-identity") {
1159
- return {
1160
- did: identity.did,
1161
- publicKey: identity.publicKey,
1162
- serviceName: config?.serviceName || "MCP-I Service",
1163
- serviceEndpoint: config?.serviceEndpoint || "https://example.com",
1164
- timestamp: this.clock.now(),
1165
- };
1166
- }
1167
- // SEP-1649 MCP Server Card for client discovery
1168
- // Support both paths during spec transition:
1169
- // - /.well-known/mcp.json (current Cursor/Claude Desktop usage)
1170
- // - /.well-known/mcp/server-card.json (SEP-1649 proposed spec)
1171
- if (path === "/.well-known/mcp.json" || path === "/.well-known/mcp/server-card.json") {
1172
- const serverName = config?.serviceName || "mcp-i-server";
1173
- // SEP-1649 compliant MCP Server Card
1174
- const mcpServerCard = {
1175
- $schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
1176
- version: "1.0",
1177
- protocolVersion: config?.protocolVersion || "2025-06-18",
1178
- serverInfo: {
1179
- name: serverName,
1180
- title: config?.serviceTitle || config?.serviceName || "MCP-I Server",
1181
- version: config?.serverVersion || "1.0.0",
1182
- },
1183
- description: config?.description || "MCP-I enabled server with cryptographic identity",
1184
- ...(config?.iconUrl && { iconUrl: config.iconUrl }),
1185
- ...(config?.documentationUrl && { documentationUrl: config.documentationUrl }),
1186
- transport: {
1187
- type: "streamable-http",
1188
- endpoint: "/mcp",
1189
- },
1190
- capabilities: {
1191
- tools: { listChanged: true },
1192
- },
1193
- ...(config?.requiresAuth && {
1194
- authentication: {
1195
- required: true,
1196
- schemes: config.authSchemes || ["oauth2"],
1197
- },
1198
- }),
1199
- ...(config?.instructions && { instructions: config.instructions }),
1200
- // Tools are dynamic - discovered via protocol
1201
- tools: "dynamic",
1202
- _meta: {
1203
- ...config?.metadata,
1204
- "mcp-i": {
1205
- agentDid: identity.did,
1206
- publicKey: identity.publicKey,
1207
- generatedAt: this.clock.now(),
1208
- },
1209
- },
1210
- };
1211
- const headers = {
1212
- "Content-Type": "application/json",
1213
- "Cache-Control": this.config.environment === "production"
1214
- ? "public, max-age=3600" // SEP-1649 recommends 1 hour
1215
- : "no-store",
1216
- // SEP-1649 CORS requirements for browser-based client discovery
1217
- "Access-Control-Allow-Origin": "*",
1218
- "Access-Control-Allow-Methods": "GET",
1219
- "Access-Control-Allow-Headers": "Content-Type",
1220
- };
1221
- return {
1222
- status: 200,
1223
- headers,
1224
- body: JSON.stringify(mcpServerCard, null, 2),
1225
- };
1226
- }
1227
- // Client configuration helper - ready-to-use config for Cursor/Claude Desktop
1228
- if (path === "/.well-known/mcp-client-config.json") {
1229
- const serviceEndpoint = config?.serviceEndpoint || "https://example.com";
1230
- const serverName = config?.serviceName || "mcp-i-server";
1231
- const mcpUrl = `${serviceEndpoint}/mcp`;
1232
- // Generate Cursor deep link
1233
- // Note: Base64 can contain +, /, = which have special meaning in URLs
1234
- // Must URL-encode the base64 string for safe transmission
1235
- const cursorConfig = { [serverName]: { url: mcpUrl, transport: "sse" } };
1236
- const encodedConfig = Buffer.from(JSON.stringify(cursorConfig)).toString("base64");
1237
- const cursorDeepLink = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(serverName)}&config=${encodeURIComponent(encodedConfig)}`;
1238
- const clientConfig = {
1239
- // For claude_desktop_config.json
1240
- claudeDesktop: {
1241
- mcpServers: {
1242
- [serverName]: {
1243
- url: mcpUrl,
1244
- transport: "sse",
1245
- },
1246
- },
1247
- },
1248
- // For .cursor/mcp.json
1249
- cursor: {
1250
- mcpServers: {
1251
- [serverName]: {
1252
- url: mcpUrl,
1253
- transport: "sse",
1254
- },
1255
- },
1256
- },
1257
- // One-click install for Cursor
1258
- cursorDeepLink,
1259
- // Additional info
1260
- _meta: {
1261
- serverName,
1262
- serverUrl: serviceEndpoint,
1263
- mcpEndpoint: mcpUrl,
1264
- agentDid: identity.did,
1265
- generatedAt: this.clock.now(),
1266
- instructions: "Copy the appropriate config section to your client configuration file.",
1267
- },
1268
- };
1269
- return {
1270
- status: 200,
1271
- headers: {
1272
- "Content-Type": "application/json",
1273
- "Cache-Control": this.config.environment === "production"
1274
- ? "public, max-age=300"
1275
- : "no-store",
1276
- },
1277
- body: JSON.stringify(clientConfig, null, 2),
1278
- };
1279
- }
1280
- return null;
1281
- };
1282
- }
1283
- /**
1284
- * Create debug endpoint (development only)
1285
- */
1286
- createDebugEndpoint() {
1287
- if (this.config.environment === "production") {
1288
- return null;
1289
- }
1290
- return async () => {
1291
- const identity = await this.getIdentity();
1292
- const session = await this.getCurrentSession();
1293
- return {
1294
- identity: {
1295
- did: identity.did,
1296
- publicKey: identity.publicKey,
1297
- },
1298
- session,
1299
- config: {
1300
- environment: this.config.environment,
1301
- timestampSkewSeconds: this.config.session?.timestampSkewSeconds,
1302
- sessionTtlMinutes: this.config.session?.ttlMinutes,
1303
- },
1304
- timestamp: this.clock.now(),
1305
- };
1306
- };
1307
- }
1308
- /**
1309
- * Get audit logger
1310
- */
1311
- getAuditLogger() {
1312
- return {
1313
- log: (event, data) => this.logAudit(event, data),
1314
- };
1315
- }
1316
- /**
1317
- * Rotate keys
1318
- */
1319
- async rotateKeys() {
1320
- const oldDid = this.cachedIdentity?.did;
1321
- const newIdentity = await this.identity.rotateKeys();
1322
- this.cachedIdentity = newIdentity;
1323
- if (this.config.audit?.enabled) {
1324
- this.logAudit("keys_rotated", {
1325
- oldDid: oldDid,
1326
- newDid: newIdentity.did,
1327
- timestamp: this.clock.now(),
1328
- });
1329
- }
1330
- return newIdentity;
1331
- }
1332
- // Helper methods
1333
- async signData(data) {
1334
- const identity = await this.getIdentity();
1335
- const dataBytes = new TextEncoder().encode(JSON.stringify(data));
1336
- const signature = await this.crypto.sign(dataBytes, identity.privateKey);
1337
- return this.bytesToBase64(signature);
1338
- }
1339
- async generateNonce() {
1340
- const bytes = await this.crypto.randomBytes(32);
1341
- return this.bytesToBase64(bytes);
1342
- }
1343
- /**
1344
- * Generate a unique session ID
1345
- * Uses mcpi_{uuid} format for MCP protocol compliance and traceability
1346
- */
1347
- async generateSessionId() {
1348
- const bytes = await this.crypto.randomBytes(16);
1349
- // Set version (4) and variant (RFC4122)
1350
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
1351
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
1352
- const hex = this.bytesToHex(bytes);
1353
- const uuid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
1354
- return `mcpi_${uuid}`;
1355
- }
1356
- /**
1357
- * Log structured events in JSON format (NOT frozen audit format).
1358
- *
1359
- * **Important:** This method logs events in JSON format for general runtime events
1360
- * (e.g., "runtime_initialized", "tool_executed", "keys_rotated").
1361
- *
1362
- * **For frozen format audit logs** (MCP-I spec compliance), use `AuditLogger.logAuditRecord()`
1363
- * from `@kya-os/mcp-i/runtime` instead. The frozen format is:
1364
- * ```
1365
- * audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
1366
- * ```
1367
- *
1368
- * **Format:** JSON object with `event`, `data`, `timestamp`, and `timestampFormatted` fields.
1369
- *
1370
- * **Privacy:** If `includePayloads` is false (default), the `data` field is omitted.
1371
- *
1372
- * **Use Cases:**
1373
- * - Developer debugging and local logging
1374
- * - Runtime initialization events
1375
- * - Tool execution tracking (non-spec-compliant)
1376
- * - Key rotation events
1377
- *
1378
- * **NOT for:**
1379
- * - MCP-I spec-compliant audit logs (use `AuditLogger`)
1380
- * - Production audit trails (use `AuditLogger`)
1381
- * - Compliance requirements (use `AuditLogger`)
1382
- *
1383
- * @param event - Event name (e.g., "runtime_initialized", "tool_executed")
1384
- * @param data - Event data (only included if `includePayloads` is true)
1385
- *
1386
- * @example
1387
- * ```typescript
1388
- * // Logs: {"event":"runtime_initialized","timestamp":1234567890,"timestampFormatted":"2024-01-01T00:00:00Z"}
1389
- * this.logAudit("runtime_initialized", { did: "did:key:..." });
1390
- * ```
1391
- *
1392
- * @internal This is a private method for internal runtime events.
1393
- * Use AuditLogger for spec-compliant frozen format audit logs.
1394
- */
1395
- logAudit(event, data) {
1396
- if (!this.config.audit?.enabled)
1397
- return;
1398
- const record = {
1399
- event,
1400
- data: this.config.audit.includePayloads ? data : undefined,
1401
- timestamp: this.clock.now(),
1402
- timestampFormatted: this.clock.format(this.clock.now()),
1403
- };
1404
- const logLine = JSON.stringify(record);
1405
- if (this.config.audit.logFunction) {
1406
- this.config.audit.logFunction(logLine);
1407
- }
1408
- else {
1409
- console.error("[AUDIT]", logLine);
1410
- }
1411
- }
1412
- createDIDDocument(identity) {
1413
- return {
1414
- "@context": ["https://www.w3.org/ns/did/v1"],
1415
- id: identity.did,
1416
- verificationMethod: [
1417
- {
1418
- id: `${identity.did}#key-1`,
1419
- type: "Ed25519VerificationKey2020",
1420
- controller: identity.did,
1421
- // Using base64 for cross-platform compatibility (Cloudflare Workers)
1422
- // W3C DID spec supports both base64 and multibase formats
1423
- publicKeyBase64: identity.publicKey,
1424
- },
1425
- ],
1426
- authentication: [`${identity.did}#key-1`],
1427
- assertionMethod: [`${identity.did}#key-1`],
1428
- };
1429
- }
1430
- extractPublicKey(didDoc) {
1431
- const verificationMethods = isRecord(didDoc)
1432
- ? didDoc.verificationMethod
1433
- : undefined;
1434
- const method = Array.isArray(verificationMethods)
1435
- ? verificationMethods[0]
1436
- : undefined;
1437
- if (method?.publicKeyBase64) {
1438
- return String(method.publicKeyBase64);
1439
- }
1440
- if (method?.publicKeyMultibase) {
1441
- // Convert multibase to base64
1442
- return String(method.publicKeyMultibase); // Simplified
1443
- }
1444
- throw new Error("Public key not found in DID document");
1445
- }
1446
- /**
1447
- * Extract public key JWK from DID document
1448
- */
1449
- extractPublicKeyJwk(didDoc, kid) {
1450
- const verificationMethods = isRecord(didDoc)
1451
- ? didDoc.verificationMethod
1452
- : undefined;
1453
- const verificationMethodList = Array.isArray(verificationMethods)
1454
- ? verificationMethods
1455
- : [];
1456
- // Try to find Ed25519 public key matching kid if provided
1457
- const verificationMethod = verificationMethodList.find((vm) => {
1458
- const vmType = typeof vm.type === "string" ? vm.type : undefined;
1459
- const vmId = typeof vm.id === "string" ? vm.id : undefined;
1460
- const matchesType = vmType === "Ed25519VerificationKey2020" ||
1461
- vmType === "JsonWebKey2020";
1462
- const matchesKid = !kid || vmId === kid || vmId?.endsWith(`#${kid}`);
1463
- return matchesType && matchesKid;
1464
- }) || verificationMethodList[0]; // Fallback to first method
1465
- if (verificationMethod && isRecord(verificationMethod.publicKeyJwk)) {
1466
- const jwk = verificationMethod.publicKeyJwk;
1467
- // Ensure it's Ed25519 format
1468
- if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
1469
- return jwk;
1470
- }
1471
- }
1472
- // Fallback: try to convert multibase to JWK (simplified)
1473
- if (verificationMethod?.publicKeyMultibase) {
1474
- // This is a simplified conversion - in production, use proper multibase decoding
1475
- console.warn("[MCPIRuntimeBase] Multibase to JWK conversion not fully implemented");
1476
- return null;
1477
- }
1478
- return null;
1479
- }
1480
- bytesToBase64(bytes) {
1481
- return Buffer.from(bytes).toString("base64");
1482
- }
1483
- base64ToBytes(base64) {
1484
- return new Uint8Array(Buffer.from(base64, "base64"));
1485
- }
1486
- bytesToHex(bytes) {
1487
- return Buffer.from(bytes).toString("hex");
1488
- }
1489
- }
1490
- exports.MCPIRuntimeBase = MCPIRuntimeBase;
1491
- //# sourceMappingURL=base.js.map