@kya-os/mcp-i-core 1.3.7-canary.clientinfo.20251126041014 → 1.3.8-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test$colon$coverage.log +2913 -2246
- package/.turbo/turbo-test.log +1207 -2842
- package/coverage/coverage-final.json +57 -56
- package/dist/__tests__/utils/mock-providers.d.ts +2 -1
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/config/remote-config.d.ts +51 -0
- package/dist/config/remote-config.d.ts.map +1 -1
- package/dist/config/remote-config.js +74 -0
- package/dist/config/remote-config.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -1
- package/dist/config.js.map +1 -1
- package/dist/services/session-registration.service.d.ts.map +1 -1
- package/dist/services/session-registration.service.js +10 -66
- package/dist/services/session-registration.service.js.map +1 -1
- package/dist/services/tool-protection.service.d.ts +4 -1
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +31 -16
- package/dist/services/tool-protection.service.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/integration/full-flow.test.ts +23 -10
- package/src/__tests__/services/agentshield-integration.test.ts +10 -3
- package/src/__tests__/services/tool-protection-merged-config.test.ts +485 -0
- package/src/__tests__/services/tool-protection.service.test.ts +18 -11
- package/src/config/__tests__/merged-config.spec.ts +445 -0
- package/src/config/remote-config.ts +90 -0
- package/src/config.ts +3 -0
- package/src/services/session-registration.service.ts +26 -92
- package/src/services/tool-protection.service.ts +76 -48
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolProtectionService Tests - Merged Config Format
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for ToolProtectionService consuming the new merged config API
|
|
5
|
+
* where tool protections are embedded at config.toolProtection.tools.
|
|
6
|
+
*
|
|
7
|
+
* These tests document the expected behavior BEFORE implementation.
|
|
8
|
+
* The service should fetch from /config endpoint and extract tool protections
|
|
9
|
+
* from the embedded tools field.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.6.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, test, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
15
|
+
import { ToolProtectionService } from '../../services/tool-protection.service';
|
|
16
|
+
import {
|
|
17
|
+
InMemoryToolProtectionCache,
|
|
18
|
+
type ToolProtectionCache,
|
|
19
|
+
} from '../../cache/tool-protection-cache';
|
|
20
|
+
import type {
|
|
21
|
+
ToolProtectionServiceConfig,
|
|
22
|
+
ToolProtectionConfig,
|
|
23
|
+
} from '../../types/tool-protection';
|
|
24
|
+
|
|
25
|
+
// Mock global fetch
|
|
26
|
+
global.fetch = vi.fn();
|
|
27
|
+
|
|
28
|
+
describe('ToolProtectionService - Merged Config API', () => {
|
|
29
|
+
let service: ToolProtectionService;
|
|
30
|
+
let cache: ToolProtectionCache;
|
|
31
|
+
let config: ToolProtectionServiceConfig;
|
|
32
|
+
const mockAgentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
cache = new InMemoryToolProtectionCache();
|
|
37
|
+
config = {
|
|
38
|
+
apiUrl: 'https://kya.vouched.id',
|
|
39
|
+
apiKey: 'test-api-key-12345',
|
|
40
|
+
projectId: 'test-project-123',
|
|
41
|
+
cacheTtl: 300000,
|
|
42
|
+
debug: false,
|
|
43
|
+
};
|
|
44
|
+
service = new ToolProtectionService(config, cache);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Fetching from /config endpoint with embedded tools', () => {
|
|
52
|
+
test('should fetch from /config endpoint and extract toolProtection.tools', async () => {
|
|
53
|
+
// New merged config API response format
|
|
54
|
+
const mergedConfigResponse = {
|
|
55
|
+
success: true,
|
|
56
|
+
data: {
|
|
57
|
+
config: {
|
|
58
|
+
identity: {
|
|
59
|
+
serverDid: 'did:key:test',
|
|
60
|
+
environment: 'production',
|
|
61
|
+
storageLocation: 'cloudflare-kv'
|
|
62
|
+
},
|
|
63
|
+
proofing: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
destinations: [],
|
|
66
|
+
batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 }
|
|
67
|
+
},
|
|
68
|
+
delegation: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
enforceStrictly: true,
|
|
71
|
+
verifier: { type: 'agentshield' },
|
|
72
|
+
authorization: {}
|
|
73
|
+
},
|
|
74
|
+
toolProtection: {
|
|
75
|
+
source: 'agentshield',
|
|
76
|
+
tools: {
|
|
77
|
+
checkout: {
|
|
78
|
+
requiresDelegation: true,
|
|
79
|
+
requiredScopes: ['cart:write', 'payment:execute'],
|
|
80
|
+
riskLevel: 'high'
|
|
81
|
+
},
|
|
82
|
+
greet: {
|
|
83
|
+
requiresDelegation: false,
|
|
84
|
+
requiredScopes: []
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
audit: { enabled: true, includeProofHashes: true, includePayloads: false },
|
|
89
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
90
|
+
platform: { type: 'cloudflare' },
|
|
91
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
metadata: {
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
cachedUntil: new Date(Date.now() + 60000).toISOString()
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
101
|
+
ok: true,
|
|
102
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
103
|
+
json: async () => mergedConfigResponse,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
107
|
+
|
|
108
|
+
// Should extract tool protections from embedded field
|
|
109
|
+
expect(result.toolProtections).toEqual({
|
|
110
|
+
checkout: {
|
|
111
|
+
requiresDelegation: true,
|
|
112
|
+
requiredScopes: ['cart:write', 'payment:execute'],
|
|
113
|
+
riskLevel: 'high'
|
|
114
|
+
},
|
|
115
|
+
greet: {
|
|
116
|
+
requiresDelegation: false,
|
|
117
|
+
requiredScopes: []
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Should have called the /config endpoint (not /tool-protections)
|
|
122
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('/config'),
|
|
124
|
+
expect.any(Object)
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should use projectId-based /config endpoint when projectId is set', async () => {
|
|
129
|
+
const projectId = 'my-project-id';
|
|
130
|
+
config.projectId = projectId;
|
|
131
|
+
service = new ToolProtectionService(config, cache);
|
|
132
|
+
|
|
133
|
+
const mergedConfigResponse = {
|
|
134
|
+
success: true,
|
|
135
|
+
data: {
|
|
136
|
+
config: {
|
|
137
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
138
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
139
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
140
|
+
toolProtection: {
|
|
141
|
+
source: 'agentshield',
|
|
142
|
+
tools: {
|
|
143
|
+
greet: { requiresDelegation: true, requiredScopes: ['greeting:write'] }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
147
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
148
|
+
platform: { type: 'node' },
|
|
149
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
155
|
+
ok: true,
|
|
156
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
157
|
+
json: async () => mergedConfigResponse,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await service.getToolProtectionConfig(mockAgentDid);
|
|
161
|
+
|
|
162
|
+
// Should call /api/v1/bouncer/projects/{projectId}/config
|
|
163
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
164
|
+
`https://kya.vouched.id/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/config`,
|
|
165
|
+
expect.any(Object)
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('should handle empty tools object from merged config', async () => {
|
|
170
|
+
const mergedConfigResponse = {
|
|
171
|
+
success: true,
|
|
172
|
+
data: {
|
|
173
|
+
config: {
|
|
174
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
175
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
176
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
177
|
+
toolProtection: {
|
|
178
|
+
source: 'agentshield',
|
|
179
|
+
tools: {} // No tools discovered yet
|
|
180
|
+
},
|
|
181
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
182
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
183
|
+
platform: { type: 'node' },
|
|
184
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
190
|
+
ok: true,
|
|
191
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
192
|
+
json: async () => mergedConfigResponse,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
196
|
+
|
|
197
|
+
expect(result.toolProtections).toEqual({});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('checkToolProtection with merged config', () => {
|
|
202
|
+
test('should correctly identify protected tool from merged config', async () => {
|
|
203
|
+
const mergedConfigResponse = {
|
|
204
|
+
success: true,
|
|
205
|
+
data: {
|
|
206
|
+
config: {
|
|
207
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
208
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
209
|
+
delegation: { enabled: true, enforceStrictly: true, verifier: { type: 'agentshield' }, authorization: {} },
|
|
210
|
+
toolProtection: {
|
|
211
|
+
source: 'agentshield',
|
|
212
|
+
tools: {
|
|
213
|
+
checkout: {
|
|
214
|
+
requiresDelegation: true,
|
|
215
|
+
requiredScopes: ['checkout:execute'],
|
|
216
|
+
riskLevel: 'high'
|
|
217
|
+
},
|
|
218
|
+
greet: {
|
|
219
|
+
requiresDelegation: false,
|
|
220
|
+
requiredScopes: []
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
225
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
226
|
+
platform: { type: 'cloudflare' },
|
|
227
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
235
|
+
json: async () => mergedConfigResponse,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Protected tool should return protection details
|
|
239
|
+
const checkoutProtection = await service.checkToolProtection('checkout', mockAgentDid);
|
|
240
|
+
expect(checkoutProtection).toEqual({
|
|
241
|
+
requiresDelegation: true,
|
|
242
|
+
requiredScopes: ['checkout:execute'],
|
|
243
|
+
riskLevel: 'high'
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Unprotected tool should return null
|
|
247
|
+
const greetProtection = await service.checkToolProtection('greet', mockAgentDid);
|
|
248
|
+
expect(greetProtection).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should return null for unknown tool', async () => {
|
|
252
|
+
const mergedConfigResponse = {
|
|
253
|
+
success: true,
|
|
254
|
+
data: {
|
|
255
|
+
config: {
|
|
256
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
257
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
258
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
259
|
+
toolProtection: {
|
|
260
|
+
source: 'agentshield',
|
|
261
|
+
tools: {
|
|
262
|
+
greet: { requiresDelegation: false, requiredScopes: [] }
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
266
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
267
|
+
platform: { type: 'node' },
|
|
268
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
274
|
+
ok: true,
|
|
275
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
276
|
+
json: async () => mergedConfigResponse,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const unknownProtection = await service.checkToolProtection('unknown-tool', mockAgentDid);
|
|
280
|
+
expect(unknownProtection).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('Fallback behavior with merged config', () => {
|
|
285
|
+
test('should use fallback config when network error occurs', async () => {
|
|
286
|
+
const fallbackConfig: ToolProtectionConfig = {
|
|
287
|
+
toolProtections: {
|
|
288
|
+
greet: {
|
|
289
|
+
requiresDelegation: true,
|
|
290
|
+
requiredScopes: ['fallback:scope']
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
config.fallbackConfig = fallbackConfig;
|
|
296
|
+
service = new ToolProtectionService(config, cache);
|
|
297
|
+
|
|
298
|
+
// Network error (not HTTP error) triggers fallback
|
|
299
|
+
(global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
|
|
300
|
+
|
|
301
|
+
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
302
|
+
|
|
303
|
+
expect(result.toolProtections.greet).toEqual({
|
|
304
|
+
requiresDelegation: true,
|
|
305
|
+
requiredScopes: ['fallback:scope']
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('should throw error when API returns HTTP error (500)', async () => {
|
|
310
|
+
// HTTP errors (4xx, 5xx) are intentionally propagated, not fallback
|
|
311
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
312
|
+
ok: false,
|
|
313
|
+
status: 500,
|
|
314
|
+
statusText: 'Internal Server Error',
|
|
315
|
+
text: async () => 'Server error',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await expect(service.getToolProtectionConfig(mockAgentDid))
|
|
319
|
+
.rejects.toThrow('Failed to fetch bouncer config: 500');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('should handle API returning config without tools field', async () => {
|
|
323
|
+
// Simulate old API format without tools field
|
|
324
|
+
const oldFormatResponse = {
|
|
325
|
+
success: true,
|
|
326
|
+
data: {
|
|
327
|
+
config: {
|
|
328
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
329
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
330
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
331
|
+
toolProtection: {
|
|
332
|
+
source: 'agentshield'
|
|
333
|
+
// No tools field - old format
|
|
334
|
+
},
|
|
335
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
336
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
337
|
+
platform: { type: 'node' },
|
|
338
|
+
metadata: { version: '1.5.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const fallbackConfig: ToolProtectionConfig = {
|
|
344
|
+
toolProtections: {
|
|
345
|
+
greet: {
|
|
346
|
+
requiresDelegation: true,
|
|
347
|
+
requiredScopes: ['fallback:scope']
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
config.fallbackConfig = fallbackConfig;
|
|
353
|
+
service = new ToolProtectionService(config, cache);
|
|
354
|
+
|
|
355
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
356
|
+
ok: true,
|
|
357
|
+
text: async () => JSON.stringify(oldFormatResponse),
|
|
358
|
+
json: async () => oldFormatResponse,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
362
|
+
|
|
363
|
+
// Should fallback to empty or use fallback config
|
|
364
|
+
// After implementation: should return empty tools or use fallback
|
|
365
|
+
expect(result.toolProtections).toBeDefined();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('Caching with merged config', () => {
|
|
370
|
+
test('should cache tool protections extracted from merged config', async () => {
|
|
371
|
+
const mergedConfigResponse = {
|
|
372
|
+
success: true,
|
|
373
|
+
data: {
|
|
374
|
+
config: {
|
|
375
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
376
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
377
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
378
|
+
toolProtection: {
|
|
379
|
+
source: 'agentshield',
|
|
380
|
+
tools: {
|
|
381
|
+
greet: { requiresDelegation: true, requiredScopes: ['greeting:write'] }
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
385
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
386
|
+
platform: { type: 'node' },
|
|
387
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
393
|
+
ok: true,
|
|
394
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
395
|
+
json: async () => mergedConfigResponse,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// First call - should fetch from API
|
|
399
|
+
await service.getToolProtectionConfig(mockAgentDid);
|
|
400
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
401
|
+
|
|
402
|
+
// Second call - should use cache
|
|
403
|
+
await service.getToolProtectionConfig(mockAgentDid);
|
|
404
|
+
expect(global.fetch).toHaveBeenCalledTimes(1); // No additional fetch
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('should respect cache TTL for merged config', async () => {
|
|
408
|
+
const mergedConfigResponse = {
|
|
409
|
+
success: true,
|
|
410
|
+
data: {
|
|
411
|
+
config: {
|
|
412
|
+
identity: { serverDid: 'did:key:test', environment: 'production', storageLocation: 'cloudflare-kv' },
|
|
413
|
+
proofing: { enabled: false, destinations: [], batchQueue: { maxBatchSize: 10, flushIntervalMs: 5000, maxRetries: 3 } },
|
|
414
|
+
delegation: { enabled: false, enforceStrictly: false, verifier: { type: 'memory' }, authorization: {} },
|
|
415
|
+
toolProtection: {
|
|
416
|
+
source: 'agentshield',
|
|
417
|
+
tools: {
|
|
418
|
+
greet: { requiresDelegation: true, requiredScopes: ['greeting:write'] }
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
audit: { enabled: false, includeProofHashes: false, includePayloads: false },
|
|
422
|
+
session: { timestampSkewSeconds: 120, ttlMinutes: 30 },
|
|
423
|
+
platform: { type: 'node' },
|
|
424
|
+
metadata: { version: '1.6.0', lastUpdated: new Date().toISOString(), source: 'dashboard' }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Use short TTL
|
|
430
|
+
config.cacheTtl = 100; // 100ms
|
|
431
|
+
service = new ToolProtectionService(config, cache);
|
|
432
|
+
|
|
433
|
+
(global.fetch as any).mockResolvedValue({
|
|
434
|
+
ok: true,
|
|
435
|
+
text: async () => JSON.stringify(mergedConfigResponse),
|
|
436
|
+
json: async () => mergedConfigResponse,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// First call
|
|
440
|
+
await service.getToolProtectionConfig(mockAgentDid);
|
|
441
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
442
|
+
|
|
443
|
+
// Wait for cache to expire
|
|
444
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
445
|
+
|
|
446
|
+
// Second call - should fetch again (cache expired)
|
|
447
|
+
await service.getToolProtectionConfig(mockAgentDid);
|
|
448
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('Backward compatibility - /tool-protections response format', () => {
|
|
453
|
+
test('should still handle legacy /tool-protections response format if encountered', async () => {
|
|
454
|
+
// This tests backward compatibility during transition period
|
|
455
|
+
// The service might encounter the old response format
|
|
456
|
+
const legacyResponse = {
|
|
457
|
+
success: true,
|
|
458
|
+
data: {
|
|
459
|
+
toolProtections: {
|
|
460
|
+
greet: { requiresDelegation: true, requiredScopes: ['greeting:write'] }
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
metadata: {
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
cachedUntil: new Date(Date.now() + 60000).toISOString()
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
470
|
+
ok: true,
|
|
471
|
+
text: async () => JSON.stringify(legacyResponse),
|
|
472
|
+
json: async () => legacyResponse,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
476
|
+
|
|
477
|
+
// Should still work with legacy format
|
|
478
|
+
expect(result.toolProtections.greet).toEqual({
|
|
479
|
+
requiresDelegation: true,
|
|
480
|
+
requiredScopes: ['greeting:write']
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
@@ -113,23 +113,29 @@ describe('ToolProtectionService', () => {
|
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
describe('API fetch -
|
|
117
|
-
test('should fetch from project-scoped endpoint when projectId is available', async () => {
|
|
116
|
+
describe('API fetch - merged config format', () => {
|
|
117
|
+
test('should fetch from project-scoped /config endpoint when projectId is available', async () => {
|
|
118
118
|
const projectId = 'test-project-123';
|
|
119
119
|
config.projectId = projectId;
|
|
120
120
|
service = new ToolProtectionService(config, cache);
|
|
121
121
|
|
|
122
|
+
// Merged config format - tools embedded at config.toolProtection.tools
|
|
122
123
|
const apiResponse = {
|
|
123
124
|
success: true,
|
|
124
125
|
data: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
},
|
|
133
139
|
},
|
|
134
140
|
},
|
|
135
141
|
},
|
|
@@ -146,8 +152,9 @@ describe('ToolProtectionService', () => {
|
|
|
146
152
|
|
|
147
153
|
const result = await service.getToolProtectionConfig(mockAgentDid);
|
|
148
154
|
|
|
155
|
+
// Now calls /config endpoint (not /tool-protections)
|
|
149
156
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
150
|
-
`https://kya.vouched.id/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/
|
|
157
|
+
`https://kya.vouched.id/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/config`,
|
|
151
158
|
expect.objectContaining({
|
|
152
159
|
method: 'GET',
|
|
153
160
|
headers: expect.objectContaining({
|