@kya-os/mcp-i-core 1.2.3-canary.6 → 1.3.0

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 (231) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.turbo/turbo-build.log +4 -0
  3. package/.turbo/turbo-test$colon$coverage.log +4514 -0
  4. package/.turbo/turbo-test.log +2973 -0
  5. package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
  6. package/Composer 3.md +615 -0
  7. package/GPT-5.md +1169 -0
  8. package/OPUS-plan.md +352 -0
  9. package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
  10. package/PHASE_3_SUMMARY.md +317 -0
  11. package/PHASE_4.1.3_SUMMARY.md +428 -0
  12. package/PHASE_4.1_COMPLETE.md +525 -0
  13. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
  14. package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
  15. package/TEST_PLAN.md +571 -0
  16. package/coverage/coverage-final.json +57 -0
  17. package/dist/__tests__/utils/mock-providers.d.ts +1 -2
  18. package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
  19. package/dist/__tests__/utils/mock-providers.js.map +1 -1
  20. package/dist/cache/oauth-config-cache.d.ts +69 -0
  21. package/dist/cache/oauth-config-cache.d.ts.map +1 -0
  22. package/dist/cache/oauth-config-cache.js +76 -0
  23. package/dist/cache/oauth-config-cache.js.map +1 -0
  24. package/dist/identity/idp-token-resolver.d.ts +53 -0
  25. package/dist/identity/idp-token-resolver.d.ts.map +1 -0
  26. package/dist/identity/idp-token-resolver.js +108 -0
  27. package/dist/identity/idp-token-resolver.js.map +1 -0
  28. package/dist/identity/idp-token-storage.interface.d.ts +42 -0
  29. package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
  30. package/dist/identity/idp-token-storage.interface.js +12 -0
  31. package/dist/identity/idp-token-storage.interface.js.map +1 -0
  32. package/dist/identity/user-did-manager.d.ts +39 -1
  33. package/dist/identity/user-did-manager.d.ts.map +1 -1
  34. package/dist/identity/user-did-manager.js +69 -3
  35. package/dist/identity/user-did-manager.js.map +1 -1
  36. package/dist/index.d.ts +22 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +39 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/runtime/audit-logger.d.ts +37 -0
  41. package/dist/runtime/audit-logger.d.ts.map +1 -0
  42. package/dist/runtime/audit-logger.js +9 -0
  43. package/dist/runtime/audit-logger.js.map +1 -0
  44. package/dist/runtime/base.d.ts +58 -2
  45. package/dist/runtime/base.d.ts.map +1 -1
  46. package/dist/runtime/base.js +266 -11
  47. package/dist/runtime/base.js.map +1 -1
  48. package/dist/services/access-control.service.d.ts.map +1 -1
  49. package/dist/services/access-control.service.js +200 -35
  50. package/dist/services/access-control.service.js.map +1 -1
  51. package/dist/services/authorization/authorization-registry.d.ts +29 -0
  52. package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
  53. package/dist/services/authorization/authorization-registry.js +57 -0
  54. package/dist/services/authorization/authorization-registry.js.map +1 -0
  55. package/dist/services/authorization/types.d.ts +53 -0
  56. package/dist/services/authorization/types.d.ts.map +1 -0
  57. package/dist/services/authorization/types.js +10 -0
  58. package/dist/services/authorization/types.js.map +1 -0
  59. package/dist/services/batch-delegation.service.d.ts +53 -0
  60. package/dist/services/batch-delegation.service.d.ts.map +1 -0
  61. package/dist/services/batch-delegation.service.js +95 -0
  62. package/dist/services/batch-delegation.service.js.map +1 -0
  63. package/dist/services/oauth-config.service.d.ts +53 -0
  64. package/dist/services/oauth-config.service.d.ts.map +1 -0
  65. package/dist/services/oauth-config.service.js +117 -0
  66. package/dist/services/oauth-config.service.js.map +1 -0
  67. package/dist/services/oauth-provider-registry.d.ts +77 -0
  68. package/dist/services/oauth-provider-registry.d.ts.map +1 -0
  69. package/dist/services/oauth-provider-registry.js +112 -0
  70. package/dist/services/oauth-provider-registry.js.map +1 -0
  71. package/dist/services/oauth-service.d.ts +77 -0
  72. package/dist/services/oauth-service.d.ts.map +1 -0
  73. package/dist/services/oauth-service.js +348 -0
  74. package/dist/services/oauth-service.js.map +1 -0
  75. package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
  76. package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
  77. package/dist/services/oauth-token-retrieval.service.js +150 -0
  78. package/dist/services/oauth-token-retrieval.service.js.map +1 -0
  79. package/dist/services/provider-resolver.d.ts +48 -0
  80. package/dist/services/provider-resolver.d.ts.map +1 -0
  81. package/dist/services/provider-resolver.js +120 -0
  82. package/dist/services/provider-resolver.js.map +1 -0
  83. package/dist/services/provider-validator.d.ts +55 -0
  84. package/dist/services/provider-validator.d.ts.map +1 -0
  85. package/dist/services/provider-validator.js +135 -0
  86. package/dist/services/provider-validator.js.map +1 -0
  87. package/dist/services/tool-context-builder.d.ts +57 -0
  88. package/dist/services/tool-context-builder.d.ts.map +1 -0
  89. package/dist/services/tool-context-builder.js +125 -0
  90. package/dist/services/tool-context-builder.js.map +1 -0
  91. package/dist/services/tool-protection.service.d.ts +87 -10
  92. package/dist/services/tool-protection.service.d.ts.map +1 -1
  93. package/dist/services/tool-protection.service.js +282 -112
  94. package/dist/services/tool-protection.service.js.map +1 -1
  95. package/dist/types/oauth-required-error.d.ts +40 -0
  96. package/dist/types/oauth-required-error.d.ts.map +1 -0
  97. package/dist/types/oauth-required-error.js +40 -0
  98. package/dist/types/oauth-required-error.js.map +1 -0
  99. package/dist/utils/did-helpers.d.ts +33 -0
  100. package/dist/utils/did-helpers.d.ts.map +1 -1
  101. package/dist/utils/did-helpers.js +40 -0
  102. package/dist/utils/did-helpers.js.map +1 -1
  103. package/dist/utils/index.d.ts +1 -0
  104. package/dist/utils/index.d.ts.map +1 -1
  105. package/dist/utils/index.js +1 -0
  106. package/dist/utils/index.js.map +1 -1
  107. package/docs/API_REFERENCE.md +1362 -0
  108. package/docs/COMPLIANCE_MATRIX.md +691 -0
  109. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  110. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  111. package/package.json +24 -50
  112. package/scripts/audit-compliance.ts +724 -0
  113. package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
  114. package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
  115. package/src/__tests__/delegation-e2e.test.ts +690 -0
  116. package/src/__tests__/identity/user-did-manager.test.ts +213 -0
  117. package/src/__tests__/index.test.ts +56 -0
  118. package/src/__tests__/integration/full-flow.test.ts +776 -0
  119. package/src/__tests__/integration.test.ts +281 -0
  120. package/src/__tests__/providers/base.test.ts +173 -0
  121. package/src/__tests__/providers/memory.test.ts +319 -0
  122. package/src/__tests__/regression/phase2-regression.test.ts +427 -0
  123. package/src/__tests__/runtime/audit-logger.test.ts +154 -0
  124. package/src/__tests__/runtime/base-extensions.test.ts +593 -0
  125. package/src/__tests__/runtime/base.test.ts +869 -0
  126. package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
  127. package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
  128. package/src/__tests__/runtime/route-interception.test.ts +686 -0
  129. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
  130. package/src/__tests__/services/agentshield-integration.test.ts +784 -0
  131. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
  132. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
  133. package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
  134. package/src/__tests__/utils/mock-providers.ts +340 -0
  135. package/src/cache/oauth-config-cache.d.ts +69 -0
  136. package/src/cache/oauth-config-cache.d.ts.map +1 -0
  137. package/src/cache/oauth-config-cache.js +71 -0
  138. package/src/cache/oauth-config-cache.js.map +1 -0
  139. package/src/cache/oauth-config-cache.ts +123 -0
  140. package/src/cache/tool-protection-cache.ts +171 -0
  141. package/src/compliance/EXAMPLE.md +412 -0
  142. package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
  143. package/src/compliance/index.ts +8 -0
  144. package/src/compliance/schema-registry.ts +460 -0
  145. package/src/compliance/schema-verifier.ts +708 -0
  146. package/src/config/__tests__/remote-config.spec.ts +268 -0
  147. package/src/config/remote-config.ts +174 -0
  148. package/src/config.ts +309 -0
  149. package/src/delegation/__tests__/audience-validator.test.ts +112 -0
  150. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  151. package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
  152. package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
  153. package/src/delegation/__tests__/utils.test.ts +152 -0
  154. package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
  155. package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
  156. package/src/delegation/audience-validator.ts +52 -0
  157. package/src/delegation/bitstring.ts +278 -0
  158. package/src/delegation/cascading-revocation.ts +370 -0
  159. package/src/delegation/delegation-graph.ts +299 -0
  160. package/src/delegation/index.ts +14 -0
  161. package/src/delegation/statuslist-manager.ts +353 -0
  162. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  163. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  164. package/src/delegation/storage/index.ts +9 -0
  165. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  166. package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
  167. package/src/delegation/utils.ts +42 -0
  168. package/src/delegation/vc-issuer.ts +232 -0
  169. package/src/delegation/vc-verifier.ts +568 -0
  170. package/src/identity/idp-token-resolver.ts +147 -0
  171. package/src/identity/idp-token-storage.interface.ts +59 -0
  172. package/src/identity/user-did-manager.ts +370 -0
  173. package/src/index.ts +260 -0
  174. package/src/providers/base.d.ts +91 -0
  175. package/src/providers/base.d.ts.map +1 -0
  176. package/src/providers/base.js +38 -0
  177. package/src/providers/base.js.map +1 -0
  178. package/src/providers/base.ts +96 -0
  179. package/src/providers/memory.ts +142 -0
  180. package/src/runtime/audit-logger.ts +39 -0
  181. package/src/runtime/base.ts +1329 -0
  182. package/src/services/__tests__/access-control.integration.test.ts +443 -0
  183. package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
  184. package/src/services/__tests__/access-control.service.test.ts +970 -0
  185. package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
  186. package/src/services/__tests__/crypto.service.test.ts +531 -0
  187. package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
  188. package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
  189. package/src/services/__tests__/proof-verifier.test.ts +489 -0
  190. package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
  191. package/src/services/__tests__/provider-resolver.test.ts +217 -0
  192. package/src/services/__tests__/storage.service.test.ts +358 -0
  193. package/src/services/access-control.service.ts +990 -0
  194. package/src/services/authorization/authorization-registry.ts +66 -0
  195. package/src/services/authorization/types.ts +71 -0
  196. package/src/services/batch-delegation.service.ts +137 -0
  197. package/src/services/crypto.service.ts +302 -0
  198. package/src/services/errors.ts +76 -0
  199. package/src/services/index.ts +9 -0
  200. package/src/services/oauth-config.service.d.ts +53 -0
  201. package/src/services/oauth-config.service.d.ts.map +1 -0
  202. package/src/services/oauth-config.service.js +113 -0
  203. package/src/services/oauth-config.service.js.map +1 -0
  204. package/src/services/oauth-config.service.ts +166 -0
  205. package/src/services/oauth-provider-registry.d.ts +57 -0
  206. package/src/services/oauth-provider-registry.d.ts.map +1 -0
  207. package/src/services/oauth-provider-registry.js +73 -0
  208. package/src/services/oauth-provider-registry.js.map +1 -0
  209. package/src/services/oauth-provider-registry.ts +123 -0
  210. package/src/services/oauth-service.ts +510 -0
  211. package/src/services/oauth-token-retrieval.service.ts +245 -0
  212. package/src/services/proof-verifier.ts +478 -0
  213. package/src/services/provider-resolver.d.ts +48 -0
  214. package/src/services/provider-resolver.d.ts.map +1 -0
  215. package/src/services/provider-resolver.js +106 -0
  216. package/src/services/provider-resolver.js.map +1 -0
  217. package/src/services/provider-resolver.ts +144 -0
  218. package/src/services/provider-validator.ts +170 -0
  219. package/src/services/storage.service.ts +566 -0
  220. package/src/services/tool-context-builder.ts +172 -0
  221. package/src/services/tool-protection.service.ts +958 -0
  222. package/src/types/oauth-required-error.ts +63 -0
  223. package/src/types/tool-protection.ts +155 -0
  224. package/src/utils/__tests__/did-helpers.test.ts +101 -0
  225. package/src/utils/base64.ts +148 -0
  226. package/src/utils/cors.ts +83 -0
  227. package/src/utils/did-helpers.ts +150 -0
  228. package/src/utils/index.ts +8 -0
  229. package/src/utils/storage-keys.ts +278 -0
  230. package/tsconfig.json +21 -0
  231. package/vitest.config.ts +56 -0
