@cloudflare/sandbox 0.3.7 → 0.4.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 (120) hide show
  1. package/.turbo/turbo-build.log +44 -0
  2. package/CHANGELOG.md +6 -14
  3. package/Dockerfile +82 -18
  4. package/README.md +89 -824
  5. package/dist/{chunk-JTKON2SH.js → chunk-BCJ7SF3Q.js} +9 -5
  6. package/dist/chunk-BCJ7SF3Q.js.map +1 -0
  7. package/dist/chunk-BFVUNTP4.js +104 -0
  8. package/dist/chunk-BFVUNTP4.js.map +1 -0
  9. package/dist/{chunk-NNGBXDMY.js → chunk-EKSWCBCA.js} +3 -6
  10. package/dist/chunk-EKSWCBCA.js.map +1 -0
  11. package/dist/chunk-HGF554LH.js +2236 -0
  12. package/dist/chunk-HGF554LH.js.map +1 -0
  13. package/dist/{chunk-6UAWTJ5S.js → chunk-Z532A7QC.js} +13 -20
  14. package/dist/{chunk-6UAWTJ5S.js.map → chunk-Z532A7QC.js.map} +1 -1
  15. package/dist/file-stream.d.ts +16 -38
  16. package/dist/file-stream.js +1 -2
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.js +35 -39
  19. package/dist/index.js.map +1 -1
  20. package/dist/interpreter.d.ts +3 -3
  21. package/dist/interpreter.js +2 -2
  22. package/dist/request-handler.d.ts +4 -3
  23. package/dist/request-handler.js +4 -7
  24. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  25. package/dist/sandbox.d.ts +3 -3
  26. package/dist/sandbox.js +4 -7
  27. package/dist/security.d.ts +4 -3
  28. package/dist/security.js +3 -3
  29. package/dist/sse-parser.js +1 -1
  30. package/package.json +11 -5
  31. package/src/clients/base-client.ts +280 -0
  32. package/src/clients/command-client.ts +115 -0
  33. package/src/clients/file-client.ts +269 -0
  34. package/src/clients/git-client.ts +92 -0
  35. package/src/clients/index.ts +63 -0
  36. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  37. package/src/clients/port-client.ts +105 -0
  38. package/src/clients/process-client.ts +177 -0
  39. package/src/clients/sandbox-client.ts +41 -0
  40. package/src/clients/types.ts +84 -0
  41. package/src/clients/utility-client.ts +94 -0
  42. package/src/errors/adapter.ts +180 -0
  43. package/src/errors/classes.ts +469 -0
  44. package/src/errors/index.ts +105 -0
  45. package/src/file-stream.ts +119 -117
  46. package/src/index.ts +81 -69
  47. package/src/interpreter.ts +17 -8
  48. package/src/request-handler.ts +69 -43
  49. package/src/sandbox.ts +694 -533
  50. package/src/security.ts +14 -23
  51. package/src/sse-parser.ts +4 -8
  52. package/startup.sh +3 -0
  53. package/tests/base-client.test.ts +328 -0
  54. package/tests/command-client.test.ts +407 -0
  55. package/tests/file-client.test.ts +643 -0
  56. package/tests/file-stream.test.ts +306 -0
  57. package/tests/git-client.test.ts +328 -0
  58. package/tests/port-client.test.ts +301 -0
  59. package/tests/process-client.test.ts +658 -0
  60. package/tests/sandbox.test.ts +465 -0
  61. package/tests/sse-parser.test.ts +290 -0
  62. package/tests/utility-client.test.ts +266 -0
  63. package/tests/wrangler.jsonc +35 -0
  64. package/tsconfig.json +9 -1
  65. package/vitest.config.ts +31 -0
  66. package/container_src/bun.lock +0 -76
  67. package/container_src/circuit-breaker.ts +0 -121
  68. package/container_src/control-process.ts +0 -784
  69. package/container_src/handler/exec.ts +0 -185
  70. package/container_src/handler/file.ts +0 -457
  71. package/container_src/handler/git.ts +0 -130
  72. package/container_src/handler/ports.ts +0 -314
  73. package/container_src/handler/process.ts +0 -568
  74. package/container_src/handler/session.ts +0 -92
  75. package/container_src/index.ts +0 -601
  76. package/container_src/interpreter-service.ts +0 -276
  77. package/container_src/isolation.ts +0 -1213
  78. package/container_src/mime-processor.ts +0 -255
  79. package/container_src/package.json +0 -18
  80. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  81. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  82. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  83. package/container_src/runtime/process-pool.ts +0 -464
  84. package/container_src/shell-escape.ts +0 -42
  85. package/container_src/startup.sh +0 -11
  86. package/container_src/types.ts +0 -131
  87. package/dist/chunk-32UDXUPC.js +0 -671
  88. package/dist/chunk-32UDXUPC.js.map +0 -1
  89. package/dist/chunk-5DILEXGY.js +0 -85
  90. package/dist/chunk-5DILEXGY.js.map +0 -1
  91. package/dist/chunk-D3U63BZP.js +0 -240
  92. package/dist/chunk-D3U63BZP.js.map +0 -1
  93. package/dist/chunk-FXYPFGOZ.js +0 -129
  94. package/dist/chunk-FXYPFGOZ.js.map +0 -1
  95. package/dist/chunk-JTKON2SH.js.map +0 -1
  96. package/dist/chunk-NNGBXDMY.js.map +0 -1
  97. package/dist/chunk-SQLJNZ3K.js +0 -674
  98. package/dist/chunk-SQLJNZ3K.js.map +0 -1
  99. package/dist/chunk-W7TVRPBG.js +0 -108
  100. package/dist/chunk-W7TVRPBG.js.map +0 -1
  101. package/dist/client-B3RUab0s.d.ts +0 -225
  102. package/dist/client.d.ts +0 -4
  103. package/dist/client.js +0 -7
  104. package/dist/client.js.map +0 -1
  105. package/dist/errors.d.ts +0 -95
  106. package/dist/errors.js +0 -27
  107. package/dist/errors.js.map +0 -1
  108. package/dist/interpreter-client.d.ts +0 -4
  109. package/dist/interpreter-client.js +0 -9
  110. package/dist/interpreter-client.js.map +0 -1
  111. package/dist/interpreter-types.d.ts +0 -259
  112. package/dist/interpreter-types.js +0 -9
  113. package/dist/interpreter-types.js.map +0 -1
  114. package/dist/types.d.ts +0 -453
  115. package/dist/types.js +0 -45
  116. package/dist/types.js.map +0 -1
  117. package/src/client.ts +0 -1048
  118. package/src/errors.ts +0 -219
  119. package/src/interpreter-types.ts +0 -390
  120. package/src/types.ts +0 -571
