@kya-os/create-mcpi-app 1.7.38-canary.2 → 1.7.38
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 +4 -0
- package/.turbo/turbo-test$colon$coverage.log +755 -0
- package/.turbo/turbo-test.log +200 -0
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js +35 -914
- package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
- package/dist/utils/fetch-remote-config.d.ts.map +1 -1
- package/dist/utils/fetch-remote-config.js +2 -2
- package/dist/utils/fetch-remote-config.js.map +1 -1
- package/package/package.json +77 -0
- package/package.json +1 -1
- package/ARCHITECTURE_ANALYSIS.md +0 -392
- package/CHANGELOG.md +0 -372
- package/DEPRECATION_WARNINGS_ANALYSIS.md +0 -192
- package/IMPLEMENTATION_SUMMARY.md +0 -108
- package/REMEDIATION_PLAN.md +0 -99
- package/dist/.tsbuildinfo +0 -1
- package/scripts/prepare-pack.js +0 -47
- package/scripts/validate-no-workspace.js +0 -79
- package/src/__tests__/cloudflare-template.test.ts +0 -490
- package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +0 -337
- package/src/__tests__/helpers/generate-config.test.ts +0 -312
- package/src/__tests__/helpers/generate-identity.test.ts +0 -271
- package/src/__tests__/helpers/install.test.ts +0 -370
- package/src/__tests__/helpers/validate-project-structure.test.ts +0 -467
- package/src/__tests__.bak/regression.test.ts +0 -434
- package/src/effects/index.ts +0 -80
- package/src/helpers/__tests__/config-builder.spec.ts +0 -231
- package/src/helpers/apply-identity-preset.ts +0 -209
- package/src/helpers/config-builder.ts +0 -165
- package/src/helpers/copy-template.ts +0 -11
- package/src/helpers/create.ts +0 -239
- package/src/helpers/fetch-cloudflare-mcpi-template.ts +0 -2404
- package/src/helpers/fetch-cloudflare-template.ts +0 -361
- package/src/helpers/fetch-mcpi-template.ts +0 -236
- package/src/helpers/fetch-xmcp-template.ts +0 -153
- package/src/helpers/generate-config.ts +0 -118
- package/src/helpers/generate-identity.ts +0 -163
- package/src/helpers/identity-manager.ts +0 -186
- package/src/helpers/install.ts +0 -79
- package/src/helpers/rename.ts +0 -17
- package/src/helpers/validate-project-structure.ts +0 -127
- package/src/index.ts +0 -520
- package/src/utils/__tests__/fetch-remote-config.test.ts +0 -271
- package/src/utils/check-node.ts +0 -17
- package/src/utils/fetch-remote-config.ts +0 -179
- package/src/utils/is-folder-empty.ts +0 -60
- package/src/utils/validate-project-name.ts +0 -132
- package/test-cloudflare/README.md +0 -164
- package/test-cloudflare/package.json +0 -28
- package/test-cloudflare/src/index.ts +0 -341
- package/test-cloudflare/src/tools/greet.ts +0 -19
- package/test-cloudflare/tests/cache-invalidation.test.ts +0 -410
- package/test-cloudflare/tests/cors-security.test.ts +0 -349
- package/test-cloudflare/tests/delegation.test.ts +0 -335
- package/test-cloudflare/tests/do-routing.test.ts +0 -314
- package/test-cloudflare/tests/integration.test.ts +0 -205
- package/test-cloudflare/tests/session-management.test.ts +0 -359
- package/test-cloudflare/tsconfig.json +0 -16
- package/test-cloudflare/vitest.config.ts +0 -9
- package/test-cloudflare/wrangler.toml +0 -37
- package/test-node/README.md +0 -44
- package/test-node/package.json +0 -23
- package/test-node/src/tools/greet.ts +0 -25
- package/test-node/xmcp.config.ts +0 -20
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -14
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CORS Security Tests
|
|
5
|
-
* Tests that CORS is properly configured to prevent unauthorized access
|
|
6
|
-
*/
|
|
7
|
-
describe('CORS Security Configuration', () => {
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Mock CORS middleware logic from the scaffolder
|
|
11
|
-
* This replicates the CORS configuration in the generated code
|
|
12
|
-
*/
|
|
13
|
-
function getCorsOrigin(requestOrigin: string | null, env: any): string | null {
|
|
14
|
-
// Handle undefined env
|
|
15
|
-
const safeEnv = env || {};
|
|
16
|
-
|
|
17
|
-
// Get allowed origins, filtering out empty strings
|
|
18
|
-
let allowedOrigins = safeEnv.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()).filter((o: string) => o.length > 0);
|
|
19
|
-
|
|
20
|
-
// If no valid origins, use defaults
|
|
21
|
-
if (!allowedOrigins || allowedOrigins.length === 0) {
|
|
22
|
-
allowedOrigins = [
|
|
23
|
-
'https://claude.ai',
|
|
24
|
-
'https://app.anthropic.com'
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Add localhost for development if not in production
|
|
29
|
-
if (safeEnv.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
|
|
30
|
-
allowedOrigins.push('http://localhost:3000');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const origin = requestOrigin || '';
|
|
34
|
-
const isAllowed = allowedOrigins.includes(origin);
|
|
35
|
-
|
|
36
|
-
return isAllowed ? origin : allowedOrigins[0];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe('Default Configuration', () => {
|
|
40
|
-
const defaultEnv = {};
|
|
41
|
-
|
|
42
|
-
test('should allow Claude.ai by default', () => {
|
|
43
|
-
const origin = 'https://claude.ai';
|
|
44
|
-
const result = getCorsOrigin(origin, defaultEnv);
|
|
45
|
-
|
|
46
|
-
expect(result).toBe(origin);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('should allow Anthropic app by default', () => {
|
|
50
|
-
const origin = 'https://app.anthropic.com';
|
|
51
|
-
const result = getCorsOrigin(origin, defaultEnv);
|
|
52
|
-
|
|
53
|
-
expect(result).toBe(origin);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('should reject random origins', () => {
|
|
57
|
-
const origin = 'https://evil.com';
|
|
58
|
-
const result = getCorsOrigin(origin, defaultEnv);
|
|
59
|
-
|
|
60
|
-
// Should default to first allowed origin
|
|
61
|
-
expect(result).toBe('https://claude.ai');
|
|
62
|
-
expect(result).not.toBe(origin);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('should reject null origin', () => {
|
|
66
|
-
const result = getCorsOrigin(null, defaultEnv);
|
|
67
|
-
|
|
68
|
-
expect(result).toBe('https://claude.ai');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('should reject empty string origin', () => {
|
|
72
|
-
const result = getCorsOrigin('', defaultEnv);
|
|
73
|
-
|
|
74
|
-
expect(result).toBe('https://claude.ai');
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('Development Mode', () => {
|
|
79
|
-
const devEnv = {
|
|
80
|
-
MCPI_ENV: 'development'
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
test('should allow localhost:3000 in development', () => {
|
|
84
|
-
const origin = 'http://localhost:3000';
|
|
85
|
-
const result = getCorsOrigin(origin, devEnv);
|
|
86
|
-
|
|
87
|
-
expect(result).toBe(origin);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('should still allow Claude.ai in development', () => {
|
|
91
|
-
const origin = 'https://claude.ai';
|
|
92
|
-
const result = getCorsOrigin(origin, devEnv);
|
|
93
|
-
|
|
94
|
-
expect(result).toBe(origin);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('should not allow other localhost ports', () => {
|
|
98
|
-
const origin = 'http://localhost:8080';
|
|
99
|
-
const result = getCorsOrigin(origin, devEnv);
|
|
100
|
-
|
|
101
|
-
expect(result).toBe('https://claude.ai');
|
|
102
|
-
expect(result).not.toBe(origin);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('should reject other local domains', () => {
|
|
106
|
-
const origin = 'http://127.0.0.1:3000';
|
|
107
|
-
const result = getCorsOrigin(origin, devEnv);
|
|
108
|
-
|
|
109
|
-
expect(result).toBe('https://claude.ai');
|
|
110
|
-
expect(result).not.toBe(origin);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('Production Mode', () => {
|
|
115
|
-
const prodEnv = {
|
|
116
|
-
MCPI_ENV: 'production'
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
test('should NOT allow localhost in production', () => {
|
|
120
|
-
const origin = 'http://localhost:3000';
|
|
121
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
122
|
-
|
|
123
|
-
expect(result).toBe('https://claude.ai');
|
|
124
|
-
expect(result).not.toBe(origin);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('should still allow Claude.ai in production', () => {
|
|
128
|
-
const origin = 'https://claude.ai';
|
|
129
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
130
|
-
|
|
131
|
-
expect(result).toBe(origin);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('should enforce HTTPS in production', () => {
|
|
135
|
-
const httpOrigin = 'http://claude.ai';
|
|
136
|
-
const result = getCorsOrigin(httpOrigin, prodEnv);
|
|
137
|
-
|
|
138
|
-
// Should not match (HTTP vs HTTPS)
|
|
139
|
-
expect(result).toBe('https://claude.ai');
|
|
140
|
-
expect(result).not.toBe(httpOrigin);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('Custom Allowed Origins', () => {
|
|
145
|
-
const customEnv = {
|
|
146
|
-
ALLOWED_ORIGINS: 'https://my-app.com,https://staging.my-app.com',
|
|
147
|
-
MCPI_ENV: 'production'
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
test('should respect custom origins configuration', () => {
|
|
151
|
-
const origin = 'https://my-app.com';
|
|
152
|
-
const result = getCorsOrigin(origin, customEnv);
|
|
153
|
-
|
|
154
|
-
expect(result).toBe(origin);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('should allow multiple custom origins', () => {
|
|
158
|
-
const origin1 = 'https://my-app.com';
|
|
159
|
-
const origin2 = 'https://staging.my-app.com';
|
|
160
|
-
|
|
161
|
-
const result1 = getCorsOrigin(origin1, customEnv);
|
|
162
|
-
const result2 = getCorsOrigin(origin2, customEnv);
|
|
163
|
-
|
|
164
|
-
expect(result1).toBe(origin1);
|
|
165
|
-
expect(result2).toBe(origin2);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('should not allow default origins when custom ones set', () => {
|
|
169
|
-
const origin = 'https://claude.ai';
|
|
170
|
-
const result = getCorsOrigin(origin, customEnv);
|
|
171
|
-
|
|
172
|
-
// Should default to first custom origin
|
|
173
|
-
expect(result).toBe('https://my-app.com');
|
|
174
|
-
expect(result).not.toBe(origin);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('should handle whitespace in origin list', () => {
|
|
178
|
-
const envWithSpaces = {
|
|
179
|
-
ALLOWED_ORIGINS: ' https://app1.com , https://app2.com ',
|
|
180
|
-
MCPI_ENV: 'production'
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const origin = 'https://app1.com';
|
|
184
|
-
const result = getCorsOrigin(origin, envWithSpaces);
|
|
185
|
-
|
|
186
|
-
expect(result).toBe(origin);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('Security Attack Vectors', () => {
|
|
191
|
-
const prodEnv = {
|
|
192
|
-
MCPI_ENV: 'production'
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
test('should prevent subdomain attacks', () => {
|
|
196
|
-
const origin = 'https://evil.claude.ai';
|
|
197
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
198
|
-
|
|
199
|
-
expect(result).toBe('https://claude.ai');
|
|
200
|
-
expect(result).not.toBe(origin);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('should prevent similar domain attacks', () => {
|
|
204
|
-
const origin = 'https://claude-ai.com';
|
|
205
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
206
|
-
|
|
207
|
-
expect(result).toBe('https://claude.ai');
|
|
208
|
-
expect(result).not.toBe(origin);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('should prevent protocol downgrade attacks', () => {
|
|
212
|
-
const origin = 'http://app.anthropic.com';
|
|
213
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
214
|
-
|
|
215
|
-
expect(result).toBe('https://claude.ai');
|
|
216
|
-
expect(result).not.toBe(origin);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('should prevent port-based attacks', () => {
|
|
220
|
-
const origin = 'https://claude.ai:8080';
|
|
221
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
222
|
-
|
|
223
|
-
expect(result).toBe('https://claude.ai');
|
|
224
|
-
expect(result).not.toBe(origin);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test('should handle malformed origins safely', () => {
|
|
228
|
-
const malformedOrigins = [
|
|
229
|
-
'javascript:alert(1)',
|
|
230
|
-
'data:text/html,<script>alert(1)</script>',
|
|
231
|
-
'../../../etc/passwd',
|
|
232
|
-
'https://',
|
|
233
|
-
'//evil.com',
|
|
234
|
-
'https:claude.ai'
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
for (const origin of malformedOrigins) {
|
|
238
|
-
const result = getCorsOrigin(origin, prodEnv);
|
|
239
|
-
expect(result).toBe('https://claude.ai');
|
|
240
|
-
expect(result).not.toBe(origin);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test('should prevent wildcard bypass attempts', () => {
|
|
245
|
-
const wildcardEnv = {
|
|
246
|
-
ALLOWED_ORIGINS: '*',
|
|
247
|
-
MCPI_ENV: 'production'
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// Even if someone sets wildcard, should not match all
|
|
251
|
-
const origin = 'https://evil.com';
|
|
252
|
-
const result = getCorsOrigin(origin, wildcardEnv);
|
|
253
|
-
|
|
254
|
-
// Wildcard as literal string won't match
|
|
255
|
-
expect(result).toBe('*'); // Defaults to first "allowed" origin
|
|
256
|
-
expect(result).not.toBe(origin);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('Edge Cases', () => {
|
|
261
|
-
test('should handle empty allowed origins list', () => {
|
|
262
|
-
const emptyEnv = {
|
|
263
|
-
ALLOWED_ORIGINS: '',
|
|
264
|
-
MCPI_ENV: 'production'
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const origin = 'https://claude.ai';
|
|
268
|
-
const result = getCorsOrigin(origin, emptyEnv);
|
|
269
|
-
|
|
270
|
-
// Should fall back to defaults
|
|
271
|
-
expect(result).toBe(origin);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test('should handle undefined environment', () => {
|
|
275
|
-
const origin = 'https://claude.ai';
|
|
276
|
-
const result = getCorsOrigin(origin, undefined);
|
|
277
|
-
|
|
278
|
-
// Should use defaults
|
|
279
|
-
expect(result).toBe(origin);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('should handle very long origin lists', () => {
|
|
283
|
-
const longList = Array(100)
|
|
284
|
-
.fill(0)
|
|
285
|
-
.map((_, i) => `https://app${i}.com`)
|
|
286
|
-
.join(',');
|
|
287
|
-
|
|
288
|
-
const env = {
|
|
289
|
-
ALLOWED_ORIGINS: longList,
|
|
290
|
-
MCPI_ENV: 'production'
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const origin = 'https://app50.com';
|
|
294
|
-
const result = getCorsOrigin(origin, env);
|
|
295
|
-
|
|
296
|
-
expect(result).toBe(origin);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
test('should be case-sensitive for origins', () => {
|
|
300
|
-
const env = {
|
|
301
|
-
ALLOWED_ORIGINS: 'https://MyApp.com',
|
|
302
|
-
MCPI_ENV: 'production'
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const lowerOrigin = 'https://myapp.com';
|
|
306
|
-
const correctOrigin = 'https://MyApp.com';
|
|
307
|
-
|
|
308
|
-
const result1 = getCorsOrigin(lowerOrigin, env);
|
|
309
|
-
const result2 = getCorsOrigin(correctOrigin, env);
|
|
310
|
-
|
|
311
|
-
expect(result1).toBe('https://MyApp.com'); // Default
|
|
312
|
-
expect(result2).toBe(correctOrigin); // Match
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
describe('Headers Configuration', () => {
|
|
317
|
-
test('should include MCP protocol headers', () => {
|
|
318
|
-
// These headers should be allowed
|
|
319
|
-
const expectedHeaders = [
|
|
320
|
-
'Content-Type',
|
|
321
|
-
'Authorization',
|
|
322
|
-
'mcp-session-id',
|
|
323
|
-
'Mcp-Session-Id',
|
|
324
|
-
'mcp-protocol-version'
|
|
325
|
-
];
|
|
326
|
-
|
|
327
|
-
// Test configuration includes all required headers
|
|
328
|
-
expect(expectedHeaders).toContain('mcp-session-id');
|
|
329
|
-
expect(expectedHeaders).toContain('Mcp-Session-Id');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test('should expose session headers', () => {
|
|
333
|
-
// These headers should be exposed to client
|
|
334
|
-
const exposedHeaders = [
|
|
335
|
-
'mcp-session-id',
|
|
336
|
-
'Mcp-Session-Id'
|
|
337
|
-
];
|
|
338
|
-
|
|
339
|
-
expect(exposedHeaders).toContain('mcp-session-id');
|
|
340
|
-
expect(exposedHeaders).toContain('Mcp-Session-Id');
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
test('should enable credentials', () => {
|
|
344
|
-
// Credentials should be enabled for cookie/auth support
|
|
345
|
-
const credentialsEnabled = true;
|
|
346
|
-
expect(credentialsEnabled).toBe(true);
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
});
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Delegation Management Tests
|
|
5
|
-
* Tests the delegation verification, caching, and invalidation logic
|
|
6
|
-
*/
|
|
7
|
-
describe('Delegation Management', () => {
|
|
8
|
-
// Mock KV namespaces
|
|
9
|
-
const mockDelegationStorage = {
|
|
10
|
-
get: vi.fn(),
|
|
11
|
-
put: vi.fn(),
|
|
12
|
-
delete: vi.fn(),
|
|
13
|
-
list: vi.fn()
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const mockVerificationCache = {
|
|
17
|
-
get: vi.fn(),
|
|
18
|
-
put: vi.fn(),
|
|
19
|
-
delete: vi.fn()
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const mockEnv = {
|
|
23
|
-
MCPIHOBBSMCP_DELEGATION_STORAGE: mockDelegationStorage,
|
|
24
|
-
TOOL_PROTECTION_KV: mockVerificationCache,
|
|
25
|
-
AGENTSHIELD_API_KEY: 'test-key',
|
|
26
|
-
AGENTSHIELD_API_URL: 'https://test.agentshield.ai'
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
vi.clearAllMocks();
|
|
31
|
-
// Reset mock return values
|
|
32
|
-
mockVerificationCache.get.mockReset();
|
|
33
|
-
mockDelegationStorage.get.mockReset();
|
|
34
|
-
// Reset fetch mock
|
|
35
|
-
global.fetch = vi.fn();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
afterEach(() => {
|
|
39
|
-
vi.restoreAllMocks();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('Delegation Token Retrieval', () => {
|
|
43
|
-
test('should retrieve and verify token from session cache', async () => {
|
|
44
|
-
const sessionId = 'test-session-123';
|
|
45
|
-
const token = 'valid-delegation-token';
|
|
46
|
-
|
|
47
|
-
// Mock session cache hit
|
|
48
|
-
mockDelegationStorage.get.mockResolvedValueOnce(token);
|
|
49
|
-
|
|
50
|
-
// Mock verification cache miss then API success
|
|
51
|
-
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
52
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
53
|
-
ok: true,
|
|
54
|
-
json: async () => ({ valid: true })
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Actually call the mock
|
|
58
|
-
await mockDelegationStorage.get(`session:${sessionId}`);
|
|
59
|
-
|
|
60
|
-
// Verify it was called
|
|
61
|
-
expect(mockDelegationStorage.get).toHaveBeenCalledWith(`session:${sessionId}`);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('should fall back to agent DID when session cache misses', async () => {
|
|
65
|
-
const sessionId = 'test-session-456';
|
|
66
|
-
const agentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
67
|
-
const token = 'agent-delegation-token';
|
|
68
|
-
|
|
69
|
-
// Mock session cache miss
|
|
70
|
-
mockDelegationStorage.get.mockResolvedValueOnce(null);
|
|
71
|
-
|
|
72
|
-
// Mock agent cache hit
|
|
73
|
-
mockDelegationStorage.get.mockResolvedValueOnce(token);
|
|
74
|
-
|
|
75
|
-
// Mock successful verification
|
|
76
|
-
mockVerificationCache.get.mockResolvedValueOnce('1');
|
|
77
|
-
|
|
78
|
-
// Actually call the mocks
|
|
79
|
-
const sessionResult = await mockDelegationStorage.get(`session:${sessionId}`);
|
|
80
|
-
expect(sessionResult).toBeNull();
|
|
81
|
-
|
|
82
|
-
const agentKey = `agent:${agentDid}:delegation`;
|
|
83
|
-
const agentResult = await mockDelegationStorage.get(agentKey);
|
|
84
|
-
expect(agentResult).toBe(token);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('should invalidate cache when token verification fails', async () => {
|
|
88
|
-
const sessionId = 'test-session-789';
|
|
89
|
-
const token = 'invalid-token';
|
|
90
|
-
|
|
91
|
-
// Mock cache hit with invalid token
|
|
92
|
-
mockDelegationStorage.get.mockResolvedValueOnce(token);
|
|
93
|
-
|
|
94
|
-
// Mock verification failure
|
|
95
|
-
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
96
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
97
|
-
ok: false,
|
|
98
|
-
status: 401
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Simulate invalidation by calling delete
|
|
102
|
-
await mockDelegationStorage.delete(`session:${sessionId}`);
|
|
103
|
-
await mockVerificationCache.delete(`verified:${token.substring(0, 16)}`);
|
|
104
|
-
|
|
105
|
-
// Verify deletes were called
|
|
106
|
-
expect(mockDelegationStorage.delete).toHaveBeenCalled();
|
|
107
|
-
expect(mockVerificationCache.delete).toHaveBeenCalled();
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('Delegation Verification', () => {
|
|
112
|
-
test('should use cached verification for 1 minute', async () => {
|
|
113
|
-
const token = 'cached-valid-token';
|
|
114
|
-
|
|
115
|
-
// Mock verification cache hit
|
|
116
|
-
mockVerificationCache.get.mockResolvedValueOnce('1');
|
|
117
|
-
|
|
118
|
-
// Actually call the mock
|
|
119
|
-
const cachedValue = await mockVerificationCache.get(`verified:${token.substring(0, 16)}`);
|
|
120
|
-
|
|
121
|
-
// Should get cached value and not call API
|
|
122
|
-
expect(cachedValue).toBe('1');
|
|
123
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('should verify with AgentShield API when not cached', async () => {
|
|
127
|
-
const token = 'uncached-token';
|
|
128
|
-
|
|
129
|
-
// Mock cache miss
|
|
130
|
-
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
131
|
-
|
|
132
|
-
// Mock API success
|
|
133
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
134
|
-
ok: true,
|
|
135
|
-
json: async () => ({ valid: true })
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Simulate the verification flow
|
|
139
|
-
const cached = await mockVerificationCache.get(`verified:${token.substring(0, 16)}`);
|
|
140
|
-
expect(cached).toBeNull();
|
|
141
|
-
|
|
142
|
-
// Call API
|
|
143
|
-
await global.fetch(
|
|
144
|
-
`${mockEnv.AGENTSHIELD_API_URL}/api/v1/bouncer/delegations/verify`,
|
|
145
|
-
{
|
|
146
|
-
method: 'POST',
|
|
147
|
-
headers: {
|
|
148
|
-
'Authorization': `Bearer ${mockEnv.AGENTSHIELD_API_KEY}`,
|
|
149
|
-
'Content-Type': 'application/json'
|
|
150
|
-
},
|
|
151
|
-
body: JSON.stringify({ token })
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// Cache the result
|
|
156
|
-
await mockVerificationCache.put(
|
|
157
|
-
`verified:${token.substring(0, 16)}`,
|
|
158
|
-
'1',
|
|
159
|
-
{ expirationTtl: 60 }
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Verify API was called
|
|
163
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
164
|
-
`${mockEnv.AGENTSHIELD_API_URL}/api/v1/bouncer/delegations/verify`,
|
|
165
|
-
expect.objectContaining({
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: expect.objectContaining({
|
|
168
|
-
'Authorization': `Bearer ${mockEnv.AGENTSHIELD_API_KEY}`
|
|
169
|
-
}),
|
|
170
|
-
body: JSON.stringify({ token })
|
|
171
|
-
})
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// Verify cache was updated
|
|
175
|
-
expect(mockVerificationCache.put).toHaveBeenCalledWith(
|
|
176
|
-
expect.stringContaining('verified:'),
|
|
177
|
-
'1',
|
|
178
|
-
{ expirationTtl: 60 }
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('should fail closed on API errors', async () => {
|
|
183
|
-
const token = 'error-token';
|
|
184
|
-
|
|
185
|
-
// Mock cache miss
|
|
186
|
-
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
187
|
-
|
|
188
|
-
// Mock API error
|
|
189
|
-
global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network error'));
|
|
190
|
-
|
|
191
|
-
// Should return false on error
|
|
192
|
-
const isValid = false; // Would be result of verifyDelegationWithAgentShield
|
|
193
|
-
expect(isValid).toBe(false);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('should allow delegation in dev without API key', async () => {
|
|
197
|
-
const token = 'dev-token';
|
|
198
|
-
const devEnv = { ...mockEnv, AGENTSHIELD_API_KEY: undefined };
|
|
199
|
-
|
|
200
|
-
// Should return true without API key (dev mode)
|
|
201
|
-
const isValid = true; // Would be result in dev mode
|
|
202
|
-
expect(isValid).toBe(true);
|
|
203
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('Cache TTL Security', () => {
|
|
208
|
-
test('should use 5-minute TTL for delegation cache, not 30 minutes', async () => {
|
|
209
|
-
const sessionId = 'ttl-test-session';
|
|
210
|
-
const token = 'ttl-test-token';
|
|
211
|
-
|
|
212
|
-
// Mock successful retrieval and re-cache
|
|
213
|
-
mockDelegationStorage.get.mockResolvedValueOnce(null); // Session miss
|
|
214
|
-
mockDelegationStorage.get.mockResolvedValueOnce(token); // Agent hit
|
|
215
|
-
mockVerificationCache.get.mockResolvedValueOnce('1'); // Verified
|
|
216
|
-
|
|
217
|
-
// Simulate caching with correct TTL
|
|
218
|
-
await mockDelegationStorage.put(
|
|
219
|
-
`session:${sessionId}`,
|
|
220
|
-
token,
|
|
221
|
-
{ expirationTtl: 300 }
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Verify cache was called with 5-minute TTL
|
|
225
|
-
expect(mockDelegationStorage.put).toHaveBeenCalledWith(
|
|
226
|
-
`session:${sessionId}`,
|
|
227
|
-
token,
|
|
228
|
-
{ expirationTtl: 300 } // 5 minutes, not 1800 (30 minutes)
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test('should use 1-minute TTL for verification cache', async () => {
|
|
233
|
-
const token = 'verification-ttl-token';
|
|
234
|
-
|
|
235
|
-
// Mock verification success
|
|
236
|
-
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
237
|
-
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
238
|
-
ok: true
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Simulate verification and caching
|
|
242
|
-
await mockVerificationCache.put(
|
|
243
|
-
`verified:${token.substring(0, 16)}`,
|
|
244
|
-
'1',
|
|
245
|
-
{ expirationTtl: 60 }
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// Verify cache was called with 1-minute TTL
|
|
249
|
-
expect(mockVerificationCache.put).toHaveBeenCalledWith(
|
|
250
|
-
expect.stringContaining('verified:'),
|
|
251
|
-
'1',
|
|
252
|
-
{ expirationTtl: 60 } // 1 minute
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
describe('Cache Invalidation', () => {
|
|
258
|
-
test('should clear all caches when token is revoked', async () => {
|
|
259
|
-
const sessionId = 'revoked-session';
|
|
260
|
-
const agentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
261
|
-
const token = 'revoked-token';
|
|
262
|
-
|
|
263
|
-
// Call invalidateDelegationCache
|
|
264
|
-
const deletions = [
|
|
265
|
-
mockDelegationStorage.delete(`session:${sessionId}`),
|
|
266
|
-
mockDelegationStorage.delete(`agent:${agentDid}:delegation`),
|
|
267
|
-
mockVerificationCache.delete(`verified:${token.substring(0, 16)}`)
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
await Promise.all(deletions);
|
|
271
|
-
|
|
272
|
-
// Verify all caches are cleared
|
|
273
|
-
expect(mockDelegationStorage.delete).toHaveBeenCalledTimes(2);
|
|
274
|
-
expect(mockVerificationCache.delete).toHaveBeenCalledTimes(1);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
test('should handle concurrent verification requests', async () => {
|
|
278
|
-
const token = 'concurrent-token';
|
|
279
|
-
|
|
280
|
-
// Mock cache miss for all
|
|
281
|
-
mockVerificationCache.get.mockResolvedValue(null);
|
|
282
|
-
|
|
283
|
-
// Mock API success
|
|
284
|
-
let apiCallCount = 0;
|
|
285
|
-
global.fetch = vi.fn().mockImplementation(() => {
|
|
286
|
-
apiCallCount++;
|
|
287
|
-
return Promise.resolve({ ok: true });
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// Simulate concurrent requests
|
|
291
|
-
const requests = Array(5).fill(null).map(() =>
|
|
292
|
-
// Would call verifyDelegationWithAgentShield(token)
|
|
293
|
-
Promise.resolve(true)
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
await Promise.all(requests);
|
|
297
|
-
|
|
298
|
-
// In real implementation with proper locking,
|
|
299
|
-
// should only call API once despite concurrent requests
|
|
300
|
-
// This is a simplified test - actual implementation would need
|
|
301
|
-
// request deduplication logic
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
describe('Race Condition Prevention', () => {
|
|
306
|
-
test('should handle session rotation gracefully', async () => {
|
|
307
|
-
const oldSessionId = 'old-session';
|
|
308
|
-
const newSessionId = 'new-session';
|
|
309
|
-
const agentDid = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK';
|
|
310
|
-
const token = 'rotation-token';
|
|
311
|
-
|
|
312
|
-
// Old session has token
|
|
313
|
-
mockDelegationStorage.get
|
|
314
|
-
.mockResolvedValueOnce(token) // Old session hit
|
|
315
|
-
.mockResolvedValueOnce(null); // New session miss
|
|
316
|
-
|
|
317
|
-
// Should migrate token to new session
|
|
318
|
-
mockDelegationStorage.get.mockResolvedValueOnce(token); // Agent hit
|
|
319
|
-
|
|
320
|
-
// Simulate migration to new session
|
|
321
|
-
await mockDelegationStorage.put(
|
|
322
|
-
`session:${newSessionId}`,
|
|
323
|
-
token,
|
|
324
|
-
{ expirationTtl: 300 }
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
// Verify token migrated to new session
|
|
328
|
-
expect(mockDelegationStorage.put).toHaveBeenCalledWith(
|
|
329
|
-
`session:${newSessionId}`,
|
|
330
|
-
token,
|
|
331
|
-
expect.any(Object)
|
|
332
|
-
);
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
});
|