@cloudflare/sandbox 0.5.6 → 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 (56) hide show
  1. package/Dockerfile +54 -56
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -1
  5. package/dist/index.js.map +1 -1
  6. package/package.json +11 -6
  7. package/.turbo/turbo-build.log +0 -23
  8. package/CHANGELOG.md +0 -463
  9. package/src/clients/base-client.ts +0 -356
  10. package/src/clients/command-client.ts +0 -133
  11. package/src/clients/file-client.ts +0 -300
  12. package/src/clients/git-client.ts +0 -98
  13. package/src/clients/index.ts +0 -64
  14. package/src/clients/interpreter-client.ts +0 -339
  15. package/src/clients/port-client.ts +0 -105
  16. package/src/clients/process-client.ts +0 -198
  17. package/src/clients/sandbox-client.ts +0 -39
  18. package/src/clients/types.ts +0 -88
  19. package/src/clients/utility-client.ts +0 -156
  20. package/src/errors/adapter.ts +0 -238
  21. package/src/errors/classes.ts +0 -594
  22. package/src/errors/index.ts +0 -109
  23. package/src/file-stream.ts +0 -175
  24. package/src/index.ts +0 -121
  25. package/src/interpreter.ts +0 -168
  26. package/src/openai/index.ts +0 -465
  27. package/src/request-handler.ts +0 -184
  28. package/src/sandbox.ts +0 -1937
  29. package/src/security.ts +0 -119
  30. package/src/sse-parser.ts +0 -147
  31. package/src/storage-mount/credential-detection.ts +0 -41
  32. package/src/storage-mount/errors.ts +0 -51
  33. package/src/storage-mount/index.ts +0 -17
  34. package/src/storage-mount/provider-detection.ts +0 -93
  35. package/src/storage-mount/types.ts +0 -17
  36. package/src/version.ts +0 -6
  37. package/tests/base-client.test.ts +0 -582
  38. package/tests/command-client.test.ts +0 -444
  39. package/tests/file-client.test.ts +0 -831
  40. package/tests/file-stream.test.ts +0 -310
  41. package/tests/get-sandbox.test.ts +0 -172
  42. package/tests/git-client.test.ts +0 -455
  43. package/tests/openai-shell-editor.test.ts +0 -434
  44. package/tests/port-client.test.ts +0 -283
  45. package/tests/process-client.test.ts +0 -649
  46. package/tests/request-handler.test.ts +0 -292
  47. package/tests/sandbox.test.ts +0 -890
  48. package/tests/sse-parser.test.ts +0 -291
  49. package/tests/storage-mount/credential-detection.test.ts +0 -119
  50. package/tests/storage-mount/provider-detection.test.ts +0 -77
  51. package/tests/utility-client.test.ts +0 -339
  52. package/tests/version.test.ts +0 -16
  53. package/tests/wrangler.jsonc +0 -35
  54. package/tsconfig.json +0 -11
  55. package/tsdown.config.ts +0 -13
  56. 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
- });