@kya-os/mcp-i-core 1.3.12 → 1.3.14

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 (254) hide show
  1. package/dist/config/remote-config.js +9 -12
  2. package/dist/runtime/base.js +11 -0
  3. package/dist/services/access-control.service.js +5 -0
  4. package/dist/services/tool-protection.service.js +17 -8
  5. package/package.json +2 -2
  6. package/.turbo/turbo-build.log +0 -4
  7. package/.turbo/turbo-test$colon$coverage.log +0 -4586
  8. package/.turbo/turbo-test.log +0 -3169
  9. package/COMPLIANCE_IMPROVEMENT_REPORT.md +0 -483
  10. package/Composer 3.md +0 -615
  11. package/GPT-5.md +0 -1169
  12. package/OPUS-plan.md +0 -352
  13. package/PHASE_3_AND_4.1_SUMMARY.md +0 -585
  14. package/PHASE_3_SUMMARY.md +0 -317
  15. package/PHASE_4.1.3_SUMMARY.md +0 -428
  16. package/PHASE_4.1_COMPLETE.md +0 -525
  17. package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +0 -1240
  18. package/SCHEMA_COMPLIANCE_REPORT.md +0 -275
  19. package/TEST_PLAN.md +0 -571
  20. package/coverage/coverage-final.json +0 -60
  21. package/dist/cache/oauth-config-cache.d.ts.map +0 -1
  22. package/dist/cache/oauth-config-cache.js.map +0 -1
  23. package/dist/cache/tool-protection-cache.d.ts.map +0 -1
  24. package/dist/cache/tool-protection-cache.js.map +0 -1
  25. package/dist/compliance/index.d.ts.map +0 -1
  26. package/dist/compliance/index.js.map +0 -1
  27. package/dist/compliance/schema-registry.d.ts.map +0 -1
  28. package/dist/compliance/schema-registry.js.map +0 -1
  29. package/dist/compliance/schema-verifier.d.ts.map +0 -1
  30. package/dist/compliance/schema-verifier.js.map +0 -1
  31. package/dist/config/remote-config.d.ts.map +0 -1
  32. package/dist/config/remote-config.js.map +0 -1
  33. package/dist/config.d.ts.map +0 -1
  34. package/dist/config.js.map +0 -1
  35. package/dist/delegation/audience-validator.d.ts.map +0 -1
  36. package/dist/delegation/audience-validator.js.map +0 -1
  37. package/dist/delegation/bitstring.d.ts.map +0 -1
  38. package/dist/delegation/bitstring.js.map +0 -1
  39. package/dist/delegation/cascading-revocation.d.ts.map +0 -1
  40. package/dist/delegation/cascading-revocation.js.map +0 -1
  41. package/dist/delegation/delegation-graph.d.ts.map +0 -1
  42. package/dist/delegation/delegation-graph.js.map +0 -1
  43. package/dist/delegation/did-key-resolver.d.ts.map +0 -1
  44. package/dist/delegation/did-key-resolver.js.map +0 -1
  45. package/dist/delegation/index.d.ts.map +0 -1
  46. package/dist/delegation/index.js.map +0 -1
  47. package/dist/delegation/statuslist-manager.d.ts.map +0 -1
  48. package/dist/delegation/statuslist-manager.js.map +0 -1
  49. package/dist/delegation/storage/index.d.ts.map +0 -1
  50. package/dist/delegation/storage/index.js.map +0 -1
  51. package/dist/delegation/storage/memory-graph-storage.d.ts.map +0 -1
  52. package/dist/delegation/storage/memory-graph-storage.js.map +0 -1
  53. package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +0 -1
  54. package/dist/delegation/storage/memory-statuslist-storage.js.map +0 -1
  55. package/dist/delegation/utils.d.ts.map +0 -1
  56. package/dist/delegation/utils.js.map +0 -1
  57. package/dist/delegation/vc-issuer.d.ts.map +0 -1
  58. package/dist/delegation/vc-issuer.js.map +0 -1
  59. package/dist/delegation/vc-verifier.d.ts.map +0 -1
  60. package/dist/delegation/vc-verifier.js.map +0 -1
  61. package/dist/identity/idp-token-resolver.d.ts.map +0 -1
  62. package/dist/identity/idp-token-resolver.js.map +0 -1
  63. package/dist/identity/idp-token-storage.interface.d.ts.map +0 -1
  64. package/dist/identity/idp-token-storage.interface.js.map +0 -1
  65. package/dist/identity/user-did-manager.d.ts.map +0 -1
  66. package/dist/identity/user-did-manager.js.map +0 -1
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/index.js.map +0 -1
  69. package/dist/providers/base.d.ts.map +0 -1
  70. package/dist/providers/base.js.map +0 -1
  71. package/dist/providers/memory.d.ts.map +0 -1
  72. package/dist/providers/memory.js.map +0 -1
  73. package/dist/runtime/audit-logger.d.ts.map +0 -1
  74. package/dist/runtime/audit-logger.js.map +0 -1
  75. package/dist/runtime/base.d.ts.map +0 -1
  76. package/dist/runtime/base.js.map +0 -1
  77. package/dist/services/access-control.service.d.ts.map +0 -1
  78. package/dist/services/access-control.service.js.map +0 -1
  79. package/dist/services/authorization/authorization-registry.d.ts.map +0 -1
  80. package/dist/services/authorization/authorization-registry.js.map +0 -1
  81. package/dist/services/authorization/types.d.ts.map +0 -1
  82. package/dist/services/authorization/types.js.map +0 -1
  83. package/dist/services/batch-delegation.service.d.ts.map +0 -1
  84. package/dist/services/batch-delegation.service.js.map +0 -1
  85. package/dist/services/crypto.service.d.ts.map +0 -1
  86. package/dist/services/crypto.service.js.map +0 -1
  87. package/dist/services/errors.d.ts.map +0 -1
  88. package/dist/services/errors.js.map +0 -1
  89. package/dist/services/index.d.ts.map +0 -1
  90. package/dist/services/index.js.map +0 -1
  91. package/dist/services/oauth-config.service.d.ts.map +0 -1
  92. package/dist/services/oauth-config.service.js.map +0 -1
  93. package/dist/services/oauth-provider-registry.d.ts.map +0 -1
  94. package/dist/services/oauth-provider-registry.js.map +0 -1
  95. package/dist/services/oauth-service.d.ts.map +0 -1
  96. package/dist/services/oauth-service.js.map +0 -1
  97. package/dist/services/oauth-token-retrieval.service.d.ts.map +0 -1
  98. package/dist/services/oauth-token-retrieval.service.js.map +0 -1
  99. package/dist/services/proof-verifier.d.ts.map +0 -1
  100. package/dist/services/proof-verifier.js.map +0 -1
  101. package/dist/services/provider-resolver.d.ts.map +0 -1
  102. package/dist/services/provider-resolver.js.map +0 -1
  103. package/dist/services/provider-validator.d.ts.map +0 -1
  104. package/dist/services/provider-validator.js.map +0 -1
  105. package/dist/services/session-registration.service.d.ts.map +0 -1
  106. package/dist/services/session-registration.service.js.map +0 -1
  107. package/dist/services/storage.service.d.ts.map +0 -1
  108. package/dist/services/storage.service.js.map +0 -1
  109. package/dist/services/tool-context-builder.d.ts.map +0 -1
  110. package/dist/services/tool-context-builder.js.map +0 -1
  111. package/dist/services/tool-protection.service.d.ts.map +0 -1
  112. package/dist/services/tool-protection.service.js.map +0 -1
  113. package/dist/types/oauth-required-error.d.ts.map +0 -1
  114. package/dist/types/oauth-required-error.js.map +0 -1
  115. package/dist/types/tool-protection.d.ts.map +0 -1
  116. package/dist/types/tool-protection.js.map +0 -1
  117. package/dist/utils/base58.d.ts.map +0 -1
  118. package/dist/utils/base58.js.map +0 -1
  119. package/dist/utils/base64.d.ts.map +0 -1
  120. package/dist/utils/base64.js.map +0 -1
  121. package/dist/utils/cors.d.ts.map +0 -1
  122. package/dist/utils/cors.js.map +0 -1
  123. package/dist/utils/did-helpers.d.ts.map +0 -1
  124. package/dist/utils/did-helpers.js.map +0 -1
  125. package/dist/utils/index.d.ts.map +0 -1
  126. package/dist/utils/index.js.map +0 -1
  127. package/dist/utils/storage-keys.d.ts.map +0 -1
  128. package/dist/utils/storage-keys.js.map +0 -1
  129. package/docs/API_REFERENCE.md +0 -1362
  130. package/docs/COMPLIANCE_MATRIX.md +0 -691
  131. package/docs/STATUSLIST2021_GUIDE.md +0 -696
  132. package/docs/W3C_VC_DELEGATION_GUIDE.md +0 -710
  133. package/src/__tests__/cache/tool-protection-cache.test.ts +0 -640
  134. package/src/__tests__/config/provider-runtime-config.test.ts +0 -309
  135. package/src/__tests__/delegation-e2e.test.ts +0 -690
  136. package/src/__tests__/identity/user-did-manager.test.ts +0 -232
  137. package/src/__tests__/index.test.ts +0 -56
  138. package/src/__tests__/integration/full-flow.test.ts +0 -789
  139. package/src/__tests__/integration.test.ts +0 -281
  140. package/src/__tests__/providers/base.test.ts +0 -173
  141. package/src/__tests__/providers/memory.test.ts +0 -319
  142. package/src/__tests__/regression/phase2-regression.test.ts +0 -429
  143. package/src/__tests__/runtime/audit-logger.test.ts +0 -154
  144. package/src/__tests__/runtime/base-extensions.test.ts +0 -595
  145. package/src/__tests__/runtime/base.test.ts +0 -869
  146. package/src/__tests__/runtime/delegation-flow.test.ts +0 -164
  147. package/src/__tests__/runtime/proof-client-did.test.ts +0 -376
  148. package/src/__tests__/runtime/route-interception.test.ts +0 -686
  149. package/src/__tests__/runtime/tool-protection-enforcement.test.ts +0 -908
  150. package/src/__tests__/services/agentshield-integration.test.ts +0 -791
  151. package/src/__tests__/services/cache-busting.test.ts +0 -125
  152. package/src/__tests__/services/oauth-service-pkce.test.ts +0 -556
  153. package/src/__tests__/services/provider-resolver-edge-cases.test.ts +0 -591
  154. package/src/__tests__/services/tool-protection-merged-config.test.ts +0 -485
  155. package/src/__tests__/services/tool-protection-oauth-provider.test.ts +0 -480
  156. package/src/__tests__/services/tool-protection.service.test.ts +0 -1373
  157. package/src/__tests__/utils/mock-providers.ts +0 -340
  158. package/src/cache/oauth-config-cache.d.ts +0 -69
  159. package/src/cache/oauth-config-cache.d.ts.map +0 -1
  160. package/src/cache/oauth-config-cache.js.map +0 -1
  161. package/src/cache/oauth-config-cache.ts +0 -123
  162. package/src/cache/tool-protection-cache.ts +0 -171
  163. package/src/compliance/EXAMPLE.md +0 -412
  164. package/src/compliance/__tests__/schema-verifier.test.ts +0 -797
  165. package/src/compliance/index.ts +0 -8
  166. package/src/compliance/schema-registry.ts +0 -460
  167. package/src/compliance/schema-verifier.ts +0 -708
  168. package/src/config/__tests__/merged-config.spec.ts +0 -445
  169. package/src/config/__tests__/remote-config.spec.ts +0 -268
  170. package/src/config/remote-config.ts +0 -264
  171. package/src/config.ts +0 -312
  172. package/src/delegation/__tests__/audience-validator.test.ts +0 -112
  173. package/src/delegation/__tests__/bitstring.test.ts +0 -346
  174. package/src/delegation/__tests__/cascading-revocation.test.ts +0 -628
  175. package/src/delegation/__tests__/delegation-graph.test.ts +0 -584
  176. package/src/delegation/__tests__/did-key-resolver.test.ts +0 -265
  177. package/src/delegation/__tests__/utils.test.ts +0 -152
  178. package/src/delegation/__tests__/vc-issuer.test.ts +0 -442
  179. package/src/delegation/__tests__/vc-verifier.test.ts +0 -922
  180. package/src/delegation/audience-validator.ts +0 -52
  181. package/src/delegation/bitstring.ts +0 -278
  182. package/src/delegation/cascading-revocation.ts +0 -370
  183. package/src/delegation/delegation-graph.ts +0 -299
  184. package/src/delegation/did-key-resolver.ts +0 -179
  185. package/src/delegation/index.ts +0 -14
  186. package/src/delegation/statuslist-manager.ts +0 -353
  187. package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +0 -366
  188. package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +0 -228
  189. package/src/delegation/storage/index.ts +0 -9
  190. package/src/delegation/storage/memory-graph-storage.ts +0 -178
  191. package/src/delegation/storage/memory-statuslist-storage.ts +0 -77
  192. package/src/delegation/utils.ts +0 -221
  193. package/src/delegation/vc-issuer.ts +0 -232
  194. package/src/delegation/vc-verifier.ts +0 -568
  195. package/src/identity/idp-token-resolver.ts +0 -181
  196. package/src/identity/idp-token-storage.interface.ts +0 -94
  197. package/src/identity/user-did-manager.ts +0 -526
  198. package/src/index.ts +0 -310
  199. package/src/providers/base.d.ts +0 -91
  200. package/src/providers/base.d.ts.map +0 -1
  201. package/src/providers/base.js.map +0 -1
  202. package/src/providers/base.ts +0 -96
  203. package/src/providers/memory.ts +0 -142
  204. package/src/runtime/audit-logger.ts +0 -39
  205. package/src/runtime/base.ts +0 -1392
  206. package/src/services/__tests__/access-control.integration.test.ts +0 -443
  207. package/src/services/__tests__/access-control.proof-response-validation.test.ts +0 -578
  208. package/src/services/__tests__/access-control.service.test.ts +0 -970
  209. package/src/services/__tests__/batch-delegation.service.test.ts +0 -351
  210. package/src/services/__tests__/crypto.service.test.ts +0 -531
  211. package/src/services/__tests__/oauth-provider-registry.test.ts +0 -142
  212. package/src/services/__tests__/proof-verifier.integration.test.ts +0 -485
  213. package/src/services/__tests__/proof-verifier.test.ts +0 -489
  214. package/src/services/__tests__/provider-resolution.integration.test.ts +0 -202
  215. package/src/services/__tests__/provider-resolver.test.ts +0 -213
  216. package/src/services/__tests__/storage.service.test.ts +0 -358
  217. package/src/services/access-control.service.ts +0 -990
  218. package/src/services/authorization/authorization-registry.ts +0 -66
  219. package/src/services/authorization/types.ts +0 -71
  220. package/src/services/batch-delegation.service.ts +0 -137
  221. package/src/services/crypto.service.ts +0 -302
  222. package/src/services/errors.ts +0 -76
  223. package/src/services/index.ts +0 -18
  224. package/src/services/oauth-config.service.d.ts +0 -53
  225. package/src/services/oauth-config.service.d.ts.map +0 -1
  226. package/src/services/oauth-config.service.js.map +0 -1
  227. package/src/services/oauth-config.service.ts +0 -192
  228. package/src/services/oauth-provider-registry.d.ts +0 -57
  229. package/src/services/oauth-provider-registry.d.ts.map +0 -1
  230. package/src/services/oauth-provider-registry.js.map +0 -1
  231. package/src/services/oauth-provider-registry.ts +0 -141
  232. package/src/services/oauth-service.ts +0 -544
  233. package/src/services/oauth-token-retrieval.service.ts +0 -245
  234. package/src/services/proof-verifier.ts +0 -478
  235. package/src/services/provider-resolver.d.ts +0 -48
  236. package/src/services/provider-resolver.d.ts.map +0 -1
  237. package/src/services/provider-resolver.js.map +0 -1
  238. package/src/services/provider-resolver.ts +0 -146
  239. package/src/services/provider-validator.ts +0 -170
  240. package/src/services/session-registration.service.ts +0 -251
  241. package/src/services/storage.service.ts +0 -566
  242. package/src/services/tool-context-builder.ts +0 -237
  243. package/src/services/tool-protection.service.ts +0 -1070
  244. package/src/types/oauth-required-error.ts +0 -63
  245. package/src/types/tool-protection.ts +0 -155
  246. package/src/utils/__tests__/did-helpers.test.ts +0 -156
  247. package/src/utils/base58.ts +0 -109
  248. package/src/utils/base64.ts +0 -148
  249. package/src/utils/cors.ts +0 -83
  250. package/src/utils/did-helpers.ts +0 -210
  251. package/src/utils/index.ts +0 -8
  252. package/src/utils/storage-keys.ts +0 -278
  253. package/tsconfig.json +0 -21
  254. 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
-