@cloudflare/sandbox 0.0.0-0608f1e → 0.0.0-12bbd12

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.
@@ -1,8 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { serve } from "bun";
3
- import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
3
+ import {
4
+ handleExecuteRequest,
5
+ handleStreamingExecuteRequest,
6
+ } from "./handler/exec";
4
7
  import {
5
8
  handleDeleteFileRequest,
9
+ handleListFilesRequest,
6
10
  handleMkdirRequest,
7
11
  handleMoveFileRequest,
8
12
  handleReadFileRequest,
@@ -25,6 +29,8 @@ import {
25
29
  handleStartProcessRequest,
26
30
  handleStreamProcessLogsRequest,
27
31
  } from "./handler/process";
32
+ import type { CreateContextRequest } from "./jupyter-server";
33
+ import { JupyterNotReadyError, JupyterService } from "./jupyter-service";
28
34
  import type { ProcessRecord, SessionData } from "./types";
29
35
 
30
36
  // In-memory session storage (in production, you'd want to use a proper database)
@@ -38,7 +44,7 @@ const processes = new Map<string, ProcessRecord>();
38
44
 
39
45
  // Generate a unique session ID using cryptographically secure randomness
40
46
  function generateSessionId(): string {
41
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
47
+ return `session_${Date.now()}_${randomBytes(6).toString("hex")}`;
42
48
  }
43
49
 
44
50
  // Clean up old sessions (older than 1 hour)
@@ -55,8 +61,31 @@ function cleanupOldSessions() {
55
61
  // Run cleanup every 10 minutes
56
62
  setInterval(cleanupOldSessions, 10 * 60 * 1000);
57
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
+
58
87
  const server = serve({
59
- fetch(req: Request) {
88
+ async fetch(req: Request) {
60
89
  const url = new URL(req.url);
61
90
  const pathname = url.pathname;
62
91
 
@@ -155,10 +184,17 @@ const server = serve({
155
184
 
156
185
  case "/api/ping":
157
186
  if (req.method === "GET") {
187
+ const health = await jupyterService.getHealthStatus();
158
188
  return new Response(
159
189
  JSON.stringify({
160
190
  message: "pong",
161
191
  timestamp: new Date().toISOString(),
192
+ jupyter: health.ready
193
+ ? "ready"
194
+ : health.initializing
195
+ ? "initializing"
196
+ : "not ready",
197
+ jupyterHealth: health,
162
198
  }),
163
199
  {
164
200
  headers: {
@@ -244,6 +280,12 @@ const server = serve({
244
280
  }
245
281
  break;
246
282
 
283
+ case "/api/list-files":
284
+ if (req.method === "POST") {
285
+ return handleListFilesRequest(req, corsHeaders);
286
+ }
287
+ break;
288
+
247
289
  case "/api/expose-port":
248
290
  if (req.method === "POST") {
249
291
  return handleExposePortRequest(exposedPorts, req, corsHeaders);
@@ -280,22 +322,273 @@ const server = serve({
280
322
  }
281
323
  break;
282
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
+
283
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
+
284
557
  // Handle dynamic routes for individual processes
285
558
  if (pathname.startsWith("/api/process/")) {
286
- const segments = pathname.split('/');
559
+ const segments = pathname.split("/");
287
560
  if (segments.length >= 4) {
288
561
  const processId = segments[3];
289
562
  const action = segments[4]; // Optional: logs, stream, etc.
290
563
 
291
564
  if (!action && req.method === "GET") {
292
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
565
+ return handleGetProcessRequest(
566
+ processes,
567
+ req,
568
+ corsHeaders,
569
+ processId
570
+ );
293
571
  } else if (!action && req.method === "DELETE") {
294
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
572
+ return handleKillProcessRequest(
573
+ processes,
574
+ req,
575
+ corsHeaders,
576
+ processId
577
+ );
295
578
  } else if (action === "logs" && req.method === "GET") {
296
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
579
+ return handleGetProcessLogsRequest(
580
+ processes,
581
+ req,
582
+ corsHeaders,
583
+ processId
584
+ );
297
585
  } else if (action === "stream" && req.method === "GET") {
298
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
586
+ return handleStreamProcessLogsRequest(
587
+ processes,
588
+ req,
589
+ corsHeaders,
590
+ processId
591
+ );
299
592
  }
300
593
  }
301
594
  }
@@ -311,7 +604,10 @@ const server = serve({
311
604
  });
312
605
  }
313
606
  } catch (error) {
314
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
607
+ console.error(
608
+ `[Container] Error handling ${req.method} ${pathname}:`,
609
+ error
610
+ );
315
611
  return new Response(
316
612
  JSON.stringify({
317
613
  error: "Internal server error",
@@ -330,7 +626,7 @@ const server = serve({
330
626
  hostname: "0.0.0.0",
331
627
  port: 3000,
332
628
  // We don't need this, but typescript complains
333
- websocket: { async message() { } },
629
+ websocket: { async message() {} },
334
630
  });
335
631
 
336
632
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -357,5 +653,11 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
653
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
654
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
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
+ );
360
662
  console.log(` GET /api/ping - Health check`);
361
663
  console.log(` GET /api/commands - List available commands`);