@cloudflare/sandbox 0.5.6 → 0.6.1

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,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
- });