@cloudflare/sandbox 0.0.9 → 0.1.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 (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/Dockerfile +1 -14
  3. package/container_src/handler/exec.ts +337 -0
  4. package/container_src/handler/file.ts +844 -0
  5. package/container_src/handler/git.ts +182 -0
  6. package/container_src/handler/ports.ts +314 -0
  7. package/container_src/handler/process.ts +640 -0
  8. package/container_src/index.ts +82 -2973
  9. package/container_src/types.ts +103 -0
  10. package/dist/chunk-6THNBO4S.js +46 -0
  11. package/dist/chunk-6THNBO4S.js.map +1 -0
  12. package/dist/chunk-6UAWTJ5S.js +85 -0
  13. package/dist/chunk-6UAWTJ5S.js.map +1 -0
  14. package/dist/chunk-G4XT4SP7.js +638 -0
  15. package/dist/chunk-G4XT4SP7.js.map +1 -0
  16. package/dist/chunk-ISFOIYQC.js +585 -0
  17. package/dist/chunk-ISFOIYQC.js.map +1 -0
  18. package/dist/chunk-NNGBXDMY.js +89 -0
  19. package/dist/chunk-NNGBXDMY.js.map +1 -0
  20. package/dist/client-Da-mLX4p.d.ts +210 -0
  21. package/dist/client.d.ts +2 -1
  22. package/dist/client.js +3 -37
  23. package/dist/index.d.ts +3 -1
  24. package/dist/index.js +13 -3
  25. package/dist/request-handler.d.ts +2 -1
  26. package/dist/request-handler.js +4 -2
  27. package/dist/sandbox.d.ts +2 -1
  28. package/dist/sandbox.js +4 -2
  29. package/dist/security.d.ts +30 -0
  30. package/dist/security.js +13 -0
  31. package/dist/security.js.map +1 -0
  32. package/dist/sse-parser.d.ts +28 -0
  33. package/dist/sse-parser.js +11 -0
  34. package/dist/sse-parser.js.map +1 -0
  35. package/dist/types.d.ts +284 -0
  36. package/dist/types.js +19 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +2 -7
  39. package/src/client.ts +235 -1286
  40. package/src/index.ts +6 -0
  41. package/src/request-handler.ts +69 -20
  42. package/src/sandbox.ts +463 -70
  43. package/src/security.ts +113 -0
  44. package/src/sse-parser.ts +147 -0
  45. package/src/types.ts +386 -0
  46. package/tsconfig.json +1 -1
  47. package/README.md +0 -65
  48. package/dist/chunk-4J5LQCCN.js +0 -1446
  49. package/dist/chunk-4J5LQCCN.js.map +0 -1
  50. package/dist/chunk-5SZ3RVJZ.js +0 -250
  51. package/dist/chunk-5SZ3RVJZ.js.map +0 -1
  52. package/dist/client-BuVjqV00.d.ts +0 -247
  53. package/tests/client.example.ts +0 -308
  54. package/tests/connection-test.ts +0 -81
  55. package/tests/simple-test.ts +0 -81
  56. package/tests/test1.ts +0 -281
  57. package/tests/test2.ts +0 -929
@@ -0,0 +1,640 @@
1
+ import { type SpawnOptions, spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import type { ProcessRecord, ProcessStatus, StartProcessRequest } from "../types";
4
+
5
+ // Generate a unique process ID using cryptographically secure randomness
6
+ function generateProcessId(): string {
7
+ return `proc_${Date.now()}_${randomBytes(6).toString('hex')}`;
8
+ }
9
+
10
+
11
+ // Process management handlers
12
+ export async function handleStartProcessRequest(
13
+ processes: Map<string, ProcessRecord>,
14
+ req: Request,
15
+ corsHeaders: Record<string, string>
16
+ ): Promise<Response> {
17
+ try {
18
+ const body = (await req.json()) as StartProcessRequest;
19
+ const { command, options = {} } = body;
20
+
21
+ if (!command || typeof command !== "string") {
22
+ return new Response(
23
+ JSON.stringify({
24
+ error: "Command is required and must be a string",
25
+ }),
26
+ {
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ ...corsHeaders,
30
+ },
31
+ status: 400,
32
+ }
33
+ );
34
+ }
35
+
36
+ const processId = options.processId || generateProcessId();
37
+ const startTime = new Date();
38
+
39
+ // Check if process ID already exists
40
+ if (processes.has(processId)) {
41
+ return new Response(
42
+ JSON.stringify({
43
+ error: `Process already exists: ${processId}`,
44
+ }),
45
+ {
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ ...corsHeaders,
49
+ },
50
+ status: 409,
51
+ }
52
+ );
53
+ }
54
+
55
+ console.log(`[Server] Starting background process: ${command} (ID: ${processId})`);
56
+
57
+ // Create process record in starting state
58
+ const processRecord: ProcessRecord = {
59
+ id: processId,
60
+ command,
61
+ status: 'starting',
62
+ startTime,
63
+ sessionId: options.sessionId,
64
+ stdout: '',
65
+ stderr: '',
66
+ outputListeners: new Set(),
67
+ statusListeners: new Set()
68
+ };
69
+
70
+ processes.set(processId, processRecord);
71
+
72
+ // Start the actual process
73
+ try {
74
+ const spawnOptions: SpawnOptions = {
75
+ cwd: options.cwd || process.cwd(),
76
+ env: { ...process.env, ...options.env },
77
+ detached: false,
78
+ shell: true,
79
+ stdio: ["pipe", "pipe", "pipe"] as const
80
+ };
81
+
82
+ // Use shell execution to preserve quotes and complex command structures
83
+ const childProcess = spawn(command, spawnOptions);
84
+ processRecord.childProcess = childProcess;
85
+ processRecord.pid = childProcess.pid;
86
+ processRecord.status = 'running';
87
+
88
+ // Set up output handling
89
+ childProcess.stdout?.on('data', (data) => {
90
+ const output = data.toString(options.encoding || 'utf8');
91
+ processRecord.stdout += output;
92
+
93
+ // Notify listeners
94
+ for (const listener of processRecord.outputListeners) {
95
+ listener('stdout', output);
96
+ }
97
+ });
98
+
99
+ childProcess.stderr?.on('data', (data) => {
100
+ const output = data.toString(options.encoding || 'utf8');
101
+ processRecord.stderr += output;
102
+
103
+ // Notify listeners
104
+ for (const listener of processRecord.outputListeners) {
105
+ listener('stderr', output);
106
+ }
107
+ });
108
+
109
+ childProcess.on('exit', (code, signal) => {
110
+ processRecord.endTime = new Date();
111
+ processRecord.exitCode = code !== null ? code : -1;
112
+
113
+ if (signal) {
114
+ processRecord.status = 'killed';
115
+ } else if (code === 0) {
116
+ processRecord.status = 'completed';
117
+ } else {
118
+ processRecord.status = 'failed';
119
+ }
120
+
121
+ // Notify status listeners
122
+ for (const listener of processRecord.statusListeners) {
123
+ listener(processRecord.status);
124
+ }
125
+
126
+ console.log(`[Server] Process ${processId} exited with code ${code} (signal: ${signal})`);
127
+ });
128
+
129
+ childProcess.on('error', (error) => {
130
+ processRecord.status = 'error';
131
+ processRecord.endTime = new Date();
132
+ console.error(`[Server] Process ${processId} error:`, error);
133
+
134
+ // Notify status listeners
135
+ for (const listener of processRecord.statusListeners) {
136
+ listener('error');
137
+ }
138
+ });
139
+
140
+ // Timeout handling
141
+ if (options.timeout) {
142
+ setTimeout(() => {
143
+ if (processRecord.status === 'running') {
144
+ childProcess.kill('SIGTERM');
145
+ console.log(`[Server] Process ${processId} timed out after ${options.timeout}ms`);
146
+ }
147
+ }, options.timeout);
148
+ }
149
+
150
+ return new Response(
151
+ JSON.stringify({
152
+ process: {
153
+ id: processRecord.id,
154
+ pid: processRecord.pid,
155
+ command: processRecord.command,
156
+ status: processRecord.status,
157
+ startTime: processRecord.startTime.toISOString(),
158
+ sessionId: processRecord.sessionId
159
+ }
160
+ }),
161
+ {
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ ...corsHeaders,
165
+ },
166
+ }
167
+ );
168
+ } catch (error) {
169
+ // Clean up on error
170
+ processes.delete(processId);
171
+ throw error;
172
+ }
173
+ } catch (error) {
174
+ console.error("[Server] Error in handleStartProcessRequest:", error);
175
+ return new Response(
176
+ JSON.stringify({
177
+ error: "Failed to start process",
178
+ message: error instanceof Error ? error.message : "Unknown error",
179
+ }),
180
+ {
181
+ headers: {
182
+ "Content-Type": "application/json",
183
+ ...corsHeaders,
184
+ },
185
+ status: 500,
186
+ }
187
+ );
188
+ }
189
+ }
190
+
191
+ export async function handleListProcessesRequest(
192
+ processes: Map<string, ProcessRecord>,
193
+ req: Request,
194
+ corsHeaders: Record<string, string>
195
+ ): Promise<Response> {
196
+ try {
197
+ const processesArray = Array.from(processes.values()).map(record => ({
198
+ id: record.id,
199
+ pid: record.pid,
200
+ command: record.command,
201
+ status: record.status,
202
+ startTime: record.startTime.toISOString(),
203
+ endTime: record.endTime?.toISOString(),
204
+ exitCode: record.exitCode,
205
+ sessionId: record.sessionId
206
+ }));
207
+
208
+ return new Response(
209
+ JSON.stringify({
210
+ processes: processesArray,
211
+ count: processesArray.length,
212
+ timestamp: new Date().toISOString(),
213
+ }),
214
+ {
215
+ headers: {
216
+ "Content-Type": "application/json",
217
+ ...corsHeaders,
218
+ },
219
+ }
220
+ );
221
+ } catch (error) {
222
+ console.error("[Server] Error in handleListProcessesRequest:", error);
223
+ return new Response(
224
+ JSON.stringify({
225
+ error: "Failed to list processes",
226
+ message: error instanceof Error ? error.message : "Unknown error",
227
+ }),
228
+ {
229
+ headers: {
230
+ "Content-Type": "application/json",
231
+ ...corsHeaders,
232
+ },
233
+ status: 500,
234
+ }
235
+ );
236
+ }
237
+ }
238
+
239
+ export async function handleGetProcessRequest(
240
+ processes: Map<string, ProcessRecord>,
241
+ req: Request,
242
+ corsHeaders: Record<string, string>,
243
+ processId: string
244
+ ): Promise<Response> {
245
+ try {
246
+ const record = processes.get(processId);
247
+
248
+ if (!record) {
249
+ return new Response(
250
+ JSON.stringify({
251
+ process: null
252
+ }),
253
+ {
254
+ headers: {
255
+ "Content-Type": "application/json",
256
+ ...corsHeaders,
257
+ },
258
+ status: 404,
259
+ }
260
+ );
261
+ }
262
+
263
+ return new Response(
264
+ JSON.stringify({
265
+ process: {
266
+ id: record.id,
267
+ pid: record.pid,
268
+ command: record.command,
269
+ status: record.status,
270
+ startTime: record.startTime.toISOString(),
271
+ endTime: record.endTime?.toISOString(),
272
+ exitCode: record.exitCode,
273
+ sessionId: record.sessionId
274
+ }
275
+ }),
276
+ {
277
+ headers: {
278
+ "Content-Type": "application/json",
279
+ ...corsHeaders,
280
+ },
281
+ }
282
+ );
283
+ } catch (error) {
284
+ console.error("[Server] Error in handleGetProcessRequest:", error);
285
+ return new Response(
286
+ JSON.stringify({
287
+ error: "Failed to get process",
288
+ message: error instanceof Error ? error.message : "Unknown error",
289
+ }),
290
+ {
291
+ headers: {
292
+ "Content-Type": "application/json",
293
+ ...corsHeaders,
294
+ },
295
+ status: 500,
296
+ }
297
+ );
298
+ }
299
+ }
300
+
301
+ export async function handleKillProcessRequest(
302
+ processes: Map<string, ProcessRecord>,
303
+ req: Request,
304
+ corsHeaders: Record<string, string>,
305
+ processId: string
306
+ ): Promise<Response> {
307
+ try {
308
+ const record = processes.get(processId);
309
+
310
+ if (!record) {
311
+ return new Response(
312
+ JSON.stringify({
313
+ error: `Process not found: ${processId}`,
314
+ }),
315
+ {
316
+ headers: {
317
+ "Content-Type": "application/json",
318
+ ...corsHeaders,
319
+ },
320
+ status: 404,
321
+ }
322
+ );
323
+ }
324
+
325
+ if (record.childProcess && record.status === 'running') {
326
+ record.childProcess.kill('SIGTERM');
327
+ console.log(`[Server] Sent SIGTERM to process ${processId}`);
328
+
329
+ // Give it a moment to terminate gracefully, then force kill
330
+ setTimeout(() => {
331
+ if (record.childProcess && record.status === 'running') {
332
+ record.childProcess.kill('SIGKILL');
333
+ console.log(`[Server] Force killed process ${processId}`);
334
+ }
335
+ }, 5000);
336
+ }
337
+
338
+ // Mark as killed locally
339
+ record.status = 'killed';
340
+ record.endTime = new Date();
341
+ record.exitCode = -1;
342
+
343
+ // Notify status listeners
344
+ for (const listener of record.statusListeners) {
345
+ listener('killed');
346
+ }
347
+
348
+ return new Response(
349
+ JSON.stringify({
350
+ success: true,
351
+ message: `Process ${processId} killed`,
352
+ timestamp: new Date().toISOString(),
353
+ }),
354
+ {
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ ...corsHeaders,
358
+ },
359
+ }
360
+ );
361
+ } catch (error) {
362
+ console.error("[Server] Error in handleKillProcessRequest:", error);
363
+ return new Response(
364
+ JSON.stringify({
365
+ error: "Failed to kill process",
366
+ message: error instanceof Error ? error.message : "Unknown error",
367
+ }),
368
+ {
369
+ headers: {
370
+ "Content-Type": "application/json",
371
+ ...corsHeaders,
372
+ },
373
+ status: 500,
374
+ }
375
+ );
376
+ }
377
+ }
378
+
379
+ export async function handleKillAllProcessesRequest(
380
+ processes: Map<string, ProcessRecord>,
381
+ req: Request,
382
+ corsHeaders: Record<string, string>
383
+ ): Promise<Response> {
384
+ try {
385
+ let killedCount = 0;
386
+
387
+ for (const [processId, record] of processes) {
388
+ if (record.childProcess && record.status === 'running') {
389
+ try {
390
+ record.childProcess.kill('SIGTERM');
391
+ record.status = 'killed';
392
+ record.endTime = new Date();
393
+ record.exitCode = -1;
394
+
395
+ // Notify status listeners
396
+ for (const listener of record.statusListeners) {
397
+ listener('killed');
398
+ }
399
+
400
+ killedCount++;
401
+ console.log(`[Server] Killed process ${processId}`);
402
+ } catch (error) {
403
+ console.error(`[Server] Failed to kill process ${processId}:`, error);
404
+ }
405
+ }
406
+ }
407
+
408
+ return new Response(
409
+ JSON.stringify({
410
+ success: true,
411
+ killedCount,
412
+ message: `Killed ${killedCount} processes`,
413
+ timestamp: new Date().toISOString(),
414
+ }),
415
+ {
416
+ headers: {
417
+ "Content-Type": "application/json",
418
+ ...corsHeaders,
419
+ },
420
+ }
421
+ );
422
+ } catch (error) {
423
+ console.error("[Server] Error in handleKillAllProcessesRequest:", error);
424
+ return new Response(
425
+ JSON.stringify({
426
+ error: "Failed to kill all processes",
427
+ message: error instanceof Error ? error.message : "Unknown error",
428
+ }),
429
+ {
430
+ headers: {
431
+ "Content-Type": "application/json",
432
+ ...corsHeaders,
433
+ },
434
+ status: 500,
435
+ }
436
+ );
437
+ }
438
+ }
439
+
440
+ export async function handleGetProcessLogsRequest(
441
+ processes: Map<string, ProcessRecord>,
442
+ req: Request,
443
+ corsHeaders: Record<string, string>,
444
+ processId: string
445
+ ): Promise<Response> {
446
+ try {
447
+ const record = processes.get(processId);
448
+
449
+ if (!record) {
450
+ return new Response(
451
+ JSON.stringify({
452
+ error: `Process not found: ${processId}`,
453
+ }),
454
+ {
455
+ headers: {
456
+ "Content-Type": "application/json",
457
+ ...corsHeaders,
458
+ },
459
+ status: 404,
460
+ }
461
+ );
462
+ }
463
+
464
+ return new Response(
465
+ JSON.stringify({
466
+ stdout: record.stdout,
467
+ stderr: record.stderr,
468
+ processId: record.id,
469
+ }),
470
+ {
471
+ headers: {
472
+ "Content-Type": "application/json",
473
+ ...corsHeaders,
474
+ },
475
+ }
476
+ );
477
+ } catch (error) {
478
+ console.error("[Server] Error in handleGetProcessLogsRequest:", error);
479
+ return new Response(
480
+ JSON.stringify({
481
+ error: "Failed to get process logs",
482
+ message: error instanceof Error ? error.message : "Unknown error",
483
+ }),
484
+ {
485
+ headers: {
486
+ "Content-Type": "application/json",
487
+ ...corsHeaders,
488
+ },
489
+ status: 500,
490
+ }
491
+ );
492
+ }
493
+ }
494
+
495
+ export async function handleStreamProcessLogsRequest(
496
+ processes: Map<string, ProcessRecord>,
497
+ req: Request,
498
+ corsHeaders: Record<string, string>,
499
+ processId: string
500
+ ): Promise<Response> {
501
+ try {
502
+ const record = processes.get(processId);
503
+
504
+ if (!record) {
505
+ return new Response(
506
+ JSON.stringify({
507
+ error: `Process not found: ${processId}`,
508
+ }),
509
+ {
510
+ headers: {
511
+ "Content-Type": "application/json",
512
+ ...corsHeaders,
513
+ },
514
+ status: 404,
515
+ }
516
+ );
517
+ }
518
+
519
+ // Create a readable stream for Server-Sent Events
520
+ let isConnected = true;
521
+
522
+ const stream = new ReadableStream({
523
+ start(controller) {
524
+ // Send existing logs first
525
+ if (record.stdout) {
526
+ const event = `data: ${JSON.stringify({
527
+ type: 'stdout',
528
+ timestamp: new Date().toISOString(),
529
+ data: record.stdout,
530
+ processId,
531
+ sessionId: record.sessionId
532
+ })}\n\n`;
533
+ controller.enqueue(new TextEncoder().encode(event));
534
+ }
535
+
536
+ if (record.stderr) {
537
+ const event = `data: ${JSON.stringify({
538
+ type: 'stderr',
539
+ timestamp: new Date().toISOString(),
540
+ data: record.stderr,
541
+ processId,
542
+ sessionId: record.sessionId
543
+ })}\n\n`;
544
+ controller.enqueue(new TextEncoder().encode(event));
545
+ }
546
+
547
+ // Send status
548
+ const statusEvent = `data: ${JSON.stringify({
549
+ type: 'status',
550
+ timestamp: new Date().toISOString(),
551
+ data: `Process status: ${record.status}`,
552
+ processId,
553
+ sessionId: record.sessionId
554
+ })}\n\n`;
555
+ controller.enqueue(new TextEncoder().encode(statusEvent));
556
+
557
+ // Set up real-time streaming for ongoing output
558
+ const outputListener = (stream: 'stdout' | 'stderr', data: string) => {
559
+ if (!isConnected) return;
560
+
561
+ const event = `data: ${JSON.stringify({
562
+ type: stream,
563
+ timestamp: new Date().toISOString(),
564
+ data,
565
+ processId,
566
+ sessionId: record.sessionId
567
+ })}\n\n`;
568
+
569
+ try {
570
+ controller.enqueue(new TextEncoder().encode(event));
571
+ } catch (error) {
572
+ console.log(`[Server] Stream closed for process ${processId}`);
573
+ isConnected = false;
574
+ }
575
+ };
576
+
577
+ const statusListener = (status: ProcessStatus) => {
578
+ if (!isConnected) return;
579
+
580
+ const event = `data: ${JSON.stringify({
581
+ type: 'status',
582
+ timestamp: new Date().toISOString(),
583
+ data: `Process status: ${status}`,
584
+ processId,
585
+ sessionId: record.sessionId
586
+ })}\n\n`;
587
+
588
+ try {
589
+ controller.enqueue(new TextEncoder().encode(event));
590
+ } catch (error) {
591
+ console.log(`[Server] Stream closed for process ${processId}`);
592
+ isConnected = false;
593
+ }
594
+
595
+ // Close stream when process completes
596
+ if (['completed', 'failed', 'killed', 'error'].includes(status)) {
597
+ setTimeout(() => {
598
+ record.outputListeners.delete(outputListener);
599
+ record.statusListeners.delete(statusListener);
600
+ controller.close();
601
+ }, 1000); // Give a moment for final events
602
+ }
603
+ };
604
+
605
+ // Add listeners
606
+ record.outputListeners.add(outputListener);
607
+ record.statusListeners.add(statusListener);
608
+ },
609
+
610
+ cancel() {
611
+ isConnected = false;
612
+ console.log(`[Server] Log stream cancelled for process ${processId}`);
613
+ }
614
+ });
615
+
616
+ return new Response(stream, {
617
+ headers: {
618
+ "Content-Type": "text/event-stream",
619
+ "Cache-Control": "no-cache",
620
+ "Connection": "keep-alive",
621
+ ...corsHeaders,
622
+ },
623
+ });
624
+ } catch (error) {
625
+ console.error("[Server] Error in handleStreamProcessLogsRequest:", error);
626
+ return new Response(
627
+ JSON.stringify({
628
+ error: "Failed to stream process logs",
629
+ message: error instanceof Error ? error.message : "Unknown error",
630
+ }),
631
+ {
632
+ headers: {
633
+ "Content-Type": "application/json",
634
+ ...corsHeaders,
635
+ },
636
+ status: 500,
637
+ }
638
+ );
639
+ }
640
+ }