@cloudflare/sandbox 0.0.0-af082ab → 0.0.0-b61841c

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 (105) hide show
  1. package/CHANGELOG.md +63 -6
  2. package/Dockerfile +91 -51
  3. package/README.md +88 -825
  4. package/dist/chunk-BFVUNTP4.js +104 -0
  5. package/dist/chunk-BFVUNTP4.js.map +1 -0
  6. package/dist/chunk-EKSWCBCA.js +86 -0
  7. package/dist/chunk-EKSWCBCA.js.map +1 -0
  8. package/dist/chunk-JXZMAU2C.js +559 -0
  9. package/dist/chunk-JXZMAU2C.js.map +1 -0
  10. package/dist/chunk-QHRFHK6X.js +7 -0
  11. package/dist/chunk-QHRFHK6X.js.map +1 -0
  12. package/dist/chunk-SFCV5YTY.js +2456 -0
  13. package/dist/chunk-SFCV5YTY.js.map +1 -0
  14. package/dist/chunk-Z532A7QC.js +78 -0
  15. package/dist/chunk-Z532A7QC.js.map +1 -0
  16. package/dist/file-stream.d.ts +43 -0
  17. package/dist/file-stream.js +9 -0
  18. package/dist/file-stream.js.map +1 -0
  19. package/dist/index.d.ts +9 -0
  20. package/dist/index.js +67 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/interpreter.d.ts +33 -0
  23. package/dist/interpreter.js +8 -0
  24. package/dist/interpreter.js.map +1 -0
  25. package/dist/request-handler.d.ts +18 -0
  26. package/dist/request-handler.js +13 -0
  27. package/dist/request-handler.js.map +1 -0
  28. package/dist/sandbox-DWQVgVTY.d.ts +603 -0
  29. package/dist/sandbox.d.ts +4 -0
  30. package/dist/sandbox.js +13 -0
  31. package/dist/sandbox.js.map +1 -0
  32. package/dist/security.d.ts +31 -0
  33. package/dist/security.js +13 -0
  34. package/dist/security.js.map +1 -0
  35. package/dist/sse-parser.d.ts +28 -0
  36. package/dist/sse-parser.js +11 -0
  37. package/dist/sse-parser.js.map +1 -0
  38. package/dist/version.d.ts +8 -0
  39. package/dist/version.js +7 -0
  40. package/dist/version.js.map +1 -0
  41. package/package.json +12 -4
  42. package/src/clients/base-client.ts +280 -0
  43. package/src/clients/command-client.ts +115 -0
  44. package/src/clients/file-client.ts +295 -0
  45. package/src/clients/git-client.ts +92 -0
  46. package/src/clients/index.ts +64 -0
  47. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +148 -171
  48. package/src/clients/port-client.ts +105 -0
  49. package/src/clients/process-client.ts +177 -0
  50. package/src/clients/sandbox-client.ts +41 -0
  51. package/src/clients/types.ts +84 -0
  52. package/src/clients/utility-client.ts +119 -0
  53. package/src/errors/adapter.ts +180 -0
  54. package/src/errors/classes.ts +469 -0
  55. package/src/errors/index.ts +105 -0
  56. package/src/file-stream.ts +119 -117
  57. package/src/index.ts +81 -69
  58. package/src/interpreter.ts +17 -8
  59. package/src/request-handler.ts +80 -44
  60. package/src/sandbox.ts +794 -537
  61. package/src/security.ts +14 -23
  62. package/src/sse-parser.ts +4 -8
  63. package/src/version.ts +6 -0
  64. package/startup.sh +3 -0
  65. package/tests/base-client.test.ts +328 -0
  66. package/tests/command-client.test.ts +407 -0
  67. package/tests/file-client.test.ts +719 -0
  68. package/tests/file-stream.test.ts +306 -0
  69. package/tests/get-sandbox.test.ts +110 -0
  70. package/tests/git-client.test.ts +328 -0
  71. package/tests/port-client.test.ts +301 -0
  72. package/tests/process-client.test.ts +658 -0
  73. package/tests/request-handler.test.ts +240 -0
  74. package/tests/sandbox.test.ts +554 -0
  75. package/tests/sse-parser.test.ts +290 -0
  76. package/tests/utility-client.test.ts +332 -0
  77. package/tests/version.test.ts +16 -0
  78. package/tests/wrangler.jsonc +35 -0
  79. package/tsconfig.json +9 -1
  80. package/vitest.config.ts +31 -0
  81. package/container_src/bun.lock +0 -76
  82. package/container_src/circuit-breaker.ts +0 -121
  83. package/container_src/control-process.ts +0 -784
  84. package/container_src/handler/exec.ts +0 -185
  85. package/container_src/handler/file.ts +0 -457
  86. package/container_src/handler/git.ts +0 -130
  87. package/container_src/handler/ports.ts +0 -314
  88. package/container_src/handler/process.ts +0 -568
  89. package/container_src/handler/session.ts +0 -92
  90. package/container_src/index.ts +0 -601
  91. package/container_src/interpreter-service.ts +0 -276
  92. package/container_src/isolation.ts +0 -1213
  93. package/container_src/mime-processor.ts +0 -255
  94. package/container_src/package.json +0 -18
  95. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  96. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  97. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  98. package/container_src/runtime/process-pool.ts +0 -464
  99. package/container_src/shell-escape.ts +0 -42
  100. package/container_src/startup.sh +0 -11
  101. package/container_src/types.ts +0 -131
  102. package/src/client.ts +0 -1048
  103. package/src/errors.ts +0 -219
  104. package/src/interpreter-types.ts +0 -390
  105. package/src/types.ts +0 -571
