@cloudflare/sandbox 0.0.0-aa00a75 → 0.0.0-aeba44f

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 (76) hide show
  1. package/CHANGELOG.md +158 -15
  2. package/Dockerfile +88 -71
  3. package/LICENSE +176 -0
  4. package/README.md +10 -5
  5. package/dist/index.d.ts +1953 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3278 -53
  8. package/dist/index.js.map +1 -1
  9. package/package.json +11 -9
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +51 -20
  13. package/src/clients/git-client.ts +9 -3
  14. package/src/clients/index.ts +16 -15
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +67 -5
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +28 -17
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +34 -21
  28. package/src/sandbox.ts +502 -151
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +6 -0
  32. package/startup.sh +1 -1
  33. package/tests/base-client.test.ts +116 -80
  34. package/tests/command-client.test.ts +149 -112
  35. package/tests/file-client.test.ts +373 -185
  36. package/tests/file-stream.test.ts +24 -20
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +260 -101
  39. package/tests/port-client.test.ts +100 -108
  40. package/tests/process-client.test.ts +204 -179
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +336 -62
  43. package/tests/sse-parser.test.ts +17 -16
  44. package/tests/utility-client.test.ts +129 -56
  45. package/tests/version.test.ts +16 -0
  46. package/tsdown.config.ts +12 -0
  47. package/vitest.config.ts +6 -6
  48. package/dist/chunk-BCJ7SF3Q.js +0 -117
  49. package/dist/chunk-BCJ7SF3Q.js.map +0 -1
  50. package/dist/chunk-BFVUNTP4.js +0 -104
  51. package/dist/chunk-BFVUNTP4.js.map +0 -1
  52. package/dist/chunk-EKSWCBCA.js +0 -86
  53. package/dist/chunk-EKSWCBCA.js.map +0 -1
  54. package/dist/chunk-HGF554LH.js +0 -2236
  55. package/dist/chunk-HGF554LH.js.map +0 -1
  56. package/dist/chunk-Z532A7QC.js +0 -78
  57. package/dist/chunk-Z532A7QC.js.map +0 -1
  58. package/dist/file-stream.d.ts +0 -43
  59. package/dist/file-stream.js +0 -9
  60. package/dist/file-stream.js.map +0 -1
  61. package/dist/interpreter.d.ts +0 -33
  62. package/dist/interpreter.js +0 -8
  63. package/dist/interpreter.js.map +0 -1
  64. package/dist/request-handler.d.ts +0 -18
  65. package/dist/request-handler.js +0 -12
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-D9K2ypln.d.ts +0 -583
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -12
  70. package/dist/sandbox.js.map +0 -1
  71. package/dist/security.d.ts +0 -31
  72. package/dist/security.js +0 -13
  73. package/dist/security.js.map +0 -1
  74. package/dist/sse-parser.d.ts +0 -28
  75. package/dist/sse-parser.js +0 -11
  76. package/dist/sse-parser.js.map +0 -1
@@ -23,7 +23,7 @@ describe('File Streaming Utilities', () => {
23
23
  'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n',
24
24
  'data: {"type":"chunk","data":"Hello"}\n\n',
25
25
  'data: {"type":"chunk","data":" World"}\n\n',
26
- 'data: {"type":"complete","bytesRead":11}\n\n',
26
+ 'data: {"type":"complete","bytesRead":11}\n\n'
27
27
  ]);
28
28
 
29
29
  const chunks: string[] = [];
@@ -44,7 +44,7 @@ describe('File Streaming Utilities', () => {
44
44
  mimeType: 'text/plain',
45
45
  size: 11,
46
46
  isBinary: false,
47
- encoding: 'utf-8',
47
+ encoding: 'utf-8'
48
48
  });
49
49
  });
50
50
 
