@cloudflare/sandbox 0.0.0-60af265 → 0.0.0-6704961

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,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
2
2
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import { serve } from "bun";
@@ -6,6 +6,8 @@ import { serve } from "bun";
6
6
  interface ExecuteRequest {
7
7
  command: string;
8
8
  args?: string[];
9
+ sessionId?: string;
10
+ background?: boolean;
9
11
  }
10
12
 
11
13
  interface GitCheckoutRequest {
@@ -51,15 +53,27 @@ interface MoveFileRequest {
51
53
  sessionId?: string;
52
54
  }
53
55
 
56
+ interface ExposePortRequest {
57
+ port: number;
58
+ name?: string;
59
+ }
60
+
61
+ interface UnexposePortRequest {
62
+ port: number;
63
+ }
64
+
54
65
  interface SessionData {
55
66
  sessionId: string;
56
- activeProcess: any | null;
67
+ activeProcess: ChildProcess | null;
57
68
  createdAt: Date;
58
69
  }
59
70
 
60
71
  // In-memory session storage (in production, you'd want to use a proper database)
61
72
  const sessions = new Map<string, SessionData>();
62
73
 
74
+ // In-memory storage for exposed ports
75
+ const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
76
+
63
77
  // Generate a unique session ID
64
78
  function generateSessionId(): string {
65
79
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -111,51 +125,6 @@ const server = serve({
111
125
  },
112
126
  });
113
127
 
114
- case "/api/hello":
115
- return new Response(
116
- JSON.stringify({
117
- message: "Hello from API!",
118
- timestamp: new Date().toISOString(),
119
- }),
120
- {
121
- headers: {
122
- "Content-Type": "application/json",
123
- ...corsHeaders,
124
- },
125
- }
126
- );
127
-
128
- case "/api/users":
129
- if (req.method === "GET") {
130
- return new Response(
131
- JSON.stringify([
132
- { id: 1, name: "Alice" },
133
- { id: 2, name: "Bob" },
134
- { id: 3, name: "Charlie" },
135
- ]),
136
- {
137
- headers: {
138
- "Content-Type": "application/json",
139
- ...corsHeaders,
140
- },
141
- }
142
- );
143
- } else if (req.method === "POST") {
144
- return new Response(
145
- JSON.stringify({
146
- message: "User created successfully",
147
- method: "POST",
148
- }),
149
- {
150
- headers: {
151
- "Content-Type": "application/json",
152
- ...corsHeaders,
153
- },
154
- }
155
- );
156
- }
157
- break;
158
-
159
128
  case "/api/session/create":
160
129
  if (req.method === "POST") {
161
130
  const sessionId = generateSessionId();
@@ -355,7 +324,30 @@ const server = serve({
355
324
  }
356
325
  break;
357
326
 
327
+ case "/api/expose-port":
328
+ if (req.method === "POST") {
329
+ return handleExposePortRequest(req, corsHeaders);
330
+ }
331
+ break;
332
+
333
+ case "/api/unexpose-port":
334
+ if (req.method === "DELETE") {
335
+ return handleUnexposePortRequest(req, corsHeaders);
336
+ }
337
+ break;
338
+
339
+ case "/api/exposed-ports":
340
+ if (req.method === "GET") {
341
+ return handleGetExposedPortsRequest(req, corsHeaders);
342
+ }
343
+ break;
344
+
358
345
  default:
346
+ // Check if this is a proxy request for an exposed port
347
+ if (pathname.startsWith("/proxy/")) {
348
+ return handleProxyRequest(req, corsHeaders);
349
+ }
350
+
359
351
  console.log(`[Container] Route not found: ${pathname}`);
360
352
  return new Response("Not Found", {
361
353
  headers: corsHeaders,
@@ -381,15 +373,17 @@ const server = serve({
381
373
  },
382
374
  hostname: "0.0.0.0",
383
375
  port: 3000,
384
- } as any);
376
+ // We don't need this, but typescript complains
377
+ websocket: { async message() { } },
378
+ });
385
379
 
386
380
  async function handleExecuteRequest(
387
381
  req: Request,
388
382
  corsHeaders: Record<string, string>
389
383
  ): Promise<Response> {
390
384
  try {
391
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
392
- const { command, args = [], sessionId } = body;
385
+ const body = (await req.json()) as ExecuteRequest;
386
+ const { command, args = [], sessionId, background } = body;
393
387
 
394
388
  if (!command || typeof command !== "string") {
395
389
  return new Response(
@@ -406,37 +400,9 @@ async function handleExecuteRequest(
406
400
  );
407
401
  }
408
402
 
409
- // Basic safety check - prevent dangerous commands
410
- const dangerousCommands = [
411
- "rm",
412
- "rmdir",
413
- "del",
414
- "format",
415
- "shutdown",
416
- "reboot",
417
- ];
418
- const lowerCommand = command.toLowerCase();
419
-
420
- if (
421
- dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
422
- ) {
423
- return new Response(
424
- JSON.stringify({
425
- error: "Dangerous command not allowed",
426
- }),
427
- {
428
- headers: {
429
- "Content-Type": "application/json",
430
- ...corsHeaders,
431
- },
432
- status: 400,
433
- }
434
- );
435
- }
436
-
437
403
  console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
438
404
 
439
- const result = await executeCommand(command, args, sessionId);
405
+ const result = await executeCommand(command, args, sessionId, background);
440
406
 
441
407
  return new Response(
442
408
  JSON.stringify({
@@ -478,8 +444,8 @@ async function handleStreamingExecuteRequest(
478
444
  corsHeaders: Record<string, string>
479
445
  ): Promise<Response> {
480
446
  try {
481
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
482
- const { command, args = [], sessionId } = body;
447
+ const body = (await req.json()) as ExecuteRequest;
448
+ const { command, args = [], sessionId, background } = body;
483
449
 
484
450
  if (!command || typeof command !== "string") {
485
451
  return new Response(
@@ -496,44 +462,19 @@ async function handleStreamingExecuteRequest(
496
462
  );
497
463
  }
498
464
 
499
- // Basic safety check - prevent dangerous commands
500
- const dangerousCommands = [
501
- "rm",
502
- "rmdir",
503
- "del",
504
- "format",
505
- "shutdown",
506
- "reboot",
507
- ];
508
- const lowerCommand = command.toLowerCase();
509
-
510
- if (
511
- dangerousCommands.some((dangerous) => lowerCommand.includes(dangerous))
512
- ) {
513
- return new Response(
514
- JSON.stringify({
515
- error: "Dangerous command not allowed",
516
- }),
517
- {
518
- headers: {
519
- "Content-Type": "application/json",
520
- ...corsHeaders,
521
- },
522
- status: 400,
523
- }
524
- );
525
- }
526
-
527
465
  console.log(
528
466
  `[Server] Executing streaming command: ${command} ${args.join(" ")}`
529
467
  );
530
468
 
531
469
  const stream = new ReadableStream({
532
470
  start(controller) {
533
- const child = spawn(command, args, {
471
+ const spawnOptions: SpawnOptions = {
534
472
  shell: true,
535
- stdio: ["pipe", "pipe", "pipe"],
536
- });
473
+ stdio: ["pipe", "pipe", "pipe"] as const,
474
+ detached: background || false,
475
+ };
476
+
477
+ const child = spawn(command, args, spawnOptions);
537
478
 
538
479
  // Store the process reference for cleanup if sessionId is provided
539
480
  if (sessionId && sessions.has(sessionId)) {
@@ -541,6 +482,11 @@ async function handleStreamingExecuteRequest(
541
482
  session.activeProcess = child;
542
483
  }
543
484
 
485
+ // For background processes, unref to prevent blocking
486
+ if (background) {
487
+ child.unref();
488
+ }
489
+
544
490
  let stdout = "";
545
491
  let stderr = "";
546
492
 
@@ -552,6 +498,7 @@ async function handleStreamingExecuteRequest(
552
498
  command,
553
499
  timestamp: new Date().toISOString(),
554
500
  type: "command_start",
501
+ background: background || false,
555
502
  })}\n\n`
556
503
  )
557
504
  );
@@ -617,7 +564,11 @@ async function handleStreamingExecuteRequest(
617
564
  )
618
565
  );
619
566
 
620
- controller.close();
567
+ // For non-background processes, close the stream
568
+ // For background processes with streaming, the stream stays open
569
+ if (!background) {
570
+ controller.close();
571
+ }
621
572
  });
622
573
 
623
574
  child.on("error", (error) => {
@@ -2507,7 +2458,8 @@ async function handleStreamingMoveFileRequest(
2507
2458
  function executeCommand(
2508
2459
  command: string,
2509
2460
  args: string[],
2510
- sessionId?: string
2461
+ sessionId?: string,
2462
+ background?: boolean
2511
2463
  ): Promise<{
2512
2464
  success: boolean;
2513
2465
  stdout: string;
@@ -2515,10 +2467,13 @@ function executeCommand(
2515
2467
  exitCode: number;
2516
2468
  }> {
2517
2469
  return new Promise((resolve, reject) => {
2518
- const child = spawn(command, args, {
2470
+ const spawnOptions: SpawnOptions = {
2519
2471
  shell: true,
2520
- stdio: ["pipe", "pipe", "pipe"],
2521
- });
2472
+ stdio: ["pipe", "pipe", "pipe"] as const,
2473
+ detached: background || false,
2474
+ };
2475
+
2476
+ const child = spawn(command, args, spawnOptions);
2522
2477
 
2523
2478
  // Store the process reference for cleanup if sessionId is provided
2524
2479
  if (sessionId && sessions.has(sessionId)) {
@@ -2537,32 +2492,54 @@ function executeCommand(
2537
2492
  stderr += data.toString();
2538
2493
  });
2539
2494
 
2540
- child.on("close", (code) => {
2541
- // Clear the active process reference
2542
- if (sessionId && sessions.has(sessionId)) {
2543
- const session = sessions.get(sessionId)!;
2544
- session.activeProcess = null;
2545
- }
2495
+ if (background) {
2496
+ // For background processes, unref and return quickly
2497
+ child.unref();
2546
2498
 
2547
- console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2499
+ // Collect initial output for 100ms then return
2500
+ setTimeout(() => {
2501
+ resolve({
2502
+ exitCode: 0, // Process is still running
2503
+ stderr,
2504
+ stdout,
2505
+ success: true,
2506
+ });
2507
+ }, 100);
2548
2508
 
2549
- resolve({
2550
- exitCode: code || 0,
2551
- stderr,
2552
- stdout,
2553
- success: code === 0,
2509
+ // Still handle errors
2510
+ child.on("error", (error) => {
2511
+ console.error(`[Server] Background process error: ${command}`, error);
2512
+ // Don't reject since we might have already resolved
2554
2513
  });
2555
- });
2514
+ } else {
2515
+ // Normal synchronous execution
2516
+ child.on("close", (code) => {
2517
+ // Clear the active process reference
2518
+ if (sessionId && sessions.has(sessionId)) {
2519
+ const session = sessions.get(sessionId)!;
2520
+ session.activeProcess = null;
2521
+ }
2556
2522
 
2557
- child.on("error", (error) => {
2558
- // Clear the active process reference
2559
- if (sessionId && sessions.has(sessionId)) {
2560
- const session = sessions.get(sessionId)!;
2561
- session.activeProcess = null;
2562
- }
2523
+ console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2563
2524
 
2564
- reject(error);
2565
- });
2525
+ resolve({
2526
+ exitCode: code || 0,
2527
+ stderr,
2528
+ stdout,
2529
+ success: code === 0,
2530
+ });
2531
+ });
2532
+
2533
+ child.on("error", (error) => {
2534
+ // Clear the active process reference
2535
+ if (sessionId && sessions.has(sessionId)) {
2536
+ const session = sessions.get(sessionId)!;
2537
+ session.activeProcess = null;
2538
+ }
2539
+
2540
+ reject(error);
2541
+ });
2542
+ }
2566
2543
  });
2567
2544
  }
2568
2545
 
@@ -2880,6 +2857,315 @@ function executeMoveFile(
2880
2857
  });
2881
2858
  }
2882
2859
 
2860
+ async function handleExposePortRequest(
2861
+ req: Request,
2862
+ corsHeaders: Record<string, string>
2863
+ ): Promise<Response> {
2864
+ try {
2865
+ const body = (await req.json()) as ExposePortRequest;
2866
+ const { port, name } = body;
2867
+
2868
+ if (!port || typeof port !== "number") {
2869
+ return new Response(
2870
+ JSON.stringify({
2871
+ error: "Port is required and must be a number",
2872
+ }),
2873
+ {
2874
+ headers: {
2875
+ "Content-Type": "application/json",
2876
+ ...corsHeaders,
2877
+ },
2878
+ status: 400,
2879
+ }
2880
+ );
2881
+ }
2882
+
2883
+ // Validate port range
2884
+ if (port < 1 || port > 65535) {
2885
+ return new Response(
2886
+ JSON.stringify({
2887
+ error: "Port must be between 1 and 65535",
2888
+ }),
2889
+ {
2890
+ headers: {
2891
+ "Content-Type": "application/json",
2892
+ ...corsHeaders,
2893
+ },
2894
+ status: 400,
2895
+ }
2896
+ );
2897
+ }
2898
+
2899
+ // Store the exposed port
2900
+ exposedPorts.set(port, { name, exposedAt: new Date() });
2901
+
2902
+ console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
2903
+
2904
+ return new Response(
2905
+ JSON.stringify({
2906
+ port,
2907
+ name,
2908
+ exposedAt: new Date().toISOString(),
2909
+ success: true,
2910
+ timestamp: new Date().toISOString(),
2911
+ }),
2912
+ {
2913
+ headers: {
2914
+ "Content-Type": "application/json",
2915
+ ...corsHeaders,
2916
+ },
2917
+ }
2918
+ );
2919
+ } catch (error) {
2920
+ console.error("[Server] Error in handleExposePortRequest:", error);
2921
+ return new Response(
2922
+ JSON.stringify({
2923
+ error: "Failed to expose port",
2924
+ message: error instanceof Error ? error.message : "Unknown error",
2925
+ }),
2926
+ {
2927
+ headers: {
2928
+ "Content-Type": "application/json",
2929
+ ...corsHeaders,
2930
+ },
2931
+ status: 500,
2932
+ }
2933
+ );
2934
+ }
2935
+ }
2936
+
2937
+ async function handleUnexposePortRequest(
2938
+ req: Request,
2939
+ corsHeaders: Record<string, string>
2940
+ ): Promise<Response> {
2941
+ try {
2942
+ const body = (await req.json()) as UnexposePortRequest;
2943
+ const { port } = body;
2944
+
2945
+ if (!port || typeof port !== "number") {
2946
+ return new Response(
2947
+ JSON.stringify({
2948
+ error: "Port is required and must be a number",
2949
+ }),
2950
+ {
2951
+ headers: {
2952
+ "Content-Type": "application/json",
2953
+ ...corsHeaders,
2954
+ },
2955
+ status: 400,
2956
+ }
2957
+ );
2958
+ }
2959
+
2960
+ // Check if port is exposed
2961
+ if (!exposedPorts.has(port)) {
2962
+ return new Response(
2963
+ JSON.stringify({
2964
+ error: "Port is not exposed",
2965
+ }),
2966
+ {
2967
+ headers: {
2968
+ "Content-Type": "application/json",
2969
+ ...corsHeaders,
2970
+ },
2971
+ status: 404,
2972
+ }
2973
+ );
2974
+ }
2975
+
2976
+ // Remove the exposed port
2977
+ exposedPorts.delete(port);
2978
+
2979
+ console.log(`[Server] Unexposed port: ${port}`);
2980
+
2981
+ return new Response(
2982
+ JSON.stringify({
2983
+ port,
2984
+ success: true,
2985
+ timestamp: new Date().toISOString(),
2986
+ }),
2987
+ {
2988
+ headers: {
2989
+ "Content-Type": "application/json",
2990
+ ...corsHeaders,
2991
+ },
2992
+ }
2993
+ );
2994
+ } catch (error) {
2995
+ console.error("[Server] Error in handleUnexposePortRequest:", error);
2996
+ return new Response(
2997
+ JSON.stringify({
2998
+ error: "Failed to unexpose port",
2999
+ message: error instanceof Error ? error.message : "Unknown error",
3000
+ }),
3001
+ {
3002
+ headers: {
3003
+ "Content-Type": "application/json",
3004
+ ...corsHeaders,
3005
+ },
3006
+ status: 500,
3007
+ }
3008
+ );
3009
+ }
3010
+ }
3011
+
3012
+ async function handleGetExposedPortsRequest(
3013
+ req: Request,
3014
+ corsHeaders: Record<string, string>
3015
+ ): Promise<Response> {
3016
+ try {
3017
+ const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
3018
+ port,
3019
+ name: info.name,
3020
+ exposedAt: info.exposedAt.toISOString(),
3021
+ }));
3022
+
3023
+ return new Response(
3024
+ JSON.stringify({
3025
+ ports,
3026
+ count: ports.length,
3027
+ timestamp: new Date().toISOString(),
3028
+ }),
3029
+ {
3030
+ headers: {
3031
+ "Content-Type": "application/json",
3032
+ ...corsHeaders,
3033
+ },
3034
+ }
3035
+ );
3036
+ } catch (error) {
3037
+ console.error("[Server] Error in handleGetExposedPortsRequest:", error);
3038
+ return new Response(
3039
+ JSON.stringify({
3040
+ error: "Failed to get exposed ports",
3041
+ message: error instanceof Error ? error.message : "Unknown error",
3042
+ }),
3043
+ {
3044
+ headers: {
3045
+ "Content-Type": "application/json",
3046
+ ...corsHeaders,
3047
+ },
3048
+ status: 500,
3049
+ }
3050
+ );
3051
+ }
3052
+ }
3053
+
3054
+ async function handleProxyRequest(
3055
+ req: Request,
3056
+ corsHeaders: Record<string, string>
3057
+ ): Promise<Response> {
3058
+ try {
3059
+ const url = new URL(req.url);
3060
+ const pathParts = url.pathname.split("/");
3061
+
3062
+ // Extract port from path like /proxy/3000/...
3063
+ if (pathParts.length < 3) {
3064
+ return new Response(
3065
+ JSON.stringify({
3066
+ error: "Invalid proxy path",
3067
+ }),
3068
+ {
3069
+ headers: {
3070
+ "Content-Type": "application/json",
3071
+ ...corsHeaders,
3072
+ },
3073
+ status: 400,
3074
+ }
3075
+ );
3076
+ }
3077
+
3078
+ const port = parseInt(pathParts[2]);
3079
+ if (!port || Number.isNaN(port)) {
3080
+ return new Response(
3081
+ JSON.stringify({
3082
+ error: "Invalid port in proxy path",
3083
+ }),
3084
+ {
3085
+ headers: {
3086
+ "Content-Type": "application/json",
3087
+ ...corsHeaders,
3088
+ },
3089
+ status: 400,
3090
+ }
3091
+ );
3092
+ }
3093
+
3094
+ // Check if port is exposed
3095
+ if (!exposedPorts.has(port)) {
3096
+ return new Response(
3097
+ JSON.stringify({
3098
+ error: `Port ${port} is not exposed`,
3099
+ }),
3100
+ {
3101
+ headers: {
3102
+ "Content-Type": "application/json",
3103
+ ...corsHeaders,
3104
+ },
3105
+ status: 404,
3106
+ }
3107
+ );
3108
+ }
3109
+
3110
+ // Construct the target URL
3111
+ const targetPath = `/${pathParts.slice(3).join("/")}`;
3112
+ // Use 127.0.0.1 instead of localhost for more reliable container networking
3113
+ const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
3114
+
3115
+ console.log(`[Server] Proxying request to: ${targetUrl}`);
3116
+ console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
3117
+
3118
+ try {
3119
+ // Forward the request to the target port
3120
+ const targetResponse = await fetch(targetUrl, {
3121
+ method: req.method,
3122
+ headers: req.headers,
3123
+ body: req.body,
3124
+ });
3125
+
3126
+ // Return the response from the target
3127
+ return new Response(targetResponse.body, {
3128
+ status: targetResponse.status,
3129
+ statusText: targetResponse.statusText,
3130
+ headers: {
3131
+ ...Object.fromEntries(targetResponse.headers.entries()),
3132
+ ...corsHeaders,
3133
+ },
3134
+ });
3135
+ } catch (fetchError) {
3136
+ console.error(`[Server] Error proxying to port ${port}:`, fetchError);
3137
+ return new Response(
3138
+ JSON.stringify({
3139
+ error: `Service on port ${port} is not responding`,
3140
+ message: fetchError instanceof Error ? fetchError.message : "Unknown error",
3141
+ }),
3142
+ {
3143
+ headers: {
3144
+ "Content-Type": "application/json",
3145
+ ...corsHeaders,
3146
+ },
3147
+ status: 502,
3148
+ }
3149
+ );
3150
+ }
3151
+ } catch (error) {
3152
+ console.error("[Server] Error in handleProxyRequest:", error);
3153
+ return new Response(
3154
+ JSON.stringify({
3155
+ error: "Failed to proxy request",
3156
+ message: error instanceof Error ? error.message : "Unknown error",
3157
+ }),
3158
+ {
3159
+ headers: {
3160
+ "Content-Type": "application/json",
3161
+ ...corsHeaders,
3162
+ },
3163
+ status: 500,
3164
+ }
3165
+ );
3166
+ }
3167
+ }
3168
+
2883
3169
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
2884
3170
  console.log(`📡 HTTP API endpoints available:`);
2885
3171
  console.log(` POST /api/session/create - Create a new session`);
@@ -2902,5 +3188,9 @@ console.log(` POST /api/rename - Rename a file`);
2902
3188
  console.log(` POST /api/rename/stream - Rename a file (streaming)`);
2903
3189
  console.log(` POST /api/move - Move a file`);
2904
3190
  console.log(` POST /api/move/stream - Move a file (streaming)`);
3191
+ console.log(` POST /api/expose-port - Expose a port for external access`);
3192
+ console.log(` DELETE /api/unexpose-port - Unexpose a port`);
3193
+ console.log(` GET /api/exposed-ports - List exposed ports`);
3194
+ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
2905
3195
  console.log(` GET /api/ping - Health check`);
2906
3196
  console.log(` GET /api/commands - List available commands`);