@ruifung/codemode-bridge 1.0.3-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +378 -0
  3. package/dist/cli/commands.d.ts +70 -0
  4. package/dist/cli/commands.js +436 -0
  5. package/dist/cli/config-manager.d.ts +53 -0
  6. package/dist/cli/config-manager.js +142 -0
  7. package/dist/cli/index.d.ts +19 -0
  8. package/dist/cli/index.js +165 -0
  9. package/dist/executor/container-executor.d.ts +81 -0
  10. package/dist/executor/container-executor.js +351 -0
  11. package/dist/executor/executor-test-suite.d.ts +22 -0
  12. package/dist/executor/executor-test-suite.js +395 -0
  13. package/dist/executor/isolated-vm-executor.d.ts +78 -0
  14. package/dist/executor/isolated-vm-executor.js +368 -0
  15. package/dist/executor/vm2-executor.d.ts +21 -0
  16. package/dist/executor/vm2-executor.js +109 -0
  17. package/dist/executor/wrap-code.d.ts +52 -0
  18. package/dist/executor/wrap-code.js +80 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/mcp/config.d.ts +44 -0
  22. package/dist/mcp/config.js +102 -0
  23. package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
  24. package/dist/mcp/e2e-bridge-test-suite.js +429 -0
  25. package/dist/mcp/executor.d.ts +31 -0
  26. package/dist/mcp/executor.js +121 -0
  27. package/dist/mcp/mcp-adapter.d.ts +12 -0
  28. package/dist/mcp/mcp-adapter.js +49 -0
  29. package/dist/mcp/mcp-client.d.ts +85 -0
  30. package/dist/mcp/mcp-client.js +441 -0
  31. package/dist/mcp/oauth-handler.d.ts +33 -0
  32. package/dist/mcp/oauth-handler.js +138 -0
  33. package/dist/mcp/server.d.ts +25 -0
  34. package/dist/mcp/server.js +322 -0
  35. package/dist/mcp/token-persistence.d.ts +57 -0
  36. package/dist/mcp/token-persistence.js +131 -0
  37. package/dist/utils/logger.d.ts +44 -0
  38. package/dist/utils/logger.js +123 -0
  39. package/package.json +56 -0