@@ -53,7 +53,7 @@ describe('File Streaming Utilities', () => {
53
53
  const stream = createMockSSEStream([
54
54
  'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n',
55
55
  'data: {"type":"chunk","data":"dGVzdA=="}\n\n',
56
- 'data: {"type":"complete","bytesRead":4}\n\n',
56
+ 'data: {"type":"complete","bytesRead":4}\n\n'
57
57
  ]);
58
58
 
59
59
  const chunks: (string | Uint8Array)[] = [];
@@ -101,7 +101,7 @@ describe('File Streaming Utilities', () => {
101
101
  it('should handle empty files', async () => {
102
102
  const stream = createMockSSEStream([
103
103
  'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n',
104
- 'data: {"type":"complete","bytesRead":0}\n\n',
104
+ 'data: {"type":"complete","bytesRead":0}\n\n'
105
105
  ]);
106
106
 
107
107
  const chunks: string[] = [];
@@ -123,7 +123,7 @@ describe('File Streaming Utilities', () => {
123
123
  const stream = createMockSSEStream([
124
124
  'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n',
125
125
  'data: {"type":"chunk","data":"Hello"}\n\n',
126
- 'data: {"type":"error","error":"Read error: Permission denied"}\n\n',
126
+ 'data: {"type":"error","error":"Read error: Permission denied"}\n\n'
127
127
  ]);
128
128
 
129
129
  const generator = streamFile(stream);
@@ -136,7 +136,9 @@ describe('File Streaming Utilities', () => {
136
136
  // Should have thrown
137
137
  expect(true).toBe(false);
138
138
  } catch (error) {
139
- expect((error as Error).message).toContain('Read error: Permission denied');
139
+ expect((error as Error).message).toContain(
140
+ 'Read error: Permission denied'
141
+ );
140
142
  }
141
143
  });
142
144
  });
@@ -147,7 +149,7 @@ describe('File Streaming Utilities', () => {
147
149
  'data: {"type":"metadata","mimeType":"text/plain","size":11,"isBinary":false,"encoding":"utf-8"}\n\n',
148
150
  'data: {"type":"chunk","data":"Hello"}\n\n',
149
151
  'data: {"type":"chunk","data":" World"}\n\n',
150
- 'data: {"type":"complete","bytesRead":11}\n\n',
152
+ 'data: {"type":"complete","bytesRead":11}\n\n'
151
153
  ]);
152
154
 
153
155
  const result = await collectFile(stream);
@@ -157,7 +159,7 @@ describe('File Streaming Utilities', () => {
157
159
  mimeType: 'text/plain',
158
160
  size: 11,
159
161
  isBinary: false,
160
- encoding: 'utf-8',
162
+ encoding: 'utf-8'
161
163
  });
162
164
  });
163
165
 
@@ -166,7 +168,7 @@ describe('File Streaming Utilities', () => {
166
168
  const stream = createMockSSEStream([
167
169
  'data: {"type":"metadata","mimeType":"image/png","size":4,"isBinary":true,"encoding":"base64"}\n\n',
168
170
  'data: {"type":"chunk","data":"dGVzdA=="}\n\n',
169
- 'data: {"type":"complete","bytesRead":4}\n\n',
171
+ 'data: {"type":"complete","bytesRead":4}\n\n'
170
172
  ]);
171
173
 
172
174
  const result = await collectFile(stream);
@@ -182,7 +184,7 @@ describe('File Streaming Utilities', () => {
182
184
  it('should handle empty files', async () => {
183
185
  const stream = createMockSSEStream([
184
186
  'data: {"type":"metadata","mimeType":"text/plain","size":0,"isBinary":false,"encoding":"utf-8"}\n\n',
185
- 'data: {"type":"complete","bytesRead":0}\n\n',
187
+ 'data: {"type":"complete","bytesRead":0}\n\n'
186
188
  ]);
187
189
 
188
190
  const result = await collectFile(stream);
@@ -195,7 +197,7 @@ describe('File Streaming Utilities', () => {
195
197
  const stream = createMockSSEStream([
196
198
  'data: {"type":"metadata","mimeType":"text/plain","size":100,"isBinary":false,"encoding":"utf-8"}\n\n',
197
199
  'data: {"type":"chunk","data":"Hello"}\n\n',
198
- 'data: {"type":"error","error":"File not found"}\n\n',
200
+ 'data: {"type":"error","error":"File not found"}\n\n'
199
201
  ]);
200
202
 
201
203
  await expect(collectFile(stream)).rejects.toThrow('File not found');
@@ -205,7 +207,7 @@ describe('File Streaming Utilities', () => {
205
207
  // Create a stream with many chunks
206
208
  const chunkCount = 100;
207
209
  const events = [
208
- 'data: {"type":"metadata","mimeType":"text/plain","size":500,"isBinary":false,"encoding":"utf-8"}\n\n',
210
+ 'data: {"type":"metadata","mimeType":"text/plain","size":500,"isBinary":false,"encoding":"utf-8"}\n\n'
209
211
  ];
210
212
 
211
213
  for (let i = 0; i < chunkCount; i++) {
@@ -227,7 +229,7 @@ describe('File Streaming Utilities', () => {
227
229
  // Create a stream with many base64 chunks
228
230
  const chunkCount = 100;
229
231
  const events = [
230
- 'data: {"type":"metadata","mimeType":"application/octet-stream","size":400,"isBinary":true,"encoding":"base64"}\n\n',
232
+ 'data: {"type":"metadata","mimeType":"application/octet-stream","size":400,"isBinary":true,"encoding":"base64"}\n\n'
231
233
  ];
232
234
 
233
235
  for (let i = 0; i < chunkCount; i++) {
@@ -250,7 +252,7 @@ describe('File Streaming Utilities', () => {
250
252
  it('should handle streams with no metadata event', async () => {
251
253
  const stream = createMockSSEStream([
252
254
  'data: {"type":"chunk","data":"Hello"}\n\n',
253
- 'data: {"type":"complete","bytesRead":5}\n\n',
255
+ 'data: {"type":"complete","bytesRead":5}\n\n'
254
256
  ]);
255
257
 
256
258
  // Without metadata, we don't know if it's binary or text
@@ -265,7 +267,9 @@ describe('File Streaming Utilities', () => {
265
267
  // Should have thrown
266
268
  expect(true).toBe(false);
267
269
  } catch (error) {
268
- expect((error as Error).message).toContain('Received chunk before metadata');
270
+ expect((error as Error).message).toContain(
271
+ 'Received chunk before metadata'
272
+ );
269
273
  }
270
274
  });
271
275
 
@@ -273,7 +277,7 @@ describe('File Streaming Utilities', () => {
273
277
  const stream = createMockSSEStream([
274
278
  'data: {"type":"metadata","mimeType":"text/plain","size":5,"isBinary":false,"encoding":"utf-8"}\n\n',
275
279
  'data: {invalid json\n\n',
276
- 'data: {"type":"complete","bytesRead":5}\n\n',
280
+ 'data: {"type":"complete","bytesRead":5}\n\n'
277
281
  ]);
278
282
 
279
283
  // Malformed JSON is logged but doesn't break the stream
@@ -285,16 +289,16 @@ describe('File Streaming Utilities', () => {
285
289
  it('should handle base64 padding correctly', async () => {
286
290
  // Test various base64 strings with different padding
287
291
  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
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
291
295
  ];
292
296
 
293
297
  for (const testCase of testCases) {
294
298
  const stream = createMockSSEStream([
295
299
  `data: {"type":"metadata","mimeType":"application/octet-stream","size":${testCase.expected.length},"isBinary":true,"encoding":"base64"}\n\n`,
296
300
  `data: {"type":"chunk","data":"${testCase.input}"}\n\n`,
297
- `data: {"type":"complete","bytesRead":${testCase.expected.length}}\n\n`,
301
+ `data: {"type":"complete","bytesRead":${testCase.expected.length}}\n\n`
298
302
  ]);
299
303
 
300
304
  const result = await collectFile(stream);
@@ -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
+ });