@cloudflare/sandbox 0.0.0-cecde0a → 0.0.0-d4bb3b7

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 (59) hide show
  1. package/CHANGELOG.md +314 -0
  2. package/Dockerfile +179 -69
  3. package/LICENSE +176 -0
  4. package/README.md +119 -315
  5. package/dist/index.d.ts +1953 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3280 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +16 -7
  10. package/src/clients/base-client.ts +295 -0
  11. package/src/clients/command-client.ts +115 -0
  12. package/src/clients/file-client.ts +300 -0
  13. package/src/clients/git-client.ts +98 -0
  14. package/src/clients/index.ts +64 -0
  15. package/src/clients/interpreter-client.ts +333 -0
  16. package/src/clients/port-client.ts +105 -0
  17. package/src/clients/process-client.ts +180 -0
  18. package/src/clients/sandbox-client.ts +39 -0
  19. package/src/clients/types.ts +88 -0
  20. package/src/clients/utility-client.ts +156 -0
  21. package/src/errors/adapter.ts +238 -0
  22. package/src/errors/classes.ts +594 -0
  23. package/src/errors/index.ts +109 -0
  24. package/src/file-stream.ts +169 -0
  25. package/src/index.ts +98 -14
  26. package/src/interpreter.ts +168 -0
  27. package/src/request-handler.ts +94 -55
  28. package/src/sandbox.ts +938 -315
  29. package/src/security.ts +34 -28
  30. package/src/sse-parser.ts +8 -11
  31. package/src/version.ts +6 -0
  32. package/startup.sh +3 -0
  33. package/tests/base-client.test.ts +364 -0
  34. package/tests/command-client.test.ts +444 -0
  35. package/tests/file-client.test.ts +831 -0
  36. package/tests/file-stream.test.ts +310 -0
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +487 -0
  39. package/tests/port-client.test.ts +293 -0
  40. package/tests/process-client.test.ts +683 -0
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +739 -0
  43. package/tests/sse-parser.test.ts +291 -0
  44. package/tests/utility-client.test.ts +339 -0
  45. package/tests/version.test.ts +16 -0
  46. package/tests/wrangler.jsonc +35 -0
  47. package/tsconfig.json +9 -1
  48. package/tsdown.config.ts +12 -0
  49. package/vitest.config.ts +31 -0
  50. package/container_src/handler/exec.ts +0 -337
  51. package/container_src/handler/file.ts +0 -844
  52. package/container_src/handler/git.ts +0 -182
  53. package/container_src/handler/ports.ts +0 -314
  54. package/container_src/handler/process.ts +0 -640
  55. package/container_src/index.ts +0 -361
  56. package/container_src/package.json +0 -9
  57. package/container_src/types.ts +0 -103
  58. package/src/client.ts +0 -1038
  59. package/src/types.ts +0 -386
package/src/security.ts CHANGED
@@ -10,7 +10,10 @@
10
10
  */
11
11
 
