@cloudflare/sandbox 0.0.0-0dad837 → 0.0.0-102fc4f

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