@cloudflare/sandbox 0.0.0-e1fa354 → 0.0.0-e489cbb

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 (94) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/Dockerfile +107 -38
  3. package/README.md +89 -771
  4. package/dist/chunk-53JFOF7F.js +2352 -0
  5. package/dist/chunk-53JFOF7F.js.map +1 -0
  6. package/dist/chunk-BFVUNTP4.js +104 -0
  7. package/dist/chunk-BFVUNTP4.js.map +1 -0
  8. package/dist/chunk-EKSWCBCA.js +86 -0
  9. package/dist/chunk-EKSWCBCA.js.map +1 -0
  10. package/dist/chunk-JXZMAU2C.js +559 -0
  11. package/dist/chunk-JXZMAU2C.js.map +1 -0
  12. package/dist/chunk-Z532A7QC.js +78 -0
  13. package/dist/chunk-Z532A7QC.js.map +1 -0
  14. package/dist/file-stream.d.ts +43 -0
  15. package/dist/file-stream.js +9 -0
  16. package/dist/file-stream.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +66 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/interpreter.d.ts +33 -0
  21. package/dist/interpreter.js +8 -0
  22. package/dist/interpreter.js.map +1 -0
  23. package/dist/request-handler.d.ts +18 -0
  24. package/dist/request-handler.js +12 -0
  25. package/dist/request-handler.js.map +1 -0
  26. package/dist/sandbox-D9K2ypln.d.ts +583 -0
  27. package/dist/sandbox.d.ts +4 -0
  28. package/dist/sandbox.js +12 -0
  29. package/dist/sandbox.js.map +1 -0
  30. package/dist/security.d.ts +31 -0
  31. package/dist/security.js +13 -0
  32. package/dist/security.js.map +1 -0
  33. package/dist/sse-parser.d.ts +28 -0
  34. package/dist/sse-parser.js +11 -0
  35. package/dist/sse-parser.js.map +1 -0
  36. package/package.json +13 -5
  37. package/src/clients/base-client.ts +280 -0
  38. package/src/clients/command-client.ts +115 -0
  39. package/src/clients/file-client.ts +269 -0
  40. package/src/clients/git-client.ts +92 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/{jupyter-client.ts → clients/interpreter-client.ts} +148 -168
  43. package/src/clients/port-client.ts +105 -0
  44. package/src/clients/process-client.ts +177 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +84 -0
  47. package/src/clients/utility-client.ts +94 -0
  48. package/src/errors/adapter.ts +180 -0
  49. package/src/errors/classes.ts +469 -0
  50. package/src/errors/index.ts +105 -0
  51. package/src/file-stream.ts +164 -0
  52. package/src/index.ts +82 -53
  53. package/src/interpreter.ts +22 -13
  54. package/src/request-handler.ts +69 -43
  55. package/src/sandbox.ts +697 -527
  56. package/src/security.ts +14 -23
  57. package/src/sse-parser.ts +4 -8
  58. package/startup.sh +3 -0
  59. package/tests/base-client.test.ts +328 -0
  60. package/tests/command-client.test.ts +407 -0
  61. package/tests/file-client.test.ts +643 -0
  62. package/tests/file-stream.test.ts +306 -0
  63. package/tests/git-client.test.ts +328 -0
  64. package/tests/port-client.test.ts +301 -0
  65. package/tests/process-client.test.ts +658 -0
  66. package/tests/sandbox.test.ts +465 -0
  67. package/tests/sse-parser.test.ts +290 -0
  68. package/tests/utility-client.test.ts +266 -0
  69. package/tests/wrangler.jsonc +35 -0
  70. package/tsconfig.json +9 -1
  71. package/vitest.config.ts +31 -0
  72. package/container_src/bun.lock +0 -122
  73. package/container_src/circuit-breaker.ts +0 -121
  74. package/container_src/control-process.ts +0 -784
  75. package/container_src/handler/exec.ts +0 -185
  76. package/container_src/handler/file.ts +0 -406
  77. package/container_src/handler/git.ts +0 -130
  78. package/container_src/handler/ports.ts +0 -314
  79. package/container_src/handler/process.ts +0 -568
  80. package/container_src/handler/session.ts +0 -92
  81. package/container_src/index.ts +0 -601
  82. package/container_src/isolation.ts +0 -1038
  83. package/container_src/jupyter-server.ts +0 -579
  84. package/container_src/jupyter-service.ts +0 -461
  85. package/container_src/jupyter_config.py +0 -48
  86. package/container_src/mime-processor.ts +0 -255
  87. package/container_src/package.json +0 -18
  88. package/container_src/shell-escape.ts +0 -42
  89. package/container_src/startup.sh +0 -84
  90. package/container_src/types.ts +0 -131
  91. package/src/client.ts +0 -1009
  92. package/src/errors.ts +0 -218
  93. package/src/interpreter-types.ts +0 -383
  94. package/src/types.ts +0 -502
