@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,1373 +0,0 @@
1
- /**
2
- * Tool Protection Service Tests
3
- *
4
- * Tests for fetching and caching tool protection configurations from AgentShield API
5
- */
6
-
7
- import { describe, test, expect, beforeEach, vi, afterEach } from 'vitest';
8
- import { ToolProtectionService } from '../../services/tool-protection.service';
9
- import {
10
- InMemoryToolProtectionCache,
11
- NoOpToolProtectionCache,
12
- type ToolProtectionCache,
13
- } from '../../cache/tool-protection-cache';
14
- import type {
15
- ToolProtectionServiceConfig,
16
- ToolProtectionConfig,
17
- DelegationRequiredError,
18
- } from '../../types/tool-protection';
19
-
20
- // Mock global fetch
21
- global.fetch = vi.fn();
22
-
23
- describe('ToolProtectionService', () => {
24
- let service: ToolProtectionService;
25
- let cache: ToolProtectionCache;
26
- let config: ToolProtectionServiceConfig;
27
- const mockAgentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
28
-
29
- beforeEach(() => {
30
- vi.clearAllMocks();
31
- cache = new InMemoryToolProtectionCache();
32
- config = {
33
- apiUrl: 'https://kya.vouched.id',
34
- apiKey: 'test-api-key-12345',
35
- cacheTtl: 300000, // 5 minutes
36
- debug: false,
37
- };
38
- service = new ToolProtectionService(config, cache);
39
- });
40
-
41
- afterEach(() => {
42
- vi.restoreAllMocks();
43
- });
44
-
45
- describe('constructor', () => {
46
- test('should initialize with config and cache', () => {
47
- expect(service).toBeDefined();
48
- expect((service as any).config).toEqual(config);
49
- expect((service as any).cache).toBe(cache);
50
- });
51
- });
52
-
53
- describe('getToolProtectionConfig', () => {
54
- describe('cache hit', () => {
55
- test('should return cached config if available', async () => {
56
- const cachedConfig: ToolProtectionConfig = {
57
- toolProtections: {
58
- greet: {
59
- requiresDelegation: false,
60
- requiredScopes: [],
61
- },
62
- },
63
- };
64
-
65
- await cache.set('agent:test-did', cachedConfig, 300000);
66
-
67
- const config = await service.getToolProtectionConfig('test-did');
68
-
69
- expect(config).toEqual(cachedConfig);
70
- expect(global.fetch).not.toHaveBeenCalled();
71
- });
72
-
73
- test('should use project-scoped cache key when projectId is set', async () => {
74
- const projectId = 'test-project-123';
75
- config.projectId = projectId;
76
- service = new ToolProtectionService(config, cache);
77
-
78
- const cachedConfig: ToolProtectionConfig = {
79
- toolProtections: {
80
- greet: {
81
- requiresDelegation: false,
82
- requiredScopes: [],
83
- },
84
- },
85
- };
86
-
87
- const cacheKey = `config:tool-protections:${projectId}`;
88
- await cache.set(cacheKey, cachedConfig, 300000);
89
-
90
- const result = await service.getToolProtectionConfig(mockAgentDid);
91
-
92
- expect(result).toEqual(cachedConfig);
93
- expect(global.fetch).not.toHaveBeenCalled();
94
- });
95
-
96
- test('should use agent-scoped cache key when projectId is not set', async () => {
97
- const cachedConfig: ToolProtectionConfig = {
98
- toolProtections: {
99
- greet: {
100
- requiresDelegation: false,
101
- requiredScopes: [],
102
- },
103
- },
104
- };
105
-
106
- const cacheKey = `agent:${mockAgentDid}`;
107
- await cache.set(cacheKey, cachedConfig, 300000);
108
-
109
- const result = await service.getToolProtectionConfig(mockAgentDid);
110
-
111
- expect(result).toEqual(cachedConfig);
112
- expect(global.fetch).not.toHaveBeenCalled();
113
- });
114
- });
115
-
116
- describe('API fetch - merged config format', () => {
117
- test('should fetch from project-scoped /config endpoint when projectId is available', async () => {
118
- const projectId = 'test-project-123';
119
- config.projectId = projectId;
120
- service = new ToolProtectionService(config, cache);
121
-
122
- // Merged config format - tools embedded at config.toolProtection.tools
123
- const apiResponse = {
124
- success: true,
125
- data: {
126
- config: {
127
- toolProtection: {
128
- source: 'agentshield',
129
- tools: {
130
- checkout: {
131
- requiresDelegation: true,
132
- requiredScopes: ['cart:write'],
133
- },
134
- search: {
135
- requiresDelegation: false,
136
- requiredScopes: [],
137
- },
138
- },
139
- },
140
- },
141
- },
142
- metadata: {
143
- timestamp: new Date().toISOString(),
144
- cachedUntil: new Date(Date.now() + 300000).toISOString(),
145
- },
146
- };
147
-
148
- (global.fetch as any).mockResolvedValueOnce({
149
- ok: true,
150
- json: async () => apiResponse,
151
- });
152
-
153
- const result = await service.getToolProtectionConfig(mockAgentDid);
154
-
155
- // Now calls /config endpoint (not /tool-protections)
156
- expect(global.fetch).toHaveBeenCalledWith(
157
- `https://kya.vouched.id/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/config`,
158
- expect.objectContaining({
159
- method: 'GET',
160
- headers: expect.objectContaining({
161
- 'Content-Type': 'application/json',
162
- 'X-API-Key': 'test-api-key-12345',
163
- 'X-Project-Id': projectId,
164
- }),
165
- })
166
- );
167
-
168
- expect(result.toolProtections.checkout).toBeDefined();
169
- expect(result.toolProtections.checkout.requiresDelegation).toBe(true);
170
- expect(result.toolProtections.checkout.requiredScopes).toEqual(['cart:write']);
171
- });
172
-
173
- test('should handle new endpoint format with toolProtections object', async () => {
174
- const projectId = 'test-project-123';
175
- config.projectId = projectId;
176
- service = new ToolProtectionService(config, cache);
177
-
178
- const apiResponse = {
179
- success: true,
180
- data: {
181
- toolProtections: {
182
- protected_tool: {
183
- requires_delegation: true,
184
- required_scopes: ['scope1', 'scope2'],
185
- },
186
- unprotected_tool: {
187
- requires_delegation: false,
188
- scopes: [],
189
- },
190
- },
191
- },
192
- metadata: {},
193
- };
194
-
195
- (global.fetch as any).mockResolvedValueOnce({
196
- ok: true,
197
- json: async () => apiResponse,
198
- });
199
-
200
- const result = await service.getToolProtectionConfig(mockAgentDid);
201
-
202
- expect(result.toolProtections.protected_tool.requiresDelegation).toBe(true);
203
- expect(result.toolProtections.protected_tool.requiredScopes).toEqual(['scope1', 'scope2']);
204
- expect(result.toolProtections.unprotected_tool.requiresDelegation).toBe(false);
205
- });
206
-
207
- test('should parse oauthProvider from new endpoint format (Phase 2)', async () => {
208
- const projectId = 'test-project-123';
209
- config.projectId = projectId;
210
- service = new ToolProtectionService(config, cache);
211
-
212
- const apiResponse = {
213
- success: true,
214
- data: {
215
- toolProtections: {
216
- read_repos: {
217
- requiresDelegation: true,
218
- requiredScopes: ['repo:read'],
219
- oauthProvider: 'github',
220
- },
221
- send_email: {
222
- requiresDelegation: true,
223
- requiredScopes: ['gmail:send'],
224
- oauthProvider: 'google',
225
- },
226
- },
227
- },
228
- metadata: {},
229
- };
230
-
231
- (global.fetch as any).mockResolvedValueOnce({
232
- ok: true,
233
- json: async () => apiResponse,
234
- });
235
-
236
- const result = await service.getToolProtectionConfig(mockAgentDid);
237
-
238
- expect(result.toolProtections.read_repos.oauthProvider).toBe('github');
239
- expect(result.toolProtections.send_email.oauthProvider).toBe('google');
240
- });
241
-
242
- test('should preserve oauthProvider through cache operations', async () => {
243
- const projectId = 'test-project-123';
244
- config.projectId = projectId;
245
- service = new ToolProtectionService(config, cache);
246
-
247
- const apiResponse = {
248
- success: true,
249
- data: {
250
- toolProtections: {
251
- read_repos: {
252
- requiresDelegation: true,
253
- requiredScopes: ['repo:read'],
254
- oauthProvider: 'github',
255
- },
256
- },
257
- },
258
- metadata: {},
259
- };
260
-
261
- (global.fetch as any).mockResolvedValueOnce({
262
- ok: true,
263
- json: async () => apiResponse,
264
- });
265
-
266
- // First call - should fetch and cache
267
- const result1 = await service.getToolProtectionConfig(mockAgentDid);
268
- expect(result1.toolProtections.read_repos.oauthProvider).toBe('github');
269
-
270
- // Second call - should use cache
271
- const result2 = await service.getToolProtectionConfig(mockAgentDid);
272
- expect(result2.toolProtections.read_repos.oauthProvider).toBe('github');
273
- expect(global.fetch).toHaveBeenCalledTimes(1); // Still 1, not 2
274
- });
275
- });
276
-
277
- describe('API fetch - old endpoint format', () => {
278
- test('should fetch from agent-scoped endpoint when projectId is not available', async () => {
279
- const apiResponse = {
280
- success: true,
281
- data: {
282
- tools: [
283
- {
284
- name: 'checkout',
285
- requiresDelegation: true,
286
- scopes: ['cart:write'],
287
- },
288
- {
289
- name: 'search',
290
- requiresDelegation: false,
291
- scopes: [],
292
- },
293
- ],
294
- },
295
- metadata: {},
296
- };
297
-
298
- (global.fetch as any).mockResolvedValueOnce({
299
- ok: true,
300
- json: async () => apiResponse,
301
- });
302
-
303
- const result = await service.getToolProtectionConfig(mockAgentDid);
304
-
305
- expect(global.fetch).toHaveBeenCalledWith(
306
- `https://kya.vouched.id/api/v1/bouncer/config?agent_did=${encodeURIComponent(mockAgentDid)}`,
307
- expect.objectContaining({
308
- method: 'GET',
309
- headers: expect.objectContaining({
310
- 'Content-Type': 'application/json',
311
- Authorization: 'Bearer test-api-key-12345',
312
- }),
313
- })
314
- );
315
-
316
- expect(result.toolProtections.checkout).toBeDefined();
317
- expect(result.toolProtections.checkout.requiresDelegation).toBe(true);
318
- expect(result.toolProtections.checkout.requiredScopes).toEqual(['cart:write']);
319
- });
320
-
321
- test('should handle old endpoint format with tools array', async () => {
322
- const apiResponse = {
323
- success: true,
324
- data: {
325
- tools: [
326
- {
327
- name: 'tool1',
328
- requires_delegation: true,
329
- required_scopes: ['scope1'],
330
- },
331
- {
332
- name: 'tool2',
333
- requiresDelegation: false,
334
- scopes: [],
335
- },
336
- ],
337
- },
338
- metadata: {},
339
- };
340
-
341
- (global.fetch as any).mockResolvedValueOnce({
342
- ok: true,
343
- json: async () => apiResponse,
344
- });
345
-
346
- const result = await service.getToolProtectionConfig(mockAgentDid);
347
-
348
- expect(result.toolProtections.tool1.requiresDelegation).toBe(true);
349
- expect(result.toolProtections.tool1.requiredScopes).toEqual(['scope1']);
350
- expect(result.toolProtections.tool2.requiresDelegation).toBe(false);
351
- });
352
-
353
- test('should parse oauthProvider from old endpoint format (tools array)', async () => {
354
- const apiResponse = {
355
- success: true,
356
- data: {
357
- tools: [
358
- {
359
- name: 'read_repos',
360
- requiresDelegation: true,
361
- requiredScopes: ['repo:read'],
362
- oauthProvider: 'github',
363
- },
364
- {
365
- name: 'send_email',
366
- requiresDelegation: true,
367
- requiredScopes: ['gmail:send'],
368
- oauthProvider: 'google',
369
- },
370
- ],
371
- },
372
- metadata: {},
373
- };
374
-
375
- (global.fetch as any).mockResolvedValueOnce({
376
- ok: true,
377
- json: async () => apiResponse,
378
- });
379
-
380
- const result = await service.getToolProtectionConfig(mockAgentDid);
381
-
382
- expect(result.toolProtections.read_repos.oauthProvider).toBe('github');
383
- expect(result.toolProtections.send_email.oauthProvider).toBe('google');
384
- });
385
-
386
- test('should handle old endpoint format with tools object', async () => {
387
- const apiResponse = {
388
- success: true,
389
- data: {
390
- tools: {
391
- tool1: {
392
- requires_delegation: true,
393
- required_scopes: ['scope1'],
394
- },
395
- tool2: {
396
- requiresDelegation: false,
397
- scopes: [],
398
- },
399
- },
400
- },
401
- metadata: {},
402
- };
403
-
404
- (global.fetch as any).mockResolvedValueOnce({
405
- ok: true,
406
- json: async () => apiResponse,
407
- });
408
-
409
- const result = await service.getToolProtectionConfig(mockAgentDid);
410
-
411
- expect(result.toolProtections.tool1.requiresDelegation).toBe(true);
412
- expect(result.toolProtections.tool1.requiredScopes).toEqual(['scope1']);
413
- expect(result.toolProtections.tool2.requiresDelegation).toBe(false);
414
- });
415
-
416
- test('should parse oauthProvider from old endpoint format (tools object)', async () => {
417
- const apiResponse = {
418
- success: true,
419
- data: {
420
- tools: {
421
- read_repos: {
422
- requiresDelegation: true,
423
- requiredScopes: ['repo:read'],
424
- oauthProvider: 'github',
425
- },
426
- send_email: {
427
- requiresDelegation: true,
428
- requiredScopes: ['gmail:send'],
429
- oauthProvider: 'google',
430
- },
431
- },
432
- },
433
- metadata: {},
434
- };
435
-
436
- (global.fetch as any).mockResolvedValueOnce({
437
- ok: true,
438
- json: async () => apiResponse,
439
- });
440
-
441
- const result = await service.getToolProtectionConfig(mockAgentDid);
442
-
443
- expect(result.toolProtections.read_repos.oauthProvider).toBe('github');
444
- expect(result.toolProtections.send_email.oauthProvider).toBe('google');
445
- });
446
-
447
- test('should skip tools without name in array format', async () => {
448
- const apiResponse = {
449
- success: true,
450
- data: {
451
- tools: [
452
- {
453
- name: 'valid_tool',
454
- requiresDelegation: true,
455
- },
456
- {
457
- // Missing name - should be skipped
458
- requiresDelegation: false,
459
- },
460
- ],
461
- },
462
- metadata: {},
463
- };
464
-
465
- config.debug = true;
466
- service = new ToolProtectionService(config, cache);
467
-
468
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
469
-
470
- (global.fetch as any).mockResolvedValueOnce({
471
- ok: true,
472
- json: async () => apiResponse,
473
- });
474
-
475
- const result = await service.getToolProtectionConfig(mockAgentDid);
476
-
477
- expect(result.toolProtections.valid_tool).toBeDefined();
478
- expect(Object.keys(result.toolProtections)).toHaveLength(1);
479
- expect(consoleSpy).toHaveBeenCalled();
480
-
481
- consoleSpy.mockRestore();
482
- });
483
- });
484
-
485
- describe('API fetch error handling', () => {
486
- test('should throw error when API key is missing', async () => {
487
- config.apiKey = '';
488
- service = new ToolProtectionService(config, cache);
489
-
490
- await expect(service.getToolProtectionConfig(mockAgentDid)).rejects.toThrow(
491
- 'ToolProtectionService: API key is missing or empty'
492
- );
493
- });
494
-
495
- test('should throw error when API key is whitespace only', async () => {
496
- config.apiKey = ' ';
497
- service = new ToolProtectionService(config, cache);
498
-
499
- await expect(service.getToolProtectionConfig(mockAgentDid)).rejects.toThrow(
500
- 'ToolProtectionService: API key is missing or empty'
501
- );
502
- });
503
-
504
- test('should throw error when API returns non-OK status', async () => {
505
- (global.fetch as any).mockResolvedValueOnce({
506
- ok: false,
507
- status: 401,
508
- statusText: 'Unauthorized',
509
- text: async () => 'Invalid API key',
510
- });
511
-
512
- await expect(service.getToolProtectionConfig(mockAgentDid)).rejects.toThrow(
513
- 'Failed to fetch bouncer config: 401 Unauthorized - Invalid API key'
514
- );
515
- });
516
-
517
- test('should throw error when API returns success: false', async () => {
518
- (global.fetch as any).mockResolvedValueOnce({
519
- ok: true,
520
- json: async () => ({
521
- success: false,
522
- error: 'Invalid request',
523
- }),
524
- });
525
-
526
- await expect(service.getToolProtectionConfig(mockAgentDid)).rejects.toThrow(
527
- 'API returned success: false'
528
- );
529
- });
530
-
531
- test('should use fallback config when API fails', async () => {
532
- const fallbackConfig: ToolProtectionConfig = {
533
- toolProtections: {
534
- fallback_tool: {
535
- requiresDelegation: true,
536
- requiredScopes: ['fallback'],
537
- },
538
- },
539
- };
540
-
541
- config.fallbackConfig = fallbackConfig;
542
-
543
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
544
-
545
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
546
-
547
- const result = await service.getToolProtectionConfig(mockAgentDid);
548
-
549
- expect(result).toEqual(fallbackConfig);
550
- expect(consoleSpy).toHaveBeenCalledWith(
551
- expect.stringContaining('API fetch failed, using fallback config'),
552
- expect.any(Object)
553
- );
554
-
555
- consoleSpy.mockRestore();
556
- });
557
-
558
- test('should return deny-all config when API fails and no fallback (fail-safe)', async () => {
559
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
560
-
561
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
562
-
563
- const result = await service.getToolProtectionConfig(mockAgentDid);
564
-
565
- expect(result.toolProtections['*']).toBeDefined();
566
- expect(result.toolProtections['*']?.requiresDelegation).toBe(true);
567
- expect(consoleSpy).toHaveBeenCalledWith(
568
- expect.stringContaining('API fetch failed, no fallback, failing closed (deny-all)'),
569
- expect.any(Object)
570
- );
571
-
572
- consoleSpy.mockRestore();
573
- });
574
-
575
- test('should return allow-all config when failSafeBehavior is allow-all', async () => {
576
- config.failSafeBehavior = 'allow-all';
577
- service = new ToolProtectionService(config, cache);
578
-
579
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
580
-
581
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
582
-
583
- const result = await service.getToolProtectionConfig(mockAgentDid);
584
-
585
- expect(result).toEqual({ toolProtections: {} });
586
- expect(consoleSpy).toHaveBeenCalledWith(
587
- expect.stringContaining('API fetch failed, no fallback, failing open (allow-all)'),
588
- expect.any(Object)
589
- );
590
-
591
- consoleSpy.mockRestore();
592
- });
593
-
594
- test('should use stale cache when API fails and stale cache is available', async () => {
595
- config.allowStaleCache = true;
596
- config.maxStaleCacheAge = 86400000; // 24 hours
597
- service = new ToolProtectionService(config, cache);
598
-
599
- // Create a stale cache entry (expired but within maxStaleCacheAge)
600
- const staleConfig: ToolProtectionConfig = {
601
- toolProtections: {
602
- stale_tool: {
603
- requiresDelegation: true,
604
- requiredScopes: ['stale'],
605
- },
606
- },
607
- };
608
-
609
- const cacheKey = `agent:${mockAgentDid}`;
610
- const expiresAt = Date.now() - 10000; // Expired 10 seconds ago
611
-
612
- // Mock cache.get() to NOT delete expired entries, so stale cache can be checked later
613
- const originalGet = cache.get.bind(cache);
614
- vi.spyOn(cache, 'get').mockImplementation(async (key: string) => {
615
- if (key === cacheKey) {
616
- // Return null (cache miss) but don't delete expired entry
617
- // The expired entry will be checked by getStaleCache() later
618
- return null;
619
- }
620
- return originalGet(key);
621
- });
622
-
623
- // Set stale entry in internal cache
624
- (cache as any).cache.set(cacheKey, {
625
- value: staleConfig,
626
- expiresAt,
627
- });
628
-
629
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
630
-
631
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
632
-
633
- const result = await service.getToolProtectionConfig(mockAgentDid);
634
-
635
- expect(result).toEqual(staleConfig);
636
- expect(consoleSpy).toHaveBeenCalledWith(
637
- expect.stringContaining('API fetch failed, using stale cache'),
638
- expect.any(Object)
639
- );
640
-
641
- consoleSpy.mockRestore();
642
- vi.restoreAllMocks();
643
- });
644
-
645
- test('should not use stale cache if maxStaleCacheAge exceeded', async () => {
646
- config.allowStaleCache = true;
647
- config.maxStaleCacheAge = 5000; // 5 seconds
648
- service = new ToolProtectionService(config, cache);
649
-
650
- // Create a stale cache entry (expired beyond maxStaleCacheAge)
651
- const staleConfig: ToolProtectionConfig = {
652
- toolProtections: {
653
- stale_tool: {
654
- requiresDelegation: true,
655
- requiredScopes: ['stale'],
656
- },
657
- },
658
- };
659
-
660
- const cacheKey = `agent:${mockAgentDid}`;
661
- const expiresAt = Date.now() - 10000; // Expired 10 seconds ago (exceeds 5s limit)
662
- (cache as any).cache.set(cacheKey, {
663
- value: staleConfig,
664
- expiresAt,
665
- });
666
-
667
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
668
-
669
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
670
-
671
- const result = await service.getToolProtectionConfig(mockAgentDid);
672
-
673
- // Should use deny-all instead of stale cache
674
- expect(result.toolProtections['*']).toBeDefined();
675
- expect(result.toolProtections['*']?.requiresDelegation).toBe(true);
676
- expect(consoleSpy).toHaveBeenCalledWith(
677
- expect.stringContaining('failing closed (deny-all)'),
678
- expect.any(Object)
679
- );
680
-
681
- consoleSpy.mockRestore();
682
- });
683
-
684
- test('should not use stale cache if allowStaleCache is false', async () => {
685
- config.allowStaleCache = false;
686
- service = new ToolProtectionService(config, cache);
687
-
688
- // Create a stale cache entry
689
- const staleConfig: ToolProtectionConfig = {
690
- toolProtections: {
691
- stale_tool: {
692
- requiresDelegation: true,
693
- requiredScopes: ['stale'],
694
- },
695
- },
696
- };
697
-
698
- const cacheKey = `agent:${mockAgentDid}`;
699
- const expiresAt = Date.now() - 10000; // Expired 10 seconds ago
700
- (cache as any).cache.set(cacheKey, {
701
- value: staleConfig,
702
- expiresAt,
703
- });
704
-
705
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
706
-
707
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
708
-
709
- const result = await service.getToolProtectionConfig(mockAgentDid);
710
-
711
- // Should use deny-all instead of stale cache
712
- expect(result.toolProtections['*']).toBeDefined();
713
- expect(result.toolProtections['*']?.requiresDelegation).toBe(true);
714
-
715
- consoleSpy.mockRestore();
716
- });
717
-
718
- test('should prioritize stale cache over fallback config', async () => {
719
- config.allowStaleCache = true;
720
- config.maxStaleCacheAge = 86400000;
721
- const fallbackConfig: ToolProtectionConfig = {
722
- toolProtections: {
723
- fallback_tool: {
724
- requiresDelegation: true,
725
- requiredScopes: ['fallback'],
726
- },
727
- },
728
- };
729
- config.fallbackConfig = fallbackConfig;
730
- service = new ToolProtectionService(config, cache);
731
-
732
- // Create a stale cache entry AFTER cache.get() is called
733
- // We need to do this by manually setting an expired entry in the internal cache
734
- // after the cache miss but before API fetch fails
735
- const staleConfig: ToolProtectionConfig = {
736
- toolProtections: {
737
- stale_tool: {
738
- requiresDelegation: true,
739
- requiredScopes: ['stale'],
740
- },
741
- },
742
- };
743
-
744
- const cacheKey = `agent:${mockAgentDid}`;
745
- const expiresAt = Date.now() - 10000; // Expired 10 seconds ago
746
-
747
- // Mock cache.get() to NOT delete expired entries, so stale cache can be checked later
748
- const originalGet = cache.get.bind(cache);
749
- vi.spyOn(cache, 'get').mockImplementation(async (key: string) => {
750
- if (key === cacheKey) {
751
- // Return null (cache miss) but don't delete expired entry
752
- // The expired entry will be checked by getStaleCache() later
753
- return null;
754
- }
755
- return originalGet(key);
756
- });
757
-
758
- // Set stale entry in internal cache
759
- (cache as any).cache.set(cacheKey, {
760
- value: staleConfig,
761
- expiresAt,
762
- });
763
-
764
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
765
-
766
- const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
767
-
768
- const result = await service.getToolProtectionConfig(mockAgentDid);
769
-
770
- // Should use stale cache (higher priority than fallback)
771
- expect(result).toEqual(staleConfig);
772
- expect(consoleSpy).toHaveBeenCalledWith(
773
- expect.stringContaining('API fetch failed, using stale cache'),
774
- expect.any(Object)
775
- );
776
-
777
- consoleSpy.mockRestore();
778
- vi.restoreAllMocks();
779
- });
780
-
781
- test('should handle network errors gracefully', async () => {
782
- (global.fetch as any).mockRejectedValueOnce(new Error('ECONNREFUSED'));
783
-
784
- const result = await service.getToolProtectionConfig(mockAgentDid);
785
-
786
- // Should fail-closed (deny-all) by default
787
- expect(result.toolProtections['*']).toBeDefined();
788
- expect(result.toolProtections['*']?.requiresDelegation).toBe(true);
789
- });
790
-
791
- test('should handle JSON parsing errors', async () => {
792
- (global.fetch as any).mockResolvedValueOnce({
793
- ok: true,
794
- json: async () => {
795
- throw new Error('Invalid JSON');
796
- },
797
- });
798
-
799
- await expect(service.getToolProtectionConfig(mockAgentDid)).rejects.toThrow();
800
- });
801
- });
802
-
803
- describe('caching behavior', () => {
804
- test('should cache successful API responses', async () => {
805
- const apiResponse = {
806
- success: true,
807
- data: {
808
- toolProtections: {
809
- tool1: {
810
- requiresDelegation: true,
811
- requiredScopes: ['scope1'],
812
- },
813
- },
814
- },
815
- metadata: {},
816
- };
817
-
818
- (global.fetch as any).mockResolvedValueOnce({
819
- ok: true,
820
- json: async () => apiResponse,
821
- });
822
-
823
- await service.getToolProtectionConfig(mockAgentDid);
824
-
825
- // Second call should use cache
826
- const result = await service.getToolProtectionConfig(mockAgentDid);
827
-
828
- expect(global.fetch).toHaveBeenCalledTimes(1);
829
- expect(result.toolProtections.tool1).toBeDefined();
830
- });
831
-
832
- test('should use default cache TTL when not specified', async () => {
833
- delete config.cacheTtl;
834
- service = new ToolProtectionService(config, cache);
835
-
836
- const apiResponse = {
837
- success: true,
838
- data: {
839
- toolProtections: {},
840
- },
841
- metadata: {},
842
- };
843
-
844
- (global.fetch as any).mockResolvedValueOnce({
845
- ok: true,
846
- json: async () => apiResponse,
847
- });
848
-
849
- const setSpy = vi.spyOn(cache, 'set');
850
-
851
- await service.getToolProtectionConfig(mockAgentDid);
852
-
853
- expect(setSpy).toHaveBeenCalledWith(
854
- expect.any(String),
855
- expect.any(Object),
856
- 300000 // Default 5 minutes
857
- );
858
- });
859
-
860
- test('should use custom cache TTL when specified', async () => {
861
- config.cacheTtl = 600000; // 10 minutes
862
- service = new ToolProtectionService(config, cache);
863
-
864
- const apiResponse = {
865
- success: true,
866
- data: {
867
- toolProtections: {},
868
- },
869
- metadata: {},
870
- };
871
-
872
- (global.fetch as any).mockResolvedValueOnce({
873
- ok: true,
874
- json: async () => apiResponse,
875
- });
876
-
877
- const setSpy = vi.spyOn(cache, 'set');
878
-
879
- await service.getToolProtectionConfig(mockAgentDid);
880
-
881
- expect(setSpy).toHaveBeenCalledWith(
882
- expect.any(String),
883
- expect.any(Object),
884
- 600000
885
- );
886
- });
887
- });
888
-
889
- describe('edge cases', () => {
890
- test('should handle empty toolProtections object', async () => {
891
- const apiResponse = {
892
- success: true,
893
- data: {
894
- toolProtections: {},
895
- },
896
- metadata: {},
897
- };
898
-
899
- (global.fetch as any).mockResolvedValueOnce({
900
- ok: true,
901
- json: async () => apiResponse,
902
- });
903
-
904
- const result = await service.getToolProtectionConfig(mockAgentDid);
905
-
906
- expect(result.toolProtections).toEqual({});
907
- });
908
-
909
- test('should handle null requiredScopes', async () => {
910
- const apiResponse = {
911
- success: true,
912
- data: {
913
- toolProtections: {
914
- tool1: {
915
- requiresDelegation: true,
916
- // Missing requiredScopes
917
- },
918
- },
919
- },
920
- metadata: {},
921
- };
922
-
923
- (global.fetch as any).mockResolvedValueOnce({
924
- ok: true,
925
- json: async () => apiResponse,
926
- });
927
-
928
- const result = await service.getToolProtectionConfig(mockAgentDid);
929
-
930
- expect(result.toolProtections.tool1.requiredScopes).toEqual([]);
931
- });
932
-
933
- test('should handle mixed camelCase and snake_case in response', async () => {
934
- const apiResponse = {
935
- success: true,
936
- data: {
937
- toolProtections: {
938
- tool1: {
939
- requiresDelegation: true,
940
- required_scopes: ['scope1'], // snake_case
941
- },
942
- tool2: {
943
- requires_delegation: false,
944
- requiredScopes: ['scope2'], // camelCase
945
- },
946
- },
947
- },
948
- metadata: {},
949
- };
950
-
951
- (global.fetch as any).mockResolvedValueOnce({
952
- ok: true,
953
- json: async () => apiResponse,
954
- });
955
-
956
- const result = await service.getToolProtectionConfig(mockAgentDid);
957
-
958
- expect(result.toolProtections.tool1.requiresDelegation).toBe(true);
959
- expect(result.toolProtections.tool1.requiredScopes).toEqual(['scope1']);
960
- expect(result.toolProtections.tool2.requiresDelegation).toBe(false);
961
- expect(result.toolProtections.tool2.requiredScopes).toEqual(['scope2']);
962
- });
963
- });
964
- });
965
-
966
- describe('checkToolProtection', () => {
967
- test('should return null when tool has no protection', async () => {
968
- const apiResponse = {
969
- success: true,
970
- data: {
971
- toolProtections: {
972
- unprotected_tool: {
973
- requiresDelegation: false,
974
- requiredScopes: [],
975
- },
976
- },
977
- },
978
- metadata: {},
979
- };
980
-
981
- (global.fetch as any).mockResolvedValueOnce({
982
- ok: true,
983
- json: async () => apiResponse,
984
- });
985
-
986
- const protection = await service.checkToolProtection('unprotected_tool', mockAgentDid);
987
-
988
- expect(protection).toBeNull();
989
- });
990
-
991
- test('should return null when tool is not in config', async () => {
992
- const apiResponse = {
993
- success: true,
994
- data: {
995
- toolProtections: {
996
- other_tool: {
997
- requiresDelegation: true,
998
- requiredScopes: ['scope1'],
999
- },
1000
- },
1001
- },
1002
- metadata: {},
1003
- };
1004
-
1005
- (global.fetch as any).mockResolvedValueOnce({
1006
- ok: true,
1007
- json: async () => apiResponse,
1008
- });
1009
-
1010
- const protection = await service.checkToolProtection('unknown_tool', mockAgentDid);
1011
-
1012
- expect(protection).toBeNull();
1013
- });
1014
-
1015
- test('should return wildcard protection when tool not found and wildcard exists', async () => {
1016
- const apiResponse = {
1017
- success: true,
1018
- data: {
1019
- toolProtections: {
1020
- '*': {
1021
- requiresDelegation: true,
1022
- requiredScopes: ['wildcard:scope'],
1023
- },
1024
- specific_tool: {
1025
- requiresDelegation: false,
1026
- requiredScopes: [],
1027
- },
1028
- },
1029
- },
1030
- metadata: {},
1031
- };
1032
-
1033
- (global.fetch as any).mockResolvedValueOnce({
1034
- ok: true,
1035
- json: async () => apiResponse,
1036
- });
1037
-
1038
- const protection = await service.checkToolProtection('unknown_tool', mockAgentDid);
1039
-
1040
- expect(protection).toBeDefined();
1041
- expect(protection?.requiresDelegation).toBe(true);
1042
- expect(protection?.requiredScopes).toEqual(['wildcard:scope']);
1043
- });
1044
-
1045
- test('should prioritize specific tool protection over wildcard', async () => {
1046
- const apiResponse = {
1047
- success: true,
1048
- data: {
1049
- toolProtections: {
1050
- '*': {
1051
- requiresDelegation: true,
1052
- requiredScopes: ['wildcard:scope'],
1053
- },
1054
- specific_tool: {
1055
- requiresDelegation: false,
1056
- requiredScopes: [],
1057
- },
1058
- },
1059
- },
1060
- metadata: {},
1061
- };
1062
-
1063
- (global.fetch as any).mockResolvedValueOnce({
1064
- ok: true,
1065
- json: async () => apiResponse,
1066
- });
1067
-
1068
- const protection = await service.checkToolProtection('specific_tool', mockAgentDid);
1069
-
1070
- // Should use specific tool (no delegation required), not wildcard
1071
- expect(protection).toBeNull();
1072
- });
1073
-
1074
- test('should use wildcard protection in fail-safe deny-all mode', async () => {
1075
- // Simulate fail-safe mode: API fails, no fallback, returns deny-all config
1076
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
1077
-
1078
- const protection = await service.checkToolProtection('any_tool', mockAgentDid);
1079
-
1080
- // Should return wildcard protection from deny-all config
1081
- expect(protection).toBeDefined();
1082
- expect(protection?.requiresDelegation).toBe(true);
1083
- expect(protection?.requiredScopes).toEqual([]);
1084
- });
1085
-
1086
- test('should return protection config when tool requires delegation', async () => {
1087
- const apiResponse = {
1088
- success: true,
1089
- data: {
1090
- toolProtections: {
1091
- protected_tool: {
1092
- requiresDelegation: true,
1093
- requiredScopes: ['scope1', 'scope2'],
1094
- },
1095
- },
1096
- },
1097
- metadata: {},
1098
- };
1099
-
1100
- (global.fetch as any).mockResolvedValueOnce({
1101
- ok: true,
1102
- json: async () => apiResponse,
1103
- });
1104
-
1105
- const protection = await service.checkToolProtection('protected_tool', mockAgentDid);
1106
-
1107
- expect(protection).toBeDefined();
1108
- expect(protection?.requiresDelegation).toBe(true);
1109
- expect(protection?.requiredScopes).toEqual(['scope1', 'scope2']);
1110
- });
1111
-
1112
- test('should log protection check when debug is enabled', async () => {
1113
- config.debug = true;
1114
- service = new ToolProtectionService(config, cache);
1115
-
1116
- const apiResponse = {
1117
- success: true,
1118
- data: {
1119
- toolProtections: {
1120
- tool1: {
1121
- requiresDelegation: true,
1122
- requiredScopes: ['scope1'],
1123
- },
1124
- },
1125
- },
1126
- metadata: {},
1127
- };
1128
-
1129
- (global.fetch as any).mockResolvedValueOnce({
1130
- ok: true,
1131
- json: async () => apiResponse,
1132
- });
1133
-
1134
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1135
-
1136
- await service.checkToolProtection('tool1', mockAgentDid);
1137
-
1138
- expect(consoleSpy).toHaveBeenCalledWith(
1139
- expect.stringContaining('Protection check'),
1140
- expect.any(Object)
1141
- );
1142
-
1143
- consoleSpy.mockRestore();
1144
- });
1145
-
1146
- test('should log protection check when delegation is required', async () => {
1147
- const apiResponse = {
1148
- success: true,
1149
- data: {
1150
- toolProtections: {
1151
- tool1: {
1152
- requiresDelegation: true,
1153
- requiredScopes: ['scope1'],
1154
- },
1155
- },
1156
- },
1157
- metadata: {},
1158
- };
1159
-
1160
- (global.fetch as any).mockResolvedValueOnce({
1161
- ok: true,
1162
- json: async () => apiResponse,
1163
- });
1164
-
1165
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1166
-
1167
- await service.checkToolProtection('tool1', mockAgentDid);
1168
-
1169
- expect(consoleSpy).toHaveBeenCalledWith(
1170
- expect.stringContaining('Protection check'),
1171
- expect.any(Object)
1172
- );
1173
-
1174
- consoleSpy.mockRestore();
1175
- });
1176
- });
1177
-
1178
- describe('clearCache', () => {
1179
- test('should clear project-scoped cache when projectId is set', async () => {
1180
- const projectId = 'test-project-123';
1181
- config.projectId = projectId;
1182
- service = new ToolProtectionService(config, cache);
1183
-
1184
- const deleteSpy = vi.spyOn(cache, 'delete');
1185
-
1186
- await service.clearCache(mockAgentDid);
1187
-
1188
- expect(deleteSpy).toHaveBeenCalledWith(`config:tool-protections:${projectId}`);
1189
- });
1190
-
1191
- test('should clear agent-scoped cache when projectId is not set', async () => {
1192
- const deleteSpy = vi.spyOn(cache, 'delete');
1193
-
1194
- await service.clearCache(mockAgentDid);
1195
-
1196
- expect(deleteSpy).toHaveBeenCalledWith(`agent:${mockAgentDid}`);
1197
- });
1198
-
1199
- test('should log cache clear when debug is enabled', async () => {
1200
- config.debug = true;
1201
- service = new ToolProtectionService(config, cache);
1202
-
1203
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1204
-
1205
- await service.clearCache(mockAgentDid);
1206
-
1207
- expect(consoleSpy).toHaveBeenCalledWith(
1208
- expect.stringContaining('Clearing cache'),
1209
- expect.any(Object)
1210
- );
1211
-
1212
- consoleSpy.mockRestore();
1213
- });
1214
- });
1215
-
1216
- describe('integration with NoOpToolProtectionCache', () => {
1217
- test('should work with NoOpToolProtectionCache', async () => {
1218
- const noOpCache = new NoOpToolProtectionCache();
1219
- service = new ToolProtectionService(config, noOpCache);
1220
-
1221
- const apiResponse = {
1222
- success: true,
1223
- data: {
1224
- toolProtections: {
1225
- tool1: {
1226
- requiresDelegation: true,
1227
- requiredScopes: ['scope1'],
1228
- },
1229
- },
1230
- },
1231
- metadata: {},
1232
- };
1233
-
1234
- (global.fetch as any).mockResolvedValue({
1235
- ok: true,
1236
- json: async () => apiResponse,
1237
- });
1238
-
1239
- // First call - should fetch from API
1240
- const result1 = await service.getToolProtectionConfig(mockAgentDid);
1241
-
1242
- // Second call - should fetch again (no cache)
1243
- const result2 = await service.getToolProtectionConfig(mockAgentDid);
1244
-
1245
- expect(result1).toEqual(result2);
1246
- expect(global.fetch).toHaveBeenCalledTimes(2);
1247
- });
1248
- });
1249
-
1250
- describe('debug logging', () => {
1251
- test('should log cache hits when debug is enabled', async () => {
1252
- config.debug = true;
1253
- service = new ToolProtectionService(config, cache);
1254
-
1255
- const cachedConfig: ToolProtectionConfig = {
1256
- toolProtections: {
1257
- tool1: {
1258
- requiresDelegation: false,
1259
- requiredScopes: [],
1260
- },
1261
- },
1262
- };
1263
-
1264
- await cache.set(`agent:${mockAgentDid}`, cachedConfig, 300000);
1265
-
1266
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1267
-
1268
- await service.getToolProtectionConfig(mockAgentDid);
1269
-
1270
- expect(consoleSpy).toHaveBeenCalledWith(
1271
- expect.stringContaining('Cache hit'),
1272
- expect.any(Object)
1273
- );
1274
-
1275
- consoleSpy.mockRestore();
1276
- });
1277
-
1278
- test('should log cache misses when debug is enabled', async () => {
1279
- config.debug = true;
1280
- service = new ToolProtectionService(config, cache);
1281
-
1282
- const apiResponse = {
1283
- success: true,
1284
- data: {
1285
- toolProtections: {},
1286
- },
1287
- metadata: {},
1288
- };
1289
-
1290
- (global.fetch as any).mockResolvedValueOnce({
1291
- ok: true,
1292
- json: async () => apiResponse,
1293
- });
1294
-
1295
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1296
-
1297
- await service.getToolProtectionConfig(mockAgentDid);
1298
-
1299
- expect(consoleSpy).toHaveBeenCalledWith(
1300
- expect.stringContaining('Cache miss'),
1301
- expect.any(Object)
1302
- );
1303
-
1304
- consoleSpy.mockRestore();
1305
- });
1306
-
1307
- test('should log API fetch details when debug is enabled', async () => {
1308
- config.debug = true;
1309
- config.projectId = 'test-project';
1310
- service = new ToolProtectionService(config, cache);
1311
-
1312
- const apiResponse = {
1313
- success: true,
1314
- data: {
1315
- toolProtections: {},
1316
- },
1317
- metadata: {},
1318
- };
1319
-
1320
- (global.fetch as any).mockResolvedValueOnce({
1321
- ok: true,
1322
- json: async () => apiResponse,
1323
- });
1324
-
1325
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1326
-
1327
- await service.getToolProtectionConfig(mockAgentDid);
1328
-
1329
- expect(consoleSpy).toHaveBeenCalledWith(
1330
- expect.stringContaining('Fetching from API'),
1331
- expect.any(String),
1332
- expect.any(Object)
1333
- );
1334
-
1335
- consoleSpy.mockRestore();
1336
- });
1337
-
1338
- test('should log config loaded when API fetch succeeds', async () => {
1339
- const apiResponse = {
1340
- success: true,
1341
- data: {
1342
- toolProtections: {
1343
- tool1: {
1344
- requiresDelegation: true,
1345
- requiredScopes: ['scope1'],
1346
- },
1347
- },
1348
- },
1349
- metadata: {},
1350
- };
1351
-
1352
- (global.fetch as any).mockResolvedValueOnce({
1353
- ok: true,
1354
- json: async () => apiResponse,
1355
- });
1356
-
1357
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1358
-
1359
- await service.getToolProtectionConfig(mockAgentDid);
1360
-
1361
- expect(consoleSpy).toHaveBeenCalledWith(
1362
- expect.stringContaining('Config loaded'),
1363
- expect.objectContaining({
1364
- toolCount: 1,
1365
- protectedTools: ['tool1'],
1366
- })
1367
- );
1368
-
1369
- consoleSpy.mockRestore();
1370
- });
1371
- });
1372
- });
1373
-