@@ -0,0 +1,395 @@
1
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
2
+ export function createExecutorTestSuite(name, createExecutor, options) {
3
+ const skipSet = new Set(options?.skipTests ?? []);
4
+ /** Use it.skip for tests in the skip list, it otherwise */
5
+ const testOrSkip = (testName, fn) => {
6
+ if (skipSet.has(testName)) {
7
+ it.skip(testName, fn);
8
+ }
9
+ else {
10
+ it(testName, fn, options?.testTimeout);
11
+ }
12
+ };
13
+ describe(`Executor: ${name}`, () => {
14
+ let executor;
15
+ beforeAll(async () => {
16
+ executor = createExecutor();
17
+ // If executor has a lazy init (e.g. container), trigger it now so the
18
+ // startup cost is not counted against the first test's timeout.
19
+ if ('init' in executor && typeof executor.init === 'function') {
20
+ await executor.init();
21
+ }
22
+ }, options?.testTimeout ? options.testTimeout * 2 : undefined);
23
+ afterAll(async () => {
24
+ // Cleanup if executor has dispose method
25
+ if ('dispose' in executor && typeof executor.dispose === 'function') {
26
+ await Promise.resolve(executor.dispose());
27
+ }
28
+ });
29
+ describe('Basic Execution', () => {
30
+ testOrSkip('should execute simple arithmetic', async () => {
31
+ const result = await executor.execute('return 1 + 2;', {});
32
+ expect(result.result).toBe(3);
33
+ expect(result.error).toBeUndefined();
34
+ });
35
+ testOrSkip('should execute async code', async () => {
36
+ const result = await executor.execute(`
37
+ const promise = new Promise(resolve =>
38
+ setTimeout(() => resolve(42), 10)
39
+ );
40
+ return await promise;
41
+ `, {});
42
+ expect(result.result).toBe(42);
43
+ });
44
+ testOrSkip('should return undefined for no return statement', async () => {
45
+ const result = await executor.execute('const x = 5;', {});
46
+ expect(result.result).toBeUndefined();
47
+ });
48
+ testOrSkip('should handle string returns', async () => {
49
+ const result = await executor.execute('return "hello";', {});
50
+ expect(result.result).toBe('hello');
51
+ });
52
+ testOrSkip('should handle object returns', async () => {
53
+ const result = await executor.execute('return { a: 1, b: "two", c: [1, 2, 3] };', {});
54
+ expect(result.result).toEqual({
55
+ a: 1,
56
+ b: 'two',
57
+ c: [1, 2, 3],
58
+ });
59
+ });
60
+ });
61
+ describe('Console Logging', () => {
62
+ testOrSkip('should capture console.log output', async () => {
63
+ const result = await executor.execute(`
64
+ console.log('Hello');
65
+ console.log('World');
66
+ return 'done';
67
+ `, {});
68
+ expect(result.logs).toContain('Hello');
69
+ expect(result.logs).toContain('World');
70
+ });
71
+ testOrSkip('should capture multiple arguments', async () => {
72
+ const result = await executor.execute(`console.log('Result:', 42, { key: 'value' });`, {});
73
+ expect(result.logs?.[0]).toContain('Result');
74
+ expect(result.logs?.[0]).toContain('42');
75
+ });
76
+ testOrSkip('should not include logs key when empty', async () => {
77
+ const result = await executor.execute('return 5;', {});
78
+ expect(result.logs).toBeUndefined();
79
+ });
80
+ });
81
+ describe('Error Handling', () => {
82
+ testOrSkip('should catch syntax errors', async () => {
83
+ const result = await executor.execute('this is not valid js', {});
84
+ expect(result.error).toBeDefined();
85
+ expect(result.result).toBeUndefined();
86
+ });
87
+ testOrSkip('should catch runtime errors', async () => {
88
+ const result = await executor.execute('throw new Error("Test error");', {});
89
+ expect(result.error).toContain('Test error');
90
+ expect(result.result).toBeUndefined();
91
+ });
92
+ testOrSkip('should catch reference errors', async () => {
93
+ const result = await executor.execute('return nonExistentVariable;', {});
94
+ expect(result.error).toBeDefined();
95
+ });
96
+ testOrSkip('should not throw exceptions, return them', async () => {
97
+ // Verify that the executor doesn't throw, only returns errors
98
+ const promise = executor.execute('throw new Error("Should not throw");', {});
99
+ await expect(promise).resolves.toHaveProperty('error');
100
+ });
101
+ });
102
+ describe('Tool Invocation', () => {
103
+ testOrSkip('should invoke tool functions', async () => {
104
+ const mockTool = vi.fn(async () => 42);
105
+ const result = await executor.execute('return await codemode.myTool();', { myTool: mockTool });
106
+ expect(result.result).toBe(42);
107
+ expect(mockTool).toHaveBeenCalled();
108
+ });
109
+ testOrSkip('should pass arguments to tool functions', async () => {
110
+ const mockTool = vi.fn(async (a, b) => a + b);
111
+ const result = await executor.execute('return await codemode.add(5, 3);', { add: mockTool });
112
+ expect(result.result).toBe(8);
113
+ expect(mockTool).toHaveBeenCalledWith(5, 3);
114
+ });
115
+ testOrSkip('should support tool with object arguments', async () => {
116
+ const mockTool = vi.fn(async (opts) => `${opts.name}=${opts.value}`);
117
+ const result = await executor.execute('return await codemode.format({ name: "test", value: 123 });', { format: mockTool });
118
+ expect(result.result).toBe('test=123');
119
+ expect(mockTool).toHaveBeenCalledWith({
120
+ name: 'test',
121
+ value: 123,
122
+ });
123
+ });
124
+ testOrSkip('should handle async tool errors', async () => {
125
+ const mockTool = vi.fn(async () => {
126
+ throw new Error('Tool failed');
127
+ });
128
+ const result = await executor.execute(`
129
+ try {
130
+ await codemode.failingTool();
131
+ return 'should not reach here';
132
+ } catch (e) {
133
+ return 'caught: ' + e.message;
134
+ }
135
+ `, { failingTool: mockTool });
136
+ expect(result.result).toContain('caught');
137
+ });
138
+ });
139
+ describe('Complex Code Patterns', () => {
140
+ testOrSkip('should handle for loops', async () => {
141
+ const result = await executor.execute(`
142
+ let sum = 0;
143
+ for (let i = 0; i < 10; i++) {
144
+ sum += i;
145
+ }
146
+ return sum;
147
+ `, {});
148
+ expect(result.result).toBe(45);
149
+ });
150
+ testOrSkip('should handle map/filter/reduce', async () => {
151
+ const result = await executor.execute(`
152
+ const arr = [1, 2, 3, 4, 5];
153
+ return arr
154
+ .filter(x => x > 2)
155
+ .map(x => x * 2)
156
+ .reduce((a, b) => a + b, 0);
157
+ `, {});
158
+ expect(result.result).toBe(24); // (3+4+5) * 2 = 24
159
+ });
160
+ testOrSkip('should handle try-catch', async () => {
161
+ const result = await executor.execute(`
162
+ try {
163
+ throw new Error('Caught!');
164
+ } catch (e) {
165
+ return e.message;
166
+ }
167
+ `, {});
168
+ expect(result.result).toBe('Caught!');
169
+ });
170
+ testOrSkip('should handle class definitions', async () => {
171
+ const result = await executor.execute(`
172
+ class Counter {
173
+ constructor(start = 0) {
174
+ this.count = start;
175
+ }
176
+ increment() {
177
+ return ++this.count;
178
+ }
179
+ }
180
+ const c = new Counter(5);
181
+ return c.increment();
182
+ `, {});
183
+ expect(result.result).toBe(6);
184
+ });
185
+ testOrSkip('should handle async/await chains', async () => {
186
+ const mockFetch = vi.fn(async (id) => ({ id, name: `Item ${id}` }));
187
+ const result = await executor.execute(`
188
+ const item1 = await codemode.fetch(1);
189
+ const item2 = await codemode.fetch(2);
190
+ return [item1, item2];
191
+ `, { fetch: mockFetch });
192
+ expect(Array.isArray(result.result)).toBe(true);
193
+ expect(result.result).toHaveLength(2);
194
+ expect(mockFetch).toHaveBeenCalledTimes(2);
195
+ });
196
+ });
197
+ describe('Isolation & Safety', () => {
198
+ testOrSkip('should not allow access to require', async () => {
199
+ const result = await executor.execute('return require("fs");', {});
200
+ expect(result.error).toBeDefined();
201
+ });
202
+ testOrSkip('should not allow process access', async () => {
203
+ const result = await executor.execute('return process.env;', {});
204
+ expect(result.error).toBeDefined();
205
+ });
206
+ testOrSkip('should not allow eval', async () => {
207
+ const result = await executor.execute('return eval("1+1");', {});
208
+ expect(result.error).toBeDefined();
209
+ });
210
+ testOrSkip('should not allow constructor to escape', async () => {
211
+ const result = await executor.execute('return (function(){}).constructor("return process")();', {});
212
+ expect(result.error).toBeDefined();
213
+ });
214
+ testOrSkip('should not allow network access', async () => {
215
+ // All executors must prevent outbound network access:
216
+ // - vm2/isolated-vm: no require/fetch/net globals available
217
+ // - container: --network=none blocks at OS level
218
+ const result = await executor.execute(`
219
+ // Try multiple network vectors
220
+ if (typeof fetch === 'function') {
221
+ await fetch('https://example.com');
222
+ return 'network allowed via fetch';
223
+ }
224
+ if (typeof require === 'function') {
225
+ const http = require('http');
226
+ await new Promise((resolve, reject) => {
227
+ http.get('http://example.com', resolve).on('error', reject);
228
+ });
229
+ return 'network allowed via http';
230
+ }
231
+ // If neither is available, that itself is blocking network access
232
+ throw new Error('no network APIs available');
233
+ `, {});
234
+ // Must either error or return no success indicator
235
+ if (result.error) {
236
+ // Good — network was blocked or APIs unavailable
237
+ expect(result.error).toBeDefined();
238
+ }
239
+ else {
240
+ // Should never reach here with 'network allowed' messages
241
+ expect(result.result).not.toBe('network allowed via fetch');
242
+ expect(result.result).not.toBe('network allowed via http');
243
+ }
244
+ });
245
+ testOrSkip('should not allow low-level socket network access', async () => {
246
+ // Validates that raw TCP/UDP socket APIs are blocked.
247
+ // - vm2/isolated-vm: require/net/dgram not available
248
+ // - container: --network=none blocks at OS level even if APIs exist
249
+ const result = await executor.execute(`
250
+ if (typeof require === 'function') {
251
+ // Try net.Socket (TCP)
252
+ try {
253
+ const net = require('net');
254
+ await new Promise((resolve, reject) => {
255
+ const sock = new net.Socket();
256
+ sock.setTimeout(2000);
257
+ sock.on('error', reject);
258
+ sock.on('timeout', () => reject(new Error('timeout')));
259
+ sock.connect(80, '1.1.1.1', resolve);
260
+ });
261
+ return 'network allowed via net.Socket';
262
+ } catch (e) {}
263
+
264
+ // Try dgram (UDP)
265
+ try {
266
+ const dgram = require('dgram');
267
+ const sock = dgram.createSocket('udp4');
268
+ await new Promise((resolve, reject) => {
269
+ sock.on('error', reject);
270
+ sock.send('ping', 53, '1.1.1.1', (err) => {
271
+ sock.close();
272
+ err ? reject(err) : resolve();
273
+ });
274
+ });
275
+ return 'network allowed via dgram';
276
+ } catch (e) {}
277
+
278
+ // Try tls.connect
279
+ try {
280
+ const tls = require('tls');
281
+ await new Promise((resolve, reject) => {
282
+ const sock = tls.connect(443, '1.1.1.1', {}, resolve);
283
+ sock.on('error', reject);
284
+ });
285
+ return 'network allowed via tls';
286
+ } catch (e) {}
287
+
288
+ // Try dns.resolve
289
+ try {
290
+ const dns = require('dns');
291
+ await new Promise((resolve, reject) => {
292
+ dns.resolve('example.com', (err, addresses) => {
293
+ err ? reject(err) : resolve(addresses);
294
+ });
295
+ });
296
+ return 'network allowed via dns';
297
+ } catch (e) {}
298
+
299
+ // All low-level socket APIs failed — network is blocked
300
+ throw new Error('all socket APIs blocked by network isolation');
301
+ }
302
+ // No require means no access to socket APIs at all
303
+ throw new Error('no require available');
304
+ `, {});
305
+ if (result.error) {
306
+ expect(result.error).toBeDefined();
307
+ }
308
+ else {
309
+ expect(result.result).not.toBe('network allowed via net.Socket');
310
+ expect(result.result).not.toBe('network allowed via dgram');
311
+ expect(result.result).not.toBe('network allowed via tls');
312
+ expect(result.result).not.toBe('network allowed via dns');
313
+ }
314
+ });
315
+ testOrSkip('should isolate prototype pollution to the current execution', async () => {
316
+ // NOTE: Prototype pollution IS possible within a single execution,
317
+ // but it is NOT a security risk because:
318
+ // 1. Each execution gets a fresh sandbox with clean prototypes
319
+ // 2. Pollution only affects that specific execution's globals
320
+ // 3. Host code is protected by serialization boundaries (JSON.stringify)
321
+ // 4. Next execution runs in a completely new sandbox
322
+ // Test 1: Pollution works within execution (expected)
323
+ const result1 = await executor.execute(`
324
+ Object.assign(Object.prototype, { polluted: true });
325
+ const obj = {};
326
+ return obj.polluted;
327
+ `, {});
328
+ expect(result1.result).toBe(true); // Pollution works within this execution
329
+ // Test 2: Next execution has clean prototypes (this proves isolation)
330
+ const result2 = await executor.execute(`
331
+ const obj = {};
332
+ return obj.polluted;
333
+ `, {});
334
+ expect(result2.result).toBeUndefined(); // Fresh sandbox, no pollution
335
+ });
336
+ });
337
+ describe('Concurrency', () => {
338
+ testOrSkip('should handle multiple concurrent executions', async () => {
339
+ const promises = Array.from({ length: 5 }, (_, i) => executor.execute(`return ${i * 10};`, {}));
340
+ const results = await Promise.all(promises);
341
+ results.forEach((result, i) => {
342
+ expect(result.result).toBe(i * 10);
343
+ });
344
+ });
345
+ testOrSkip('should isolate data between concurrent executions', async () => {
346
+ const promises = Array.from({ length: 3 }, (_, i) => executor.execute(`
347
+ global.sharedValue = ${i};
348
+ return global.sharedValue;
349
+ `, {}));
350
+ const results = await Promise.all(promises);
351
+ // Each should return its own value, not shared
352
+ results.forEach((result, i) => {
353
+ expect(result.result).toBe(i);
354
+ });
355
+ });
356
+ });
357
+ describe('Performance Baseline', () => {
358
+ testOrSkip('should execute simple code quickly', async () => {
359
+ const start = performance.now();
360
+ await executor.execute('return 1 + 1;', {});
361
+ const elapsed = performance.now() - start;
362
+ // Simple execution should be <100ms
363
+ expect(elapsed).toBeLessThan(100);
364
+ });
365
+ testOrSkip('should handle multiple sequential executions', async () => {
366
+ const start = performance.now();
367
+ for (let i = 0; i < 10; i++) {
368
+ await executor.execute(`return ${i};`, {});
369
+ }
370
+ const elapsed = performance.now() - start;
371
+ // 10 executions should be <500ms total
372
+ expect(elapsed).toBeLessThan(500);
373
+ });
374
+ });
375
+ });
376
+ }
377
+ // Run the test suite against both executors if available
378
+ if (require.main === module) {
379
+ // Import and run against vm2
380
+ try {
381
+ const { createVM2Executor } = require('./vm2-executor');
382
+ createExecutorTestSuite('vm2', () => createVM2Executor());
383
+ }
384
+ catch (e) {
385
+ console.log('vm2 executor not available');
386
+ }
387
+ // Import and run against isolated-vm
388
+ try {
389
+ const { createIsolatedVmExecutor } = require('./isolated-vm-executor');
390
+ createExecutorTestSuite('isolated-vm', () => createIsolatedVmExecutor({ memoryLimit: 256 }));
391
+ }
392
+ catch (e) {
393
+ console.log('isolated-vm executor not available');
394
+ }
395
+ }
@@ -0,0 +1,78 @@
1
+ import type { Executor, ExecuteResult } from '@cloudflare/codemode';
2
+ export interface IsolatedVmExecutorOptions {
3
+ /** Memory limit in MB (default: 128) */
4
+ memoryLimit?: number;
5
+ /** Enable inspector (default: false) */
6
+ inspector?: boolean;
7
+ /** Execution timeout in ms (default: 30000) */
8
+ timeout?: number;
9
+ }
10
+ export interface ExecutionMetrics {
11
+ cpuTime: bigint;
12
+ wallTime: bigint;
13
+ heapUsed: number;
14
+ heapLimit: number;
15
+ }
16
+ /**
17
+ * Executor implementation using isolated-vm
18
+ *
19
+ * Architecture: Uses Promise chain pattern
20
+ * - Async IIFE is executed in the isolate
21
+ * - Its returned Promise is chained with .then() inside the isolate
22
+ * - The .then handlers call sync Callbacks on the host side
23
+ * - Host Promise resolves when the callback is invoked
24
+ *
25
+ * Features:
26
+ * - True memory isolation (V8 Isolate level)
27
+ * - Hard memory limit enforcement
28
+ * - Accurate timeout handling
29
+ * - Promise-based result capturing
30
+ *
31
+ * Security:
32
+ * - Separate V8 memory heap
33
+ * - No shared memory access
34
+ * - Explicit serialization boundaries
35
+ */
36
+ export declare class IsolatedVmExecutor implements Executor {
37
+ private isolate;
38
+ private context;
39
+ private metrics;
40
+ private readonly options;
41
+ constructor(options?: IsolatedVmExecutorOptions);
42
+ /**
43
+ * Execute code within a sandboxed isolate using Promise chain pattern
44
+ *
45
+ * The function `fns` are made available as `codemode.*` within the sandbox.
46
+ * Uses Promise chain: async IIFE → .then() in isolate → sync callback → host Promise
47
+ *
48
+ * Returns ExecuteResult with:
49
+ * - result: The return value of the code
50
+ * - error: Error message if execution failed
51
+ * - logs: Array of console.log outputs
52
+ */
53
+ execute(code: string, fns: Record<string, (...args: unknown[]) => Promise<unknown>>): Promise<ExecuteResult>;
54
+ /**
55
+ * Cleanup resources
56
+ */
57
+ dispose(): void;
58
+ /**
59
+ * Get heap statistics from the isolate
60
+ */
61
+ getHeapStatistics(): Promise<{
62
+ total: number;
63
+ used: number;
64
+ limit: number;
65
+ }>;
66
+ /**
67
+ * Create or recreate context (resets globals for isolation)
68
+ */
69
+ private createContext;
70
+ /**
71
+ * Capture execution metrics
72
+ */
73
+ private captureMetrics;
74
+ }
75
+ /**
76
+ * Factory function to create an isolated-vm executor instance
77
+ */
78
+ export declare function createIsolatedVmExecutor(options?: IsolatedVmExecutorOptions): Executor;