@cloudflare/sandbox 0.0.0-46eb4e6 → 0.0.0-485cf61

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 (95) hide show
  1. package/CHANGELOG.md +0 -6
  2. package/Dockerfile +82 -18
  3. package/README.md +89 -824
  4. package/dist/chunk-3NEP4CNV.js +99 -0
  5. package/dist/chunk-3NEP4CNV.js.map +1 -0
  6. package/dist/chunk-6IYG2RIN.js +117 -0
  7. package/dist/chunk-6IYG2RIN.js.map +1 -0
  8. package/dist/chunk-HB44YO2A.js +2331 -0
  9. package/dist/chunk-HB44YO2A.js.map +1 -0
  10. package/dist/chunk-KPVMMMIP.js +105 -0
  11. package/dist/chunk-KPVMMMIP.js.map +1 -0
  12. package/dist/chunk-NNGBXDMY.js +89 -0
  13. package/dist/chunk-NNGBXDMY.js.map +1 -0
  14. package/dist/file-stream.d.ts +43 -0
  15. package/dist/file-stream.js +9 -0
  16. package/dist/file-stream.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +55 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/interpreter.d.ts +33 -0
  21. package/dist/interpreter.js +8 -0
  22. package/dist/interpreter.js.map +1 -0
  23. package/dist/request-handler.d.ts +18 -0
  24. package/dist/request-handler.js +12 -0
  25. package/dist/request-handler.js.map +1 -0
  26. package/dist/sandbox-CtlKjZwf.d.ts +583 -0
  27. package/dist/sandbox.d.ts +4 -0
  28. package/dist/sandbox.js +12 -0
  29. package/dist/sandbox.js.map +1 -0
  30. package/dist/security.d.ts +35 -0
  31. package/dist/security.js +15 -0
  32. package/dist/security.js.map +1 -0
  33. package/dist/sse-parser.d.ts +28 -0
  34. package/dist/sse-parser.js +11 -0
  35. package/dist/sse-parser.js.map +1 -0
  36. package/package.json +11 -5
  37. package/src/clients/base-client.ts +297 -0
  38. package/src/clients/command-client.ts +118 -0
  39. package/src/clients/file-client.ts +272 -0
  40. package/src/clients/git-client.ts +95 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/{interpreter-client.ts → clients/interpreter-client.ts} +151 -171
  43. package/src/clients/port-client.ts +108 -0
  44. package/src/clients/process-client.ts +180 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +81 -0
  47. package/src/clients/utility-client.ts +97 -0
  48. package/src/errors/adapter.ts +180 -0
  49. package/src/errors/classes.ts +469 -0
  50. package/src/errors/index.ts +105 -0
  51. package/src/file-stream.ts +119 -117
  52. package/src/index.ts +81 -69
  53. package/src/interpreter.ts +17 -8
  54. package/src/request-handler.ts +61 -7
  55. package/src/sandbox.ts +698 -495
  56. package/src/security.ts +20 -0
  57. package/startup.sh +7 -0
  58. package/tests/base-client.test.ts +328 -0
  59. package/tests/command-client.test.ts +407 -0
  60. package/tests/file-client.test.ts +643 -0
  61. package/tests/file-stream.test.ts +306 -0
  62. package/tests/git-client.test.ts +328 -0
  63. package/tests/port-client.test.ts +301 -0
  64. package/tests/process-client.test.ts +658 -0
  65. package/tests/sandbox.test.ts +465 -0
  66. package/tests/sse-parser.test.ts +291 -0
  67. package/tests/utility-client.test.ts +266 -0
  68. package/tests/wrangler.jsonc +35 -0
  69. package/tsconfig.json +9 -1
  70. package/vitest.config.ts +31 -0
  71. package/container_src/bun.lock +0 -76
  72. package/container_src/circuit-breaker.ts +0 -121
  73. package/container_src/control-process.ts +0 -784
  74. package/container_src/handler/exec.ts +0 -185
  75. package/container_src/handler/file.ts +0 -457
  76. package/container_src/handler/git.ts +0 -130
  77. package/container_src/handler/ports.ts +0 -314
  78. package/container_src/handler/process.ts +0 -568
  79. package/container_src/handler/session.ts +0 -92
  80. package/container_src/index.ts +0 -600
  81. package/container_src/interpreter-service.ts +0 -276
  82. package/container_src/isolation.ts +0 -1213
  83. package/container_src/mime-processor.ts +0 -255
  84. package/container_src/package.json +0 -18
  85. package/container_src/runtime/executors/javascript/node_executor.ts +0 -123
  86. package/container_src/runtime/executors/python/ipython_executor.py +0 -338
  87. package/container_src/runtime/executors/typescript/ts_executor.ts +0 -138
  88. package/container_src/runtime/process-pool.ts +0 -464
  89. package/container_src/shell-escape.ts +0 -42
  90. package/container_src/startup.sh +0 -11
  91. package/container_src/types.ts +0 -131
  92. package/src/client.ts +0 -1048
  93. package/src/errors.ts +0 -219
  94. package/src/interpreter-types.ts +0 -390
  95. package/src/types.ts +0 -571
