@cloudflare/sandbox 0.0.0-eb0ea62 → 0.0.0-f106fda

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)}`;
@@ -84,6 +98,8 @@ const server = serve({
84
98
  const url = new URL(req.url);
85
99
  const pathname = url.pathname;
86
100
 
101
+ console.log(`[Container] Incoming ${req.method} request to ${pathname}`);
102
+
87
103
  // Handle CORS
88
104
  const corsHeaders = {
89
105
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
@@ -93,11 +109,13 @@ const server = serve({
93
109
 
94
110
  // Handle preflight requests
95
111
  if (req.method === "OPTIONS") {
112
+ console.log(`[Container] Handling CORS preflight for ${pathname}`);
96
113
  return new Response(null, { headers: corsHeaders, status: 200 });
97
114
  }
98
115
 
99
116
  try {
100
117
  // Handle different routes
118
+ console.log(`[Container] Processing ${req.method} ${pathname}`);
101
119
  switch (pathname) {
102
120
  case "/":
103
121
  return new Response("Hello from Bun server! 🚀", {
@@ -107,51 +125,6 @@ const server = serve({
107
125
  },
108
126
  });
109
127
 
110
- case "/api/hello":
111
- return new Response(
112
- JSON.stringify({
113
- message: "Hello from API!",
114
- timestamp: new Date().toISOString(),
115
- }),
116
- {
117
- headers: {
118
- "Content-Type": "application/json",
119
- ...corsHeaders,
120
- },
121
- }
122
- );
123
-
124
- case "/api/users":
125
- if (req.method === "GET") {
126
- return new Response(
127
- JSON.stringify([
128
- { id: 1, name: "Alice" },
129
- { id: 2, name: "Bob" },
130
- { id: 3, name: "Charlie" },
131
- ]),
132
- {
133
- headers: {
134
- "Content-Type": "application/json",
135
- ...corsHeaders,
136
- },
137
- }
138
- );
139
- } else if (req.method === "POST") {
140
- return new Response(
141
- JSON.stringify({
142
- message: "User created successfully",
143
- method: "POST",
144
- }),
145
- {
146
- headers: {
147
- "Content-Type": "application/json",
148
- ...corsHeaders,
149
- },
150
- }
151
- );
152
- }
153
- break;
154
-
155
128
  case "/api/session/create":
156
129
  if (req.method === "POST") {
157
130
  const sessionId = generateSessionId();
@@ -351,14 +324,38 @@ const server = serve({
351
324
  }
352
325
  break;
353
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
+
354
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
+
351
+ console.log(`[Container] Route not found: ${pathname}`);
355
352
  return new Response("Not Found", {
356
353
  headers: corsHeaders,
357
354
  status: 404,
358
355
  });
359
356
  }
360
357
  } catch (error) {
361
- console.error("[Server] Error handling request:", error);
358
+ console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
362
359
  return new Response(
363
360
  JSON.stringify({
364
361
  error: "Internal server error",
@@ -374,16 +371,19 @@ const server = serve({
374
371
  );
375
372
  }
376
373
  },
374
+ hostname: "0.0.0.0",
377
375
  port: 3000,
378
- } as any);
376
+ // We don't need this, but typescript complains
377
+ websocket: { async message() { } },
378
+ });
379
379
 
380
380
  async function handleExecuteRequest(
381
381
  req: Request,
382
382
  corsHeaders: Record<string, string>
383
383
  ): Promise<Response> {
384
384
  try {
385
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
386
- const { command, args = [], sessionId } = body;
385
+ const body = (await req.json()) as ExecuteRequest;
386
+ const { command, args = [], sessionId, background } = body;
387
387
 
388
388
  if (!command || typeof command !== "string") {
389
389
  return new Response(
@@ -430,7 +430,7 @@ async function handleExecuteRequest(
430
430
 
431
431
  console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
432
432
 
433
- const result = await executeCommand(command, args, sessionId);
433
+ const result = await executeCommand(command, args, sessionId, background);
434
434
 
435
435
  return new Response(
436
436
  JSON.stringify({
@@ -472,8 +472,8 @@ async function handleStreamingExecuteRequest(
472
472
  corsHeaders: Record<string, string>
473
473
  ): Promise<Response> {
474
474
  try {
475
- const body = (await req.json()) as ExecuteRequest & { sessionId?: string };
476
- const { command, args = [], sessionId } = body;
475
+ const body = (await req.json()) as ExecuteRequest;
476
+ const { command, args = [], sessionId, background } = body;
477
477
 
478
478
  if (!command || typeof command !== "string") {
479
479
  return new Response(
@@ -524,10 +524,13 @@ async function handleStreamingExecuteRequest(
524
524
 
525
525
  const stream = new ReadableStream({
526
526
  start(controller) {
527
- const child = spawn(command, args, {
527
+ const spawnOptions: SpawnOptions = {
528
528
  shell: true,
529
- stdio: ["pipe", "pipe", "pipe"],
530
- });
529
+ stdio: ["pipe", "pipe", "pipe"] as const,
530
+ detached: background || false,
531
+ };
532
+
533
+ const child = spawn(command, args, spawnOptions);
531
534
 
532
535
  // Store the process reference for cleanup if sessionId is provided
533
536
  if (sessionId && sessions.has(sessionId)) {
@@ -535,6 +538,11 @@ async function handleStreamingExecuteRequest(
535
538
  session.activeProcess = child;
536
539
  }
537
540
 
541
+ // For background processes, unref to prevent blocking
542
+ if (background) {
543
+ child.unref();
544
+ }
545
+
538
546
  let stdout = "";
539
547
  let stderr = "";
540
548
 
@@ -546,6 +554,7 @@ async function handleStreamingExecuteRequest(
546
554
  command,
547
555
  timestamp: new Date().toISOString(),
548
556
  type: "command_start",
557
+ background: background || false,
549
558
  })}\n\n`
