@kya-os/mcp-i-core 1.3.13 → 1.3.15

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