@cloudflare/sandbox 0.2.4 → 0.3.1

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 +75 -0
  2. package/Dockerfile +9 -11
  3. package/README.md +69 -7
  4. package/container_src/control-process.ts +784 -0
  5. package/container_src/handler/exec.ts +99 -254
  6. package/container_src/handler/file.ts +179 -837
  7. package/container_src/handler/git.ts +28 -80
  8. package/container_src/handler/process.ts +443 -515
  9. package/container_src/handler/session.ts +92 -0
  10. package/container_src/index.ts +68 -130
  11. package/container_src/isolation.ts +1038 -0
  12. package/container_src/shell-escape.ts +42 -0
  13. package/container_src/types.ts +27 -13
  14. package/dist/{chunk-HHUDRGPY.js → chunk-BEQUGUY4.js} +2 -2
  15. package/dist/{chunk-CKIGERRS.js → chunk-LFLJGISB.js} +240 -264
  16. package/dist/chunk-LFLJGISB.js.map +1 -0
  17. package/dist/{chunk-3CQ6THKA.js → chunk-SMUEY5JR.js} +85 -103
  18. package/dist/chunk-SMUEY5JR.js.map +1 -0
  19. package/dist/{client-Ce40ujDF.d.ts → client-Dny_ro_v.d.ts} +41 -25
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js +1 -1
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +8 -9
  24. package/dist/interpreter.d.ts +1 -1
  25. package/dist/jupyter-client.d.ts +1 -1
  26. package/dist/jupyter-client.js +2 -2
  27. package/dist/request-handler.d.ts +1 -1
  28. package/dist/request-handler.js +3 -5
  29. package/dist/sandbox.d.ts +1 -1
  30. package/dist/sandbox.js +3 -5
  31. package/dist/types.d.ts +10 -21
  32. package/dist/types.js +35 -9
  33. package/dist/types.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/client.ts +120 -135
  36. package/src/index.ts +8 -0
  37. package/src/sandbox.ts +290 -331
  38. package/src/types.ts +15 -24
  39. package/dist/chunk-3CQ6THKA.js.map +0 -1
  40. package/dist/chunk-6EWSYSO7.js +0 -46
  41. package/dist/chunk-6EWSYSO7.js.map +0 -1
  42. package/dist/chunk-CKIGERRS.js.map +0 -1
  43. /package/dist/{chunk-HHUDRGPY.js.map → chunk-BEQUGUY4.js.map} +0 -0
