@kya-os/mcp-i-core 1.2.3-canary.7 → 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,869 @@
1
+ /**
2
+ * Tests for MCPIRuntimeBase
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
6
+ import { MCPIRuntimeBase } from "../../runtime/base";
7
+ import { ProviderRuntimeConfig } from "../../config";
8
+ import {
9
+ createMockProviders,
10
+ MockClockProvider,
11
+ MockFetchProvider,
12
+ MockIdentityProvider,
13
+ MockNonceCacheProvider,
14
+ } from "../utils/mock-providers";
15
+
16
+ describe("MCPIRuntimeBase", () => {
17
+ let runtime: MCPIRuntimeBase;
18
+ let config: ProviderRuntimeConfig;
19
+ let mockProviders: ReturnType<typeof createMockProviders>;
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ mockProviders = createMockProviders();
24
+ config = {
25
+ ...mockProviders,
26
+ environment: "development",
27
+ session: {
28
+ timestampSkewSeconds: 120,
29
+ ttlMinutes: 30,
30
+ },
31
+ audit: {
32
+ enabled: true,
33
+ logFunction: vi.fn(),
34
+ includePayloads: true,
35
+ },
36
+ wellKnown: {
37
+ enabled: true,
38
+ serviceName: "Test Service",
39
+ serviceEndpoint: "https://test.example.com",
40
+ },
41
+ showVerifyLink: true,
42
+ identityBadge: true,
43
+ };
44
+ runtime = new MCPIRuntimeBase(config);
45
+ });
46
+
47
+ describe("constructor", () => {
48
+ it("should initialize with providers", () => {
49
+ expect(runtime).toBeDefined();
50
+ expect((runtime as any).crypto).toBe(mockProviders.cryptoProvider);
51
+ expect((runtime as any).clock).toBe(mockProviders.clockProvider);
52
+ expect((runtime as any).fetch).toBe(mockProviders.fetchProvider);
53
+ expect((runtime as any).storage).toBe(mockProviders.storageProvider);
54
+ expect((runtime as any).nonceCache).toBe(
55
+ mockProviders.nonceCacheProvider
56
+ );
57
+ expect((runtime as any).identity).toBe(mockProviders.identityProvider);
58
+ });
59
+
60
+ it("should store config", () => {
61
+ expect((runtime as any).config).toEqual(config);
62
+ });
63
+ });
64
+
65
+ describe("initialize", () => {
66
+ it("should load identity on initialization", async () => {
67
+ await runtime.initialize();
68
+ const identity = await runtime.getIdentity();
69
+ expect(identity.did).toBe("did:key:zmock123");
70
+ });
71
+
72
+ it("should call nonce cache initialize if available", async () => {
73
+ const initializeFn = vi.fn();
74
+ (mockProviders.nonceCacheProvider as any).initialize = initializeFn;
75
+
76
+ await runtime.initialize();
77
+ expect(initializeFn).toHaveBeenCalled();
78
+ });
79
+
80
+ it("should log audit event on initialization", async () => {
81
+ await runtime.initialize();
82
+
83
+ expect(config.audit!.logFunction).toHaveBeenCalledWith(
84
+ expect.stringContaining("runtime_initialized")
85
+ );
86
+
87
+ const logCall = (config.audit!.logFunction as any).mock.calls[0][0];
88
+ const logData = JSON.parse(logCall);
89
+ expect(logData.event).toBe("runtime_initialized");
90
+ expect(logData.data.did).toBe("did:key:zmock123");
91
+ expect(logData.data.environment).toBe("development");
92
+ });
93
+
94
+ it("should not log audit if disabled", async () => {
95
+ config.audit!.enabled = false;
96
+ await runtime.initialize();
97
+ expect(config.audit!.logFunction).not.toHaveBeenCalled();
98
+ });
99
+ });
100
+
101
+ describe("getIdentity", () => {
102
+ it("should return cached identity if available", async () => {
103
+ await runtime.initialize();
104
+ const identity1 = await runtime.getIdentity();
105
+ const identity2 = await runtime.getIdentity();
106
+
107
+ expect(identity1).toBe(identity2);
108
+ });
109
+
110
+ it("should load identity if not cached", async () => {
111
+ const identity = await runtime.getIdentity();
112
+ expect(identity.did).toBe("did:key:zmock123");
113
+ });
114
+ });
115
+
116
+ describe("handleHandshake", () => {
117
+ const handshakeRequest = {
118
+ nonce: "test-nonce-123",
119
+ audience: "https://client.example.com",
120
+ timestamp: Date.now(),
121
+ clientDid: "did:key:zclient123",
122
+ };
123
+
124
+ beforeEach(async () => {
125
+ await runtime.initialize();
126
+ });
127
+
128
+ it("should create session and return handshake response", async () => {
129
+ const response = await runtime.handleHandshake(handshakeRequest);
130
+
131
+ expect(response.sessionId).toBeDefined();
132
+ expect(response.agentDid).toBe("did:key:zmock123");
133
+ expect(response.timestamp).toBe(
134
+ (mockProviders.clockProvider as MockClockProvider).now()
135
+ );
136
+ expect(response.capabilities).toEqual(["identity", "proof", "audit"]);
137
+ expect(response.signature).toBeDefined();
138
+ });
139
+
140
+ it("should store session internally", async () => {
141
+ const response = await runtime.handleHandshake(handshakeRequest);
142
+ const session = await runtime.getCurrentSession();
143
+
144
+ expect(session).toBeDefined();
145
+ expect(session.id).toBe(response.sessionId);
146
+ expect(session.clientDid).toBe(handshakeRequest.clientDid);
147
+ expect(session.agentDid).toBeUndefined(); // ✅ FIXED: agentDid should be undefined when not provided (no fallback)
148
+ expect(session.serverDid).toBe("did:key:zmock123"); // ✅ NEW: serverDid should be set to identity.did
149
+ expect(session.audience).toBe(handshakeRequest.audience);
150
+ });
151
+
152
+ it("should set session expiry", async () => {
153
+ const clock = mockProviders.clockProvider as MockClockProvider;
154
+ const currentTime = Date.now();
155
+ clock.setTime(currentTime);
156
+
157
+ await runtime.handleHandshake(handshakeRequest);
158
+ const session = await runtime.getCurrentSession();
159
+
160
+ expect(session.expiresAt).toBe(currentTime + 30 * 60 * 1000); // 30 minutes
161
+ });
162
+
163
+ it("should sign handshake response", async () => {
164
+ const response = await runtime.handleHandshake(handshakeRequest);
165
+ expect(response.signature).toBe(
166
+ Buffer.from(new Uint8Array([1, 2, 3, 4, 5])).toString("base64")
167
+ );
168
+ });
169
+
170
+ it("should use agentDid when provided in handshake request", async () => {
171
+ const handshakeWithAgentDid = {
172
+ ...handshakeRequest,
173
+ agentDid: "did:key:zagent456",
174
+ };
175
+
176
+ const response = await runtime.handleHandshake(handshakeWithAgentDid);
177
+ const session = await runtime.getCurrentSession();
178
+
179
+ expect(session.agentDid).toBe("did:key:zagent456"); // ✅ Should use agentDid, not clientDid
180
+ expect(session.clientDid).toBe(handshakeRequest.clientDid); // clientDid should remain unchanged
181
+ expect(session.serverDid).toBe("did:key:zmock123"); // ✅ serverDid should be set to identity.did
182
+ });
183
+
184
+ it("should extract and store clientInfo when provided", async () => {
185
+ const handshakeWithClientInfo = {
186
+ ...handshakeRequest,
187
+ clientInfo: {
188
+ name: "Claude Desktop",
189
+ title: "Claude Desktop Preview",
190
+ version: "1.0.0",
191
+ platform: "desktop",
192
+ vendor: "Anthropic",
193
+ persistentId: "persistent-client-id",
194
+ },
195
+ clientProtocolVersion: "2025-06-18",
196
+ clientCapabilities: {
197
+ tools: { listChanged: true },
198
+ },
199
+ };
200
+
201
+ await runtime.handleHandshake(handshakeWithClientInfo);
202
+ const session = await runtime.getCurrentSession();
203
+
204
+ expect(session.clientInfo).toBeDefined();
205
+ expect(session.clientInfo?.name).toBe("Claude Desktop");
206
+ expect(session.clientInfo?.title).toBe("Claude Desktop Preview");
207
+ expect(session.clientInfo?.version).toBe("1.0.0");
208
+ expect(session.clientInfo?.platform).toBe("desktop");
209
+ expect(session.clientInfo?.clientId).toBeDefined();
210
+ expect(typeof session.clientInfo?.clientId).toBe("string");
211
+ expect(session.clientInfo?.vendor).toBe("Anthropic");
212
+ expect(session.clientInfo?.persistentId).toBe("persistent-client-id");
213
+ expect(session.clientInfo?.protocolVersion).toBe("2025-06-18");
214
+ expect(session.clientInfo?.capabilities).toEqual({
215
+ tools: { listChanged: true },
216
+ });
217
+ });
218
+
219
+ it("should handle handshake without clientInfo gracefully", async () => {
220
+ await runtime.handleHandshake(handshakeRequest);
221
+ const session = await runtime.getCurrentSession();
222
+
223
+ expect(session.clientInfo).toBeUndefined();
224
+ });
225
+
226
+ it("should preserve clientId when provided by adapter", async () => {
227
+ const handshakeWithClientId = {
228
+ ...handshakeRequest,
229
+ clientInfo: {
230
+ name: "Claude Desktop",
231
+ clientId: "server-specified-client-id",
232
+ },
233
+ };
234
+
235
+ await runtime.handleHandshake(handshakeWithClientId);
236
+ const session = await runtime.getCurrentSession();
237
+
238
+ expect(session.clientInfo?.clientId).toBe("server-specified-client-id");
239
+ });
240
+
241
+ it("should default clientInfo name to 'unknown' when name is missing", async () => {
242
+ const handshakeWithPartialClientInfo = {
243
+ ...handshakeRequest,
244
+ clientInfo: {
245
+ version: "1.0.0",
246
+ platform: "desktop",
247
+ },
248
+ };
249
+
250
+ await runtime.handleHandshake(handshakeWithPartialClientInfo);
251
+ const session = await runtime.getCurrentSession();
252
+
253
+ expect(session.clientInfo).toBeDefined();
254
+ expect(session.clientInfo?.name).toBe("unknown");
255
+ expect(session.clientInfo?.version).toBe("1.0.0");
256
+ expect(session.clientInfo?.platform).toBe("desktop");
257
+ });
258
+ });
259
+
260
+ describe("processToolCall", () => {
261
+ const mockHandler = vi.fn().mockResolvedValue({ result: "success" });
262
+ const session = {
263
+ id: "session123",
264
+ audience: "https://client.example.com",
265
+ nonce: "test-nonce",
266
+ };
267
+
268
+ beforeEach(async () => {
269
+ await runtime.initialize();
270
+ });
271
+
272
+ it("should execute tool handler and return clean result", async () => {
273
+ const result = await runtime.processToolCall(
274
+ "testTool",
275
+ { arg: "value" },
276
+ mockHandler,
277
+ session
278
+ );
279
+
280
+ expect(mockHandler).toHaveBeenCalledWith({ arg: "value" });
281
+ expect(result).toEqual({ result: "success" });
282
+ });
283
+
284
+ it("should create and store proof", async () => {
285
+ await runtime.processToolCall(
286
+ "testTool",
287
+ { arg: "value" },
288
+ mockHandler,
289
+ session
290
+ );
291
+
292
+ const proof = runtime.getLastProof();
293
+ expect(proof).toBeDefined();
294
+ expect(proof.did).toBe("did:key:zmock123");
295
+ expect(proof.signature).toBeDefined();
296
+ expect(proof.nonce).toBe("test-nonce");
297
+ expect(proof.sessionId).toBe("session123");
298
+ expect(proof.audience).toBe("https://client.example.com");
299
+ });
300
+
301
+ it("should log audit event", async () => {
302
+ const clock = mockProviders.clockProvider as MockClockProvider;
303
+ const currentTime = Date.now();
304
+ clock.setTime(currentTime);
305
+
306
+ await runtime.processToolCall(
307
+ "testTool",
308
+ { arg: "value" },
309
+ mockHandler,
310
+ session
311
+ );
312
+
313
+ expect(config.audit!.logFunction).toHaveBeenCalled();
314
+
315
+ // Find the tool_executed log entry (might not be the first one)
316
+ const logCalls = (config.audit!.logFunction as any).mock.calls;
317
+ const toolLogCall = logCalls.find((call: any[]) => {
318
+ const log = JSON.parse(call[0]);
319
+ return log.event === "tool_executed";
320
+ });
321
+
322
+ expect(toolLogCall).toBeDefined();
323
+ const logData = JSON.parse(toolLogCall[0]);
324
+ expect(logData.event).toBe("tool_executed");
325
+ expect(logData.data.tool).toBe("testTool");
326
+ expect(logData.data.sessionId).toBe("session123");
327
+ expect(logData.data.timestamp).toBe(currentTime);
328
+ });
329
+
330
+ it("should handle tool handler errors", async () => {
331
+ const errorHandler = vi.fn().mockRejectedValue(new Error("Tool failed"));
332
+
333
+ await expect(
334
+ runtime.processToolCall("errorTool", {}, errorHandler)
335
+ ).rejects.toThrow("Tool failed");
336
+ });
337
+ });
338
+
339
+ describe("issueNonce", () => {
340
+ beforeEach(async () => {
341
+ await runtime.initialize();
342
+ });
343
+
344
+ it("should generate and cache nonce", async () => {
345
+ const nonce = await runtime.issueNonce("session123");
346
+
347
+ expect(nonce).toBeDefined();
348
+ expect(nonce).toHaveLength(44); // Base64 encoded 32 bytes
349
+
350
+ const nonceCache =
351
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
352
+ const identity = await runtime.getIdentity();
353
+ expect(await nonceCache.has(nonce, identity.did)).toBe(true);
354
+ });
355
+
356
+ it("should set 5 minute expiry", async () => {
357
+ const clock = mockProviders.clockProvider as MockClockProvider;
358
+ const currentTime = Date.now();
359
+ clock.setTime(currentTime);
360
+
361
+ const nonce = await runtime.issueNonce("session123");
362
+
363
+ // Check that nonce expires after 5 minutes
364
+ clock.setTime(currentTime + 5 * 60 * 1000 + 1);
365
+ const nonceCache =
366
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
367
+ const identity = await runtime.getIdentity();
368
+ expect(await nonceCache.has(nonce, identity.did)).toBe(false);
369
+ });
370
+ });
371
+
372
+ describe("createProof", () => {
373
+ const testData = { foo: "bar" };
374
+
375
+ beforeEach(async () => {
376
+ await runtime.initialize();
377
+ });
378
+
379
+ it("should create proof with all fields", async () => {
380
+ const clock = mockProviders.clockProvider as MockClockProvider;
381
+ const currentTime = Date.now();
382
+ clock.setTime(currentTime);
383
+
384
+ const proof = await runtime.createProof(testData);
385
+
386
+ expect(proof.timestamp).toBe(currentTime);
387
+ expect(proof.nonce).toBeDefined();
388
+ expect(proof.did).toBe("did:key:zmock123");
389
+ expect(proof.signature).toBeDefined();
390
+ expect(proof.algorithm).toBe("Ed25519");
391
+ expect(proof.sessionId).toBeUndefined();
392
+ expect(proof.audience).toBeUndefined();
393
+ });
394
+
395
+ it("should use session nonce if provided", async () => {
396
+ const session = {
397
+ id: "session123",
398
+ nonce: "session-nonce",
399
+ audience: "https://client.example.com",
400
+ };
401
+
402
+ const proof = await runtime.createProof(testData, session);
403
+
404
+ expect(proof.nonce).toBe("session-nonce");
405
+ expect(proof.sessionId).toBe("session123");
406
+ expect(proof.audience).toBe("https://client.example.com");
407
+ });
408
+
409
+ it("should generate and cache new nonce if not in session", async () => {
410
+ const proof = await runtime.createProof(testData);
411
+
412
+ const nonceCache =
413
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
414
+ const identity = await runtime.getIdentity();
415
+ expect(await nonceCache.has(proof.nonce, identity.did)).toBe(true);
416
+ });
417
+ });
418
+
419
+ describe("verifyProof", () => {
420
+ const testData = { foo: "bar" };
421
+ let validProof: any;
422
+
423
+ beforeEach(async () => {
424
+ await runtime.initialize();
425
+
426
+ // Set up DID document for verification
427
+ const fetchProvider = mockProviders.fetchProvider as MockFetchProvider;
428
+ fetchProvider.setDIDDocument("did:key:zmock123", {
429
+ verificationMethod: [
430
+ {
431
+ publicKeyBase64: "mock-public-key",
432
+ },
433
+ ],
434
+ });
435
+
436
+ // Create a valid proof
437
+ validProof = await runtime.createProof(testData);
438
+ });
439
+
440
+ it("should verify valid proof", async () => {
441
+ // Remove the nonce from cache first (simulating a fresh verification)
442
+ const nonceCache =
443
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
444
+ nonceCache.clear();
445
+
446
+ const isValid = await runtime.verifyProof(testData, validProof);
447
+ expect(isValid).toBe(true);
448
+ });
449
+
450
+ it("should reject if nonce already used", async () => {
451
+ // Pre-add nonce to cache with agent-scoped key
452
+ const nonceCache =
453
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
454
+ await nonceCache.add(
455
+ validProof.nonce,
456
+ Date.now() + 10000,
457
+ validProof.did
458
+ );
459
+
460
+ const isValid = await runtime.verifyProof(testData, validProof);
461
+ expect(isValid).toBe(false);
462
+ });
463
+
464
+ it("should use agent-scoped nonces in legacy verification (prevents cross-agent replay)", async () => {
465
+ const agentDid1 = "did:key:agent1";
466
+ const agentDid2 = "did:key:agent2";
467
+ const sameNonce = "same-nonce-value";
468
+
469
+ // Set up DID documents for both agents
470
+ const fetchProvider = mockProviders.fetchProvider as MockFetchProvider;
471
+ fetchProvider.setDIDDocument(agentDid1, {
472
+ verificationMethod: [
473
+ {
474
+ publicKeyBase64: "mock-public-key-agent1",
475
+ },
476
+ ],
477
+ });
478
+ fetchProvider.setDIDDocument(agentDid2, {
479
+ verificationMethod: [
480
+ {
481
+ publicKeyBase64: "mock-public-key-agent2",
482
+ },
483
+ ],
484
+ });
485
+
486
+ // Create proof for agent 1
487
+ const proof1 = {
488
+ ...validProof,
489
+ did: agentDid1,
490
+ nonce: sameNonce,
491
+ };
492
+
493
+ // Create proof for agent 2 with same nonce
494
+ const proof2 = {
495
+ ...validProof,
496
+ did: agentDid2,
497
+ nonce: sameNonce,
498
+ };
499
+
500
+ const nonceCache =
501
+ mockProviders.nonceCacheProvider as MockNonceCacheProvider;
502
+ nonceCache.clear();
503
+
504
+ // Verify proof for agent 1 (should succeed)
505
+ const isValid1 = await runtime.verifyProof(testData, proof1);
506
+ expect(isValid1).toBe(true);
507
+
508
+ // Verify that nonce was added with agent-scoped key
509
+ // The nonce should be scoped to agentDid1, so agentDid2 should be able to use the same nonce
510
+ const hasNonceAgent1 = await nonceCache.has(sameNonce, agentDid1);
511
+ expect(hasNonceAgent1).toBe(true);
512
+
513
+ // Verify proof for agent 2 with same nonce (should succeed because nonces are agent-scoped)
514
+ const isValid2 = await runtime.verifyProof(testData, proof2);
515
+ expect(isValid2).toBe(true);
516
+
517
+ // Verify that both agents' nonces are cached separately
518
+ const hasNonceAgent2 = await nonceCache.has(sameNonce, agentDid2);
519
+ expect(hasNonceAgent2).toBe(true);
520
+ });
521
+
522
+ it("should reject if timestamp outside skew", async () => {
523
+ const clock = mockProviders.clockProvider as MockClockProvider;
524
+ const currentTime = Date.now();
525
+ clock.setTime(currentTime + 121 * 1000); // 121 seconds later (skew is 120)
526
+
527
+ const isValid = await runtime.verifyProof(testData, validProof);
528
+ expect(isValid).toBe(false);
529
+ });
530
+
531
+ it("should reject if DID cannot be resolved", async () => {
532
+ const fetchProvider = mockProviders.fetchProvider as MockFetchProvider;
533
+ fetchProvider.setDIDDocument("did:key:zmock123", null as any);
534
+
535
+ const isValid = await runtime.verifyProof(testData, validProof);
536
+ expect(isValid).toBe(false);
537
+ });
538
+
539
+ it("should reject if signature invalid", async () => {
540
+ validProof.signature = "invalid-signature";
541
+
542
+ const isValid = await runtime.verifyProof(testData, validProof);
543
+ expect(isValid).toBe(false);
544
+ });
545
+
546
+ it("should handle verification errors gracefully", async () => {
547
+ const fetchProvider = mockProviders.fetchProvider as MockFetchProvider;
548
+ (fetchProvider.resolveDID as any) = vi
549
+ .fn()
550
+ .mockRejectedValue(new Error("Network error"));
551
+
552
+ const isValid = await runtime.verifyProof(testData, validProof);
553
+ expect(isValid).toBe(false);
554
+ });
555
+ });
556
+
557
+ describe("getCurrentSession", () => {
558
+ beforeEach(async () => {
559
+ await runtime.initialize();
560
+ });
561
+
562
+ it("should return active session", async () => {
563
+ await runtime.handleHandshake({
564
+ clientDid: "did:key:zclient123",
565
+ audience: "https://client.example.com",
566
+ });
567
+
568
+ const session = await runtime.getCurrentSession();
569
+ expect(session).toBeDefined();
570
+ expect(session.clientDid).toBe("did:key:zclient123");
571
+ });
572
+
573
+ it("should return null if no sessions", async () => {
574
+ const session = await runtime.getCurrentSession();
575
+ expect(session).toBeNull();
576
+ });
577
+
578
+ it("should return null if session expired", async () => {
579
+ const clock = mockProviders.clockProvider as MockClockProvider;
580
+ const currentTime = Date.now();
581
+ clock.setTime(currentTime);
582
+
583
+ await runtime.handleHandshake({
584
+ clientDid: "did:key:zclient123",
585
+ audience: "https://client.example.com",
586
+ });
587
+
588
+ // Advance time past session expiry
589
+ clock.setTime(currentTime + 31 * 60 * 1000); // 31 minutes
590
+
591
+ const session = await runtime.getCurrentSession();
592
+ expect(session).toBeNull();
593
+ });
594
+ });
595
+
596
+ describe("getLastProof", () => {
597
+ it("should return undefined initially", () => {
598
+ const proof = runtime.getLastProof();
599
+ expect(proof).toBeUndefined();
600
+ });
601
+
602
+ it("should return last generated proof", async () => {
603
+ await runtime.initialize();
604
+ await runtime.processToolCall(
605
+ "testTool",
606
+ {},
607
+ vi.fn().mockResolvedValue({ result: "ok" })
608
+ );
609
+
610
+ const proof = runtime.getLastProof();
611
+ expect(proof).toBeDefined();
612
+ expect(proof.did).toBe("did:key:zmock123");
613
+ });
614
+ });
615
+
616
+ describe("createWellKnownHandler", () => {
617
+ let handler: any;
618
+
619
+ beforeEach(async () => {
620
+ await runtime.initialize();
621
+ handler = runtime.createWellKnownHandler({
622
+ serviceName: "Test Service",
623
+ serviceEndpoint: "https://test.example.com",
624
+ });
625
+ });
626
+
627
+ it("should handle /.well-known/did.json", async () => {
628
+ const response = await handler("/.well-known/did.json");
629
+
630
+ expect(response.status).toBe(200);
631
+ expect(response.headers["Content-Type"]).toBe("application/did+json");
632
+
633
+ const result = JSON.parse(response.body);
634
+ expect(result).toEqual({
635
+ "@context": ["https://www.w3.org/ns/did/v1"],
636
+ id: "did:key:zmock123",
637
+ verificationMethod: [
638
+ {
639
+ id: "did:key:zmock123#key-1",
640
+ type: "Ed25519VerificationKey2020",
641
+ controller: "did:key:zmock123",
642
+ publicKeyBase64: "mock-public-key",
643
+ },
644
+ ],
645
+ authentication: ["did:key:zmock123#key-1"],
646
+ assertionMethod: ["did:key:zmock123#key-1"],
647
+ });
648
+ });
649
+
650
+ it("should handle /.well-known/mcp-identity", async () => {
651
+ const clock = mockProviders.clockProvider as MockClockProvider;
652
+ const currentTime = Date.now();
653
+ clock.setTime(currentTime);
654
+
655
+ const result = await handler("/.well-known/mcp-identity");
656
+
657
+ expect(result).toEqual({
658
+ did: "did:key:zmock123",
659
+ publicKey: "mock-public-key",
660
+ serviceName: "Test Service",
661
+ serviceEndpoint: "https://test.example.com",
662
+ timestamp: currentTime,
663
+ });
664
+ });
665
+
666
+ it("should return null for unknown paths", async () => {
667
+ const result = await handler("/unknown/path");
668
+ expect(result).toBeNull();
669
+ });
670
+
671
+ it("should use default values if config not provided", async () => {
672
+ const defaultHandler = runtime.createWellKnownHandler();
673
+ const result = await defaultHandler("/.well-known/mcp-identity");
674
+
675
+ expect(result.serviceName).toBe("MCP-I Service");
676
+ expect(result.serviceEndpoint).toBe("https://example.com");
677
+ });
678
+ });
679
+
680
+ describe("createDebugEndpoint", () => {
681
+ beforeEach(async () => {
682
+ await runtime.initialize();
683
+ });
684
+
685
+ it("should return debug info in development", async () => {
686
+ const clock = mockProviders.clockProvider as MockClockProvider;
687
+ const currentTime = Date.now();
688
+ clock.setTime(currentTime);
689
+
690
+ const endpoint = runtime.createDebugEndpoint();
691
+ expect(endpoint).toBeDefined();
692
+
693
+ const result = await endpoint();
694
+ expect(result).toEqual({
695
+ identity: {
696
+ did: "did:key:zmock123",
697
+ publicKey: "mock-public-key",
698
+ },
699
+ session: null,
700
+ config: {
701
+ environment: "development",
702
+ timestampSkewSeconds: 120,
703
+ sessionTtlMinutes: 30,
704
+ },
705
+ timestamp: currentTime,
706
+ });
707
+ });
708
+
709
+ it("should include session if active", async () => {
710
+ await runtime.handleHandshake({
711
+ clientDid: "did:key:zclient123",
712
+ audience: "https://client.example.com",
713
+ });
714
+
715
+ const endpoint = runtime.createDebugEndpoint();
716
+ const result = await endpoint();
717
+
718
+ expect(result.session).toBeDefined();
719
+ expect(result.session.clientDid).toBe("did:key:zclient123");
720
+ });
721
+
722
+ it("should return null in production", () => {
723
+ config.environment = "production";
724
+ const prodRuntime = new MCPIRuntimeBase(config);
725
+
726
+ const endpoint = prodRuntime.createDebugEndpoint();
727
+ expect(endpoint).toBeNull();
728
+ });
729
+ });
730
+
731
+ describe("getAuditLogger", () => {
732
+ it("should return audit logger", () => {
733
+ const logger = runtime.getAuditLogger();
734
+ expect(logger).toBeDefined();
735
+ expect(logger.log).toBeDefined();
736
+ });
737
+
738
+ it("should log through runtime audit", async () => {
739
+ await runtime.initialize();
740
+ const logger = runtime.getAuditLogger();
741
+
742
+ logger.log("test_event", { test: "data" });
743
+
744
+ expect(config.audit!.logFunction).toHaveBeenCalledWith(
745
+ expect.stringContaining("test_event")
746
+ );
747
+ });
748
+ });
749
+
750
+ describe("rotateKeys", () => {
751
+ beforeEach(async () => {
752
+ await runtime.initialize();
753
+ });
754
+
755
+ it("should rotate keys and update identity", async () => {
756
+ const oldIdentity = await runtime.getIdentity();
757
+ const newIdentity = await runtime.rotateKeys();
758
+
759
+ expect(newIdentity.did).toBe("did:key:zmock456-1");
760
+ expect(newIdentity.privateKey).toBe("mock-private-key-rotated-1");
761
+ expect(newIdentity.publicKey).toBe("mock-public-key-rotated-1");
762
+
763
+ const currentIdentity = await runtime.getIdentity();
764
+ expect(currentIdentity).toEqual(newIdentity);
765
+ });
766
+
767
+ it("should log audit event", async () => {
768
+ const clock = mockProviders.clockProvider as MockClockProvider;
769
+ const currentTime = Date.now();
770
+ clock.setTime(currentTime);
771
+
772
+ // Get the old identity first
773
+ const oldIdentity = await runtime.getIdentity();
774
+
775
+ await runtime.rotateKeys();
776
+
777
+ expect(config.audit!.logFunction).toHaveBeenCalled();
778
+
779
+ // Find the keys_rotated log entry
780
+ const logCalls = (config.audit!.logFunction as any).mock.calls;
781
+ const rotateLogCall = logCalls.find((call: any[]) => {
782
+ const log = JSON.parse(call[0]);
783
+ return log.event === "keys_rotated";
784
+ });
785
+
786
+ expect(rotateLogCall).toBeDefined();
787
+ const logData = JSON.parse(rotateLogCall[0]);
788
+ expect(logData.event).toBe("keys_rotated");
789
+ expect(logData.data.oldDid).toBe(oldIdentity.did);
790
+ expect(logData.data.newDid).toBe("did:key:zmock456-1");
791
+ expect(logData.data.timestamp).toBe(currentTime);
792
+ });
793
+ });
794
+
795
+ describe("audit logging", () => {
796
+ it("should use custom log function if provided", async () => {
797
+ await runtime.initialize();
798
+ expect(config.audit!.logFunction).toHaveBeenCalled();
799
+ });
800
+
801
+ it("should console.log if no custom function", async () => {
802
+ const consoleLogSpy = vi
803
+ .spyOn(console, "log")
804
+ .mockImplementation(() => {});
805
+
806
+ config.audit!.logFunction = undefined;
807
+ const runtimeNoLogFn = new MCPIRuntimeBase(config);
808
+ await runtimeNoLogFn.initialize();
809
+
810
+ expect(consoleLogSpy).toHaveBeenCalledWith("[AUDIT]", expect.any(String));
811
+ consoleLogSpy.mockRestore();
812
+ });
813
+
814
+ it("should exclude payloads if configured", async () => {
815
+ config.audit!.includePayloads = false;
816
+ const runtimeNoPayloads = new MCPIRuntimeBase(config);
817
+ await runtimeNoPayloads.initialize();
818
+
819
+ const logCall = (config.audit!.logFunction as any).mock.calls[0][0];
820
+ const logData = JSON.parse(logCall);
821
+ expect(logData.data).toBeUndefined();
822
+ });
823
+
824
+ it("should include formatted timestamp", async () => {
825
+ const clock = mockProviders.clockProvider as MockClockProvider;
826
+ const currentTime = Date.now();
827
+ clock.setTime(currentTime);
828
+
829
+ await runtime.initialize();
830
+
831
+ const logCall = (config.audit!.logFunction as any).mock.calls[0][0];
832
+ const logData = JSON.parse(logCall);
833
+ expect(logData.timestampFormatted).toBe(
834
+ new Date(currentTime).toISOString()
835
+ );
836
+ });
837
+ });
838
+
839
+ describe("helper methods", () => {
840
+ it("should handle DID document with publicKeyMultibase", async () => {
841
+ await runtime.initialize();
842
+
843
+ const fetchProvider = mockProviders.fetchProvider as MockFetchProvider;
844
+ fetchProvider.setDIDDocument("did:key:ztest", {
845
+ verificationMethod: [
846
+ {
847
+ publicKeyMultibase: "zBase58EncodedKey",
848
+ },
849
+ ],
850
+ });
851
+
852
+ // This would be called internally during verification
853
+ const didDoc = await fetchProvider.resolveDID("did:key:ztest");
854
+ const extractPublicKey = (runtime as any).extractPublicKey.bind(runtime);
855
+ const publicKey = extractPublicKey(didDoc);
856
+
857
+ expect(publicKey).toBe("zBase58EncodedKey");
858
+ });
859
+
860
+ it("should throw if no public key in DID document", async () => {
861
+ await runtime.initialize();
862
+
863
+ const didDoc = { verificationMethod: [{}] };
864
+ const extractPublicKey = (runtime as any).extractPublicKey.bind(runtime);
865
+
866
+ expect(() => extractPublicKey(didDoc)).toThrow("Public key not found");
867
+ });
868
+ });
869
+ });