@kya-os/mcp-i-core 1.2.3-canary.7 → 1.3.0-canary.clientinfo.20251126003544

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 (239) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +4239 -0
  3. package/.turbo/turbo-test.log +2973 -0
  4. package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
  5. package/Composer 3.md +615 -0
  6. package/GPT-5.md +1169 -0
  7. package/OPUS-plan.md +352 -0
  8. package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
  9. package/PHASE_3_SUMMARY.md +317 -0
  10. package/PHASE_4.1.3_SUMMARY.md +428 -0
  11. package/PHASE_4.1_COMPLETE.md +525 -0
  12. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
  13. package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
  14. package/TEST_PLAN.md +571 -0
  15. package/coverage/coverage-final.json +57 -0
  16. package/dist/__tests__/utils/mock-providers.d.ts +1 -2
  17. package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
  18. package/dist/__tests__/utils/mock-providers.js.map +1 -1
  19. package/dist/cache/oauth-config-cache.d.ts +69 -0
  20. package/dist/cache/oauth-config-cache.d.ts.map +1 -0
  21. package/dist/cache/oauth-config-cache.js +76 -0
  22. package/dist/cache/oauth-config-cache.js.map +1 -0
  23. package/dist/identity/idp-token-resolver.d.ts +53 -0
  24. package/dist/identity/idp-token-resolver.d.ts.map +1 -0
  25. package/dist/identity/idp-token-resolver.js +108 -0
  26. package/dist/identity/idp-token-resolver.js.map +1 -0
  27. package/dist/identity/idp-token-storage.interface.d.ts +42 -0
  28. package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
  29. package/dist/identity/idp-token-storage.interface.js +12 -0
  30. package/dist/identity/idp-token-storage.interface.js.map +1 -0
  31. package/dist/identity/user-did-manager.d.ts +39 -1
  32. package/dist/identity/user-did-manager.d.ts.map +1 -1
  33. package/dist/identity/user-did-manager.js +69 -3
  34. package/dist/identity/user-did-manager.js.map +1 -1
  35. package/dist/index.d.ts +24 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +43 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/runtime/audit-logger.d.ts +37 -0
  40. package/dist/runtime/audit-logger.d.ts.map +1 -0
  41. package/dist/runtime/audit-logger.js +9 -0
  42. package/dist/runtime/audit-logger.js.map +1 -0
  43. package/dist/runtime/base.d.ts +58 -2
  44. package/dist/runtime/base.d.ts.map +1 -1
  45. package/dist/runtime/base.js +266 -11
  46. package/dist/runtime/base.js.map +1 -1
  47. package/dist/services/access-control.service.d.ts.map +1 -1
  48. package/dist/services/access-control.service.js +200 -35
  49. package/dist/services/access-control.service.js.map +1 -1
  50. package/dist/services/authorization/authorization-registry.d.ts +29 -0
  51. package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
  52. package/dist/services/authorization/authorization-registry.js +57 -0
  53. package/dist/services/authorization/authorization-registry.js.map +1 -0
  54. package/dist/services/authorization/types.d.ts +53 -0
  55. package/dist/services/authorization/types.d.ts.map +1 -0
  56. package/dist/services/authorization/types.js +10 -0
  57. package/dist/services/authorization/types.js.map +1 -0
  58. package/dist/services/batch-delegation.service.d.ts +53 -0
  59. package/dist/services/batch-delegation.service.d.ts.map +1 -0
  60. package/dist/services/batch-delegation.service.js +95 -0
  61. package/dist/services/batch-delegation.service.js.map +1 -0
  62. package/dist/services/index.d.ts +2 -0
  63. package/dist/services/index.d.ts.map +1 -1
  64. package/dist/services/index.js +4 -1
  65. package/dist/services/index.js.map +1 -1
  66. package/dist/services/oauth-config.service.d.ts +53 -0
  67. package/dist/services/oauth-config.service.d.ts.map +1 -0
  68. package/dist/services/oauth-config.service.js +117 -0
  69. package/dist/services/oauth-config.service.js.map +1 -0
  70. package/dist/services/oauth-provider-registry.d.ts +77 -0
  71. package/dist/services/oauth-provider-registry.d.ts.map +1 -0
  72. package/dist/services/oauth-provider-registry.js +112 -0
  73. package/dist/services/oauth-provider-registry.js.map +1 -0
  74. package/dist/services/oauth-service.d.ts +77 -0
  75. package/dist/services/oauth-service.d.ts.map +1 -0
  76. package/dist/services/oauth-service.js +348 -0
  77. package/dist/services/oauth-service.js.map +1 -0
  78. package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
  79. package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
  80. package/dist/services/oauth-token-retrieval.service.js +150 -0
  81. package/dist/services/oauth-token-retrieval.service.js.map +1 -0
  82. package/dist/services/provider-resolver.d.ts +48 -0
  83. package/dist/services/provider-resolver.d.ts.map +1 -0
  84. package/dist/services/provider-resolver.js +120 -0
  85. package/dist/services/provider-resolver.js.map +1 -0
  86. package/dist/services/provider-validator.d.ts +55 -0
  87. package/dist/services/provider-validator.d.ts.map +1 -0
  88. package/dist/services/provider-validator.js +135 -0
  89. package/dist/services/provider-validator.js.map +1 -0
  90. package/dist/services/session-registration.service.d.ts +80 -0
  91. package/dist/services/session-registration.service.d.ts.map +1 -0
  92. package/dist/services/session-registration.service.js +172 -0
  93. package/dist/services/session-registration.service.js.map +1 -0
  94. package/dist/services/tool-context-builder.d.ts +57 -0
  95. package/dist/services/tool-context-builder.d.ts.map +1 -0
  96. package/dist/services/tool-context-builder.js +125 -0
  97. package/dist/services/tool-context-builder.js.map +1 -0
  98. package/dist/services/tool-protection.service.d.ts +87 -10
  99. package/dist/services/tool-protection.service.d.ts.map +1 -1
  100. package/dist/services/tool-protection.service.js +282 -112
  101. package/dist/services/tool-protection.service.js.map +1 -1
  102. package/dist/types/oauth-required-error.d.ts +40 -0
  103. package/dist/types/oauth-required-error.d.ts.map +1 -0
  104. package/dist/types/oauth-required-error.js +40 -0
  105. package/dist/types/oauth-required-error.js.map +1 -0
  106. package/dist/utils/did-helpers.d.ts +33 -0
  107. package/dist/utils/did-helpers.d.ts.map +1 -1
  108. package/dist/utils/did-helpers.js +40 -0
  109. package/dist/utils/did-helpers.js.map +1 -1
  110. package/dist/utils/index.d.ts +1 -0
  111. package/dist/utils/index.d.ts.map +1 -1
  112. package/dist/utils/index.js +1 -0
  113. package/dist/utils/index.js.map +1 -1
  114. package/docs/API_REFERENCE.md +1362 -0
  115. package/docs/COMPLIANCE_MATRIX.md +691 -0
  116. package/docs/STATUSLIST2021_GUIDE.md +696 -0
  117. package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
  118. package/package.json +24 -50
  119. package/scripts/audit-compliance.ts +724 -0
  120. package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
  121. package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
  122. package/src/__tests__/delegation-e2e.test.ts +690 -0
  123. package/src/__tests__/identity/user-did-manager.test.ts +213 -0
  124. package/src/__tests__/index.test.ts +56 -0
  125. package/src/__tests__/integration/full-flow.test.ts +776 -0
  126. package/src/__tests__/integration.test.ts +281 -0
  127. package/src/__tests__/providers/base.test.ts +173 -0
  128. package/src/__tests__/providers/memory.test.ts +319 -0
  129. package/src/__tests__/regression/phase2-regression.test.ts +427 -0
  130. package/src/__tests__/runtime/audit-logger.test.ts +154 -0
  131. package/src/__tests__/runtime/base-extensions.test.ts +593 -0
  132. package/src/__tests__/runtime/base.test.ts +869 -0
  133. package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
  134. package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
  135. package/src/__tests__/runtime/route-interception.test.ts +686 -0
  136. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
  137. package/src/__tests__/services/agentshield-integration.test.ts +784 -0
  138. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
  139. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
  140. package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
  141. package/src/__tests__/utils/mock-providers.ts +340 -0
  142. package/src/cache/oauth-config-cache.d.ts +69 -0
  143. package/src/cache/oauth-config-cache.d.ts.map +1 -0
  144. package/src/cache/oauth-config-cache.js +71 -0
  145. package/src/cache/oauth-config-cache.js.map +1 -0
  146. package/src/cache/oauth-config-cache.ts +123 -0
  147. package/src/cache/tool-protection-cache.ts +171 -0
  148. package/src/compliance/EXAMPLE.md +412 -0
  149. package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
  150. package/src/compliance/index.ts +8 -0
  151. package/src/compliance/schema-registry.ts +460 -0
  152. package/src/compliance/schema-verifier.ts +708 -0
  153. package/src/config/__tests__/remote-config.spec.ts +268 -0
  154. package/src/config/remote-config.ts +174 -0
  155. package/src/config.ts +309 -0
  156. package/src/delegation/__tests__/audience-validator.test.ts +112 -0
  157. package/src/delegation/__tests__/bitstring.test.ts +346 -0
  158. package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
  159. package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
  160. package/src/delegation/__tests__/utils.test.ts +152 -0
  161. package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
  162. package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
  163. package/src/delegation/audience-validator.ts +52 -0
  164. package/src/delegation/bitstring.ts +278 -0
  165. package/src/delegation/cascading-revocation.ts +370 -0
  166. package/src/delegation/delegation-graph.ts +299 -0
  167. package/src/delegation/index.ts +14 -0
  168. package/src/delegation/statuslist-manager.ts +353 -0
  169. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
  170. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
  171. package/src/delegation/storage/index.ts +9 -0
  172. package/src/delegation/storage/memory-graph-storage.ts +178 -0
  173. package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
  174. package/src/delegation/utils.ts +42 -0
  175. package/src/delegation/vc-issuer.ts +232 -0
  176. package/src/delegation/vc-verifier.ts +568 -0
  177. package/src/identity/idp-token-resolver.ts +147 -0
  178. package/src/identity/idp-token-storage.interface.ts +59 -0
  179. package/src/identity/user-did-manager.ts +370 -0
  180. package/src/index.ts +271 -0
  181. package/src/providers/base.d.ts +91 -0
  182. package/src/providers/base.d.ts.map +1 -0
  183. package/src/providers/base.js +38 -0
  184. package/src/providers/base.js.map +1 -0
  185. package/src/providers/base.ts +96 -0
  186. package/src/providers/memory.ts +142 -0
  187. package/src/runtime/audit-logger.ts +39 -0
  188. package/src/runtime/base.ts +1329 -0
  189. package/src/services/__tests__/access-control.integration.test.ts +443 -0
  190. package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
  191. package/src/services/__tests__/access-control.service.test.ts +970 -0
  192. package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
  193. package/src/services/__tests__/crypto.service.test.ts +531 -0
  194. package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
  195. package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
  196. package/src/services/__tests__/proof-verifier.test.ts +489 -0
  197. package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
  198. package/src/services/__tests__/provider-resolver.test.ts +217 -0
  199. package/src/services/__tests__/storage.service.test.ts +358 -0
  200. package/src/services/access-control.service.ts +990 -0
  201. package/src/services/authorization/authorization-registry.ts +66 -0
  202. package/src/services/authorization/types.ts +71 -0
  203. package/src/services/batch-delegation.service.ts +137 -0
  204. package/src/services/crypto.service.ts +302 -0
  205. package/src/services/errors.ts +76 -0
  206. package/src/services/index.ts +18 -0
  207. package/src/services/oauth-config.service.d.ts +53 -0
  208. package/src/services/oauth-config.service.d.ts.map +1 -0
  209. package/src/services/oauth-config.service.js +113 -0
  210. package/src/services/oauth-config.service.js.map +1 -0
  211. package/src/services/oauth-config.service.ts +166 -0
  212. package/src/services/oauth-provider-registry.d.ts +57 -0
  213. package/src/services/oauth-provider-registry.d.ts.map +1 -0
  214. package/src/services/oauth-provider-registry.js +73 -0
  215. package/src/services/oauth-provider-registry.js.map +1 -0
  216. package/src/services/oauth-provider-registry.ts +123 -0
  217. package/src/services/oauth-service.ts +510 -0
  218. package/src/services/oauth-token-retrieval.service.ts +245 -0
  219. package/src/services/proof-verifier.ts +478 -0
  220. package/src/services/provider-resolver.d.ts +48 -0
  221. package/src/services/provider-resolver.d.ts.map +1 -0
  222. package/src/services/provider-resolver.js +106 -0
  223. package/src/services/provider-resolver.js.map +1 -0
  224. package/src/services/provider-resolver.ts +144 -0
  225. package/src/services/provider-validator.ts +170 -0
  226. package/src/services/session-registration.service.ts +251 -0
  227. package/src/services/storage.service.ts +566 -0
  228. package/src/services/tool-context-builder.ts +172 -0
  229. package/src/services/tool-protection.service.ts +958 -0
  230. package/src/types/oauth-required-error.ts +63 -0
  231. package/src/types/tool-protection.ts +155 -0
  232. package/src/utils/__tests__/did-helpers.test.ts +101 -0
  233. package/src/utils/base64.ts +148 -0
  234. package/src/utils/cors.ts +83 -0
  235. package/src/utils/did-helpers.ts +150 -0
  236. package/src/utils/index.ts +8 -0
  237. package/src/utils/storage-keys.ts +278 -0
  238. package/tsconfig.json +21 -0
  239. package/vitest.config.ts +56 -0
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Tests for ProofVerifier
3
+ *
4
+ * Comprehensive security test coverage for proof verification service.
5
+ * Tests nonce replay protection, timestamp skew validation, canonical payload reconstruction,
6
+ * and various security attack scenarios.
7
+ *
8
+ * Test Coverage Requirements: 100% - All security-critical code paths
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
12
+ import { ProofVerifier } from '../proof-verifier.js';
13
+ import { CryptoService, type Ed25519JWK } from '../crypto.service.js';
14
+ import type {
15
+ CryptoProvider,
16
+ ClockProvider,
17
+ NonceCacheProvider,
18
+ FetchProvider,
19
+ } from '../../providers/base.js';
20
+ import type { DetachedProof } from '@kya-os/contracts/proof';
21
+ import {
22
+ ProofVerificationError,
23
+ PROOF_VERIFICATION_ERROR_CODES,
24
+ } from '../errors.js';
25
+
26
+ describe('ProofVerifier Security', () => {
27
+ let proofVerifier: ProofVerifier;
28
+ let mockCryptoProvider: CryptoProvider;
29
+ let mockClockProvider: ClockProvider;
30
+ let mockNonceCache: NonceCacheProvider;
31
+ let mockFetchProvider: FetchProvider;
32
+ let cryptoService: CryptoService;
33
+
34
+ const validJwk: Ed25519JWK = {
35
+ kty: 'OKP',
36
+ crv: 'Ed25519',
37
+ x: 'VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ',
38
+ kid: 'did:key:z123#key-1',
39
+ };
40
+
41
+ const createValidProof = (): DetachedProof => {
42
+ const header = { alg: 'EdDSA', typ: 'JWT' };
43
+ // Create a proper JSON payload that matches the meta structure
44
+ const payload = {
45
+ aud: 'test-audience',
46
+ sub: 'did:key:z123',
47
+ iss: 'did:key:z123',
48
+ nonce: 'nonce123',
49
+ ts: Math.floor(Date.now() / 1000),
50
+ sessionId: 'session123',
51
+ requestHash: 'sha256:' + 'a'.repeat(64),
52
+ responseHash: 'sha256:' + 'b'.repeat(64),
53
+ };
54
+ // Use btoa for base64 encoding (available in test environment via polyfill)
55
+ const headerB64 = btoa(JSON.stringify(header))
56
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
57
+ const payloadB64 = btoa(JSON.stringify(payload))
58
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
59
+ const signatureB64 = btoa('signature')
60
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
61
+ const jws = `${headerB64}.${payloadB64}.${signatureB64}`;
62
+
63
+ return {
64
+ jws,
65
+ meta: {
66
+ did: 'did:key:z123',
67
+ kid: 'did:key:z123#key-1',
68
+ ts: Math.floor(Date.now() / 1000),
69
+ nonce: 'nonce123',
70
+ audience: 'test-audience',
71
+ sessionId: 'session123',
72
+ requestHash: 'sha256:' + 'a'.repeat(64),
73
+ responseHash: 'sha256:' + 'b'.repeat(64),
74
+ },
75
+ };
76
+ };
77
+
78
+ beforeEach(() => {
79
+ mockCryptoProvider = {
80
+ sign: vi.fn(),
81
+ verify: vi.fn().mockResolvedValue(true),
82
+ generateKeyPair: vi.fn(),
83
+ hash: vi.fn(),
84
+ randomBytes: vi.fn(),
85
+ };
86
+
87
+ cryptoService = new CryptoService(mockCryptoProvider);
88
+
89
+ mockClockProvider = {
90
+ now: vi.fn().mockReturnValue(Date.now()), // Return milliseconds
91
+ isWithinSkew: vi.fn().mockReturnValue(true),
92
+ hasExpired: vi.fn(),
93
+ calculateExpiry: vi.fn((ttlSeconds: number) => Date.now() + (ttlSeconds * 1000)), // Return milliseconds
94
+ format: vi.fn(),
95
+ };
96
+
97
+ mockNonceCache = {
98
+ has: vi.fn().mockResolvedValue(false),
99
+ add: vi.fn().mockResolvedValue(undefined),
100
+ cleanup: vi.fn().mockResolvedValue(undefined),
101
+ destroy: vi.fn().mockResolvedValue(undefined),
102
+ };
103
+
104
+ mockFetchProvider = {
105
+ resolveDID: vi.fn().mockResolvedValue({
106
+ verificationMethod: [{
107
+ id: 'did:key:z123#key-1',
108
+ publicKeyJwk: validJwk,
109
+ }],
110
+ }),
111
+ fetchStatusList: vi.fn(),
112
+ fetchDelegationChain: vi.fn(),
113
+ fetch: vi.fn(),
114
+ };
115
+
116
+ proofVerifier = new ProofVerifier({
117
+ cryptoProvider: mockCryptoProvider,
118
+ clockProvider: mockClockProvider,
119
+ nonceCacheProvider: mockNonceCache,
120
+ fetchProvider: mockFetchProvider,
121
+ timestampSkewSeconds: 120,
122
+ nonceTtlSeconds: 300,
123
+ });
124
+ });
125
+
126
+ describe('Nonce Replay Protection', () => {
127
+ it('should prevent nonce replay attacks', async () => {
128
+ const proof = createValidProof();
129
+
130
+ // First verification should succeed
131
+ const result1 = await proofVerifier.verifyProof(proof, validJwk);
132
+ expect(result1.valid).toBe(true);
133
+ expect(mockNonceCache.has).toHaveBeenCalledWith('nonce123', 'did:key:z123');
134
+ expect(mockNonceCache.add).toHaveBeenCalled();
135
+
136
+ // Reset mock to simulate second attempt
137
+ mockNonceCache.has = vi.fn().mockResolvedValue(true);
138
+
139
+ // Second verification with same nonce should fail
140
+ const result2 = await proofVerifier.verifyProof(proof, validJwk);
141
+ expect(result2.valid).toBe(false);
142
+ expect(result2.reason).toContain('replay');
143
+ });
144
+
145
+ it('should add nonce to cache after successful verification', async () => {
146
+ const proof = createValidProof();
147
+
148
+ await proofVerifier.verifyProof(proof, validJwk);
149
+
150
+ expect(mockNonceCache.add).toHaveBeenCalledWith(
151
+ 'nonce123',
152
+ expect.any(Number),
153
+ 'did:key:z123'
154
+ );
155
+ });
156
+ });
157
+
158
+ describe('Timestamp Skew Validation', () => {
159
+ it('should enforce timestamp skew limits', async () => {
160
+ const proof = createValidProof();
161
+ const currentTime = Date.now(); // milliseconds
162
+
163
+ // Set clock to 5 minutes in the future
164
+ mockClockProvider.now = vi.fn().mockReturnValue(currentTime);
165
+ mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(false);
166
+
167
+ const result = await proofVerifier.verifyProof(proof, validJwk);
168
+
169
+ expect(result.valid).toBe(false);
170
+ expect(result.reason).toContain('skew');
171
+ // isWithinSkew is called with timestamp in milliseconds (converted from seconds)
172
+ expect(mockClockProvider.isWithinSkew).toHaveBeenCalledWith(
173
+ proof.meta.ts * 1000, // Convert seconds to milliseconds
174
+ 120
175
+ );
176
+ });
177
+
178
+ it('should accept timestamps within skew window', async () => {
179
+ const proof = createValidProof();
180
+ mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(true);
181
+
182
+ const result = await proofVerifier.verifyProof(proof, validJwk);
183
+
184
+ expect(result.valid).toBe(true);
185
+ });
186
+
187
+ it('should use custom timestamp skew seconds', async () => {
188
+ const customProofVerifier = new ProofVerifier({
189
+ cryptoProvider: mockCryptoProvider,
190
+ clockProvider: mockClockProvider,
191
+ nonceCacheProvider: mockNonceCache,
192
+ fetchProvider: mockFetchProvider,
193
+ timestampSkewSeconds: 300, // 5 minutes
194
+ nonceTtlSeconds: 300,
195
+ });
196
+
197
+ const proof = createValidProof();
198
+ mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(false);
199
+
200
+ await customProofVerifier.verifyProof(proof, validJwk);
201
+
202
+ // isWithinSkew is called with timestamp in milliseconds (converted from seconds)
203
+ expect(mockClockProvider.isWithinSkew).toHaveBeenCalledWith(
204
+ proof.meta.ts * 1000, // Convert seconds to milliseconds
205
+ 300
206
+ );
207
+ });
208
+ });
209
+
210
+ describe('Canonical Payload Reconstruction', () => {
211
+ it('should reconstruct canonical payload from meta', async () => {
212
+ const proof = createValidProof();
213
+
214
+ await proofVerifier.verifyProof(proof, validJwk);
215
+
216
+ // Verify that verifyJWS was called with detached payload
217
+ expect(mockCryptoProvider.verify).toHaveBeenCalled();
218
+ });
219
+
220
+ it('should validate canonical payload ordering determinism', () => {
221
+ const meta1 = {
222
+ z: 1,
223
+ a: 2,
224
+ m: 3,
225
+ did: 'did:test',
226
+ kid: 'kid',
227
+ ts: 123,
228
+ nonce: 'nonce',
229
+ audience: 'aud',
230
+ sessionId: 'session',
231
+ requestHash: 'sha256:' + 'a'.repeat(64),
232
+ responseHash: 'sha256:' + 'b'.repeat(64),
233
+ };
234
+ const meta2 = {
235
+ a: 2,
236
+ m: 3,
237
+ z: 1,
238
+ did: 'did:test',
239
+ kid: 'kid',
240
+ ts: 123,
241
+ nonce: 'nonce',
242
+ audience: 'aud',
243
+ sessionId: 'session',
244
+ requestHash: 'sha256:' + 'a'.repeat(64),
245
+ responseHash: 'sha256:' + 'b'.repeat(64),
246
+ };
247
+
248
+ const canonical1 = proofVerifier.buildCanonicalPayload(meta1);
249
+ const canonical2 = proofVerifier.buildCanonicalPayload(meta2);
250
+
251
+ // Should be identical despite different key order
252
+ expect(canonical1).toBe(canonical2);
253
+ });
254
+
255
+ it('should handle detached JWS reconstruction', async () => {
256
+ const header = { alg: 'EdDSA' };
257
+ const headerB64 = btoa(JSON.stringify(header))
258
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
259
+ const signatureB64 = btoa('signature')
260
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
261
+ const detachedJws = `${headerB64}..${signatureB64}`;
262
+
263
+ const proof: DetachedProof = {
264
+ jws: detachedJws,
265
+ meta: createValidProof().meta,
266
+ };
267
+
268
+ const result = await proofVerifier.verifyProof(proof, validJwk);
269
+
270
+ // Should call verifyJWS with detached payload
271
+ expect(mockCryptoProvider.verify).toHaveBeenCalled();
272
+ expect(result.valid).toBe(true);
273
+ });
274
+ });
275
+
276
+ describe('Proof Structure Validation', () => {
277
+ it('should reject invalid proof structure', async () => {
278
+ const invalidProof = {
279
+ jws: 'invalid',
280
+ meta: {
281
+ // Missing required fields
282
+ did: 'did:test',
283
+ },
284
+ } as any;
285
+
286
+ const result = await proofVerifier.verifyProof(invalidProof, validJwk);
287
+
288
+ expect(result.valid).toBe(false);
289
+ expect(result.reason).toContain('Invalid proof structure');
290
+ });
291
+
292
+ it('should reject proof with missing required meta fields', async () => {
293
+ const invalidProof: DetachedProof = {
294
+ jws: 'header.payload.signature',
295
+ meta: {
296
+ did: 'did:test',
297
+ kid: 'kid',
298
+ ts: 123,
299
+ nonce: 'nonce',
300
+ audience: 'aud',
301
+ sessionId: 'session',
302
+ // Missing requestHash and responseHash
303
+ requestHash: '' as any,
304
+ responseHash: '' as any,
305
+ },
306
+ };
307
+
308
+ const result = await proofVerifier.verifyProof(invalidProof, validJwk);
309
+
310
+ expect(result.valid).toBe(false);
311
+ });
312
+ });
313
+
314
+ describe('Signature Verification', () => {
315
+ it('should reject proof with invalid signature', async () => {
316
+ const proof = createValidProof();
317
+ mockCryptoProvider.verify = vi.fn().mockResolvedValue(false);
318
+
319
+ const result = await proofVerifier.verifyProof(proof, validJwk);
320
+
321
+ expect(result.valid).toBe(false);
322
+ expect(result.reason).toContain('Invalid JWS signature');
323
+ });
324
+
325
+ it('should handle signature verification errors gracefully', async () => {
326
+ const proof = createValidProof();
327
+ mockCryptoProvider.verify = vi.fn().mockRejectedValue(
328
+ new Error('Crypto error')
329
+ );
330
+
331
+ const result = await proofVerifier.verifyProof(proof, validJwk);
332
+
333
+ expect(result.valid).toBe(false);
334
+ expect(result.reason).toBeDefined();
335
+ // Should not throw, should return error result
336
+ });
337
+ });
338
+
339
+ describe('verifyProofDetached', () => {
340
+ it('should verify proof with string canonical payload', async () => {
341
+ const proof = createValidProof();
342
+ const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
343
+
344
+ const result = await proofVerifier.verifyProofDetached(
345
+ proof,
346
+ canonicalPayload,
347
+ validJwk
348
+ );
349
+
350
+ expect(result.valid).toBe(true);
351
+ });
352
+
353
+ it('should verify proof with Uint8Array canonical payload', async () => {
354
+ const proof = createValidProof();
355
+ const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
356
+ const canonicalPayloadBytes = new TextEncoder().encode(canonicalPayload);
357
+
358
+ const result = await proofVerifier.verifyProofDetached(
359
+ proof,
360
+ canonicalPayloadBytes,
361
+ validJwk
362
+ );
363
+
364
+ expect(result.valid).toBe(true);
365
+ });
366
+
367
+ it('should prevent nonce replay in verifyProofDetached', async () => {
368
+ const proof = createValidProof();
369
+ const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
370
+
371
+ // First verification
372
+ const result1 = await proofVerifier.verifyProofDetached(
373
+ proof,
374
+ canonicalPayload,
375
+ validJwk
376
+ );
377
+ expect(result1.valid).toBe(true);
378
+
379
+ // Second verification should fail
380
+ mockNonceCache.has = vi.fn().mockResolvedValue(true);
381
+ const result2 = await proofVerifier.verifyProofDetached(
382
+ proof,
383
+ canonicalPayload,
384
+ validJwk
385
+ );
386
+ expect(result2.valid).toBe(false);
387
+ expect(result2.reason).toContain('replay');
388
+ });
389
+ });
390
+
391
+ describe('Error Handling', () => {
392
+ it('should never throw on verification errors', async () => {
393
+ const proof = createValidProof();
394
+
395
+ // Simulate various error conditions
396
+ mockNonceCache.has = vi.fn().mockRejectedValue(new Error('Cache error'));
397
+
398
+ const result = await proofVerifier.verifyProof(proof, validJwk);
399
+
400
+ // Should return error result, not throw
401
+ expect(result.valid).toBe(false);
402
+ expect(result.reason).toBeDefined();
403
+ });
404
+
405
+ it('should handle clock provider errors gracefully', async () => {
406
+ const proof = createValidProof();
407
+ mockClockProvider.isWithinSkew = vi.fn().mockImplementation(() => {
408
+ throw new Error('Clock error');
409
+ });
410
+
411
+ const result = await proofVerifier.verifyProof(proof, validJwk);
412
+
413
+ expect(result.valid).toBe(false);
414
+ expect(result.reason).toBeDefined();
415
+ });
416
+ });
417
+
418
+ describe('fetchPublicKeyFromDID', () => {
419
+ it('should fetch public key from DID document', async () => {
420
+ const jwk = await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'key-1');
421
+
422
+ expect(jwk).toEqual(validJwk);
423
+ expect(mockFetchProvider.resolveDID).toHaveBeenCalledWith('did:key:z123');
424
+ });
425
+
426
+ it('should throw ProofVerificationError if DID document not found', async () => {
427
+ mockFetchProvider.resolveDID = vi.fn().mockResolvedValue(null);
428
+
429
+ await expect(
430
+ proofVerifier.fetchPublicKeyFromDID('did:key:z123')
431
+ ).rejects.toThrow(ProofVerificationError);
432
+
433
+ try {
434
+ await proofVerifier.fetchPublicKeyFromDID('did:key:z123');
435
+ } catch (error) {
436
+ expect(error).toBeInstanceOf(ProofVerificationError);
437
+ expect((error as ProofVerificationError).code).toBe(
438
+ PROOF_VERIFICATION_ERROR_CODES.DID_DOCUMENT_NOT_FOUND
439
+ );
440
+ }
441
+ });
442
+
443
+ it('should throw ProofVerificationError if verification method not found', async () => {
444
+ mockFetchProvider.resolveDID = vi.fn().mockResolvedValue({
445
+ verificationMethod: [],
446
+ });
447
+
448
+ await expect(
449
+ proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'key-1')
450
+ ).rejects.toThrow(ProofVerificationError);
451
+
452
+ try {
453
+ await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'key-1');
454
+ } catch (error) {
455
+ expect(error).toBeInstanceOf(ProofVerificationError);
456
+ expect((error as ProofVerificationError).code).toBe(
457
+ PROOF_VERIFICATION_ERROR_CODES.VERIFICATION_METHOD_NOT_FOUND
458
+ );
459
+ }
460
+ });
461
+
462
+ it('should throw ProofVerificationError if JWK is not Ed25519', async () => {
463
+ mockFetchProvider.resolveDID = vi.fn().mockResolvedValue({
464
+ verificationMethod: [{
465
+ id: 'did:key:z123#key-1',
466
+ publicKeyJwk: {
467
+ kty: 'RSA',
468
+ crv: 'RS256',
469
+ n: 'invalid',
470
+ },
471
+ }],
472
+ });
473
+
474
+ await expect(
475
+ proofVerifier.fetchPublicKeyFromDID('did:key:z123')
476
+ ).rejects.toThrow(ProofVerificationError);
477
+
478
+ try {
479
+ await proofVerifier.fetchPublicKeyFromDID('did:key:z123');
480
+ } catch (error) {
481
+ expect(error).toBeInstanceOf(ProofVerificationError);
482
+ expect((error as ProofVerificationError).code).toBe(
483
+ PROOF_VERIFICATION_ERROR_CODES.INVALID_JWK_FORMAT
484
+ );
485
+ }
486
+ });
487
+ });
488
+ });
489
+
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Provider Resolution Integration Tests
3
+ *
4
+ * Tests end-to-end provider resolution flow and backward compatibility.
5
+ *
6
+ * @package @kya-os/mcp-i-core
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from "vitest";
10
+ import { OAuthProviderRegistry } from "../oauth-provider-registry.js";
11
+ import { ProviderResolver } from "../provider-resolver.js";
12
+ import { OAuthConfigService } from "../oauth-config.service.js";
13
+ import { ToolProtectionService } from "../tool-protection.service.js";
14
+ import type { ToolProtection } from "@kya-os/contracts/tool-protection";
15
+ import type { OAuthConfig } from "@kya-os/contracts/config";
16
+
17
+ describe("Provider Resolution Integration", () => {
18
+ let mockConfigService: OAuthConfigService;
19
+ let mockToolProtectionService: ToolProtectionService;
20
+ let providerRegistry: OAuthProviderRegistry;
21
+ let providerResolver: ProviderResolver;
22
+
23
+ const mockOAuthConfig: OAuthConfig = {
24
+ providers: {
25
+ github: {
26
+ clientId: "github_client_id",
27
+ authorizationUrl: "https://github.com/login/oauth/authorize",
28
+ tokenUrl: "https://github.com/login/oauth/access_token",
29
+ supportsPKCE: true,
30
+ requiresClientSecret: false,
31
+ },
32
+ google: {
33
+ clientId: "google_client_id",
34
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
35
+ tokenUrl: "https://oauth2.googleapis.com/token",
36
+ supportsPKCE: true,
37
+ requiresClientSecret: false,
38
+ },
39
+ },
40
+ };
41
+
42
+ beforeEach(() => {
43
+ mockConfigService = {
44
+ getOAuthConfig: vi.fn().mockResolvedValue(mockOAuthConfig),
45
+ } as any;
46
+
47
+ mockToolProtectionService = {
48
+ checkToolProtection: vi.fn(),
49
+ getProjectId: vi.fn().mockReturnValue("test-project"),
50
+ } as any;
51
+
52
+ providerRegistry = new OAuthProviderRegistry(mockConfigService);
53
+ providerResolver = new ProviderResolver(providerRegistry, mockConfigService);
54
+ });
55
+
56
+ describe("End-to-end provider resolution", () => {
57
+ it("should resolve provider for tool with oauthProvider field", async () => {
58
+ await providerRegistry.loadFromAgentShield("test-project");
59
+
60
+ const toolProtection: ToolProtection = {
61
+ requiresDelegation: true,
62
+ requiredScopes: ["repo:read"],
63
+ oauthProvider: "github",
64
+ };
65
+
66
+ const provider = await providerResolver.resolveProvider(
67
+ toolProtection,
68
+ "test-project"
69
+ );
70
+
71
+ expect(provider).toBe("github");
72
+ });
73
+
74
+ it("should resolve provider via scope inference when oauthProvider not specified", async () => {
75
+ await providerRegistry.loadFromAgentShield("test-project");
76
+
77
+ const toolProtection: ToolProtection = {
78
+ requiresDelegation: true,
79
+ requiredScopes: ["github:repo:read"],
80
+ // No oauthProvider field (Phase 1 compatibility)
81
+ };
82
+
83
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
84
+
85
+ const provider = await providerResolver.resolveProvider(
86
+ toolProtection,
87
+ "test-project"
88
+ );
89
+
90
+ expect(provider).toBe("github");
91
+ expect(consoleSpy).toHaveBeenCalledWith(
92
+ expect.stringContaining("Inferred provider")
93
+ );
94
+
95
+ consoleSpy.mockRestore();
96
+ });
97
+
98
+ it("should fall back to first configured provider for Phase 1 compatibility", async () => {
99
+ await providerRegistry.loadFromAgentShield("test-project");
100
+
101
+ const toolProtection: ToolProtection = {
102
+ requiresDelegation: true,
103
+ requiredScopes: ["custom:scope"], // No recognizable prefix
104
+ // No oauthProvider field
105
+ };
106
+
107
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
108
+
109
+ const provider = await providerResolver.resolveProvider(
110
+ toolProtection,
111
+ "test-project"
112
+ );
113
+
114
+ // Should use first configured provider (github)
115
+ expect(provider).toBe("github");
116
+ expect(consoleSpy).toHaveBeenCalledWith(
117
+ expect.stringContaining("deprecated")
118
+ );
119
+
120
+ consoleSpy.mockRestore();
121
+ });
122
+ });
123
+
124
+ describe("Backward compatibility", () => {
125
+ it("should work with Phase 1 tools (no oauthProvider field)", async () => {
126
+ await providerRegistry.loadFromAgentShield("test-project");
127
+
128
+ const toolProtection: ToolProtection = {
129
+ requiresDelegation: true,
130
+ requiredScopes: ["repo:read"],
131
+ // No oauthProvider - Phase 1 style
132
+ };
133
+
134
+ // Should not throw - uses fallback resolution
135
+ const provider = await providerResolver.resolveProvider(
136
+ toolProtection,
137
+ "test-project"
138
+ );
139
+
140
+ expect(provider).toBeDefined();
141
+ expect(typeof provider).toBe("string");
142
+ });
143
+
144
+ it("should handle tools with empty requiredScopes", async () => {
145
+ await providerRegistry.loadFromAgentShield("test-project");
146
+
147
+ const toolProtection: ToolProtection = {
148
+ requiresDelegation: true,
149
+ requiredScopes: [],
150
+ // No oauthProvider
151
+ };
152
+
153
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
154
+
155
+ const provider = await providerResolver.resolveProvider(
156
+ toolProtection,
157
+ "test-project"
158
+ );
159
+
160
+ // Should fall back to first provider
161
+ expect(provider).toBe("github");
162
+ expect(consoleSpy).toHaveBeenCalled();
163
+
164
+ consoleSpy.mockRestore();
165
+ });
166
+ });
167
+
168
+ describe("Multi-provider scenarios", () => {
169
+ it("should resolve different providers for different tools", async () => {
170
+ await providerRegistry.loadFromAgentShield("test-project");
171
+
172
+ const githubTool: ToolProtection = {
173
+ requiresDelegation: true,
174
+ requiredScopes: ["repo:read"],
175
+ oauthProvider: "github",
176
+ };
177
+
178
+ const googleTool: ToolProtection = {
179
+ requiresDelegation: true,
180
+ requiredScopes: ["calendar:read"],
181
+ oauthProvider: "google",
182
+ };
183
+
184
+ const githubProvider = await providerResolver.resolveProvider(
185
+ githubTool,
186
+ "test-project"
187
+ );
188
+ const googleProvider = await providerResolver.resolveProvider(
189
+ googleTool,
190
+ "test-project"
191
+ );
192
+
193
+ expect(githubProvider).toBe("github");
194
+ expect(googleProvider).toBe("google");
195
+ });
196
+ });
197
+ });
198
+