@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.
- package/dist/config/remote-config.js +9 -12
- package/dist/runtime/base.d.ts +2 -1
- package/dist/runtime/base.js +34 -6
- package/dist/services/access-control.service.js +5 -0
- package/dist/services/tool-protection.service.js +17 -8
- package/package.json +2 -2
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test$colon$coverage.log +0 -4586
- package/.turbo/turbo-test.log +0 -4631
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +0 -483
- package/Composer 3.md +0 -615
- package/GPT-5.md +0 -1169
- package/OPUS-plan.md +0 -352
- package/PHASE_3_AND_4.1_SUMMARY.md +0 -585
- package/PHASE_3_SUMMARY.md +0 -317
- package/PHASE_4.1.3_SUMMARY.md +0 -428
- package/PHASE_4.1_COMPLETE.md +0 -525
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +0 -1240
- package/SCHEMA_COMPLIANCE_REPORT.md +0 -275
- package/TEST_PLAN.md +0 -571
- package/coverage/coverage-final.json +0 -60
- package/dist/cache/oauth-config-cache.d.ts.map +0 -1
- package/dist/cache/oauth-config-cache.js.map +0 -1
- package/dist/cache/tool-protection-cache.d.ts.map +0 -1
- package/dist/cache/tool-protection-cache.js.map +0 -1
- package/dist/compliance/index.d.ts.map +0 -1
- package/dist/compliance/index.js.map +0 -1
- package/dist/compliance/schema-registry.d.ts.map +0 -1
- package/dist/compliance/schema-registry.js.map +0 -1
- package/dist/compliance/schema-verifier.d.ts.map +0 -1
- package/dist/compliance/schema-verifier.js.map +0 -1
- package/dist/config/remote-config.d.ts.map +0 -1
- package/dist/config/remote-config.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/delegation/audience-validator.d.ts.map +0 -1
- package/dist/delegation/audience-validator.js.map +0 -1
- package/dist/delegation/bitstring.d.ts.map +0 -1
- package/dist/delegation/bitstring.js.map +0 -1
- package/dist/delegation/cascading-revocation.d.ts.map +0 -1
- package/dist/delegation/cascading-revocation.js.map +0 -1
- package/dist/delegation/delegation-graph.d.ts.map +0 -1
- package/dist/delegation/delegation-graph.js.map +0 -1
- package/dist/delegation/did-key-resolver.d.ts.map +0 -1
- package/dist/delegation/did-key-resolver.js.map +0 -1
- package/dist/delegation/index.d.ts.map +0 -1
- package/dist/delegation/index.js.map +0 -1
- package/dist/delegation/statuslist-manager.d.ts.map +0 -1
- package/dist/delegation/statuslist-manager.js.map +0 -1
- package/dist/delegation/storage/index.d.ts.map +0 -1
- package/dist/delegation/storage/index.js.map +0 -1
- package/dist/delegation/storage/memory-graph-storage.d.ts.map +0 -1
- package/dist/delegation/storage/memory-graph-storage.js.map +0 -1
- package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +0 -1
- package/dist/delegation/storage/memory-statuslist-storage.js.map +0 -1
- package/dist/delegation/utils.d.ts.map +0 -1
- package/dist/delegation/utils.js.map +0 -1
- package/dist/delegation/vc-issuer.d.ts.map +0 -1
- package/dist/delegation/vc-issuer.js.map +0 -1
- package/dist/delegation/vc-verifier.d.ts.map +0 -1
- package/dist/delegation/vc-verifier.js.map +0 -1
- package/dist/identity/idp-token-resolver.d.ts.map +0 -1
- package/dist/identity/idp-token-resolver.js.map +0 -1
- package/dist/identity/idp-token-storage.interface.d.ts.map +0 -1
- package/dist/identity/idp-token-storage.interface.js.map +0 -1
- package/dist/identity/user-did-manager.d.ts.map +0 -1
- package/dist/identity/user-did-manager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers/base.d.ts.map +0 -1
- package/dist/providers/base.js.map +0 -1
- package/dist/providers/memory.d.ts.map +0 -1
- package/dist/providers/memory.js.map +0 -1
- package/dist/runtime/audit-logger.d.ts.map +0 -1
- package/dist/runtime/audit-logger.js.map +0 -1
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js.map +0 -1
- package/dist/services/access-control.service.d.ts.map +0 -1
- package/dist/services/access-control.service.js.map +0 -1
- package/dist/services/authorization/authorization-registry.d.ts.map +0 -1
- package/dist/services/authorization/authorization-registry.js.map +0 -1
- package/dist/services/authorization/types.d.ts.map +0 -1
- package/dist/services/authorization/types.js.map +0 -1
- package/dist/services/batch-delegation.service.d.ts.map +0 -1
- package/dist/services/batch-delegation.service.js.map +0 -1
- package/dist/services/crypto.service.d.ts.map +0 -1
- package/dist/services/crypto.service.js.map +0 -1
- package/dist/services/errors.d.ts.map +0 -1
- package/dist/services/errors.js.map +0 -1
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/index.js.map +0 -1
- package/dist/services/oauth-config.service.d.ts.map +0 -1
- package/dist/services/oauth-config.service.js.map +0 -1
- package/dist/services/oauth-provider-registry.d.ts.map +0 -1
- package/dist/services/oauth-provider-registry.js.map +0 -1
- package/dist/services/oauth-service.d.ts.map +0 -1
- package/dist/services/oauth-service.js.map +0 -1
- package/dist/services/oauth-token-retrieval.service.d.ts.map +0 -1
- package/dist/services/oauth-token-retrieval.service.js.map +0 -1
- package/dist/services/proof-verifier.d.ts.map +0 -1
- package/dist/services/proof-verifier.js.map +0 -1
- package/dist/services/provider-resolver.d.ts.map +0 -1
- package/dist/services/provider-resolver.js.map +0 -1
- package/dist/services/provider-validator.d.ts.map +0 -1
- package/dist/services/provider-validator.js.map +0 -1
- package/dist/services/session-registration.service.d.ts.map +0 -1
- package/dist/services/session-registration.service.js.map +0 -1
- package/dist/services/storage.service.d.ts.map +0 -1
- package/dist/services/storage.service.js.map +0 -1
- package/dist/services/tool-context-builder.d.ts.map +0 -1
- package/dist/services/tool-context-builder.js.map +0 -1
- package/dist/services/tool-protection.service.d.ts.map +0 -1
- package/dist/services/tool-protection.service.js.map +0 -1
- package/dist/types/oauth-required-error.d.ts.map +0 -1
- package/dist/types/oauth-required-error.js.map +0 -1
- package/dist/types/tool-protection.d.ts.map +0 -1
- package/dist/types/tool-protection.js.map +0 -1
- package/dist/utils/base58.d.ts.map +0 -1
- package/dist/utils/base58.js.map +0 -1
- package/dist/utils/base64.d.ts.map +0 -1
- package/dist/utils/base64.js.map +0 -1
- package/dist/utils/cors.d.ts.map +0 -1
- package/dist/utils/cors.js.map +0 -1
- package/dist/utils/did-helpers.d.ts.map +0 -1
- package/dist/utils/did-helpers.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/storage-keys.d.ts.map +0 -1
- package/dist/utils/storage-keys.js.map +0 -1
- package/docs/API_REFERENCE.md +0 -1362
- package/docs/COMPLIANCE_MATRIX.md +0 -691
- package/docs/STATUSLIST2021_GUIDE.md +0 -696
- package/docs/W3C_VC_DELEGATION_GUIDE.md +0 -710
- package/src/__tests__/cache/tool-protection-cache.test.ts +0 -640
- package/src/__tests__/config/provider-runtime-config.test.ts +0 -309
- package/src/__tests__/delegation-e2e.test.ts +0 -690
- package/src/__tests__/identity/user-did-manager.test.ts +0 -232
- package/src/__tests__/index.test.ts +0 -56
- package/src/__tests__/integration/full-flow.test.ts +0 -789
- package/src/__tests__/integration.test.ts +0 -281
- package/src/__tests__/providers/base.test.ts +0 -173
- package/src/__tests__/providers/memory.test.ts +0 -319
- package/src/__tests__/regression/phase2-regression.test.ts +0 -429
- package/src/__tests__/runtime/audit-logger.test.ts +0 -154
- package/src/__tests__/runtime/base-extensions.test.ts +0 -595
- package/src/__tests__/runtime/base.test.ts +0 -869
- package/src/__tests__/runtime/delegation-flow.test.ts +0 -164
- package/src/__tests__/runtime/proof-client-did.test.ts +0 -376
- package/src/__tests__/runtime/route-interception.test.ts +0 -686
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +0 -908
- package/src/__tests__/services/agentshield-integration.test.ts +0 -791
- package/src/__tests__/services/cache-busting.test.ts +0 -125
- package/src/__tests__/services/oauth-service-pkce.test.ts +0 -556
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +0 -591
- package/src/__tests__/services/tool-protection-merged-config.test.ts +0 -485
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +0 -480
- package/src/__tests__/services/tool-protection.service.test.ts +0 -1373
- package/src/__tests__/utils/mock-providers.ts +0 -340
- package/src/cache/oauth-config-cache.d.ts +0 -69
- package/src/cache/oauth-config-cache.d.ts.map +0 -1
- package/src/cache/oauth-config-cache.js.map +0 -1
- package/src/cache/oauth-config-cache.ts +0 -123
- package/src/cache/tool-protection-cache.ts +0 -171
- package/src/compliance/EXAMPLE.md +0 -412
- package/src/compliance/__tests__/schema-verifier.test.ts +0 -797
- package/src/compliance/index.ts +0 -8
- package/src/compliance/schema-registry.ts +0 -460
- package/src/compliance/schema-verifier.ts +0 -708
- package/src/config/__tests__/merged-config.spec.ts +0 -445
- package/src/config/__tests__/remote-config.spec.ts +0 -268
- package/src/config/remote-config.ts +0 -264
- package/src/config.ts +0 -312
- package/src/delegation/__tests__/audience-validator.test.ts +0 -112
- package/src/delegation/__tests__/bitstring.test.ts +0 -346
- package/src/delegation/__tests__/cascading-revocation.test.ts +0 -628
- package/src/delegation/__tests__/delegation-graph.test.ts +0 -584
- package/src/delegation/__tests__/did-key-resolver.test.ts +0 -265
- package/src/delegation/__tests__/utils.test.ts +0 -152
- package/src/delegation/__tests__/vc-issuer.test.ts +0 -442
- package/src/delegation/__tests__/vc-verifier.test.ts +0 -922
- package/src/delegation/audience-validator.ts +0 -52
- package/src/delegation/bitstring.ts +0 -278
- package/src/delegation/cascading-revocation.ts +0 -370
- package/src/delegation/delegation-graph.ts +0 -299
- package/src/delegation/did-key-resolver.ts +0 -179
- package/src/delegation/index.ts +0 -14
- package/src/delegation/statuslist-manager.ts +0 -353
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +0 -366
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +0 -228
- package/src/delegation/storage/index.ts +0 -9
- package/src/delegation/storage/memory-graph-storage.ts +0 -178
- package/src/delegation/storage/memory-statuslist-storage.ts +0 -77
- package/src/delegation/utils.ts +0 -221
- package/src/delegation/vc-issuer.ts +0 -232
- package/src/delegation/vc-verifier.ts +0 -568
- package/src/identity/idp-token-resolver.ts +0 -181
- package/src/identity/idp-token-storage.interface.ts +0 -94
- package/src/identity/user-did-manager.ts +0 -526
- package/src/index.ts +0 -310
- package/src/providers/base.d.ts +0 -91
- package/src/providers/base.d.ts.map +0 -1
- package/src/providers/base.js.map +0 -1
- package/src/providers/base.ts +0 -96
- package/src/providers/memory.ts +0 -142
- package/src/runtime/audit-logger.ts +0 -39
- package/src/runtime/base.ts +0 -1392
- package/src/services/__tests__/access-control.integration.test.ts +0 -443
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +0 -578
- package/src/services/__tests__/access-control.service.test.ts +0 -970
- package/src/services/__tests__/batch-delegation.service.test.ts +0 -351
- package/src/services/__tests__/crypto.service.test.ts +0 -531
- package/src/services/__tests__/oauth-provider-registry.test.ts +0 -142
- package/src/services/__tests__/proof-verifier.integration.test.ts +0 -485
- package/src/services/__tests__/proof-verifier.test.ts +0 -489
- package/src/services/__tests__/provider-resolution.integration.test.ts +0 -202
- package/src/services/__tests__/provider-resolver.test.ts +0 -213
- package/src/services/__tests__/storage.service.test.ts +0 -358
- package/src/services/access-control.service.ts +0 -990
- package/src/services/authorization/authorization-registry.ts +0 -66
- package/src/services/authorization/types.ts +0 -71
- package/src/services/batch-delegation.service.ts +0 -137
- package/src/services/crypto.service.ts +0 -302
- package/src/services/errors.ts +0 -76
- package/src/services/index.ts +0 -18
- package/src/services/oauth-config.service.d.ts +0 -53
- package/src/services/oauth-config.service.d.ts.map +0 -1
- package/src/services/oauth-config.service.js.map +0 -1
- package/src/services/oauth-config.service.ts +0 -192
- package/src/services/oauth-provider-registry.d.ts +0 -57
- package/src/services/oauth-provider-registry.d.ts.map +0 -1
- package/src/services/oauth-provider-registry.js.map +0 -1
- package/src/services/oauth-provider-registry.ts +0 -141
- package/src/services/oauth-service.ts +0 -544
- package/src/services/oauth-token-retrieval.service.ts +0 -245
- package/src/services/proof-verifier.ts +0 -478
- package/src/services/provider-resolver.d.ts +0 -48
- package/src/services/provider-resolver.d.ts.map +0 -1
- package/src/services/provider-resolver.js.map +0 -1
- package/src/services/provider-resolver.ts +0 -146
- package/src/services/provider-validator.ts +0 -170
- package/src/services/session-registration.service.ts +0 -251
- package/src/services/storage.service.ts +0 -566
- package/src/services/tool-context-builder.ts +0 -237
- package/src/services/tool-protection.service.ts +0 -1070
- package/src/types/oauth-required-error.ts +0 -63
- package/src/types/tool-protection.ts +0 -155
- package/src/utils/__tests__/did-helpers.test.ts +0 -156
- package/src/utils/base58.ts +0 -109
- package/src/utils/base64.ts +0 -148
- package/src/utils/cors.ts +0 -83
- package/src/utils/did-helpers.ts +0 -210
- package/src/utils/index.ts +0 -8
- package/src/utils/storage-keys.ts +0 -278
- package/tsconfig.json +0 -21
- 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
|
-
|