@@ -0,0 +1,2331 @@
1
+ import {
2
+ CodeInterpreter
3
+ } from "./chunk-6IYG2RIN.js";
4
+ import {
5
+ SecurityError,
6
+ logSecurityEvent,
7
+ sanitizeSandboxId,
8
+ validatePort
9
+ } from "./chunk-3NEP4CNV.js";
10
+ import {
11
+ parseSSEStream
12
+ } from "./chunk-NNGBXDMY.js";
13
+
14
+ // src/sandbox.ts
15
+ import { Container, getContainer } from "@cloudflare/containers";
16
+
17
+ // src/errors/index.ts
18
+ import { ErrorCode as ErrorCode2, Operation } from "@repo/shared/errors";
19
+
20
+ // src/errors/adapter.ts
21
+ import { ErrorCode } from "@repo/shared/errors";
22
+
23
+ // src/errors/classes.ts
24
+ var SandboxError = class extends Error {
25
+ constructor(errorResponse) {
26
+ super(errorResponse.message);
27
+ this.errorResponse = errorResponse;
28
+ this.name = "SandboxError";
29
+ }
30
+ // Convenience accessors
31
+ get code() {
32
+ return this.errorResponse.code;
33
+ }
34
+ get context() {
35
+ return this.errorResponse.context;
36
+ }
37
+ get httpStatus() {
38
+ return this.errorResponse.httpStatus;
39
+ }
40
+ get operation() {
41
+ return this.errorResponse.operation;
42
+ }
43
+ get suggestion() {
44
+ return this.errorResponse.suggestion;
45
+ }
46
+ get timestamp() {
47
+ return this.errorResponse.timestamp;
48
+ }
49
+ get documentation() {
50
+ return this.errorResponse.documentation;
51
+ }
52
+ // Custom serialization for logging
53
+ toJSON() {
54
+ return {
55
+ name: this.name,
56
+ message: this.message,
57
+ code: this.code,
58
+ context: this.context,
59
+ httpStatus: this.httpStatus,
60
+ operation: this.operation,
61
+ suggestion: this.suggestion,
62
+ timestamp: this.timestamp,
63
+ documentation: this.documentation,
64
+ stack: this.stack
65
+ };
66
+ }
67
+ };
68
+ var FileNotFoundError = class extends SandboxError {
69
+ constructor(errorResponse) {
70
+ super(errorResponse);
71
+ this.name = "FileNotFoundError";
72
+ }
73
+ // Type-safe accessors
74
+ get path() {
75
+ return this.context.path;
76
+ }
77
+ };
78
+ var FileExistsError = class extends SandboxError {
79
+ constructor(errorResponse) {
80
+ super(errorResponse);
81
+ this.name = "FileExistsError";
82
+ }
83
+ // Type-safe accessor
84
+ get path() {
85
+ return this.context.path;
86
+ }
87
+ };
88
+ var FileSystemError = class extends SandboxError {
89
+ constructor(errorResponse) {
90
+ super(errorResponse);
91
+ this.name = "FileSystemError";
92
+ }
93
+ // Type-safe accessors
94
+ get path() {
95
+ return this.context.path;
96
+ }
97
+ get stderr() {
98
+ return this.context.stderr;
99
+ }
100
+ get exitCode() {
101
+ return this.context.exitCode;
102
+ }
103
+ };
104
+ var PermissionDeniedError = class extends SandboxError {
105
+ constructor(errorResponse) {
106
+ super(errorResponse);
107
+ this.name = "PermissionDeniedError";
108
+ }
109
+ get path() {
110
+ return this.context.path;
111
+ }
112
+ };
113
+ var CommandNotFoundError = class extends SandboxError {
114
+ constructor(errorResponse) {
115
+ super(errorResponse);
116
+ this.name = "CommandNotFoundError";
117
+ }
118
+ // Type-safe accessor
119
+ get command() {
120
+ return this.context.command;
121
+ }
122
+ };
123
+ var CommandError = class extends SandboxError {
124
+ constructor(errorResponse) {
125
+ super(errorResponse);
126
+ this.name = "CommandError";
127
+ }
128
+ // Type-safe accessors
129
+ get command() {
130
+ return this.context.command;
131
+ }
132
+ get exitCode() {
133
+ return this.context.exitCode;
134
+ }
135
+ get stdout() {
136
+ return this.context.stdout;
137
+ }
138
+ get stderr() {
139
+ return this.context.stderr;
140
+ }
141
+ };
142
+ var ProcessNotFoundError = class extends SandboxError {
143
+ constructor(errorResponse) {
144
+ super(errorResponse);
145
+ this.name = "ProcessNotFoundError";
146
+ }
147
+ // Type-safe accessor
148
+ get processId() {
149
+ return this.context.processId;
150
+ }
151
+ };
152
+ var ProcessError = class extends SandboxError {
153
+ constructor(errorResponse) {
154
+ super(errorResponse);
155
+ this.name = "ProcessError";
156
+ }
157
+ // Type-safe accessors
158
+ get processId() {
159
+ return this.context.processId;
160
+ }
161
+ get pid() {
162
+ return this.context.pid;
163
+ }
164
+ get exitCode() {
165
+ return this.context.exitCode;
166
+ }
167
+ get stderr() {
168
+ return this.context.stderr;
169
+ }
170
+ };
171
+ var PortAlreadyExposedError = class extends SandboxError {
172
+ constructor(errorResponse) {
173
+ super(errorResponse);
174
+ this.name = "PortAlreadyExposedError";
175
+ }
176
+ // Type-safe accessors
177
+ get port() {
178
+ return this.context.port;
179
+ }
180
+ get portName() {
181
+ return this.context.portName;
182
+ }
183
+ };
184
+ var PortNotExposedError = class extends SandboxError {
185
+ constructor(errorResponse) {
186
+ super(errorResponse);
187
+ this.name = "PortNotExposedError";
188
+ }
189
+ // Type-safe accessor
190
+ get port() {
191
+ return this.context.port;
192
+ }
193
+ };
194
+ var InvalidPortError = class extends SandboxError {
195
+ constructor(errorResponse) {
196
+ super(errorResponse);
197
+ this.name = "InvalidPortError";
198
+ }
199
+ // Type-safe accessors
200
+ get port() {
201
+ return this.context.port;
202
+ }
203
+ get reason() {
204
+ return this.context.reason;
205
+ }
206
+ };
207
+ var ServiceNotRespondingError = class extends SandboxError {
208
+ constructor(errorResponse) {
209
+ super(errorResponse);
210
+ this.name = "ServiceNotRespondingError";
211
+ }
212
+ // Type-safe accessors
213
+ get port() {
214
+ return this.context.port;
215
+ }
216
+ get portName() {
217
+ return this.context.portName;
218
+ }
219
+ };
220
+ var PortInUseError = class extends SandboxError {
221
+ constructor(errorResponse) {
222
+ super(errorResponse);
223
+ this.name = "PortInUseError";
224
+ }
225
+ // Type-safe accessor
226
+ get port() {
227
+ return this.context.port;
228
+ }
229
+ };
230
+ var PortError = class extends SandboxError {
231
+ constructor(errorResponse) {
232
+ super(errorResponse);
233
+ this.name = "PortError";
234
+ }
235
+ // Type-safe accessors
236
+ get port() {
237
+ return this.context.port;
238
+ }
239
+ get portName() {
240
+ return this.context.portName;
241
+ }
242
+ get stderr() {
243
+ return this.context.stderr;
244
+ }
245
+ };
246
+ var CustomDomainRequiredError = class extends SandboxError {
247
+ constructor(errorResponse) {
248
+ super(errorResponse);
249
+ this.name = "CustomDomainRequiredError";
250
+ }
251
+ };
252
+ var GitRepositoryNotFoundError = class extends SandboxError {
253
+ constructor(errorResponse) {
254
+ super(errorResponse);
255
+ this.name = "GitRepositoryNotFoundError";
256
+ }
257
+ // Type-safe accessor
258
+ get repository() {
259
+ return this.context.repository;
260
+ }
261
+ };
262
+ var GitAuthenticationError = class extends SandboxError {
263
+ constructor(errorResponse) {
264
+ super(errorResponse);
265
+ this.name = "GitAuthenticationError";
266
+ }
267
+ // Type-safe accessor
268
+ get repository() {
269
+ return this.context.repository;
270
+ }
271
+ };
272
+ var GitBranchNotFoundError = class extends SandboxError {
273
+ constructor(errorResponse) {
274
+ super(errorResponse);
275
+ this.name = "GitBranchNotFoundError";
276
+ }
277
+ // Type-safe accessors
278
+ get branch() {
279
+ return this.context.branch;
280
+ }
281
+ get repository() {
282
+ return this.context.repository;
283
+ }
284
+ };
285
+ var GitNetworkError = class extends SandboxError {
286
+ constructor(errorResponse) {
287
+ super(errorResponse);
288
+ this.name = "GitNetworkError";
289
+ }
290
+ // Type-safe accessors
291
+ get repository() {
292
+ return this.context.repository;
293
+ }
294
+ get branch() {
295
+ return this.context.branch;
296
+ }
297
+ get targetDir() {
298
+ return this.context.targetDir;
299
+ }
300
+ };
301
+ var GitCloneError = class extends SandboxError {
302
+ constructor(errorResponse) {
303
+ super(errorResponse);
304
+ this.name = "GitCloneError";
305
+ }
306
+ // Type-safe accessors
307
+ get repository() {
308
+ return this.context.repository;
309
+ }
310
+ get targetDir() {
311
+ return this.context.targetDir;
312
+ }
313
+ get stderr() {
314
+ return this.context.stderr;
315
+ }
316
+ get exitCode() {
317
+ return this.context.exitCode;
318
+ }
319
+ };
320
+ var GitCheckoutError = class extends SandboxError {
321
+ constructor(errorResponse) {
322
+ super(errorResponse);
323
+ this.name = "GitCheckoutError";
324
+ }
325
+ // Type-safe accessors
326
+ get branch() {
327
+ return this.context.branch;
328
+ }
329
+ get repository() {
330
+ return this.context.repository;
331
+ }
332
+ get stderr() {
333
+ return this.context.stderr;
334
+ }
335
+ };
336
+ var InvalidGitUrlError = class extends SandboxError {
337
+ constructor(errorResponse) {
338
+ super(errorResponse);
339
+ this.name = "InvalidGitUrlError";
340
+ }
341
+ // Type-safe accessor
342
+ get validationErrors() {
343
+ return this.context.validationErrors;
344
+ }
345
+ };
346
+ var GitError = class extends SandboxError {
347
+ constructor(errorResponse) {
348
+ super(errorResponse);
349
+ this.name = "GitError";
350
+ }
351
+ // Type-safe accessors
352
+ get repository() {
353
+ return this.context.repository;
354
+ }
355
+ get branch() {
356
+ return this.context.branch;
357
+ }
358
+ get targetDir() {
359
+ return this.context.targetDir;
360
+ }
361
+ get stderr() {
362
+ return this.context.stderr;
363
+ }
364
+ get exitCode() {
365
+ return this.context.exitCode;
366
+ }
367
+ };
368
+ var InterpreterNotReadyError = class extends SandboxError {
369
+ constructor(errorResponse) {
370
+ super(errorResponse);
371
+ this.name = "InterpreterNotReadyError";
372
+ }
373
+ // Type-safe accessors
374
+ get retryAfter() {
375
+ return this.context.retryAfter;
376
+ }
377
+ get progress() {
378
+ return this.context.progress;
379
+ }
380
+ };
381
+ var ContextNotFoundError = class extends SandboxError {
382
+ constructor(errorResponse) {
383
+ super(errorResponse);
384
+ this.name = "ContextNotFoundError";
385
+ }
386
+ // Type-safe accessor
387
+ get contextId() {
388
+ return this.context.contextId;
389
+ }
390
+ };
391
+ var CodeExecutionError = class extends SandboxError {
392
+ constructor(errorResponse) {
393
+ super(errorResponse);
394
+ this.name = "CodeExecutionError";
395
+ }
396
+ // Type-safe accessors
397
+ get contextId() {
398
+ return this.context.contextId;
399
+ }
400
+ get ename() {
401
+ return this.context.ename;
402
+ }
403
+ get evalue() {
404
+ return this.context.evalue;
405
+ }
406
+ get traceback() {
407
+ return this.context.traceback;
408
+ }
409
+ };
410
+ var ValidationFailedError = class extends SandboxError {
411
+ constructor(errorResponse) {
412
+ super(errorResponse);
413
+ this.name = "ValidationFailedError";
414
+ }
415
+ // Type-safe accessor
416
+ get validationErrors() {
417
+ return this.context.validationErrors;
418
+ }
419
+ };
420
+
421
+ // src/errors/adapter.ts
422
+ function createErrorFromResponse(errorResponse) {
423
+ switch (errorResponse.code) {
424
+ // File System Errors
425
+ case ErrorCode.FILE_NOT_FOUND:
426
+ return new FileNotFoundError(errorResponse);
427
+ case ErrorCode.FILE_EXISTS:
428
+ return new FileExistsError(errorResponse);
429
+ case ErrorCode.PERMISSION_DENIED:
430
+ return new PermissionDeniedError(errorResponse);
431
+ case ErrorCode.IS_DIRECTORY:
432
+ case ErrorCode.NOT_DIRECTORY:
433
+ case ErrorCode.NO_SPACE:
434
+ case ErrorCode.TOO_MANY_FILES:
435
+ case ErrorCode.RESOURCE_BUSY:
436
+ case ErrorCode.READ_ONLY:
437
+ case ErrorCode.NAME_TOO_LONG:
438
+ case ErrorCode.TOO_MANY_LINKS:
439
+ case ErrorCode.FILESYSTEM_ERROR:
440
+ return new FileSystemError(errorResponse);
441
+ // Command Errors
442
+ case ErrorCode.COMMAND_NOT_FOUND:
443
+ return new CommandNotFoundError(errorResponse);
444
+ case ErrorCode.COMMAND_PERMISSION_DENIED:
445
+ case ErrorCode.COMMAND_EXECUTION_ERROR:
446
+ case ErrorCode.INVALID_COMMAND:
447
+ case ErrorCode.STREAM_START_ERROR:
448
+ return new CommandError(errorResponse);
449
+ // Process Errors
450
+ case ErrorCode.PROCESS_NOT_FOUND:
451
+ return new ProcessNotFoundError(errorResponse);
452
+ case ErrorCode.PROCESS_PERMISSION_DENIED:
453
+ case ErrorCode.PROCESS_ERROR:
454
+ return new ProcessError(errorResponse);
455
+ // Port Errors
456
+ case ErrorCode.PORT_ALREADY_EXPOSED:
457
+ return new PortAlreadyExposedError(errorResponse);
458
+ case ErrorCode.PORT_NOT_EXPOSED:
459
+ return new PortNotExposedError(errorResponse);
460
+ case ErrorCode.INVALID_PORT_NUMBER:
461
+ case ErrorCode.INVALID_PORT:
462
+ return new InvalidPortError(errorResponse);
463
+ case ErrorCode.SERVICE_NOT_RESPONDING:
464
+ return new ServiceNotRespondingError(errorResponse);
465
+ case ErrorCode.PORT_IN_USE:
466
+ return new PortInUseError(errorResponse);
467
+ case ErrorCode.PORT_OPERATION_ERROR:
468
+ return new PortError(errorResponse);
469
+ case ErrorCode.CUSTOM_DOMAIN_REQUIRED:
470
+ return new CustomDomainRequiredError(errorResponse);
471
+ // Git Errors
472
+ case ErrorCode.GIT_REPOSITORY_NOT_FOUND:
473
+ return new GitRepositoryNotFoundError(errorResponse);
474
+ case ErrorCode.GIT_AUTH_FAILED:
475
+ return new GitAuthenticationError(errorResponse);
476
+ case ErrorCode.GIT_BRANCH_NOT_FOUND:
477
+ return new GitBranchNotFoundError(errorResponse);
478
+ case ErrorCode.GIT_NETWORK_ERROR:
479
+ return new GitNetworkError(errorResponse);
480
+ case ErrorCode.GIT_CLONE_FAILED:
481
+ return new GitCloneError(errorResponse);
482
+ case ErrorCode.GIT_CHECKOUT_FAILED:
483
+ return new GitCheckoutError(errorResponse);
484
+ case ErrorCode.INVALID_GIT_URL:
485
+ return new InvalidGitUrlError(errorResponse);
486
+ case ErrorCode.GIT_OPERATION_FAILED:
487
+ return new GitError(errorResponse);
488
+ // Code Interpreter Errors
489
+ case ErrorCode.INTERPRETER_NOT_READY:
490
+ return new InterpreterNotReadyError(errorResponse);
491
+ case ErrorCode.CONTEXT_NOT_FOUND:
492
+ return new ContextNotFoundError(errorResponse);
493
+ case ErrorCode.CODE_EXECUTION_ERROR:
494
+ return new CodeExecutionError(errorResponse);
495
+ // Validation Errors
496
+ case ErrorCode.VALIDATION_FAILED:
497
+ return new ValidationFailedError(errorResponse);
498
+ // Generic Errors
499
+ case ErrorCode.INVALID_JSON_RESPONSE:
500
+ case ErrorCode.UNKNOWN_ERROR:
501
+ case ErrorCode.INTERNAL_ERROR:
502
+ return new SandboxError(errorResponse);
503
+ default:
504
+ return new SandboxError(errorResponse);
505
+ }
506
+ }
507
+
508
+ // src/clients/base-client.ts
509
+ var TIMEOUT_MS = 6e4;
510
+ var MIN_TIME_FOR_RETRY_MS = 1e4;
511
+ var BaseHttpClient = class {
512
+ baseUrl;
513
+ options;
514
+ constructor(options = {}) {
515
+ this.options = {
516
+ ...options
517
+ };
518
+ this.baseUrl = this.options.baseUrl;
519
+ }
520
+ /**
521
+ * Core HTTP request method with automatic retry for container provisioning delays
522
+ */
523
+ async doFetch(path, options) {
524
+ const startTime = Date.now();
525
+ let attempt = 0;
526
+ console.log(`[DEBUG] doFetch called for ${options?.method || "GET"} ${path}`);
527
+ while (true) {
528
+ const response = await this.executeFetch(path, options);
529
+ console.log(`[DEBUG] Response status: ${response.status}`);
530
+ if (response.status === 503) {
531
+ console.log("[DEBUG] Got 503 response, checking if container provisioning error...");
532
+ const isContainerProvisioning = await this.isContainerProvisioningError(response);
533
+ console.log("[DEBUG] isContainerProvisioning result:", isContainerProvisioning);
534
+ if (isContainerProvisioning) {
535
+ const elapsed = Date.now() - startTime;
536
+ const remaining = TIMEOUT_MS - elapsed;
537
+ console.log(`[DEBUG] Elapsed: ${elapsed}ms, Remaining: ${remaining}ms`);
538
+ if (remaining > MIN_TIME_FOR_RETRY_MS) {
539
+ const delay = Math.min(2e3 * 2 ** attempt, 16e3);
540
+ console.log(
541
+ `[Sandbox SDK] Container provisioning in progress (attempt ${attempt + 1}), retrying in ${delay}ms (${Math.floor(remaining / 1e3)}s remaining)`
542
+ );
543
+ await new Promise((resolve) => setTimeout(resolve, delay));
544
+ attempt++;
545
+ continue;
546
+ } else {
547
+ console.error(
548
+ `[Sandbox SDK] Container failed to provision after ${attempt + 1} attempts over 60s.`
549
+ );
550
+ return response;
551
+ }
552
+ } else {
553
+ console.log("[DEBUG] Not a container provisioning error, returning 503 immediately");
554
+ }
555
+ }
556
+ if (response.status !== 200) {
557
+ console.log(`[DEBUG] Returning response with status ${response.status}`);
558
+ }
559
+ return response;
560
+ }
561
+ }
562
+ /**
563
+ * Make a POST request with JSON body
564
+ */
565
+ async post(endpoint, data, responseHandler) {
566
+ const response = await this.doFetch(endpoint, {
567
+ method: "POST",
568
+ headers: {
569
+ "Content-Type": "application/json"
570
+ },
571
+ body: JSON.stringify(data)
572
+ });
573
+ return this.handleResponse(response, responseHandler);
574
+ }
575
+ /**
576
+ * Make a GET request
577
+ */
578
+ async get(endpoint, responseHandler) {
579
+ const response = await this.doFetch(endpoint, {
580
+ method: "GET"
581
+ });
582
+ return this.handleResponse(response, responseHandler);
583
+ }
584
+ /**
585
+ * Make a DELETE request
586
+ */
587
+ async delete(endpoint, responseHandler) {
588
+ const response = await this.doFetch(endpoint, {
589
+ method: "DELETE"
590
+ });
591
+ return this.handleResponse(response, responseHandler);
592
+ }
593
+ /**
594
+ * Handle HTTP response with error checking and parsing
595
+ */
596
+ async handleResponse(response, customHandler) {
597
+ if (!response.ok) {
598
+ await this.handleErrorResponse(response);
599
+ }
600
+ if (customHandler) {
601
+ return customHandler(response);
602
+ }
603
+ try {
604
+ return await response.json();
605
+ } catch (error) {
606
+ const errorResponse = {
607
+ code: ErrorCode2.INVALID_JSON_RESPONSE,
608
+ message: `Invalid JSON response: ${error instanceof Error ? error.message : "Unknown parsing error"}`,
609
+ context: {},
610
+ httpStatus: response.status,
611
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
612
+ };
613
+ throw createErrorFromResponse(errorResponse);
614
+ }
615
+ }
616
+ /**
617
+ * Handle error responses with consistent error throwing
618
+ */
619
+ async handleErrorResponse(response) {
620
+ let errorData;
621
+ try {
622
+ errorData = await response.json();
623
+ } catch {
624
+ errorData = {
625
+ code: ErrorCode2.INTERNAL_ERROR,
626
+ message: `HTTP error! status: ${response.status}`,
627
+ context: { statusText: response.statusText },
628
+ httpStatus: response.status,
629
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
630
+ };
631
+ }
632
+ const error = createErrorFromResponse(errorData);
633
+ this.options.onError?.(errorData.message, void 0);
634
+ throw error;
635
+ }
636
+ /**
637
+ * Create a streaming response handler for Server-Sent Events
638
+ */
639
+ async handleStreamResponse(response) {
640
+ if (!response.ok) {
641
+ await this.handleErrorResponse(response);
642
+ }
643
+ if (!response.body) {
644
+ throw new Error("No response body for streaming");
645
+ }
646
+ return response.body;
647
+ }
648
+ /**
649
+ * Utility method to log successful operations
650
+ */
651
+ logSuccess(operation, details) {
652
+ const message = details ? `[HTTP Client] ${operation}: ${details}` : `[HTTP Client] ${operation} completed successfully`;
653
+ console.log(message);
654
+ }
655
+ /**
656
+ * Utility method to log errors
657
+ */
658
+ logError(operation, error) {
659
+ console.error(`[HTTP Client] Error in ${operation}:`, error);
660
+ }
661
+ /**
662
+ * Check if 503 response is from container provisioning (retryable)
663
+ * vs user application (not retryable)
664
+ */
665
+ async isContainerProvisioningError(response) {
666
+ try {
667
+ const cloned = response.clone();
668
+ const text = await cloned.text();
669
+ console.log("[DEBUG] 503 response body:", text.substring(0, 200));
670
+ const isProvisioning = text.includes("There is no Container instance available");
671
+ console.log("[DEBUG] Is container provisioning error?", isProvisioning);
672
+ return isProvisioning;
673
+ } catch (error) {
674
+ console.error("[DEBUG] Error checking response body:", error);
675
+ return false;
676
+ }
677
+ }
678
+ async executeFetch(path, options) {
679
+ const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`;
680
+ const method = options?.method || "GET";
681
+ console.log(`[HTTP Client] Making ${method} request to ${url}`);
682
+ try {
683
+ let response;
684
+ if (this.options.stub) {
685
+ response = await this.options.stub.containerFetch(
686
+ url,
687
+ options || {},
688
+ this.options.port
689
+ );
690
+ } else {
691
+ response = await fetch(url, options);
692
+ }
693
+ console.log(
694
+ `[HTTP Client] Response: ${response.status} ${response.statusText}`
695
+ );
696
+ if (!response.ok) {
697
+ console.error(
698
+ `[HTTP Client] Request failed: ${method} ${url} - ${response.status} ${response.statusText}`
699
+ );
700
+ }
701
+ return response;
702
+ } catch (error) {
703
+ console.error(`[HTTP Client] Request error: ${method} ${url}`, error);
704
+ throw error;
705
+ }
706
+ }
707
+ };
708
+
709
+ // src/clients/command-client.ts
710
+ var CommandClient = class extends BaseHttpClient {
711
+ constructor(options = {}) {
712
+ super(options);
713
+ }
714
+ /**
715
+ * Execute a command and return the complete result
716
+ * @param command - The command to execute
717
+ * @param sessionId - The session ID for this command execution
718
+ * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
719
+ */
720
+ async execute(command, sessionId, timeoutMs) {
721
+ try {
722
+ const data = {
723
+ command,
724
+ sessionId,
725
+ ...timeoutMs !== void 0 && { timeoutMs }
726
+ };
727
+ const response = await this.post(
728
+ "/api/execute",
729
+ data
730
+ );
731
+ this.logSuccess(
732
+ "Command executed",
733
+ `${command}, Success: ${response.success}`
734
+ );
735
+ this.options.onCommandComplete?.(
736
+ response.success,
737
+ response.exitCode,
738
+ response.stdout,
739
+ response.stderr,
740
+ response.command
741
+ );
742
+ return response;
743
+ } catch (error) {
744
+ this.logError("execute", error);
745
+ this.options.onError?.(
746
+ error instanceof Error ? error.message : String(error),
747
+ command
748
+ );
749
+ throw error;
750
+ }
751
+ }
752
+ /**
753
+ * Execute a command and return a stream of events
754
+ * @param command - The command to execute
755
+ * @param sessionId - The session ID for this command execution
756
+ */
757
+ async executeStream(command, sessionId) {
758
+ try {
759
+ const data = { command, sessionId };
760
+ const response = await this.doFetch("/api/execute/stream", {
761
+ method: "POST",
762
+ headers: {
763
+ "Content-Type": "application/json"
764
+ },
765
+ body: JSON.stringify(data)
766
+ });
767
+ const stream = await this.handleStreamResponse(response);
768
+ this.logSuccess("Command stream started", command);
769
+ return stream;
770
+ } catch (error) {
771
+ this.logError("executeStream", error);
772
+ this.options.onError?.(
773
+ error instanceof Error ? error.message : String(error),
774
+ command
775
+ );
776
+ throw error;
777
+ }
778
+ }
779
+ };
780
+
781
+ // src/clients/file-client.ts
782
+ var FileClient = class extends BaseHttpClient {
783
+ constructor(options = {}) {
784
+ super(options);
785
+ }
786
+ /**
787
+ * Create a directory
788
+ * @param path - Directory path to create
789
+ * @param sessionId - The session ID for this operation
790
+ * @param options - Optional settings (recursive)
791
+ */
792
+ async mkdir(path, sessionId, options) {
793
+ try {
794
+ const data = {
795
+ path,
796
+ sessionId,
797
+ recursive: options?.recursive ?? false
798
+ };
799
+ const response = await this.post("/api/mkdir", data);
800
+ this.logSuccess("Directory created", `${path} (recursive: ${data.recursive})`);
801
+ return response;
802
+ } catch (error) {
803
+ this.logError("mkdir", error);
804
+ throw error;
805
+ }
806
+ }
807
+ /**
808
+ * Write content to a file
809
+ * @param path - File path to write to
810
+ * @param content - Content to write
811
+ * @param sessionId - The session ID for this operation
812
+ * @param options - Optional settings (encoding)
813
+ */
814
+ async writeFile(path, content, sessionId, options) {
815
+ try {
816
+ const data = {
817
+ path,
818
+ content,
819
+ sessionId,
820
+ encoding: options?.encoding ?? "utf8"
821
+ };
822
+ const response = await this.post("/api/write", data);
823
+ this.logSuccess("File written", `${path} (${content.length} chars)`);
824
+ return response;
825
+ } catch (error) {
826
+ this.logError("writeFile", error);
827
+ throw error;
828
+ }
829
+ }
830
+ /**
831
+ * Read content from a file
832
+ * @param path - File path to read from
833
+ * @param sessionId - The session ID for this operation
834
+ * @param options - Optional settings (encoding)
835
+ */
836
+ async readFile(path, sessionId, options) {
837
+ try {
838
+ const data = {
839
+ path,
840
+ sessionId,
841
+ encoding: options?.encoding ?? "utf8"
842
+ };
843
+ const response = await this.post("/api/read", data);
844
+ this.logSuccess("File read", `${path} (${response.content.length} chars)`);
845
+ return response;
846
+ } catch (error) {
847
+ this.logError("readFile", error);
848
+ throw error;
849
+ }
850
+ }
851
+ /**
852
+ * Stream a file using Server-Sent Events
853
+ * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
854
+ * @param path - File path to stream
855
+ * @param sessionId - The session ID for this operation
856
+ */
857
+ async readFileStream(path, sessionId) {
858
+ try {
859
+ const data = {
860
+ path,
861
+ sessionId
862
+ };
863
+ const response = await this.doFetch("/api/read/stream", {
864
+ method: "POST",
865
+ headers: {
866
+ "Content-Type": "application/json"
867
+ },
868
+ body: JSON.stringify(data)
869
+ });
870
+ const stream = await this.handleStreamResponse(response);
871
+ this.logSuccess("File stream started", path);
872
+ return stream;
873
+ } catch (error) {
874
+ this.logError("readFileStream", error);
875
+ throw error;
876
+ }
877
+ }
878
+ /**
879
+ * Delete a file
880
+ * @param path - File path to delete
881
+ * @param sessionId - The session ID for this operation
882
+ */
883
+ async deleteFile(path, sessionId) {
884
+ try {
885
+ const data = { path, sessionId };
886
+ const response = await this.post("/api/delete", data);
887
+ this.logSuccess("File deleted", path);
888
+ return response;
889
+ } catch (error) {
890
+ this.logError("deleteFile", error);
891
+ throw error;
892
+ }
893
+ }
894
+ /**
895
+ * Rename a file
896
+ * @param path - Current file path
897
+ * @param newPath - New file path
898
+ * @param sessionId - The session ID for this operation
899
+ */
900
+ async renameFile(path, newPath, sessionId) {
901
+ try {
902
+ const data = { oldPath: path, newPath, sessionId };
903
+ const response = await this.post("/api/rename", data);
904
+ this.logSuccess("File renamed", `${path} -> ${newPath}`);
905
+ return response;
906
+ } catch (error) {
907
+ this.logError("renameFile", error);
908
+ throw error;
909
+ }
910
+ }
911
+ /**
912
+ * Move a file
913
+ * @param path - Current file path
914
+ * @param newPath - Destination file path
915
+ * @param sessionId - The session ID for this operation
916
+ */
917
+ async moveFile(path, newPath, sessionId) {
918
+ try {
919
+ const data = { sourcePath: path, destinationPath: newPath, sessionId };
920
+ const response = await this.post("/api/move", data);
921
+ this.logSuccess("File moved", `${path} -> ${newPath}`);
922
+ return response;
923
+ } catch (error) {
924
+ this.logError("moveFile", error);
925
+ throw error;
926
+ }
927
+ }
928
+ /**
929
+ * List files in a directory
930
+ * @param path - Directory path to list
931
+ * @param sessionId - The session ID for this operation
932
+ * @param options - Optional settings (recursive, includeHidden)
933
+ */
934
+ async listFiles(path, sessionId, options) {
935
+ try {
936
+ const data = {
937
+ path,
938
+ sessionId,
939
+ options: options || {}
940
+ };
941
+ const response = await this.post("/api/list-files", data);
942
+ this.logSuccess("Files listed", `${path} (${response.count} files)`);
943
+ return response;
944
+ } catch (error) {
945
+ this.logError("listFiles", error);
946
+ throw error;
947
+ }
948
+ }
949
+ };
950
+
951
+ // src/clients/git-client.ts
952
+ var GitClient = class extends BaseHttpClient {
953
+ constructor(options = {}) {
954
+ super(options);
955
+ }
956
+ /**
957
+ * Clone a Git repository
958
+ * @param repoUrl - URL of the Git repository to clone
959
+ * @param sessionId - The session ID for this operation
960
+ * @param options - Optional settings (branch, targetDir)
961
+ */
962
+ async checkout(repoUrl, sessionId, options) {
963
+ try {
964
+ let targetDir = options?.targetDir;
965
+ if (!targetDir) {
966
+ const repoName = this.extractRepoName(repoUrl);
967
+ targetDir = `/workspace/${repoName}`;
968
+ }
969
+ const data = {
970
+ repoUrl,
971
+ sessionId,
972
+ targetDir
973
+ };
974
+ if (options?.branch) {
975
+ data.branch = options.branch;
976
+ }
977
+ const response = await this.post(
978
+ "/api/git/checkout",
979
+ data
980
+ );
981
+ this.logSuccess(
982
+ "Repository cloned",
983
+ `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
984
+ );
985
+ return response;
986
+ } catch (error) {
987
+ this.logError("checkout", error);
988
+ throw error;
989
+ }
990
+ }
991
+ /**
992
+ * Extract repository name from URL for default directory name
993
+ */
994
+ extractRepoName(repoUrl) {
995
+ try {
996
+ const url = new URL(repoUrl);
997
+ const pathParts = url.pathname.split("/");
998
+ const repoName = pathParts[pathParts.length - 1];
999
+ return repoName.replace(/\.git$/, "");
1000
+ } catch {
1001
+ const parts = repoUrl.split("/");
1002
+ const repoName = parts[parts.length - 1];
1003
+ return repoName.replace(/\.git$/, "") || "repo";
1004
+ }
1005
+ }
1006
+ };
1007
+
1008
+ // src/clients/interpreter-client.ts
1009
+ import {
1010
+ ResultImpl
1011
+ } from "@repo/shared";
1012
+ var InterpreterClient = class extends BaseHttpClient {
1013
+ maxRetries = 3;
1014
+ retryDelayMs = 1e3;
1015
+ constructor(options = {}) {
1016
+ super(options);
1017
+ }
1018
+ async createCodeContext(options = {}) {
1019
+ return this.executeWithRetry(async () => {
1020
+ const response = await this.doFetch("/api/contexts", {
1021
+ method: "POST",
1022
+ headers: { "Content-Type": "application/json" },
1023
+ body: JSON.stringify({
1024
+ language: options.language || "python",
1025
+ cwd: options.cwd || "/workspace",
1026
+ env_vars: options.envVars
1027
+ })
1028
+ });
1029
+ if (!response.ok) {
1030
+ const error = await this.parseErrorResponse(response);
1031
+ throw error;
1032
+ }
1033
+ const data = await response.json();
1034
+ if (!data.success) {
1035
+ throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
1036
+ }
1037
+ return {
1038
+ id: data.contextId,
1039
+ language: data.language,
1040
+ cwd: data.cwd || "/workspace",
1041
+ createdAt: new Date(data.timestamp),
1042
+ lastUsed: new Date(data.timestamp)
1043
+ };
1044
+ });
1045
+ }
1046
+ async runCodeStream(contextId, code, language, callbacks, timeoutMs) {
1047
+ return this.executeWithRetry(async () => {
1048
+ const response = await this.doFetch("/api/execute/code", {
1049
+ method: "POST",
1050
+ headers: {
1051
+ "Content-Type": "application/json",
1052
+ Accept: "text/event-stream"
1053
+ },
1054
+ body: JSON.stringify({
1055
+ context_id: contextId,
1056
+ code,
1057
+ language,
1058
+ ...timeoutMs !== void 0 && { timeout_ms: timeoutMs }
1059
+ })
1060
+ });
1061
+ if (!response.ok) {
1062
+ const error = await this.parseErrorResponse(response);
1063
+ throw error;
1064
+ }
1065
+ if (!response.body) {
1066
+ throw new Error("No response body for streaming execution");
1067
+ }
1068
+ for await (const chunk of this.readLines(response.body)) {
1069
+ await this.parseExecutionResult(chunk, callbacks);
1070
+ }
1071
+ });
1072
+ }
1073
+ async listCodeContexts() {
1074
+ return this.executeWithRetry(async () => {
1075
+ const response = await this.doFetch("/api/contexts", {
1076
+ method: "GET",
1077
+ headers: { "Content-Type": "application/json" }
1078
+ });
1079
+ if (!response.ok) {
1080
+ const error = await this.parseErrorResponse(response);
1081
+ throw error;
1082
+ }
1083
+ const data = await response.json();
1084
+ if (!data.success) {
1085
+ throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
1086
+ }
1087
+ return data.contexts.map((ctx) => ({
1088
+ id: ctx.id,
1089
+ language: ctx.language,
1090
+ cwd: ctx.cwd || "/workspace",
1091
+ createdAt: new Date(data.timestamp),
1092
+ lastUsed: new Date(data.timestamp)
1093
+ }));
1094
+ });
1095
+ }
1096
+ async deleteCodeContext(contextId) {
1097
+ return this.executeWithRetry(async () => {
1098
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
1099
+ method: "DELETE",
1100
+ headers: { "Content-Type": "application/json" }
1101
+ });
1102
+ if (!response.ok) {
1103
+ const error = await this.parseErrorResponse(response);
1104
+ throw error;
1105
+ }
1106
+ });
1107
+ }
1108
+ /**
1109
+ * Execute an operation with automatic retry for transient errors
1110
+ */
1111
+ async executeWithRetry(operation) {
1112
+ let lastError;
1113
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
1114
+ try {
1115
+ return await operation();
1116
+ } catch (error) {
1117
+ lastError = error;
1118
+ if (this.isRetryableError(error)) {
1119
+ if (attempt < this.maxRetries - 1) {
1120
+ const delay = this.retryDelayMs * 2 ** attempt + Math.random() * 1e3;
1121
+ await new Promise((resolve) => setTimeout(resolve, delay));
1122
+ continue;
1123
+ }
1124
+ }
1125
+ throw error;
1126
+ }
1127
+ }
1128
+ throw lastError || new Error("Execution failed after retries");
1129
+ }
1130
+ isRetryableError(error) {
1131
+ if (error instanceof InterpreterNotReadyError) {
1132
+ return true;
1133
+ }
1134
+ if (error instanceof Error) {
1135
+ return error.message.includes("not ready") || error.message.includes("initializing");
1136
+ }
1137
+ return false;
1138
+ }
1139
+ async parseErrorResponse(response) {
1140
+ try {
1141
+ const errorData = await response.json();
1142
+ return createErrorFromResponse(errorData);
1143
+ } catch {
1144
+ const errorResponse = {
1145
+ code: ErrorCode2.INTERNAL_ERROR,
1146
+ message: `HTTP ${response.status}: ${response.statusText}`,
1147
+ context: {},
1148
+ httpStatus: response.status,
1149
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1150
+ };
1151
+ return createErrorFromResponse(errorResponse);
1152
+ }
1153
+ }
1154
+ async *readLines(stream) {
1155
+ const reader = stream.getReader();
1156
+ let buffer = "";
1157
+ try {
1158
+ while (true) {
1159
+ const { done, value } = await reader.read();
1160
+ if (value) {
1161
+ buffer += new TextDecoder().decode(value);
1162
+ }
1163
+ if (done) break;
1164
+ let newlineIdx = buffer.indexOf("\n");
1165
+ while (newlineIdx !== -1) {
1166
+ yield buffer.slice(0, newlineIdx);
1167
+ buffer = buffer.slice(newlineIdx + 1);
1168
+ newlineIdx = buffer.indexOf("\n");
1169
+ }
1170
+ }
1171
+ if (buffer.length > 0) {
1172
+ yield buffer;
1173
+ }
1174
+ } finally {
1175
+ reader.releaseLock();
1176
+ }
1177
+ }
1178
+ async parseExecutionResult(line, callbacks) {
1179
+ if (!line.trim()) return;
1180
+ if (!line.startsWith("data: ")) return;
1181
+ try {
1182
+ const jsonData = line.substring(6);
1183
+ const data = JSON.parse(jsonData);
1184
+ switch (data.type) {
1185
+ case "stdout":
1186
+ if (callbacks.onStdout && data.text) {
1187
+ await callbacks.onStdout({
1188
+ text: data.text,
1189
+ timestamp: data.timestamp || Date.now()
1190
+ });
1191
+ }
1192
+ break;
1193
+ case "stderr":
1194
+ if (callbacks.onStderr && data.text) {
1195
+ await callbacks.onStderr({
1196
+ text: data.text,
1197
+ timestamp: data.timestamp || Date.now()
1198
+ });
1199
+ }
1200
+ break;
1201
+ case "result":
1202
+ if (callbacks.onResult) {
1203
+ const result = new ResultImpl(data);
1204
+ await callbacks.onResult(result);
1205
+ }
1206
+ break;
1207
+ case "error":
1208
+ if (callbacks.onError) {
1209
+ await callbacks.onError({
1210
+ name: data.ename || "Error",
1211
+ message: data.evalue || "Unknown error",
1212
+ traceback: data.traceback || []
1213
+ });
1214
+ }
1215
+ break;
1216
+ case "execution_complete":
1217
+ break;
1218
+ }
1219
+ } catch (error) {
1220
+ console.error("Failed to parse execution result:", error);
1221
+ }
1222
+ }
1223
+ };
1224
+
1225
+ // src/clients/port-client.ts
1226
+ var PortClient = class extends BaseHttpClient {
1227
+ constructor(options = {}) {
1228
+ super(options);
1229
+ }
1230
+ /**
1231
+ * Expose a port and get a preview URL
1232
+ * @param port - Port number to expose
1233
+ * @param sessionId - The session ID for this operation
1234
+ * @param name - Optional name for the port
1235
+ */
1236
+ async exposePort(port, sessionId, name) {
1237
+ try {
1238
+ const data = { port, sessionId, name };
1239
+ const response = await this.post(
1240
+ "/api/expose-port",
1241
+ data
1242
+ );
1243
+ this.logSuccess(
1244
+ "Port exposed",
1245
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ""}`
1246
+ );
1247
+ return response;
1248
+ } catch (error) {
1249
+ this.logError("exposePort", error);
1250
+ throw error;
1251
+ }
1252
+ }
1253
+ /**
1254
+ * Unexpose a port and remove its preview URL
1255
+ * @param port - Port number to unexpose
1256
+ * @param sessionId - The session ID for this operation
1257
+ */
1258
+ async unexposePort(port, sessionId) {
1259
+ try {
1260
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
1261
+ const response = await this.delete(url);
1262
+ this.logSuccess("Port unexposed", `${port}`);
1263
+ return response;
1264
+ } catch (error) {
1265
+ this.logError("unexposePort", error);
1266
+ throw error;
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Get all currently exposed ports
1271
+ * @param sessionId - The session ID for this operation
1272
+ */
1273
+ async getExposedPorts(sessionId) {
1274
+ try {
1275
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
1276
+ const response = await this.get(url);
1277
+ this.logSuccess(
1278
+ "Exposed ports retrieved",
1279
+ `${response.ports.length} ports exposed`
1280
+ );
1281
+ return response;
1282
+ } catch (error) {
1283
+ this.logError("getExposedPorts", error);
1284
+ throw error;
1285
+ }
1286
+ }
1287
+ };
1288
+
1289
+ // src/clients/process-client.ts
1290
+ var ProcessClient = class extends BaseHttpClient {
1291
+ constructor(options = {}) {
1292
+ super(options);
1293
+ }
1294
+ /**
1295
+ * Start a background process
1296
+ * @param command - Command to execute as a background process
1297
+ * @param sessionId - The session ID for this operation
1298
+ * @param options - Optional settings (processId)
1299
+ */
1300
+ async startProcess(command, sessionId, options) {
1301
+ try {
1302
+ const data = {
1303
+ command,
1304
+ sessionId,
1305
+ processId: options?.processId
1306
+ };
1307
+ const response = await this.post(
1308
+ "/api/process/start",
1309
+ data
1310
+ );
1311
+ this.logSuccess(
1312
+ "Process started",
1313
+ `${command} (ID: ${response.processId})`
1314
+ );
1315
+ return response;
1316
+ } catch (error) {
1317
+ this.logError("startProcess", error);
1318
+ throw error;
1319
+ }
1320
+ }
1321
+ /**
1322
+ * List all processes (sandbox-scoped, not session-scoped)
1323
+ */
1324
+ async listProcesses() {
1325
+ try {
1326
+ const url = `/api/process/list`;
1327
+ const response = await this.get(url);
1328
+ this.logSuccess("Processes listed", `${response.processes.length} processes`);
1329
+ return response;
1330
+ } catch (error) {
1331
+ this.logError("listProcesses", error);
1332
+ throw error;
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Get information about a specific process (sandbox-scoped, not session-scoped)
1337
+ * @param processId - ID of the process to retrieve
1338
+ */
1339
+ async getProcess(processId) {
1340
+ try {
1341
+ const url = `/api/process/${processId}`;
1342
+ const response = await this.get(url);
1343
+ this.logSuccess("Process retrieved", `ID: ${processId}`);
1344
+ return response;
1345
+ } catch (error) {
1346
+ this.logError("getProcess", error);
1347
+ throw error;
1348
+ }
1349
+ }
1350
+ /**
1351
+ * Kill a specific process (sandbox-scoped, not session-scoped)
1352
+ * @param processId - ID of the process to kill
1353
+ */
1354
+ async killProcess(processId) {
1355
+ try {
1356
+ const url = `/api/process/${processId}`;
1357
+ const response = await this.delete(url);
1358
+ this.logSuccess("Process killed", `ID: ${processId}`);
1359
+ return response;
1360
+ } catch (error) {
1361
+ this.logError("killProcess", error);
1362
+ throw error;
1363
+ }
1364
+ }
1365
+ /**
1366
+ * Kill all running processes (sandbox-scoped, not session-scoped)
1367
+ */
1368
+ async killAllProcesses() {
1369
+ try {
1370
+ const url = `/api/process/kill-all`;
1371
+ const response = await this.delete(url);
1372
+ this.logSuccess(
1373
+ "All processes killed",
1374
+ `${response.cleanedCount} processes terminated`
1375
+ );
1376
+ return response;
1377
+ } catch (error) {
1378
+ this.logError("killAllProcesses", error);
1379
+ throw error;
1380
+ }
1381
+ }
1382
+ /**
1383
+ * Get logs from a specific process (sandbox-scoped, not session-scoped)
1384
+ * @param processId - ID of the process to get logs from
1385
+ */
1386
+ async getProcessLogs(processId) {
1387
+ try {
1388
+ const url = `/api/process/${processId}/logs`;
1389
+ const response = await this.get(url);
1390
+ this.logSuccess(
1391
+ "Process logs retrieved",
1392
+ `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`
1393
+ );
1394
+ return response;
1395
+ } catch (error) {
1396
+ this.logError("getProcessLogs", error);
1397
+ throw error;
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Stream logs from a specific process (sandbox-scoped, not session-scoped)
1402
+ * @param processId - ID of the process to stream logs from
1403
+ */
1404
+ async streamProcessLogs(processId) {
1405
+ try {
1406
+ const url = `/api/process/${processId}/stream`;
1407
+ const response = await this.doFetch(url, {
1408
+ method: "GET"
1409
+ });
1410
+ const stream = await this.handleStreamResponse(response);
1411
+ this.logSuccess("Process log stream started", `ID: ${processId}`);
1412
+ return stream;
1413
+ } catch (error) {
1414
+ this.logError("streamProcessLogs", error);
1415
+ throw error;
1416
+ }
1417
+ }
1418
+ };
1419
+
1420
+ // src/clients/utility-client.ts
1421
+ var UtilityClient = class extends BaseHttpClient {
1422
+ constructor(options = {}) {
1423
+ super(options);
1424
+ }
1425
+ /**
1426
+ * Ping the sandbox to check if it's responsive
1427
+ */
1428
+ async ping() {
1429
+ try {
1430
+ const response = await this.get("/api/ping");
1431
+ this.logSuccess("Ping successful", response.message);
1432
+ return response.message;
1433
+ } catch (error) {
1434
+ this.logError("ping", error);
1435
+ throw error;
1436
+ }
1437
+ }
1438
+ /**
1439
+ * Get list of available commands in the sandbox environment
1440
+ */
1441
+ async getCommands() {
1442
+ try {
1443
+ const response = await this.get("/api/commands");
1444
+ this.logSuccess(
1445
+ "Commands retrieved",
1446
+ `${response.count} commands available`
1447
+ );
1448
+ return response.availableCommands;
1449
+ } catch (error) {
1450
+ this.logError("getCommands", error);
1451
+ throw error;
1452
+ }
1453
+ }
1454
+ /**
1455
+ * Create a new execution session
1456
+ * @param options - Session configuration (id, env, cwd)
1457
+ */
1458
+ async createSession(options) {
1459
+ try {
1460
+ const response = await this.post(
1461
+ "/api/session/create",
1462
+ options
1463
+ );
1464
+ this.logSuccess("Session created", `ID: ${options.id}`);
1465
+ return response;
1466
+ } catch (error) {
1467
+ this.logError("createSession", error);
1468
+ throw error;
1469
+ }
1470
+ }
1471
+ };
1472
+
1473
+ // src/clients/sandbox-client.ts
1474
+ var SandboxClient = class {
1475
+ commands;
1476
+ files;
1477
+ processes;
1478
+ ports;
1479
+ git;
1480
+ interpreter;
1481
+ utils;
1482
+ constructor(options = {}) {
1483
+ const clientOptions = {
1484
+ baseUrl: "http://localhost:3000",
1485
+ ...options
1486
+ };
1487
+ this.commands = new CommandClient(clientOptions);
1488
+ this.files = new FileClient(clientOptions);
1489
+ this.processes = new ProcessClient(clientOptions);
1490
+ this.ports = new PortClient(clientOptions);
1491
+ this.git = new GitClient(clientOptions);
1492
+ this.interpreter = new InterpreterClient(clientOptions);
1493
+ this.utils = new UtilityClient(clientOptions);
1494
+ }
1495
+ };
1496
+
1497
+ // src/sandbox.ts
1498
+ function getSandbox(ns, id) {
1499
+ const stub = getContainer(ns, id);
1500
+ stub.setSandboxName?.(id);
1501
+ return stub;
1502
+ }
1503
+ var Sandbox = class extends Container {
1504
+ defaultPort = 3e3;
1505
+ // Default port for the container's Bun server
1506
+ sleepAfter = "3m";
1507
+ // Sleep the sandbox if no requests are made in this timeframe
1508
+ client;
1509
+ codeInterpreter;
1510
+ sandboxName = null;
1511
+ portTokens = /* @__PURE__ */ new Map();
1512
+ defaultSession = null;
1513
+ envVars = {};
1514
+ constructor(ctx, env) {
1515
+ super(ctx, env);
1516
+ this.client = new SandboxClient({
1517
+ onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
1518
+ console.log(
1519
+ `[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
1520
+ );
1521
+ },
1522
+ onError: (error, _command) => {
1523
+ console.error(`[Container] Command error: ${error}`);
1524
+ },
1525
+ port: 3e3,
1526
+ // Control plane port
1527
+ stub: this
1528
+ });
1529
+ this.codeInterpreter = new CodeInterpreter(this);
1530
+ this.ctx.blockConcurrencyWhile(async () => {
1531
+ this.sandboxName = await this.ctx.storage.get("sandboxName") || null;
1532
+ const storedTokens = await this.ctx.storage.get("portTokens") || {};
1533
+ this.portTokens = /* @__PURE__ */ new Map();
1534
+ for (const [portStr, token] of Object.entries(storedTokens)) {
1535
+ this.portTokens.set(parseInt(portStr, 10), token);
1536
+ }
1537
+ });
1538
+ }
1539
+ // RPC method to set the sandbox name
1540
+ async setSandboxName(name) {
1541
+ if (!this.sandboxName) {
1542
+ this.sandboxName = name;
1543
+ await this.ctx.storage.put("sandboxName", name);
1544
+ console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
1545
+ }
1546
+ }
1547
+ // RPC method to set environment variables
1548
+ async setEnvVars(envVars) {
1549
+ this.envVars = { ...this.envVars, ...envVars };
1550
+ if (this.defaultSession) {
1551
+ for (const [key, value] of Object.entries(envVars)) {
1552
+ const escapedValue = value.replace(/'/g, "'\\''");
1553
+ const exportCommand = `export ${key}='${escapedValue}'`;
1554
+ const result = await this.client.commands.execute(exportCommand, this.defaultSession);
1555
+ if (result.exitCode !== 0) {
1556
+ throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
1557
+ }
1558
+ }
1559
+ console.log(`[Sandbox] Updated environment variables in existing session`);
1560
+ } else {
1561
+ console.log(`[Sandbox] Updated environment variables (will be set when session is created)`);
1562
+ }
1563
+ }
1564
+ /**
1565
+ * Cleanup and destroy the sandbox container
1566
+ */
1567
+ async destroy() {
1568
+ console.log(`[Sandbox] Cleanup requested, destroying container`);
1569
+ await super.destroy();
1570
+ }
1571
+ onStart() {
1572
+ console.log("Sandbox successfully started");
1573
+ }
1574
+ onStop() {
1575
+ console.log("Sandbox successfully shut down");
1576
+ }
1577
+ onError(error) {
1578
+ console.log("Sandbox error:", error);
1579
+ }
1580
+ // Override fetch to route internal container requests to appropriate ports
1581
+ async fetch(request) {
1582
+ const url = new URL(request.url);
1583
+ if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
1584
+ const name = request.headers.get("X-Sandbox-Name");
1585
+ this.sandboxName = name;
1586
+ await this.ctx.storage.put("sandboxName", name);
1587
+ console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
1588
+ }
1589
+ const port = this.determinePort(url);
1590
+ return await this.containerFetch(request, port);
1591
+ }
1592
+ determinePort(url) {
1593
+ const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
1594
+ if (proxyMatch) {
1595
+ return parseInt(proxyMatch[1], 10);
1596
+ }
1597
+ return 3e3;
1598
+ }
1599
+ /**
1600
+ * Ensure default session exists - lazy initialization
1601
+ * This is called automatically by all public methods that need a session
1602
+ */
1603
+ async ensureDefaultSession() {
1604
+ if (!this.defaultSession) {
1605
+ const sessionId = `sandbox-${this.sandboxName || "default"}`;
1606
+ await this.client.utils.createSession({
1607
+ id: sessionId,
1608
+ env: this.envVars || {},
1609
+ cwd: "/workspace"
1610
+ });
1611
+ this.defaultSession = sessionId;
1612
+ console.log(`[Sandbox] Default session initialized: ${sessionId}`);
1613
+ }
1614
+ return this.defaultSession;
1615
+ }
1616
+ // Enhanced exec method - always returns ExecResult with optional streaming
1617
+ // This replaces the old exec method to match ISandbox interface
1618
+ async exec(command, options) {
1619
+ const session = await this.ensureDefaultSession();
1620
+ return this.execWithSession(command, session, options);
1621
+ }
1622
+ /**
1623
+ * Internal session-aware exec implementation
1624
+ * Used by both public exec() and session wrappers
1625
+ */
1626
+ async execWithSession(command, sessionId, options) {
1627
+ const startTime = Date.now();
1628
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1629
+ let timeoutId;
1630
+ try {
1631
+ if (options?.signal?.aborted) {
1632
+ throw new Error("Operation was aborted");
1633
+ }
1634
+ let result;
1635
+ if (options?.stream && options?.onOutput) {
1636
+ result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
1637
+ } else {
1638
+ const response = await this.client.commands.execute(command, sessionId);
1639
+ const duration = Date.now() - startTime;
1640
+ result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
1641
+ }
1642
+ if (options?.onComplete) {
1643
+ options.onComplete(result);
1644
+ }
1645
+ return result;
1646
+ } catch (error) {
1647
+ if (options?.onError && error instanceof Error) {
1648
+ options.onError(error);
1649
+ }
1650
+ throw error;
1651
+ } finally {
1652
+ if (timeoutId) {
1653
+ clearTimeout(timeoutId);
1654
+ }
1655
+ }
1656
+ }
1657
+ async executeWithStreaming(command, sessionId, options, startTime, timestamp) {
1658
+ let stdout = "";
1659
+ let stderr = "";
1660
+ try {
1661
+ const stream = await this.client.commands.executeStream(command, sessionId);
1662
+ for await (const event of parseSSEStream(stream)) {
1663
+ if (options.signal?.aborted) {
1664
+ throw new Error("Operation was aborted");
1665
+ }
1666
+ switch (event.type) {
1667
+ case "stdout":
1668
+ case "stderr":
1669
+ if (event.data) {
1670
+ if (event.type === "stdout") stdout += event.data;
1671
+ if (event.type === "stderr") stderr += event.data;
1672
+ if (options.onOutput) {
1673
+ options.onOutput(event.type, event.data);
1674
+ }
1675
+ }
1676
+ break;
1677
+ case "complete": {
1678
+ const duration = Date.now() - startTime;
1679
+ return {
1680
+ success: (event.exitCode ?? 0) === 0,
1681
+ exitCode: event.exitCode ?? 0,
1682
+ stdout,
1683
+ stderr,
1684
+ command,
1685
+ duration,
1686
+ timestamp,
1687
+ sessionId
1688
+ };
1689
+ }
1690
+ case "error":
1691
+ throw new Error(event.data || "Command execution failed");
1692
+ }
1693
+ }
1694
+ throw new Error("Stream ended without completion event");
1695
+ } catch (error) {
1696
+ if (options.signal?.aborted) {
1697
+ throw new Error("Operation was aborted");
1698
+ }
1699
+ throw error;
1700
+ }
1701
+ }
1702
+ mapExecuteResponseToExecResult(response, duration, sessionId) {
1703
+ return {
1704
+ success: response.success,
1705
+ exitCode: response.exitCode,
1706
+ stdout: response.stdout,
1707
+ stderr: response.stderr,
1708
+ command: response.command,
1709
+ duration,
1710
+ timestamp: response.timestamp,
1711
+ sessionId
1712
+ };
1713
+ }
1714
+ /**
1715
+ * Create a Process domain object from HTTP client DTO
1716
+ * Centralizes process object creation with bound methods
1717
+ * This eliminates duplication across startProcess, listProcesses, getProcess, and session wrappers
1718
+ */
1719
+ createProcessFromDTO(data, sessionId) {
1720
+ return {
1721
+ id: data.id,
1722
+ pid: data.pid,
1723
+ command: data.command,
1724
+ status: data.status,
1725
+ startTime: typeof data.startTime === "string" ? new Date(data.startTime) : data.startTime,
1726
+ endTime: data.endTime ? typeof data.endTime === "string" ? new Date(data.endTime) : data.endTime : void 0,
1727
+ exitCode: data.exitCode,
1728
+ sessionId,
1729
+ kill: async (signal) => {
1730
+ await this.killProcess(data.id, signal);
1731
+ },
1732
+ getStatus: async () => {
1733
+ const current = await this.getProcess(data.id);
1734
+ return current?.status || "error";
1735
+ },
1736
+ getLogs: async () => {
1737
+ const logs = await this.getProcessLogs(data.id);
1738
+ return { stdout: logs.stdout, stderr: logs.stderr };
1739
+ }
1740
+ };
1741
+ }
1742
+ // Background process management
1743
+ async startProcess(command, options, sessionId) {
1744
+ try {
1745
+ const session = sessionId ?? await this.ensureDefaultSession();
1746
+ const response = await this.client.processes.startProcess(command, session, {
1747
+ processId: options?.processId
1748
+ });
1749
+ const processObj = this.createProcessFromDTO({
1750
+ id: response.processId,
1751
+ pid: response.pid,
1752
+ command: response.command,
1753
+ status: "running",
1754
+ startTime: /* @__PURE__ */ new Date(),
1755
+ endTime: void 0,
1756
+ exitCode: void 0
1757
+ }, session);
1758
+ if (options?.onStart) {
1759
+ options.onStart(processObj);
1760
+ }
1761
+ return processObj;
1762
+ } catch (error) {
1763
+ if (options?.onError && error instanceof Error) {
1764
+ options.onError(error);
1765
+ }
1766
+ throw error;
1767
+ }
1768
+ }
1769
+ async listProcesses(sessionId) {
1770
+ const session = sessionId ?? await this.ensureDefaultSession();
1771
+ const response = await this.client.processes.listProcesses();
1772
+ return response.processes.map(
1773
+ (processData) => this.createProcessFromDTO({
1774
+ id: processData.id,
1775
+ pid: processData.pid,
1776
+ command: processData.command,
1777
+ status: processData.status,
1778
+ startTime: processData.startTime,
1779
+ endTime: processData.endTime,
1780
+ exitCode: processData.exitCode
1781
+ }, session)
1782
+ );
1783
+ }
1784
+ async getProcess(id, sessionId) {
1785
+ const session = sessionId ?? await this.ensureDefaultSession();
1786
+ const response = await this.client.processes.getProcess(id);
1787
+ if (!response.process) {
1788
+ return null;
1789
+ }
1790
+ const processData = response.process;
1791
+ return this.createProcessFromDTO({
1792
+ id: processData.id,
1793
+ pid: processData.pid,
1794
+ command: processData.command,
1795
+ status: processData.status,
1796
+ startTime: processData.startTime,
1797
+ endTime: processData.endTime,
1798
+ exitCode: processData.exitCode
1799
+ }, session);
1800
+ }
1801
+ async killProcess(id, signal, sessionId) {
1802
+ await this.client.processes.killProcess(id);
1803
+ }
1804
+ async killAllProcesses(sessionId) {
1805
+ const response = await this.client.processes.killAllProcesses();
1806
+ return response.cleanedCount;
1807
+ }
1808
+ async cleanupCompletedProcesses(sessionId) {
1809
+ return 0;
1810
+ }
1811
+ async getProcessLogs(id, sessionId) {
1812
+ const response = await this.client.processes.getProcessLogs(id);
1813
+ return {
1814
+ stdout: response.stdout,
1815
+ stderr: response.stderr,
1816
+ processId: response.processId
1817
+ };
1818
+ }
1819
+ // Streaming methods - return ReadableStream for RPC compatibility
1820
+ async execStream(command, options) {
1821
+ if (options?.signal?.aborted) {
1822
+ throw new Error("Operation was aborted");
1823
+ }
1824
+ const session = await this.ensureDefaultSession();
1825
+ return this.client.commands.executeStream(command, session);
1826
+ }
1827
+ /**
1828
+ * Internal session-aware execStream implementation
1829
+ */
1830
+ async execStreamWithSession(command, sessionId, options) {
1831
+ if (options?.signal?.aborted) {
1832
+ throw new Error("Operation was aborted");
1833
+ }
1834
+ return this.client.commands.executeStream(command, sessionId);
1835
+ }
1836
+ async streamProcessLogs(processId, options) {
1837
+ if (options?.signal?.aborted) {
1838
+ throw new Error("Operation was aborted");
1839
+ }
1840
+ return this.client.processes.streamProcessLogs(processId);
1841
+ }
1842
+ /**
1843
+ * Internal session-aware streamProcessLogs implementation
1844
+ */
1845
+ async streamProcessLogsWithSession(processId, sessionId, options) {
1846
+ if (options?.signal?.aborted) {
1847
+ throw new Error("Operation was aborted");
1848
+ }
1849
+ return this.client.processes.streamProcessLogs(processId);
1850
+ }
1851
+ async gitCheckout(repoUrl, options) {
1852
+ const session = options.sessionId ?? await this.ensureDefaultSession();
1853
+ return this.client.git.checkout(repoUrl, session, {
1854
+ branch: options.branch,
1855
+ targetDir: options.targetDir
1856
+ });
1857
+ }
1858
+ async mkdir(path, options = {}) {
1859
+ const session = options.sessionId ?? await this.ensureDefaultSession();
1860
+ return this.client.files.mkdir(path, session, { recursive: options.recursive });
1861
+ }
1862
+ async writeFile(path, content, options = {}) {
1863
+ const session = options.sessionId ?? await this.ensureDefaultSession();
1864
+ return this.client.files.writeFile(path, content, session, { encoding: options.encoding });
1865
+ }
1866
+ async deleteFile(path, sessionId) {
1867
+ const session = sessionId ?? await this.ensureDefaultSession();
1868
+ return this.client.files.deleteFile(path, session);
1869
+ }
1870
+ async renameFile(oldPath, newPath, sessionId) {
1871
+ const session = sessionId ?? await this.ensureDefaultSession();
1872
+ return this.client.files.renameFile(oldPath, newPath, session);
1873
+ }
1874
+ async moveFile(sourcePath, destinationPath, sessionId) {
1875
+ const session = sessionId ?? await this.ensureDefaultSession();
1876
+ return this.client.files.moveFile(sourcePath, destinationPath, session);
1877
+ }
1878
+ async readFile(path, options = {}) {
1879
+ const session = options.sessionId ?? await this.ensureDefaultSession();
1880
+ return this.client.files.readFile(path, session, { encoding: options.encoding });
1881
+ }
1882
+ /**
1883
+ * Stream a file from the sandbox using Server-Sent Events
1884
+ * Returns a ReadableStream that can be consumed with streamFile() or collectFile() utilities
1885
+ * @param path - Path to the file to stream
1886
+ * @param options - Optional session ID
1887
+ */
1888
+ async readFileStream(path, options = {}) {
1889
+ const session = options.sessionId ?? await this.ensureDefaultSession();
1890
+ return this.client.files.readFileStream(path, session);
1891
+ }
1892
+ async listFiles(path, options) {
1893
+ const session = await this.ensureDefaultSession();
1894
+ return this.client.files.listFiles(path, session, options);
1895
+ }
1896
+ async exposePort(port, options) {
1897
+ if (options.hostname.endsWith(".workers.dev")) {
1898
+ const errorResponse = {
1899
+ code: ErrorCode2.CUSTOM_DOMAIN_REQUIRED,
1900
+ message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
1901
+ context: { originalError: options.hostname },
1902
+ httpStatus: 400,
1903
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1904
+ };
1905
+ throw new CustomDomainRequiredError(errorResponse);
1906
+ }
1907
+ const sessionId = await this.ensureDefaultSession();
1908
+ await this.client.ports.exposePort(port, sessionId, options?.name);
1909
+ if (!this.sandboxName) {
1910
+ throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
1911
+ }
1912
+ const token = this.generatePortToken();
1913
+ this.portTokens.set(port, token);
1914
+ await this.persistPortTokens();
1915
+ const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
1916
+ logSecurityEvent("PORT_TOKEN_GENERATED", {
1917
+ port,
1918
+ sandboxId: this.sandboxName,
1919
+ tokenLength: token.length
1920
+ }, "low");
1921
+ return {
1922
+ url,
1923
+ port,
1924
+ name: options?.name
1925
+ };
1926
+ }
1927
+ async unexposePort(port) {
1928
+ if (!validatePort(port)) {
1929
+ logSecurityEvent("INVALID_PORT_UNEXPOSE", {
1930
+ port
1931
+ }, "high");
1932
+ throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
1933
+ }
1934
+ const sessionId = await this.ensureDefaultSession();
1935
+ await this.client.ports.unexposePort(port, sessionId);
1936
+ if (this.portTokens.has(port)) {
1937
+ this.portTokens.delete(port);
1938
+ await this.persistPortTokens();
1939
+ }
1940
+ logSecurityEvent("PORT_UNEXPOSED", {
1941
+ port
1942
+ }, "low");
1943
+ }
1944
+ async getExposedPorts(hostname) {
1945
+ const sessionId = await this.ensureDefaultSession();
1946
+ const response = await this.client.ports.getExposedPorts(sessionId);
1947
+ if (!this.sandboxName) {
1948
+ throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
1949
+ }
1950
+ return response.ports.map((port) => {
1951
+ const token = this.portTokens.get(port.port);
1952
+ if (!token) {
1953
+ throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
1954
+ }
1955
+ return {
1956
+ url: this.constructPreviewUrl(port.port, this.sandboxName, hostname, token),
1957
+ port: port.port,
1958
+ status: port.status
1959
+ };
1960
+ });
1961
+ }
1962
+ async isPortExposed(port) {
1963
+ try {
1964
+ const sessionId = await this.ensureDefaultSession();
1965
+ const response = await this.client.ports.getExposedPorts(sessionId);
1966
+ return response.ports.some((exposedPort) => exposedPort.port === port);
1967
+ } catch (error) {
1968
+ console.error(`[Sandbox] Error checking if port ${port} is exposed:`, error);
1969
+ return false;
1970
+ }
1971
+ }
1972
+ async validatePortToken(port, token) {
1973
+ const isExposed = await this.isPortExposed(port);
1974
+ if (!isExposed) {
1975
+ return false;
1976
+ }
1977
+ const storedToken = this.portTokens.get(port);
1978
+ if (!storedToken) {
1979
+ console.error(`Port ${port} is exposed but has no token. This indicates a bug.`);
1980
+ return false;
1981
+ }
1982
+ return storedToken === token;
1983
+ }
1984
+ generatePortToken() {
1985
+ const array = new Uint8Array(12);
1986
+ crypto.getRandomValues(array);
1987
+ const base64 = btoa(String.fromCharCode(...array));
1988
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
1989
+ }
1990
+ async persistPortTokens() {
1991
+ const tokensObj = {};
1992
+ for (const [port, token] of this.portTokens.entries()) {
1993
+ tokensObj[port.toString()] = token;
1994
+ }
1995
+ await this.ctx.storage.put("portTokens", tokensObj);
1996
+ }
1997
+ constructPreviewUrl(port, sandboxId, hostname, token) {
1998
+ if (!validatePort(port)) {
1999
+ logSecurityEvent("INVALID_PORT_REJECTED", {
2000
+ port,
2001
+ sandboxId,
2002
+ hostname
2003
+ }, "high");
2004
+ throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
2005
+ }
2006
+ let sanitizedSandboxId;
2007
+ try {
2008
+ sanitizedSandboxId = sanitizeSandboxId(sandboxId);
2009
+ } catch (error) {
2010
+ logSecurityEvent("INVALID_SANDBOX_ID_REJECTED", {
2011
+ sandboxId,
2012
+ port,
2013
+ hostname,
2014
+ error: error instanceof Error ? error.message : "Unknown error"
2015
+ }, "high");
2016
+ throw error;
2017
+ }
2018
+ const isLocalhost = isLocalhostPattern(hostname);
2019
+ if (isLocalhost) {
2020
+ const [host, portStr] = hostname.split(":");
2021
+ const mainPort = portStr || "80";
2022
+ try {
2023
+ const baseUrl = new URL(`http://${host}:${mainPort}`);
2024
+ const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${host}`;
2025
+ baseUrl.hostname = subdomainHost;
2026
+ const finalUrl = baseUrl.toString();
2027
+ logSecurityEvent("PREVIEW_URL_CONSTRUCTED", {
2028
+ port,
2029
+ sandboxId: sanitizedSandboxId,
2030
+ hostname,
2031
+ resultUrl: finalUrl,
2032
+ environment: "localhost"
2033
+ }, "low");
2034
+ return finalUrl;
2035
+ } catch (error) {
2036
+ logSecurityEvent("URL_CONSTRUCTION_FAILED", {
2037
+ port,
2038
+ sandboxId: sanitizedSandboxId,
2039
+ hostname,
2040
+ error: error instanceof Error ? error.message : "Unknown error"
2041
+ }, "high");
2042
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
2043
+ }
2044
+ }
2045
+ try {
2046
+ const protocol = "https";
2047
+ const baseUrl = new URL(`${protocol}://${hostname}`);
2048
+ const subdomainHost = `${port}-${sanitizedSandboxId}-${token}.${hostname}`;
2049
+ baseUrl.hostname = subdomainHost;
2050
+ const finalUrl = baseUrl.toString();
2051
+ logSecurityEvent("PREVIEW_URL_CONSTRUCTED", {
2052
+ port,
2053
+ sandboxId: sanitizedSandboxId,
2054
+ hostname,
2055
+ resultUrl: finalUrl,
2056
+ environment: "production"
2057
+ }, "low");
2058
+ return finalUrl;
2059
+ } catch (error) {
2060
+ logSecurityEvent("URL_CONSTRUCTION_FAILED", {
2061
+ port,
2062
+ sandboxId: sanitizedSandboxId,
2063
+ hostname,
2064
+ error: error instanceof Error ? error.message : "Unknown error"
2065
+ }, "high");
2066
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
2067
+ }
2068
+ }
2069
+ // ============================================================================
2070
+ // Session Management - Advanced Use Cases
2071
+ // ============================================================================
2072
+ /**
2073
+ * Create isolated execution session for advanced use cases
2074
+ * Returns ExecutionSession with full sandbox API bound to specific session
2075
+ */
2076
+ async createSession(options) {
2077
+ const sessionId = options?.id || `session-${Date.now()}`;
2078
+ await this.client.utils.createSession({
2079
+ id: sessionId,
2080
+ env: options?.env,
2081
+ cwd: options?.cwd
2082
+ });
2083
+ return this.getSessionWrapper(sessionId);
2084
+ }
2085
+ /**
2086
+ * Get an existing session by ID
2087
+ * Returns ExecutionSession wrapper bound to the specified session
2088
+ *
2089
+ * This is useful for retrieving sessions across different requests/contexts
2090
+ * without storing the ExecutionSession object (which has RPC lifecycle limitations)
2091
+ *
2092
+ * @param sessionId - The ID of an existing session
2093
+ * @returns ExecutionSession wrapper bound to the session
2094
+ */
2095
+ async getSession(sessionId) {
2096
+ return this.getSessionWrapper(sessionId);
2097
+ }
2098
+ /**
2099
+ * Internal helper to create ExecutionSession wrapper for a given sessionId
2100
+ * Used by both createSession and getSession
2101
+ */
2102
+ getSessionWrapper(sessionId) {
2103
+ return {
2104
+ id: sessionId,
2105
+ // Command execution - delegate to internal session-aware methods
2106
+ exec: (command, options) => this.execWithSession(command, sessionId, options),
2107
+ execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
2108
+ // Process management
2109
+ startProcess: (command, options) => this.startProcess(command, options, sessionId),
2110
+ listProcesses: () => this.listProcesses(sessionId),
2111
+ getProcess: (id) => this.getProcess(id, sessionId),
2112
+ killProcess: (id, signal) => this.killProcess(id, signal),
2113
+ killAllProcesses: () => this.killAllProcesses(),
2114
+ cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
2115
+ getProcessLogs: (id) => this.getProcessLogs(id),
2116
+ streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
2117
+ // File operations - pass sessionId via options or parameter
2118
+ writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
2119
+ readFile: (path, options) => this.readFile(path, { ...options, sessionId }),
2120
+ readFileStream: (path) => this.readFileStream(path, { sessionId }),
2121
+ mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
2122
+ deleteFile: (path) => this.deleteFile(path, sessionId),
2123
+ renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
2124
+ moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
2125
+ listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
2126
+ // Git operations
2127
+ gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
2128
+ // Environment management - needs special handling
2129
+ setEnvVars: async (envVars) => {
2130
+ try {
2131
+ for (const [key, value] of Object.entries(envVars)) {
2132
+ const escapedValue = value.replace(/'/g, "'\\''");
2133
+ const exportCommand = `export ${key}='${escapedValue}'`;
2134
+ const result = await this.client.commands.execute(exportCommand, sessionId);
2135
+ if (result.exitCode !== 0) {
2136
+ throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
2137
+ }
2138
+ }
2139
+ console.log(`[Session ${sessionId}] Environment variables updated successfully`);
2140
+ } catch (error) {
2141
+ console.error(`[Session ${sessionId}] Failed to set environment variables:`, error);
2142
+ throw error;
2143
+ }
2144
+ },
2145
+ // Code interpreter methods - delegate to sandbox's code interpreter
2146
+ createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
2147
+ runCode: async (code, options) => {
2148
+ const execution = await this.codeInterpreter.runCode(code, options);
2149
+ return execution.toJSON();
2150
+ },
2151
+ runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
2152
+ listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
2153
+ deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId)
2154
+ };
2155
+ }
2156
+ // ============================================================================
2157
+ // Code interpreter methods - delegate to CodeInterpreter wrapper
2158
+ // ============================================================================
2159
+ async createCodeContext(options) {
2160
+ return this.codeInterpreter.createCodeContext(options);
2161
+ }
2162
+ async runCode(code, options) {
2163
+ const execution = await this.codeInterpreter.runCode(code, options);
2164
+ return execution.toJSON();
2165
+ }
2166
+ async runCodeStream(code, options) {
2167
+ return this.codeInterpreter.runCodeStream(code, options);
2168
+ }
2169
+ async listCodeContexts() {
2170
+ return this.codeInterpreter.listCodeContexts();
2171
+ }
2172
+ async deleteCodeContext(contextId) {
2173
+ return this.codeInterpreter.deleteCodeContext(contextId);
2174
+ }
2175
+ };
2176
+
2177
+ // src/request-handler.ts
2178
+ async function proxyToSandbox(request, env) {
2179
+ try {
2180
+ const url = new URL(request.url);
2181
+ const routeInfo = extractSandboxRoute(url);
2182
+ if (!routeInfo) {
2183
+ return null;
2184
+ }
2185
+ const { sandboxId, port, path, token } = routeInfo;
2186
+ const sandbox = getSandbox(env.Sandbox, sandboxId);
2187
+ if (port !== 3e3) {
2188
+ const isValidToken = await sandbox.validatePortToken(port, token);
2189
+ if (!isValidToken) {
2190
+ logSecurityEvent("INVALID_TOKEN_ACCESS_BLOCKED", {
2191
+ port,
2192
+ sandboxId,
2193
+ path,
2194
+ hostname: url.hostname,
2195
+ url: request.url,
2196
+ method: request.method,
2197
+ userAgent: request.headers.get("User-Agent") || "unknown"
2198
+ }, "high");
2199
+ return new Response(
2200
+ JSON.stringify({
2201
+ error: `Access denied: Invalid token or port not exposed`,
2202
+ code: "INVALID_TOKEN"
2203
+ }),
2204
+ {
2205
+ status: 404,
2206
+ headers: {
2207
+ "Content-Type": "application/json"
2208
+ }
2209
+ }
2210
+ );
2211
+ }
2212
+ }
2213
+ let proxyUrl;
2214
+ if (port !== 3e3) {
2215
+ proxyUrl = `http://localhost:${port}${path}${url.search}`;
2216
+ } else {
2217
+ proxyUrl = `http://localhost:3000${path}${url.search}`;
2218
+ }
2219
+ const proxyRequest = new Request(proxyUrl, {
2220
+ method: request.method,
2221
+ headers: {
2222
+ ...Object.fromEntries(request.headers),
2223
+ "X-Original-URL": request.url,
2224
+ "X-Forwarded-Host": url.hostname,
2225
+ "X-Forwarded-Proto": url.protocol.replace(":", ""),
2226
+ "X-Sandbox-Name": sandboxId
2227
+ // Pass the friendly name
2228
+ },
2229
+ body: request.body,
2230
+ // @ts-expect-error - duplex required for body streaming in modern runtimes
2231
+ duplex: "half"
2232
+ });
2233
+ return sandbox.containerFetch(proxyRequest, port);
2234
+ } catch (error) {
2235
+ console.error("[Sandbox] Proxy routing error:", error);
2236
+ return new Response("Proxy routing error", { status: 500 });
2237
+ }
2238
+ }
2239
+ function extractSandboxRoute(url) {
2240
+ const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*[^.-]|[^.-])-([a-zA-Z0-9_-]{12,20})\.(.+)$/);
2241
+ if (!subdomainMatch) {
2242
+ if (url.hostname.includes("-") && url.hostname.includes(".")) {
2243
+ logSecurityEvent("MALFORMED_SUBDOMAIN_ATTEMPT", {
2244
+ hostname: url.hostname,
2245
+ url: url.toString()
2246
+ }, "medium");
2247
+ }
2248
+ return null;
2249
+ }
2250
+ const portStr = subdomainMatch[1];
2251
+ const sandboxId = subdomainMatch[2];
2252
+ const token = subdomainMatch[3];
2253
+ const domain = subdomainMatch[4];
2254
+ const port = parseInt(portStr, 10);
2255
+ if (!validatePort(port)) {
2256
+ logSecurityEvent("INVALID_PORT_IN_SUBDOMAIN", {
2257
+ port,
2258
+ portStr,
2259
+ sandboxId,
2260
+ hostname: url.hostname,
2261
+ url: url.toString()
2262
+ }, "high");
2263
+ return null;
2264
+ }
2265
+ let sanitizedSandboxId;
2266
+ try {
2267
+ sanitizedSandboxId = sanitizeSandboxId(sandboxId);
2268
+ } catch (error) {
2269
+ logSecurityEvent("INVALID_SANDBOX_ID_IN_SUBDOMAIN", {
2270
+ sandboxId,
2271
+ port,
2272
+ hostname: url.hostname,
2273
+ url: url.toString(),
2274
+ error: error instanceof Error ? error.message : "Unknown error"
2275
+ }, "high");
2276
+ return null;
2277
+ }
2278
+ if (sandboxId.length > 63) {
2279
+ logSecurityEvent("SANDBOX_ID_LENGTH_VIOLATION", {
2280
+ sandboxId,
2281
+ length: sandboxId.length,
2282
+ port,
2283
+ hostname: url.hostname
2284
+ }, "medium");
2285
+ return null;
2286
+ }
2287
+ logSecurityEvent("SANDBOX_ROUTE_EXTRACTED", {
2288
+ port,
2289
+ sandboxId: sanitizedSandboxId,
2290
+ domain,
2291
+ path: url.pathname || "/",
2292
+ hostname: url.hostname,
2293
+ hasToken: !!token
2294
+ }, "low");
2295
+ return {
2296
+ port,
2297
+ sandboxId: sanitizedSandboxId,
2298
+ path: url.pathname || "/",
2299
+ token
2300
+ };
2301
+ }
2302
+ function isLocalhostPattern(hostname) {
2303
+ if (hostname.startsWith("[")) {
2304
+ if (hostname.includes("]:")) {
2305
+ const ipv6Part = hostname.substring(0, hostname.indexOf("]:") + 1);
2306
+ return ipv6Part === "[::1]";
2307
+ } else {
2308
+ return hostname === "[::1]";
2309
+ }
2310
+ }
2311
+ if (hostname === "::1") {
2312
+ return true;
2313
+ }
2314
+ const hostPart = hostname.split(":")[0];
2315
+ return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
2316
+ }
2317
+
2318
+ export {
2319
+ CommandClient,
2320
+ FileClient,
2321
+ GitClient,
2322
+ PortClient,
2323
+ ProcessClient,
2324
+ UtilityClient,
2325
+ SandboxClient,
2326
+ proxyToSandbox,
2327
+ isLocalhostPattern,
2328
+ getSandbox,
2329
+ Sandbox
2330
+ };
2331
+ //# sourceMappingURL=chunk-HB44YO2A.js.map