@cloudflare/sandbox 0.5.4 → 0.6.0

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 (57) hide show
  1. package/Dockerfile +54 -59
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +13 -8
  8. package/.turbo/turbo-build.log +0 -23
  9. package/CHANGELOG.md +0 -441
  10. package/src/clients/base-client.ts +0 -356
  11. package/src/clients/command-client.ts +0 -133
  12. package/src/clients/file-client.ts +0 -300
  13. package/src/clients/git-client.ts +0 -98
  14. package/src/clients/index.ts +0 -64
  15. package/src/clients/interpreter-client.ts +0 -333
  16. package/src/clients/port-client.ts +0 -105
  17. package/src/clients/process-client.ts +0 -198
  18. package/src/clients/sandbox-client.ts +0 -39
  19. package/src/clients/types.ts +0 -88
  20. package/src/clients/utility-client.ts +0 -156
  21. package/src/errors/adapter.ts +0 -238
  22. package/src/errors/classes.ts +0 -594
  23. package/src/errors/index.ts +0 -109
  24. package/src/file-stream.ts +0 -169
  25. package/src/index.ts +0 -121
  26. package/src/interpreter.ts +0 -168
  27. package/src/openai/index.ts +0 -465
  28. package/src/request-handler.ts +0 -184
  29. package/src/sandbox.ts +0 -1937
  30. package/src/security.ts +0 -119
  31. package/src/sse-parser.ts +0 -144
  32. package/src/storage-mount/credential-detection.ts +0 -41
  33. package/src/storage-mount/errors.ts +0 -51
  34. package/src/storage-mount/index.ts +0 -17
  35. package/src/storage-mount/provider-detection.ts +0 -93
  36. package/src/storage-mount/types.ts +0 -17
  37. package/src/version.ts +0 -6
  38. package/tests/base-client.test.ts +0 -582
  39. package/tests/command-client.test.ts +0 -444
  40. package/tests/file-client.test.ts +0 -831
  41. package/tests/file-stream.test.ts +0 -310
  42. package/tests/get-sandbox.test.ts +0 -172
  43. package/tests/git-client.test.ts +0 -455
  44. package/tests/openai-shell-editor.test.ts +0 -434
  45. package/tests/port-client.test.ts +0 -283
  46. package/tests/process-client.test.ts +0 -649
  47. package/tests/request-handler.test.ts +0 -292
  48. package/tests/sandbox.test.ts +0 -890
  49. package/tests/sse-parser.test.ts +0 -291
  50. package/tests/storage-mount/credential-detection.test.ts +0 -119
  51. package/tests/storage-mount/provider-detection.test.ts +0 -77
  52. package/tests/utility-client.test.ts +0 -339
  53. package/tests/version.test.ts +0 -16
  54. package/tests/wrangler.jsonc +0 -35
  55. package/tsconfig.json +0 -11
  56. package/tsdown.config.ts +0 -13
  57. package/vitest.config.ts +0 -31
@@ -1,310 +0,0 @@
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
- });
@@ -1,172 +0,0 @@
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(
48
- 'test-sandbox',
49
- undefined
50
- );
51
- });
52
-
53
- it('should apply sleepAfter option when provided as string', () => {
54
- const mockNamespace = {} as any;
55
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
56
- sleepAfter: '5m'
57
- });
58
-
59
- expect(sandbox.sleepAfter).toBe('5m');
60
- });
61
-
62
- it('should apply sleepAfter option when provided as number', () => {
63
- const mockNamespace = {} as any;
64
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
65
- sleepAfter: 300 // 5 minutes in seconds
66
- });
67
-
68
- expect(sandbox.sleepAfter).toBe(300);
69
- });
70
-
71
- it('should apply baseUrl option when provided', () => {
72
- const mockNamespace = {} as any;
73
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
74
- baseUrl: 'https://example.com'
75
- });
76
-
77
- expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
78
- });
79
-
80
- it('should apply both sleepAfter and baseUrl options together', () => {
81
- const mockNamespace = {} as any;
82
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
83
- sleepAfter: '10m',
84
- baseUrl: 'https://example.com'
85
- });
86
-
87
- expect(sandbox.sleepAfter).toBe('10m');
88
- expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
89
- });
90
-
91
- it('should not apply sleepAfter when not provided', () => {
92
- const mockNamespace = {} as any;
93
- const sandbox = getSandbox(mockNamespace, 'test-sandbox');
94
-
95
- // Should remain default value from Container
96
- expect(sandbox.sleepAfter).toBe('10m');
97
- });
98
-
99
- it('should accept various time string formats for sleepAfter', () => {
100
- const mockNamespace = {} as any;
101
- const testCases = ['30s', '1m', '10m', '1h', '2h'];
102
-
103
- for (const timeString of testCases) {
104
- // Reset the mock stub for each iteration
105
- mockStub.sleepAfter = '3m';
106
-
107
- const sandbox = getSandbox(mockNamespace, `test-sandbox-${timeString}`, {
108
- sleepAfter: timeString
109
- });
110
-
111
- expect(sandbox.sleepAfter).toBe(timeString);
112
- }
113
- });
114
-
115
- it('should apply keepAlive option when provided as true', () => {
116
- const mockNamespace = {} as any;
117
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
118
- keepAlive: true
119
- });
120
-
121
- expect(sandbox.setKeepAlive).toHaveBeenCalledWith(true);
122
- });
123
-
124
- it('should apply keepAlive option when provided as false', () => {
125
- const mockNamespace = {} as any;
126
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
127
- keepAlive: false
128
- });
129
-
130
- expect(sandbox.setKeepAlive).toHaveBeenCalledWith(false);
131
- });
132
-
133
- it('should not call setKeepAlive when keepAlive option not provided', () => {
134
- const mockNamespace = {} as any;
135
- getSandbox(mockNamespace, 'test-sandbox');
136
-
137
- expect(mockStub.setKeepAlive).not.toHaveBeenCalled();
138
- });
139
-
140
- it('should apply keepAlive alongside other options', () => {
141
- const mockNamespace = {} as any;
142
- const sandbox = getSandbox(mockNamespace, 'test-sandbox', {
143
- sleepAfter: '5m',
144
- baseUrl: 'https://example.com',
145
- keepAlive: true
146
- });
147
-
148
- expect(sandbox.sleepAfter).toBe('5m');
149
- expect(sandbox.setBaseUrl).toHaveBeenCalledWith('https://example.com');
150
- expect(sandbox.setKeepAlive).toHaveBeenCalledWith(true);
151
- });
152
-
153
- it('should preserve sandbox ID case by default', () => {
154
- const mockNamespace = {} as any;
155
- getSandbox(mockNamespace, 'MyProject-ABC123');
156
-
157
- expect(mockGetContainer).toHaveBeenCalledWith(
158
- mockNamespace,
159
- 'MyProject-ABC123'
160
- );
161
- });
162
-
163
- it('should normalize sandbox ID to lowercase when normalizeId option is true', () => {
164
- const mockNamespace = {} as any;
165
- getSandbox(mockNamespace, 'MyProject-ABC123', { normalizeId: true });
166
-
167
- expect(mockGetContainer).toHaveBeenCalledWith(
168
- mockNamespace,
169
- 'myproject-abc123'
170
- );
171
- });
172
- });