@@ -0,0 +1,306 @@
1
+ import type { FileMetadata } from '@repo/shared';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { collectFile, streamFile } from '../src/file-stream';
4
+
5
+ describe('File Streaming Utilities', () => {
6
+ /**
7
+ * Helper to create a mock SSE stream for testing
8
+ */
9
+ function createMockSSEStream(events: string[]): ReadableStream<Uint8Array> {
10
+ return new ReadableStream({
11
+ start(controller) {
12
+ for (const event of events) {
13
+ controller.enqueue(new TextEncoder().encode(event));
14
+ }
15
+ controller.close();
16
+ }
17
+ });
18
+ }
19
+
20
+ describe('streamFile', () => {
21
+ it('should stream text file chunks and return metadata', async () => {
22
+ const stream = createMockSSEStream([
23
+ 'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n',
24
+ 'data: {"type":"chunk","data":"Hello"}\n\n',
25
+ 'data: {"type":"chunk","data":" World"}\n\n',
26
+ 'data: {"type":"complete","bytesRead":11}\n\n',
27
+ ]);
28
+
29
+ const chunks: string[] = [];
30
+ const generator = streamFile(stream);
31
+ let result = await generator.next();
32
+
33
+ // Collect chunks
34
+ while (!result.done) {
35
+ chunks.push(result.value as string);
36
+ result = await generator.next();
37
+ }
38
+
39
+ // Metadata is the return value
40
+ const metadata = result.value;
41
+
42
+ expect(chunks).toEqual(['Hello', ' World']);
43
+ expect(metadata).toEqual({
44
+ mimeType: 'text/plain',
45
+ size: 11,
46
+ isBinary: false,
47
+ encoding: 'utf-8',
48
+ });
49
+ });
50
+
51
+ it('should stream binary file with base64 decoding', async () => {
52
+ // Base64 encoded "test" = "dGVzdA=="
53
+ const stream = createMockSSEStream([
54
+ 'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n',
55
+ 'data: {"type":"chunk","data":"dGVzdA=="}\n\n',
56
+ 'data: {"type":"complete","bytesRead":4}\n\n',
57
+ ]);
58
+
59
+ const chunks: (string | Uint8Array)[] = [];
60
+ const generator = streamFile(stream);
61
+ let result = await generator.next();
62
+
63
+ // Collect chunks
64
+ while (!result.done) {
65
+ chunks.push(result.value);
66
+ result = await generator.next();
67
+ }
68
+
69
+ const metadata = result.value;
70
+
71
+ // For binary files, chunks should be Uint8Array
72
+ expect(chunks.length).toBeGreaterThan(0);
73
+ expect(chunks[0]).toBeInstanceOf(Uint8Array);
74
+
75
+ // Verify we can reconstruct the original data
76
+ const allBytes = new Uint8Array(
77
+ chunks.reduce((acc, chunk) => {
78
+ if (chunk instanceof Uint8Array) {
79
+ return acc + chunk.length;
80
+ }
81
+ return acc;
82
+ }, 0)
83
+ );
84
+
85
+ let offset = 0;
86
+ for (const chunk of chunks) {
87
+ if (chunk instanceof Uint8Array) {
88
+ allBytes.set(chunk, offset);
89
+ offset += chunk.length;
90
+ }
91
+ }
92
+
93
+ const decoded = new TextDecoder().decode(allBytes);
94
+ expect(decoded).toBe('test');
95
+
96
+ expect(metadata?.isBinary).toBe(true);
97
+ expect(metadata?.encoding).toBe('base64');
98
+ expect(metadata?.mimeType).toBe('image/png');
99
+ });
100
+
101
+ it('should handle empty files', async () => {
102
+ const stream = createMockSSEStream([
103
+ 'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n',
104
+ 'data: {"type":"complete","bytesRead":0}\n\n',
105
+ ]);
106
+
107
+ const chunks: string[] = [];
108
+ const generator = streamFile(stream);
109
+ let result = await generator.next();
110
+
111
+ while (!result.done) {
112
+ chunks.push(result.value as string);
113
+ result = await generator.next();
114
+ }
115
+
116
+ const metadata = result.value;
117
+
118
+ expect(chunks).toEqual([]);
119
+ expect(metadata?.size).toBe(0);
120
+ });
121
+
122
+ it('should handle error events', async () => {
123
+ const stream = createMockSSEStream([
124
+ 'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n',
125
+ 'data: {"type":"chunk","data":"Hello"}\n\n',
126
+ 'data: {"type":"error","error":"Read error: Permission denied"}\n\n',
127
+ ]);
128
+
129
+ const generator = streamFile(stream);
130
+
131
+ try {
132
+ let result = await generator.next();
133
+ while (!result.done) {
134
+ result = await generator.next();
135
+ }
136
+ // Should have thrown
137
+ expect(true).toBe(false);
138
+ } catch (error) {
139
+ expect((error as Error).message).toContain('Read error: Permission denied');
140
+ }
141
+ });
142
+ });
143
+
144
+ describe('collectFile', () => {
145
+ it('should collect entire text file into string', async () => {
146
+ const stream = createMockSSEStream([
147
+ 'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n',
148
+ 'data: {"type":"chunk","data":"Hello"}\n\n',
149
+ 'data: {"type":"chunk","data":" World"}\n\n',
150
+ 'data: {"type":"complete","bytesRead":11}\n\n',
151
+ ]);
152
+
153
+ const result = await collectFile(stream);
154
+
155
+ expect(result.content).toBe('Hello World');
156
+ expect(result.metadata).toEqual({
157
+ mimeType: 'text/plain',
158
+ size: 11,
159
+ isBinary: false,
160
+ encoding: 'utf-8',
161
+ });
162
+ });
163
+
164
+ it('should collect entire binary file into Uint8Array', async () => {
165
+ // Base64 encoded "test" = "dGVzdA=="
166
+ const stream = createMockSSEStream([
167
+ 'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n',
168
+ 'data: {"type":"chunk","data":"dGVzdA=="}\n\n',
169
+ 'data: {"type":"complete","bytesRead":4}\n\n',
170
+ ]);
171
+
172
+ const result = await collectFile(stream);
173
+
174
+ expect(result.content).toBeInstanceOf(Uint8Array);
175
+ expect(result.metadata.isBinary).toBe(true);
176
+
177
+ // Decode to verify content
178
+ const decoded = new TextDecoder().decode(result.content as Uint8Array);
179
+ expect(decoded).toBe('test');
180
+ });
181
+
182
+ it('should handle empty files', async () => {
183
+ const stream = createMockSSEStream([
184
+ 'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n',
185
+ 'data: {"type":"complete","bytesRead":0}\n\n',
186
+ ]);
187
+
188
+ const result = await collectFile(stream);
189
+
190
+ expect(result.content).toBe('');
191
+ expect(result.metadata.size).toBe(0);
192
+ });
193
+
194
+ it('should propagate errors from stream', async () => {
195
+ const stream = createMockSSEStream([
196
+ 'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n',
197
+ 'data: {"type":"chunk","data":"Hello"}\n\n',
198
+ 'data: {"type":"error","error":"File not found"}\n\n',
199
+ ]);
200
+
201
+ await expect(collectFile(stream)).rejects.toThrow('File not found');
202
+ });
203
+
204
+ it('should handle large text files efficiently', async () => {
205
+ // Create a stream with many chunks
206
+ const chunkCount = 100;
207
+ const events = [
208
+ 'data: {"type":"metadata","mimeType":"text/plain","size":500,"isBinary":false,"encoding":"utf-8"}\n\n',
209
+ ];
210
+
211
+ for (let i = 0; i < chunkCount; i++) {
212
+ events.push(`data: {"type":"chunk","data":"chunk${i}"}\n\n`);
213
+ }
214
+
215
+ events.push('data: {"type":"complete","bytesRead":500}\n\n');
216
+
217
+ const stream = createMockSSEStream(events);
218
+ const result = await collectFile(stream);
219
+
220
+ expect(typeof result.content).toBe('string');
221
+ expect(result.content).toContain('chunk0');
222
+ expect(result.content).toContain('chunk99');
223
+ expect(result.metadata.encoding).toBe('utf-8');
224
+ });
225
+
226
+ it('should handle large binary files efficiently', async () => {
227
+ // Create a stream with many base64 chunks
228
+ const chunkCount = 100;
229
+ const events = [
230
+ 'data: {"type":"metadata","mimeType":"application/octet-stream","size":400,"isBinary":true,"encoding":"base64"}\n\n',
231
+ ];
232
+
233
+ for (let i = 0; i < chunkCount; i++) {
234
+ // Each "AAAA" base64 chunk decodes to 3 bytes (0x00, 0x00, 0x00)
235
+ events.push('data: {"type":"chunk","data":"AAAA"}\n\n');
236
+ }
237
+
238
+ events.push('data: {"type":"complete","bytesRead":400}\n\n');
239
+
240
+ const stream = createMockSSEStream(events);
241
+ const result = await collectFile(stream);
242
+
243
+ expect(result.content).toBeInstanceOf(Uint8Array);
244
+ expect((result.content as Uint8Array).length).toBeGreaterThan(0);
245
+ expect(result.metadata.isBinary).toBe(true);
246
+ });
247
+ });
248
+
249
+ describe('edge cases', () => {
250
+ it('should handle streams with no metadata event', async () => {
251
+ const stream = createMockSSEStream([
252
+ 'data: {"type":"chunk","data":"Hello"}\n\n',
253
+ 'data: {"type":"complete","bytesRead":5}\n\n',
254
+ ]);
255
+
256
+ // Without metadata, we don't know if it's binary or text
257
+ // The implementation should throw
258
+ const generator = streamFile(stream);
259
+
260
+ try {
261
+ let result = await generator.next();
262
+ while (!result.done) {
263
+ result = await generator.next();
264
+ }
265
+ // Should have thrown
266
+ expect(true).toBe(false);
267
+ } catch (error) {
268
+ expect((error as Error).message).toContain('Received chunk before metadata');
269
+ }
270
+ });
271
+
272
+ it('should handle malformed JSON in SSE events', async () => {
273
+ const stream = createMockSSEStream([
274
+ 'data: {"type":"metadata","mimeType":"text/plain","size":5,"isBinary":false,"encoding":"utf-8"}\n\n',
275
+ 'data: {invalid json\n\n',
276
+ 'data: {"type":"complete","bytesRead":5}\n\n',
277
+ ]);
278
+
279
+ // Malformed JSON is logged but doesn't break the stream
280
+ // It should complete successfully but with no chunks
281
+ const result = await collectFile(stream);
282
+ expect(result.content).toBe('');
283
+ });
284
+
285
+ it('should handle base64 padding correctly', async () => {
286
+ // Test various base64 strings with different padding
287
+ const testCases = [
288
+ { input: 'YQ==', expected: 'a' }, // 1 byte, 2 padding
289
+ { input: 'YWI=', expected: 'ab' }, // 2 bytes, 1 padding
290
+ { input: 'YWJj', expected: 'abc' }, // 3 bytes, no padding
291
+ ];
292
+
293
+ for (const testCase of testCases) {
294
+ const stream = createMockSSEStream([
295
+ `data: {"type":"metadata","mimeType":"application/octet-stream","size":${testCase.expected.length},"isBinary":true,"encoding":"base64"}\n\n`,
296
+ `data: {"type":"chunk","data":"${testCase.input}"}\n\n`,
297
+ `data: {"type":"complete","bytesRead":${testCase.expected.length}}\n\n`,
298
+ ]);
299
+
300
+ const result = await collectFile(stream);
301
+ const decoded = new TextDecoder().decode(result.content as Uint8Array);
302
+ expect(decoded).toBe(testCase.expected);
303
+ }
304
+ });
305
+ });
306
+ });
@@ -0,0 +1,110 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getSandbox } from '../src/sandbox';
3
+
4
+ // Mock the Container module
5
+ vi.mock('@cloudflare/containers', () => ({
6
+ Container: class Container {
7
+ ctx: any;
8
+ env: any;
9
+ sleepAfter: string | number = '10m';
10
+ constructor(ctx: any, env: any) {
11
+ this.ctx = ctx;
12
+ this.env = env;
13
+ }
14
+ },
15
+ getContainer: vi.fn(),
16
+ }));
17
+
18
+ describe('getSandbox', () => {
19
+ let mockStub: any;
20
+ let mockGetContainer: any;
21
+
22
+ beforeEach(async () => {
23
+ vi.clearAllMocks();
24
+
25
+ // Create a fresh mock stub for each test
26
+ mockStub = {
27
+ sleepAfter: '10m',
28
+ setSandboxName: vi.fn(),
29
+ setBaseUrl: vi.fn(),
30
+ setSleepAfter: vi.fn((value: string | number) => {
31
+ mockStub.sleepAfter = value;
32
+ }),
33
+ };
34
+
35
+ // Mock getContainer to return our stub
36
+ const containers = await import('@cloudflare/containers');
37
+ mockGetContainer = vi.mocked(containers.getContainer);
38
+ mockGetContainer.mockReturnValue(mockStub);
39
+ });
40
+
41
+ it('should create a sandbox instance with default sleepAfter', () => {
42
+ const mockNamespace = {} as any;
43
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox');
44
+
45
+ expect(sandbox).toBeDefined();
46
+ expect(sandbox.setSandboxName).toHaveBeenCalledWith('test-sandbox');
47
+ });
48
+
49
+ it('should apply sleepAfter option when provided as string', () => {
50
+ const mockNamespace = {} as any;
51
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
52
+ sleepAfter: '5m',
53
+ });
54
+
55
+ expect(sandbox.sleepAfter).toBe('5m');
56
+ });
57
+
58
+ it('should apply sleepAfter option when provided as number', () => {
59
+ const mockNamespace = {} as any;
60
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
61
+ sleepAfter: 300, // 5 minutes in seconds
62
+ });
63
+
64
+ expect(sandbox.sleepAfter).toBe(300);
65
+ });
66
+
67
+ it('should apply baseUrl option when provided', () => {
68
+ const mockNamespace = {} as any;
69
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
70
+ baseUrl: 'https://example.com',
71
+ });
72
+
73
+ expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
74
+ });
75
+
76
+ it('should apply both sleepAfter and baseUrl options together', () => {
77
+ const mockNamespace = {} as any;
78
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
79
+ sleepAfter: '10m',
80
+ baseUrl: 'https://example.com',
81
+ });
82
+
83
+ expect(sandbox.sleepAfter).toBe('10m');
84
+ expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
85
+ });
86
+
87
+ it('should not apply sleepAfter when not provided', () => {
88
+ const mockNamespace = {} as any;
89
+ const sandbox = getSandbox(mockNamespace, 'test-sandbox');
90
+
91
+ // Should remain default value from Container
92
+ expect(sandbox.sleepAfter).toBe('10m');
93
+ });
94
+
95
+ it('should accept various time string formats for sleepAfter', () => {
96
+ const mockNamespace = {} as any;
97
+ const testCases = ['30s', '1m', '10m', '1h', '2h'];
98
+
99
+ for (const timeString of testCases) {
100
+ // Reset the mock stub for each iteration
101
+ mockStub.sleepAfter = '3m';
102
+
103
+ const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, {
104
+ sleepAfter: timeString,
105
+ });
106
+
107
+ expect(sandbox.sleepAfter).toBe(timeString);
108
+ }
109
+ });
110
+ });