550
559
  )
551
560
  );
@@ -611,7 +620,11 @@ async function handleStreamingExecuteRequest(
611
620
  )
612
621
  );
613
622
 
614
- controller.close();
623
+ // For non-background processes, close the stream
624
+ // For background processes with streaming, the stream stays open
625
+ if (!background) {
626
+ controller.close();
627
+ }
615
628
  });
616
629
 
617
630
  child.on("error", (error) => {
@@ -2501,7 +2514,8 @@ async function handleStreamingMoveFileRequest(
2501
2514
  function executeCommand(
2502
2515
  command: string,
2503
2516
  args: string[],
2504
- sessionId?: string
2517
+ sessionId?: string,
2518
+ background?: boolean
2505
2519
  ): Promise<{
2506
2520
  success: boolean;
2507
2521
  stdout: string;
@@ -2509,10 +2523,13 @@ function executeCommand(
2509
2523
  exitCode: number;
2510
2524
  }> {
2511
2525
  return new Promise((resolve, reject) => {
2512
- const child = spawn(command, args, {
2526
+ const spawnOptions: SpawnOptions = {
2513
2527
  shell: true,
2514
- stdio: ["pipe", "pipe", "pipe"],
2515
- });
2528
+ stdio: ["pipe", "pipe", "pipe"] as const,
2529
+ detached: background || false,
2530
+ };
2531
+
2532
+ const child = spawn(command, args, spawnOptions);
2516
2533
 
2517
2534
  // Store the process reference for cleanup if sessionId is provided
2518
2535
  if (sessionId && sessions.has(sessionId)) {
@@ -2531,32 +2548,54 @@ function executeCommand(
2531
2548
  stderr += data.toString();
2532
2549
  });
2533
2550
 
2534
- child.on("close", (code) => {
2535
- // Clear the active process reference
2536
- if (sessionId && sessions.has(sessionId)) {
2537
- const session = sessions.get(sessionId)!;
2538
- session.activeProcess = null;
2539
- }
2551
+ if (background) {
2552
+ // For background processes, unref and return quickly
2553
+ child.unref();
2540
2554
 
2541
- console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2555
+ // Collect initial output for 100ms then return
2556
+ setTimeout(() => {
2557
+ resolve({
2558
+ exitCode: 0, // Process is still running
2559
+ stderr,
2560
+ stdout,
2561
+ success: true,
2562
+ });
2563
+ }, 100);
2542
2564
 
2543
- resolve({
2544
- exitCode: code || 0,
2545
- stderr,
2546
- stdout,
2547
- success: code === 0,
2565
+ // Still handle errors
2566
+ child.on("error", (error) => {
2567
+ console.error(`[Server] Background process error: ${command}`, error);
2568
+ // Don't reject since we might have already resolved
2548
2569
  });
2549
- });
2570
+ } else {
2571
+ // Normal synchronous execution
2572
+ child.on("close", (code) => {
2573
+ // Clear the active process reference
2574
+ if (sessionId && sessions.has(sessionId)) {
2575
+ const session = sessions.get(sessionId)!;
2576
+ session.activeProcess = null;
2577
+ }
2550
2578
 
2551
- child.on("error", (error) => {
2552
- // Clear the active process reference
2553
- if (sessionId && sessions.has(sessionId)) {
2554
- const session = sessions.get(sessionId)!;
2555
- session.activeProcess = null;
2556
- }
2579
+ console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2557
2580
 
2558
- reject(error);
2559
- });
2581
+ resolve({
2582
+ exitCode: code || 0,
2583
+ stderr,
2584
+ stdout,
2585
+ success: code === 0,
2586
+ });
2587
+ });
2588
+
2589
+ child.on("error", (error) => {
2590
+ // Clear the active process reference
2591
+ if (sessionId && sessions.has(sessionId)) {
2592
+ const session = sessions.get(sessionId)!;
2593
+ session.activeProcess = null;
2594
+ }
2595
+
2596
+ reject(error);
2597
+ });
2598
+ }
2560
2599
  });
2561
2600
  }
2562
2601
 
@@ -2874,7 +2913,316 @@ function executeMoveFile(
2874
2913
  });
2875
2914
  }
2876
2915
 
2877
- console.log(`🚀 Bun server running on http://localhost:${server.port}`);
2916
+ async function handleExposePortRequest(
2917
+ req: Request,
2918
+ corsHeaders: Record<string, string>
2919
+ ): Promise<Response> {
2920
+ try {
2921
+ const body = (await req.json()) as ExposePortRequest;
2922
+ const { port, name } = body;
2923
+
2924
+ if (!port || typeof port !== "number") {
2925
+ return new Response(
2926
+ JSON.stringify({
2927
+ error: "Port is required and must be a number",
2928
+ }),
2929
+ {
2930
+ headers: {
2931
+ "Content-Type": "application/json",
2932
+ ...corsHeaders,
2933
+ },
2934
+ status: 400,
2935
+ }
2936
+ );
2937
+ }
2938
+
2939
+ // Validate port range
2940
+ if (port < 1 || port > 65535) {
2941
+ return new Response(
2942
+ JSON.stringify({
2943
+ error: "Port must be between 1 and 65535",
2944
+ }),
2945
+ {
2946
+ headers: {
2947
+ "Content-Type": "application/json",
2948
+ ...corsHeaders,
2949
+ },
2950
+ status: 400,
2951
+ }
2952
+ );
2953
+ }
2954
+
2955
+ // Store the exposed port
2956
+ exposedPorts.set(port, { name, exposedAt: new Date() });
2957
+
2958
+ console.log(`[Server] Exposed port: ${port}${name ? ` (${name})` : ""}`);
2959
+
2960
+ return new Response(
2961
+ JSON.stringify({
2962
+ port,
2963
+ name,
2964
+ exposedAt: new Date().toISOString(),
2965
+ success: true,
2966
+ timestamp: new Date().toISOString(),
2967
+ }),
2968
+ {
2969
+ headers: {
2970
+ "Content-Type": "application/json",
2971
+ ...corsHeaders,
2972
+ },
2973
+ }
2974
+ );
2975
+ } catch (error) {
2976
+ console.error("[Server] Error in handleExposePortRequest:", error);
2977
+ return new Response(
2978
+ JSON.stringify({
2979
+ error: "Failed to expose port",
2980
+ message: error instanceof Error ? error.message : "Unknown error",
2981
+ }),
2982
+ {
2983
+ headers: {
2984
+ "Content-Type": "application/json",
2985
+ ...corsHeaders,
2986
+ },
2987
+ status: 500,
2988
+ }
2989
+ );
2990
+ }
2991
+ }
2992
+
2993
+ async function handleUnexposePortRequest(
2994
+ req: Request,
2995
+ corsHeaders: Record<string, string>
2996
+ ): Promise<Response> {
2997
+ try {
2998
+ const body = (await req.json()) as UnexposePortRequest;
2999
+ const { port } = body;
3000
+
3001
+ if (!port || typeof port !== "number") {
3002
+ return new Response(
3003
+ JSON.stringify({
3004
+ error: "Port is required and must be a number",
3005
+ }),
3006
+ {
3007
+ headers: {
3008
+ "Content-Type": "application/json",
3009
+ ...corsHeaders,
3010
+ },
3011
+ status: 400,
3012
+ }
3013
+ );
3014
+ }
3015
+
3016
+ // Check if port is exposed
3017
+ if (!exposedPorts.has(port)) {
3018
+ return new Response(
3019
+ JSON.stringify({
3020
+ error: "Port is not exposed",
3021
+ }),
3022
+ {
3023
+ headers: {
3024
+ "Content-Type": "application/json",
3025
+ ...corsHeaders,
3026
+ },
3027
+ status: 404,
3028
+ }
3029
+ );
3030
+ }
3031
+
3032
+ // Remove the exposed port
3033
+ exposedPorts.delete(port);
3034
+
3035
+ console.log(`[Server] Unexposed port: ${port}`);
3036
+
3037
+ return new Response(
3038
+ JSON.stringify({
3039
+ port,
3040
+ success: true,
3041
+ timestamp: new Date().toISOString(),
3042
+ }),
3043
+ {
3044
+ headers: {
3045
+ "Content-Type": "application/json",
3046
+ ...corsHeaders,
3047
+ },
3048
+ }
3049
+ );
3050
+ } catch (error) {
3051
+ console.error("[Server] Error in handleUnexposePortRequest:", error);
3052
+ return new Response(
3053
+ JSON.stringify({
3054
+ error: "Failed to unexpose port",
3055
+ message: error instanceof Error ? error.message : "Unknown error",
3056
+ }),
3057
+ {
3058
+ headers: {
3059
+ "Content-Type": "application/json",
3060
+ ...corsHeaders,
3061
+ },
3062
+ status: 500,
3063
+ }
3064
+ );
3065
+ }
3066
+ }
3067
+
3068
+ async function handleGetExposedPortsRequest(
3069
+ req: Request,
3070
+ corsHeaders: Record<string, string>
3071
+ ): Promise<Response> {
3072
+ try {
3073
+ const ports = Array.from(exposedPorts.entries()).map(([port, info]) => ({
3074
+ port,
3075
+ name: info.name,
3076
+ exposedAt: info.exposedAt.toISOString(),
3077
+ }));
3078
+
3079
+ return new Response(
3080
+ JSON.stringify({
3081
+ ports,
3082
+ count: ports.length,
3083
+ timestamp: new Date().toISOString(),
3084
+ }),
3085
+ {
3086
+ headers: {
3087
+ "Content-Type": "application/json",
3088
+ ...corsHeaders,
3089
+ },
3090
+ }
3091
+ );
3092
+ } catch (error) {
3093
+ console.error("[Server] Error in handleGetExposedPortsRequest:", error);
3094
+ return new Response(
3095
+ JSON.stringify({
3096
+ error: "Failed to get exposed ports",
3097
+ message: error instanceof Error ? error.message : "Unknown error",
3098
+ }),
3099
+ {
3100
+ headers: {
3101
+ "Content-Type": "application/json",
3102
+ ...corsHeaders,
3103
+ },
3104
+ status: 500,
3105
+ }
3106
+ );
3107
+ }
3108
+ }
3109
+
3110
+ async function handleProxyRequest(
3111
+ req: Request,
3112
+ corsHeaders: Record<string, string>
3113
+ ): Promise<Response> {
3114
+ try {
3115
+ const url = new URL(req.url);
3116
+ const pathParts = url.pathname.split("/");
3117
+
3118
+ // Extract port from path like /proxy/3000/...
3119
+ if (pathParts.length < 3) {
3120
+ return new Response(
3121
+ JSON.stringify({
3122
+ error: "Invalid proxy path",
3123
+ }),
3124
+ {
3125
+ headers: {
3126
+ "Content-Type": "application/json",
3127
+ ...corsHeaders,
3128
+ },
3129
+ status: 400,
3130
+ }
3131
+ );
3132
+ }
3133
+
3134
+ const port = parseInt(pathParts[2]);
3135
+ if (!port || Number.isNaN(port)) {
3136
+ return new Response(
3137
+ JSON.stringify({
3138
+ error: "Invalid port in proxy path",
3139
+ }),
3140
+ {
3141
+ headers: {
3142
+ "Content-Type": "application/json",
3143
+ ...corsHeaders,
3144
+ },
3145
+ status: 400,
3146
+ }
3147
+ );
3148
+ }
3149
+
3150
+ // Check if port is exposed
3151
+ if (!exposedPorts.has(port)) {
3152
+ return new Response(
3153
+ JSON.stringify({
3154
+ error: `Port ${port} is not exposed`,
3155
+ }),
3156
+ {
3157
+ headers: {
3158
+ "Content-Type": "application/json",
3159
+ ...corsHeaders,
3160
+ },
3161
+ status: 404,
3162
+ }
3163
+ );
3164
+ }
3165
+
3166
+ // Construct the target URL
3167
+ const targetPath = `/${pathParts.slice(3).join("/")}`;
3168
+ // Use 127.0.0.1 instead of localhost for more reliable container networking
3169
+ const targetUrl = `http://127.0.0.1:${port}${targetPath}${url.search}`;
3170
+
3171
+ console.log(`[Server] Proxying request to: ${targetUrl}`);
3172
+ console.log(`[Server] Method: ${req.method}, Port: ${port}, Path: ${targetPath}`);
3173
+
3174
+ try {
3175
+ // Forward the request to the target port
3176
+ const targetResponse = await fetch(targetUrl, {
3177
+ method: req.method,
3178
+ headers: req.headers,
3179
+ body: req.body,
3180
+ });
3181
+
3182
+ // Return the response from the target
3183
+ return new Response(targetResponse.body, {
3184
+ status: targetResponse.status,
3185
+ statusText: targetResponse.statusText,
3186
+ headers: {
3187
+ ...Object.fromEntries(targetResponse.headers.entries()),
3188
+ ...corsHeaders,
3189
+ },
3190
+ });
3191
+ } catch (fetchError) {
3192
+ console.error(`[Server] Error proxying to port ${port}:`, fetchError);
3193
+ return new Response(
3194
+ JSON.stringify({
3195
+ error: `Service on port ${port} is not responding`,
3196
+ message: fetchError instanceof Error ? fetchError.message : "Unknown error",
3197
+ }),
3198
+ {
3199
+ headers: {
3200
+ "Content-Type": "application/json",
3201
+ ...corsHeaders,
3202
+ },
3203
+ status: 502,
3204
+ }
3205
+ );
3206
+ }
3207
+ } catch (error) {
3208
+ console.error("[Server] Error in handleProxyRequest:", error);
3209
+ return new Response(
3210
+ JSON.stringify({
3211
+ error: "Failed to proxy request",
3212
+ message: error instanceof Error ? error.message : "Unknown error",
3213
+ }),
3214
+ {
3215
+ headers: {
3216
+ "Content-Type": "application/json",
3217
+ ...corsHeaders,
3218
+ },
3219
+ status: 500,
3220
+ }
3221
+ );
3222
+ }
3223
+ }
3224
+
3225
+ console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
2878
3226
  console.log(`📡 HTTP API endpoints available:`);
2879
3227
  console.log(` POST /api/session/create - Create a new session`);
2880
3228
  console.log(` GET /api/session/list - List all sessions`);
@@ -2896,5 +3244,9 @@ console.log(` POST /api/rename - Rename a file`);
2896
3244
  console.log(` POST /api/rename/stream - Rename a file (streaming)`);
2897
3245
  console.log(` POST /api/move - Move a file`);
2898
3246
  console.log(` POST /api/move/stream - Move a file (streaming)`);
3247
+ console.log(` POST /api/expose-port - Expose a port for external access`);
3248
+ console.log(` DELETE /api/unexpose-port - Unexpose a port`);
3249
+ console.log(` GET /api/exposed-ports - List exposed ports`);
3250
+ console.log(` GET /proxy/{port}/* - Proxy requests to exposed ports`);
2899
3251
  console.log(` GET /api/ping - Health check`);
2900
3252
  console.log(` GET /api/commands - List available commands`);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@cloudflare/sandbox",
3
- "version": "0.0.0-eb0ea62",
3
+ "version": "0.0.0-f106fda",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cloudflare/sandbox-sdk"
7
7
  },
8
8
  "description": "A sandboxed environment for running commands",
9
9
  "dependencies": {
10
- "@cloudflare/containers": "^0.0.13"
10
+ "@cloudflare/containers": "^0.0.23"
11
11
  },
12
12
  "tags": [
13
13
  "sandbox",
@@ -17,7 +17,9 @@
17
17
  "durable objects"
18
18
  ],
19
19
  "scripts": {
20
- "build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm"
20
+ "build": "rm -rf dist && tsup src/*.ts --outDir dist --dts --sourcemap --format esm",
21
+ "docker:build": "docker build -t ghostwriternr/cloudflare-sandbox:$npm_package_version .",
22
+ "docker:publish": "docker push docker.io/ghostwriternr/cloudflare-sandbox:$npm_package_version"
21
23
  },
22
24
  "exports": {
23
25
  ".": {