12
12
  export class SecurityError extends Error {
13
- constructor(message: string, public readonly code?: string) {
13
+ constructor(
14
+ message: string,
15
+ public readonly code?: string
16
+ ) {
14
17
  super(message);
15
18
  this.name = 'SecurityError';
16
19
  }
@@ -34,7 +37,7 @@ export function validatePort(port: number): boolean {
34
37
  // Exclude ports reserved by our system
35
38
  const reservedPorts = [
36
39
  3000, // Control plane port
37
- 8787, // Common wrangler dev port
40
+ 8787 // Common wrangler dev port
38
41
  ];
39
42
 
40
43
  if (reservedPorts.includes(port)) {
@@ -67,8 +70,13 @@ export function sanitizeSandboxId(id: string): string {
67
70
 
68
71
  // Prevent reserved names that cause technical conflicts
69
72
  const reservedNames = [
70
- 'www', 'api', 'admin', 'root', 'system',
71
- 'cloudflare', 'workers'
73
+ 'www',
74
+ 'api',
75
+ 'admin',
76
+ 'root',
77
+ 'system',
78
+ 'cloudflare',
79
+ 'workers'
72
80
  ];
73
81
 
74
82
  const lowerCaseId = id.toLowerCase();
@@ -82,32 +90,30 @@ export function sanitizeSandboxId(id: string): string {
82
90
  return id;
83
91
  }
84
92
 
85
-
86
93
  /**
87
- * Logs security events for monitoring
94
+ * Validates language for code interpreter
95
+ * Only allows supported languages
88
96
  */
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
- };
97
+ export function validateLanguage(language: string | undefined): void {
98
+ if (!language) {
99
+ return; // undefined is valid, will default to python
100
+ }
101
+
102
+ const supportedLanguages = [
103
+ 'python',
104
+ 'python3',
105
+ 'javascript',
106
+ 'js',
107
+ 'node',
108
+ 'typescript',
109
+ 'ts'
110
+ ];
111
+ const normalized = language.toLowerCase();
100
112
 
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;
113
+ if (!supportedLanguages.includes(normalized)) {
114
+ throw new SecurityError(
115
+ `Unsupported language '${language}'. Supported languages: python, javascript, typescript`,
116
+ 'INVALID_LANGUAGE'
117
+ );
112
118
  }
113
119
  }
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
  }
@@ -79,7 +76,6 @@ export async function* parseSSEStream<T>(
79
76
  }
80
77
  }
81
78
 
82
-
83
79
  /**
84
80
  * Helper to convert a Response with SSE stream directly to AsyncIterable
85
81
  * @param response - Response object with SSE stream
@@ -90,7 +86,9 @@ export async function* responseToAsyncIterable<T>(
90
86
  signal?: AbortSignal
91
87
  ): AsyncIterable<T> {
92
88
  if (!response.ok) {
93
- throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
89
+ throw new Error(
90
+ `Response not ok: ${response.status} ${response.statusText}`
91
+ );
94
92
  }
95
93
 
96
94
  if (!response.body) {
@@ -141,7 +139,6 @@ export function asyncIterableToSSEStream<T>(
141
139
 
142
140
  cancel() {
143
141
  // Handle stream cancellation
144
- console.log('SSE stream cancelled');
145
142
  }
146
143
  });
147
- }
144
+ }
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * SDK version - automatically synchronized with package.json by Changesets
3
+ * This file is auto-updated by .github/changeset-version.ts during releases
4
+ * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5
+ */
6
+ export const SDK_VERSION = '0.4.19';
package/startup.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ exec bun /container-server/dist/index.js
@@ -0,0 +1,364 @@
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>(
39
+ endpoint: string,
40
+ data?: Record<string, unknown>
41
+ ): Promise<T> {
42
+ if (data) {
43
+ return this.post<T>(endpoint, data);
44
+ }
45
+ return this.get<T>(endpoint);
46
+ }
47
+
48
+ public async testStreamRequest(endpoint: string): Promise<ReadableStream> {
49
+ const response = await this.doFetch(endpoint);
50
+ return this.handleStreamResponse(response);
51
+ }
52
+
53
+ public async testErrorHandling(errorResponse: ErrorResponse) {
54
+ const response = new Response(JSON.stringify(errorResponse), {
55
+ status: errorResponse.httpStatus || 400
56
+ });
57
+ return this.handleErrorResponse(response);
58
+ }
59
+ }
60
+
61
+ describe('BaseHttpClient', () => {
62
+ let client: TestHttpClient;
63
+ let mockFetch: ReturnType<typeof vi.fn>;
64
+ let onError: ReturnType<typeof vi.fn>;
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+
69
+ mockFetch = vi.fn();
70
+ global.fetch = mockFetch as unknown as typeof fetch;
71
+ onError = vi.fn();
72
+
73
+ client = new TestHttpClient({
74
+ baseUrl: 'http://test.com',
75
+ port: 3000,
76
+ onError
77
+ });
78
+ });
79
+
80
+ afterEach(() => {
81
+ vi.restoreAllMocks();
82
+ });
83
+
84
+ describe('core request functionality', () => {
85
+ it('should handle successful API requests', async () => {
86
+ mockFetch.mockResolvedValue(
87
+ new Response(
88
+ JSON.stringify({ success: true, data: 'operation completed' }),
89
+ { status: 200 }
90
+ )
91
+ );
92
+
93
+ const result = await client.testRequest<TestDataResponse>('/api/test');
94
+
95
+ expect(result.success).toBe(true);
96
+ expect(result.data).toBe('operation completed');
97
+ });
98
+
99
+ it('should handle POST requests with data', async () => {
100
+ const requestData = { action: 'create', name: 'test-resource' };
101
+ mockFetch.mockResolvedValue(
102
+ new Response(JSON.stringify({ success: true, id: 'resource-123' }), {
103
+ status: 201
104
+ })
105
+ );
106
+
107
+ const result = await client.testRequest<TestResourceResponse>(
108
+ '/api/create',
109
+ requestData
110
+ );
111
+
112
+ expect(result.success).toBe(true);
113
+ expect(result.id).toBe('resource-123');
114
+
115
+ const [url, options] = mockFetch.mock.calls[0];
116
+ expect(url).toBe('http://test.com/api/create');
117
+ expect(options.method).toBe('POST');
118
+ expect(options.headers['Content-Type']).toBe('application/json');
119
+ expect(JSON.parse(options.body)).toEqual(requestData);
120
+ });
121
+ });
122
+
123
+ describe('error handling and mapping', () => {
124
+ it('should map container errors to client errors', async () => {
125
+ const errorMappingTests = [
126
+ {
127
+ containerError: {
128
+ code: 'FILE_NOT_FOUND',
129
+ message: 'File not found: /test.txt',
130
+ context: { path: '/test.txt' },
131
+ httpStatus: 404,
132
+ timestamp: new Date().toISOString()
133
+ },
134
+ expectedError: FileNotFoundError
135
+ },
136
+ {
137
+ containerError: {
138
+ code: 'PERMISSION_DENIED',
139
+ message: 'Permission denied',
140
+ context: { path: '/secure.txt' },
141
+ httpStatus: 403,
142
+ timestamp: new Date().toISOString()
143
+ },
144
+ expectedError: PermissionDeniedError
145
+ },
146
+ {
147
+ containerError: {
148
+ code: 'COMMAND_EXECUTION_ERROR',
149
+ message: 'Command failed: badcmd',
150
+ context: { command: 'badcmd' },
151
+ httpStatus: 400,
152
+ timestamp: new Date().toISOString()
153
+ },
154
+ expectedError: CommandError
155
+ },
156
+ {
157
+ containerError: {
158
+ code: 'FILESYSTEM_ERROR',
159
+ message: 'Filesystem error',
160
+ context: { path: '/test' },
161
+ httpStatus: 500,
162
+ timestamp: new Date().toISOString()
163
+ },
164
+ expectedError: FileSystemError
165
+ },
166
+ {
167
+ containerError: {
168
+ code: 'UNKNOWN_ERROR',
169
+ message: 'Unknown error',
170
+ context: {},
171
+ httpStatus: 500,
172
+ timestamp: new Date().toISOString()
173
+ },
174
+ expectedError: SandboxError
175
+ }
176
+ ];
177
+
178
+ for (const test of errorMappingTests) {
179
+ await expect(
180
+ client.testErrorHandling(test.containerError as ErrorResponse)
181
+ ).rejects.toThrow(test.expectedError);
182
+
183
+ expect(onError).toHaveBeenCalledWith(
184
+ test.containerError.message,
185
+ undefined
186
+ );
187
+ }
188
+ });
189
+
190
+ it('should handle malformed error responses', async () => {
191
+ mockFetch.mockResolvedValue(
192
+ new Response('invalid json {', { status: 500 })
193
+ );
194
+
195
+ await expect(client.testRequest('/api/test')).rejects.toThrow(
196
+ SandboxError
197
+ );
198
+ });
199
+
200
+ it('should handle network failures', async () => {
201
+ mockFetch.mockRejectedValue(new Error('Network connection timeout'));
202
+
203
+ await expect(client.testRequest('/api/test')).rejects.toThrow(
204
+ 'Network connection timeout'
205
+ );
206
+ });
207
+
208
+ it('should handle server unavailable scenarios', async () => {
209
+ mockFetch.mockResolvedValue(
210
+ new Response('Service Unavailable', { status: 503 })
211
+ );
212
+
213
+ await expect(client.testRequest('/api/test')).rejects.toThrow(
214
+ SandboxError
215
+ );
216
+
217
+ expect(onError).toHaveBeenCalledWith(
218
+ 'HTTP error! status: 503',
219
+ undefined
220
+ );
221
+ });
222
+ });
223
+
224
+ describe('streaming functionality', () => {
225
+ it('should handle streaming responses', async () => {
226
+ const streamData = 'data: {"type":"output","content":"stream data"}\n\n';
227
+ const mockStream = new ReadableStream({
228
+ start(controller) {
229
+ controller.enqueue(new TextEncoder().encode(streamData));
230
+ controller.close();
231
+ }
232
+ });
233
+
234
+ mockFetch.mockResolvedValue(
235
+ new Response(mockStream, {
236
+ status: 200,
237
+ headers: { 'Content-Type': 'text/event-stream' }
238
+ })
239
+ );
240
+
241
+ const stream = await client.testStreamRequest('/api/stream');
242
+
243
+ expect(stream).toBeInstanceOf(ReadableStream);
244
+
245
+ const reader = stream.getReader();
246
+ const { done, value } = await reader.read();
247
+ const content = new TextDecoder().decode(value);
248
+
249
+ expect(done).toBe(false);
250
+ expect(content).toContain('stream data');
251
+
252
+ reader.releaseLock();
253
+ });
254
+
255
+ it('should handle streaming errors', async () => {
256
+ mockFetch.mockResolvedValue(
257
+ new Response(
258
+ JSON.stringify({
259
+ error: 'Stream initialization failed',
260
+ code: 'STREAM_ERROR'
261
+ }),
262
+ { status: 400 }
263
+ )
264
+ );
265
+
266
+ await expect(client.testStreamRequest('/api/bad-stream')).rejects.toThrow(
267
+ SandboxError
268
+ );
269
+ });
270
+
271
+ it('should handle missing stream body', async () => {
272
+ mockFetch.mockResolvedValue(
273
+ new Response(null, {
274
+ status: 200,
275
+ headers: { 'Content-Type': 'text/event-stream' }
276
+ })
277
+ );
278
+
279
+ await expect(
280
+ client.testStreamRequest('/api/empty-stream')
281
+ ).rejects.toThrow('No response body for streaming');
282
+ });
283
+ });
284
+
285
+ describe('stub integration', () => {
286
+ it('should use stub when provided instead of fetch', async () => {
287
+ const stubFetch = vi.fn().mockResolvedValue(
288
+ new Response(JSON.stringify({ success: true, source: 'stub' }), {
289
+ status: 200
290
+ })
291
+ );
292
+
293
+ const stub = { containerFetch: stubFetch };
294
+ const stubClient = new TestHttpClient({
295
+ baseUrl: 'http://test.com',
296
+ port: 3000,
297
+ stub
298
+ });
299
+
300
+ const result =
301
+ await stubClient.testRequest<TestSourceResponse>('/api/stub-test');
302
+
303
+ expect(result.success).toBe(true);
304
+ expect(result.source).toBe('stub');
305
+ expect(stubFetch).toHaveBeenCalledWith(
306
+ 'http://localhost:3000/api/stub-test',
307
+ { method: 'GET' },
308
+ 3000
309
+ );
310
+ expect(mockFetch).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('should handle stub errors', async () => {
314
+ const stubFetch = vi
315
+ .fn()
316
+ .mockRejectedValue(new Error('Stub connection failed'));
317
+ const stub = { containerFetch: stubFetch };
318
+ const stubClient = new TestHttpClient({
319
+ baseUrl: 'http://test.com',
320
+ port: 3000,
321
+ stub
322
+ });
323
+
324
+ await expect(stubClient.testRequest('/api/stub-error')).rejects.toThrow(
325
+ 'Stub connection failed'
326
+ );
327
+ });
328
+ });
329
+
330
+ describe('edge cases and resilience', () => {
331
+ it('should handle responses with unusual status codes', async () => {
332
+ const unusualStatusTests = [
333
+ { status: 201, shouldSucceed: true },
334
+ { status: 202, shouldSucceed: true },
335
+ { status: 409, shouldSucceed: false },
336
+ { status: 422, shouldSucceed: false },
337
+ { status: 429, shouldSucceed: false }
338
+ ];
339
+
340
+ for (const test of unusualStatusTests) {
341
+ mockFetch.mockResolvedValueOnce(
342
+ new Response(
343
+ test.shouldSucceed
344
+ ? JSON.stringify({ success: true, status: test.status })
345
+ : JSON.stringify({ error: `Status ${test.status}` }),
346
+ { status: test.status }
347
+ )
348
+ );
349
+
350
+ if (test.shouldSucceed) {
351
+ const result = await client.testRequest<TestStatusResponse>(
352
+ '/api/unusual-status'
353
+ );
354
+ expect(result.success).toBe(true);
355
+ expect(result.status).toBe(test.status);
356
+ } else {
357
+ await expect(
358
+ client.testRequest('/api/unusual-status')
359
+ ).rejects.toThrow();
360
+ }
361
+ }
362
+ });
363
+ });
364
+ });