@@ -0,0 +1,1329 @@
1
+ /**
2
+ * MCPIRuntimeBase - Provider-based runtime
3
+ *
4
+ * Core runtime that accepts providers for all platform-specific operations.
5
+ * This enables the same runtime logic to work across Node.js, Cloudflare Workers,
6
+ * and other platforms.
7
+ */
8
+
9
+ import {
10
+ CryptoProvider,
11
+ ClockProvider,
12
+ FetchProvider,
13
+ StorageProvider,
14
+ NonceCacheProvider,
15
+ IdentityProvider,
16
+ AgentIdentity,
17
+ } from "../providers/base";
18
+ import { DelegationRequiredError } from "../types/tool-protection.js";
19
+ import { CryptoService, type Ed25519JWK } from "../services/crypto.service.js";
20
+ import { ProofVerifier } from "../services/proof-verifier.js";
21
+ import type { DetachedProof } from "@kya-os/contracts/proof";
22
+ import type {
23
+ DIDDocument,
24
+ AgentDocument,
25
+ MCPIdentity,
26
+ WellKnownConfig,
27
+ WellKnownResponse,
28
+ } from "@kya-os/contracts/well-known";
29
+ import type { AccessControlApiService } from "../services/access-control.service.js";
30
+ import type { VerifyDelegationRequest } from "@kya-os/contracts/agentshield-api";
31
+ import { AgentShieldAPIError } from "@kya-os/contracts/agentshield-api";
32
+
33
+ // Import the new provider runtime config
34
+ import type { ProviderRuntimeConfig } from "../config";
35
+ import { UserDidManager } from "../identity/user-did-manager";
36
+ import type { InterceptedToolCall } from "../types/tool-protection.js";
37
+
38
+ /**
39
+ * Interface for runtime instances that have AccessControlApiService available
40
+ * This allows type-safe access to the access control service without using `as any`
41
+ *
42
+ * @deprecated AccessControlApiService is now directly available as protected property on MCPIRuntimeBase
43
+ */
44
+ export interface RuntimeWithAccessControl {
45
+ accessControlService?: AccessControlApiService;
46
+ }
47
+
48
+ export class MCPIRuntimeBase {
49
+ protected crypto: CryptoProvider;
50
+ protected clock: ClockProvider;
51
+ protected fetch: FetchProvider;
52
+ protected storage: StorageProvider;
53
+ protected nonceCache: NonceCacheProvider;
54
+ protected identity: IdentityProvider;
55
+ protected config: ProviderRuntimeConfig;
56
+ private cachedIdentity?: AgentIdentity;
57
+ private sessions: Map<string, any> = new Map();
58
+ private lastProof?: any;
59
+ private userDidManager?: UserDidManager;
60
+ private interceptedCalls: Map<string, any> = new Map(); // Store intercepted tool calls by resume token
61
+ private cryptoService?: CryptoService;
62
+ protected proofVerifier?: ProofVerifier; // Optional ProofVerifier (injected by subclasses)
63
+ protected accessControlService?: AccessControlApiService; // Optional AccessControlApiService (injected by subclasses)
64
+
65
+ constructor(config: ProviderRuntimeConfig) {
66
+ this.config = config;
67
+ this.crypto = config.cryptoProvider;
68
+ this.clock = config.clockProvider;
69
+ this.fetch = config.fetchProvider;
70
+ this.storage = config.storageProvider;
71
+ this.nonceCache = config.nonceCacheProvider;
72
+ this.identity = config.identityProvider;
73
+ // Initialize CryptoService for JWS verification
74
+ this.cryptoService = new CryptoService(this.crypto);
75
+ }
76
+
77
+ /**
78
+ * Initialize the runtime
79
+ */
80
+ async initialize(): Promise<void> {
81
+ // Load or generate identity
82
+ this.cachedIdentity = await this.identity.getIdentity();
83
+
84
+ // Initialize user DID manager if identity config enables it
85
+ if (this.config.identity?.generateUserDids) {
86
+ this.userDidManager = new UserDidManager({
87
+ crypto: this.crypto,
88
+ storage: this.config.identity?.userDidStorage
89
+ ? {
90
+ get: async (key: string) =>
91
+ await this.storage.get(`userDid:${key}`),
92
+ set: async (key: string, value: string, ttl?: number) => {
93
+ await this.storage.set(`userDid:${key}`, value);
94
+ },
95
+ delete: async (key: string) =>
96
+ await this.storage.delete(`userDid:${key}`),
97
+ }
98
+ : undefined,
99
+ useDidWeb: this.config.identity?.userDidStorage === "persistent",
100
+ });
101
+ }
102
+
103
+ // Initialize nonce cache if it has an initialize method
104
+ if (
105
+ "initialize" in this.nonceCache &&
106
+ typeof (this.nonceCache as any).initialize === "function"
107
+ ) {
108
+ await (this.nonceCache as any).initialize();
109
+ }
110
+
111
+ // Log initialization if audit is enabled
112
+ if (this.config.audit?.enabled) {
113
+ this.logAudit("runtime_initialized", {
114
+ did: this.cachedIdentity.did,
115
+ environment: this.config.environment || "development",
116
+ userDidGeneration: this.config.identity?.generateUserDids
117
+ ? "enabled"
118
+ : "disabled",
119
+ });
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Get the current agent identity
125
+ */
126
+ async getIdentity(): Promise<AgentIdentity> {
127
+ if (!this.cachedIdentity) {
128
+ this.cachedIdentity = await this.identity.getIdentity();
129
+ }
130
+ return this.cachedIdentity;
131
+ }
132
+
133
+ /**
134
+ * Handle handshake request
135
+ */
136
+ /**
137
+ * Handle MCP handshake request
138
+ *
139
+ * @param request - Handshake request object (may include oauthIdentity for persistent user DID lookup)
140
+ * @returns Handshake response with session ID and agent DID
141
+ *
142
+ * @remarks
143
+ * - Accepts optional oauthIdentity via request.oauthIdentity (backward compatible)
144
+ * - If OAuth identity provided, uses it to retrieve/create persistent user DID
145
+ * - Falls back to ephemeral user DID generation if OAuth unavailable
146
+ */
147
+ async handleHandshake(
148
+ request: any & {
149
+ oauthIdentity?:
150
+ | import("../identity/user-did-manager").OAuthIdentity
151
+ | null;
152
+ }
153
+ ): Promise<any> {
154
+ const identity = await this.getIdentity();
155
+ const timestamp = this.clock.now();
156
+ const sessionId = await this.generateSessionId();
157
+
158
+ // Generate user DID if user DID generation is enabled
159
+ // Use OAuth identity if provided for persistent user DID lookup
160
+ let userDid: string | undefined;
161
+ if (this.userDidManager) {
162
+ try {
163
+ const oauthIdentity = request.oauthIdentity;
164
+ userDid = await this.userDidManager.getOrCreateUserDid(
165
+ sessionId,
166
+ oauthIdentity
167
+ );
168
+ if (this.config.audit?.enabled) {
169
+ console.log("[MCP-I] Generated user DID for session:", {
170
+ userDid: userDid.substring(0, 20) + "...",
171
+ hasOAuth: !!oauthIdentity,
172
+ provider: oauthIdentity?.provider,
173
+ });
174
+ }
175
+ } catch (error) {
176
+ console.warn("[MCP-I] Failed to generate user DID:", error);
177
+ // Continue without user DID - not critical
178
+ }
179
+ }
180
+
181
+ // Extract client info if available
182
+ const normalizeString = (value: unknown): string | undefined => {
183
+ if (typeof value !== "string") {
184
+ return undefined;
185
+ }
186
+ const trimmed = value.trim();
187
+ return trimmed.length > 0 ? trimmed : undefined;
188
+ };
189
+
190
+ const protocolVersion = normalizeString(request.clientProtocolVersion);
191
+ const clientCapabilities = request.clientCapabilities;
192
+
193
+ const requestClientInfo = request.clientInfo;
194
+ const shouldPersistClientInfo =
195
+ requestClientInfo ||
196
+ typeof protocolVersion === "string" ||
197
+ typeof clientCapabilities !== "undefined";
198
+
199
+ const clientInfo = shouldPersistClientInfo
200
+ ? {
201
+ name: normalizeString(requestClientInfo?.name) ?? "unknown",
202
+ title: normalizeString(requestClientInfo?.title),
203
+ version: normalizeString(requestClientInfo?.version),
204
+ platform: normalizeString(requestClientInfo?.platform),
205
+ vendor: normalizeString(requestClientInfo?.vendor),
206
+ persistentId: normalizeString(requestClientInfo?.persistentId),
207
+ clientId:
208
+ normalizeString(requestClientInfo?.clientId) ?? crypto.randomUUID(),
209
+ protocolVersion,
210
+ capabilities: clientCapabilities,
211
+ }
212
+ : undefined;
213
+
214
+ // Create session
215
+ const session = {
216
+ id: sessionId,
217
+ clientDid: request.clientDid || userDid, // Use provided clientDid or generated userDid
218
+ userDid: userDid, // Store generated user DID separately
219
+ agentDid: request.agentDid, // ✅ FIXED: Only agent DID, no fallback
220
+ serverDid: identity.did, // ✅ NEW: Server's DID (for clarity)
221
+ audience: request.audience,
222
+ createdAt: timestamp,
223
+ expiresAt: this.clock.calculateExpiry(
224
+ (this.config.session?.ttlMinutes || 30) * 60
225
+ ),
226
+ clientInfo, // NEW: Store client information
227
+ };
228
+
229
+ this.sessions.set(sessionId, session);
230
+
231
+ // Create handshake response
232
+ const response = {
233
+ sessionId,
234
+ agentDid: identity.did,
235
+ timestamp,
236
+ capabilities: ["identity", "proof", "audit"],
237
+ ...(userDid && { userDid }), // Include user DID in response if generated
238
+ };
239
+
240
+ // Sign the response
241
+ const signature = await this.signData(response);
242
+
243
+ return {
244
+ ...response,
245
+ signature,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Process tool call with automatic proof generation
251
+ * Returns clean result only - proof is stored for out-of-band retrieval
252
+ *
253
+ * @param toolName - Name of the tool being called
254
+ * @param args - Tool arguments
255
+ * @param handler - Tool execution handler
256
+ * @param session - Session context (expected fields: id, audience, nonce?, delegationToken?, consentProof?)
257
+ */
258
+ async processToolCall(
259
+ toolName: string,
260
+ args: any,
261
+ handler: (args: any) => Promise<any>,
262
+ session?: any
263
+ ): Promise<any> {
264
+ // Check tool protection (delegation requirement)
265
+ if (this.config.toolProtectionService) {
266
+ // Get agent identity to check protection
267
+ const identity = await this.getIdentity();
268
+
269
+ if (this.config.audit?.enabled) {
270
+ console.log("[MCP-I] Checking tool protection:", {
271
+ tool: toolName,
272
+ agentDid: identity.did.slice(0, 20) + "...",
273
+ hasDelegation: !!(session?.delegationToken || session?.consentProof),
274
+ });
275
+ }
276
+
277
+ const protection =
278
+ await this.config.toolProtectionService.checkToolProtection(
279
+ toolName,
280
+ identity.did
281
+ );
282
+
283
+ if (protection) {
284
+ // Tool requires delegation
285
+ const hasDelegation = session?.delegationToken || session?.consentProof;
286
+
287
+ if (!hasDelegation) {
288
+ // No delegation provided - intercept the tool call and store context
289
+ // Store intercepted tool call context for resumption
290
+ const interceptedCall: InterceptedToolCall = {
291
+ toolName,
292
+ args,
293
+ sessionId: session?.id || "unknown",
294
+ timestamp: this.clock.now(),
295
+ expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
296
+ };
297
+
298
+ // Generate resume token
299
+ const resumeToken = this.generateResumeToken(interceptedCall);
300
+
301
+ // Build consent URL with resume token
302
+ // Note: projectId is not available in base class - subclasses should override buildConsentUrl
303
+ const consentUrl = this.buildConsentUrl(
304
+ toolName,
305
+ protection.requiredScopes,
306
+ session,
307
+ resumeToken
308
+ );
309
+
310
+ // Create error with intercepted call context and pre-generated resume token
311
+ const error = new DelegationRequiredError(
312
+ toolName,
313
+ protection.requiredScopes,
314
+ consentUrl,
315
+ interceptedCall,
316
+ resumeToken
317
+ );
318
+
319
+ // Store intercepted call for resumption
320
+ this.interceptedCalls.set(resumeToken, interceptedCall);
321
+
322
+ // Clean up expired intercepted calls periodically
323
+ this.cleanupExpiredInterceptedCalls();
324
+
325
+ if (this.config.audit?.enabled) {
326
+ console.warn(
327
+ "[MCP-I] BLOCKED: Tool requires delegation but none provided",
328
+ {
329
+ tool: toolName,
330
+ requiredScopes: protection.requiredScopes,
331
+ agentDid: identity.did.slice(0, 20) + "...",
332
+ resumeToken: error.resumeToken,
333
+ consentUrl,
334
+ }
335
+ );
336
+ }
337
+
338
+ throw error;
339
+ }
340
+
341
+ // Delegation provided - verify it with AccessControlApiService
342
+ const delegationToken = session?.delegationToken;
343
+ const consentProof = session?.consentProof;
344
+
345
+ if (!this.accessControlService) {
346
+ // Access control service not available - log warning but allow execution
347
+ // This enables graceful degradation when service is not configured
348
+ if (this.config.audit?.enabled) {
349
+ console.warn(
350
+ "[MCP-I] ⚠️ Delegation token provided but AccessControlApiService not configured - skipping verification",
351
+ {
352
+ tool: toolName,
353
+ agentDid: identity.did.slice(0, 20) + "...",
354
+ hasDelegationToken: !!delegationToken,
355
+ hasConsentProof: !!consentProof,
356
+ }
357
+ );
358
+ }
359
+ } else {
360
+ // Verify delegation token with AccessControlApiService
361
+ try {
362
+ if (this.config.audit?.enabled) {
363
+ console.log(
364
+ "[MCP-I] 🔐 Verifying delegation token with AccessControlApiService",
365
+ {
366
+ tool: toolName,
367
+ agentDid: identity.did.slice(0, 20) + "...",
368
+ hasDelegationToken: !!delegationToken,
369
+ hasConsentProof: !!consentProof,
370
+ requiredScopes: protection.requiredScopes,
371
+ }
372
+ );
373
+ }
374
+
375
+ // Build verification request
376
+ const verifyRequest: VerifyDelegationRequest = {
377
+ agent_did: identity.did,
378
+ scopes: protection.requiredScopes,
379
+ };
380
+
381
+ // Add delegation token if available (preferred over consent proof)
382
+ if (delegationToken) {
383
+ verifyRequest.delegation_token = delegationToken;
384
+ } else if (consentProof) {
385
+ // Consent proof is a JWT credential - use as credential_jwt
386
+ verifyRequest.credential_jwt = consentProof;
387
+ }
388
+
389
+ // Add optional timestamp for verification
390
+ verifyRequest.timestamp = this.clock.now();
391
+
392
+ // Add client info from session if available
393
+ if (session?.clientDid || session?.clientId) {
394
+ verifyRequest.client_info = {
395
+ origin: session?.serverOrigin,
396
+ user_agent: session?.userAgent,
397
+ };
398
+ }
399
+
400
+ // Perform verification
401
+ const verificationResult =
402
+ await this.accessControlService.verifyDelegation(verifyRequest, {
403
+ delegationToken: delegationToken || undefined,
404
+ credentialJwt: consentProof || undefined,
405
+ });
406
+
407
+ // Check verification result
408
+ if (!verificationResult.data.valid) {
409
+ // Delegation verification failed
410
+ const reason =
411
+ verificationResult.data.reason ||
412
+ "Delegation token invalid or expired";
413
+ const errorDetails = verificationResult.data.error;
414
+
415
+ if (this.config.audit?.enabled) {
416
+ console.error("[MCP-I] ❌ Delegation verification FAILED", {
417
+ tool: toolName,
418
+ agentDid: identity.did.slice(0, 20) + "...",
419
+ reason,
420
+ errorCode: errorDetails?.code,
421
+ errorMessage: errorDetails?.message,
422
+ requiredScopes: protection.requiredScopes,
423
+ });
424
+ }
425
+
426
+ // Throw DelegationRequiredError to trigger consent flow
427
+ const interceptedCall: InterceptedToolCall = {
428
+ toolName,
429
+ args,
430
+ sessionId: session?.id || "unknown",
431
+ timestamp: this.clock.now(),
432
+ expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
433
+ };
434
+
435
+ const resumeToken = this.generateResumeToken(interceptedCall);
436
+ const consentUrl = this.buildConsentUrl(
437
+ toolName,
438
+ protection.requiredScopes,
439
+ session,
440
+ resumeToken
441
+ );
442
+
443
+ this.interceptedCalls.set(resumeToken, interceptedCall);
444
+ this.cleanupExpiredInterceptedCalls();
445
+
446
+ throw new DelegationRequiredError(
447
+ toolName,
448
+ protection.requiredScopes,
449
+ consentUrl,
450
+ interceptedCall,
451
+ resumeToken
452
+ );
453
+ }
454
+
455
+ // ✅ SECURITY: Validate user_identifier matches session userDid
456
+ // This ensures delegations are user-specific and prevents user isolation bypass
457
+ const credential = verificationResult.data.credential;
458
+ const delegationUserIdentifier = credential?.user_identifier;
459
+ const sessionUserDid = session?.userDid;
460
+
461
+ if (delegationUserIdentifier && sessionUserDid) {
462
+ if (delegationUserIdentifier !== sessionUserDid) {
463
+ // User identifier mismatch - potential security issue
464
+ const securityError = `Delegation user_identifier mismatch: delegation has "${delegationUserIdentifier.substring(0, 20)}..." but session has "${sessionUserDid.substring(0, 20)}..."`;
465
+
466
+ if (this.config.audit?.enabled) {
467
+ console.error(
468
+ "[MCP-I] 🔒 SECURITY: User identifier validation FAILED",
469
+ {
470
+ tool: toolName,
471
+ agentDid: identity.did.slice(0, 20) + "...",
472
+ delegationUserIdentifier:
473
+ delegationUserIdentifier.substring(0, 20) + "...",
474
+ sessionUserDid: sessionUserDid.substring(0, 20) + "...",
475
+ sessionId: session?.id?.substring(0, 20) + "...",
476
+ reason: "user_identifier_mismatch",
477
+ severity: "high",
478
+ }
479
+ );
480
+ }
481
+
482
+ // Throw DelegationRequiredError to force re-authentication
483
+ const interceptedCall: InterceptedToolCall = {
484
+ toolName,
485
+ args,
486
+ sessionId: session?.id || "unknown",
487
+ timestamp: this.clock.now(),
488
+ expiresAt: this.clock.calculateExpiry(1800), // 30 minutes
489
+ };
490
+
491
+ const resumeToken = this.generateResumeToken(interceptedCall);
492
+ const consentUrl = this.buildConsentUrl(
493
+ toolName,
494
+ protection.requiredScopes,
495
+ session,
496
+ resumeToken
497
+ );
498
+
499
+ this.interceptedCalls.set(resumeToken, interceptedCall);
500
+ this.cleanupExpiredInterceptedCalls();
501
+
502
+ throw new DelegationRequiredError(
503
+ toolName,
504
+ protection.requiredScopes,
505
+ consentUrl,
506
+ interceptedCall,
507
+ resumeToken
508
+ );
509
+ }
510
+
511
+ // User identifier matches - log success for audit
512
+ if (this.config.audit?.enabled) {
513
+ console.log("[MCP-I] ✅ User identifier validation PASSED", {
514
+ tool: toolName,
515
+ agentDid: identity.did.slice(0, 20) + "...",
516
+ userDid: sessionUserDid.substring(0, 20) + "...",
517
+ sessionId: session?.id?.substring(0, 20) + "...",
518
+ });
519
+ }
520
+ } else if (delegationUserIdentifier && !sessionUserDid) {
521
+ // Delegation has user_identifier but session doesn't - log warning
522
+ if (this.config.audit?.enabled) {
523
+ console.warn(
524
+ "[MCP-I] ⚠️ Delegation has user_identifier but session missing userDid",
525
+ {
526
+ tool: toolName,
527
+ agentDid: identity.did.slice(0, 20) + "...",
528
+ delegationUserIdentifier:
529
+ delegationUserIdentifier.substring(0, 20) + "...",
530
+ sessionId: session?.id?.substring(0, 20) + "...",
531
+ }
532
+ );
533
+ }
534
+ }
535
+
536
+ // Verification succeeded
537
+ if (this.config.audit?.enabled) {
538
+ console.log("[MCP-I] ✅ Delegation verification SUCCEEDED", {
539
+ tool: toolName,
540
+ agentDid: identity.did.slice(0, 20) + "...",
541
+ delegationId: verificationResult.data.delegation_id,
542
+ credentialScopes: verificationResult.data.credential?.scopes,
543
+ requiredScopes: protection.requiredScopes,
544
+ });
545
+ }
546
+ } catch (error: any) {
547
+ // Handle verification errors
548
+ if (error instanceof DelegationRequiredError) {
549
+ // Re-throw DelegationRequiredError as-is (already handled above)
550
+ throw error;
551
+ }
552
+
553
+ // Handle AgentShieldAPIError (network errors, API errors, etc.)
554
+ if (error instanceof AgentShieldAPIError) {
555
+ if (this.config.audit?.enabled) {
556
+ console.error(
557
+ "[MCP-I] ❌ Delegation verification error (API failure)",
558
+ {
559
+ tool: toolName,
560
+ agentDid: identity.did.slice(0, 20) + "...",
561
+ errorCode: error.code,
562
+ errorMessage: error.message,
563
+ errorDetails: error.details,
564
+ }
565
+ );
566
+ }
567
+
568
+ // On API errors, fail securely by requiring delegation
569
+ // This prevents unauthorized access when verification service is unavailable
570
+ const interceptedCall: InterceptedToolCall = {
571
+ toolName,
572
+ args,
573
+ sessionId: session?.id || "unknown",
574
+ timestamp: this.clock.now(),
575
+ expiresAt: this.clock.calculateExpiry(1800),
576
+ };
577
+
578
+ const resumeToken = this.generateResumeToken(interceptedCall);
579
+ const consentUrl = this.buildConsentUrl(
580
+ toolName,
581
+ protection.requiredScopes,
582
+ session,
583
+ resumeToken
584
+ );
585
+
586
+ this.interceptedCalls.set(resumeToken, interceptedCall);
587
+ this.cleanupExpiredInterceptedCalls();
588
+
589
+ throw new DelegationRequiredError(
590
+ toolName,
591
+ protection.requiredScopes,
592
+ consentUrl,
593
+ interceptedCall,
594
+ resumeToken
595
+ );
596
+ }
597
+
598
+ // Unexpected error - log and fail securely
599
+ if (this.config.audit?.enabled) {
600
+ console.error(
601
+ "[MCP-I] ❌ Unexpected error during delegation verification",
602
+ {
603
+ tool: toolName,
604
+ agentDid: identity.did.slice(0, 20) + "...",
605
+ error: error.message || String(error),
606
+ errorStack: error.stack,
607
+ }
608
+ );
609
+ }
610
+
611
+ // Fail securely - require delegation on unexpected errors
612
+ const interceptedCall: InterceptedToolCall = {
613
+ toolName,
614
+ args,
615
+ sessionId: session?.id || "unknown",
616
+ timestamp: this.clock.now(),
617
+ expiresAt: this.clock.calculateExpiry(1800),
618
+ };
619
+
620
+ const resumeToken = this.generateResumeToken(interceptedCall);
621
+ const consentUrl = this.buildConsentUrl(
622
+ toolName,
623
+ protection.requiredScopes,
624
+ session,
625
+ resumeToken
626
+ );
627
+
628
+ this.interceptedCalls.set(resumeToken, interceptedCall);
629
+ this.cleanupExpiredInterceptedCalls();
630
+
631
+ throw new DelegationRequiredError(
632
+ toolName,
633
+ protection.requiredScopes,
634
+ consentUrl,
635
+ interceptedCall,
636
+ resumeToken
637
+ );
638
+ }
639
+ }
640
+ } else {
641
+ // No protection required - tool can be executed freely
642
+ if (this.config.audit?.enabled) {
643
+ console.log(
644
+ "[MCP-I] Tool protection check passed (no delegation required)",
645
+ {
646
+ tool: toolName,
647
+ agentDid: identity.did.slice(0, 20) + "...",
648
+ reason: "Tool not configured to require delegation",
649
+ }
650
+ );
651
+ }
652
+ }
653
+ }
654
+
655
+ // Execute the tool
656
+ const result = await handler(args);
657
+
658
+ // Create proof
659
+ const proof = await this.createProof(result, session);
660
+
661
+ // Store proof for out-of-band retrieval
662
+ this.lastProof = proof;
663
+
664
+ // Log if audit is enabled
665
+ if (this.config.audit?.enabled) {
666
+ this.logAudit("tool_executed", {
667
+ tool: toolName,
668
+ sessionId: session?.id,
669
+ timestamp: this.clock.now(),
670
+ });
671
+ }
672
+
673
+ // Return clean result only (no proof in LLM context)
674
+ return result;
675
+ }
676
+
677
+ /**
678
+ * Resume a tool call after authorization
679
+ *
680
+ * @param resumeToken - Token from DelegationRequiredError
681
+ * @param handler - Tool execution handler
682
+ * @param delegationToken - Delegation token from authorization
683
+ * @returns Tool execution result
684
+ */
685
+ async resumeToolCall(
686
+ resumeToken: string,
687
+ handler: (args: any) => Promise<any>,
688
+ delegationToken?: string
689
+ ): Promise<any> {
690
+ const interceptedCall = this.interceptedCalls.get(resumeToken);
691
+
692
+ if (!interceptedCall) {
693
+ throw new Error(`Invalid or expired resume token: ${resumeToken}`);
694
+ }
695
+
696
+ // Check if call has expired
697
+ if (this.clock.hasExpired(interceptedCall.expiresAt)) {
698
+ this.interceptedCalls.delete(resumeToken);
699
+ throw new Error(`Resume token expired: ${resumeToken}`);
700
+ }
701
+
702
+ // Get session for the intercepted call
703
+ const session = this.sessions.get(interceptedCall.sessionId);
704
+ if (!session) {
705
+ throw new Error(
706
+ `Session not found for intercepted call: ${interceptedCall.sessionId}`
707
+ );
708
+ }
709
+
710
+ // Add delegation token to session
711
+ const enhancedSession = {
712
+ ...session,
713
+ delegationToken,
714
+ };
715
+
716
+ // Resume the tool call with delegation
717
+ const result = await this.processToolCall(
718
+ interceptedCall.toolName,
719
+ interceptedCall.args,
720
+ handler,
721
+ enhancedSession
722
+ );
723
+
724
+ // Clean up intercepted call after successful resumption to prevent memory leak
725
+ this.interceptedCalls.delete(resumeToken);
726
+
727
+ return result;
728
+ }
729
+
730
+ /**
731
+ * Generate a resume token for intercepted tool call
732
+ */
733
+ private generateResumeToken(call: InterceptedToolCall): string {
734
+ // Create a deterministic token from the call context
735
+ const tokenData = JSON.stringify({
736
+ tool: call.toolName,
737
+ args: call.args,
738
+ sessionId: call.sessionId,
739
+ timestamp: call.timestamp,
740
+ });
741
+
742
+ // Simple hash-based token (in production, use proper crypto)
743
+ let hash = 0;
744
+ for (let i = 0; i < tokenData.length; i++) {
745
+ const char = tokenData.charCodeAt(i);
746
+ hash = (hash << 5) - hash + char;
747
+ hash = hash & hash; // Convert to 32bit integer
748
+ }
749
+
750
+ return `resume_${Math.abs(hash).toString(36)}_${Date.now().toString(36)}`;
751
+ }
752
+
753
+ /**
754
+ * Clean up expired intercepted calls
755
+ */
756
+ private cleanupExpiredInterceptedCalls(): void {
757
+ const now = this.clock.now();
758
+ for (const [token, call] of this.interceptedCalls.entries()) {
759
+ if (this.clock.hasExpired(call.expiresAt)) {
760
+ this.interceptedCalls.delete(token);
761
+ }
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Build consent URL for AgentShield delegation flow
767
+ *
768
+ * Note: Parameter names use snake_case for AgentShield API compatibility.
769
+ * This is documented in docs/API_PARITY_GUIDE.md under "Field Naming Conventions".
770
+ *
771
+ * AgentShield API requires snake_case in URL parameters:
772
+ * - session_id (not sessionId)
773
+ * - agent_did (not agentDid)
774
+ * - resume_token (not resumeToken)
775
+ *
776
+ * @param toolName - Tool that requires delegation
777
+ * @param scopes - Required scopes for the tool
778
+ * @param session - Current session context
779
+ * @param resumeToken - Token to resume after delegation
780
+ * @param projectId - Project ID for AgentShield API
781
+ * @returns Full consent URL with snake_case parameters
782
+ */
783
+ protected buildConsentUrl(
784
+ toolName: string,
785
+ scopes: string[],
786
+ session?: any,
787
+ resumeToken?: string,
788
+ projectId?: string
789
+ ): string {
790
+ // Default implementation - override in subclasses
791
+ // This URL should point to AgentShield's consent page
792
+ // Parameter names use snake_case for AgentShield API compatibility
793
+ const params = new URLSearchParams({
794
+ tool: toolName,
795
+ scopes: scopes.join(","),
796
+ session_id: session?.id || "",
797
+ agent_did: session?.agentDid || "",
798
+ });
799
+
800
+ // Add project_id if provided (required for AgentShield consent endpoint)
801
+ if (projectId) {
802
+ params.set("project_id", projectId);
803
+ }
804
+
805
+ // Add resume token if provided
806
+ if (resumeToken) {
807
+ params.set("resume_token", resumeToken);
808
+ }
809
+
810
+ // Use AgentShield consent endpoint
811
+ return `https://kya.vouched.id/bouncer/consent?${params.toString()}`;
812
+ }
813
+
814
+ /**
815
+ * Issue a new nonce and register it in the cache
816
+ * Use this to get a nonce for the session context before calling processToolCall
817
+ */
818
+ async issueNonce(sessionId: string): Promise<string> {
819
+ const nonce = await this.generateNonce();
820
+ // Get session to extract agentDid for agent-scoped nonce caching
821
+ const session = this.sessions.get(sessionId);
822
+ const agentDid = session?.agentDid || (await this.getIdentity()).did;
823
+ await this.nonceCache.add(
824
+ nonce,
825
+ 300, // 5 minute expiry in seconds
826
+ agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
827
+ );
828
+ return nonce;
829
+ }
830
+
831
+ /**
832
+ * Create cryptographic proof for data
833
+ */
834
+ async createProof(data: any, session?: any): Promise<any> {
835
+ const identity = await this.getIdentity();
836
+ const timestamp = this.clock.now();
837
+
838
+ // Use nonce from session if provided, otherwise generate new one
839
+ let nonce: string;
840
+ if (session?.nonce) {
841
+ nonce = session.nonce;
842
+ } else {
843
+ nonce = await this.generateNonce();
844
+ // Add nonce to cache to prevent replay (agent-scoped to prevent cross-agent replay attacks)
845
+ const agentDid = session?.agentDid || identity.did;
846
+ await this.nonceCache.add(
847
+ nonce,
848
+ 300, // 5 minute expiry in seconds
849
+ agentDid // Agent-scoped nonce to prevent cross-agent replay attacks
850
+ );
851
+ }
852
+
853
+ const proofData = {
854
+ data,
855
+ timestamp,
856
+ nonce,
857
+ did: identity.did,
858
+ sessionId: session?.id,
859
+ audience: session?.audience,
860
+ };
861
+
862
+ const signature = await this.signData(proofData);
863
+
864
+ return {
865
+ timestamp,
866
+ nonce,
867
+ did: identity.did,
868
+ signature,
869
+ algorithm: "Ed25519",
870
+ sessionId: session?.id,
871
+ audience: session?.audience,
872
+ };
873
+ }
874
+
875
+ /**
876
+ * Verify a proof
877
+ *
878
+ * Supports both old format (data, proof) and new DetachedProof format.
879
+ * When DetachedProof format is used, ProofVerifier is used if available.
880
+ *
881
+ * @param dataOrProof - Either raw data (old format) or DetachedProof (new format)
882
+ * @param proofOrSession - Either proof object (old format) or session context (new format)
883
+ * @returns true if proof is valid, false otherwise
884
+ */
885
+ async verifyProof(dataOrProof: any, proofOrSession?: any): Promise<boolean> {
886
+ // Check if first argument is DetachedProof format
887
+ if (
888
+ dataOrProof &&
889
+ typeof dataOrProof === "object" &&
890
+ "jws" in dataOrProof &&
891
+ "meta" in dataOrProof
892
+ ) {
893
+ // New DetachedProof format
894
+ const detachedProof = dataOrProof as DetachedProof;
895
+ const session = proofOrSession;
896
+
897
+ // Use ProofVerifier if available
898
+ if (this.proofVerifier) {
899
+ try {
900
+ // Resolve DID to get public key
901
+ const didDoc = await this.fetch.resolveDID(detachedProof.meta.did);
902
+ const publicKeyJwk = this.extractPublicKeyJwk(
903
+ didDoc,
904
+ detachedProof.meta.kid
905
+ );
906
+
907
+ if (!publicKeyJwk) {
908
+ console.error("[MCPIRuntimeBase] Failed to extract public key JWK");
909
+ return false;
910
+ }
911
+
912
+ // Verify proof using ProofVerifier
913
+ const result = await this.proofVerifier.verifyProof(
914
+ detachedProof,
915
+ publicKeyJwk
916
+ );
917
+
918
+ if (result.valid && session) {
919
+ // Store canonical payload in session for detached JWS consumers
920
+ const canonicalPayload = this.proofVerifier.buildCanonicalPayload(
921
+ detachedProof.meta
922
+ );
923
+ session.canonicalPayload = canonicalPayload;
924
+ }
925
+
926
+ return result.valid;
927
+ } catch (error) {
928
+ console.error("[MCPIRuntimeBase] Proof verification failed:", error);
929
+ return false;
930
+ }
931
+ } else {
932
+ // Fallback to old verification if ProofVerifier not available
933
+ console.warn(
934
+ "[MCPIRuntimeBase] ProofVerifier not available, using fallback verification"
935
+ );
936
+ return this.verifyProofLegacy(dataOrProof, proofOrSession);
937
+ }
938
+ } else {
939
+ // Old format (data, proof)
940
+ return this.verifyProofLegacy(dataOrProof, proofOrSession);
941
+ }
942
+ }
943
+
944
+ /**
945
+ * Legacy proof verification (backward compatibility)
946
+ * @internal
947
+ */
948
+ private async verifyProofLegacy(data: any, proof: any): Promise<boolean> {
949
+ try {
950
+ // Check nonce hasn't been used (scoped to agent DID to prevent cross-agent replay attacks)
951
+ if (await this.nonceCache.has(proof.nonce, proof.did)) {
952
+ return false;
953
+ }
954
+
955
+ // Check timestamp is within skew
956
+ if (
957
+ !this.clock.isWithinSkew(
958
+ proof.timestamp,
959
+ this.config.session?.timestampSkewSeconds || 120
960
+ )
961
+ ) {
962
+ return false;
963
+ }
964
+
965
+ // Resolve DID to get public key
966
+ const didDoc = await this.fetch.resolveDID(proof.did);
967
+ const publicKey = this.extractPublicKey(didDoc);
968
+
969
+ // Verify signature
970
+ const proofData = {
971
+ data,
972
+ timestamp: proof.timestamp,
973
+ nonce: proof.nonce,
974
+ did: proof.did,
975
+ sessionId: proof.sessionId,
976
+ };
977
+
978
+ const dataBytes = new TextEncoder().encode(JSON.stringify(proofData));
979
+ const signatureBytes = this.base64ToBytes(proof.signature);
980
+
981
+ const isValid = await this.crypto.verify(
982
+ dataBytes,
983
+ signatureBytes,
984
+ publicKey
985
+ );
986
+
987
+ // If signature is valid, add nonce to cache to prevent replay (scoped to agent DID)
988
+ if (isValid) {
989
+ // Pass TTL in seconds, not absolute timestamp
990
+ const ttlSeconds = (this.config.session?.ttlMinutes || 30) * 60; // Convert minutes to seconds
991
+ await this.nonceCache.add(proof.nonce, ttlSeconds, proof.did);
992
+ }
993
+
994
+ return isValid;
995
+ } catch (error) {
996
+ console.error("Proof verification failed:", error);
997
+ return false;
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Verify a JWS proof (full compact JWS format: header.payload.signature)
1003
+ *
1004
+ * This method provides full cryptographic signature verification using CryptoService.
1005
+ * Use this when you have a DetachedProof with JWS instead of raw signature.
1006
+ *
1007
+ * @param jws - Full compact JWS string (header.payload.signature)
1008
+ * @param publicKeyJwk - Ed25519 public key in JWK format
1009
+ * @param detachedPayload - Optional detached payload for detached JWS format
1010
+ * @returns true if signature is valid, false otherwise
1011
+ */
1012
+ async verifyProofJWS(
1013
+ jws: string,
1014
+ publicKeyJwk: Ed25519JWK,
1015
+ detachedPayload?: string | Uint8Array
1016
+ ): Promise<boolean> {
1017
+ if (!this.cryptoService) {
1018
+ console.error("[MCPIRuntimeBase] CryptoService not initialized");
1019
+ return false;
1020
+ }
1021
+
1022
+ try {
1023
+ const options =
1024
+ detachedPayload !== undefined ? { detachedPayload } : undefined;
1025
+ return await this.cryptoService.verifyJWS(jws, publicKeyJwk, options);
1026
+ } catch (error) {
1027
+ console.error("[MCPIRuntimeBase] JWS verification failed:", error);
1028
+ return false;
1029
+ }
1030
+ }
1031
+
1032
+ /**
1033
+ * Get current session
1034
+ */
1035
+ async getCurrentSession(): Promise<any> {
1036
+ // Find non-expired session
1037
+ for (const [id, session] of this.sessions) {
1038
+ if (!this.clock.hasExpired(session.expiresAt)) {
1039
+ return session;
1040
+ }
1041
+ }
1042
+ return null;
1043
+ }
1044
+
1045
+ /**
1046
+ * Get the last generated proof for out-of-band transport
1047
+ */
1048
+ getLastProof(): any {
1049
+ return this.lastProof;
1050
+ }
1051
+
1052
+ /**
1053
+ * Create well-known handler for identity verification
1054
+ */
1055
+ createWellKnownHandler(
1056
+ config?: WellKnownConfig
1057
+ ): (path: string) => Promise<WellKnownResponse | MCPIdentity | null> {
1058
+ return async (path: string) => {
1059
+ const identity = await this.getIdentity();
1060
+
1061
+ if (path === "/.well-known/did.json") {
1062
+ const didDocument = this.createDIDDocument(identity);
1063
+ return {
1064
+ status: 200,
1065
+ headers: {
1066
+ "Content-Type": "application/did+json",
1067
+ "Cache-Control":
1068
+ this.config.environment === "production"
1069
+ ? "public, max-age=300"
1070
+ : "no-store",
1071
+ },
1072
+ body: JSON.stringify(didDocument, null, 2),
1073
+ };
1074
+ }
1075
+
1076
+ if (path === "/.well-known/agent.json") {
1077
+ const agentDocument: AgentDocument = {
1078
+ id: identity.did,
1079
+ capabilities: {
1080
+ "mcp-i": ["handshake", "signing", "verification"],
1081
+ },
1082
+ };
1083
+
1084
+ if (config?.serviceName || config?.serviceEndpoint) {
1085
+ agentDocument.metadata = {
1086
+ ...(config?.serviceName && { name: config.serviceName }),
1087
+ ...(config?.serviceEndpoint && {
1088
+ serviceEndpoint: config.serviceEndpoint,
1089
+ }),
1090
+ };
1091
+ }
1092
+
1093
+ return {
1094
+ status: 200,
1095
+ headers: {
1096
+ "Content-Type": "application/json",
1097
+ "Cache-Control":
1098
+ this.config.environment === "production"
1099
+ ? "public, max-age=300"
1100
+ : "no-store",
1101
+ },
1102
+ body: JSON.stringify(agentDocument, null, 2),
1103
+ };
1104
+ }
1105
+
1106
+ if (path === "/.well-known/mcp-identity") {
1107
+ return {
1108
+ did: identity.did,
1109
+ publicKey: identity.publicKey,
1110
+ serviceName: config?.serviceName || "MCP-I Service",
1111
+ serviceEndpoint: config?.serviceEndpoint || "https://example.com",
1112
+ timestamp: this.clock.now(),
1113
+ };
1114
+ }
1115
+
1116
+ return null;
1117
+ };
1118
+ }
1119
+
1120
+ /**
1121
+ * Create debug endpoint (development only)
1122
+ */
1123
+ createDebugEndpoint(): any {
1124
+ if (this.config.environment === "production") {
1125
+ return null;
1126
+ }
1127
+
1128
+ return async () => {
1129
+ const identity = await this.getIdentity();
1130
+ const session = await this.getCurrentSession();
1131
+
1132
+ return {
1133
+ identity: {
1134
+ did: identity.did,
1135
+ publicKey: identity.publicKey,
1136
+ },
1137
+ session,
1138
+ config: {
1139
+ environment: this.config.environment,
1140
+ timestampSkewSeconds: this.config.session?.timestampSkewSeconds,
1141
+ sessionTtlMinutes: this.config.session?.ttlMinutes,
1142
+ },
1143
+ timestamp: this.clock.now(),
1144
+ };
1145
+ };
1146
+ }
1147
+
1148
+ /**
1149
+ * Get audit logger
1150
+ */
1151
+ getAuditLogger(): any {
1152
+ return {
1153
+ log: (event: string, data: any) => this.logAudit(event, data),
1154
+ };
1155
+ }
1156
+
1157
+ /**
1158
+ * Rotate keys
1159
+ */
1160
+ async rotateKeys(): Promise<AgentIdentity> {
1161
+ const oldDid = this.cachedIdentity?.did;
1162
+ const newIdentity = await this.identity.rotateKeys();
1163
+ this.cachedIdentity = newIdentity;
1164
+
1165
+ if (this.config.audit?.enabled) {
1166
+ this.logAudit("keys_rotated", {
1167
+ oldDid: oldDid,
1168
+ newDid: newIdentity.did,
1169
+ timestamp: this.clock.now(),
1170
+ });
1171
+ }
1172
+
1173
+ return newIdentity;
1174
+ }
1175
+
1176
+ // Helper methods
1177
+
1178
+ private async signData(data: any): Promise<string> {
1179
+ const identity = await this.getIdentity();
1180
+ const dataBytes = new TextEncoder().encode(JSON.stringify(data));
1181
+ const signature = await this.crypto.sign(dataBytes, identity.privateKey);
1182
+ return this.bytesToBase64(signature);
1183
+ }
1184
+
1185
+ private async generateNonce(): Promise<string> {
1186
+ const bytes = await this.crypto.randomBytes(32);
1187
+ return this.bytesToBase64(bytes);
1188
+ }
1189
+
1190
+ private async generateSessionId(): Promise<string> {
1191
+ const bytes = await this.crypto.randomBytes(16);
1192
+ return this.bytesToHex(bytes);
1193
+ }
1194
+
1195
+ /**
1196
+ * Log structured events in JSON format (NOT frozen audit format).
1197
+ *
1198
+ * **Important:** This method logs events in JSON format for general runtime events
1199
+ * (e.g., "runtime_initialized", "tool_executed", "keys_rotated").
1200
+ *
1201
+ * **For frozen format audit logs** (MCP-I spec compliance), use `AuditLogger.logAuditRecord()`
1202
+ * from `@kya-os/mcp-i/runtime` instead. The frozen format is:
1203
+ * ```
1204
+ * audit.v1 ts=<unix> session=<id> audience=<host> did=<did> kid=<kid> reqHash=<sha256:..> resHash=<sha256:..> verified=yes|no scope=<scopeId|->
1205
+ * ```
1206
+ *
1207
+ * **Format:** JSON object with `event`, `data`, `timestamp`, and `timestampFormatted` fields.
1208
+ *
1209
+ * **Privacy:** If `includePayloads` is false (default), the `data` field is omitted.
1210
+ *
1211
+ * **Use Cases:**
1212
+ * - Developer debugging and local logging
1213
+ * - Runtime initialization events
1214
+ * - Tool execution tracking (non-spec-compliant)
1215
+ * - Key rotation events
1216
+ *
1217
+ * **NOT for:**
1218
+ * - MCP-I spec-compliant audit logs (use `AuditLogger`)
1219
+ * - Production audit trails (use `AuditLogger`)
1220
+ * - Compliance requirements (use `AuditLogger`)
1221
+ *
1222
+ * @param event - Event name (e.g., "runtime_initialized", "tool_executed")
1223
+ * @param data - Event data (only included if `includePayloads` is true)
1224
+ *
1225
+ * @example
1226
+ * ```typescript
1227
+ * // Logs: {"event":"runtime_initialized","timestamp":1234567890,"timestampFormatted":"2024-01-01T00:00:00Z"}
1228
+ * this.logAudit("runtime_initialized", { did: "did:key:..." });
1229
+ * ```
1230
+ *
1231
+ * @internal This is a private method for internal runtime events.
1232
+ * Use AuditLogger for spec-compliant frozen format audit logs.
1233
+ */
1234
+ private logAudit(event: string, data: any): void {
1235
+ if (!this.config.audit?.enabled) return;
1236
+
1237
+ const record = {
1238
+ event,
1239
+ data: this.config.audit.includePayloads ? data : undefined,
1240
+ timestamp: this.clock.now(),
1241
+ timestampFormatted: this.clock.format(this.clock.now()),
1242
+ };
1243
+
1244
+ const logLine = JSON.stringify(record);
1245
+
1246
+ if (this.config.audit.logFunction) {
1247
+ this.config.audit.logFunction(logLine);
1248
+ } else {
1249
+ console.log("[AUDIT]", logLine);
1250
+ }
1251
+ }
1252
+
1253
+ private createDIDDocument(identity: AgentIdentity): DIDDocument {
1254
+ return {
1255
+ "@context": ["https://www.w3.org/ns/did/v1"],
1256
+ id: identity.did,
1257
+ verificationMethod: [
1258
+ {
1259
+ id: `${identity.did}#key-1`,
1260
+ type: "Ed25519VerificationKey2020",
1261
+ controller: identity.did,
1262
+ // Using base64 for cross-platform compatibility (Cloudflare Workers)
1263
+ // W3C DID spec supports both base64 and multibase formats
1264
+ publicKeyBase64: identity.publicKey,
1265
+ },
1266
+ ],
1267
+ authentication: [`${identity.did}#key-1`],
1268
+ assertionMethod: [`${identity.did}#key-1`],
1269
+ };
1270
+ }
1271
+
1272
+ private extractPublicKey(didDoc: any): string {
1273
+ const method = didDoc.verificationMethod?.[0];
1274
+ if (method?.publicKeyBase64) {
1275
+ return method.publicKeyBase64;
1276
+ }
1277
+ if (method?.publicKeyMultibase) {
1278
+ // Convert multibase to base64
1279
+ return method.publicKeyMultibase; // Simplified
1280
+ }
1281
+ throw new Error("Public key not found in DID document");
1282
+ }
1283
+
1284
+ /**
1285
+ * Extract public key JWK from DID document
1286
+ */
1287
+ private extractPublicKeyJwk(didDoc: any, kid?: string): Ed25519JWK | null {
1288
+ // Try to find Ed25519 public key matching kid if provided
1289
+ const verificationMethod =
1290
+ didDoc.verificationMethod?.find((vm: any) => {
1291
+ const matchesType =
1292
+ vm.type === "Ed25519VerificationKey2020" ||
1293
+ vm.type === "JsonWebKey2020";
1294
+ const matchesKid = !kid || vm.id === kid || vm.id.endsWith(`#${kid}`);
1295
+ return matchesType && matchesKid;
1296
+ }) || didDoc.verificationMethod?.[0]; // Fallback to first method
1297
+
1298
+ if (verificationMethod?.publicKeyJwk) {
1299
+ const jwk = verificationMethod.publicKeyJwk;
1300
+ // Ensure it's Ed25519 format
1301
+ if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
1302
+ return jwk as Ed25519JWK;
1303
+ }
1304
+ }
1305
+
1306
+ // Fallback: try to convert multibase to JWK (simplified)
1307
+ if (verificationMethod?.publicKeyMultibase) {
1308
+ // This is a simplified conversion - in production, use proper multibase decoding
1309
+ console.warn(
1310
+ "[MCPIRuntimeBase] Multibase to JWK conversion not fully implemented"
1311
+ );
1312
+ return null;
1313
+ }
1314
+
1315
+ return null;
1316
+ }
1317
+
1318
+ private bytesToBase64(bytes: Uint8Array): string {
1319
+ return Buffer.from(bytes).toString("base64");
1320
+ }
1321
+
1322
+ private base64ToBytes(base64: string): Uint8Array {
1323
+ return new Uint8Array(Buffer.from(base64, "base64"));
1324
+ }
1325
+
1326
+ private bytesToHex(bytes: Uint8Array): string {
1327
+ return Buffer.from(bytes).toString("hex");
1328
+ }
1329
+ }