package/src/security.ts CHANGED
@@ -84,30 +84,21 @@ export function sanitizeSandboxId(id: string): string {
84
84
 
85
85
 
86
86
  /**
87
- * Logs security events for monitoring
87
+ * Validates language for code interpreter
88
+ * Only allows supported languages
88
89
  */
89
- export function logSecurityEvent(
90
- event: string,
91
- details: Record<string, any>,
92
- severity: 'low' | 'medium' | 'high' | 'critical' = 'medium'
93
- ): void {
94
- const logEntry = {
95
- timestamp: new Date().toISOString(),
96
- event,
97
- severity,
98
- ...details
99
- };
90
+ export function validateLanguage(language: string | undefined): void {
91
+ if (!language) {
92
+ return; // undefined is valid, will default to python
93
+ }
94
+
95
+ const supportedLanguages = ['python', 'python3', 'javascript', 'js', 'node', 'typescript', 'ts'];
96
+ const normalized = language.toLowerCase();
100
97
 
101
- switch (severity) {
102
- case 'critical':
103
- case 'high':
104
- console.error(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
105
- break;
106
- case 'medium':
107
- console.warn(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
108
- break;
109
- case 'low':
110
- console.info(`[SECURITY:${severity.toUpperCase()}] ${event}:`, JSON.stringify(logEntry));
111
- break;
98
+ if (!supportedLanguages.includes(normalized)) {
99
+ throw new SecurityError(
100
+ `Unsupported language '${language}'. Supported languages: python, javascript, typescript`,
101
+ 'INVALID_LANGUAGE'
102
+ );
112
103
  }
113
104
  }
package/src/sse-parser.ts CHANGED
@@ -49,11 +49,8 @@ export async function* parseSSEStream<T>(
49
49
  try {
50
50
  const event = JSON.parse(data) as T;
51
51
  yield event;
52
- } catch (error) {
53
- // Log parsing errors but continue processing
54
- console.error('Failed to parse SSE event:', data, error);
55
- // Optionally yield an error event
56
- // yield { type: 'error', data: `Parse error: ${error.message}` } as T;
52
+ } catch {
53
+ // Skip invalid JSON events and continue processing
57
54
  }
58
55
  }
59
56
  // Handle other SSE fields if needed (event:, id:, retry:)
@@ -68,8 +65,8 @@ export async function* parseSSEStream<T>(
68
65
  try {
69
66
  const event = JSON.parse(data) as T;
70
67
  yield event;
71
- } catch (error) {
72
- console.error('Failed to parse final SSE event:', data, error);
68
+ } catch {
69
+ // Skip invalid JSON in final event
73
70
  }
74
71
  }
75
72
  }
@@ -141,7 +138,6 @@ export function asyncIterableToSSEStream<T>(
141
138
 
142
139
  cancel() {
143
140
  // Handle stream cancellation
144
- console.log('SSE stream cancelled');
145
141
  }
146
142
  });
147
143
  }
package/startup.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ exec bun dist/index.js
@@ -0,0 +1,328 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { BaseApiResponse, HttpClientOptions } from '../src/clients';
3
+ import { BaseHttpClient } from '../src/clients/base-client';
4
+ import type { ErrorResponse } from '../src/errors';
5
+ import {
6
+ CommandError,
7
+ FileNotFoundError,
8
+ FileSystemError,
9
+ PermissionDeniedError,
10
+ SandboxError
11
+ } from '../src/errors';
12
+
13
+ interface TestDataResponse extends BaseApiResponse {
14
+ data: string;
15
+ }
16
+
17
+ interface TestResourceResponse extends BaseApiResponse {
18
+ id: string;
19
+ }
20
+
21
+ interface TestSourceResponse extends BaseApiResponse {
22
+ source: string;
23
+ }
24
+
25
+ interface TestStatusResponse extends BaseApiResponse {
26
+ status: string;
27
+ }
28
+
29
+ class TestHttpClient extends BaseHttpClient {
30
+ constructor(options: HttpClientOptions = {}) {
31
+ super({
32
+ baseUrl: 'http://test.com',
33
+ port: 3000,
34
+ ...options,
35
+ });
36
+ }
37
+
38
+ public async testRequest<T = BaseApiResponse>(endpoint: string, data?: Record<string, unknown>): Promise<T> {
39
+ if (data) {
40
+ return this.post<T>(endpoint, data);
41
+ }
42
+ return this.get<T>(endpoint);
43
+ }
44
+
45
+ public async testStreamRequest(endpoint: string): Promise<ReadableStream> {
46
+ const response = await this.doFetch(endpoint);
47
+ return this.handleStreamResponse(response);
48
+ }
49
+
50
+ public async testErrorHandling(errorResponse: ErrorResponse) {
51
+ const response = new Response(
52
+ JSON.stringify(errorResponse),
53
+ { status: errorResponse.httpStatus || 400 }
54
+ );
55
+ return this.handleErrorResponse(response);
56
+ }
57
+ }
58
+
59
+ describe('BaseHttpClient', () => {
60
+ let client: TestHttpClient;
61
+ let mockFetch: ReturnType<typeof vi.fn>;
62
+ let onError: ReturnType<typeof vi.fn>;
63
+
64
+ beforeEach(() => {
65
+ vi.clearAllMocks();
66
+
67
+ mockFetch = vi.fn();
68
+ global.fetch = mockFetch as unknown as typeof fetch;
69
+ onError = vi.fn();
70
+
71
+ client = new TestHttpClient({
72
+ baseUrl: 'http://test.com',
73
+ port: 3000,
74
+ onError,
75
+ });
76
+ });
77
+
78
+ afterEach(() => {
79
+ vi.restoreAllMocks();
80
+ });
81
+
82
+ describe('core request functionality', () => {
83
+ it('should handle successful API requests', async () => {
84
+ mockFetch.mockResolvedValue(new Response(
85
+ JSON.stringify({ success: true, data: 'operation completed' }),
86
+ { status: 200 }
87
+ ));
88
+
89
+ const result = await client.testRequest<TestDataResponse>('/api/test');
90
+
91
+ expect(result.success).toBe(true);
92
+ expect(result.data).toBe('operation completed');
93
+ });
94
+
95
+ it('should handle POST requests with data', async () => {
96
+ const requestData = { action: 'create', name: 'test-resource' };
97
+ mockFetch.mockResolvedValue(new Response(
98
+ JSON.stringify({ success: true, id: 'resource-123' }),
99
+ { status: 201 }
100
+ ));
101
+
102
+ const result = await client.testRequest<TestResourceResponse>('/api/create', requestData);
103
+
104
+ expect(result.success).toBe(true);
105
+ expect(result.id).toBe('resource-123');
106
+
107
+ const [url, options] = mockFetch.mock.calls[0];
108
+ expect(url).toBe('http://test.com/api/create');
109
+ expect(options.method).toBe('POST');
110
+ expect(options.headers['Content-Type']).toBe('application/json');
111
+ expect(JSON.parse(options.body)).toEqual(requestData);
112
+ });
113
+ });
114
+
115
+ describe('error handling and mapping', () => {
116
+ it('should map container errors to client errors', async () => {
117
+ const errorMappingTests = [
118
+ {
119
+ containerError: {
120
+ code: 'FILE_NOT_FOUND',
121
+ message: 'File not found: /test.txt',
122
+ context: { path: '/test.txt' },
123
+ httpStatus: 404,
124
+ timestamp: new Date().toISOString()
125
+ },
126
+ expectedError: FileNotFoundError,
127
+ },
128
+ {
129
+ containerError: {
130
+ code: 'PERMISSION_DENIED',
131
+ message: 'Permission denied',
132
+ context: { path: '/secure.txt' },
133
+ httpStatus: 403,
134
+ timestamp: new Date().toISOString()
135
+ },
136
+ expectedError: PermissionDeniedError,
137
+ },
138
+ {
139
+ containerError: {
140
+ code: 'COMMAND_EXECUTION_ERROR',
141
+ message: 'Command failed: badcmd',
142
+ context: { command: 'badcmd' },
143
+ httpStatus: 400,
144
+ timestamp: new Date().toISOString()
145
+ },
146
+ expectedError: CommandError,
147
+ },
148
+ {
149
+ containerError: {
150
+ code: 'FILESYSTEM_ERROR',
151
+ message: 'Filesystem error',
152
+ context: { path: '/test' },
153
+ httpStatus: 500,
154
+ timestamp: new Date().toISOString()
155
+ },
156
+ expectedError: FileSystemError,
157
+ },
158
+ {
159
+ containerError: {
160
+ code: 'UNKNOWN_ERROR',
161
+ message: 'Unknown error',
162
+ context: {},
163
+ httpStatus: 500,
164
+ timestamp: new Date().toISOString()
165
+ },
166
+ expectedError: SandboxError,
167
+ }
168
+ ];
169
+
170
+ for (const test of errorMappingTests) {
171
+ await expect(client.testErrorHandling(test.containerError as ErrorResponse))
172
+ .rejects.toThrow(test.expectedError);
173
+
174
+ expect(onError).toHaveBeenCalledWith(test.containerError.message, undefined);
175
+ }
176
+ });
177
+
178
+ it('should handle malformed error responses', async () => {
179
+ mockFetch.mockResolvedValue(new Response(
180
+ 'invalid json {',
181
+ { status: 500 }
182
+ ));
183
+
184
+ await expect(client.testRequest('/api/test'))
185
+ .rejects.toThrow(SandboxError);
186
+ });
187
+
188
+ it('should handle network failures', async () => {
189
+ mockFetch.mockRejectedValue(new Error('Network connection timeout'));
190
+
191
+ await expect(client.testRequest('/api/test'))
192
+ .rejects.toThrow('Network connection timeout');
193
+ });
194
+
195
+ it('should handle server unavailable scenarios', async () => {
196
+ mockFetch.mockResolvedValue(new Response(
197
+ 'Service Unavailable',
198
+ { status: 503 }
199
+ ));
200
+
201
+ await expect(client.testRequest('/api/test'))
202
+ .rejects.toThrow(SandboxError);
203
+
204
+ expect(onError).toHaveBeenCalledWith('HTTP error! status: 503', undefined);
205
+ });
206
+ });
207
+
208
+
209
+ describe('streaming functionality', () => {
210
+ it('should handle streaming responses', async () => {
211
+ const streamData = 'data: {"type":"output","content":"stream data"}\n\n';
212
+ const mockStream = new ReadableStream({
213
+ start(controller) {
214
+ controller.enqueue(new TextEncoder().encode(streamData));
215
+ controller.close();
216
+ }
217
+ });
218
+
219
+ mockFetch.mockResolvedValue(new Response(mockStream, {
220
+ status: 200,
221
+ headers: { 'Content-Type': 'text/event-stream' }
222
+ }));
223
+
224
+ const stream = await client.testStreamRequest('/api/stream');
225
+
226
+ expect(stream).toBeInstanceOf(ReadableStream);
227
+
228
+ const reader = stream.getReader();
229
+ const { done, value } = await reader.read();
230
+ const content = new TextDecoder().decode(value);
231
+
232
+ expect(done).toBe(false);
233
+ expect(content).toContain('stream data');
234
+
235
+ reader.releaseLock();
236
+ });
237
+
238
+ it('should handle streaming errors', async () => {
239
+ mockFetch.mockResolvedValue(new Response(
240
+ JSON.stringify({ error: 'Stream initialization failed', code: 'STREAM_ERROR' }),
241
+ { status: 400 }
242
+ ));
243
+
244
+ await expect(client.testStreamRequest('/api/bad-stream'))
245
+ .rejects.toThrow(SandboxError);
246
+ });
247
+
248
+ it('should handle missing stream body', async () => {
249
+ mockFetch.mockResolvedValue(new Response(null, {
250
+ status: 200,
251
+ headers: { 'Content-Type': 'text/event-stream' }
252
+ }));
253
+
254
+ await expect(client.testStreamRequest('/api/empty-stream'))
255
+ .rejects.toThrow('No response body for streaming');
256
+ });
257
+ });
258
+
259
+ describe('stub integration', () => {
260
+ it('should use stub when provided instead of fetch', async () => {
261
+ const stubFetch = vi.fn().mockResolvedValue(new Response(
262
+ JSON.stringify({ success: true, source: 'stub' }),
263
+ { status: 200 }
264
+ ));
265
+
266
+ const stub = { containerFetch: stubFetch };
267
+ const stubClient = new TestHttpClient({
268
+ baseUrl: 'http://test.com',
269
+ port: 3000,
270
+ stub,
271
+ });
272
+
273
+ const result = await stubClient.testRequest<TestSourceResponse>('/api/stub-test');
274
+
275
+ expect(result.success).toBe(true);
276
+ expect(result.source).toBe('stub');
277
+ expect(stubFetch).toHaveBeenCalledWith(
278
+ 'http://localhost:3000/api/stub-test',
279
+ { method: 'GET' },
280
+ 3000
281
+ );
282
+ expect(mockFetch).not.toHaveBeenCalled();
283
+ });
284
+
285
+ it('should handle stub errors', async () => {
286
+ const stubFetch = vi.fn().mockRejectedValue(new Error('Stub connection failed'));
287
+ const stub = { containerFetch: stubFetch };
288
+ const stubClient = new TestHttpClient({
289
+ baseUrl: 'http://test.com',
290
+ port: 3000,
291
+ stub,
292
+ });
293
+
294
+ await expect(stubClient.testRequest('/api/stub-error'))
295
+ .rejects.toThrow('Stub connection failed');
296
+ });
297
+ });
298
+
299
+ describe('edge cases and resilience', () => {
300
+ it('should handle responses with unusual status codes', async () => {
301
+ const unusualStatusTests = [
302
+ { status: 201, shouldSucceed: true },
303
+ { status: 202, shouldSucceed: true },
304
+ { status: 409, shouldSucceed: false },
305
+ { status: 422, shouldSucceed: false },
306
+ { status: 429, shouldSucceed: false },
307
+ ];
308
+
309
+ for (const test of unusualStatusTests) {
310
+ mockFetch.mockResolvedValueOnce(new Response(
311
+ test.shouldSucceed
312
+ ? JSON.stringify({ success: true, status: test.status })
313
+ : JSON.stringify({ error: `Status ${test.status}` }),
314
+ { status: test.status }
315
+ ));
316
+
317
+ if (test.shouldSucceed) {
318
+ const result = await client.testRequest<TestStatusResponse>('/api/unusual-status');
319
+ expect(result.success).toBe(true);
320
+ expect(result.status).toBe(test.status);
321
+ } else {
322
+ await expect(client.testRequest('/api/unusual-status'))
323
+ .rejects.toThrow();
324
+ }
325
+ }
326
+ });
327
+ });
328
+ });