@cloudflare/sandbox 0.0.0-feafd32 → 0.0.0-ff2fa91

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