@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,649 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import type {
3
- ProcessCleanupResult,
4
- ProcessInfoResult,
5
- ProcessKillResult,
6
- ProcessListResult,
7
- ProcessLogsResult,
8
- ProcessStartResult
9
- } from '../src/clients';
10
- import { ProcessClient } from '../src/clients/process-client';
11
- import {
12
- CommandNotFoundError,
13
- ProcessError,
14
- ProcessNotFoundError,
15
- SandboxError
16
- } from '../src/errors';
17
-
18
- describe('ProcessClient', () => {
19
- let client: ProcessClient;
20
- let mockFetch: ReturnType<typeof vi.fn>;
21
-
22
- beforeEach(() => {
23
- vi.clearAllMocks();
24
-
25
- mockFetch = vi.fn();
26
- global.fetch = mockFetch as unknown as typeof fetch;
27
-
28
- client = new ProcessClient({
29
- baseUrl: 'http://test.com',
30
- port: 3000
31
- });
32
- });
33
-
34
- afterEach(() => {
35
- vi.restoreAllMocks();
36
- });
37
-
38
- describe('process lifecycle management', () => {
39
- it('should start background processes successfully', async () => {
40
- const mockResponse: ProcessStartResult = {
41
- success: true,
42
- processId: 'proc-web-server',
43
- command: 'npm run dev',
44
- pid: 12345,
45
- timestamp: '2023-01-01T00:00:00Z'
46
- };
47
-
48
- mockFetch.mockResolvedValue(
49
- new Response(JSON.stringify(mockResponse), { status: 200 })
50
- );
51
-
52
- const result = await client.startProcess('npm run dev', 'session-123');
53
-
54
- expect(result.success).toBe(true);
55
- expect(result.command).toBe('npm run dev');
56
- expect(result.pid).toBe(12345);
57
- expect(result.processId).toBe('proc-web-server');
58
- });
59
-
60
- it('should start processes with custom process IDs', async () => {
61
- const mockResponse: ProcessStartResult = {
62
- success: true,
63
- processId: 'my-api-server',
64
- command: 'python app.py',
65
- pid: 54321,
66
- timestamp: '2023-01-01T00:00:00Z'
67
- };
68
-
69
- mockFetch.mockResolvedValue(
70
- new Response(JSON.stringify(mockResponse), { status: 200 })
71
- );
72
-
73
- const result = await client.startProcess('python app.py', 'session-456', {
74
- processId: 'my-api-server'
75
- });
76
-
77
- expect(result.success).toBe(true);
78
- expect(result.processId).toBe('my-api-server');
79
- expect(result.command).toBe('python app.py');
80
- });
81
-
82
- it('should handle long-running process startup', async () => {
83
- const mockResponse: ProcessStartResult = {
84
- success: true,
85
- processId: 'proc-database',
86
- command: 'docker run postgres',
87
- pid: 99999,
88
- timestamp: '2023-01-01T00:00:05Z'
89
- };
90
-
91
- mockFetch.mockImplementation(
92
- () =>
93
- new Promise((resolve) =>
94
- setTimeout(
95
- () =>
96
- resolve(
97
- new Response(JSON.stringify(mockResponse), { status: 200 })
98
- ),
99
- 100
100
- )
101
- )
102
- );
103
-
104
- const result = await client.startProcess(
105
- 'docker run postgres',
106
- 'session-789'
107
- );
108
-
109
- expect(result.success).toBe(true);
110
- expect(result.processId).toBe('proc-database');
111
- expect(result.command).toBe('docker run postgres');
112
- });
113
-
114
- it('should handle command not found errors', async () => {
115
- const errorResponse = {
116
- error: 'Command not found: invalidcmd',
117
- code: 'COMMAND_NOT_FOUND'
118
- };
119
-
120
- mockFetch.mockResolvedValue(
121
- new Response(JSON.stringify(errorResponse), { status: 404 })
122
- );
123
-
124
- await expect(
125
- client.startProcess('invalidcmd', 'session-err')
126
- ).rejects.toThrow(CommandNotFoundError);
127
- });
128
-
129
- it('should handle process startup failures', async () => {
130
- const errorResponse = {
131
- error: 'Process failed to start: permission denied',
132
- code: 'PROCESS_ERROR'
133
- };
134
-
135
- mockFetch.mockResolvedValue(
136
- new Response(JSON.stringify(errorResponse), { status: 500 })
137
- );
138
-
139
- await expect(
140
- client.startProcess('sudo privileged-command', 'session-err')
141
- ).rejects.toThrow(ProcessError);
142
- });
143
- });
144
-
145
- describe('process monitoring and inspection', () => {
146
- it('should list running processes', async () => {
147
- const mockResponse: ProcessListResult = {
148
- success: true,
149
- processes: [
150
- {
151
- id: 'proc-web',
152
- command: 'npm run dev',
153
- status: 'running',
154
- pid: 12345,
155
- startTime: '2023-01-01T00:00:00Z'
156
- },
157
- {
158
- id: 'proc-api',
159
- command: 'python api.py',
160
- status: 'running',
161
- pid: 12346,
162
- startTime: '2023-01-01T00:00:30Z'
163
- },
164
- {
165
- id: 'proc-worker',
166
- command: 'node worker.js',
167
- status: 'completed',
168
- pid: 12347,
169
- exitCode: 0,
170
- startTime: '2023-01-01T00:01:00Z',
171
- endTime: '2023-01-01T00:05:00Z'
172
- }
173
- ],
174
- timestamp: '2023-01-01T00:05:30Z'
175
- };
176
-
177
- mockFetch.mockResolvedValue(
178
- new Response(JSON.stringify(mockResponse), { status: 200 })
179
- );
180
-
181
- const result = await client.listProcesses();
182
-
183
- expect(result.success).toBe(true);
184
- expect(result.processes).toHaveLength(3);
185
-
186
- const runningProcesses = result.processes.filter(
187
- (p) => p.status === 'running'
188
- );
189
- expect(runningProcesses).toHaveLength(2);
190
- expect(runningProcesses[0].pid).toBeDefined();
191
- expect(runningProcesses[1].pid).toBeDefined();
192
-
193
- const completedProcess = result.processes.find(
194
- (p) => p.status === 'completed'
195
- );
196
- expect(completedProcess?.exitCode).toBe(0);
197
- expect(completedProcess?.endTime).toBeDefined();
198
- });
199
-
200
- it('should get specific process details', async () => {
201
- const mockResponse: ProcessInfoResult = {
202
- success: true,
203
- process: {
204
- id: 'proc-analytics',
205
- command: 'python analytics.py --batch-size=1000',
206
- status: 'running',
207
- pid: 98765,
208
- startTime: '2023-01-01T00:00:00Z'
209
- },
210
- timestamp: '2023-01-01T00:10:00Z'
211
- };
212
-
213
- mockFetch.mockResolvedValue(
214
- new Response(JSON.stringify(mockResponse), { status: 200 })
215
- );
216
-
217
- const result = await client.getProcess('proc-analytics');
218
-
219
- expect(result.success).toBe(true);
220
- expect(result.process.id).toBe('proc-analytics');
221
- expect(result.process.command).toContain('--batch-size=1000');
222
- expect(result.process.status).toBe('running');
223
- expect(result.process.pid).toBe(98765);
224
- });
225
-
226
- it('should handle process not found error', async () => {
227
- const errorResponse = {
228
- error: 'Process not found: nonexistent-proc',
229
- code: 'PROCESS_NOT_FOUND'
230
- };
231
-
232
- mockFetch.mockResolvedValue(
233
- new Response(JSON.stringify(errorResponse), { status: 404 })
234
- );
235
-
236
- await expect(client.getProcess('nonexistent-proc')).rejects.toThrow(
237
- ProcessNotFoundError
238
- );
239
- });
240
-
241
- it('should handle empty process list', async () => {
242
- const mockResponse: ProcessListResult = {
243
- success: true,
244
- processes: [],
245
- timestamp: '2023-01-01T00:00:00Z'
246
- };
247
-
248
- mockFetch.mockResolvedValue(
249
- new Response(JSON.stringify(mockResponse), { status: 200 })
250
- );
251
-
252
- const result = await client.listProcesses();
253
-
254
- expect(result.success).toBe(true);
255
- expect(result.processes).toHaveLength(0);
256
- });
257
- });
258
-
259
- describe('process termination', () => {
260
- it('should kill individual processes', async () => {
261
- const mockResponse: ProcessKillResult = {
262
- success: true,
263
- processId: 'test-process',
264
- timestamp: '2023-01-01T00:10:00Z'
265
- };
266
-
267
- mockFetch.mockResolvedValue(
268
- new Response(JSON.stringify(mockResponse), { status: 200 })
269
- );
270
-
271
- const result = await client.killProcess('proc-web');
272
-
273
- expect(result.success).toBe(true);
274
- });
275
-
276
- it('should handle kill non-existent process', async () => {
277
- const errorResponse = {
278
- error: 'Process not found: already-dead-proc',
279
- code: 'PROCESS_NOT_FOUND'
280
- };
281
-
282
- mockFetch.mockResolvedValue(
283
- new Response(JSON.stringify(errorResponse), { status: 404 })
284
- );
285
-
286
- await expect(client.killProcess('already-dead-proc')).rejects.toThrow(
287
- ProcessNotFoundError
288
- );
289
- });
290
-
291
- it('should kill all processes at once', async () => {
292
- const mockResponse: ProcessCleanupResult = {
293
- success: true,
294
- cleanedCount: 0,
295
- timestamp: '2023-01-01T00:15:00Z'
296
- };
297
-
298
- mockFetch.mockResolvedValue(
299
- new Response(JSON.stringify(mockResponse), { status: 200 })
300
- );
301
-
302
- const result = await client.killAllProcesses();
303
-
304
- expect(result.success).toBe(true);
305
- });
306
-
307
- it('should handle kill all when no processes running', async () => {
308
- const mockResponse: ProcessCleanupResult = {
309
- success: true,
310
- cleanedCount: 0,
311
- timestamp: '2023-01-01T00:00:00Z'
312
- };
313
-
314
- mockFetch.mockResolvedValue(
315
- new Response(JSON.stringify(mockResponse), { status: 200 })
316
- );
317
-
318
- const result = await client.killAllProcesses();
319
-
320
- expect(result.success).toBe(true);
321
- });
322
-
323
- it('should handle kill failures', async () => {
324
- const errorResponse = {
325
- error: 'Failed to kill process: process is protected',
326
- code: 'PROCESS_ERROR'
327
- };
328
-
329
- mockFetch.mockResolvedValue(
330
- new Response(JSON.stringify(errorResponse), { status: 500 })
331
- );
332
-
333
- await expect(client.killProcess('protected-proc')).rejects.toThrow(
334
- ProcessError
335
- );
336
- });
337
- });
338
-
339
- describe('process log management', () => {
340
- it('should retrieve process logs', async () => {
341
- const mockResponse: ProcessLogsResult = {
342
- success: true,
343
- processId: 'proc-server',
344
- stdout: `Server starting...
345
- ✓ Database connected
346
- ✓ Routes loaded
347
- ✓ Server listening on port 3000
348
- [INFO] Request: GET /api/health
349
- [INFO] Response: 200 OK`,
350
- stderr: `[WARN] Deprecated function used in auth.js:45
351
- [WARN] High memory usage: 85%`,
352
- timestamp: '2023-01-01T00:10:00Z'
353
- };
354
-
355
- mockFetch.mockResolvedValue(
356
- new Response(JSON.stringify(mockResponse), { status: 200 })
357
- );
358
-
359
- const result = await client.getProcessLogs('proc-server');
360
-
361
- expect(result.success).toBe(true);
362
- expect(result.processId).toBe('proc-server');
363
- expect(result.stdout).toContain('Server listening on port 3000');
364
- expect(result.stdout).toContain('Request: GET /api/health');
365
- expect(result.stderr).toContain('Deprecated function used');
366
- expect(result.stderr).toContain('High memory usage');
367
- });
368
-
369
- it('should handle logs for non-existent process', async () => {
370
- const errorResponse = {
371
- error: 'Process not found: missing-proc',
372
- code: 'PROCESS_NOT_FOUND'
373
- };
374
-
375
- mockFetch.mockResolvedValue(
376
- new Response(JSON.stringify(errorResponse), { status: 404 })
377
- );
378
-
379
- await expect(client.getProcessLogs('missing-proc')).rejects.toThrow(
380
- ProcessNotFoundError
381
- );
382
- });
383
-
384
- it('should retrieve logs for processes with large output', async () => {
385
- const largeStdout = 'Log entry with details\n'.repeat(10000);
386
- const largeStderr = 'Error trace line\n'.repeat(1000);
387
-
388
- const mockResponse: ProcessLogsResult = {
389
- success: true,
390
- processId: 'proc-batch',
391
- stdout: largeStdout,
392
- stderr: largeStderr,
393
- timestamp: '2023-01-01T00:30:00Z'
394
- };
395
-
396
- mockFetch.mockResolvedValue(
397
- new Response(JSON.stringify(mockResponse), { status: 200 })
398
- );
399
-
400
- const result = await client.getProcessLogs('proc-batch');
401
-
402
- expect(result.success).toBe(true);
403
- expect(result.stdout.length).toBeGreaterThan(200000);
404
- expect(result.stderr.length).toBeGreaterThan(15000);
405
- expect(result.stdout.split('\n')).toHaveLength(10001);
406
- expect(result.stderr.split('\n')).toHaveLength(1001);
407
- });
408
-
409
- it('should handle empty process logs', async () => {
410
- const mockResponse: ProcessLogsResult = {
411
- success: true,
412
- processId: 'proc-silent',
413
- stdout: '',
414
- stderr: '',
415
- timestamp: '2023-01-01T00:05:00Z'
416
- };
417
-
418
- mockFetch.mockResolvedValue(
419
- new Response(JSON.stringify(mockResponse), { status: 200 })
420
- );
421
-
422
- const result = await client.getProcessLogs('proc-silent');
423
-
424
- expect(result.success).toBe(true);
425
- expect(result.stdout).toBe('');
426
- expect(result.stderr).toBe('');
427
- expect(result.processId).toBe('proc-silent');
428
- });
429
- });
430
-
431
- describe('log streaming', () => {
432
- it('should stream process logs in real-time', async () => {
433
- const logData = `data: {"type":"stdout","data":"Server starting...\\n","timestamp":"2023-01-01T00:00:01Z"}
434
-
435
- data: {"type":"stdout","data":"Database connected\\n","timestamp":"2023-01-01T00:00:02Z"}
436
-
437
- data: {"type":"stderr","data":"Warning: deprecated API\\n","timestamp":"2023-01-01T00:00:03Z"}
438
-
439
- data: {"type":"stdout","data":"Server ready on port 3000\\n","timestamp":"2023-01-01T00:00:04Z"}
440
-
441
- `;
442
-
443
- const mockStream = new ReadableStream({
444
- start(controller) {
445
- controller.enqueue(new TextEncoder().encode(logData));
446
- controller.close();
447
- }
448
- });
449
-
450
- mockFetch.mockResolvedValue(
451
- new Response(mockStream, {
452
- status: 200,
453
- headers: { 'Content-Type': 'text/event-stream' }
454
- })
455
- );
456
-
457
- const stream = await client.streamProcessLogs('proc-realtime');
458
-
459
- expect(stream).toBeInstanceOf(ReadableStream);
460
-
461
- const reader = stream.getReader();
462
- const decoder = new TextDecoder();
463
- let content = '';
464
-
465
- try {
466
- while (true) {
467
- const { done, value } = await reader.read();
468
- if (done) break;
469
- content += decoder.decode(value);
470
- }
471
- } finally {
472
- reader.releaseLock();
473
- }
474
-
475
- expect(content).toContain('Server starting');
476
- expect(content).toContain('Database connected');
477
- expect(content).toContain('Warning: deprecated API');
478
- expect(content).toContain('Server ready on port 3000');
479
- });
480
-
481
- it('should handle streaming for non-existent process', async () => {
482
- const errorResponse = {
483
- error: 'Process not found: stream-missing',
484
- code: 'PROCESS_NOT_FOUND'
485
- };
486
-
487
- mockFetch.mockResolvedValue(
488
- new Response(JSON.stringify(errorResponse), { status: 404 })
489
- );
490
-
491
- await expect(client.streamProcessLogs('stream-missing')).rejects.toThrow(
492
- ProcessNotFoundError
493
- );
494
- });
495
-
496
- it('should handle streaming setup failures', async () => {
497
- const errorResponse = {
498
- error: 'Failed to setup log stream: process not outputting logs',
499
- code: 'PROCESS_ERROR'
500
- };
501
-
502
- mockFetch.mockResolvedValue(
503
- new Response(JSON.stringify(errorResponse), { status: 500 })
504
- );
505
-
506
- await expect(client.streamProcessLogs('proc-no-logs')).rejects.toThrow(
507
- ProcessError
508
- );
509
- });
510
-
511
- it('should handle missing stream body', async () => {
512
- mockFetch.mockResolvedValue(
513
- new Response(null, {
514
- status: 200,
515
- headers: { 'Content-Type': 'text/event-stream' }
516
- })
517
- );
518
-
519
- await expect(
520
- client.streamProcessLogs('proc-empty-stream')
521
- ).rejects.toThrow('No response body for streaming');
522
- });
523
- });
524
-
525
- describe('session integration', () => {
526
- it('should include session in process operations', async () => {
527
- const mockResponse: ProcessStartResult = {
528
- success: true,
529
- processId: 'proc-session-test',
530
- command: 'echo session-test',
531
- pid: 11111,
532
- timestamp: '2023-01-01T00:00:00Z'
533
- };
534
-
535
- mockFetch.mockResolvedValue(
536
- new Response(JSON.stringify(mockResponse), { status: 200 })
537
- );
538
-
539
- const result = await client.startProcess(
540
- 'echo session-test',
541
- 'session-test'
542
- );
543
-
544
- expect(result.success).toBe(true);
545
-
546
- const [url, options] = mockFetch.mock.calls[0];
547
- const requestBody = JSON.parse(options.body);
548
- expect(requestBody.sessionId).toBe('session-test');
549
- expect(requestBody.command).toBe('echo session-test');
550
- });
551
- });
552
-
553
- describe('concurrent process operations', () => {
554
- it('should handle multiple simultaneous process operations', async () => {
555
- mockFetch.mockImplementation((url: string, options: RequestInit) => {
556
- if (url.includes('/start')) {
557
- return Promise.resolve(
558
- new Response(
559
- JSON.stringify({
560
- success: true,
561
- process: {
562
- id: `proc-${Date.now()}`,
563
- command: JSON.parse(options.body as string).command,
564
- status: 'running',
565
- pid: Math.floor(Math.random() * 90000) + 10000,
566
- startTime: new Date().toISOString()
567
- },
568
- timestamp: new Date().toISOString()
569
- })
570
- )
571
- );
572
- } else if (url.includes('/list')) {
573
- return Promise.resolve(
574
- new Response(
575
- JSON.stringify({
576
- success: true,
577
- processes: [],
578
- timestamp: new Date().toISOString()
579
- })
580
- )
581
- );
582
- } else if (url.includes('/logs')) {
583
- return Promise.resolve(
584
- new Response(
585
- JSON.stringify({
586
- success: true,
587
- processId: url.split('/')[4],
588
- stdout: 'log output',
589
- stderr: '',
590
- timestamp: new Date().toISOString()
591
- })
592
- )
593
- );
594
- }
595
- return Promise.resolve(new Response('{}', { status: 200 }));
596
- });
597
-
598
- const operations = await Promise.all([
599
- client.startProcess('npm run dev', 'session-concurrent'),
600
- client.startProcess('python api.py', 'session-concurrent'),
601
- client.listProcesses(),
602
- client.getProcessLogs('existing-proc'),
603
- client.startProcess('node worker.js', 'session-concurrent')
604
- ]);
605
-
606
- expect(operations).toHaveLength(5);
607
- operations.forEach((result) => {
608
- expect(result.success).toBe(true);
609
- });
610
-
611
- expect(mockFetch).toHaveBeenCalledTimes(5);
612
- });
613
- });
614
-
615
- describe('error handling', () => {
616
- it('should handle network failures gracefully', async () => {
617
- mockFetch.mockRejectedValue(new Error('Network connection failed'));
618
-
619
- await expect(client.listProcesses()).rejects.toThrow(
620
- 'Network connection failed'
621
- );
622
- });
623
-
624
- it('should handle malformed server responses', async () => {
625
- mockFetch.mockResolvedValue(
626
- new Response('invalid json {', { status: 200 })
627
- );
628
-
629
- await expect(
630
- client.startProcess('test-command', 'session-err')
631
- ).rejects.toThrow(SandboxError);
632
- });
633
- });
634
-
635
- describe('constructor options', () => {
636
- it('should initialize with minimal options', () => {
637
- const minimalClient = new ProcessClient();
638
- expect(minimalClient).toBeDefined();
639
- });
640
-
641
- it('should initialize with full options', () => {
642
- const fullOptionsClient = new ProcessClient({
643
- baseUrl: 'http://custom.com',
644
- port: 8080
645
- });
646
- expect(fullOptionsClient).toBeDefined();
647
- });
648
- });
649
- });