@cloudflare/sandbox 0.0.0-dc66e8e → 0.0.0-e1fa354

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,8 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { serve } from "bun";
3
2
  import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
4
3
  import {
5
4
  handleDeleteFileRequest,
5
+ handleListFilesRequest,
6
6
  handleMkdirRequest,
7
7
  handleMoveFileRequest,
8
8
  handleReadFileRequest,
@@ -25,38 +25,78 @@ import {
25
25
  handleStartProcessRequest,
26
26
  handleStreamProcessLogsRequest,
27
27
  } from "./handler/process";
28
- import type { ProcessRecord, SessionData } from "./types";
29
-
30
- // In-memory session storage (in production, you'd want to use a proper database)
31
- const sessions = new Map<string, SessionData>();
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
32
 
33
33
  // In-memory storage for exposed ports
34
34
  const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
35
35
 
36
- // In-memory process storage - cleared on container restart
37
- const processes = new Map<string, ProcessRecord>();
38
-
39
- // Generate a unique session ID using cryptographically secure randomness
40
- function generateSessionId(): string {
41
- return `session_${Date.now()}_${randomBytes(6).toString('hex')}`;
42
- }
43
-
44
- // Clean up old sessions (older than 1 hour)
45
- function cleanupOldSessions() {
46
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
47
- for (const [sessionId, session] of sessions.entries()) {
48
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
49
- sessions.delete(sessionId);
50
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
51
- }
52
- }
53
- }
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
+ });
54
67
 
55
- // Run cleanup every 10 minutes
56
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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
+ });
57
97
 
