@cloudflare/sandbox 0.0.0-db09b4d → 0.0.0-dcf36ef

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