@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,582 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import type { BaseApiResponse, HttpClientOptions } from '../src/clients';
3
- import { BaseHttpClient } from '../src/clients/base-client';
4
- import type { ErrorResponse } from '../src/errors';
5
- import {
6
- CommandError,
7
- FileNotFoundError,
8
- FileSystemError,
9
- PermissionDeniedError,
10
- SandboxError
11
- } from '../src/errors';
12
-
13
- interface TestDataResponse extends BaseApiResponse {
14
- data: string;
15
- }
16
-
17
- interface TestResourceResponse extends BaseApiResponse {
18
- id: string;
19
- }
20
-
21
- interface TestSourceResponse extends BaseApiResponse {
22
- source: string;
23
- }
24
-
25
- interface TestStatusResponse extends BaseApiResponse {
26
- status: string;
27
- }
28
-
29
- class TestHttpClient extends BaseHttpClient {
30
- constructor(options: HttpClientOptions = {}) {
31
- super({
32
- baseUrl: 'http://test.com',
33
- port: 3000,
34
- ...options
35
- });
36
- }
37
-
38
- public async testRequest<T = BaseApiResponse>(
39
- endpoint: string,
40
- data?: Record<string, unknown>
41
- ): Promise<T> {
42
- if (data) {
43
- return this.post<T>(endpoint, data);
44
- }
45
- return this.get<T>(endpoint);
46
- }
47
-
48
- public async testStreamRequest(endpoint: string): Promise<ReadableStream> {
49
- const response = await this.doFetch(endpoint);
50
- return this.handleStreamResponse(response);
51
- }
52
-
53
- public async testErrorHandling(errorResponse: ErrorResponse) {
54
- const response = new Response(JSON.stringify(errorResponse), {
55
- status: errorResponse.httpStatus || 400
56
- });
57
- return this.handleErrorResponse(response);
58
- }
59
- }
60
-
61
- describe('BaseHttpClient', () => {
62
- let client: TestHttpClient;
63
- let mockFetch: ReturnType<typeof vi.fn>;
64
- let onError: ReturnType<typeof vi.fn>;
65
-
66
- beforeEach(() => {
67
- vi.clearAllMocks();
68
-
69
- mockFetch = vi.fn();
70
- global.fetch = mockFetch as unknown as typeof fetch;
71
- onError = vi.fn();
72
-
73
- client = new TestHttpClient({
74
- baseUrl: 'http://test.com',
75
- port: 3000,
76
- onError
77
- });
78
- });
79
-
80
- afterEach(() => {
81
- vi.restoreAllMocks();
82
- });
83
-
84
- describe('core request functionality', () => {
85
- it('should handle successful API requests', async () => {
86
- mockFetch.mockResolvedValue(
87
- new Response(
88
- JSON.stringify({ success: true, data: 'operation completed' }),
89
- { status: 200 }
90
- )
91
- );
92
-
93
- const result = await client.testRequest<TestDataResponse>('/api/test');
94
-
95
- expect(result.success).toBe(true);
96
- expect(result.data).toBe('operation completed');
97
- });
98
-
99
- it('should handle POST requests with data', async () => {
100
- const requestData = { action: 'create', name: 'test-resource' };
101
- mockFetch.mockResolvedValue(
102
- new Response(JSON.stringify({ success: true, id: 'resource-123' }), {
103
- status: 201
104
- })
105
- );
106
-
107
- const result = await client.testRequest<TestResourceResponse>(
108
- '/api/create',
109
- requestData
110
- );
111
-
112
- expect(result.success).toBe(true);
113
- expect(result.id).toBe('resource-123');
114
-
115
- const [url, options] = mockFetch.mock.calls[0];
116
- expect(url).toBe('http://test.com/api/create');
117
- expect(options.method).toBe('POST');
118
- expect(options.headers['Content-Type']).toBe('application/json');
119
- expect(JSON.parse(options.body)).toEqual(requestData);
120
- });
121
- });
122
-
123
- describe('error handling and mapping', () => {
124
- it('should map container errors to client errors', async () => {
125
- const errorMappingTests = [
126
- {
127
- containerError: {
128
- code: 'FILE_NOT_FOUND',
129
- message: 'File not found: /test.txt',
130
- context: { path: '/test.txt' },
131
- httpStatus: 404,
132
- timestamp: new Date().toISOString()
133
- },
134
- expectedError: FileNotFoundError
135
- },
136
- {
137
- containerError: {
138
- code: 'PERMISSION_DENIED',
139
- message: 'Permission denied',
140
- context: { path: '/secure.txt' },
141
- httpStatus: 403,
142
- timestamp: new Date().toISOString()
143
- },
144
- expectedError: PermissionDeniedError
145
- },
146
- {
147
- containerError: {
148
- code: 'COMMAND_EXECUTION_ERROR',
149
- message: 'Command failed: badcmd',
150
- context: { command: 'badcmd' },
151
- httpStatus: 400,
152
- timestamp: new Date().toISOString()
153
- },
154
- expectedError: CommandError
155
- },
156
- {
157
- containerError: {
158
- code: 'FILESYSTEM_ERROR',
159
- message: 'Filesystem error',
160
- context: { path: '/test' },
161
- httpStatus: 500,
162
- timestamp: new Date().toISOString()
163
- },
164
- expectedError: FileSystemError
165
- },
166
- {
167
- containerError: {
168
- code: 'UNKNOWN_ERROR',
169
- message: 'Unknown error',
170
- context: {},
171
- httpStatus: 500,
172
- timestamp: new Date().toISOString()
173
- },
174
- expectedError: SandboxError
175
- }
176
- ];
177
-
178
- for (const test of errorMappingTests) {
179
- await expect(
180
- client.testErrorHandling(test.containerError as ErrorResponse)
181
- ).rejects.toThrow(test.expectedError);
182
-
183
- expect(onError).toHaveBeenCalledWith(
184
- test.containerError.message,
185
- undefined
186
- );
187
- }
188
- });
189
-
190
- it('should handle malformed error responses', async () => {
191
- mockFetch.mockResolvedValue(
192
- new Response('invalid json {', { status: 500 })
193
- );
194
-
195
- await expect(client.testRequest('/api/test')).rejects.toThrow(
196
- SandboxError
197
- );
198
- });
199
-
200
- it('should handle network failures', async () => {
201
- mockFetch.mockRejectedValue(new Error('Network connection timeout'));
202
-
203
- await expect(client.testRequest('/api/test')).rejects.toThrow(
204
- 'Network connection timeout'
205
- );
206
- });
207
-
208
- it('should handle server unavailable scenarios', async () => {
209
- mockFetch.mockResolvedValue(
210
- new Response('Service Unavailable', { status: 503 })
211
- );
212
-
213
- await expect(client.testRequest('/api/test')).rejects.toThrow(
214
- SandboxError
215
- );
216
-
217
- expect(onError).toHaveBeenCalledWith(
218
- 'HTTP error! status: 503',
219
- undefined
220
- );
221
- });
222
- });
223
-
224
- describe('streaming functionality', () => {
225
- it('should handle streaming responses', async () => {
226
- const streamData = 'data: {"type":"output","content":"stream data"}\n\n';
227
- const mockStream = new ReadableStream({
228
- start(controller) {
229
- controller.enqueue(new TextEncoder().encode(streamData));
230
- controller.close();
231
- }
232
- });
233
-
234
- mockFetch.mockResolvedValue(
235
- new Response(mockStream, {
236
- status: 200,
237
- headers: { 'Content-Type': 'text/event-stream' }
238
- })
239
- );
240
-
241
- const stream = await client.testStreamRequest('/api/stream');
242
-
243
- expect(stream).toBeInstanceOf(ReadableStream);
244
-
245
- const reader = stream.getReader();
246
- const { done, value } = await reader.read();
247
- const content = new TextDecoder().decode(value);
248
-
249
- expect(done).toBe(false);
250
- expect(content).toContain('stream data');
251
-
252
- reader.releaseLock();
253
- });
254
-
255
- it('should handle streaming errors', async () => {
256
- mockFetch.mockResolvedValue(
257
- new Response(
258
- JSON.stringify({
259
- error: 'Stream initialization failed',
260
- code: 'STREAM_ERROR'
261
- }),
262
- { status: 400 }
263
- )
264
- );
265
-
266
- await expect(client.testStreamRequest('/api/bad-stream')).rejects.toThrow(
267
- SandboxError
268
- );
269
- });
270
-
271
- it('should handle missing stream body', async () => {
272
- mockFetch.mockResolvedValue(
273
- new Response(null, {
274
- status: 200,
275
- headers: { 'Content-Type': 'text/event-stream' }
276
- })
277
- );
278
-
279
- await expect(
280
- client.testStreamRequest('/api/empty-stream')
281
- ).rejects.toThrow('No response body for streaming');
282
- });
283
- });
284
-
285
- describe('stub integration', () => {
286
- it('should use stub when provided instead of fetch', async () => {
287
- const stubFetch = vi.fn().mockResolvedValue(
288
- new Response(JSON.stringify({ success: true, source: 'stub' }), {
289
- status: 200
290
- })
291
- );
292
-
293
- const stub = { containerFetch: stubFetch };
294
- const stubClient = new TestHttpClient({
295
- baseUrl: 'http://test.com',
296
- port: 3000,
297
- stub
298
- });
299
-
300
- const result =
301
- await stubClient.testRequest<TestSourceResponse>('/api/stub-test');
302
-
303
- expect(result.success).toBe(true);
304
- expect(result.source).toBe('stub');
305
- expect(stubFetch).toHaveBeenCalledWith(
306
- 'http://localhost:3000/api/stub-test',
307
- { method: 'GET' },
308
- 3000
309
- );
310
- expect(mockFetch).not.toHaveBeenCalled();
311
- });
312
-
313
- it('should handle stub errors', async () => {
314
- const stubFetch = vi
315
- .fn()
316
- .mockRejectedValue(new Error('Stub connection failed'));
317
- const stub = { containerFetch: stubFetch };
318
- const stubClient = new TestHttpClient({
319
- baseUrl: 'http://test.com',
320
- port: 3000,
321
- stub
322
- });
323
-
324
- await expect(stubClient.testRequest('/api/stub-error')).rejects.toThrow(
325
- 'Stub connection failed'
326
- );
327
- });
328
- });
329
-
330
- describe('edge cases and resilience', () => {
331
- it('should handle responses with unusual status codes', async () => {
332
- const unusualStatusTests = [
333
- { status: 201, shouldSucceed: true },
334
- { status: 202, shouldSucceed: true },
335
- { status: 409, shouldSucceed: false },
336
- { status: 422, shouldSucceed: false },
337
- { status: 429, shouldSucceed: false }
338
- ];
339
-
340
- for (const test of unusualStatusTests) {
341
- mockFetch.mockResolvedValueOnce(
342
- new Response(
343
- test.shouldSucceed
344
- ? JSON.stringify({ success: true, status: test.status })
345
- : JSON.stringify({ error: `Status ${test.status}` }),
346
- { status: test.status }
347
- )
348
- );
349
-
350
- if (test.shouldSucceed) {
351
- const result = await client.testRequest<TestStatusResponse>(
352
- '/api/unusual-status'
353
- );
354
- expect(result.success).toBe(true);
355
- expect(result.status).toBe(test.status);
356
- } else {
357
- await expect(
358
- client.testRequest('/api/unusual-status')
359
- ).rejects.toThrow();
360
- }
361
- }
362
- });
363
- });
364
-
365
- describe('container startup retry logic', () => {
366
- it('should retry 503 errors with "no container instance available"', async () => {
367
- vi.useFakeTimers();
368
-
369
- mockFetch
370
- .mockResolvedValueOnce(
371
- new Response('Error: There is no container instance available', {
372
- status: 503
373
- })
374
- )
375
- .mockResolvedValueOnce(
376
- new Response(JSON.stringify({ success: true, data: 'recovered' }), {
377
- status: 200
378
- })
379
- );
380
-
381
- const promise = client.testRequest<TestDataResponse>('/api/test');
382
- await vi.advanceTimersByTimeAsync(5_000);
383
- const result = await promise;
384
-
385
- expect(mockFetch).toHaveBeenCalledTimes(2);
386
- expect(result.success).toBe(true);
387
- expect(result.data).toBe('recovered');
388
-
389
- vi.useRealTimers();
390
- });
391
-
392
- it('should retry 500 errors with "container port not found"', async () => {
393
- vi.useFakeTimers();
394
-
395
- mockFetch
396
- .mockResolvedValueOnce(
397
- new Response('Connection refused: container port not found', {
398
- status: 500
399
- })
400
- )
401
- .mockResolvedValueOnce(
402
- new Response(JSON.stringify({ success: true }), { status: 200 })
403
- );
404
-
405
- const promise = client.testRequest('/api/test');
406
- await vi.advanceTimersByTimeAsync(5_000);
407
- const result = await promise;
408
-
409
- expect(mockFetch).toHaveBeenCalledTimes(2);
410
- expect(result.success).toBe(true);
411
-
412
- vi.useRealTimers();
413
- });
414
-
415
- it('should retry 500 errors with "the container is not listening"', async () => {
416
- vi.useFakeTimers();
417
-
418
- mockFetch
419
- .mockResolvedValueOnce(
420
- new Response('Error: the container is not listening on port 3000', {
421
- status: 500
422
- })
423
- )
424
- .mockResolvedValueOnce(
425
- new Response(JSON.stringify({ success: true }), { status: 200 })
426
- );
427
-
428
- const promise = client.testRequest('/api/test');
429
- await vi.advanceTimersByTimeAsync(5_000);
430
- const result = await promise;
431
-
432
- expect(mockFetch).toHaveBeenCalledTimes(2);
433
- expect(result.success).toBe(true);
434
-
435
- vi.useRealTimers();
436
- });
437
-
438
- it('should NOT retry 500 errors with "no such image"', async () => {
439
- mockFetch.mockResolvedValueOnce(
440
- new Response('Error: no such image: my-container:latest', {
441
- status: 500
442
- })
443
- );
444
-
445
- await expect(client.testRequest('/api/test')).rejects.toThrow();
446
- expect(mockFetch).toHaveBeenCalledTimes(1); // No retry
447
- });
448
-
449
- it('should NOT retry 500 errors with "container already exists"', async () => {
450
- mockFetch.mockResolvedValueOnce(
451
- new Response('Error: container already exists', { status: 500 })
452
- );
453
-
454
- await expect(client.testRequest('/api/test')).rejects.toThrow();
455
- expect(mockFetch).toHaveBeenCalledTimes(1); // No retry
456
- });
457
-
458
- it('should NOT retry 500 errors with unknown patterns', async () => {
459
- mockFetch.mockResolvedValueOnce(
460
- new Response('Internal server error: database connection failed', {
461
- status: 500
462
- })
463
- );
464
-
465
- await expect(client.testRequest('/api/test')).rejects.toThrow();
466
- expect(mockFetch).toHaveBeenCalledTimes(1); // Fail-safe: don't retry
467
- });
468
-
469
- it('should NOT retry 404 or other non-500/503 errors', async () => {
470
- mockFetch.mockResolvedValueOnce(
471
- new Response('Not found', { status: 404 })
472
- );
473
-
474
- await expect(client.testRequest('/api/test')).rejects.toThrow();
475
- expect(mockFetch).toHaveBeenCalledTimes(1);
476
- });
477
-
478
- it('should respect MIN_TIME_FOR_RETRY_MS and stop retrying', async () => {
479
- vi.useFakeTimers();
480
-
481
- // Mock responses that would trigger retry
482
- mockFetch.mockResolvedValue(
483
- new Response('No container instance available', { status: 503 })
484
- );
485
-
486
- const promise = client.testRequest('/api/test');
487
-
488
- // Fast-forward past retry budget (120s)
489
- await vi.advanceTimersByTimeAsync(125_000);
490
-
491
- // Should eventually give up and throw the 503 error
492
- await expect(promise).rejects.toThrow();
493
-
494
- vi.useRealTimers();
495
- });
496
-
497
- it('should use exponential backoff: 3s, 6s, 12s, 24s, 30s', async () => {
498
- vi.useFakeTimers();
499
- const delays: number[] = [];
500
- let callCount = 0;
501
-
502
- mockFetch.mockImplementation(async () => {
503
- delays.push(Date.now());
504
- callCount++;
505
- // After 5 attempts, return success to avoid timeout
506
- if (callCount >= 5) {
507
- return new Response(JSON.stringify({ success: true }), {
508
- status: 200
509
- });
510
- }
511
- return new Response('No container instance available', { status: 503 });
512
- });
513
-
514
- const promise = client.testRequest('/api/test');
515
-
516
- // Advance time to allow all retries
517
- await vi.advanceTimersByTimeAsync(80_000);
518
-
519
- await promise;
520
-
521
- // Check delays between attempts (approximately)
522
- // Attempt 1 at 0ms, Attempt 2 at ~3000ms, Attempt 3 at ~9000ms, etc.
523
- expect(delays.length).toBeGreaterThanOrEqual(4);
524
-
525
- vi.useRealTimers();
526
- });
527
-
528
- it('should retry multiple transient errors in sequence', async () => {
529
- vi.useFakeTimers();
530
-
531
- mockFetch
532
- .mockResolvedValueOnce(
533
- new Response('No container instance available', { status: 503 })
534
- )
535
- .mockResolvedValueOnce(
536
- new Response('Container port not found', { status: 500 })
537
- )
538
- .mockResolvedValueOnce(
539
- new Response('The container is not listening', { status: 500 })
540
- )
541
- .mockResolvedValueOnce(
542
- new Response(JSON.stringify({ success: true }), { status: 200 })
543
- );
544
-
545
- const promise = client.testRequest('/api/test');
546
-
547
- // Advance time to allow all retries (3s + 6s + 12s = 21s)
548
- await vi.advanceTimersByTimeAsync(25_000);
549
-
550
- const result = await promise;
551
-
552
- expect(mockFetch).toHaveBeenCalledTimes(4);
553
- expect(result.success).toBe(true);
554
-
555
- vi.useRealTimers();
556
- });
557
-
558
- it('should handle case-insensitive error matching', async () => {
559
- vi.useFakeTimers();
560
-
561
- mockFetch
562
- .mockResolvedValueOnce(
563
- new Response('ERROR: CONTAINER PORT NOT FOUND', { status: 500 })
564
- )
565
- .mockResolvedValueOnce(
566
- new Response(JSON.stringify({ success: true }), { status: 200 })
567
- );
568
-
569
- const promise = client.testRequest('/api/test');
570
-
571
- // Advance time for first retry (3s)
572
- await vi.advanceTimersByTimeAsync(5_000);
573
-
574
- const result = await promise;
575
-
576
- expect(mockFetch).toHaveBeenCalledTimes(2);
577
- expect(result.success).toBe(true);
578
-
579
- vi.useRealTimers();
580
- });
581
- });
582
- });