58
98
  const server = serve({
59
- fetch(req: Request) {
99
+ async fetch(req: Request) {
60
100
  const url = new URL(req.url);
61
101
  const pathname = url.pathname;
62
102
 
@@ -89,108 +129,41 @@ const server = serve({
89
129
 
90
130
  case "/api/session/create":
91
131
  if (req.method === "POST") {
92
- const sessionId = generateSessionId();
93
- const sessionData: SessionData = {
94
- activeProcess: null,
95
- createdAt: new Date(),
96
- sessionId,
97
- };
98
- sessions.set(sessionId, sessionData);
99
-
100
- console.log(`[Server] Created new session: ${sessionId}`);
101
-
102
- return new Response(
103
- JSON.stringify({
104
- message: "Session created successfully",
105
- sessionId,
106
- timestamp: new Date().toISOString(),
107
- }),
108
- {
109
- headers: {
110
- "Content-Type": "application/json",
111
- ...corsHeaders,
112
- },
113
- }
114
- );
132
+ return handleCreateSession(req, corsHeaders, sessionManager);
115
133
  }
116
134
  break;
117
135
 
118
136
  case "/api/session/list":
119
137
  if (req.method === "GET") {
120
- const sessionList = Array.from(sessions.values()).map(
121
- (session) => ({
122
- createdAt: session.createdAt.toISOString(),
123
- hasActiveProcess: !!session.activeProcess,
124
- sessionId: session.sessionId,
125
- })
126
- );
127
-
128
- return new Response(
129
- JSON.stringify({
130
- count: sessionList.length,
131
- sessions: sessionList,
132
- timestamp: new Date().toISOString(),
133
- }),
134
- {
135
- headers: {
136
- "Content-Type": "application/json",
137
- ...corsHeaders,
138
- },
139
- }
140
- );
138
+ return handleListSessions(corsHeaders, sessionManager);
141
139
  }
142
140
  break;
143
141
 
144
142
  case "/api/execute":
145
143
  if (req.method === "POST") {
146
- return handleExecuteRequest(sessions, req, corsHeaders);
144
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
147
145
  }
148
146
  break;
149
-
147
+
150
148
  case "/api/execute/stream":
151
149
  if (req.method === "POST") {
152
- return handleStreamingExecuteRequest(sessions, req, corsHeaders);
150
+ return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
153
151
  }
154
152
  break;
155
153
 
156
154
  case "/api/ping":
157
155
  if (req.method === "GET") {
156
+ const health = await jupyterService.getHealthStatus();
158
157
  return new Response(
159
158
  JSON.stringify({
160
159
  message: "pong",
161
160
  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/commands":
174
- if (req.method === "GET") {
175
- return new Response(
176
- JSON.stringify({
177
- availableCommands: [
178
- "ls",
179
- "pwd",
180
- "echo",
181
- "cat",
182
- "grep",
183
- "find",
184
- "whoami",
185
- "date",
186
- "uptime",
187
- "ps",
188
- "top",
189
- "df",
190
- "du",
191
- "free",
192
- ],
193
- timestamp: new Date().toISOString(),
161
+ jupyter: health.ready
162
+ ? "ready"
163
+ : health.initializing
164
+ ? "initializing"
165
+ : "not ready",
166
+ jupyterHealth: health,
194
167
  }),
195
168
  {
196
169
  headers: {
@@ -204,43 +177,49 @@ const server = serve({
204
177
 
205
178
  case "/api/git/checkout":
206
179
  if (req.method === "POST") {
207
- return handleGitCheckoutRequest(sessions, req, corsHeaders);
180
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
208
181
  }
209
182
  break;
210
183
 
211
184
  case "/api/mkdir":
212
185
  if (req.method === "POST") {
213
- return handleMkdirRequest(sessions, req, corsHeaders);
186
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
214
187
  }
215
188
  break;
216
189
 
217
190
  case "/api/write":
218
191
  if (req.method === "POST") {
219
- return handleWriteFileRequest(req, corsHeaders);
192
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
220
193
  }
221
194
  break;
222
195
 
223
196
  case "/api/read":
224
197
  if (req.method === "POST") {
225
- return handleReadFileRequest(req, corsHeaders);
198
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
226
199
  }
227
200
  break;
228
201
 
229
202
  case "/api/delete":
230
203
  if (req.method === "POST") {
231
- return handleDeleteFileRequest(req, corsHeaders);
204
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
232
205
  }
233
206
  break;
234
207
 
235
208
  case "/api/rename":
236
209
  if (req.method === "POST") {
237
- return handleRenameFileRequest(req, corsHeaders);
210
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
238
211
  }
239
212
  break;
240
213
 
241
214
  case "/api/move":
242
215
  if (req.method === "POST") {
243
- return handleMoveFileRequest(req, corsHeaders);
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);
244
223
  }
245
224
  break;
246
225
 
@@ -264,38 +243,291 @@ const server = serve({
264
243
 
265
244
  case "/api/process/start":
266
245
  if (req.method === "POST") {
267
- return handleStartProcessRequest(processes, req, corsHeaders);
246
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
268
247
  }
269
248
  break;
270
249
 
271
250
  case "/api/process/list":
272
251
  if (req.method === "GET") {
273
- return handleListProcessesRequest(processes, req, corsHeaders);
252
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
274
253
  }
275
254
  break;
276
255
 
277
256
  case "/api/process/kill-all":
278
257
  if (req.method === "DELETE") {
279
- return handleKillAllProcessesRequest(processes, req, corsHeaders);
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
+ }
280
433
  }
281
434
  break;
282
435
 
283
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
+
284
496
  // Handle dynamic routes for individual processes
285
497
  if (pathname.startsWith("/api/process/")) {
286
- const segments = pathname.split('/');
498
+ const segments = pathname.split("/");
287
499
  if (segments.length >= 4) {
288
500
  const processId = segments[3];
289
501
  const action = segments[4]; // Optional: logs, stream, etc.
290
502
 
291
503
  if (!action && req.method === "GET") {
292
- return handleGetProcessRequest(processes, req, corsHeaders, processId);
504
+ return handleGetProcessRequest(
505
+ req,
506
+ corsHeaders,
507
+ processId,
508
+ sessionManager
509
+ );
293
510
  } else if (!action && req.method === "DELETE") {
294
- return handleKillProcessRequest(processes, req, corsHeaders, processId);
511
+ return handleKillProcessRequest(
512
+ req,
513
+ corsHeaders,
514
+ processId,
515
+ sessionManager
516
+ );
295
517
  } else if (action === "logs" && req.method === "GET") {
296
- return handleGetProcessLogsRequest(processes, req, corsHeaders, processId);
518
+ return handleGetProcessLogsRequest(
519
+ req,
520
+ corsHeaders,
521
+ processId,
522
+ sessionManager
523
+ );
297
524
  } else if (action === "stream" && req.method === "GET") {
298
- return handleStreamProcessLogsRequest(processes, req, corsHeaders, processId);
525
+ return handleStreamProcessLogsRequest(
526
+ req,
527
+ corsHeaders,
528
+ processId,
529
+ sessionManager
530
+ );
299
531
  }
300
532
  }
301
533
  }
@@ -311,7 +543,10 @@ const server = serve({
311
543
  });
312
544
  }
313
545
  } catch (error) {
314
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
546
+ console.error(
547
+ `[Container] Error handling ${req.method} ${pathname}:`,
548
+ error
549
+ );
315
550
  return new Response(
316
551
  JSON.stringify({
317
552
  error: "Internal server error",
@@ -330,7 +565,7 @@ const server = serve({
330
565
  hostname: "0.0.0.0",
331
566
  port: 3000,
332
567
  // We don't need this, but typescript complains
333
- websocket: { async message() { } },
568
+ websocket: { async message() {} },
334
569
  });
335
570
 
336
571
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
@@ -357,5 +592,10 @@ console.log(` GET /api/process/{id}/logs - Get process logs`);
357
592
  console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
358
593
  console.log(` DELETE /api/process/kill-all - Kill all processes`);
359
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
+ );
360
601
  console.log(` GET /api/ping - Health check`);
361
- console.log(` GET /api/commands - List available commands`);