@cloudflare/sandbox 0.0.0-f5fcd52 → 0.0.0-fb3c9c2

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 (40) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/Dockerfile +47 -36
  3. package/README.md +805 -24
  4. package/container_src/bun.lock +122 -0
  5. package/container_src/circuit-breaker.ts +121 -0
  6. package/container_src/control-process.ts +784 -0
  7. package/container_src/handler/exec.ts +185 -0
  8. package/container_src/handler/file.ts +406 -0
  9. package/container_src/handler/git.ts +130 -0
  10. package/container_src/handler/ports.ts +314 -0
  11. package/container_src/handler/process.ts +568 -0
  12. package/container_src/handler/session.ts +92 -0
  13. package/container_src/index.ts +411 -3006
  14. package/container_src/isolation.ts +1038 -0
  15. package/container_src/jupyter-server.ts +579 -0
  16. package/container_src/jupyter-service.ts +461 -0
  17. package/container_src/jupyter_config.py +48 -0
  18. package/container_src/mime-processor.ts +255 -0
  19. package/container_src/package.json +9 -0
  20. package/container_src/shell-escape.ts +42 -0
  21. package/container_src/startup.sh +84 -0
  22. package/container_src/types.ts +131 -0
  23. package/package.json +5 -9
  24. package/src/client.ts +333 -1413
  25. package/src/errors.ts +218 -0
  26. package/src/index.ts +60 -10
  27. package/src/interpreter-types.ts +383 -0
  28. package/src/interpreter.ts +150 -0
  29. package/src/jupyter-client.ts +349 -0
  30. package/src/request-handler.ts +69 -20
  31. package/src/sandbox.ts +594 -105
  32. package/src/security.ts +113 -0
  33. package/src/sse-parser.ts +147 -0
  34. package/src/types.ts +502 -0
  35. package/tsconfig.json +1 -1
  36. package/tests/client.example.ts +0 -308
  37. package/tests/connection-test.ts +0 -81
  38. package/tests/simple-test.ts +0 -81
  39. package/tests/test1.ts +0 -281
  40. package/tests/test2.ts +0 -929
@@ -1,100 +1,102 @@
1
- import { type ChildProcess, type SpawnOptions, spawn } from "node:child_process";
2
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
1
  import { serve } from "bun";
2
+ import { handleExecuteRequest, handleStreamingExecuteRequest } from "./handler/exec";
3
+ import {
4
+ handleDeleteFileRequest,
5
+ handleListFilesRequest,
6
+ handleMkdirRequest,
7
+ handleMoveFileRequest,
8
+ handleReadFileRequest,
9
+ handleRenameFileRequest,
10
+ handleWriteFileRequest,
11
+ } from "./handler/file";
12
+ import { handleGitCheckoutRequest } from "./handler/git";
13
+ import {
14
+ handleExposePortRequest,
15
+ handleGetExposedPortsRequest,
16
+ handleProxyRequest,
17
+ handleUnexposePortRequest,
18
+ } from "./handler/ports";
19
+ import {
20
+ handleGetProcessLogsRequest,
21
+ handleGetProcessRequest,
22
+ handleKillAllProcessesRequest,
23
+ handleKillProcessRequest,
24
+ handleListProcessesRequest,
25
+ handleStartProcessRequest,
26
+ handleStreamProcessLogsRequest,
27
+ } from "./handler/process";
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";
5
32
 
6
- interface ExecuteRequest {
7
- command: string;
8
- args?: string[];
9
- sessionId?: string;
10
- background?: boolean;
11
- }
12
-
13
- interface GitCheckoutRequest {
14
- repoUrl: string;
15
- branch?: string;
16
- targetDir?: string;
17
- sessionId?: string;
18
- }
19
-
20
- interface MkdirRequest {
21
- path: string;
22
- recursive?: boolean;
23
- sessionId?: string;
24
- }
25
-
26
- interface WriteFileRequest {
27
- path: string;
28
- content: string;
29
- encoding?: string;
30
- sessionId?: string;
31
- }
32
-
33
- interface ReadFileRequest {
34
- path: string;
35
- encoding?: string;
36
- sessionId?: string;
37
- }
38
-
39
- interface DeleteFileRequest {
40
- path: string;
41
- sessionId?: string;
42
- }
43
-
44
- interface RenameFileRequest {
45
- oldPath: string;
46
- newPath: string;
47
- sessionId?: string;
48
- }
33
+ // In-memory storage for exposed ports
34
+ const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
49
35
 
50
- interface MoveFileRequest {
51
- sourcePath: string;
52
- destinationPath: string;
53
- sessionId?: string;
54
- }
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
+ );
55
45
 
56
- interface ExposePortRequest {
57
- port: number;
58
- name?: string;
59
- }
46
+ // Session manager for secure execution with isolation
47
+ const sessionManager = new SessionManager();
60
48
 
61
- interface UnexposePortRequest {
62
- port: number;
63
- }
49
+ // Graceful shutdown handler
50
+ const SHUTDOWN_GRACE_PERIOD_MS = 5000; // Grace period for cleanup (5 seconds for proper async cleanup)
64
51
 
65
- interface SessionData {
66
- sessionId: string;
67
- activeProcess: ChildProcess | null;
68
- createdAt: Date;
69
- }
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
+ });
70
59
 
71
- // In-memory session storage (in production, you'd want to use a proper database)
72
- const sessions = new Map<string, SessionData>();
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
+ });
73
67
 
74
- // In-memory storage for exposed ports
75
- const exposedPorts = new Map<number, { name?: string; exposedAt: Date }>();
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
+ });
76
74
 
77
- // Generate a unique session ID
78
- function generateSessionId(): string {
79
- return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
80
- }
75
+ // Initialize Jupyter service with graceful degradation
76
+ const jupyterService = new JupyterService();
81
77
 
82
- // Clean up old sessions (older than 1 hour)
83
- function cleanupOldSessions() {
84
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
85
- for (const [sessionId, session] of sessions.entries()) {
86
- if (session.createdAt < oneHourAgo && !session.activeProcess) {
87
- sessions.delete(sessionId);
88
- console.log(`[Server] Cleaned up old session: ${sessionId}`);
89
- }
90
- }
91
- }
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
+ );
92
83
 
93
- // Run cleanup every 10 minutes
94
- setInterval(cleanupOldSessions, 10 * 60 * 1000);
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
+ });
95
97
 