@@ -1,601 +0,0 @@
1
- import { serve } from "bun";
2
- import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
3
- import {
4
- handleDeleteFileRequest,
5
- handleListFilesRequest,
6
- handleMkdirRequest,
7
- handleMoveFileRequest,
8
- handleReadFileRequest,
9
- handleRenameFileRequest,
10
- handleWriteFileRequest,
11
- } from "./handler/file";
12
- import { handleGitCheckoutRequest } from "./handler/git";
13
- import {
14
- handleExposePortRequest,
15
- handleGetExposedPortsRequest,
16
- handleProxyRequest,
17
- handleUnexposePortRequest,
18
- } from "./handler/ports";
19
- import {
20
- handleGetProcessLogsRequest,
21
- handleGetProcessRequest,
22
- handleKillAllProcessesRequest,
23
- handleKillProcessRequest,
24
- handleListProcessesRequest,
25
- handleStartProcessRequest,
26
- handleStreamProcessLogsRequest,
27
- } from "./handler/process";
28
- import { handleCreateSession, handleListSessions } from "./handler/session";
29
- import { hasNamespaceSupport, SessionManager } from "./isolation";
30
- import type { CreateContextRequest } from "./jupyter-server";
31
- import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
32
-
33
- // In-memory storage for exposed ports
34
- const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
35
-
36
- // Check isolation capabilities on startup
37
- const isolationAvailable = hasNamespaceSupport();
38
- console.log(
39
- `[Container] Process isolation: ${
40
- isolationAvailable
41
- ? "ENABLED (production mode)"
42
- : "DISABLED (development mode)"
43
- }`
44
- );
45
-
46
- // Session manager for secure execution with isolation
47
- const sessionManager = new SessionManager();
48
-
49
- // Graceful shutdown handler
50
- const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
51
-
52
- process.on("SIGTERM", async () => {
53
- console.log("[Container] SIGTERM received, cleaning up sessions...");
54
- await sessionManager.destroyAll();
55
- setTimeout(() => {
56
- process.exit(0);
57
- }, SHUTDOWN_GRACE_PERIOD_MS);
58
- });
59
-
60
- process.on("SIGINT", async () => {
61
- console.log("[Container] SIGINT received, cleaning up sessions...");
62
- await sessionManager.destroyAll();
63
- setTimeout(() => {
64
- process.exit(0);
65
- }, SHUTDOWN_GRACE_PERIOD_MS);
66
- });
67
-
68
- // Cleanup on uncaught exceptions (log but still exit)
69
- process.on("uncaughtException", async (error) => {
70
- console.error("[Container] Uncaught exception:", error);
71
- await sessionManager.destroyAll();
72
- process.exit(1);
73
- });
74
-
75
- // Initialize Jupyter service with graceful degradation
76
- const jupyterService = new JupyterService();
77
-
78
- // Start Jupyter initialization in background (non-blocking)
79
- console.log("[Container] Starting Jupyter initialization in background...");
80
- console.log(
81
- "[Container] API endpoints are available immediately. Jupyter-dependent features will be available shortly."
82
- );
83
-
84
- jupyterService
85
- .initialize()
86
- .then(() => {
87
- console.log(
88
- "[Container] Jupyter fully initialized - all features available"
89
- );
90
- })
91
- .catch((error) => {
92
- console.error("[Container] Jupyter initialization failed:", error.message);
93
- console.error(
94
- "[Container] The API will continue in degraded mode without code execution capabilities"
95
- );
96
- });
97
-
98
- const server = serve({
99
- async fetch(req: Request) {
100
- const url = new URL(req.url);
101
- const pathname = url.pathname;
102
-
103
- console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
104
-
105
- // Handle CORS
106
- const corsHeaders = {
107
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
108
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
109
- "Access-Control-Allow-Origin": "*",
110
- };
111
-
112
- // Handle preflight requests
113
- if (req.method === "OPTIONS") {
114
- console.log(`[Container] Handling CORS preflight for ${pathname}`);
115
- return new Response(null, { headers: corsHeaders, status: 200 });
116
- }
117
-
118
- try {
119
- // Handle different routes
120
- console.log(`[Container] Processing ${req.method} ${pathname}`);
121
- switch (pathname) {
122
- case "/":
123
- return new Response("Hello from Bun server! 🚀", {
124
- headers: {
125
- "Content-Type": "text/plain; charset=utf-8",
126
- ...corsHeaders,
127
- },
128
- });
129
-
130
- case "/api/session/create":
131
- if (req.method === "POST") {
132
- return handleCreateSession(req, corsHeaders, sessionManager);
133
- }
134
- break;
135
-
136
- case "/api/session/list":
137
- if (req.method === "GET") {
138
- return handleListSessions(corsHeaders, sessionManager);
139
- }
140
- break;
141
-
142
- case "/api/execute":
143
- if (req.method === "POST") {
144
- return handleExecuteRequest(req, corsHeaders, sessionManager);
145
- }
146
- break;
147
-
148
- case "/api/execute/stream":
149
- if (req.method === "POST") {
150
- return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
151
- }
152
- break;
153
-
154
- case "/api/ping":
155
- if (req.method === "GET") {
156
- const health = await jupyterService.getHealthStatus();
157
- return new Response(
158
- JSON.stringify({
159
- message: "pong",
160
- timestamp: new Date().toISOString(),
161
- jupyter: health.ready
162
- ? "ready"
163
- : health.initializing
164
- ? "initializing"
165
- : "not ready",
166
- jupyterHealth: health,
167
- }),
168
- {
169
- headers: {
170
- "Content-Type": "application/json",
171
- ...corsHeaders,
172
- },
173
- }
174
- );
175
- }
176
- break;
177
-
178
- case "/api/git/checkout":
179
- if (req.method === "POST") {
180
- return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
181
- }
182
- break;
183
-
184
- case "/api/mkdir":
185
- if (req.method === "POST") {
186
- return handleMkdirRequest(req, corsHeaders, sessionManager);
187
- }
188
- break;
189
-
190
- case "/api/write":
191
- if (req.method === "POST") {
192
- return handleWriteFileRequest(req, corsHeaders, sessionManager);
193
- }
194
- break;
195
-
196
- case "/api/read":
197
- if (req.method === "POST") {
198
- return handleReadFileRequest(req, corsHeaders, sessionManager);
199
- }
200
- break;
201
-
202
- case "/api/delete":
203
- if (req.method === "POST") {
204
- return handleDeleteFileRequest(req, corsHeaders, sessionManager);
205
- }
206
- break;
207
-
208
- case "/api/rename":
209
- if (req.method === "POST") {
210
- return handleRenameFileRequest(req, corsHeaders, sessionManager);
211
- }
212
- break;
213
-
214
- case "/api/move":
215
- if (req.method === "POST") {
216
- return handleMoveFileRequest(req, corsHeaders, sessionManager);
217
- }
218
- break;
219
-
220
- case "/api/list-files":
221
- if (req.method === "POST") {
222
- return handleListFilesRequest(req, corsHeaders, sessionManager);
223
- }
224
- break;
225
-
226
- case "/api/expose-port":
227
- if (req.method === "POST") {
228
- return handleExposePortRequest(exposedPorts, req, corsHeaders);
229
- }
230
- break;
231
-
232
- case "/api/unexpose-port":
233
- if (req.method === "DELETE") {
234
- return handleUnexposePortRequest(exposedPorts, req, corsHeaders);
235
- }
236
- break;
237
-
238
- case "/api/exposed-ports":
239
- if (req.method === "GET") {
240
- return handleGetExposedPortsRequest(exposedPorts, req, corsHeaders);
241
- }
242
- break;
243
-
244
- case "/api/process/start":
245
- if (req.method === "POST") {
246
- return handleStartProcessRequest(req, corsHeaders, sessionManager);
247
- }
248
- break;
249
-
250
- case "/api/process/list":
251
- if (req.method === "GET") {
252
- return handleListProcessesRequest(req, corsHeaders, sessionManager);
253
- }
254
- break;
255
-
256
- case "/api/process/kill-all":
257
- if (req.method === "DELETE") {
258
- return handleKillAllProcessesRequest(
259
- req,
260
- corsHeaders,
261
- sessionManager
262
- );
263
- }
264
- break;
265
-
266
- case "/api/contexts":
267
- if (req.method === "POST") {
268
- try {
269
- const body = (await req.json()) as CreateContextRequest;
270
- const context = await jupyterService.createContext(body);
271
- return new Response(
272
- JSON.stringify({
273
- id: context.id,
274
- language: context.language,
275
- cwd: context.cwd,
276
- createdAt: context.createdAt,
277
- lastUsed: context.lastUsed,
278
- }),
279
- {
280
- headers: {
281
- "Content-Type": "application/json",
282
- ...corsHeaders,
283
- },
284
- }
285
- );
286
- } catch (error) {
287
- if (error instanceof JupyterNotReadyError) {
288
- console.log(
289
- `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
290
- );
291
- return new Response(
292
- JSON.stringify({
293
- error: error.message,
294
- status: "initializing",
295
- progress: error.progress,
296
- }),
297
- {
298
- status: 503,
299
- headers: {
300
- "Content-Type": "application/json",
301
- "Retry-After": String(error.retryAfter),
302
- ...corsHeaders,
303
- },
304
- }
305
- );
306
- }
307
-
308
- // Check if it's a circuit breaker error
309
- if (
310
- error instanceof Error &&
311
- error.message.includes("Circuit breaker is open")
312
- ) {
313
- console.log(
314
- "[Container] Circuit breaker is open:",
315
- error.message
316
- );
317
- return new Response(
318
- JSON.stringify({
319
- error:
320
- "Service temporarily unavailable due to high error rate. Please try again later.",
321
- status: "circuit_open",
322
- details: error.message,
323
- }),
324
- {
325
- status: 503,
326
- headers: {
327
- "Content-Type": "application/json",
328
- "Retry-After": "60",
329
- ...corsHeaders,
330
- },
331
- }
332
- );
333
- }
334
-
335
- // Only log actual errors with stack traces
336
- console.error("[Container] Error creating context:", error);
337
- return new Response(
338
- JSON.stringify({
339
- error:
340
- error instanceof Error
341
- ? error.message
342
- : "Failed to create context",
343
- }),
344
- {
345
- status: 500,
346
- headers: {
347
- "Content-Type": "application/json",
348
- ...corsHeaders,
349
- },
350
- }
351
- );
352
- }
353
- } else if (req.method === "GET") {
354
- const contexts = await jupyterService.listContexts();
355
- return new Response(JSON.stringify({ contexts }), {
356
- headers: {
357
- "Content-Type": "application/json",
358
- ...corsHeaders,
359
- },
360
- });
361
- }
362
- break;
363
-
364
- case "/api/execute/code":
365
- if (req.method === "POST") {
366
- try {
367
- const body = (await req.json()) as {
368
- context_id: string;
369
- code: string;
370
- language?: string;
371
- };
372
- return await jupyterService.executeCode(
373
- body.context_id,
374
- body.code,
375
- body.language
376
- );
377
- } catch (error) {
378
- // Check if it's a circuit breaker error
379
- if (
380
- error instanceof Error &&
381
- error.message.includes("Circuit breaker is open")
382
- ) {
383
- console.log(
384
- "[Container] Circuit breaker is open for code execution:",
385
- error.message
386
- );
387
- return new Response(
388
- JSON.stringify({
389
- error:
390
- "Service temporarily unavailable due to high error rate. Please try again later.",
391
- status: "circuit_open",
392
- details: error.message,
393
- }),
394
- {
395
- status: 503,
396
- headers: {
397
- "Content-Type": "application/json",
398
- "Retry-After": "30",
399
- ...corsHeaders,
400
- },
401
- }
402
- );
403
- }
404
-
405
- // Don't log stack traces for expected initialization state
406
- if (
407
- error instanceof Error &&
408
- error.message.includes("initializing")
409
- ) {
410
- console.log(
411
- "[Container] Code execution deferred - Jupyter still initializing"
412
- );
413
- } else {
414
- console.error("[Container] Error executing code:", error);
415
- }
416
- // Error response is already handled by jupyterService.executeCode for not ready state
417
- return new Response(
418
- JSON.stringify({
419
- error:
420
- error instanceof Error
421
- ? error.message
422
- : "Failed to execute code",
423
- }),
424
- {
425
- status: 500,
426
- headers: {
427
- "Content-Type": "application/json",
428
- ...corsHeaders,
429
- },
430
- }
431
- );
432
- }
433
- }
434
- break;
435
-
436
- default:
437
- // Handle dynamic routes for contexts
438
- if (
439
- pathname.startsWith("/api/contexts/") &&
440
- pathname.split("/").length === 4
441
- ) {
442
- const contextId = pathname.split("/")[3];
443
- if (req.method === "DELETE") {
444
- try {
445
- await jupyterService.deleteContext(contextId);
446
- return new Response(JSON.stringify({ success: true }), {
447
- headers: {
448
- "Content-Type": "application/json",
449
- ...corsHeaders,
450
- },
451
- });
452
- } catch (error) {
453
- if (error instanceof JupyterNotReadyError) {
454
- console.log(
455
- `[Container] Request timed out waiting for Jupyter (${error.progress}% complete)`
456
- );
457
- return new Response(
458
- JSON.stringify({
459
- error: error.message,
460
- status: "initializing",
461
- progress: error.progress,
462
- }),
463
- {
464
- status: 503,
465
- headers: {
466
- "Content-Type": "application/json",
467
- "Retry-After": "5",
468
- ...corsHeaders,
469
- },
470
- }
471
- );
472
- }
473
- return new Response(
474
- JSON.stringify({
475
- error:
476
- error instanceof Error
477
- ? error.message
478
- : "Failed to delete context",
479
- }),
480
- {
481
- status:
482
- error instanceof Error &&
483
- error.message.includes("not found")
484
- ? 404
485
- : 500,
486
- headers: {
487
- "Content-Type": "application/json",
488
- ...corsHeaders,
489
- },
490
- }
491
- );
492
- }
493
- }
494
- }
495
-
496
- // Handle dynamic routes for individual processes
497
- if (pathname.startsWith("/api/process/")) {
498
- const segments = pathname.split("/");
499
- if (segments.length >= 4) {
500
- const processId = segments[3];
501
- const action = segments[4]; // Optional: logs, stream, etc.
502
-
503
- if (!action && req.method === "GET") {
504
- return handleGetProcessRequest(
505
- req,
506
- corsHeaders,
507
- processId,
508
- sessionManager
509
- );
510
- } else if (!action && req.method === "DELETE") {
511
- return handleKillProcessRequest(
512
- req,
513
- corsHeaders,
514
- processId,
515
- sessionManager
516
- );
517
- } else if (action === "logs" && req.method === "GET") {
518
- return handleGetProcessLogsRequest(
519
- req,
520
- corsHeaders,
521
- processId,
522
- sessionManager
523
- );
524
- } else if (action === "stream" && req.method === "GET") {
525
- return handleStreamProcessLogsRequest(
526
- req,
527
- corsHeaders,
528
- processId,
529
- sessionManager
530
- );
531
- }
532
- }
533
- }
534
- // Check if this is a proxy request for an exposed port
535
- if (pathname.startsWith("/proxy/")) {
536
- return handleProxyRequest(exposedPorts, req, corsHeaders);
537
- }
538
-
539
- console.log(`[Container] Route not found: ${pathname}`);
540
- return new Response("Not Found", {
541
- headers: corsHeaders,
542
- status: 404,
543
- });
544
- }
545
- } catch (error) {
546
- console.error(
547
- `[Container] Error handling ${req.method} ${pathname}:`,
548
- error
549
- );
550
- return new Response(
551
- JSON.stringify({
552
- error: "Internal server error",
553
- message: error instanceof Error ? error.message : "Unknown error",
554
- }),
555
- {
556
- headers: {
557
- "Content-Type": "application/json",
558
- ...corsHeaders,
559
- },
560
- status: 500,
561
- }
562
- );
563
- }
564
- },
565
- hostname: "0.0.0.0",
566
- port: 3000,
567
- // We don't need this, but typescript complains
568
- websocket: { async message() {} },
569
- });
570
-
571
- console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
572
- console.log(`📡 HTTP API endpoints available:`);
573
- console.log(` POST /api/session/create - Create a new session`);
574
- console.log(` GET /api/session/list - List all sessions`);
575
- console.log(` POST /api/execute - Execute a command (non-streaming)`);
576
- console.log(` POST /api/execute/stream - Execute a command (streaming)`);
577
- console.log(` POST /api/git/checkout - Checkout a git repository`);
578
- console.log(` POST /api/mkdir - Create a directory`);
579
- console.log(` POST /api/write - Write a file`);
580
- console.log(` POST /api/read - Read a file`);
581
- console.log(` POST /api/delete - Delete a file`);
582
- console.log(` POST /api/rename - Rename a file`);
583
- console.log(` POST /api/move - Move a file`);
584
- console.log(` POST /api/expose-port - Expose a port for external access`);
585
- console.log(` DELETE /api/unexpose-port - Unexpose a port`);
586
- console.log(` GET /api/exposed-ports - List exposed ports`);
587
- console.log(` POST /api/process/start - Start a background process`);
588
- console.log(` GET /api/process/list - List all processes`);
589
- console.log(` GET /api/process/{id} - Get process status`);
590
- console.log(` DELETE /api/process/{id} - Kill a process`);
591
- console.log(` GET /api/process/{id}/logs - Get process logs`);
592
- console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
593
- console.log(` DELETE /api/process/kill-all - Kill all processes`);
594
- console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
595
- console.log(` POST /api/contexts - Create a code execution context`);
596
- console.log(` GET /api/contexts - List all contexts`);
597
- console.log(` DELETE /api/contexts/{id} - Delete a context`);
598
- console.log(
599
- ` POST /api/execute/code - Execute code in a context (streaming)`
600
- );
601
- console.log(` GET /api/ping - Health check`);