@@ -1,6 +1,4 @@
1
- import { spawn } from "node:child_process";
2
- import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
1
+ import type { SessionManager } from "../isolation";
4
2
  import type {
5
3
  DeleteFileRequest,
6
4
  ListFilesRequest,
@@ -8,154 +6,111 @@ import type {
8
6
  MoveFileRequest,
9
7
  ReadFileRequest,
10
8
  RenameFileRequest,
11
- SessionData,
12
9
  WriteFileRequest
13
10
  } from "../types";
14
11
 
15
- function executeMkdir(
16
- sessions: Map<string, SessionData>,
17
- path: string,
18
- recursive: boolean,
19
- sessionId?: string
20
- ): Promise<{
21
- success: boolean;
22
- stdout: string;
23
- stderr: string;
24
- exitCode: number;
25
- }> {
26
- return new Promise((resolve, reject) => {
27
- const args = `${recursive ? "-p " : ""} ${path}`;
28
- const mkdirChild = spawn(`mkdir ${args}`, {
29
- shell: true,
30
- stdio: ["pipe", "pipe", "pipe"],
31
- });
32
-
33
- // Store the process reference for cleanup if sessionId is provided
34
- if (sessionId && sessions.has(sessionId)) {
35
- const session = sessions.get(sessionId)!;
36
- session.activeProcess = mkdirChild;
12
+ // Common path validation patterns
13
+ const DANGEROUS_PATH_PATTERNS = [
14
+ /^\/$/, // Root directory
15
+ /^\/etc/, // System directories
16
+ /^\/var/, // System directories
17
+ /^\/usr/, // System directories
18
+ /^\/bin/, // System directories
19
+ /^\/sbin/, // System directories
20
+ /^\/boot/, // System directories
21
+ /^\/dev/, // System directories
22
+ /^\/proc/, // System directories
23
+ /^\/sys/, // System directories
24
+ /^\/tmp\/\.\./, // Path traversal attempts
25
+ /\.\./, // Path traversal attempts
26
+ ];
27
+
28
+ // Path validation utility
29
+ function validatePath(...paths: string[]): string | null {
30
+ for (const path of paths) {
31
+ if (!path || typeof path !== "string") {
32
+ return "Path is required and must be a string";
37
33
  }
34
+
35
+ if (DANGEROUS_PATH_PATTERNS.some((pattern) => pattern.test(path))) {
36
+ return "Dangerous path not allowed";
37
+ }
38
+ }
39
+ return null;
40
+ }
38
41
 
39
- let stdout = "";
40
- let stderr = "";
41
-
42
- mkdirChild.stdout?.on("data", (data) => {
43
- stdout += data.toString();
44
- });
45
-
46
- mkdirChild.stderr?.on("data", (data) => {
47
- stderr += data.toString();
48
- });
49
-
50
- mkdirChild.on("close", (code) => {
51
- // Clear the active process reference
52
- if (sessionId && sessions.has(sessionId)) {
53
- const session = sessions.get(sessionId)!;
54
- session.activeProcess = null;
55
- }
56
-
57
- if (code === 0) {
58
- console.log(`[Server] Directory created successfully: ${path}`);
59
- resolve({
60
- exitCode: code || 0,
61
- stderr,
62
- stdout,
63
- success: true,
64
- });
65
- } else {
66
- console.error(
67
- `[Server] Failed to create directory: ${path}, Exit code: ${code}`
68
- );
69
- resolve({
70
- exitCode: code || 1,
71
- stderr,
72
- stdout,
73
- success: false,
74
- });
75
- }
76
- });
77
-
78
- mkdirChild.on("error", (error) => {
79
- // Clear the active process reference
80
- if (sessionId && sessions.has(sessionId)) {
81
- const session = sessions.get(sessionId)!;
82
- session.activeProcess = null;
83
- }
42
+ // Common error response utility
43
+ function createPathErrorResponse(
44
+ error: string,
45
+ corsHeaders: Record<string, string>
46
+ ): Response {
47
+ return new Response(
48
+ JSON.stringify({ error }),
49
+ {
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ ...corsHeaders,
53
+ },
54
+ status: 400,
55
+ }
56
+ );
57
+ }
84
58
 
85
- console.error(`[Server] Error creating directory: ${path}`, error);
86
- reject(error);
87
- });
88
- });
59
+ // Common server error response utility
60
+ function createServerErrorResponse(
61
+ operation: string,
62
+ error: unknown,
63
+ corsHeaders: Record<string, string>
64
+ ): Response {
65
+ console.error(`[Server] Error in ${operation}:`, error);
66
+ return new Response(
67
+ JSON.stringify({
68
+ error: `Failed to ${operation.replace('handle', '').replace('Request', '').toLowerCase().replace('file', ' file')}`,
69
+ message: error instanceof Error ? error.message : "Unknown error",
70
+ }),
71
+ {
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ ...corsHeaders,
75
+ },
76
+ status: 500,
77
+ }
78
+ );
89
79
  }
90
80
 
91
81
  export async function handleMkdirRequest(
92
- sessions: Map<string, SessionData>,
93
82
  req: Request,
94
- corsHeaders: Record<string, string>
83
+ corsHeaders: Record<string, string>,
84
+ sessionManager: SessionManager
95
85
  ): Promise<Response> {
96
86
  try {
97
87
  const body = (await req.json()) as MkdirRequest;
98
88
  const { path, recursive = false, sessionId } = body;
99
89
 
100
- if (!path || typeof path !== "string") {
101
- return new Response(
102
- JSON.stringify({
103
- error: "Path is required and must be a string",
104
- }),
105
- {
106
- headers: {
107
- "Content-Type": "application/json",
108
- ...corsHeaders,
109
- },
110
- status: 400,
111
- }
112
- );
90
+ // Validate path
91
+ const pathError = validatePath(path);
92
+ if (pathError) {
93
+ return createPathErrorResponse(pathError, corsHeaders);
113
94
  }
114
95
 
115
- // Basic safety check - prevent dangerous paths
116
- const dangerousPatterns = [
117
- /^\/$/, // Root directory
118
- /^\/etc/, // System directories
119
- /^\/var/, // System directories
120
- /^\/usr/, // System directories
121
- /^\/bin/, // System directories
122
- /^\/sbin/, // System directories
123
- /^\/boot/, // System directories
124
- /^\/dev/, // System directories
125
- /^\/proc/, // System directories
126
- /^\/sys/, // System directories
127
- /^\/tmp\/\.\./, // Path traversal attempts
128
- /\.\./, // Path traversal attempts
129
- ];
96
+ console.log(`[Server] Creating directory: ${path} (recursive: ${recursive})${sessionId ? ` in session: ${sessionId}` : ''}`);
130
97
 
131
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
132
- return new Response(
133
- JSON.stringify({
134
- error: "Dangerous path not allowed",
135
- }),
136
- {
137
- headers: {
138
- "Content-Type": "application/json",
139
- ...corsHeaders,
140
- },
141
- status: 400,
142
- }
143
- );
98
+ // Use specific session if provided, otherwise default session
99
+ const result = sessionId
100
+ ? await sessionManager.getSession(sessionId)?.mkdirOperation(path, recursive)
101
+ : await sessionManager.mkdir(path, recursive);
102
+
103
+ if (!result) {
104
+ return createServerErrorResponse("handleMkdirRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
144
105
  }
145
106
 
146
- console.log(
147
- `[Server] Creating directory: ${path} (recursive: ${recursive})`
148
- );
149
-
150
- const result = await executeMkdir(sessions, path, recursive, sessionId);
151
-
152
107
  return new Response(
153
108
  JSON.stringify({
154
109
  exitCode: result.exitCode,
155
110
  path,
156
111
  recursive,
157
- stderr: result.stderr,
158
- stdout: result.stdout,
112
+ stderr: "",
113
+ stdout: "",
159
114
  success: result.success,
160
115
  timestamp: new Date().toISOString(),
161
116
  }),
@@ -167,120 +122,36 @@ export async function handleMkdirRequest(
167
122
  }
168
123
  );
169
124
  } catch (error) {
170
- console.error("[Server] Error in handleMkdirRequest:", error);
171
- return new Response(
172
- JSON.stringify({
173
- error: "Failed to create directory",
174
- message: error instanceof Error ? error.message : "Unknown error",
175
- }),
176
- {
177
- headers: {
178
- "Content-Type": "application/json",
179
- ...corsHeaders,
180
- },
181
- status: 500,
182
- }
183
- );
125
+ return createServerErrorResponse("handleMkdirRequest", error, corsHeaders);
184
126
  }
185
127
  }
186
128
 
187
-
188
- function executeWriteFile(
189
- path: string,
190
- content: string,
191
- encoding: string,
192
- sessionId?: string
193
- ): Promise<{
194
- success: boolean;
195
- exitCode: number;
196
- }> {
197
- return new Promise((resolve, reject) => {
198
- (async () => {
199
- try {
200
- // Ensure the directory exists
201
- const dir = dirname(path);
202
- if (dir !== ".") {
203
- await mkdir(dir, { recursive: true });
204
- }
205
-
206
- // Write the file
207
- await writeFile(path, content, {
208
- encoding: encoding as BufferEncoding,
209
- });
210
-
211
- console.log(`[Server] File written successfully: ${path}`);
212
- resolve({
213
- exitCode: 0,
214
- success: true,
215
- });
216
- } catch (error) {
217
- console.error(`[Server] Error writing file: ${path}`, error);
218
- reject(error);
219
- }
220
- })();
221
- });
222
- }
223
-
224
129
  export async function handleWriteFileRequest(
225
130
  req: Request,
226
- corsHeaders: Record<string, string>
131
+ corsHeaders: Record<string, string>,
132
+ sessionManager: SessionManager
227
133
  ): Promise<Response> {
228
134
  try {
229
135
  const body = (await req.json()) as WriteFileRequest;
230
136
  const { path, content, encoding = "utf-8", sessionId } = body;
231
137
 
232
- if (!path || typeof path !== "string") {
233
- return new Response(
234
- JSON.stringify({
235
- error: "Path is required and must be a string",
236
- }),
237
- {
238
- headers: {
239
- "Content-Type": "application/json",
240
- ...corsHeaders,
241
- },
242
- status: 400,
243
- }
244
- );
138
+ // Validate path
139
+ const pathError = validatePath(path);
140
+ if (pathError) {
141
+ return createPathErrorResponse(pathError, corsHeaders);
245
142
  }
246
143
 
247
- // Basic safety check - prevent dangerous paths
248
- const dangerousPatterns = [
249
- /^\/$/, // Root directory
250
- /^\/etc/, // System directories
251
- /^\/var/, // System directories
252
- /^\/usr/, // System directories
253
- /^\/bin/, // System directories
254
- /^\/sbin/, // System directories
255
- /^\/boot/, // System directories
256
- /^\/dev/, // System directories
257
- /^\/proc/, // System directories
258
- /^\/sys/, // System directories
259
- /^\/tmp\/\.\./, // Path traversal attempts
260
- /\.\./, // Path traversal attempts
261
- ];
144
+ console.log(`[Server] Writing file: ${path} (content length: ${content.length})${sessionId ? ` in session: ${sessionId}` : ''}`);
262
145
 
263
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
264
- return new Response(
265
- JSON.stringify({
266
- error: "Dangerous path not allowed",
267
- }),
268
- {
269
- headers: {
270
- "Content-Type": "application/json",
271
- ...corsHeaders,
272
- },
273
- status: 400,
274
- }
275
- );
146
+ // Use specific session if provided, otherwise default session
147
+ const result = sessionId
148
+ ? await sessionManager.getSession(sessionId)?.writeFileOperation(path, content, encoding)
149
+ : await sessionManager.writeFile(path, content, encoding);
150
+
151
+ if (!result) {
152
+ return createServerErrorResponse("handleWriteFileRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
276
153
  }
277
154
 
278
- console.log(
279
- `[Server] Writing file: ${path} (content length: ${content.length})`
280
- );
281
-
282
- const result = await executeWriteFile(path, content, encoding, sessionId);
283
-
284
155
  return new Response(
285
156
  JSON.stringify({
286
157
  exitCode: result.exitCode,
@@ -296,113 +167,36 @@ export async function handleWriteFileRequest(
296
167
  }
297
168
  );
298
169
  } catch (error) {
299
- console.error("[Server] Error in handleWriteFileRequest:", error);
300
- return new Response(
301
- JSON.stringify({
302
- error: "Failed to write file",
303
- message: error instanceof Error ? error.message : "Unknown error",
304
- }),
305
- {
306
- headers: {
307
- "Content-Type": "application/json",
308
- ...corsHeaders,
309
- },
310
- status: 500,
311
- }
312
- );
170
+ return createServerErrorResponse("handleWriteFileRequest", error, corsHeaders);
313
171
  }
314
172
  }
315
173
 
316
-
317
- function executeReadFile(
318
- path: string,
319
- encoding: string,
320
- sessionId?: string
321
- ): Promise<{
322
- success: boolean;
323
- exitCode: number;
324
- content: string;
325
- }> {
326
- return new Promise((resolve, reject) => {
327
- (async () => {
328
- try {
329
- // Read the file
330
- const content = await readFile(path, {
331
- encoding: encoding as BufferEncoding,
332
- });
333
-
334
- console.log(`[Server] File read successfully: ${path}`);
335
- resolve({
336
- content,
337
- exitCode: 0,
338
- success: true,
339
- });
340
- } catch (error) {
341
- console.error(`[Server] Error reading file: ${path}`, error);
342
- reject(error);
343
- }
344
- })();
345
- });
346
- }
347
-
348
174
  export async function handleReadFileRequest(
349
175
  req: Request,
350
- corsHeaders: Record<string, string>
176
+ corsHeaders: Record<string, string>,
177
+ sessionManager: SessionManager
351
178
  ): Promise<Response> {
352
179
  try {
353
180
  const body = (await req.json()) as ReadFileRequest;
354
181
  const { path, encoding = "utf-8", sessionId } = body;
355
182
 
356
- if (!path || typeof path !== "string") {
357
- return new Response(
358
- JSON.stringify({
359
- error: "Path is required and must be a string",
360
- }),
361
- {
362
- headers: {
363
- "Content-Type": "application/json",
364
- ...corsHeaders,
365
- },
366
- status: 400,
367
- }
368
- );
183
+ // Validate path
184
+ const pathError = validatePath(path);
185
+ if (pathError) {
186
+ return createPathErrorResponse(pathError, corsHeaders);
369
187
  }
370
188
 
371
- // Basic safety check - prevent dangerous paths
372
- const dangerousPatterns = [
373
- /^\/$/, // Root directory
374
- /^\/etc/, // System directories
375
- /^\/var/, // System directories
376
- /^\/usr/, // System directories
377
- /^\/bin/, // System directories
378
- /^\/sbin/, // System directories
379
- /^\/boot/, // System directories
380
- /^\/dev/, // System directories
381
- /^\/proc/, // System directories
382
- /^\/sys/, // System directories
383
- /^\/tmp\/\.\./, // Path traversal attempts
384
- /\.\./, // Path traversal attempts
385
- ];
189
+ console.log(`[Server] Reading file: ${path}${sessionId ? ` in session: ${sessionId}` : ''}`);
386
190
 
387
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
388
- return new Response(
389
- JSON.stringify({
390
- error: "Dangerous path not allowed",
391
- }),
392
- {
393
- headers: {
394
- "Content-Type": "application/json",
395
- ...corsHeaders,
396
- },
397
- status: 400,
398
- }
399
- );
191
+ // Use specific session if provided, otherwise default session
192
+ const result = sessionId
193
+ ? await sessionManager.getSession(sessionId)?.readFileOperation(path, encoding)
194
+ : await sessionManager.readFile(path, encoding);
195
+
196
+ if (!result) {
197
+ return createServerErrorResponse("handleReadFileRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
400
198
  }
401
199
 
402
- console.log(`[Server] Reading file: ${path}`);
403
-
404
- const result = await executeReadFile(path, encoding, sessionId);
405
-
406
200
  return new Response(
407
201
  JSON.stringify({
408
202
  content: result.content,
@@ -419,108 +213,36 @@ export async function handleReadFileRequest(
419
213
  }
420
214
  );
421
215
  } catch (error) {
422
- console.error("[Server] Error in handleReadFileRequest:", error);
423
- return new Response(
424
- JSON.stringify({
425
- error: "Failed to read file",
426
- message: error instanceof Error ? error.message : "Unknown error",
427
- }),
428
- {
429
- headers: {
430
- "Content-Type": "application/json",
431
- ...corsHeaders,
432
- },
433
- status: 500,
434
- }
435
- );
216
+ return createServerErrorResponse("handleReadFileRequest", error, corsHeaders);
436
217
  }
437
218
  }
438
219
 
439
-
440
- function executeDeleteFile(
441
- path: string,
442
- sessionId?: string
443
- ): Promise<{
444
- success: boolean;
445
- exitCode: number;
446
- }> {
447
- return new Promise((resolve, reject) => {
448
- (async () => {
449
- try {
450
- // Delete the file
451
- await unlink(path);
452
-
453
- console.log(`[Server] File deleted successfully: ${path}`);
454
- resolve({
455
- exitCode: 0,
456
- success: true,
457
- });
458
- } catch (error) {
459
- console.error(`[Server] Error deleting file: ${path}`, error);
460
- reject(error);
461
- }
462
- })();
463
- });
464
- }
465
-
466
220
  export async function handleDeleteFileRequest(
467
221
  req: Request,
468
- corsHeaders: Record<string, string>
222
+ corsHeaders: Record<string, string>,
223
+ sessionManager: SessionManager
469
224
  ): Promise<Response> {
470
225
  try {
471
226
  const body = (await req.json()) as DeleteFileRequest;
472
227
  const { path, sessionId } = body;
473
228
 
474
- if (!path || typeof path !== "string") {
475
- return new Response(
476
- JSON.stringify({
477
- error: "Path is required and must be a string",
478
- }),
479
- {
480
- headers: {
481
- "Content-Type": "application/json",
482
- ...corsHeaders,
483
- },
484
- status: 400,
485
- }
486
- );
229
+ // Validate path
230
+ const pathError = validatePath(path);
231
+ if (pathError) {
232
+ return createPathErrorResponse(pathError, corsHeaders);
487
233
  }
488
234
 
489
- // Basic safety check - prevent dangerous paths
490
- const dangerousPatterns = [
491
- /^\/$/, // Root directory
492
- /^\/etc/, // System directories
493
- /^\/var/, // System directories
494
- /^\/usr/, // System directories
495
- /^\/bin/, // System directories
496
- /^\/sbin/, // System directories
497
- /^\/boot/, // System directories
498
- /^\/dev/, // System directories
499
- /^\/proc/, // System directories
500
- /^\/sys/, // System directories
501
- /^\/tmp\/\.\./, // Path traversal attempts
502
- /\.\./, // Path traversal attempts
503
- ];
235
+ console.log(`[Server] Deleting file: ${path}${sessionId ? ` in session: ${sessionId}` : ''}`);
504
236
 
505
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
506
- return new Response(
507
- JSON.stringify({
508
- error: "Dangerous path not allowed",
509
- }),
510
- {
511
- headers: {
512
- "Content-Type": "application/json",
513
- ...corsHeaders,
514
- },
515
- status: 400,
516
- }
517
- );
237
+ // Use specific session if provided, otherwise default session
238
+ const result = sessionId
239
+ ? await sessionManager.getSession(sessionId)?.deleteFileOperation(path)
240
+ : await sessionManager.deleteFile(path);
241
+
242
+ if (!result) {
243
+ return createServerErrorResponse("handleDeleteFileRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
518
244
  }
519
245
 
520
- console.log(`[Server] Deleting file: ${path}`);
521
-
522
- const result = await executeDeleteFile(path, sessionId);
523
-
524
246
  return new Response(
525
247
  JSON.stringify({
526
248
  exitCode: result.exitCode,
@@ -536,133 +258,36 @@ export async function handleDeleteFileRequest(
536
258
  }
537
259
  );
538
260
  } catch (error) {
539
- console.error("[Server] Error in handleDeleteFileRequest:", error);
540
- return new Response(
541
- JSON.stringify({
542
- error: "Failed to delete file",
543
- message: error instanceof Error ? error.message : "Unknown error",
544
- }),
545
- {
546
- headers: {
547
- "Content-Type": "application/json",
548
- ...corsHeaders,
549
- },
550
- status: 500,
551
- }
552
- );
261
+ return createServerErrorResponse("handleDeleteFileRequest", error, corsHeaders);
553
262
  }
554
263
  }
555
264
 
556
-
557
- function executeRenameFile(
558
- oldPath: string,
559
- newPath: string,
560
- sessionId?: string
561
- ): Promise<{
562
- success: boolean;
563
- exitCode: number;
564
- }> {
565
- return new Promise((resolve, reject) => {
566
- (async () => {
567
- try {
568
- // Rename the file
569
- await rename(oldPath, newPath);
570
-
571
- console.log(
572
- `[Server] File renamed successfully: ${oldPath} -> ${newPath}`
573
- );
574
- resolve({
575
- exitCode: 0,
576
- success: true,
577
- });
578
- } catch (error) {
579
- console.error(
580
- `[Server] Error renaming file: ${oldPath} -> ${newPath}`,
581
- error
582
- );
583
- reject(error);
584
- }
585
- })();
586
- });
587
- }
588
-
589
265
  export async function handleRenameFileRequest(
590
266
  req: Request,
591
- corsHeaders: Record<string, string>
267
+ corsHeaders: Record<string, string>,
268
+ sessionManager: SessionManager
592
269
  ): Promise<Response> {
593
270
  try {
594
271
  const body = (await req.json()) as RenameFileRequest;
595
272
  const { oldPath, newPath, sessionId } = body;
596
273
 
597
- if (!oldPath || typeof oldPath !== "string") {
598
- return new Response(
599
- JSON.stringify({
600
- error: "Old path is required and must be a string",
601
- }),
602
- {
603
- headers: {
604
- "Content-Type": "application/json",
605
- ...corsHeaders,
606
- },
607
- status: 400,
608
- }
609
- );
274
+ // Validate paths
275
+ const pathError = validatePath(oldPath, newPath);
276
+ if (pathError) {
277
+ return createPathErrorResponse(pathError, corsHeaders);
610
278
  }
611
279
 
612
- if (!newPath || typeof newPath !== "string") {
613
- return new Response(
614
- JSON.stringify({
615
- error: "New path is required and must be a string",
616
- }),
617
- {
618
- headers: {
619
- "Content-Type": "application/json",
620
- ...corsHeaders,
621
- },
622
- status: 400,
623
- }
624
- );
625
- }
626
-
627
- // Basic safety check - prevent dangerous paths
628
- const dangerousPatterns = [
629
- /^\/$/, // Root directory
630
- /^\/etc/, // System directories
631
- /^\/var/, // System directories
632
- /^\/usr/, // System directories
633
- /^\/bin/, // System directories
634
- /^\/sbin/, // System directories
635
- /^\/boot/, // System directories
636
- /^\/dev/, // System directories
637
- /^\/proc/, // System directories
638
- /^\/sys/, // System directories
639
- /^\/tmp\/\.\./, // Path traversal attempts
640
- /\.\./, // Path traversal attempts
641
- ];
280
+ console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}${sessionId ? ` in session: ${sessionId}` : ''}`);
642
281
 
643
- if (
644
- dangerousPatterns.some(
645
- (pattern) => pattern.test(oldPath) || pattern.test(newPath)
646
- )
647
- ) {
648
- return new Response(
649
- JSON.stringify({
650
- error: "Dangerous path not allowed",
651
- }),
652
- {
653
- headers: {
654
- "Content-Type": "application/json",
655
- ...corsHeaders,
656
- },
657
- status: 400,
658
- }
659
- );
282
+ // Use specific session if provided, otherwise default session
283
+ const result = sessionId
284
+ ? await sessionManager.getSession(sessionId)?.renameFileOperation(oldPath, newPath)
285
+ : await sessionManager.renameFile(oldPath, newPath);
286
+
287
+ if (!result) {
288
+ return createServerErrorResponse("handleRenameFileRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
660
289
  }
661
290
 
662
- console.log(`[Server] Renaming file: ${oldPath} -> ${newPath}`);
663
-
664
- const result = await executeRenameFile(oldPath, newPath, sessionId);
665
-
666
291
  return new Response(
667
292
  JSON.stringify({
668
293
  exitCode: result.exitCode,
@@ -679,137 +304,36 @@ export async function handleRenameFileRequest(
679
304
  }
680
305
  );
681
306
  } catch (error) {
682
- console.error("[Server] Error in handleRenameFileRequest:", error);
683
- return new Response(
684
- JSON.stringify({
685
- error: "Failed to rename file",
686
- message: error instanceof Error ? error.message : "Unknown error",
687
- }),
688
- {
689
- headers: {
690
- "Content-Type": "application/json",
691
- ...corsHeaders,
692
- },
693
- status: 500,
694
- }
695
- );
307
+ return createServerErrorResponse("handleRenameFileRequest", error, corsHeaders);
696
308
  }
697
309
  }
698
310
 
699
-
700
- function executeMoveFile(
701
- sourcePath: string,
702
- destinationPath: string,
703
- sessionId?: string
704
- ): Promise<{
705
- success: boolean;
706
- exitCode: number;
707
- }> {
708
- return new Promise((resolve, reject) => {
709
- (async () => {
710
- try {
711
- // Move the file
712
- await rename(sourcePath, destinationPath);
713
-
714
- console.log(
715
- `[Server] File moved successfully: ${sourcePath} -> ${destinationPath}`
716
- );
717
- resolve({
718
- exitCode: 0,
719
- success: true,
720
- });
721
- } catch (error) {
722
- console.error(
723
- `[Server] Error moving file: ${sourcePath} -> ${destinationPath}`,
724
- error
725
- );
726
- reject(error);
727
- }
728
- })();
729
- });
730
- }
731
-
732
311
  export async function handleMoveFileRequest(
733
312
  req: Request,
734
- corsHeaders: Record<string, string>
313
+ corsHeaders: Record<string, string>,
314
+ sessionManager: SessionManager
735
315
  ): Promise<Response> {
736
316
  try {
737
317
  const body = (await req.json()) as MoveFileRequest;
738
318
  const { sourcePath, destinationPath, sessionId } = body;
739
319
 
740
- if (!sourcePath || typeof sourcePath !== "string") {
741
- return new Response(
742
- JSON.stringify({
743
- error: "Source path is required and must be a string",
744
- }),
745
- {
746
- headers: {
747
- "Content-Type": "application/json",
748
- ...corsHeaders,
749
- },
750
- status: 400,
751
- }
752
- );
320
+ // Validate paths
321
+ const pathError = validatePath(sourcePath, destinationPath);
322
+ if (pathError) {
323
+ return createPathErrorResponse(pathError, corsHeaders);
753
324
  }
754
325
 
755
- if (!destinationPath || typeof destinationPath !== "string") {
756
- return new Response(
757
- JSON.stringify({
758
- error: "Destination path is required and must be a string",
759
- }),
760
- {
761
- headers: {
762
- "Content-Type": "application/json",
763
- ...corsHeaders,
764
- },
765
- status: 400,
766
- }
767
- );
768
- }
326
+ console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}${sessionId ? ` in session: ${sessionId}` : ''}`);
769
327
 
770
- // Basic safety check - prevent dangerous paths
771
- const dangerousPatterns = [
772
- /^\/$/, // Root directory
773
- /^\/etc/, // System directories
774
- /^\/var/, // System directories
775
- /^\/usr/, // System directories
776
- /^\/bin/, // System directories
777
- /^\/sbin/, // System directories
778
- /^\/boot/, // System directories
779
- /^\/dev/, // System directories
780
- /^\/proc/, // System directories
781
- /^\/sys/, // System directories
782
- /^\/tmp\/\.\./, // Path traversal attempts
783
- /\.\./, // Path traversal attempts
784
- ];
785
-
786
- if (
787
- dangerousPatterns.some(
788
- (pattern) => pattern.test(sourcePath) || pattern.test(destinationPath)
789
- )
790
- ) {
791
- return new Response(
792
- JSON.stringify({
793
- error: "Dangerous path not allowed",
794
- }),
795
- {
796
- headers: {
797
- "Content-Type": "application/json",
798
- ...corsHeaders,
799
- },
800
- status: 400,
801
- }
802
- );
328
+ // Use specific session if provided, otherwise default session
329
+ const result = sessionId
330
+ ? await sessionManager.getSession(sessionId)?.moveFileOperation(sourcePath, destinationPath)
331
+ : await sessionManager.moveFile(sourcePath, destinationPath);
332
+
333
+ if (!result) {
334
+ return createServerErrorResponse("handleMoveFileRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
803
335
  }
804
336
 
805
- console.log(`[Server] Moving file: ${sourcePath} -> ${destinationPath}`);
806
-
807
- const result = await executeMoveFile(
808
- sourcePath,
809
- destinationPath,
810
- sessionId
811
- );
812
-
813
337
  return new Response(
814
338
  JSON.stringify({
815
339
  destinationPath,
@@ -826,208 +350,40 @@ export async function handleMoveFileRequest(
826
350
  }
827
351
  );
828
352
  } catch (error) {
829
- console.error("[Server] Error in handleMoveFileRequest:", error);
830
- return new Response(
831
- JSON.stringify({
832
- error: "Failed to move file",
833
- message: error instanceof Error ? error.message : "Unknown error",
834
- }),
835
- {
836
- headers: {
837
- "Content-Type": "application/json",
838
- ...corsHeaders,
839
- },
840
- status: 500,
841
- }
842
- );
843
- }
844
- }
845
-
846
-
847
- async function executeListFiles(
848
- path: string,
849
- options?: {
850
- recursive?: boolean;
851
- includeHidden?: boolean;
852
- }
853
- ): Promise<{
854
- success: boolean;
855
- exitCode: number;
856
- files: Array<{
857
- name: string;
858
- absolutePath: string;
859
- relativePath: string;
860
- type: 'file' | 'directory' | 'symlink' | 'other';
861
- size: number;
862
- modifiedAt: string;
863
- mode: string;
864
- permissions: {
865
- readable: boolean;
866
- writable: boolean;
867
- executable: boolean;
868
- };
869
- }>;
870
- }> {
871
- try {
872
- const basePath = path.endsWith('/') ? path.slice(0, -1) : path;
873
- const files: Array<{
874
- name: string;
875
- absolutePath: string;
876
- relativePath: string;
877
- type: 'file' | 'directory' | 'symlink' | 'other';
878
- size: number;
879
- modifiedAt: string;
880
- mode: string;
881
- permissions: {
882
- readable: boolean;
883
- writable: boolean;
884
- executable: boolean;
885
- };
886
- }> = [];
887
-
888
- // Helper function to convert numeric mode to string like "rwxr-xr-x"
889
- function modeToString(mode: number): string {
890
- const perms = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'];
891
- const user = (mode >> 6) & 7;
892
- const group = (mode >> 3) & 7;
893
- const other = mode & 7;
894
- return perms[user] + perms[group] + perms[other];
895
- }
896
-
897
- // Helper function to extract permission booleans for current user
898
- function getPermissions(mode: number): { readable: boolean; writable: boolean; executable: boolean } {
899
- // Extract user permissions (owner permissions)
900
- const userPerms = (mode >> 6) & 7;
901
- return {
902
- readable: (userPerms & 4) !== 0,
903
- writable: (userPerms & 2) !== 0,
904
- executable: (userPerms & 1) !== 0
905
- };
906
- }
907
-
908
- async function scanDirectory(dirPath: string): Promise<void> {
909
- const entries = await readdir(dirPath);
910
-
911
- for (const entry of entries) {
912
- // Skip hidden files unless includeHidden is true
913
- if (!options?.includeHidden && entry.startsWith('.')) {
914
- continue;
915
- }
916
-
917
- const fullPath = join(dirPath, entry);
918
- const stats = await stat(fullPath);
919
-
920
- let type: 'file' | 'directory' | 'symlink' | 'other';
921
- if (stats.isDirectory()) {
922
- type = 'directory';
923
- } else if (stats.isFile()) {
924
- type = 'file';
925
- } else if (stats.isSymbolicLink()) {
926
- type = 'symlink';
927
- } else {
928
- type = 'other';
929
- }
930
-
931
- // Extract mode (permissions) - stats.mode is a number
932
- const mode = stats.mode & 0o777; // Get only permission bits
933
-
934
- // Calculate relative path from base directory
935
- const relativePath = fullPath.startsWith(`${basePath}/`)
936
- ? fullPath.substring(basePath.length + 1)
937
- : fullPath === basePath
938
- ? '.'
939
- : entry;
940
-
941
- const fileInfo = {
942
- name: entry,
943
- absolutePath: fullPath,
944
- relativePath,
945
- type,
946
- size: stats.size,
947
- modifiedAt: stats.mtime.toISOString(),
948
- mode: modeToString(mode),
949
- permissions: getPermissions(mode)
950
- };
951
-
952
- files.push(fileInfo);
953
-
954
- // Recursively scan subdirectories if requested
955
- if (options?.recursive && type === 'directory') {
956
- await scanDirectory(fullPath);
957
- }
958
- }
959
- }
960
-
961
- await scanDirectory(path);
962
-
963
- console.log(`[Server] Listed ${files.length} files in: ${path}`);
964
- return {
965
- exitCode: 0,
966
- files,
967
- success: true,
968
- };
969
- } catch (error) {
970
- console.error(`[Server] Error listing files in: ${path}`, error);
971
- throw error;
353
+ return createServerErrorResponse("handleMoveFileRequest", error, corsHeaders);
972
354
  }
973
355
  }
974
356
 
975
357
  export async function handleListFilesRequest(
976
358
  req: Request,
977
- corsHeaders: Record<string, string>
359
+ corsHeaders: Record<string, string>,
360
+ sessionManager: SessionManager
978
361
  ): Promise<Response> {
979
362
  try {
980
363
  const body = (await req.json()) as ListFilesRequest;
981
- const { path, options } = body;
364
+ const { path, options, sessionId } = body;
982
365
 
983
- if (!path || typeof path !== "string") {
984
- return new Response(
985
- JSON.stringify({
986
- error: "Path is required and must be a string",
987
- }),
988
- {
989
- headers: {
990
- "Content-Type": "application/json",
991
- ...corsHeaders,
992
- },
993
- status: 400,
994
- }
995
- );
366
+ // Validate path (note: listFiles allows root directory listing)
367
+ const pathError = validatePath(path);
368
+ if (pathError && pathError !== "Dangerous path not allowed") {
369
+ return createPathErrorResponse(pathError, corsHeaders);
996
370
  }
997
-
998
- // Basic safety check - prevent dangerous paths
999
- const dangerousPatterns = [
1000
- /^\/etc/, // System directories
1001
- /^\/var/, // System directories
1002
- /^\/usr/, // System directories
1003
- /^\/bin/, // System directories
1004
- /^\/sbin/, // System directories
1005
- /^\/boot/, // System directories
1006
- /^\/dev/, // System directories
1007
- /^\/proc/, // System directories
1008
- /^\/sys/, // System directories
1009
- /^\/tmp\/\.\./, // Path traversal attempts
1010
- /\.\./, // Path traversal attempts
1011
- ];
1012
-
1013
- if (dangerousPatterns.some((pattern) => pattern.test(path))) {
1014
- return new Response(
1015
- JSON.stringify({
1016
- error: "Dangerous path not allowed",
1017
- }),
1018
- {
1019
- headers: {
1020
- "Content-Type": "application/json",
1021
- ...corsHeaders,
1022
- },
1023
- status: 400,
1024
- }
1025
- );
371
+
372
+ // For listFiles, we allow root directory but still check other dangerous patterns
373
+ if (path !== "/" && DANGEROUS_PATH_PATTERNS.slice(1).some((pattern) => pattern.test(path))) {
374
+ return createPathErrorResponse("Dangerous path not allowed", corsHeaders);
1026
375
  }
1027
376
 
1028
- console.log(`[Server] Listing files in: ${path}`);
377
+ console.log(`[Server] Listing files in: ${path}${sessionId ? ` in session: ${sessionId}` : ''}`);
1029
378
 
1030
- const result = await executeListFiles(path, options);
379
+ // Use specific session if provided, otherwise default session
380
+ const result = sessionId
381
+ ? await sessionManager.getSession(sessionId)?.listFilesOperation(path, options)
382
+ : await sessionManager.listFiles(path, options);
383
+
384
+ if (!result) {
385
+ return createServerErrorResponse("handleListFilesRequest", new Error(`Session '${sessionId}' not found`), corsHeaders);
386
+ }
1031
387
 
1032
388
  return new Response(
1033
389
  JSON.stringify({
@@ -1045,20 +401,6 @@ export async function handleListFilesRequest(
1045
401
  }
1046
402
  );
1047
403
  } catch (error) {
1048
- console.error("[Server] Error in handleListFilesRequest:", error);
1049
- return new Response(
1050
- JSON.stringify({
1051
- error: "Failed to list files",
1052
- message: error instanceof Error ? error.message : "Unknown error",
1053
- }),
1054
- {
1055
- headers: {
1056
- "Content-Type": "application/json",
1057
- ...corsHeaders,
1058
- },
1059
- status: 500,
1060
- }
1061
- );
404
+ return createServerErrorResponse("handleListFilesRequest", error, corsHeaders);
1062
405
  }
1063
- }
1064
-
406
+ }