96
98
  const server = serve({
97
- fetch(req: Request) {
99
+ async fetch(req: Request) {
98
100
  const url = new URL(req.url);
99
101
  const pathname = url.pathname;
100
102
 
@@ -127,108 +129,41 @@ const server = serve({
127
129
 
128
130
  case "/api/session/create":
129
131
  if (req.method === "POST") {
130
- const sessionId = generateSessionId();
131
- const sessionData: SessionData = {
132
- activeProcess: null,
133
- createdAt: new Date(),
134
- sessionId,
135
- };
136
- sessions.set(sessionId, sessionData);
137
-
138
- console.log(`[Server] Created new session: ${sessionId}`);
139
-
140
- return new Response(
141
- JSON.stringify({
142
- message: "Session created successfully",
143
- sessionId,
144
- timestamp: new Date().toISOString(),
145
- }),
146
- {
147
- headers: {
148
- "Content-Type": "application/json",
149
- ...corsHeaders,
150
- },
151
- }
152
- );
132
+ return handleCreateSession(req, corsHeaders, sessionManager);
153
133
  }
154
134
  break;
155
135
 
156
136
  case "/api/session/list":
157
137
  if (req.method === "GET") {
158
- const sessionList = Array.from(sessions.values()).map(
159
- (session) => ({
160
- createdAt: session.createdAt.toISOString(),
161
- hasActiveProcess: !!session.activeProcess,
162
- sessionId: session.sessionId,
163
- })
164
- );
165
-
166
- return new Response(
167
- JSON.stringify({
168
- count: sessionList.length,
169
- sessions: sessionList,
170
- timestamp: new Date().toISOString(),
171
- }),
172
- {
173
- headers: {
174
- "Content-Type": "application/json",
175
- ...corsHeaders,
176
- },
177
- }
178
- );
138
+ return handleListSessions(corsHeaders, sessionManager);
179
139
  }
180
140
  break;
181
141
 
182
142
  case "/api/execute":
183
143
  if (req.method === "POST") {
184
- return handleExecuteRequest(req, corsHeaders);
144
+ return handleExecuteRequest(req, corsHeaders, sessionManager);
185
145
  }
186
146
  break;
187
-
147
+
188
148
  case "/api/execute/stream":
189
149
  if (req.method === "POST") {
190
- return handleStreamingExecuteRequest(req, corsHeaders);
150
+ return handleStreamingExecuteRequest(req, sessionManager, corsHeaders);
191
151
  }
192
152
  break;
193
153
 
194
154
  case "/api/ping":
195
155
  if (req.method === "GET") {
156
+ const health = await jupyterService.getHealthStatus();
196
157
  return new Response(
197
158
  JSON.stringify({
198
159
  message: "pong",
199
160
  timestamp: new Date().toISOString(),
200
- }),
201
- {
202
- headers: {
203
- "Content-Type": "application/json",
204
- ...corsHeaders,
205
- },
206
- }
207
- );
208
- }
209
- break;
210
-
211
- case "/api/commands":
212
- if (req.method === "GET") {
213
- return new Response(
214
- JSON.stringify({
215
- availableCommands: [
216
- "ls",
217
- "pwd",
218
- "echo",
219
- "cat",
220
- "grep",
221
- "find",
222
- "whoami",
223
- "date",
224
- "uptime",
225
- "ps",
226
- "top",
227
- "df",
228
- "du",
229
- "free",
230
- ],
231
- timestamp: new Date().toISOString(),
161
+ jupyter: health.ready
162
+ ? "ready"
163
+ : health.initializing
164
+ ? "initializing"
165
+ : "not ready",
166
+ jupyterHealth: health,
232
167
  }),
233
168
  {
234
169
  headers: {
@@ -242,110 +177,363 @@ const server = serve({
242
177
 
243
178
  case "/api/git/checkout":
244
179
  if (req.method === "POST") {
245
- return handleGitCheckoutRequest(req, corsHeaders);
180
+ return handleGitCheckoutRequest(req, corsHeaders, sessionManager);
246
181
  }
247
182
  break;
248
183
 
249
- case "/api/git/checkout/stream":
184
+ case "/api/mkdir":
250
185
  if (req.method === "POST") {
251
- return handleStreamingGitCheckoutRequest(req, corsHeaders);
186
+ return handleMkdirRequest(req, corsHeaders, sessionManager);
252
187
  }
253
188
  break;
254
189
 
255
- case "/api/mkdir":
190
+ case "/api/write":
256
191
  if (req.method === "POST") {
257
- return handleMkdirRequest(req, corsHeaders);
192
+ return handleWriteFileRequest(req, corsHeaders, sessionManager);
258
193
  }
259
194
  break;
260
195
 
261
- case "/api/mkdir/stream":
196
+ case "/api/read":
262
197
  if (req.method === "POST") {
263
- return handleStreamingMkdirRequest(req, corsHeaders);
198
+ return handleReadFileRequest(req, corsHeaders, sessionManager);
264
199
  }
265
200
  break;
266
201
 
267
- case "/api/write":
202
+ case "/api/delete":
268
203
  if (req.method === "POST") {
269
- return handleWriteFileRequest(req, corsHeaders);
204
+ return handleDeleteFileRequest(req, corsHeaders, sessionManager);
270
205
  }
271
206
  break;
272
207
 
273
- case "/api/write/stream":
208
+ case "/api/rename":
274
209
  if (req.method === "POST") {
275
- return handleStreamingWriteFileRequest(req, corsHeaders);
210
+ return handleRenameFileRequest(req, corsHeaders, sessionManager);
276
211
  }
277
212
  break;
278
213
 
279
- case "/api/read":
214
+ case "/api/move":
280
215
  if (req.method === "POST") {
281
- return handleReadFileRequest(req, corsHeaders);
216
+ return handleMoveFileRequest(req, corsHeaders, sessionManager);
282
217
  }
283
218
  break;
284
219
 
285
- case "/api/read/stream":
220
+ case "/api/list-files":
286
221
  if (req.method === "POST") {
287
- return handleStreamingReadFileRequest(req, corsHeaders);
222
+ return handleListFilesRequest(req, corsHeaders, sessionManager);
288
223
  }
289
224
  break;
290
225
 
291
- case "/api/delete":
226
+ case "/api/expose-port":
292
227
  if (req.method === "POST") {
293
- return handleDeleteFileRequest(req, corsHeaders);
228
+ return handleExposePortRequest(exposedPorts, req, corsHeaders);
294
229
  }
295
230
  break;
296
231
 
297
- case "/api/delete/stream":
298
- if (req.method === "POST") {
299
- return handleStreamingDeleteFileRequest(req, corsHeaders);
232
+ case "/api/unexpose-port":
233
+ if (req.method === "DELETE") {
234
+ return handleUnexposePortRequest(exposedPorts, req, corsHeaders);
300
235
  }
301
236
  break;
302
237
 
303
- case "/api/rename":
304
- if (req.method === "POST") {
305
- return handleRenameFileRequest(req, corsHeaders);
238
+ case "/api/exposed-ports":
239
+ if (req.method === "GET") {
240
+ return handleGetExposedPortsRequest(exposedPorts, req, corsHeaders);
306
241
  }
307
242
  break;
308
243
 
309
- case "/api/rename/stream":
244
+ case "/api/process/start":
310
245
  if (req.method === "POST") {
311
- return handleStreamingRenameFileRequest(req, corsHeaders);
246
+ return handleStartProcessRequest(req, corsHeaders, sessionManager);
312
247
  }
313
248
  break;
314
249
 
315
- case "/api/move":
316
- if (req.method === "POST") {
317
- return handleMoveFileRequest(req, corsHeaders);
250
+ case "/api/process/list":
251
+ if (req.method === "GET") {
252
+ return handleListProcessesRequest(req, corsHeaders, sessionManager);
318
253
  }
319
254
  break;
320
255
 
321
- case "/api/move/stream":
322
- if (req.method === "POST") {
323
- return handleStreamingMoveFileRequest(req, corsHeaders);
256
+ case "/api/process/kill-all":
257
+ if (req.method === "DELETE") {
258
+ return handleKillAllProcessesRequest(
259
+ req,
260
+ corsHeaders,
261
+ sessionManager
262
+ );
324
263
  }
325
264
  break;
326
265
 
327
- case "/api/expose-port":
266
+ case "/api/contexts":
328
267
  if (req.method === "POST") {
329
- return handleExposePortRequest(req, corsHeaders);
330
- }
331
- break;
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
+ }
332
307
 
333
- case "/api/unexpose-port":
334
- if (req.method === "DELETE") {
335
- return handleUnexposePortRequest(req, corsHeaders);
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
+ });
336
361
  }
337
362
  break;
338
363
 
339
- case "/api/exposed-ports":
340
- if (req.method === "GET") {
341
- return handleGetExposedPortsRequest(req, corsHeaders);
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
+ }
342
433
  }
343
434
  break;
344
435
 
345
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
+
496
+ // Handle dynamic routes for individual processes
497
+ if (pathname.startsWith("/api/process/")) {
498
+ const segments = pathname.split("/");
499
+ if (segments.length >= 4) {
500
+ const processId = segments[3];
501
+ const action = segments[4]; // Optional: logs, stream, etc.
502
+
503
+ if (!action && req.method === "GET") {
504
+ return handleGetProcessRequest(
505
+ req,
506
+ corsHeaders,
507
+ processId,
508
+ sessionManager
509
+ );
510
+ } else if (!action && req.method === "DELETE") {
511
+ return handleKillProcessRequest(
512
+ req,
513
+ corsHeaders,
514
+ processId,
515
+ sessionManager
516
+ );
517
+ } else if (action === "logs" && req.method === "GET") {
518
+ return handleGetProcessLogsRequest(
519
+ req,
520
+ corsHeaders,
521
+ processId,
522
+ sessionManager
523
+ );
524
+ } else if (action === "stream" && req.method === "GET") {
525
+ return handleStreamProcessLogsRequest(
526
+ req,
527
+ corsHeaders,
528
+ processId,
529
+ sessionManager
530
+ );
531
+ }
532
+ }
533
+ }
346
534
  // Check if this is a proxy request for an exposed port
347
535
  if (pathname.startsWith("/proxy/")) {
348
- return handleProxyRequest(req, corsHeaders);
536
+ return handleProxyRequest(exposedPorts, req, corsHeaders);
349
537
  }
350
538
 
351
539
  console.log(`[Container] Route not found: ${pathname}`);
@@ -355,7 +543,10 @@ const server = serve({
355
543
  });
356
544
  }
357
545
  } catch (error) {
358
- console.error(`[Container] Error handling ${req.method} ${pathname}:`, error);
546
+ console.error(
547
+ `[Container] Error handling ${req.method} ${pathname}:`,
548
+ error
549
+ );
359
550
  return new Response(
360
551
  JSON.stringify({
361
552
  error: "Internal server error",
@@ -374,2798 +565,9 @@ const server = serve({
374
565
  hostname: "0.0.0.0",
375
566
  port: 3000,
376
567
  // We don't need this, but typescript complains
377
- websocket: { async message() { } },
568
+ websocket: { async message() {} },
378
569
  });
379
570
 
380
- async function handleExecuteRequest(
381
- req: Request,
382
- corsHeaders: Record<string, string>
383
- ): Promise<Response> {
384
- try {
385
- const body = (await req.json()) as ExecuteRequest;
386
- const { command, args = [], sessionId, background } = body;
387
-
388
- if (!command || typeof command !== "string") {
389
- return new Response(
390
- JSON.stringify({
391
- error: "Command is required and must be a string",
392
- }),
393
- {
394
- headers: {
395
- "Content-Type": "application/json",
396
- ...corsHeaders,
397
- },
398
- status: 400,
399
- }
400
- );
401
- }
402
-
403
- console.log(`[Server] Executing command: ${command} ${args.join(" ")}`);
404
-
405
- const result = await executeCommand(command, args, sessionId, background);
406
-
407
- return new Response(
408
- JSON.stringify({
409
- args,
410
- command,
411
- exitCode: result.exitCode,
412
- stderr: result.stderr,
413
- stdout: result.stdout,
414
- success: result.success,
415
- timestamp: new Date().toISOString(),
416
- }),
417
- {
418
- headers: {
419
- "Content-Type": "application/json",
420
- ...corsHeaders,
421
- },
422
- }
423
- );
424
- } catch (error) {
425
- console.error("[Server] Error in handleExecuteRequest:", error);
426
- return new Response(
427
- JSON.stringify({
428
- error: "Failed to execute command",
429
- message: error instanceof Error ? error.message : "Unknown error",
430
- }),
431
- {
432
- headers: {
433
- "Content-Type": "application/json",
434
- ...corsHeaders,
435
- },
436
- status: 500,
437
- }
438
- );
439
- }
440
- }
441
-
442
- async function handleStreamingExecuteRequest(
443
- req: Request,
444
- corsHeaders: Record<string, string>
445
- ): Promise<Response> {
446
- try {
447
- const body = (await req.json()) as ExecuteRequest;
448
- const { command, args = [], sessionId, background } = body;
449
-
450
- if (!command || typeof command !== "string") {
451
- return new Response(
452
- JSON.stringify({
453
- error: "Command is required and must be a string",
454
- }),
455
- {
456
- headers: {
457
- "Content-Type": "application/json",
458
- ...corsHeaders,
459
- },
460
- status: 400,
461
- }
462
- );
463
- }
464
-
465
- console.log(
466
- `[Server] Executing streaming command: ${command} ${args.join(" ")}`
467
- );
468
-
469
- const stream = new ReadableStream({
470
- start(controller) {
471
- const spawnOptions: SpawnOptions = {
472
- shell: true,
473
- stdio: ["pipe", "pipe", "pipe"] as const,
474
- detached: background || false,
475
- };
476
-
477
- const child = spawn(command, args, spawnOptions);
478
-
479
- // Store the process reference for cleanup if sessionId is provided
480
- if (sessionId && sessions.has(sessionId)) {
481
- const session = sessions.get(sessionId)!;
482
- session.activeProcess = child;
483
- }
484
-
485
- // For background processes, unref to prevent blocking
486
- if (background) {
487
- child.unref();
488
- }
489
-
490
- let stdout = "";
491
- let stderr = "";
492
-
493
- // Send command start event
494
- controller.enqueue(
495
- new TextEncoder().encode(
496
- `data: ${JSON.stringify({
497
- args,
498
- command,
499
- timestamp: new Date().toISOString(),
500
- type: "command_start",
501
- background: background || false,
502
- })}\n\n`
503
- )
504
- );
505
-
506
- child.stdout?.on("data", (data) => {
507
- const output = data.toString();
508
- stdout += output;
509
-
510
- // Send real-time output
511
- controller.enqueue(
512
- new TextEncoder().encode(
513
- `data: ${JSON.stringify({
514
- command,
515
- data: output,
516
- stream: "stdout",
517
- type: "output",
518
- })}\n\n`
519
- )
520
- );
521
- });
522
-
523
- child.stderr?.on("data", (data) => {
524
- const output = data.toString();
525
- stderr += output;
526
-
527
- // Send real-time error output
528
- controller.enqueue(
529
- new TextEncoder().encode(
530
- `data: ${JSON.stringify({
531
- command,
532
- data: output,
533
- stream: "stderr",
534
- type: "output",
535
- })}\n\n`
536
- )
537
- );
538
- });
539
-
540
- child.on("close", (code) => {
541
- // Clear the active process reference
542
- if (sessionId && sessions.has(sessionId)) {
543
- const session = sessions.get(sessionId)!;
544
- session.activeProcess = null;
545
- }
546
-
547
- console.log(
548
- `[Server] Command completed: ${command}, Exit code: ${code}`
549
- );
550
-
551
- // Send command completion event
552
- controller.enqueue(
553
- new TextEncoder().encode(
554
- `data: ${JSON.stringify({
555
- args,
556
- command,
557
- exitCode: code,
558
- stderr,
559
- stdout,
560
- success: code === 0,
561
- timestamp: new Date().toISOString(),
562
- type: "command_complete",
563
- })}\n\n`
564
- )
565
- );
566
-
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
- }
572
- });
573
-
574
- child.on("error", (error) => {
575
- // Clear the active process reference
576
- if (sessionId && sessions.has(sessionId)) {
577
- const session = sessions.get(sessionId)!;
578
- session.activeProcess = null;
579
- }
580
-
581
- controller.enqueue(
582
- new TextEncoder().encode(
583
- `data: ${JSON.stringify({
584
- args,
585
- command,
586
- error: error.message,
587
- type: "error",
588
- })}\n\n`
589
- )
590
- );
591
-
592
- controller.close();
593
- });
594
- },
595
- });
596
-
597
- return new Response(stream, {
598
- headers: {
599
- "Cache-Control": "no-cache",
600
- Connection: "keep-alive",
601
- "Content-Type": "text/event-stream",
602
- ...corsHeaders,
603
- },
604
- });
605
- } catch (error) {
606
- console.error("[Server] Error in handleStreamingExecuteRequest:", error);
607
- return new Response(
608
- JSON.stringify({
609
- error: "Failed to execute streaming command",
610
- message: error instanceof Error ? error.message : "Unknown error",
611
- }),
612
- {
613
- headers: {
614
- "Content-Type": "application/json",
615
- ...corsHeaders,
616
- },
617
- status: 500,
618
- }
619
- );
620
- }
621
- }
622
-
623
- async function handleGitCheckoutRequest(
624
- req: Request,
625
- corsHeaders: Record<string, string>
626
- ): Promise<Response> {
627
- try {
628
- const body = (await req.json()) as GitCheckoutRequest;
629
- const { repoUrl, branch = "main", targetDir, sessionId } = body;
630
-
631
- if (!repoUrl || typeof repoUrl !== "string") {
632
- return new Response(
633
- JSON.stringify({
634
- error: "Repository URL is required and must be a string",
635
- }),
636
- {
637
- headers: {
638
- "Content-Type": "application/json",
639
- ...corsHeaders,
640
- },
641
- status: 400,
642
- }
643
- );
644
- }
645
-
646
- // Validate repository URL format
647
- const urlPattern =
648
- /^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
649
- if (!urlPattern.test(repoUrl)) {
650
- return new Response(
651
- JSON.stringify({
652
- error: "Invalid repository URL format",
653
- }),
654
- {
655
- headers: {
656
- "Content-Type": "application/json",
657
- ...corsHeaders,
658
- },
659
- status: 400,
660
- }
661
- );
662
- }
663
-
664
- // Generate target directory if not provided
665
- const checkoutDir =
666
- targetDir ||
667
- `repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
668
-
669
- console.log(
670
- `[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
671
- );
672
-
673
- const result = await executeGitCheckout(
674
- repoUrl,
675
- branch,
676
- checkoutDir,
677
- sessionId
678
- );
679
-
680
- return new Response(
681
- JSON.stringify({
682
- branch,
683
- exitCode: result.exitCode,
684
- repoUrl,
685
- stderr: result.stderr,
686
- stdout: result.stdout,
687
- success: result.success,
688
- targetDir: checkoutDir,
689
- timestamp: new Date().toISOString(),
690
- }),
691
- {
692
- headers: {
693
- "Content-Type": "application/json",
694
- ...corsHeaders,
695
- },
696
- }
697
- );
698
- } catch (error) {
699
- console.error("[Server] Error in handleGitCheckoutRequest:", error);
700
- return new Response(
701
- JSON.stringify({
702
- error: "Failed to checkout repository",
703
- message: error instanceof Error ? error.message : "Unknown error",
704
- }),
705
- {
706
- headers: {
707
- "Content-Type": "application/json",
708
- ...corsHeaders,
709
- },
710
- status: 500,
711
- }
712
- );
713
- }
714
- }
715
-
716
- async function handleStreamingGitCheckoutRequest(
717
- req: Request,
718
- corsHeaders: Record<string, string>
719
- ): Promise<Response> {
720
- try {
721
- const body = (await req.json()) as GitCheckoutRequest;
722
- const { repoUrl, branch = "main", targetDir, sessionId } = body;
723
-
724
- if (!repoUrl || typeof repoUrl !== "string") {
725
- return new Response(
726
- JSON.stringify({
727
- error: "Repository URL is required and must be a string",
728
- }),
729
- {
730
- headers: {
731
- "Content-Type": "application/json",
732
- ...corsHeaders,
733
- },
734
- status: 400,
735
- }
736
- );
737
- }
738
-
739
- // Validate repository URL format
740
- const urlPattern =
741
- /^(https?:\/\/|git@|ssh:\/\/).*\.git$|^https?:\/\/.*\/.*$/;
742
- if (!urlPattern.test(repoUrl)) {
743
- return new Response(
744
- JSON.stringify({
745
- error: "Invalid repository URL format",
746
- }),
747
- {
748
- headers: {
749
- "Content-Type": "application/json",
750
- ...corsHeaders,
751
- },
752
- status: 400,
753
- }
754
- );
755
- }
756
-
757
- // Generate target directory if not provided
758
- const checkoutDir =
759
- targetDir ||
760
- `repo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
761
-
762
- console.log(
763
- `[Server] Checking out repository: ${repoUrl} to ${checkoutDir}`
764
- );
765
-
766
- const stream = new ReadableStream({
767
- start(controller) {
768
- const child = spawn(
769
- "git",
770
- ["clone", "-b", branch, repoUrl, checkoutDir],
771
- {
772
- shell: true,
773
- stdio: ["pipe", "pipe", "pipe"],
774
- }
775
- );
776
-
777
- // Store the process reference for cleanup if sessionId is provided
778
- if (sessionId && sessions.has(sessionId)) {
779
- const session = sessions.get(sessionId)!;
780
- session.activeProcess = child;
781
- }
782
-
783
- let stdout = "";
784
- let stderr = "";
785
-
786
- // Send command start event
787
- controller.enqueue(
788
- new TextEncoder().encode(
789
- `data: ${JSON.stringify({
790
- args: [branch, repoUrl, checkoutDir],
791
- command: "git clone",
792
- timestamp: new Date().toISOString(),
793
- type: "command_start",
794
- })}\n\n`
795
- )
796
- );
797
-
798
- child.stdout?.on("data", (data) => {
799
- const output = data.toString();
800
- stdout += output;
801
-
802
- // Send real-time output
803
- controller.enqueue(
804
- new TextEncoder().encode(
805
- `data: ${JSON.stringify({
806
- command: "git clone",
807
- data: output,
808
- stream: "stdout",
809
- type: "output",
810
- })}\n\n`
811
- )
812
- );
813
- });
814
-
815
- child.stderr?.on("data", (data) => {
816
- const output = data.toString();
817
- stderr += output;
818
-
819
- // Send real-time error output
820
- controller.enqueue(
821
- new TextEncoder().encode(
822
- `data: ${JSON.stringify({
823
- command: "git clone",
824
- data: output,
825
- stream: "stderr",
826
- type: "output",
827
- })}\n\n`
828
- )
829
- );
830
- });
831
-
832
- child.on("close", (code) => {
833
- // Clear the active process reference
834
- if (sessionId && sessions.has(sessionId)) {
835
- const session = sessions.get(sessionId)!;
836
- session.activeProcess = null;
837
- }
838
-
839
- console.log(
840
- `[Server] Command completed: git clone, Exit code: ${code}`
841
- );
842
-
843
- // Send command completion event
844
- controller.enqueue(
845
- new TextEncoder().encode(
846
- `data: ${JSON.stringify({
847
- args: [branch, repoUrl, checkoutDir],
848
- command: "git clone",
849
- exitCode: code,
850
- stderr,
851
- stdout,
852
- success: code === 0,
853
- timestamp: new Date().toISOString(),
854
- type: "command_complete",
855
- })}\n\n`
856
- )
857
- );
858
-
859
- controller.close();
860
- });
861
-
862
- child.on("error", (error) => {
863
- // Clear the active process reference
864
- if (sessionId && sessions.has(sessionId)) {
865
- const session = sessions.get(sessionId)!;
866
- session.activeProcess = null;
867
- }
868
-
869
- controller.enqueue(
870
- new TextEncoder().encode(
871
- `data: ${JSON.stringify({
872
- args: [branch, repoUrl, checkoutDir],
873
- command: "git clone",
874
- error: error.message,
875
- type: "error",
876
- })}\n\n`
877
- )
878
- );
879
-
880
- controller.close();
881
- });
882
- },
883
- });
884
-
885
- return new Response(stream, {
886
- headers: {
887
- "Cache-Control": "no-cache",
888
- Connection: "keep-alive",
889
- "Content-Type": "text/event-stream",
890
- ...corsHeaders,
891
- },
892
- });
893
- } catch (error) {
894
- console.error(
895
- "[Server] Error in handleStreamingGitCheckoutRequest:",
896
- error
897
- );
898
- return new Response(
899
- JSON.stringify({
900
- error: "Failed to checkout repository",
901
- message: error instanceof Error ? error.message : "Unknown error",
902
- }),
903
- {
904
- headers: {
905
- "Content-Type": "application/json",
906
- ...corsHeaders,
907
- },
908
- status: 500,
909
- }
910
- );
911
- }
912
- }
913
-
914
- async function handleMkdirRequest(
915
- req: Request,
916
- corsHeaders: Record<string, string>
917
- ): Promise<Response> {
918
- try {
919
- const body = (await req.json()) as MkdirRequest;
920
- const { path, recursive = false, sessionId } = body;
921
-
922
- if (!path || typeof path !== "string") {
923
- return new Response(
924
- JSON.stringify({
925
- error: "Path is required and must be a string",
926
- }),
927
- {
928
- headers: {
929
- "Content-Type": "application/json",
930
- ...corsHeaders,
931
- },
932
- status: 400,
933
- }
934
- );
935
- }
936
-
937
- // Basic safety check - prevent dangerous paths
938
- const dangerousPatterns = [
939
- /^\/$/, // Root directory
940
- /^\/etc/, // System directories
941
- /^\/var/, // System directories
942
- /^\/usr/, // System directories
943
- /^\/bin/, // System directories
944
- /^\/sbin/, // System directories
945
- /^\/boot/, // System directories
946
- /^\/dev/, // System directories
947
- /^\/proc/, // System directories
948
- /^\/sys/, // System directories
949
- /^\/tmp\/\.\./, // Path traversal attempts
950
- /\.\./, // Path traversal attempts
951
- ];
952
-
953
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
954
- return new Response(
955
- JSON.stringify({
956
- error: "Dangerous path not allowed",
957
- }),
958
- {
959
- headers: {
960
- "Content-Type": "application/json",
961
- ...corsHeaders,
962
- },
963
- status: 400,
964
- }
965
- );
966
- }
967
-
968
- console.log(
969
- `[Server] Creating directory: ${path} (recursive: ${recursive})`
970
- );
971
-
972
- const result = await executeMkdir(path, recursive, sessionId);
973
-
974
- return new Response(
975
- JSON.stringify({
976
- exitCode: result.exitCode,
977
- path,
978
- recursive,
979
- stderr: result.stderr,
980
- stdout: result.stdout,
981
- success: result.success,
982
- timestamp: new Date().toISOString(),
983
- }),
984
- {
985
- headers: {
986
- "Content-Type": "application/json",
987
- ...corsHeaders,
988
- },
989
- }
990
- );
991
- } catch (error) {
992
- console.error("[Server] Error in handleMkdirRequest:", error);
993
- return new Response(
994
- JSON.stringify({
995
- error: "Failed to create directory",
996
- message: error instanceof Error ? error.message : "Unknown error",
997
- }),
998
- {
999
- headers: {
1000
- "Content-Type": "application/json",
1001
- ...corsHeaders,
1002
- },
1003
- status: 500,
1004
- }
1005
- );
1006
- }
1007
- }
1008
-
1009
- async function handleStreamingMkdirRequest(
1010
- req: Request,
1011
- corsHeaders: Record<string, string>
1012
- ): Promise<Response> {
1013
- try {
1014
- const body = (await req.json()) as MkdirRequest;
1015
- const { path, recursive = false, sessionId } = body;
1016
-
1017
- if (!path || typeof path !== "string") {
1018
- return new Response(
1019
- JSON.stringify({
1020
- error: "Path is required and must be a string",
1021
- }),
1022
- {
1023
- headers: {
1024
- "Content-Type": "application/json",
1025
- ...corsHeaders,
1026
- },
1027
- status: 400,
1028
- }
1029
- );
1030
- }
1031
-
1032
- // Basic safety check - prevent dangerous paths
1033
- const dangerousPatterns = [
1034
- /^\/$/, // Root directory
1035
- /^\/etc/, // System directories
1036
- /^\/var/, // System directories
1037
- /^\/usr/, // System directories
1038
- /^\/bin/, // System directories
1039
- /^\/sbin/, // System directories
1040
- /^\/boot/, // System directories
1041
- /^\/dev/, // System directories
1042
- /^\/proc/, // System directories
1043
- /^\/sys/, // System directories
1044
- /^\/tmp\/\.\./, // Path traversal attempts
1045
- /\.\./, // Path traversal attempts
1046
- ];
1047
-
1048
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1049
- return new Response(
1050
- JSON.stringify({
1051
- error: "Dangerous path not allowed",
1052
- }),
1053
- {
1054
- headers: {
1055
- "Content-Type": "application/json",
1056
- ...corsHeaders,
1057
- },
1058
- status: 400,
1059
- }
1060
- );
1061
- }
1062
-
1063
- console.log(
1064
- `[Server] Creating directory: ${path} (recursive: ${recursive})`
1065
- );
1066
-
1067
- const stream = new ReadableStream({
1068
- start(controller) {
1069
- const args = recursive ? ["-p", path] : [path];
1070
- const child = spawn("mkdir", args, {
1071
- shell: true,
1072
- stdio: ["pipe", "pipe", "pipe"],
1073
- });
1074
-
1075
- // Store the process reference for cleanup if sessionId is provided
1076
- if (sessionId && sessions.has(sessionId)) {
1077
- const session = sessions.get(sessionId)!;
1078
- session.activeProcess = child;
1079
- }
1080
-
1081
- let stdout = "";
1082
- let stderr = "";
1083
-
1084
- // Send command start event
1085
- controller.enqueue(
1086
- new TextEncoder().encode(
1087
- `data: ${JSON.stringify({
1088
- args,
1089
- command: "mkdir",
1090
- timestamp: new Date().toISOString(),
1091
- type: "command_start",
1092
- })}\n\n`
1093
- )
1094
- );
1095
-
1096
- child.stdout?.on("data", (data) => {
1097
- const output = data.toString();
1098
- stdout += output;
1099
-
1100
- // Send real-time output
1101
- controller.enqueue(
1102
- new TextEncoder().encode(
1103
- `data: ${JSON.stringify({
1104
- command: "mkdir",
1105
- data: output,
1106
- stream: "stdout",
1107
- type: "output",
1108
- })}\n\n`
1109
- )
1110
- );
1111
- });
1112
-
1113
- child.stderr?.on("data", (data) => {
1114
- const output = data.toString();
1115
- stderr += output;
1116
-
1117
- // Send real-time error output
1118
- controller.enqueue(
1119
- new TextEncoder().encode(
1120
- `data: ${JSON.stringify({
1121
- command: "mkdir",
1122
- data: output,
1123
- stream: "stderr",
1124
- type: "output",
1125
- })}\n\n`
1126
- )
1127
- );
1128
- });
1129
-
1130
- child.on("close", (code) => {
1131
- // Clear the active process reference
1132
- if (sessionId && sessions.has(sessionId)) {
1133
- const session = sessions.get(sessionId)!;
1134
- session.activeProcess = null;
1135
- }
1136
-
1137
- console.log(`[Server] Command completed: mkdir, Exit code: ${code}`);
1138
-
1139
- // Send command completion event
1140
- controller.enqueue(
1141
- new TextEncoder().encode(
1142
- `data: ${JSON.stringify({
1143
- args,
1144
- command: "mkdir",
1145
- exitCode: code,
1146
- stderr,
1147
- stdout,
1148
- success: code === 0,
1149
- timestamp: new Date().toISOString(),
1150
- type: "command_complete",
1151
- })}\n\n`
1152
- )
1153
- );
1154
-
1155
- controller.close();
1156
- });
1157
-
1158
- child.on("error", (error) => {
1159
- // Clear the active process reference
1160
- if (sessionId && sessions.has(sessionId)) {
1161
- const session = sessions.get(sessionId)!;
1162
- session.activeProcess = null;
1163
- }
1164
-
1165
- controller.enqueue(
1166
- new TextEncoder().encode(
1167
- `data: ${JSON.stringify({
1168
- args,
1169
- command: "mkdir",
1170
- error: error.message,
1171
- type: "error",
1172
- })}\n\n`
1173
- )
1174
- );
1175
-
1176
- controller.close();
1177
- });
1178
- },
1179
- });
1180
-
1181
- return new Response(stream, {
1182
- headers: {
1183
- "Cache-Control": "no-cache",
1184
- Connection: "keep-alive",
1185
- "Content-Type": "text/event-stream",
1186
- ...corsHeaders,
1187
- },
1188
- });
1189
- } catch (error) {
1190
- console.error("[Server] Error in handleStreamingMkdirRequest:", error);
1191
- return new Response(
1192
- JSON.stringify({
1193
- error: "Failed to create directory",
1194
- message: error instanceof Error ? error.message : "Unknown error",
1195
- }),
1196
- {
1197
- headers: {
1198
- "Content-Type": "application/json",
1199
- ...corsHeaders,
1200
- },
1201
- status: 500,
1202
- }
1203
- );
1204
- }
1205
- }
1206
-
1207
- async function handleWriteFileRequest(
1208
- req: Request,
1209
- corsHeaders: Record<string, string>
1210
- ): Promise<Response> {
1211
- try {
1212
- const body = (await req.json()) as WriteFileRequest;
1213
- const { path, content, encoding = "utf-8", sessionId } = body;
1214
-
1215
- if (!path || typeof path !== "string") {
1216
- return new Response(
1217
- JSON.stringify({
1218
- error: "Path is required and must be a string",
1219
- }),
1220
- {
1221
- headers: {
1222
- "Content-Type": "application/json",
1223
- ...corsHeaders,
1224
- },
1225
- status: 400,
1226
- }
1227
- );
1228
- }
1229
-
1230
- // Basic safety check - prevent dangerous paths
1231
- const dangerousPatterns = [
1232
- /^\/$/, // Root directory
1233
- /^\/etc/, // System directories
1234
- /^\/var/, // System directories
1235
- /^\/usr/, // System directories
1236
- /^\/bin/, // System directories
1237
- /^\/sbin/, // System directories
1238
- /^\/boot/, // System directories
1239
- /^\/dev/, // System directories
1240
- /^\/proc/, // System directories
1241
- /^\/sys/, // System directories
1242
- /^\/tmp\/\.\./, // Path traversal attempts
1243
- /\.\./, // Path traversal attempts
1244
- ];
1245
-
1246
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1247
- return new Response(
1248
- JSON.stringify({
1249
- error: "Dangerous path not allowed",
1250
- }),
1251
- {
1252
- headers: {
1253
- "Content-Type": "application/json",
1254
- ...corsHeaders,
1255
- },
1256
- status: 400,
1257
- }
1258
- );
1259
- }
1260
-
1261
- console.log(
1262
- `[Server] Writing file: ${path} (content length: ${content.length})`
1263
- );
1264
-
1265
- const result = await executeWriteFile(path, content, encoding, sessionId);
1266
-
1267
- return new Response(
1268
- JSON.stringify({
1269
- exitCode: result.exitCode,
1270
- path,
1271
- success: result.success,
1272
- timestamp: new Date().toISOString(),
1273
- }),
1274
- {
1275
- headers: {
1276
- "Content-Type": "application/json",
1277
- ...corsHeaders,
1278
- },
1279
- }
1280
- );
1281
- } catch (error) {
1282
- console.error("[Server] Error in handleWriteFileRequest:", error);
1283
- return new Response(
1284
- JSON.stringify({
1285
- error: "Failed to write file",
1286
- message: error instanceof Error ? error.message : "Unknown error",
1287
- }),
1288
- {
1289
- headers: {
1290
- "Content-Type": "application/json",
1291
- ...corsHeaders,
1292
- },
1293
- status: 500,
1294
- }
1295
- );
1296
- }
1297
- }
1298
-
1299
- async function handleStreamingWriteFileRequest(
1300
- req: Request,
1301
- corsHeaders: Record<string, string>
1302
- ): Promise<Response> {
1303
- try {
1304
- const body = (await req.json()) as WriteFileRequest;
1305
- const { path, content, encoding = "utf-8", sessionId } = body;
1306
-
1307
- if (!path || typeof path !== "string") {
1308
- return new Response(
1309
- JSON.stringify({
1310
- error: "Path is required and must be a string",
1311
- }),
1312
- {
1313
- headers: {
1314
- "Content-Type": "application/json",
1315
- ...corsHeaders,
1316
- },
1317
- status: 400,
1318
- }
1319
- );
1320
- }
1321
-
1322
- // Basic safety check - prevent dangerous paths
1323
- const dangerousPatterns = [
1324
- /^\/$/, // Root directory
1325
- /^\/etc/, // System directories
1326
- /^\/var/, // System directories
1327
- /^\/usr/, // System directories
1328
- /^\/bin/, // System directories
1329
- /^\/sbin/, // System directories
1330
- /^\/boot/, // System directories
1331
- /^\/dev/, // System directories
1332
- /^\/proc/, // System directories
1333
- /^\/sys/, // System directories
1334
- /^\/tmp\/\.\./, // Path traversal attempts
1335
- /\.\./, // Path traversal attempts
1336
- ];
1337
-
1338
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1339
- return new Response(
1340
- JSON.stringify({
1341
- error: "Dangerous path not allowed",
1342
- }),
1343
- {
1344
- headers: {
1345
- "Content-Type": "application/json",
1346
- ...corsHeaders,
1347
- },
1348
- status: 400,
1349
- }
1350
- );
1351
- }
1352
-
1353
- console.log(
1354
- `[Server] Writing file (streaming): ${path} (content length: ${content.length})`
1355
- );
1356
-
1357
- const stream = new ReadableStream({
1358
- start(controller) {
1359
- (async () => {
1360
- try {
1361
- // Send command start event
1362
- controller.enqueue(
1363
- new TextEncoder().encode(
1364
- `data: ${JSON.stringify({
1365
- path,
1366
- timestamp: new Date().toISOString(),
1367
- type: "command_start",
1368
- })}\n\n`
1369
- )
1370
- );
1371
-
1372
- // Ensure the directory exists
1373
- const dir = dirname(path);
1374
- if (dir !== ".") {
1375
- await mkdir(dir, { recursive: true });
1376
-
1377
- // Send directory creation event
1378
- controller.enqueue(
1379
- new TextEncoder().encode(
1380
- `data: ${JSON.stringify({
1381
- message: `Created directory: ${dir}`,
1382
- type: "output",
1383
- })}\n\n`
1384
- )
1385
- );
1386
- }
1387
-
1388
- // Write the file
1389
- await writeFile(path, content, {
1390
- encoding: encoding as BufferEncoding,
1391
- });
1392
-
1393
- console.log(`[Server] File written successfully: ${path}`);
1394
-
1395
- // Send command completion event
1396
- controller.enqueue(
1397
- new TextEncoder().encode(
1398
- `data: ${JSON.stringify({
1399
- path,
1400
- success: true,
1401
- timestamp: new Date().toISOString(),
1402
- type: "command_complete",
1403
- })}\n\n`
1404
- )
1405
- );
1406
-
1407
- controller.close();
1408
- } catch (error) {
1409
- console.error(`[Server] Error writing file: ${path}`, error);
1410
-
1411
- controller.enqueue(
1412
- new TextEncoder().encode(
1413
- `data: ${JSON.stringify({
1414
- error:
1415
- error instanceof Error ? error.message : "Unknown error",
1416
- path,
1417
- type: "error",
1418
- })}\n\n`
1419
- )
1420
- );
1421
-
1422
- controller.close();
1423
- }
1424
- })();
1425
- },
1426
- });
1427
-
1428
- return new Response(stream, {
1429
- headers: {
1430
- "Cache-Control": "no-cache",
1431
- Connection: "keep-alive",
1432
- "Content-Type": "text/event-stream",
1433
- ...corsHeaders,
1434
- },
1435
- });
1436
- } catch (error) {
1437
- console.error("[Server] Error in handleStreamingWriteFileRequest:", error);
1438
- return new Response(
1439
- JSON.stringify({
1440
- error: "Failed to write file",
1441
- message: error instanceof Error ? error.message : "Unknown error",
1442
- }),
1443
- {
1444
- headers: {
1445
- "Content-Type": "application/json",
1446
- ...corsHeaders,
1447
- },
1448
- status: 500,
1449
- }
1450
- );
1451
- }
1452
- }
1453
-
1454
- async function handleReadFileRequest(
1455
- req: Request,
1456
- corsHeaders: Record<string, string>
1457
- ): Promise<Response> {
1458
- try {
1459
- const body = (await req.json()) as ReadFileRequest;
1460
- const { path, encoding = "utf-8", sessionId } = body;
1461
-
1462
- if (!path || typeof path !== "string") {
1463
- return new Response(
1464
- JSON.stringify({
1465
- error: "Path is required and must be a string",
1466
- }),
1467
- {
1468
- headers: {
1469
- "Content-Type": "application/json",
1470
- ...corsHeaders,
1471
- },
1472
- status: 400,
1473
- }
1474
- );
1475
- }
1476
-
1477
- // Basic safety check - prevent dangerous paths
1478
- const dangerousPatterns = [
1479
- /^\/$/, // Root directory
1480
- /^\/etc/, // System directories
1481
- /^\/var/, // System directories
1482
- /^\/usr/, // System directories
1483
- /^\/bin/, // System directories
1484
- /^\/sbin/, // System directories
1485
- /^\/boot/, // System directories
1486
- /^\/dev/, // System directories
1487
- /^\/proc/, // System directories
1488
- /^\/sys/, // System directories
1489
- /^\/tmp\/\.\./, // Path traversal attempts
1490
- /\.\./, // Path traversal attempts
1491
- ];
1492
-
1493
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1494
- return new Response(
1495
- JSON.stringify({
1496
- error: "Dangerous path not allowed",
1497
- }),
1498
- {
1499
- headers: {
1500
- "Content-Type": "application/json",
1501
- ...corsHeaders,
1502
- },
1503
- status: 400,
1504
- }
1505
- );
1506
- }
1507
-
1508
- console.log(`[Server] Reading file: ${path}`);
1509
-
1510
- const result = await executeReadFile(path, encoding, sessionId);
1511
-
1512
- return new Response(
1513
- JSON.stringify({
1514
- content: result.content,
1515
- exitCode: result.exitCode,
1516
- path,
1517
- success: result.success,
1518
- timestamp: new Date().toISOString(),
1519
- }),
1520
- {
1521
- headers: {
1522
- "Content-Type": "application/json",
1523
- ...corsHeaders,
1524
- },
1525
- }
1526
- );
1527
- } catch (error) {
1528
- console.error("[Server] Error in handleReadFileRequest:", error);
1529
- return new Response(
1530
- JSON.stringify({
1531
- error: "Failed to read file",
1532
- message: error instanceof Error ? error.message : "Unknown error",
1533
- }),
1534
- {
1535
- headers: {
1536
- "Content-Type": "application/json",
1537
- ...corsHeaders,
1538
- },
1539
- status: 500,
1540
- }
1541
- );
1542
- }
1543
- }
1544
-
1545
- async function handleStreamingReadFileRequest(
1546
- req: Request,
1547
- corsHeaders: Record<string, string>
1548
- ): Promise<Response> {
1549
- try {
1550
- const body = (await req.json()) as ReadFileRequest;
1551
- const { path, encoding = "utf-8", sessionId } = body;
1552
-
1553
- if (!path || typeof path !== "string") {
1554
- return new Response(
1555
- JSON.stringify({
1556
- error: "Path is required and must be a string",
1557
- }),
1558
- {
1559
- headers: {
1560
- "Content-Type": "application/json",
1561
- ...corsHeaders,
1562
- },
1563
- status: 400,
1564
- }
1565
- );
1566
- }
1567
-
1568
- // Basic safety check - prevent dangerous paths
1569
- const dangerousPatterns = [
1570
- /^\/$/, // Root directory
1571
- /^\/etc/, // System directories
1572
- /^\/var/, // System directories
1573
- /^\/usr/, // System directories
1574
- /^\/bin/, // System directories
1575
- /^\/sbin/, // System directories
1576
- /^\/boot/, // System directories
1577
- /^\/dev/, // System directories
1578
- /^\/proc/, // System directories
1579
- /^\/sys/, // System directories
1580
- /^\/tmp\/\.\./, // Path traversal attempts
1581
- /\.\./, // Path traversal attempts
1582
- ];
1583
-
1584
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1585
- return new Response(
1586
- JSON.stringify({
1587
- error: "Dangerous path not allowed",
1588
- }),
1589
- {
1590
- headers: {
1591
- "Content-Type": "application/json",
1592
- ...corsHeaders,
1593
- },
1594
- status: 400,
1595
- }
1596
- );
1597
- }
1598
-
1599
- console.log(`[Server] Reading file (streaming): ${path}`);
1600
-
1601
- const stream = new ReadableStream({
1602
- start(controller) {
1603
- (async () => {
1604
- try {
1605
- // Send command start event
1606
- controller.enqueue(
1607
- new TextEncoder().encode(
1608
- `data: ${JSON.stringify({
1609
- path,
1610
- timestamp: new Date().toISOString(),
1611
- type: "command_start",
1612
- })}\n\n`
1613
- )
1614
- );
1615
-
1616
- // Read the file
1617
- const content = await readFile(path, {
1618
- encoding: encoding as BufferEncoding,
1619
- });
1620
-
1621
- console.log(`[Server] File read successfully: ${path}`);
1622
-
1623
- // Send command completion event
1624
- controller.enqueue(
1625
- new TextEncoder().encode(
1626
- `data: ${JSON.stringify({
1627
- content,
1628
- path,
1629
- success: true,
1630
- timestamp: new Date().toISOString(),
1631
- type: "command_complete",
1632
- })}\n\n`
1633
- )
1634
- );
1635
-
1636
- controller.close();
1637
- } catch (error) {
1638
- console.error(`[Server] Error reading file: ${path}`, error);
1639
-
1640
- controller.enqueue(
1641
- new TextEncoder().encode(
1642
- `data: ${JSON.stringify({
1643
- error:
1644
- error instanceof Error ? error.message : "Unknown error",
1645
- path,
1646
- type: "error",
1647
- })}\n\n`
1648
- )
1649
- );
1650
-
1651
- controller.close();
1652
- }
1653
- })();
1654
- },
1655
- });
1656
-
1657
- return new Response(stream, {
1658
- headers: {
1659
- "Cache-Control": "no-cache",
1660
- Connection: "keep-alive",
1661
- "Content-Type": "text/event-stream",
1662
- ...corsHeaders,
1663
- },
1664
- });
1665
- } catch (error) {
1666
- console.error("[Server] Error in handleStreamingReadFileRequest:", error);
1667
- return new Response(
1668
- JSON.stringify({
1669
- error: "Failed to read file",
1670
- message: error instanceof Error ? error.message : "Unknown error",
1671
- }),
1672
- {
1673
- headers: {
1674
- "Content-Type": "application/json",
1675
- ...corsHeaders,
1676
- },
1677
- status: 500,
1678
- }
1679
- );
1680
- }
1681
- }
1682
-
1683
- async function handleDeleteFileRequest(
1684
- req: Request,
1685
- corsHeaders: Record<string, string>
1686
- ): Promise<Response> {
1687
- try {
1688
- const body = (await req.json()) as DeleteFileRequest;
1689
- const { path, sessionId } = body;
1690
-
1691
- if (!path || typeof path !== "string") {
1692
- return new Response(
1693
- JSON.stringify({
1694
- error: "Path is required and must be a string",
1695
- }),
1696
- {
1697
- headers: {
1698
- "Content-Type": "application/json",
1699
- ...corsHeaders,
1700
- },
1701
- status: 400,
1702
- }
1703
- );
1704
- }
1705
-
1706
- // Basic safety check - prevent dangerous paths
1707
- const dangerousPatterns = [
1708
- /^\/$/, // Root directory
1709
- /^\/etc/, // System directories
1710
- /^\/var/, // System directories
1711
- /^\/usr/, // System directories
1712
- /^\/bin/, // System directories
1713
- /^\/sbin/, // System directories
1714
- /^\/boot/, // System directories
1715
- /^\/dev/, // System directories
1716
- /^\/proc/, // System directories
1717
- /^\/sys/, // System directories
1718
- /^\/tmp\/\.\./, // Path traversal attempts
1719
- /\.\./, // Path traversal attempts
1720
- ];
1721
-
1722
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1723
- return new Response(
1724
- JSON.stringify({
1725
- error: "Dangerous path not allowed",
1726
- }),
1727
- {
1728
- headers: {
1729
- "Content-Type": "application/json",
1730
- ...corsHeaders,
1731
- },
1732
- status: 400,
1733
- }
1734
- );
1735
- }
1736
-
1737
- console.log(`[Server] Deleting file: ${path}`);
1738
-
1739
- const result = await executeDeleteFile(path, sessionId);
1740
-
1741
- return new Response(
1742
- JSON.stringify({
1743
- exitCode: result.exitCode,
1744
- path,
1745
- success: result.success,
1746
- timestamp: new Date().toISOString(),
1747
- }),
1748
- {
1749
- headers: {
1750
- "Content-Type": "application/json",
1751
- ...corsHeaders,
1752
- },
1753
- }
1754
- );
1755
- } catch (error) {
1756
- console.error("[Server] Error in handleDeleteFileRequest:", error);
1757
- return new Response(
1758
- JSON.stringify({
1759
- error: "Failed to delete file",
1760
- message: error instanceof Error ? error.message : "Unknown error",
1761
- }),
1762
- {
1763
- headers: {
1764
- "Content-Type": "application/json",
1765
- ...corsHeaders,
1766
- },
1767
- status: 500,
1768
- }
1769
- );
1770
- }
1771
- }
1772
-
1773
- async function handleStreamingDeleteFileRequest(
1774
- req: Request,
1775
- corsHeaders: Record<string, string>
1776
- ): Promise<Response> {
1777
- try {
1778
- const body = (await req.json()) as DeleteFileRequest;
1779
- const { path, sessionId } = body;
1780
-
1781
- if (!path || typeof path !== "string") {
1782
- return new Response(
1783
- JSON.stringify({
1784
- error: "Path is required and must be a string",
1785
- }),
1786
- {
1787
- headers: {
1788
- "Content-Type": "application/json",
1789
- ...corsHeaders,
1790
- },
1791
- status: 400,
1792
- }
1793
- );
1794
- }
1795
-
1796
- // Basic safety check - prevent dangerous paths
1797
- const dangerousPatterns = [
1798
- /^\/$/, // Root directory
1799
- /^\/etc/, // System directories
1800
- /^\/var/, // System directories
1801
- /^\/usr/, // System directories
1802
- /^\/bin/, // System directories
1803
- /^\/sbin/, // System directories
1804
- /^\/boot/, // System directories
1805
- /^\/dev/, // System directories
1806
- /^\/proc/, // System directories
1807
- /^\/sys/, // System directories
1808
- /^\/tmp\/\.\./, // Path traversal attempts
1809
- /\.\./, // Path traversal attempts
1810
- ];
1811
-
1812
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1813
- return new Response(
1814
- JSON.stringify({
1815
- error: "Dangerous path not allowed",
1816
- }),
1817
- {
1818
- headers: {
1819
- "Content-Type": "application/json",
1820
- ...corsHeaders,
1821
- },
1822
- status: 400,
1823
- }
1824
- );
1825
- }
1826
-
1827
- console.log(`[Server] Deleting file (streaming): ${path}`);
1828
-
1829
- const stream = new ReadableStream({
1830
- start(controller) {
1831
- (async () => {
1832
- try {
1833
- // Send command start event
1834
- controller.enqueue(
1835
- new TextEncoder().encode(
1836
- `data: ${JSON.stringify({
1837
- path,
1838
- timestamp: new Date().toISOString(),
1839
- type: "command_start",
1840
- })}\n\n`
1841
- )
1842
- );
1843
-
1844
- // Delete the file
1845
- await executeDeleteFile(path, sessionId);
1846
-
1847
- console.log(`[Server] File deleted successfully: ${path}`);
1848
-
1849
- // Send command completion event
1850
- controller.enqueue(
1851
- new TextEncoder().encode(
1852
- `data: ${JSON.stringify({
1853
- path,
1854
- success: true,
1855
- timestamp: new Date().toISOString(),
1856
- type: "command_complete",
1857
- })}\n\n`
1858
- )
1859
- );
1860
-
1861
- controller.close();
1862
- } catch (error) {
1863
- console.error(`[Server] Error deleting file: ${path}`, error);
1864
-
1865
- controller.enqueue(
1866
- new TextEncoder().encode(
1867
- `data: ${JSON.stringify({
1868
- error:
1869
- error instanceof Error ? error.message : "Unknown error",
1870
- path,
1871
- type: "error",
1872
- })}\n\n`
1873
- )
1874
- );
1875
-
1876
- controller.close();
1877
- }
1878
- })();
1879
- },
1880
- });
1881
-
1882
- return new Response(stream, {
1883
- headers: {
1884
- "Cache-Control": "no-cache",
1885
- Connection: "keep-alive",
1886
- "Content-Type": "text/event-stream",
1887
- ...corsHeaders,
1888
- },
1889
- });
1890
- } catch (error) {
1891
- console.error("[Server] Error in handleStreamingDeleteFileRequest:", error);
1892
- return new Response(
1893
- JSON.stringify({
1894
- error: "Failed to delete file",
1895
- message: error instanceof Error ? error.message : "Unknown error",
1896
- }),
1897
- {
1898
- headers: {
1899
- "Content-Type": "application/json",
1900
- ...corsHeaders,
1901
- },
1902
- status: 500,
1903
- }
1904
- );
1905
- }
1906
- }
1907
-
1908
- async function handleRenameFileRequest(
1909
- req: Request,
1910
- corsHeaders: Record<string, string>
1911
- ): Promise<Response> {
1912
- try {
1913
- const body = (await req.json()) as RenameFileRequest;
1914
- const { oldPath, newPath, sessionId } = body;
1915
-
1916
- if (!oldPath || typeof oldPath !== "string") {
1917
- return new Response(
1918
- JSON.stringify({
1919
- error: "Old path is required and must be a string",
1920
- }),
1921
- {
1922
- headers: {
1923
- "Content-Type": "application/json",
1924
- ...corsHeaders,
1925
- },
1926
- status: 400,
1927
- }
1928
- );
1929
- }
1930
-
1931
- if (!newPath || typeof newPath !== "string") {
1932
- return new Response(
1933
- JSON.stringify({
1934
- error: "New path is required and must be a string",
1935
- }),
1936
- {
1937
- headers: {
1938
- "Content-Type": "application/json",
1939
- ...corsHeaders,
1940
- },
1941
- status: 400,
1942
- }
1943
- );
1944
- }
1945
-
1946
- // Basic safety check - prevent dangerous paths
1947
- const dangerousPatterns = [
1948
- /^\/$/, // Root directory
1949
- /^\/etc/, // System directories
1950
- /^\/var/, // System directories
1951
- /^\/usr/, // System directories
1952
- /^\/bin/, // System directories
1953
- /^\/sbin/, // System directories
1954
- /^\/boot/, // System directories
1955
- /^\/dev/, // System directories
1956
- /^\/proc/, // System directories
1957
- /^\/sys/, // System directories
1958
- /^\/tmp\/\.\./, // Path traversal attempts
1959
- /\.\./, // Path traversal attempts
1960
- ];
1961
-
1962
- if (
1963
- dangerousPatterns.some(
1964
- (pattern) => pattern.test(oldPath) || pattern.test(newPath)
1965
- )
1966
- ) {
1967
- return new Response(
1968
- JSON.stringify({
1969
- error: "Dangerous path not allowed",
1970
- }),
1971
- {
1972
- headers: {
1973
- "Content-Type": "application/json",
1974
- ...corsHeaders,
1975
- },
1976
- status: 400,
1977
- }
1978
- );
1979
- }
1980
-
1981
- console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
1982
-
1983
- const result = await executeRenameFile(oldPath, newPath, sessionId);
1984
-
1985
- return new Response(
1986
- JSON.stringify({
1987
- exitCode: result.exitCode,
1988
- newPath,
1989
- oldPath,
1990
- success: result.success,
1991
- timestamp: new Date().toISOString(),
1992
- }),
1993
- {
1994
- headers: {
1995
- "Content-Type": "application/json",
1996
- ...corsHeaders,
1997
- },
1998
- }
1999
- );
2000
- } catch (error) {
2001
- console.error("[Server] Error in handleRenameFileRequest:", error);
2002
- return new Response(
2003
- JSON.stringify({
2004
- error: "Failed to rename file",
2005
- message: error instanceof Error ? error.message : "Unknown error",
2006
- }),
2007
- {
2008
- headers: {
2009
- "Content-Type": "application/json",
2010
- ...corsHeaders,
2011
- },
2012
- status: 500,
2013
- }
2014
- );
2015
- }
2016
- }
2017
-
2018
- async function handleStreamingRenameFileRequest(
2019
- req: Request,
2020
- corsHeaders: Record<string, string>
2021
- ): Promise<Response> {
2022
- try {
2023
- const body = (await req.json()) as RenameFileRequest;
2024
- const { oldPath, newPath, sessionId } = body;
2025
-
2026
- if (!oldPath || typeof oldPath !== "string") {
2027
- return new Response(
2028
- JSON.stringify({
2029
- error: "Old path is required and must be a string",
2030
- }),
2031
- {
2032
- headers: {
2033
- "Content-Type": "application/json",
2034
- ...corsHeaders,
2035
- },
2036
- status: 400,
2037
- }
2038
- );
2039
- }
2040
-
2041
- if (!newPath || typeof newPath !== "string") {
2042
- return new Response(
2043
- JSON.stringify({
2044
- error: "New path is required and must be a string",
2045
- }),
2046
- {
2047
- headers: {
2048
- "Content-Type": "application/json",
2049
- ...corsHeaders,
2050
- },
2051
- status: 400,
2052
- }
2053
- );
2054
- }
2055
-
2056
- // Basic safety check - prevent dangerous paths
2057
- const dangerousPatterns = [
2058
- /^\/$/, // Root directory
2059
- /^\/etc/, // System directories
2060
- /^\/var/, // System directories
2061
- /^\/usr/, // System directories
2062
- /^\/bin/, // System directories
2063
- /^\/sbin/, // System directories
2064
- /^\/boot/, // System directories
2065
- /^\/dev/, // System directories
2066
- /^\/proc/, // System directories
2067
- /^\/sys/, // System directories
2068
- /^\/tmp\/\.\./, // Path traversal attempts
2069
- /\.\./, // Path traversal attempts
2070
- ];
2071
-
2072
- if (
2073
- dangerousPatterns.some(
2074
- (pattern) => pattern.test(oldPath) || pattern.test(newPath)
2075
- )
2076
- ) {
2077
- return new Response(
2078
- JSON.stringify({
2079
- error: "Dangerous path not allowed",
2080
- }),
2081
- {
2082
- headers: {
2083
- "Content-Type": "application/json",
2084
- ...corsHeaders,
2085
- },
2086
- status: 400,
2087
- }
2088
- );
2089
- }
2090
-
2091
- console.log(`[Server] Renaming file (streaming): ${oldPath} -> ${newPath}`);
2092
-
2093
- const stream = new ReadableStream({
2094
- start(controller) {
2095
- (async () => {
2096
- try {
2097
- // Send command start event
2098
- controller.enqueue(
2099
- new TextEncoder().encode(
2100
- `data: ${JSON.stringify({
2101
- newPath,
2102
- oldPath,
2103
- timestamp: new Date().toISOString(),
2104
- type: "command_start",
2105
- })}\n\n`
2106
- )
2107
- );
2108
-
2109
- // Rename the file
2110
- await executeRenameFile(oldPath, newPath, sessionId);
2111
-
2112
- console.log(
2113
- `[Server] File renamed successfully: ${oldPath} -> ${newPath}`
2114
- );
2115
-
2116
- // Send command completion event
2117
- controller.enqueue(
2118
- new TextEncoder().encode(
2119
- `data: ${JSON.stringify({
2120
- newPath,
2121
- oldPath,
2122
- success: true,
2123
- timestamp: new Date().toISOString(),
2124
- type: "command_complete",
2125
- })}\n\n`
2126
- )
2127
- );
2128
-
2129
- controller.close();
2130
- } catch (error) {
2131
- console.error(
2132
- `[Server] Error renaming file: ${oldPath} -> ${newPath}`,
2133
- error
2134
- );
2135
-
2136
- controller.enqueue(
2137
- new TextEncoder().encode(
2138
- `data: ${JSON.stringify({
2139
- error:
2140
- error instanceof Error ? error.message : "Unknown error",
2141
- newPath,
2142
- oldPath,
2143
- type: "error",
2144
- })}\n\n`
2145
- )
2146
- );
2147
-
2148
- controller.close();
2149
- }
2150
- })();
2151
- },
2152
- });
2153
-
2154
- return new Response(stream, {
2155
- headers: {
2156
- "Cache-Control": "no-cache",
2157
- Connection: "keep-alive",
2158
- "Content-Type": "text/event-stream",
2159
- ...corsHeaders,
2160
- },
2161
- });
2162
- } catch (error) {
2163
- console.error("[Server] Error in handleStreamingRenameFileRequest:", error);
2164
- return new Response(
2165
- JSON.stringify({
2166
- error: "Failed to rename file",
2167
- message: error instanceof Error ? error.message : "Unknown error",
2168
- }),
2169
- {
2170
- headers: {
2171
- "Content-Type": "application/json",
2172
- ...corsHeaders,
2173
- },
2174
- status: 500,
2175
- }
2176
- );
2177
- }
2178
- }
2179
-
2180
- async function handleMoveFileRequest(
2181
- req: Request,
2182
- corsHeaders: Record<string, string>
2183
- ): Promise<Response> {
2184
- try {
2185
- const body = (await req.json()) as MoveFileRequest;
2186
- const { sourcePath, destinationPath, sessionId } = body;
2187
-
2188
- if (!sourcePath || typeof sourcePath !== "string") {
2189
- return new Response(
2190
- JSON.stringify({
2191
- error: "Source path is required and must be a string",
2192
- }),
2193
- {
2194
- headers: {
2195
- "Content-Type": "application/json",
2196
- ...corsHeaders,
2197
- },
2198
- status: 400,
2199
- }
2200
- );
2201
- }
2202
-
2203
- if (!destinationPath || typeof destinationPath !== "string") {
2204
- return new Response(
2205
- JSON.stringify({
2206
- error: "Destination path is required and must be a string",
2207
- }),
2208
- {
2209
- headers: {
2210
- "Content-Type": "application/json",
2211
- ...corsHeaders,
2212
- },
2213
- status: 400,
2214
- }
2215
- );
2216
- }
2217
-
2218
- // Basic safety check - prevent dangerous paths
2219
- const dangerousPatterns = [
2220
- /^\/$/, // Root directory
2221
- /^\/etc/, // System directories
2222
- /^\/var/, // System directories
2223
- /^\/usr/, // System directories
2224
- /^\/bin/, // System directories
2225
- /^\/sbin/, // System directories
2226
- /^\/boot/, // System directories
2227
- /^\/dev/, // System directories
2228
- /^\/proc/, // System directories
2229
- /^\/sys/, // System directories
2230
- /^\/tmp\/\.\./, // Path traversal attempts
2231
- /\.\./, // Path traversal attempts
2232
- ];
2233
-
2234
- if (
2235
- dangerousPatterns.some(
2236
- (pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
2237
- )
2238
- ) {
2239
- return new Response(
2240
- JSON.stringify({
2241
- error: "Dangerous path not allowed",
2242
- }),
2243
- {
2244
- headers: {
2245
- "Content-Type": "application/json",
2246
- ...corsHeaders,
2247
- },
2248
- status: 400,
2249
- }
2250
- );
2251
- }
2252
-
2253
- console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
2254
-
2255
- const result = await executeMoveFile(
2256
- sourcePath,
2257
- destinationPath,
2258
- sessionId
2259
- );
2260
-
2261
- return new Response(
2262
- JSON.stringify({
2263
- destinationPath,
2264
- exitCode: result.exitCode,
2265
- sourcePath,
2266
- success: result.success,
2267
- timestamp: new Date().toISOString(),
2268
- }),
2269
- {
2270
- headers: {
2271
- "Content-Type": "application/json",
2272
- ...corsHeaders,
2273
- },
2274
- }
2275
- );
2276
- } catch (error) {
2277
- console.error("[Server] Error in handleMoveFileRequest:", error);
2278
- return new Response(
2279
- JSON.stringify({
2280
- error: "Failed to move file",
2281
- message: error instanceof Error ? error.message : "Unknown error",
2282
- }),
2283
- {
2284
- headers: {
2285
- "Content-Type": "application/json",
2286
- ...corsHeaders,
2287
- },
2288
- status: 500,
2289
- }
2290
- );
2291
- }
2292
- }
2293
-
2294
- async function handleStreamingMoveFileRequest(
2295
- req: Request,
2296
- corsHeaders: Record<string, string>
2297
- ): Promise<Response> {
2298
- try {
2299
- const body = (await req.json()) as MoveFileRequest;
2300
- const { sourcePath, destinationPath, sessionId } = body;
2301
-
2302
- if (!sourcePath || typeof sourcePath !== "string") {
2303
- return new Response(
2304
- JSON.stringify({
2305
- error: "Source path is required and must be a string",
2306
- }),
2307
- {
2308
- headers: {
2309
- "Content-Type": "application/json",
2310
- ...corsHeaders,
2311
- },
2312
- status: 400,
2313
- }
2314
- );
2315
- }
2316
-
2317
- if (!destinationPath || typeof destinationPath !== "string") {
2318
- return new Response(
2319
- JSON.stringify({
2320
- error: "Destination path is required and must be a string",
2321
- }),
2322
- {
2323
- headers: {
2324
- "Content-Type": "application/json",
2325
- ...corsHeaders,
2326
- },
2327
- status: 400,
2328
- }
2329
- );
2330
- }
2331
-
2332
- // Basic safety check - prevent dangerous paths
2333
- const dangerousPatterns = [
2334
- /^\/$/, // Root directory
2335
- /^\/etc/, // System directories
2336
- /^\/var/, // System directories
2337
- /^\/usr/, // System directories
2338
- /^\/bin/, // System directories
2339
- /^\/sbin/, // System directories
2340
- /^\/boot/, // System directories
2341
- /^\/dev/, // System directories
2342
- /^\/proc/, // System directories
2343
- /^\/sys/, // System directories
2344
- /^\/tmp\/\.\./, // Path traversal attempts
2345
- /\.\./, // Path traversal attempts
2346
- ];
2347
-
2348
- if (
2349
- dangerousPatterns.some(
2350
- (pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
2351
- )
2352
- ) {
2353
- return new Response(
2354
- JSON.stringify({
2355
- error: "Dangerous path not allowed",
2356
- }),
2357
- {
2358
- headers: {
2359
- "Content-Type": "application/json",
2360
- ...corsHeaders,
2361
- },
2362
- status: 400,
2363
- }
2364
- );
2365
- }
2366
-
2367
- console.log(
2368
- `[Server] Moving file (streaming): ${sourcePath} -> ${destinationPath}`
2369
- );
2370
-
2371
- const stream = new ReadableStream({
2372
- start(controller) {
2373
- (async () => {
2374
- try {
2375
- // Send command start event
2376
- controller.enqueue(
2377
- new TextEncoder().encode(
2378
- `data: ${JSON.stringify({
2379
- destinationPath,
2380
- sourcePath,
2381
- timestamp: new Date().toISOString(),
2382
- type: "command_start",
2383
- })}\n\n`
2384
- )
2385
- );
2386
-
2387
- // Move the file
2388
- await executeMoveFile(sourcePath, destinationPath, sessionId);
2389
-
2390
- console.log(
2391
- `[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
2392
- );
2393
-
2394
- // Send command completion event
2395
- controller.enqueue(
2396
- new TextEncoder().encode(
2397
- `data: ${JSON.stringify({
2398
- destinationPath,
2399
- sourcePath,
2400
- success: true,
2401
- timestamp: new Date().toISOString(),
2402
- type: "command_complete",
2403
- })}\n\n`
2404
- )
2405
- );
2406
-
2407
- controller.close();
2408
- } catch (error) {
2409
- console.error(
2410
- `[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
2411
- error
2412
- );
2413
-
2414
- controller.enqueue(
2415
- new TextEncoder().encode(
2416
- `data: ${JSON.stringify({
2417
- destinationPath,
2418
- error:
2419
- error instanceof Error ? error.message : "Unknown error",
2420
- sourcePath,
2421
- type: "error",
2422
- })}\n\n`
2423
- )
2424
- );
2425
-
2426
- controller.close();
2427
- }
2428
- })();
2429
- },
2430
- });
2431
-
2432
- return new Response(stream, {
2433
- headers: {
2434
- "Cache-Control": "no-cache",
2435
- Connection: "keep-alive",
2436
- "Content-Type": "text/event-stream",
2437
- ...corsHeaders,
2438
- },
2439
- });
2440
- } catch (error) {
2441
- console.error("[Server] Error in handleStreamingMoveFileRequest:", error);
2442
- return new Response(
2443
- JSON.stringify({
2444
- error: "Failed to move file",
2445
- message: error instanceof Error ? error.message : "Unknown error",
2446
- }),
2447
- {
2448
- headers: {
2449
- "Content-Type": "application/json",
2450
- ...corsHeaders,
2451
- },
2452
- status: 500,
2453
- }
2454
- );
2455
- }
2456
- }
2457
-
2458
- function executeCommand(
2459
- command: string,
2460
- args: string[],
2461
- sessionId?: string,
2462
- background?: boolean
2463
- ): Promise<{
2464
- success: boolean;
2465
- stdout: string;
2466
- stderr: string;
2467
- exitCode: number;
2468
- }> {
2469
- return new Promise((resolve, reject) => {
2470
- const spawnOptions: SpawnOptions = {
2471
- shell: true,
2472
- stdio: ["pipe", "pipe", "pipe"] as const,
2473
- detached: background || false,
2474
- };
2475
-
2476
- const child = spawn(command, args, spawnOptions);
2477
-
2478
- // Store the process reference for cleanup if sessionId is provided
2479
- if (sessionId && sessions.has(sessionId)) {
2480
- const session = sessions.get(sessionId)!;
2481
- session.activeProcess = child;
2482
- }
2483
-
2484
- let stdout = "";
2485
- let stderr = "";
2486
-
2487
- child.stdout?.on("data", (data) => {
2488
- stdout += data.toString();
2489
- });
2490
-
2491
- child.stderr?.on("data", (data) => {
2492
- stderr += data.toString();
2493
- });
2494
-
2495
- if (background) {
2496
- // For background processes, unref and return quickly
2497
- child.unref();
2498
-
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);
2508
-
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
2513
- });
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
- }
2522
-
2523
- console.log(`[Server] Command completed: ${command}, Exit code: ${code}`);
2524
-
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
- }
2543
- });
2544
- }
2545
-
2546
- function executeGitCheckout(
2547
- repoUrl: string,
2548
- branch: string,
2549
- targetDir: string,
2550
- sessionId?: string
2551
- ): Promise<{
2552
- success: boolean;
2553
- stdout: string;
2554
- stderr: string;
2555
- exitCode: number;
2556
- }> {
2557
- return new Promise((resolve, reject) => {
2558
- // First, clone the repository
2559
- const cloneChild = spawn(
2560
- "git",
2561
- ["clone", "-b", branch, repoUrl, targetDir],
2562
- {
2563
- shell: true,
2564
- stdio: ["pipe", "pipe", "pipe"],
2565
- }
2566
- );
2567
-
2568
- // Store the process reference for cleanup if sessionId is provided
2569
- if (sessionId && sessions.has(sessionId)) {
2570
- const session = sessions.get(sessionId)!;
2571
- session.activeProcess = cloneChild;
2572
- }
2573
-
2574
- let stdout = "";
2575
- let stderr = "";
2576
-
2577
- cloneChild.stdout?.on("data", (data) => {
2578
- stdout += data.toString();
2579
- });
2580
-
2581
- cloneChild.stderr?.on("data", (data) => {
2582
- stderr += data.toString();
2583
- });
2584
-
2585
- cloneChild.on("close", (code) => {
2586
- // Clear the active process reference
2587
- if (sessionId && sessions.has(sessionId)) {
2588
- const session = sessions.get(sessionId)!;
2589
- session.activeProcess = null;
2590
- }
2591
-
2592
- if (code === 0) {
2593
- console.log(
2594
- `[Server] Repository cloned successfully: ${repoUrl} to ${targetDir}`
2595
- );
2596
- resolve({
2597
- exitCode: code || 0,
2598
- stderr,
2599
- stdout,
2600
- success: true,
2601
- });
2602
- } else {
2603
- console.error(
2604
- `[Server] Failed to clone repository: ${repoUrl}, Exit code: ${code}`
2605
- );
2606
- resolve({
2607
- exitCode: code || 1,
2608
- stderr,
2609
- stdout,
2610
- success: false,
2611
- });
2612
- }
2613
- });
2614
-
2615
- cloneChild.on("error", (error) => {
2616
- // Clear the active process reference
2617
- if (sessionId && sessions.has(sessionId)) {
2618
- const session = sessions.get(sessionId)!;
2619
- session.activeProcess = null;
2620
- }
2621
-
2622
- console.error(`[Server] Error cloning repository: ${repoUrl}`, error);
2623
- reject(error);
2624
- });
2625
- });
2626
- }
2627
-
2628
- function executeMkdir(
2629
- path: string,
2630
- recursive: boolean,
2631
- sessionId?: string
2632
- ): Promise<{
2633
- success: boolean;
2634
- stdout: string;
2635
- stderr: string;
2636
- exitCode: number;
2637
- }> {
2638
- return new Promise((resolve, reject) => {
2639
- const args = recursive ? ["-p", path] : [path];
2640
- const mkdirChild = spawn("mkdir", args, {
2641
- shell: true,
2642
- stdio: ["pipe", "pipe", "pipe"],
2643
- });
2644
-
2645
- // Store the process reference for cleanup if sessionId is provided
2646
- if (sessionId && sessions.has(sessionId)) {
2647
- const session = sessions.get(sessionId)!;
2648
- session.activeProcess = mkdirChild;
2649
- }
2650
-
2651
- let stdout = "";
2652
- let stderr = "";
2653
-
2654
- mkdirChild.stdout?.on("data", (data) => {
2655
- stdout += data.toString();
2656
- });
2657
-
2658
- mkdirChild.stderr?.on("data", (data) => {
2659
- stderr += data.toString();
2660
- });
2661
-
2662
- mkdirChild.on("close", (code) => {
2663
- // Clear the active process reference
2664
- if (sessionId && sessions.has(sessionId)) {
2665
- const session = sessions.get(sessionId)!;
2666
- session.activeProcess = null;
2667
- }
2668
-
2669
- if (code === 0) {
2670
- console.log(`[Server] Directory created successfully: ${path}`);
2671
- resolve({
2672
- exitCode: code || 0,
2673
- stderr,
2674
- stdout,
2675
- success: true,
2676
- });
2677
- } else {
2678
- console.error(
2679
- `[Server] Failed to create directory: ${path}, Exit code: ${code}`
2680
- );
2681
- resolve({
2682
- exitCode: code || 1,
2683
- stderr,
2684
- stdout,
2685
- success: false,
2686
- });
2687
- }
2688
- });
2689
-
2690
- mkdirChild.on("error", (error) => {
2691
- // Clear the active process reference
2692
- if (sessionId && sessions.has(sessionId)) {
2693
- const session = sessions.get(sessionId)!;
2694
- session.activeProcess = null;
2695
- }
2696
-
2697
- console.error(`[Server] Error creating directory: ${path}`, error);
2698
- reject(error);
2699
- });
2700
- });
2701
- }
2702
-
2703
- function executeWriteFile(
2704
- path: string,
2705
- content: string,
2706
- encoding: string,
2707
- sessionId?: string
2708
- ): Promise<{
2709
- success: boolean;
2710
- exitCode: number;
2711
- }> {
2712
- return new Promise((resolve, reject) => {
2713
- (async () => {
2714
- try {
2715
- // Ensure the directory exists
2716
- const dir = dirname(path);
2717
- if (dir !== ".") {
2718
- await mkdir(dir, { recursive: true });
2719
- }
2720
-
2721
- // Write the file
2722
- await writeFile(path, content, {
2723
- encoding: encoding as BufferEncoding,
2724
- });
2725
-
2726
- console.log(`[Server] File written successfully: ${path}`);
2727
- resolve({
2728
- exitCode: 0,
2729
- success: true,
2730
- });
2731
- } catch (error) {
2732
- console.error(`[Server] Error writing file: ${path}`, error);
2733
- reject(error);
2734
- }
2735
- })();
2736
- });
2737
- }
2738
-
2739
- function executeReadFile(
2740
- path: string,
2741
- encoding: string,
2742
- sessionId?: string
2743
- ): Promise<{
2744
- success: boolean;
2745
- exitCode: number;
2746
- content: string;
2747
- }> {
2748
- return new Promise((resolve, reject) => {
2749
- (async () => {
2750
- try {
2751
- // Read the file
2752
- const content = await readFile(path, {
2753
- encoding: encoding as BufferEncoding,
2754
- });
2755
-
2756
- console.log(`[Server] File read successfully: ${path}`);
2757
- resolve({
2758
- content,
2759
- exitCode: 0,
2760
- success: true,
2761
- });
2762
- } catch (error) {
2763
- console.error(`[Server] Error reading file: ${path}`, error);
2764
- reject(error);
2765
- }
2766
- })();
2767
- });
2768
- }
2769
-
2770
- function executeDeleteFile(
2771
- path: string,
2772
- sessionId?: string
2773
- ): Promise<{
2774
- success: boolean;
2775
- exitCode: number;
2776
- }> {
2777
- return new Promise((resolve, reject) => {
2778
- (async () => {
2779
- try {
2780
- // Delete the file
2781
- await unlink(path);
2782
-
2783
- console.log(`[Server] File deleted successfully: ${path}`);
2784
- resolve({
2785
- exitCode: 0,
2786
- success: true,
2787
- });
2788
- } catch (error) {
2789
- console.error(`[Server] Error deleting file: ${path}`, error);
2790
- reject(error);
2791
- }
2792
- })();
2793
- });
2794
- }
2795
-
2796
- function executeRenameFile(
2797
- oldPath: string,
2798
- newPath: string,
2799
- sessionId?: string
2800
- ): Promise<{
2801
- success: boolean;
2802
- exitCode: number;
2803
- }> {
2804
- return new Promise((resolve, reject) => {
2805
- (async () => {
2806
- try {
2807
- // Rename the file
2808
- await rename(oldPath, newPath);
2809
-
2810
- console.log(
2811
- `[Server] File renamed successfully: ${oldPath} -> ${newPath}`
2812
- );
2813
- resolve({
2814
- exitCode: 0,
2815
- success: true,
2816
- });
2817
- } catch (error) {
2818
- console.error(
2819
- `[Server] Error renaming file: ${oldPath} -> ${newPath}`,
2820
- error
2821
- );
2822
- reject(error);
2823
- }
2824
- })();
2825
- });
2826
- }
2827
-
2828
- function executeMoveFile(
2829
- sourcePath: string,
2830
- destinationPath: string,
2831
- sessionId?: string
2832
- ): Promise<{
2833
- success: boolean;
2834
- exitCode: number;
2835
- }> {
2836
- return new Promise((resolve, reject) => {
2837
- (async () => {
2838
- try {
2839
- // Move the file
2840
- await rename(sourcePath, destinationPath);
2841
-
2842
- console.log(
2843
- `[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
2844
- );
2845
- resolve({
2846
- exitCode: 0,
2847
- success: true,
2848
- });
2849
- } catch (error) {
2850
- console.error(
2851
- `[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
2852
- error
2853
- );
2854
- reject(error);
2855
- }
2856
- })();
2857
- });
2858
- }
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
-
3169
571
  console.log(`🚀 Bun server running on http://0.0.0.0:${server.port}`);
3170
572
  console.log(`📡 HTTP API endpoints available:`);
3171
573
  console.log(` POST /api/session/create - Create a new session`);
@@ -3173,24 +575,27 @@ console.log(` GET /api/session/list - List all sessions`);
3173
575
  console.log(` POST /api/execute - Execute a command (non-streaming)`);
3174
576
  console.log(` POST /api/execute/stream - Execute a command (streaming)`);
3175
577
  console.log(` POST /api/git/checkout - Checkout a git repository`);
3176
- console.log(
3177
- ` POST /api/git/checkout/stream - Checkout a git repository (streaming)`
3178
- );
3179
578
  console.log(` POST /api/mkdir - Create a directory`);
3180
- console.log(` POST /api/mkdir/stream - Create a directory (streaming)`);
3181
579
  console.log(` POST /api/write - Write a file`);
3182
- console.log(` POST /api/write/stream - Write a file (streaming)`);
3183
580
  console.log(` POST /api/read - Read a file`);
3184
- console.log(` POST /api/read/stream - Read a file (streaming)`);
3185
581
  console.log(` POST /api/delete - Delete a file`);
3186
- console.log(` POST /api/delete/stream - Delete a file (streaming)`);
3187
582
  console.log(` POST /api/rename - Rename a file`);
3188
- console.log(` POST /api/rename/stream - Rename a file (streaming)`);
3189
583
  console.log(` POST /api/move - Move a file`);
3190
- console.log(` POST /api/move/stream - Move a file (streaming)`);
3191
584
  console.log(` POST /api/expose-port - Expose a port for external access`);
3192
585
  console.log(` DELETE /api/unexpose-port - Unexpose a port`);
3193
586
  console.log(` GET /api/exposed-ports - List exposed ports`);
587
+ console.log(` POST /api/process/start - Start a background process`);
588
+ console.log(` GET /api/process/list - List all processes`);
589
+ console.log(` GET /api/process/{id} - Get process status`);
590
+ console.log(` DELETE /api/process/{id} - Kill a process`);
591
+ console.log(` GET /api/process/{id}/logs - Get process logs`);
592
+ console.log(` GET /api/process/{id}/stream - Stream process logs (SSE)`);
593
+ console.log(` DELETE /api/process/kill-all - Kill all processes`);
3194
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
+ );
3195
601
  console.log(` GET /api/ping - Health check`);
3196
- console.log(` GET /api/commands - List available commands`);