@cloudflare/sandbox 0.0.0-aa00a75 → 0.0.0-aeba44f

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 (76) hide show
  1. package/CHANGELOG.md +158 -15
  2. package/Dockerfile +88 -71
  3. package/LICENSE +176 -0
  4. package/README.md +10 -5
  5. package/dist/index.d.ts +1953 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3278 -53
  8. package/dist/index.js.map +1 -1
  9. package/package.json +11 -9
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +51 -20
  13. package/src/clients/git-client.ts +9 -3
  14. package/src/clients/index.ts +16 -15
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +67 -5
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +28 -17
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +34 -21
  28. package/src/sandbox.ts +502 -151
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +6 -0
  32. package/startup.sh +1 -1
  33. package/tests/base-client.test.ts +116 -80
  34. package/tests/command-client.test.ts +149 -112
  35. package/tests/file-client.test.ts +373 -185
  36. package/tests/file-stream.test.ts +24 -20
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +260 -101
  39. package/tests/port-client.test.ts +100 -108
  40. package/tests/process-client.test.ts +204 -179
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +336 -62
  43. package/tests/sse-parser.test.ts +17 -16
  44. package/tests/utility-client.test.ts +129 -56
  45. package/tests/version.test.ts +16 -0
  46. package/tsdown.config.ts +12 -0
  47. package/vitest.config.ts +6 -6
  48. package/dist/chunk-BCJ7SF3Q.js +0 -117
  49. package/dist/chunk-BCJ7SF3Q.js.map +0 -1
  50. package/dist/chunk-BFVUNTP4.js +0 -104
  51. package/dist/chunk-BFVUNTP4.js.map +0 -1
  52. package/dist/chunk-EKSWCBCA.js +0 -86
  53. package/dist/chunk-EKSWCBCA.js.map +0 -1
  54. package/dist/chunk-HGF554LH.js +0 -2236
  55. package/dist/chunk-HGF554LH.js.map +0 -1
  56. package/dist/chunk-Z532A7QC.js +0 -78
  57. package/dist/chunk-Z532A7QC.js.map +0 -1
  58. package/dist/file-stream.d.ts +0 -43
  59. package/dist/file-stream.js +0 -9
  60. package/dist/file-stream.js.map +0 -1
  61. package/dist/interpreter.d.ts +0 -33
  62. package/dist/interpreter.js +0 -8
  63. package/dist/interpreter.js.map +0 -1
  64. package/dist/request-handler.d.ts +0 -18
  65. package/dist/request-handler.js +0 -12
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-D9K2ypln.d.ts +0 -583
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -12
  70. package/dist/sandbox.js.map +0 -1
  71. package/dist/security.d.ts +0 -31
  72. package/dist/security.js +0 -13
  73. package/dist/security.js.map +0 -1
  74. package/dist/sse-parser.d.ts +0 -28
  75. package/dist/sse-parser.js +0 -11
  76. package/dist/sse-parser.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,55 +1,3280 @@
1
- import {
2
- collectFile,
3
- streamFile
4
- } from "./chunk-BFVUNTP4.js";
5
- import {
6
- CommandClient,
7
- FileClient,
8
- GitClient,
9
- PortClient,
10
- ProcessClient,
11
- Sandbox,
12
- SandboxClient,
13
- UtilityClient,
14
- getSandbox,
15
- proxyToSandbox
16
- } from "./chunk-HGF554LH.js";
17
- import {
18
- CodeInterpreter
19
- } from "./chunk-BCJ7SF3Q.js";
20
- import "./chunk-Z532A7QC.js";
21
- import {
22
- asyncIterableToSSEStream,
23
- parseSSEStream,
24
- responseToAsyncIterable
25
- } from "./chunk-EKSWCBCA.js";
26
-
27
- // src/index.ts
28
- export * from "@repo/shared";
29
- import {
30
- isExecResult,
31
- isProcess,
32
- isProcessStatus
33
- } from "@repo/shared";
34
- export {
35
- CodeInterpreter,
36
- CommandClient,
37
- FileClient,
38
- GitClient,
39
- PortClient,
40
- ProcessClient,
41
- Sandbox,
42
- SandboxClient,
43
- UtilityClient,
44
- asyncIterableToSSEStream,
45
- collectFile,
46
- getSandbox,
47
- isExecResult,
48
- isProcess,
49
- isProcessStatus,
50
- parseSSEStream,
51
- proxyToSandbox,
52
- responseToAsyncIterable,
53
- streamFile
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { Container, getContainer, switchPort } from "@cloudflare/containers";
3
+
4
+ //#region ../shared/dist/git.js
5
+ /**
6
+ * Redact credentials from URLs for secure logging
7
+ *
8
+ * Replaces any credentials (username:password, tokens, etc.) embedded
9
+ * in URLs with ****** to prevent sensitive data exposure in logs.
10
+ * Works with URLs embedded in text (e.g., "Error: https://token@github.com/repo.git failed")
11
+ *
12
+ * @param text - String that may contain URLs with credentials
13
+ * @returns String with credentials redacted from any URLs
14
+ */
15
+ function redactCredentials(text) {
16
+ let result = text;
17
+ let pos = 0;
18
+ while (pos < result.length) {
19
+ const httpPos = result.indexOf("http://", pos);
20
+ const httpsPos = result.indexOf("https://", pos);
21
+ let protocolPos = -1;
22
+ let protocolLen = 0;
23
+ if (httpPos === -1 && httpsPos === -1) break;
24
+ if (httpPos !== -1 && (httpsPos === -1 || httpPos < httpsPos)) {
25
+ protocolPos = httpPos;
26
+ protocolLen = 7;
27
+ } else {
28
+ protocolPos = httpsPos;
29
+ protocolLen = 8;
30
+ }
31
+ const searchStart = protocolPos + protocolLen;
32
+ const atPos = result.indexOf("@", searchStart);
33
+ let urlEnd = searchStart;
34
+ while (urlEnd < result.length) {
35
+ const char = result[urlEnd];
36
+ if (/[\s"'`<>,;{}[\]]/.test(char)) break;
37
+ urlEnd++;
38
+ }
39
+ if (atPos !== -1 && atPos < urlEnd) {
40
+ result = `${result.substring(0, searchStart)}******${result.substring(atPos)}`;
41
+ pos = searchStart + 6;
42
+ } else pos = protocolPos + protocolLen;
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Sanitize data by redacting credentials from any strings
48
+ * Recursively processes objects and arrays to ensure credentials are never leaked
49
+ */
50
+ function sanitizeGitData(data) {
51
+ if (typeof data === "string") return redactCredentials(data);
52
+ if (data === null || data === void 0) return data;
53
+ if (Array.isArray(data)) return data.map((item) => sanitizeGitData(item));
54
+ if (typeof data === "object") {
55
+ const result = {};
56
+ for (const [key, value] of Object.entries(data)) result[key] = sanitizeGitData(value);
57
+ return result;
58
+ }
59
+ return data;
60
+ }
61
+ /**
62
+ * Logger wrapper that automatically sanitizes git credentials
63
+ */
64
+ var GitLogger = class GitLogger {
65
+ baseLogger;
66
+ constructor(baseLogger) {
67
+ this.baseLogger = baseLogger;
68
+ }
69
+ sanitizeContext(context) {
70
+ return context ? sanitizeGitData(context) : context;
71
+ }
72
+ sanitizeError(error) {
73
+ if (!error) return error;
74
+ const sanitized = new Error(redactCredentials(error.message));
75
+ sanitized.name = error.name;
76
+ if (error.stack) sanitized.stack = redactCredentials(error.stack);
77
+ const sanitizedRecord = sanitized;
78
+ const errorRecord = error;
79
+ for (const key of Object.keys(error)) if (key !== "message" && key !== "stack" && key !== "name") sanitizedRecord[key] = sanitizeGitData(errorRecord[key]);
80
+ return sanitized;
81
+ }
82
+ debug(message, context) {
83
+ this.baseLogger.debug(message, this.sanitizeContext(context));
84
+ }
85
+ info(message, context) {
86
+ this.baseLogger.info(message, this.sanitizeContext(context));
87
+ }
88
+ warn(message, context) {
89
+ this.baseLogger.warn(message, this.sanitizeContext(context));
90
+ }
91
+ error(message, error, context) {
92
+ this.baseLogger.error(message, this.sanitizeError(error), this.sanitizeContext(context));
93
+ }
94
+ child(context) {
95
+ const sanitized = sanitizeGitData(context);
96
+ return new GitLogger(this.baseLogger.child(sanitized));
97
+ }
98
+ };
99
+
100
+ //#endregion
101
+ //#region ../shared/dist/interpreter-types.js
102
+ var Execution = class {
103
+ code;
104
+ context;
105
+ /**
106
+ * All results from the execution
107
+ */
108
+ results = [];
109
+ /**
110
+ * Accumulated stdout and stderr
111
+ */
112
+ logs = {
113
+ stdout: [],
114
+ stderr: []
115
+ };
116
+ /**
117
+ * Execution error if any
118
+ */
119
+ error;
120
+ /**
121
+ * Execution count (for interpreter)
122
+ */
123
+ executionCount;
124
+ constructor(code, context) {
125
+ this.code = code;
126
+ this.context = context;
127
+ }
128
+ /**
129
+ * Convert to a plain object for serialization
130
+ */
131
+ toJSON() {
132
+ return {
133
+ code: this.code,
134
+ logs: this.logs,
135
+ error: this.error,
136
+ executionCount: this.executionCount,
137
+ results: this.results.map((result) => ({
138
+ text: result.text,
139
+ html: result.html,
140
+ png: result.png,
141
+ jpeg: result.jpeg,
142
+ svg: result.svg,
143
+ latex: result.latex,
144
+ markdown: result.markdown,
145
+ javascript: result.javascript,
146
+ json: result.json,
147
+ chart: result.chart,
148
+ data: result.data
149
+ }))
150
+ };
151
+ }
152
+ };
153
+ var ResultImpl = class {
154
+ raw;
155
+ constructor(raw) {
156
+ this.raw = raw;
157
+ }
158
+ get text() {
159
+ return this.raw.text || this.raw.data?.["text/plain"];
160
+ }
161
+ get html() {
162
+ return this.raw.html || this.raw.data?.["text/html"];
163
+ }
164
+ get png() {
165
+ return this.raw.png || this.raw.data?.["image/png"];
166
+ }
167
+ get jpeg() {
168
+ return this.raw.jpeg || this.raw.data?.["image/jpeg"];
169
+ }
170
+ get svg() {
171
+ return this.raw.svg || this.raw.data?.["image/svg+xml"];
172
+ }
173
+ get latex() {
174
+ return this.raw.latex || this.raw.data?.["text/latex"];
175
+ }
176
+ get markdown() {
177
+ return this.raw.markdown || this.raw.data?.["text/markdown"];
178
+ }
179
+ get javascript() {
180
+ return this.raw.javascript || this.raw.data?.["application/javascript"];
181
+ }
182
+ get json() {
183
+ return this.raw.json || this.raw.data?.["application/json"];
184
+ }
185
+ get chart() {
186
+ return this.raw.chart;
187
+ }
188
+ get data() {
189
+ return this.raw.data;
190
+ }
191
+ formats() {
192
+ const formats = [];
193
+ if (this.text) formats.push("text");
194
+ if (this.html) formats.push("html");
195
+ if (this.png) formats.push("png");
196
+ if (this.jpeg) formats.push("jpeg");
197
+ if (this.svg) formats.push("svg");
198
+ if (this.latex) formats.push("latex");
199
+ if (this.markdown) formats.push("markdown");
200
+ if (this.javascript) formats.push("javascript");
201
+ if (this.json) formats.push("json");
202
+ if (this.chart) formats.push("chart");
203
+ return formats;
204
+ }
205
+ };
206
+
207
+ //#endregion
208
+ //#region ../shared/dist/logger/types.js
209
+ /**
210
+ * Logger types for Cloudflare Sandbox SDK
211
+ *
212
+ * Provides structured, trace-aware logging across Worker, Durable Object, and Container.
213
+ */
214
+ /**
215
+ * Log levels (from most to least verbose)
216
+ */
217
+ var LogLevel;
218
+ (function(LogLevel$1) {
219
+ LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
220
+ LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
221
+ LogLevel$1[LogLevel$1["WARN"] = 2] = "WARN";
222
+ LogLevel$1[LogLevel$1["ERROR"] = 3] = "ERROR";
223
+ })(LogLevel || (LogLevel = {}));
224
+
225
+ //#endregion
226
+ //#region ../shared/dist/logger/logger.js
227
+ /**
228
+ * ANSI color codes for terminal output
229
+ */
230
+ const COLORS = {
231
+ reset: "\x1B[0m",
232
+ debug: "\x1B[36m",
233
+ info: "\x1B[32m",
234
+ warn: "\x1B[33m",
235
+ error: "\x1B[31m",
236
+ dim: "\x1B[2m"
237
+ };
238
+ /**
239
+ * CloudflareLogger implements structured logging with support for
240
+ * both JSON output (production) and pretty printing (development).
241
+ */
242
+ var CloudflareLogger = class CloudflareLogger {
243
+ baseContext;
244
+ minLevel;
245
+ pretty;
246
+ /**
247
+ * Create a new CloudflareLogger
248
+ *
249
+ * @param baseContext Base context included in all log entries
250
+ * @param minLevel Minimum log level to output (default: INFO)
251
+ * @param pretty Enable pretty printing for human-readable output (default: false)
252
+ */
253
+ constructor(baseContext, minLevel = LogLevel.INFO, pretty = false) {
254
+ this.baseContext = baseContext;
255
+ this.minLevel = minLevel;
256
+ this.pretty = pretty;
257
+ }
258
+ /**
259
+ * Log debug-level message
260
+ */
261
+ debug(message, context) {
262
+ if (this.shouldLog(LogLevel.DEBUG)) {
263
+ const logData = this.buildLogData("debug", message, context);
264
+ this.output(console.log, logData);
265
+ }
266
+ }
267
+ /**
268
+ * Log info-level message
269
+ */
270
+ info(message, context) {
271
+ if (this.shouldLog(LogLevel.INFO)) {
272
+ const logData = this.buildLogData("info", message, context);
273
+ this.output(console.log, logData);
274
+ }
275
+ }
276
+ /**
277
+ * Log warning-level message
278
+ */
279
+ warn(message, context) {
280
+ if (this.shouldLog(LogLevel.WARN)) {
281
+ const logData = this.buildLogData("warn", message, context);
282
+ this.output(console.warn, logData);
283
+ }
284
+ }
285
+ /**
286
+ * Log error-level message
287
+ */
288
+ error(message, error, context) {
289
+ if (this.shouldLog(LogLevel.ERROR)) {
290
+ const logData = this.buildLogData("error", message, context, error);
291
+ this.output(console.error, logData);
292
+ }
293
+ }
294
+ /**
295
+ * Create a child logger with additional context
296
+ */
297
+ child(context) {
298
+ return new CloudflareLogger({
299
+ ...this.baseContext,
300
+ ...context
301
+ }, this.minLevel, this.pretty);
302
+ }
303
+ /**
304
+ * Check if a log level should be output
305
+ */
306
+ shouldLog(level) {
307
+ return level >= this.minLevel;
308
+ }
309
+ /**
310
+ * Build log data object
311
+ */
312
+ buildLogData(level, message, context, error) {
313
+ const logData = {
314
+ level,
315
+ msg: message,
316
+ ...this.baseContext,
317
+ ...context,
318
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
319
+ };
320
+ if (error) logData.error = {
321
+ message: error.message,
322
+ stack: error.stack,
323
+ name: error.name
324
+ };
325
+ return logData;
326
+ }
327
+ /**
328
+ * Output log data to console (pretty or JSON)
329
+ */
330
+ output(consoleFn, data) {
331
+ if (this.pretty) this.outputPretty(consoleFn, data);
332
+ else this.outputJson(consoleFn, data);
333
+ }
334
+ /**
335
+ * Output as JSON (production)
336
+ */
337
+ outputJson(consoleFn, data) {
338
+ consoleFn(JSON.stringify(data));
339
+ }
340
+ /**
341
+ * Output as pretty-printed, colored text (development)
342
+ *
343
+ * Format: LEVEL [component] message (trace: tr_...) {context}
344
+ * Example: INFO [sandbox-do] Command started (trace: tr_7f3a9b2c) {commandId: "cmd-123"}
345
+ */
346
+ outputPretty(consoleFn, data) {
347
+ const { level, msg, timestamp, traceId, component, sandboxId, sessionId, processId, commandId, operation, duration, error,...rest } = data;
348
+ const levelStr = String(level || "INFO").toUpperCase();
349
+ const levelColor = this.getLevelColor(levelStr);
350
+ const componentBadge = component ? `[${component}]` : "";
351
+ const traceIdShort = traceId ? String(traceId).substring(0, 12) : "";
352
+ let logLine = `${levelColor}${levelStr.padEnd(5)}${COLORS.reset} ${componentBadge} ${msg}`;
353
+ if (traceIdShort) logLine += ` ${COLORS.dim}(trace: ${traceIdShort})${COLORS.reset}`;
354
+ const contextFields = [];
355
+ if (operation) contextFields.push(`operation: ${operation}`);
356
+ if (commandId) contextFields.push(`commandId: ${String(commandId).substring(0, 12)}`);
357
+ if (sandboxId) contextFields.push(`sandboxId: ${sandboxId}`);
358
+ if (sessionId) contextFields.push(`sessionId: ${String(sessionId).substring(0, 12)}`);
359
+ if (processId) contextFields.push(`processId: ${processId}`);
360
+ if (duration !== void 0) contextFields.push(`duration: ${duration}ms`);
361
+ if (contextFields.length > 0) logLine += ` ${COLORS.dim}{${contextFields.join(", ")}}${COLORS.reset}`;
362
+ consoleFn(logLine);
363
+ if (error && typeof error === "object") {
364
+ const errorObj = error;
365
+ if (errorObj.message) consoleFn(` ${COLORS.error}Error: ${errorObj.message}${COLORS.reset}`);
366
+ if (errorObj.stack) consoleFn(` ${COLORS.dim}${errorObj.stack}${COLORS.reset}`);
367
+ }
368
+ if (Object.keys(rest).length > 0) consoleFn(` ${COLORS.dim}${JSON.stringify(rest, null, 2)}${COLORS.reset}`);
369
+ }
370
+ /**
371
+ * Get ANSI color code for log level
372
+ */
373
+ getLevelColor(level) {
374
+ switch (level.toLowerCase()) {
375
+ case "debug": return COLORS.debug;
376
+ case "info": return COLORS.info;
377
+ case "warn": return COLORS.warn;
378
+ case "error": return COLORS.error;
379
+ default: return COLORS.reset;
380
+ }
381
+ }
382
+ };
383
+
384
+ //#endregion
385
+ //#region ../shared/dist/logger/trace-context.js
386
+ /**
387
+ * Trace context utilities for request correlation
388
+ *
389
+ * Trace IDs enable correlating logs across distributed components:
390
+ * Worker → Durable Object → Container → back
391
+ *
392
+ * The trace ID is propagated via the X-Trace-Id HTTP header.
393
+ */
394
+ /**
395
+ * Utility for managing trace context across distributed components
396
+ */
397
+ var TraceContext = class TraceContext {
398
+ /**
399
+ * HTTP header name for trace ID propagation
400
+ */
401
+ static TRACE_HEADER = "X-Trace-Id";
402
+ /**
403
+ * Generate a new trace ID
404
+ *
405
+ * Format: "tr_" + 16 random hex characters
406
+ * Example: "tr_7f3a9b2c4e5d6f1a"
407
+ *
408
+ * @returns Newly generated trace ID
409
+ */
410
+ static generate() {
411
+ return `tr_${crypto.randomUUID().replace(/-/g, "").substring(0, 16)}`;
412
+ }
413
+ /**
414
+ * Extract trace ID from HTTP request headers
415
+ *
416
+ * @param headers Request headers
417
+ * @returns Trace ID if present, null otherwise
418
+ */
419
+ static fromHeaders(headers) {
420
+ return headers.get(TraceContext.TRACE_HEADER);
421
+ }
422
+ /**
423
+ * Create headers object with trace ID for outgoing requests
424
+ *
425
+ * @param traceId Trace ID to include
426
+ * @returns Headers object with X-Trace-Id set
427
+ */
428
+ static toHeaders(traceId) {
429
+ return { [TraceContext.TRACE_HEADER]: traceId };
430
+ }
431
+ /**
432
+ * Get the header name used for trace ID propagation
433
+ *
434
+ * @returns Header name ("X-Trace-Id")
435
+ */
436
+ static getHeaderName() {
437
+ return TraceContext.TRACE_HEADER;
438
+ }
439
+ };
440
+
441
+ //#endregion
442
+ //#region ../shared/dist/logger/index.js
443
+ /**
444
+ * Create a no-op logger for testing
445
+ *
446
+ * Returns a logger that implements the Logger interface but does nothing.
447
+ * Useful for tests that don't need actual logging output.
448
+ *
449
+ * @returns No-op logger instance
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * // In tests
454
+ * const client = new HttpClient({
455
+ * baseUrl: 'http://test.com',
456
+ * logger: createNoOpLogger() // Optional - tests can enable real logging if needed
457
+ * });
458
+ * ```
459
+ */
460
+ function createNoOpLogger() {
461
+ return {
462
+ debug: () => {},
463
+ info: () => {},
464
+ warn: () => {},
465
+ error: () => {},
466
+ child: () => createNoOpLogger()
467
+ };
468
+ }
469
+ /**
470
+ * AsyncLocalStorage for logger context
471
+ *
472
+ * Enables implicit logger propagation throughout the call stack without
473
+ * explicit parameter passing. The logger is stored per async context.
474
+ */
475
+ const loggerStorage = new AsyncLocalStorage();
476
+ /**
477
+ * Get the current logger from AsyncLocalStorage
478
+ *
479
+ * @throws Error if no logger is initialized in the current async context
480
+ * @returns Current logger instance
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * function someHelperFunction() {
485
+ * const logger = getLogger(); // Automatically has all context!
486
+ * logger.info('Helper called');
487
+ * }
488
+ * ```
489
+ */
490
+ function getLogger() {
491
+ const logger = loggerStorage.getStore();
492
+ if (!logger) throw new Error("Logger not initialized in async context. Ensure runWithLogger() is called at the entry point (e.g., fetch handler).");
493
+ return logger;
494
+ }
495
+ /**
496
+ * Run a function with a logger stored in AsyncLocalStorage
497
+ *
498
+ * The logger is available to all code within the function via getLogger().
499
+ * This is typically called at request entry points (fetch handler) and when
500
+ * creating child loggers with additional context.
501
+ *
502
+ * @param logger Logger instance to store in context
503
+ * @param fn Function to execute with logger context
504
+ * @returns Result of the function
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * // At request entry point
509
+ * async fetch(request: Request): Promise<Response> {
510
+ * const logger = createLogger({ component: 'sandbox-do', traceId: 'tr_abc' });
511
+ * return runWithLogger(logger, async () => {
512
+ * return await this.handleRequest(request);
513
+ * });
514
+ * }
515
+ *
516
+ * // When adding operation context
517
+ * async exec(command: string) {
518
+ * const logger = getLogger().child({ operation: 'exec', commandId: 'cmd-123' });
519
+ * return runWithLogger(logger, async () => {
520
+ * logger.info('Command started');
521
+ * await this.executeCommand(command); // Nested calls get the child logger
522
+ * logger.info('Command completed');
523
+ * });
524
+ * }
525
+ * ```
526
+ */
527
+ function runWithLogger(logger, fn) {
528
+ return loggerStorage.run(logger, fn);
529
+ }
530
+ /**
531
+ * Create a new logger instance
532
+ *
533
+ * @param context Base context for the logger. Must include 'component'.
534
+ * TraceId will be auto-generated if not provided.
535
+ * @returns New logger instance
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * // In Durable Object
540
+ * const logger = createLogger({
541
+ * component: 'sandbox-do',
542
+ * traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
543
+ * sandboxId: this.id
544
+ * });
545
+ *
546
+ * // In Container
547
+ * const logger = createLogger({
548
+ * component: 'container',
549
+ * traceId: TraceContext.fromHeaders(request.headers)!,
550
+ * sessionId: this.id
551
+ * });
552
+ * ```
553
+ */
554
+ function createLogger(context) {
555
+ const minLevel = getLogLevelFromEnv();
556
+ const pretty = isPrettyPrintEnabled();
557
+ return new CloudflareLogger({
558
+ ...context,
559
+ traceId: context.traceId || TraceContext.generate(),
560
+ component: context.component
561
+ }, minLevel, pretty);
562
+ }
563
+ /**
564
+ * Get log level from environment variable
565
+ *
566
+ * Checks SANDBOX_LOG_LEVEL env var, falls back to default based on environment.
567
+ * Default: 'debug' for development, 'info' for production
568
+ */
569
+ function getLogLevelFromEnv() {
570
+ switch ((getEnvVar("SANDBOX_LOG_LEVEL") || "info").toLowerCase()) {
571
+ case "debug": return LogLevel.DEBUG;
572
+ case "info": return LogLevel.INFO;
573
+ case "warn": return LogLevel.WARN;
574
+ case "error": return LogLevel.ERROR;
575
+ default: return LogLevel.INFO;
576
+ }
577
+ }
578
+ /**
579
+ * Check if pretty printing should be enabled
580
+ *
581
+ * Checks SANDBOX_LOG_FORMAT env var, falls back to auto-detection:
582
+ * - Local development: pretty (colored, human-readable)
583
+ * - Production: json (structured)
584
+ */
585
+ function isPrettyPrintEnabled() {
586
+ const format = getEnvVar("SANDBOX_LOG_FORMAT");
587
+ if (format) return format.toLowerCase() === "pretty";
588
+ return false;
589
+ }
590
+ /**
591
+ * Get environment variable value
592
+ *
593
+ * Supports both Node.js (process.env) and Bun (Bun.env)
594
+ */
595
+ function getEnvVar(name) {
596
+ if (typeof process !== "undefined" && process.env) return process.env[name];
597
+ if (typeof Bun !== "undefined") {
598
+ const bunEnv = Bun.env;
599
+ if (bunEnv) return bunEnv[name];
600
+ }
601
+ }
602
+
603
+ //#endregion
604
+ //#region ../shared/dist/types.js
605
+ function isExecResult(value) {
606
+ return value && typeof value.success === "boolean" && typeof value.exitCode === "number" && typeof value.stdout === "string" && typeof value.stderr === "string";
607
+ }
608
+ function isProcess(value) {
609
+ return value && typeof value.id === "string" && typeof value.command === "string" && typeof value.status === "string";
610
+ }
611
+ function isProcessStatus(value) {
612
+ return [
613
+ "starting",
614
+ "running",
615
+ "completed",
616
+ "failed",
617
+ "killed",
618
+ "error"
619
+ ].includes(value);
620
+ }
621
+
622
+ //#endregion
623
+ //#region ../shared/dist/errors/codes.js
624
+ /**
625
+ * Centralized error code registry
626
+ * Each code maps to a specific error type with consistent semantics
627
+ */
628
+ const ErrorCode = {
629
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
630
+ PERMISSION_DENIED: "PERMISSION_DENIED",
631
+ FILE_EXISTS: "FILE_EXISTS",
632
+ IS_DIRECTORY: "IS_DIRECTORY",
633
+ NOT_DIRECTORY: "NOT_DIRECTORY",
634
+ NO_SPACE: "NO_SPACE",
635
+ TOO_MANY_FILES: "TOO_MANY_FILES",
636
+ RESOURCE_BUSY: "RESOURCE_BUSY",
637
+ READ_ONLY: "READ_ONLY",
638
+ NAME_TOO_LONG: "NAME_TOO_LONG",
639
+ TOO_MANY_LINKS: "TOO_MANY_LINKS",
640
+ FILESYSTEM_ERROR: "FILESYSTEM_ERROR",
641
+ COMMAND_NOT_FOUND: "COMMAND_NOT_FOUND",
642
+ COMMAND_PERMISSION_DENIED: "COMMAND_PERMISSION_DENIED",
643
+ INVALID_COMMAND: "INVALID_COMMAND",
644
+ COMMAND_EXECUTION_ERROR: "COMMAND_EXECUTION_ERROR",
645
+ STREAM_START_ERROR: "STREAM_START_ERROR",
646
+ PROCESS_NOT_FOUND: "PROCESS_NOT_FOUND",
647
+ PROCESS_PERMISSION_DENIED: "PROCESS_PERMISSION_DENIED",
648
+ PROCESS_ERROR: "PROCESS_ERROR",
649
+ PORT_ALREADY_EXPOSED: "PORT_ALREADY_EXPOSED",
650
+ PORT_IN_USE: "PORT_IN_USE",
651
+ PORT_NOT_EXPOSED: "PORT_NOT_EXPOSED",
652
+ INVALID_PORT_NUMBER: "INVALID_PORT_NUMBER",
653
+ INVALID_PORT: "INVALID_PORT",
654
+ SERVICE_NOT_RESPONDING: "SERVICE_NOT_RESPONDING",
655
+ PORT_OPERATION_ERROR: "PORT_OPERATION_ERROR",
656
+ CUSTOM_DOMAIN_REQUIRED: "CUSTOM_DOMAIN_REQUIRED",
657
+ GIT_REPOSITORY_NOT_FOUND: "GIT_REPOSITORY_NOT_FOUND",
658
+ GIT_BRANCH_NOT_FOUND: "GIT_BRANCH_NOT_FOUND",
659
+ GIT_AUTH_FAILED: "GIT_AUTH_FAILED",
660
+ GIT_NETWORK_ERROR: "GIT_NETWORK_ERROR",
661
+ INVALID_GIT_URL: "INVALID_GIT_URL",
662
+ GIT_CLONE_FAILED: "GIT_CLONE_FAILED",
663
+ GIT_CHECKOUT_FAILED: "GIT_CHECKOUT_FAILED",
664
+ GIT_OPERATION_FAILED: "GIT_OPERATION_FAILED",
665
+ INTERPRETER_NOT_READY: "INTERPRETER_NOT_READY",
666
+ CONTEXT_NOT_FOUND: "CONTEXT_NOT_FOUND",
667
+ CODE_EXECUTION_ERROR: "CODE_EXECUTION_ERROR",
668
+ VALIDATION_FAILED: "VALIDATION_FAILED",
669
+ INVALID_JSON_RESPONSE: "INVALID_JSON_RESPONSE",
670
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
671
+ INTERNAL_ERROR: "INTERNAL_ERROR"
672
+ };
673
+
674
+ //#endregion
675
+ //#region ../shared/dist/errors/status-map.js
676
+ /**
677
+ * Maps error codes to HTTP status codes
678
+ * Centralized mapping ensures consistency across SDK
679
+ */
680
+ const ERROR_STATUS_MAP = {
681
+ [ErrorCode.FILE_NOT_FOUND]: 404,
682
+ [ErrorCode.COMMAND_NOT_FOUND]: 404,
683
+ [ErrorCode.PROCESS_NOT_FOUND]: 404,
684
+ [ErrorCode.PORT_NOT_EXPOSED]: 404,
685
+ [ErrorCode.GIT_REPOSITORY_NOT_FOUND]: 404,
686
+ [ErrorCode.GIT_BRANCH_NOT_FOUND]: 404,
687
+ [ErrorCode.CONTEXT_NOT_FOUND]: 404,
688
+ [ErrorCode.IS_DIRECTORY]: 400,
689
+ [ErrorCode.NOT_DIRECTORY]: 400,
690
+ [ErrorCode.INVALID_COMMAND]: 400,
691
+ [ErrorCode.INVALID_PORT_NUMBER]: 400,
692
+ [ErrorCode.INVALID_PORT]: 400,
693
+ [ErrorCode.INVALID_GIT_URL]: 400,
694
+ [ErrorCode.CUSTOM_DOMAIN_REQUIRED]: 400,
695
+ [ErrorCode.INVALID_JSON_RESPONSE]: 400,
696
+ [ErrorCode.NAME_TOO_LONG]: 400,
697
+ [ErrorCode.VALIDATION_FAILED]: 400,
698
+ [ErrorCode.GIT_AUTH_FAILED]: 401,
699
+ [ErrorCode.PERMISSION_DENIED]: 403,
700
+ [ErrorCode.COMMAND_PERMISSION_DENIED]: 403,
701
+ [ErrorCode.PROCESS_PERMISSION_DENIED]: 403,
702
+ [ErrorCode.READ_ONLY]: 403,
703
+ [ErrorCode.FILE_EXISTS]: 409,
704
+ [ErrorCode.PORT_ALREADY_EXPOSED]: 409,
705
+ [ErrorCode.PORT_IN_USE]: 409,
706
+ [ErrorCode.RESOURCE_BUSY]: 409,
707
+ [ErrorCode.SERVICE_NOT_RESPONDING]: 502,
708
+ [ErrorCode.GIT_NETWORK_ERROR]: 502,
709
+ [ErrorCode.INTERPRETER_NOT_READY]: 503,
710
+ [ErrorCode.NO_SPACE]: 500,
711
+ [ErrorCode.TOO_MANY_FILES]: 500,
712
+ [ErrorCode.TOO_MANY_LINKS]: 500,
713
+ [ErrorCode.FILESYSTEM_ERROR]: 500,
714
+ [ErrorCode.COMMAND_EXECUTION_ERROR]: 500,
715
+ [ErrorCode.STREAM_START_ERROR]: 500,
716
+ [ErrorCode.PROCESS_ERROR]: 500,
717
+ [ErrorCode.PORT_OPERATION_ERROR]: 500,
718
+ [ErrorCode.GIT_CLONE_FAILED]: 500,
719
+ [ErrorCode.GIT_CHECKOUT_FAILED]: 500,
720
+ [ErrorCode.GIT_OPERATION_FAILED]: 500,
721
+ [ErrorCode.CODE_EXECUTION_ERROR]: 500,
722
+ [ErrorCode.UNKNOWN_ERROR]: 500,
723
+ [ErrorCode.INTERNAL_ERROR]: 500
724
+ };
725
+
726
+ //#endregion
727
+ //#region src/errors/classes.ts
728
+ /**
729
+ * Base SDK error that wraps ErrorResponse
730
+ * Preserves all error information from container
731
+ */
732
+ var SandboxError = class extends Error {
733
+ constructor(errorResponse) {
734
+ super(errorResponse.message);
735
+ this.errorResponse = errorResponse;
736
+ this.name = "SandboxError";
737
+ }
738
+ get code() {
739
+ return this.errorResponse.code;
740
+ }
741
+ get context() {
742
+ return this.errorResponse.context;
743
+ }
744
+ get httpStatus() {
745
+ return this.errorResponse.httpStatus;
746
+ }
747
+ get operation() {
748
+ return this.errorResponse.operation;
749
+ }
750
+ get suggestion() {
751
+ return this.errorResponse.suggestion;
752
+ }
753
+ get timestamp() {
754
+ return this.errorResponse.timestamp;
755
+ }
756
+ get documentation() {
757
+ return this.errorResponse.documentation;
758
+ }
759
+ toJSON() {
760
+ return {
761
+ name: this.name,
762
+ message: this.message,
763
+ code: this.code,
764
+ context: this.context,
765
+ httpStatus: this.httpStatus,
766
+ operation: this.operation,
767
+ suggestion: this.suggestion,
768
+ timestamp: this.timestamp,
769
+ documentation: this.documentation,
770
+ stack: this.stack
771
+ };
772
+ }
773
+ };
774
+ /**
775
+ * Error thrown when a file or directory is not found
776
+ */
777
+ var FileNotFoundError = class extends SandboxError {
778
+ constructor(errorResponse) {
779
+ super(errorResponse);
780
+ this.name = "FileNotFoundError";
781
+ }
782
+ get path() {
783
+ return this.context.path;
784
+ }
785
+ };
786
+ /**
787
+ * Error thrown when a file already exists
788
+ */
789
+ var FileExistsError = class extends SandboxError {
790
+ constructor(errorResponse) {
791
+ super(errorResponse);
792
+ this.name = "FileExistsError";
793
+ }
794
+ get path() {
795
+ return this.context.path;
796
+ }
797
+ };
798
+ /**
799
+ * Generic file system error (permissions, disk full, etc.)
800
+ */
801
+ var FileSystemError = class extends SandboxError {
802
+ constructor(errorResponse) {
803
+ super(errorResponse);
804
+ this.name = "FileSystemError";
805
+ }
806
+ get path() {
807
+ return this.context.path;
808
+ }
809
+ get stderr() {
810
+ return this.context.stderr;
811
+ }
812
+ get exitCode() {
813
+ return this.context.exitCode;
814
+ }
815
+ };
816
+ /**
817
+ * Error thrown when permission is denied
818
+ */
819
+ var PermissionDeniedError = class extends SandboxError {
820
+ constructor(errorResponse) {
821
+ super(errorResponse);
822
+ this.name = "PermissionDeniedError";
823
+ }
824
+ get path() {
825
+ return this.context.path;
826
+ }
827
+ };
828
+ /**
829
+ * Error thrown when a command is not found
830
+ */
831
+ var CommandNotFoundError = class extends SandboxError {
832
+ constructor(errorResponse) {
833
+ super(errorResponse);
834
+ this.name = "CommandNotFoundError";
835
+ }
836
+ get command() {
837
+ return this.context.command;
838
+ }
839
+ };
840
+ /**
841
+ * Generic command execution error
842
+ */
843
+ var CommandError = class extends SandboxError {
844
+ constructor(errorResponse) {
845
+ super(errorResponse);
846
+ this.name = "CommandError";
847
+ }
848
+ get command() {
849
+ return this.context.command;
850
+ }
851
+ get exitCode() {
852
+ return this.context.exitCode;
853
+ }
854
+ get stdout() {
855
+ return this.context.stdout;
856
+ }
857
+ get stderr() {
858
+ return this.context.stderr;
859
+ }
860
+ };
861
+ /**
862
+ * Error thrown when a process is not found
863
+ */
864
+ var ProcessNotFoundError = class extends SandboxError {
865
+ constructor(errorResponse) {
866
+ super(errorResponse);
867
+ this.name = "ProcessNotFoundError";
868
+ }
869
+ get processId() {
870
+ return this.context.processId;
871
+ }
872
+ };
873
+ /**
874
+ * Generic process error
875
+ */
876
+ var ProcessError = class extends SandboxError {
877
+ constructor(errorResponse) {
878
+ super(errorResponse);
879
+ this.name = "ProcessError";
880
+ }
881
+ get processId() {
882
+ return this.context.processId;
883
+ }
884
+ get pid() {
885
+ return this.context.pid;
886
+ }
887
+ get exitCode() {
888
+ return this.context.exitCode;
889
+ }
890
+ get stderr() {
891
+ return this.context.stderr;
892
+ }
893
+ };
894
+ /**
895
+ * Error thrown when a port is already exposed
896
+ */
897
+ var PortAlreadyExposedError = class extends SandboxError {
898
+ constructor(errorResponse) {
899
+ super(errorResponse);
900
+ this.name = "PortAlreadyExposedError";
901
+ }
902
+ get port() {
903
+ return this.context.port;
904
+ }
905
+ get portName() {
906
+ return this.context.portName;
907
+ }
908
+ };
909
+ /**
910
+ * Error thrown when a port is not exposed
911
+ */
912
+ var PortNotExposedError = class extends SandboxError {
913
+ constructor(errorResponse) {
914
+ super(errorResponse);
915
+ this.name = "PortNotExposedError";
916
+ }
917
+ get port() {
918
+ return this.context.port;
919
+ }
920
+ };
921
+ /**
922
+ * Error thrown when a port number is invalid
923
+ */
924
+ var InvalidPortError = class extends SandboxError {
925
+ constructor(errorResponse) {
926
+ super(errorResponse);
927
+ this.name = "InvalidPortError";
928
+ }
929
+ get port() {
930
+ return this.context.port;
931
+ }
932
+ get reason() {
933
+ return this.context.reason;
934
+ }
935
+ };
936
+ /**
937
+ * Error thrown when a service on a port is not responding
938
+ */
939
+ var ServiceNotRespondingError = class extends SandboxError {
940
+ constructor(errorResponse) {
941
+ super(errorResponse);
942
+ this.name = "ServiceNotRespondingError";
943
+ }
944
+ get port() {
945
+ return this.context.port;
946
+ }
947
+ get portName() {
948
+ return this.context.portName;
949
+ }
950
+ };
951
+ /**
952
+ * Error thrown when a port is already in use
953
+ */
954
+ var PortInUseError = class extends SandboxError {
955
+ constructor(errorResponse) {
956
+ super(errorResponse);
957
+ this.name = "PortInUseError";
958
+ }
959
+ get port() {
960
+ return this.context.port;
961
+ }
962
+ };
963
+ /**
964
+ * Generic port operation error
965
+ */
966
+ var PortError = class extends SandboxError {
967
+ constructor(errorResponse) {
968
+ super(errorResponse);
969
+ this.name = "PortError";
970
+ }
971
+ get port() {
972
+ return this.context.port;
973
+ }
974
+ get portName() {
975
+ return this.context.portName;
976
+ }
977
+ get stderr() {
978
+ return this.context.stderr;
979
+ }
980
+ };
981
+ /**
982
+ * Error thrown when port exposure requires a custom domain
983
+ */
984
+ var CustomDomainRequiredError = class extends SandboxError {
985
+ constructor(errorResponse) {
986
+ super(errorResponse);
987
+ this.name = "CustomDomainRequiredError";
988
+ }
989
+ };
990
+ /**
991
+ * Error thrown when a git repository is not found
992
+ */
993
+ var GitRepositoryNotFoundError = class extends SandboxError {
994
+ constructor(errorResponse) {
995
+ super(errorResponse);
996
+ this.name = "GitRepositoryNotFoundError";
997
+ }
998
+ get repository() {
999
+ return this.context.repository;
1000
+ }
1001
+ };
1002
+ /**
1003
+ * Error thrown when git authentication fails
1004
+ */
1005
+ var GitAuthenticationError = class extends SandboxError {
1006
+ constructor(errorResponse) {
1007
+ super(errorResponse);
1008
+ this.name = "GitAuthenticationError";
1009
+ }
1010
+ get repository() {
1011
+ return this.context.repository;
1012
+ }
1013
+ };
1014
+ /**
1015
+ * Error thrown when a git branch is not found
1016
+ */
1017
+ var GitBranchNotFoundError = class extends SandboxError {
1018
+ constructor(errorResponse) {
1019
+ super(errorResponse);
1020
+ this.name = "GitBranchNotFoundError";
1021
+ }
1022
+ get branch() {
1023
+ return this.context.branch;
1024
+ }
1025
+ get repository() {
1026
+ return this.context.repository;
1027
+ }
1028
+ };
1029
+ /**
1030
+ * Error thrown when a git network operation fails
1031
+ */
1032
+ var GitNetworkError = class extends SandboxError {
1033
+ constructor(errorResponse) {
1034
+ super(errorResponse);
1035
+ this.name = "GitNetworkError";
1036
+ }
1037
+ get repository() {
1038
+ return this.context.repository;
1039
+ }
1040
+ get branch() {
1041
+ return this.context.branch;
1042
+ }
1043
+ get targetDir() {
1044
+ return this.context.targetDir;
1045
+ }
1046
+ };
1047
+ /**
1048
+ * Error thrown when git clone fails
1049
+ */
1050
+ var GitCloneError = class extends SandboxError {
1051
+ constructor(errorResponse) {
1052
+ super(errorResponse);
1053
+ this.name = "GitCloneError";
1054
+ }
1055
+ get repository() {
1056
+ return this.context.repository;
1057
+ }
1058
+ get targetDir() {
1059
+ return this.context.targetDir;
1060
+ }
1061
+ get stderr() {
1062
+ return this.context.stderr;
1063
+ }
1064
+ get exitCode() {
1065
+ return this.context.exitCode;
1066
+ }
54
1067
  };
1068
+ /**
1069
+ * Error thrown when git checkout fails
1070
+ */
1071
+ var GitCheckoutError = class extends SandboxError {
1072
+ constructor(errorResponse) {
1073
+ super(errorResponse);
1074
+ this.name = "GitCheckoutError";
1075
+ }
1076
+ get branch() {
1077
+ return this.context.branch;
1078
+ }
1079
+ get repository() {
1080
+ return this.context.repository;
1081
+ }
1082
+ get stderr() {
1083
+ return this.context.stderr;
1084
+ }
1085
+ };
1086
+ /**
1087
+ * Error thrown when a git URL is invalid
1088
+ */
1089
+ var InvalidGitUrlError = class extends SandboxError {
1090
+ constructor(errorResponse) {
1091
+ super(errorResponse);
1092
+ this.name = "InvalidGitUrlError";
1093
+ }
1094
+ get validationErrors() {
1095
+ return this.context.validationErrors;
1096
+ }
1097
+ };
1098
+ /**
1099
+ * Generic git operation error
1100
+ */
1101
+ var GitError = class extends SandboxError {
1102
+ constructor(errorResponse) {
1103
+ super(errorResponse);
1104
+ this.name = "GitError";
1105
+ }
1106
+ get repository() {
1107
+ return this.context.repository;
1108
+ }
1109
+ get branch() {
1110
+ return this.context.branch;
1111
+ }
1112
+ get targetDir() {
1113
+ return this.context.targetDir;
1114
+ }
1115
+ get stderr() {
1116
+ return this.context.stderr;
1117
+ }
1118
+ get exitCode() {
1119
+ return this.context.exitCode;
1120
+ }
1121
+ };
1122
+ /**
1123
+ * Error thrown when interpreter is not ready
1124
+ */
1125
+ var InterpreterNotReadyError = class extends SandboxError {
1126
+ constructor(errorResponse) {
1127
+ super(errorResponse);
1128
+ this.name = "InterpreterNotReadyError";
1129
+ }
1130
+ get retryAfter() {
1131
+ return this.context.retryAfter;
1132
+ }
1133
+ get progress() {
1134
+ return this.context.progress;
1135
+ }
1136
+ };
1137
+ /**
1138
+ * Error thrown when a context is not found
1139
+ */
1140
+ var ContextNotFoundError = class extends SandboxError {
1141
+ constructor(errorResponse) {
1142
+ super(errorResponse);
1143
+ this.name = "ContextNotFoundError";
1144
+ }
1145
+ get contextId() {
1146
+ return this.context.contextId;
1147
+ }
1148
+ };
1149
+ /**
1150
+ * Error thrown when code execution fails
1151
+ */
1152
+ var CodeExecutionError = class extends SandboxError {
1153
+ constructor(errorResponse) {
1154
+ super(errorResponse);
1155
+ this.name = "CodeExecutionError";
1156
+ }
1157
+ get contextId() {
1158
+ return this.context.contextId;
1159
+ }
1160
+ get ename() {
1161
+ return this.context.ename;
1162
+ }
1163
+ get evalue() {
1164
+ return this.context.evalue;
1165
+ }
1166
+ get traceback() {
1167
+ return this.context.traceback;
1168
+ }
1169
+ };
1170
+ /**
1171
+ * Error thrown when validation fails
1172
+ */
1173
+ var ValidationFailedError = class extends SandboxError {
1174
+ constructor(errorResponse) {
1175
+ super(errorResponse);
1176
+ this.name = "ValidationFailedError";
1177
+ }
1178
+ get validationErrors() {
1179
+ return this.context.validationErrors;
1180
+ }
1181
+ };
1182
+
1183
+ //#endregion
1184
+ //#region src/errors/adapter.ts
1185
+ /**
1186
+ * Convert ErrorResponse to appropriate Error class
1187
+ * Simple switch statement - we trust the container sends correct context
1188
+ */
1189
+ function createErrorFromResponse(errorResponse) {
1190
+ switch (errorResponse.code) {
1191
+ case ErrorCode.FILE_NOT_FOUND: return new FileNotFoundError(errorResponse);
1192
+ case ErrorCode.FILE_EXISTS: return new FileExistsError(errorResponse);
1193
+ case ErrorCode.PERMISSION_DENIED: return new PermissionDeniedError(errorResponse);
1194
+ case ErrorCode.IS_DIRECTORY:
1195
+ case ErrorCode.NOT_DIRECTORY:
1196
+ case ErrorCode.NO_SPACE:
1197
+ case ErrorCode.TOO_MANY_FILES:
1198
+ case ErrorCode.RESOURCE_BUSY:
1199
+ case ErrorCode.READ_ONLY:
1200
+ case ErrorCode.NAME_TOO_LONG:
1201
+ case ErrorCode.TOO_MANY_LINKS:
1202
+ case ErrorCode.FILESYSTEM_ERROR: return new FileSystemError(errorResponse);
1203
+ case ErrorCode.COMMAND_NOT_FOUND: return new CommandNotFoundError(errorResponse);
1204
+ case ErrorCode.COMMAND_PERMISSION_DENIED:
1205
+ case ErrorCode.COMMAND_EXECUTION_ERROR:
1206
+ case ErrorCode.INVALID_COMMAND:
1207
+ case ErrorCode.STREAM_START_ERROR: return new CommandError(errorResponse);
1208
+ case ErrorCode.PROCESS_NOT_FOUND: return new ProcessNotFoundError(errorResponse);
1209
+ case ErrorCode.PROCESS_PERMISSION_DENIED:
1210
+ case ErrorCode.PROCESS_ERROR: return new ProcessError(errorResponse);
1211
+ case ErrorCode.PORT_ALREADY_EXPOSED: return new PortAlreadyExposedError(errorResponse);
1212
+ case ErrorCode.PORT_NOT_EXPOSED: return new PortNotExposedError(errorResponse);
1213
+ case ErrorCode.INVALID_PORT_NUMBER:
1214
+ case ErrorCode.INVALID_PORT: return new InvalidPortError(errorResponse);
1215
+ case ErrorCode.SERVICE_NOT_RESPONDING: return new ServiceNotRespondingError(errorResponse);
1216
+ case ErrorCode.PORT_IN_USE: return new PortInUseError(errorResponse);
1217
+ case ErrorCode.PORT_OPERATION_ERROR: return new PortError(errorResponse);
1218
+ case ErrorCode.CUSTOM_DOMAIN_REQUIRED: return new CustomDomainRequiredError(errorResponse);
1219
+ case ErrorCode.GIT_REPOSITORY_NOT_FOUND: return new GitRepositoryNotFoundError(errorResponse);
1220
+ case ErrorCode.GIT_AUTH_FAILED: return new GitAuthenticationError(errorResponse);
1221
+ case ErrorCode.GIT_BRANCH_NOT_FOUND: return new GitBranchNotFoundError(errorResponse);
1222
+ case ErrorCode.GIT_NETWORK_ERROR: return new GitNetworkError(errorResponse);
1223
+ case ErrorCode.GIT_CLONE_FAILED: return new GitCloneError(errorResponse);
1224
+ case ErrorCode.GIT_CHECKOUT_FAILED: return new GitCheckoutError(errorResponse);
1225
+ case ErrorCode.INVALID_GIT_URL: return new InvalidGitUrlError(errorResponse);
1226
+ case ErrorCode.GIT_OPERATION_FAILED: return new GitError(errorResponse);
1227
+ case ErrorCode.INTERPRETER_NOT_READY: return new InterpreterNotReadyError(errorResponse);
1228
+ case ErrorCode.CONTEXT_NOT_FOUND: return new ContextNotFoundError(errorResponse);
1229
+ case ErrorCode.CODE_EXECUTION_ERROR: return new CodeExecutionError(errorResponse);
1230
+ case ErrorCode.VALIDATION_FAILED: return new ValidationFailedError(errorResponse);
1231
+ case ErrorCode.INVALID_JSON_RESPONSE:
1232
+ case ErrorCode.UNKNOWN_ERROR:
1233
+ case ErrorCode.INTERNAL_ERROR: return new SandboxError(errorResponse);
1234
+ default: return new SandboxError(errorResponse);
1235
+ }
1236
+ }
1237
+
1238
+ //#endregion
1239
+ //#region src/clients/base-client.ts
1240
+ const TIMEOUT_MS = 6e4;
1241
+ const MIN_TIME_FOR_RETRY_MS = 1e4;
1242
+ /**
1243
+ * Abstract base class providing common HTTP functionality for all domain clients
1244
+ */
1245
+ var BaseHttpClient = class {
1246
+ baseUrl;
1247
+ options;
1248
+ logger;
1249
+ constructor(options = {}) {
1250
+ this.options = options;
1251
+ this.logger = options.logger ?? createNoOpLogger();
1252
+ this.baseUrl = this.options.baseUrl;
1253
+ }
1254
+ /**
1255
+ * Core HTTP request method with automatic retry for container provisioning delays
1256
+ */
1257
+ async doFetch(path, options) {
1258
+ const startTime = Date.now();
1259
+ let attempt = 0;
1260
+ while (true) {
1261
+ const response = await this.executeFetch(path, options);
1262
+ if (response.status === 503) {
1263
+ if (await this.isContainerProvisioningError(response)) {
1264
+ const remaining = TIMEOUT_MS - (Date.now() - startTime);
1265
+ if (remaining > MIN_TIME_FOR_RETRY_MS) {
1266
+ const delay = Math.min(2e3 * 2 ** attempt, 16e3);
1267
+ this.logger.info("Container provisioning in progress, retrying", {
1268
+ attempt: attempt + 1,
1269
+ delayMs: delay,
1270
+ remainingSec: Math.floor(remaining / 1e3)
1271
+ });
1272
+ await new Promise((resolve) => setTimeout(resolve, delay));
1273
+ attempt++;
1274
+ continue;
1275
+ } else {
1276
+ this.logger.error("Container failed to provision after multiple attempts", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over 60s`));
1277
+ return response;
1278
+ }
1279
+ }
1280
+ }
1281
+ return response;
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Make a POST request with JSON body
1286
+ */
1287
+ async post(endpoint, data, responseHandler) {
1288
+ const response = await this.doFetch(endpoint, {
1289
+ method: "POST",
1290
+ headers: { "Content-Type": "application/json" },
1291
+ body: JSON.stringify(data)
1292
+ });
1293
+ return this.handleResponse(response, responseHandler);
1294
+ }
1295
+ /**
1296
+ * Make a GET request
1297
+ */
1298
+ async get(endpoint, responseHandler) {
1299
+ const response = await this.doFetch(endpoint, { method: "GET" });
1300
+ return this.handleResponse(response, responseHandler);
1301
+ }
1302
+ /**
1303
+ * Make a DELETE request
1304
+ */
1305
+ async delete(endpoint, responseHandler) {
1306
+ const response = await this.doFetch(endpoint, { method: "DELETE" });
1307
+ return this.handleResponse(response, responseHandler);
1308
+ }
1309
+ /**
1310
+ * Handle HTTP response with error checking and parsing
1311
+ */
1312
+ async handleResponse(response, customHandler) {
1313
+ if (!response.ok) await this.handleErrorResponse(response);
1314
+ if (customHandler) return customHandler(response);
1315
+ try {
1316
+ return await response.json();
1317
+ } catch (error) {
1318
+ throw createErrorFromResponse({
1319
+ code: ErrorCode.INVALID_JSON_RESPONSE,
1320
+ message: `Invalid JSON response: ${error instanceof Error ? error.message : "Unknown parsing error"}`,
1321
+ context: {},
1322
+ httpStatus: response.status,
1323
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1324
+ });
1325
+ }
1326
+ }
1327
+ /**
1328
+ * Handle error responses with consistent error throwing
1329
+ */
1330
+ async handleErrorResponse(response) {
1331
+ let errorData;
1332
+ try {
1333
+ errorData = await response.json();
1334
+ } catch {
1335
+ errorData = {
1336
+ code: ErrorCode.INTERNAL_ERROR,
1337
+ message: `HTTP error! status: ${response.status}`,
1338
+ context: { statusText: response.statusText },
1339
+ httpStatus: response.status,
1340
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1341
+ };
1342
+ }
1343
+ const error = createErrorFromResponse(errorData);
1344
+ this.options.onError?.(errorData.message, void 0);
1345
+ throw error;
1346
+ }
1347
+ /**
1348
+ * Create a streaming response handler for Server-Sent Events
1349
+ */
1350
+ async handleStreamResponse(response) {
1351
+ if (!response.ok) await this.handleErrorResponse(response);
1352
+ if (!response.body) throw new Error("No response body for streaming");
1353
+ return response.body;
1354
+ }
1355
+ /**
1356
+ * Utility method to log successful operations
1357
+ */
1358
+ logSuccess(operation, details) {
1359
+ this.logger.info(`${operation} completed successfully`, details ? { details } : void 0);
1360
+ }
1361
+ /**
1362
+ * Utility method to log errors intelligently
1363
+ * Only logs unexpected errors (5xx), not expected errors (4xx)
1364
+ *
1365
+ * - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
1366
+ * - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
1367
+ */
1368
+ logError(operation, error) {
1369
+ if (error && typeof error === "object" && "httpStatus" in error) {
1370
+ const httpStatus = error.httpStatus;
1371
+ if (httpStatus >= 500) this.logger.error(`Unexpected error in ${operation}`, error instanceof Error ? error : new Error(String(error)), { httpStatus });
1372
+ } else this.logger.error(`Error in ${operation}`, error instanceof Error ? error : new Error(String(error)));
1373
+ }
1374
+ /**
1375
+ * Check if 503 response is from container provisioning (retryable)
1376
+ * vs user application (not retryable)
1377
+ */
1378
+ async isContainerProvisioningError(response) {
1379
+ try {
1380
+ return (await response.clone().text()).includes("There is no Container instance available");
1381
+ } catch (error) {
1382
+ this.logger.error("Error checking response body", error instanceof Error ? error : new Error(String(error)));
1383
+ return false;
1384
+ }
1385
+ }
1386
+ async executeFetch(path, options) {
1387
+ const url = this.options.stub ? `http://localhost:${this.options.port}${path}` : `${this.baseUrl}${path}`;
1388
+ try {
1389
+ if (this.options.stub) return await this.options.stub.containerFetch(url, options || {}, this.options.port);
1390
+ else return await fetch(url, options);
1391
+ } catch (error) {
1392
+ this.logger.error("HTTP request error", error instanceof Error ? error : new Error(String(error)), {
1393
+ method: options?.method || "GET",
1394
+ url
1395
+ });
1396
+ throw error;
1397
+ }
1398
+ }
1399
+ };
1400
+
1401
+ //#endregion
1402
+ //#region src/clients/command-client.ts
1403
+ /**
1404
+ * Client for command execution operations
1405
+ */
1406
+ var CommandClient = class extends BaseHttpClient {
1407
+ /**
1408
+ * Execute a command and return the complete result
1409
+ * @param command - The command to execute
1410
+ * @param sessionId - The session ID for this command execution
1411
+ * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
1412
+ */
1413
+ async execute(command, sessionId, timeoutMs) {
1414
+ try {
1415
+ const data = {
1416
+ command,
1417
+ sessionId,
1418
+ ...timeoutMs !== void 0 && { timeoutMs }
1419
+ };
1420
+ const response = await this.post("/api/execute", data);
1421
+ this.logSuccess("Command executed", `${command}, Success: ${response.success}`);
1422
+ this.options.onCommandComplete?.(response.success, response.exitCode, response.stdout, response.stderr, response.command);
1423
+ return response;
1424
+ } catch (error) {
1425
+ this.logError("execute", error);
1426
+ this.options.onError?.(error instanceof Error ? error.message : String(error), command);
1427
+ throw error;
1428
+ }
1429
+ }
1430
+ /**
1431
+ * Execute a command and return a stream of events
1432
+ * @param command - The command to execute
1433
+ * @param sessionId - The session ID for this command execution
1434
+ */
1435
+ async executeStream(command, sessionId) {
1436
+ try {
1437
+ const data = {
1438
+ command,
1439
+ sessionId
1440
+ };
1441
+ const response = await this.doFetch("/api/execute/stream", {
1442
+ method: "POST",
1443
+ headers: { "Content-Type": "application/json" },
1444
+ body: JSON.stringify(data)
1445
+ });
1446
+ const stream = await this.handleStreamResponse(response);
1447
+ this.logSuccess("Command stream started", command);
1448
+ return stream;
1449
+ } catch (error) {
1450
+ this.logError("executeStream", error);
1451
+ this.options.onError?.(error instanceof Error ? error.message : String(error), command);
1452
+ throw error;
1453
+ }
1454
+ }
1455
+ };
1456
+
1457
+ //#endregion
1458
+ //#region src/clients/file-client.ts
1459
+ /**
1460
+ * Client for file system operations
1461
+ */
1462
+ var FileClient = class extends BaseHttpClient {
1463
+ /**
1464
+ * Create a directory
1465
+ * @param path - Directory path to create
1466
+ * @param sessionId - The session ID for this operation
1467
+ * @param options - Optional settings (recursive)
1468
+ */
1469
+ async mkdir(path, sessionId, options) {
1470
+ try {
1471
+ const data = {
1472
+ path,
1473
+ sessionId,
1474
+ recursive: options?.recursive ?? false
1475
+ };
1476
+ const response = await this.post("/api/mkdir", data);
1477
+ this.logSuccess("Directory created", `${path} (recursive: ${data.recursive})`);
1478
+ return response;
1479
+ } catch (error) {
1480
+ this.logError("mkdir", error);
1481
+ throw error;
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Write content to a file
1486
+ * @param path - File path to write to
1487
+ * @param content - Content to write
1488
+ * @param sessionId - The session ID for this operation
1489
+ * @param options - Optional settings (encoding)
1490
+ */
1491
+ async writeFile(path, content, sessionId, options) {
1492
+ try {
1493
+ const data = {
1494
+ path,
1495
+ content,
1496
+ sessionId,
1497
+ encoding: options?.encoding
1498
+ };
1499
+ const response = await this.post("/api/write", data);
1500
+ this.logSuccess("File written", `${path} (${content.length} chars)`);
1501
+ return response;
1502
+ } catch (error) {
1503
+ this.logError("writeFile", error);
1504
+ throw error;
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Read content from a file
1509
+ * @param path - File path to read from
1510
+ * @param sessionId - The session ID for this operation
1511
+ * @param options - Optional settings (encoding)
1512
+ */
1513
+ async readFile(path, sessionId, options) {
1514
+ try {
1515
+ const data = {
1516
+ path,
1517
+ sessionId,
1518
+ encoding: options?.encoding
1519
+ };
1520
+ const response = await this.post("/api/read", data);
1521
+ this.logSuccess("File read", `${path} (${response.content.length} chars)`);
1522
+ return response;
1523
+ } catch (error) {
1524
+ this.logError("readFile", error);
1525
+ throw error;
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Stream a file using Server-Sent Events
1530
+ * Returns a ReadableStream of SSE events containing metadata, chunks, and completion
1531
+ * @param path - File path to stream
1532
+ * @param sessionId - The session ID for this operation
1533
+ */
1534
+ async readFileStream(path, sessionId) {
1535
+ try {
1536
+ const data = {
1537
+ path,
1538
+ sessionId
1539
+ };
1540
+ const response = await this.doFetch("/api/read/stream", {
1541
+ method: "POST",
1542
+ headers: { "Content-Type": "application/json" },
1543
+ body: JSON.stringify(data)
1544
+ });
1545
+ const stream = await this.handleStreamResponse(response);
1546
+ this.logSuccess("File stream started", path);
1547
+ return stream;
1548
+ } catch (error) {
1549
+ this.logError("readFileStream", error);
1550
+ throw error;
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Delete a file
1555
+ * @param path - File path to delete
1556
+ * @param sessionId - The session ID for this operation
1557
+ */
1558
+ async deleteFile(path, sessionId) {
1559
+ try {
1560
+ const data = {
1561
+ path,
1562
+ sessionId
1563
+ };
1564
+ const response = await this.post("/api/delete", data);
1565
+ this.logSuccess("File deleted", path);
1566
+ return response;
1567
+ } catch (error) {
1568
+ this.logError("deleteFile", error);
1569
+ throw error;
1570
+ }
1571
+ }
1572
+ /**
1573
+ * Rename a file
1574
+ * @param path - Current file path
1575
+ * @param newPath - New file path
1576
+ * @param sessionId - The session ID for this operation
1577
+ */
1578
+ async renameFile(path, newPath, sessionId) {
1579
+ try {
1580
+ const data = {
1581
+ oldPath: path,
1582
+ newPath,
1583
+ sessionId
1584
+ };
1585
+ const response = await this.post("/api/rename", data);
1586
+ this.logSuccess("File renamed", `${path} -> ${newPath}`);
1587
+ return response;
1588
+ } catch (error) {
1589
+ this.logError("renameFile", error);
1590
+ throw error;
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Move a file
1595
+ * @param path - Current file path
1596
+ * @param newPath - Destination file path
1597
+ * @param sessionId - The session ID for this operation
1598
+ */
1599
+ async moveFile(path, newPath, sessionId) {
1600
+ try {
1601
+ const data = {
1602
+ sourcePath: path,
1603
+ destinationPath: newPath,
1604
+ sessionId
1605
+ };
1606
+ const response = await this.post("/api/move", data);
1607
+ this.logSuccess("File moved", `${path} -> ${newPath}`);
1608
+ return response;
1609
+ } catch (error) {
1610
+ this.logError("moveFile", error);
1611
+ throw error;
1612
+ }
1613
+ }
1614
+ /**
1615
+ * List files in a directory
1616
+ * @param path - Directory path to list
1617
+ * @param sessionId - The session ID for this operation
1618
+ * @param options - Optional settings (recursive, includeHidden)
1619
+ */
1620
+ async listFiles(path, sessionId, options) {
1621
+ try {
1622
+ const data = {
1623
+ path,
1624
+ sessionId,
1625
+ options: options || {}
1626
+ };
1627
+ const response = await this.post("/api/list-files", data);
1628
+ this.logSuccess("Files listed", `${path} (${response.count} files)`);
1629
+ return response;
1630
+ } catch (error) {
1631
+ this.logError("listFiles", error);
1632
+ throw error;
1633
+ }
1634
+ }
1635
+ /**
1636
+ * Check if a file or directory exists
1637
+ * @param path - Path to check
1638
+ * @param sessionId - The session ID for this operation
1639
+ */
1640
+ async exists(path, sessionId) {
1641
+ try {
1642
+ const data = {
1643
+ path,
1644
+ sessionId
1645
+ };
1646
+ const response = await this.post("/api/exists", data);
1647
+ this.logSuccess("Path existence checked", `${path} (exists: ${response.exists})`);
1648
+ return response;
1649
+ } catch (error) {
1650
+ this.logError("exists", error);
1651
+ throw error;
1652
+ }
1653
+ }
1654
+ };
1655
+
1656
+ //#endregion
1657
+ //#region src/clients/git-client.ts
1658
+ /**
1659
+ * Client for Git repository operations
1660
+ */
1661
+ var GitClient = class extends BaseHttpClient {
1662
+ constructor(options = {}) {
1663
+ super(options);
1664
+ this.logger = new GitLogger(this.logger);
1665
+ }
1666
+ /**
1667
+ * Clone a Git repository
1668
+ * @param repoUrl - URL of the Git repository to clone
1669
+ * @param sessionId - The session ID for this operation
1670
+ * @param options - Optional settings (branch, targetDir)
1671
+ */
1672
+ async checkout(repoUrl, sessionId, options) {
1673
+ try {
1674
+ let targetDir = options?.targetDir;
1675
+ if (!targetDir) targetDir = `/workspace/${this.extractRepoName(repoUrl)}`;
1676
+ const data = {
1677
+ repoUrl,
1678
+ sessionId,
1679
+ targetDir
1680
+ };
1681
+ if (options?.branch) data.branch = options.branch;
1682
+ const response = await this.post("/api/git/checkout", data);
1683
+ this.logSuccess("Repository cloned", `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`);
1684
+ return response;
1685
+ } catch (error) {
1686
+ this.logError("checkout", error);
1687
+ throw error;
1688
+ }
1689
+ }
1690
+ /**
1691
+ * Extract repository name from URL for default directory name
1692
+ */
1693
+ extractRepoName(repoUrl) {
1694
+ try {
1695
+ const pathParts = new URL(repoUrl).pathname.split("/");
1696
+ return pathParts[pathParts.length - 1].replace(/\.git$/, "");
1697
+ } catch {
1698
+ const parts = repoUrl.split("/");
1699
+ return parts[parts.length - 1].replace(/\.git$/, "") || "repo";
1700
+ }
1701
+ }
1702
+ };
1703
+
1704
+ //#endregion
1705
+ //#region src/clients/interpreter-client.ts
1706
+ var InterpreterClient = class extends BaseHttpClient {
1707
+ maxRetries = 3;
1708
+ retryDelayMs = 1e3;
1709
+ async createCodeContext(options = {}) {
1710
+ return this.executeWithRetry(async () => {
1711
+ const response = await this.doFetch("/api/contexts", {
1712
+ method: "POST",
1713
+ headers: { "Content-Type": "application/json" },
1714
+ body: JSON.stringify({
1715
+ language: options.language || "python",
1716
+ cwd: options.cwd || "/workspace",
1717
+ env_vars: options.envVars
1718
+ })
1719
+ });
1720
+ if (!response.ok) throw await this.parseErrorResponse(response);
1721
+ const data = await response.json();
1722
+ if (!data.success) throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
1723
+ return {
1724
+ id: data.contextId,
1725
+ language: data.language,
1726
+ cwd: data.cwd || "/workspace",
1727
+ createdAt: new Date(data.timestamp),
1728
+ lastUsed: new Date(data.timestamp)
1729
+ };
1730
+ });
1731
+ }
1732
+ async runCodeStream(contextId, code, language, callbacks, timeoutMs) {
1733
+ return this.executeWithRetry(async () => {
1734
+ const response = await this.doFetch("/api/execute/code", {
1735
+ method: "POST",
1736
+ headers: {
1737
+ "Content-Type": "application/json",
1738
+ Accept: "text/event-stream"
1739
+ },
1740
+ body: JSON.stringify({
1741
+ context_id: contextId,
1742
+ code,
1743
+ language,
1744
+ ...timeoutMs !== void 0 && { timeout_ms: timeoutMs }
1745
+ })
1746
+ });
1747
+ if (!response.ok) throw await this.parseErrorResponse(response);
1748
+ if (!response.body) throw new Error("No response body for streaming execution");
1749
+ for await (const chunk of this.readLines(response.body)) await this.parseExecutionResult(chunk, callbacks);
1750
+ });
1751
+ }
1752
+ async listCodeContexts() {
1753
+ return this.executeWithRetry(async () => {
1754
+ const response = await this.doFetch("/api/contexts", {
1755
+ method: "GET",
1756
+ headers: { "Content-Type": "application/json" }
1757
+ });
1758
+ if (!response.ok) throw await this.parseErrorResponse(response);
1759
+ const data = await response.json();
1760
+ if (!data.success) throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
1761
+ return data.contexts.map((ctx) => ({
1762
+ id: ctx.id,
1763
+ language: ctx.language,
1764
+ cwd: ctx.cwd || "/workspace",
1765
+ createdAt: new Date(data.timestamp),
1766
+ lastUsed: new Date(data.timestamp)
1767
+ }));
1768
+ });
1769
+ }
1770
+ async deleteCodeContext(contextId) {
1771
+ return this.executeWithRetry(async () => {
1772
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
1773
+ method: "DELETE",
1774
+ headers: { "Content-Type": "application/json" }
1775
+ });
1776
+ if (!response.ok) throw await this.parseErrorResponse(response);
1777
+ });
1778
+ }
1779
+ /**
1780
+ * Execute an operation with automatic retry for transient errors
1781
+ */
1782
+ async executeWithRetry(operation) {
1783
+ let lastError;
1784
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) try {
1785
+ return await operation();
1786
+ } catch (error) {
1787
+ this.logError("executeWithRetry", error);
1788
+ lastError = error;
1789
+ if (this.isRetryableError(error)) {
1790
+ if (attempt < this.maxRetries - 1) {
1791
+ const delay = this.retryDelayMs * 2 ** attempt + Math.random() * 1e3;
1792
+ await new Promise((resolve) => setTimeout(resolve, delay));
1793
+ continue;
1794
+ }
1795
+ }
1796
+ throw error;
1797
+ }
1798
+ throw lastError || /* @__PURE__ */ new Error("Execution failed after retries");
1799
+ }
1800
+ isRetryableError(error) {
1801
+ if (error instanceof InterpreterNotReadyError) return true;
1802
+ if (error instanceof Error) return error.message.includes("not ready") || error.message.includes("initializing");
1803
+ return false;
1804
+ }
1805
+ async parseErrorResponse(response) {
1806
+ try {
1807
+ return createErrorFromResponse(await response.json());
1808
+ } catch {
1809
+ return createErrorFromResponse({
1810
+ code: ErrorCode.INTERNAL_ERROR,
1811
+ message: `HTTP ${response.status}: ${response.statusText}`,
1812
+ context: {},
1813
+ httpStatus: response.status,
1814
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1815
+ });
1816
+ }
1817
+ }
1818
+ async *readLines(stream) {
1819
+ const reader = stream.getReader();
1820
+ let buffer = "";
1821
+ try {
1822
+ while (true) {
1823
+ const { done, value } = await reader.read();
1824
+ if (value) buffer += new TextDecoder().decode(value);
1825
+ if (done) break;
1826
+ let newlineIdx = buffer.indexOf("\n");
1827
+ while (newlineIdx !== -1) {
1828
+ yield buffer.slice(0, newlineIdx);
1829
+ buffer = buffer.slice(newlineIdx + 1);
1830
+ newlineIdx = buffer.indexOf("\n");
1831
+ }
1832
+ }
1833
+ if (buffer.length > 0) yield buffer;
1834
+ } finally {
1835
+ reader.releaseLock();
1836
+ }
1837
+ }
1838
+ async parseExecutionResult(line, callbacks) {
1839
+ if (!line.trim()) return;
1840
+ if (!line.startsWith("data: ")) return;
1841
+ try {
1842
+ const jsonData = line.substring(6);
1843
+ const data = JSON.parse(jsonData);
1844
+ switch (data.type) {
1845
+ case "stdout":
1846
+ if (callbacks.onStdout && data.text) await callbacks.onStdout({
1847
+ text: data.text,
1848
+ timestamp: data.timestamp || Date.now()
1849
+ });
1850
+ break;
1851
+ case "stderr":
1852
+ if (callbacks.onStderr && data.text) await callbacks.onStderr({
1853
+ text: data.text,
1854
+ timestamp: data.timestamp || Date.now()
1855
+ });
1856
+ break;
1857
+ case "result":
1858
+ if (callbacks.onResult) {
1859
+ const result = new ResultImpl(data);
1860
+ await callbacks.onResult(result);
1861
+ }
1862
+ break;
1863
+ case "error":
1864
+ if (callbacks.onError) await callbacks.onError({
1865
+ name: data.ename || "Error",
1866
+ message: data.evalue || "Unknown error",
1867
+ traceback: data.traceback || []
1868
+ });
1869
+ break;
1870
+ case "execution_complete": break;
1871
+ }
1872
+ } catch (error) {
1873
+ this.logError("parseExecutionResult", error);
1874
+ }
1875
+ }
1876
+ };
1877
+
1878
+ //#endregion
1879
+ //#region src/clients/port-client.ts
1880
+ /**
1881
+ * Client for port management and preview URL operations
1882
+ */
1883
+ var PortClient = class extends BaseHttpClient {
1884
+ /**
1885
+ * Expose a port and get a preview URL
1886
+ * @param port - Port number to expose
1887
+ * @param sessionId - The session ID for this operation
1888
+ * @param name - Optional name for the port
1889
+ */
1890
+ async exposePort(port, sessionId, name) {
1891
+ try {
1892
+ const data = {
1893
+ port,
1894
+ sessionId,
1895
+ name
1896
+ };
1897
+ const response = await this.post("/api/expose-port", data);
1898
+ this.logSuccess("Port exposed", `${port} exposed at ${response.url}${name ? ` (${name})` : ""}`);
1899
+ return response;
1900
+ } catch (error) {
1901
+ this.logError("exposePort", error);
1902
+ throw error;
1903
+ }
1904
+ }
1905
+ /**
1906
+ * Unexpose a port and remove its preview URL
1907
+ * @param port - Port number to unexpose
1908
+ * @param sessionId - The session ID for this operation
1909
+ */
1910
+ async unexposePort(port, sessionId) {
1911
+ try {
1912
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
1913
+ const response = await this.delete(url);
1914
+ this.logSuccess("Port unexposed", `${port}`);
1915
+ return response;
1916
+ } catch (error) {
1917
+ this.logError("unexposePort", error);
1918
+ throw error;
1919
+ }
1920
+ }
1921
+ /**
1922
+ * Get all currently exposed ports
1923
+ * @param sessionId - The session ID for this operation
1924
+ */
1925
+ async getExposedPorts(sessionId) {
1926
+ try {
1927
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
1928
+ const response = await this.get(url);
1929
+ this.logSuccess("Exposed ports retrieved", `${response.ports.length} ports exposed`);
1930
+ return response;
1931
+ } catch (error) {
1932
+ this.logError("getExposedPorts", error);
1933
+ throw error;
1934
+ }
1935
+ }
1936
+ };
1937
+
1938
+ //#endregion
1939
+ //#region src/clients/process-client.ts
1940
+ /**
1941
+ * Client for background process management
1942
+ */
1943
+ var ProcessClient = class extends BaseHttpClient {
1944
+ /**
1945
+ * Start a background process
1946
+ * @param command - Command to execute as a background process
1947
+ * @param sessionId - The session ID for this operation
1948
+ * @param options - Optional settings (processId)
1949
+ */
1950
+ async startProcess(command, sessionId, options) {
1951
+ try {
1952
+ const data = {
1953
+ command,
1954
+ sessionId,
1955
+ processId: options?.processId
1956
+ };
1957
+ const response = await this.post("/api/process/start", data);
1958
+ this.logSuccess("Process started", `${command} (ID: ${response.processId})`);
1959
+ return response;
1960
+ } catch (error) {
1961
+ this.logError("startProcess", error);
1962
+ throw error;
1963
+ }
1964
+ }
1965
+ /**
1966
+ * List all processes (sandbox-scoped, not session-scoped)
1967
+ */
1968
+ async listProcesses() {
1969
+ try {
1970
+ const response = await this.get(`/api/process/list`);
1971
+ this.logSuccess("Processes listed", `${response.processes.length} processes`);
1972
+ return response;
1973
+ } catch (error) {
1974
+ this.logError("listProcesses", error);
1975
+ throw error;
1976
+ }
1977
+ }
1978
+ /**
1979
+ * Get information about a specific process (sandbox-scoped, not session-scoped)
1980
+ * @param processId - ID of the process to retrieve
1981
+ */
1982
+ async getProcess(processId) {
1983
+ try {
1984
+ const url = `/api/process/${processId}`;
1985
+ const response = await this.get(url);
1986
+ this.logSuccess("Process retrieved", `ID: ${processId}`);
1987
+ return response;
1988
+ } catch (error) {
1989
+ this.logError("getProcess", error);
1990
+ throw error;
1991
+ }
1992
+ }
1993
+ /**
1994
+ * Kill a specific process (sandbox-scoped, not session-scoped)
1995
+ * @param processId - ID of the process to kill
1996
+ */
1997
+ async killProcess(processId) {
1998
+ try {
1999
+ const url = `/api/process/${processId}`;
2000
+ const response = await this.delete(url);
2001
+ this.logSuccess("Process killed", `ID: ${processId}`);
2002
+ return response;
2003
+ } catch (error) {
2004
+ this.logError("killProcess", error);
2005
+ throw error;
2006
+ }
2007
+ }
2008
+ /**
2009
+ * Kill all running processes (sandbox-scoped, not session-scoped)
2010
+ */
2011
+ async killAllProcesses() {
2012
+ try {
2013
+ const response = await this.delete(`/api/process/kill-all`);
2014
+ this.logSuccess("All processes killed", `${response.cleanedCount} processes terminated`);
2015
+ return response;
2016
+ } catch (error) {
2017
+ this.logError("killAllProcesses", error);
2018
+ throw error;
2019
+ }
2020
+ }
2021
+ /**
2022
+ * Get logs from a specific process (sandbox-scoped, not session-scoped)
2023
+ * @param processId - ID of the process to get logs from
2024
+ */
2025
+ async getProcessLogs(processId) {
2026
+ try {
2027
+ const url = `/api/process/${processId}/logs`;
2028
+ const response = await this.get(url);
2029
+ this.logSuccess("Process logs retrieved", `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`);
2030
+ return response;
2031
+ } catch (error) {
2032
+ this.logError("getProcessLogs", error);
2033
+ throw error;
2034
+ }
2035
+ }
2036
+ /**
2037
+ * Stream logs from a specific process (sandbox-scoped, not session-scoped)
2038
+ * @param processId - ID of the process to stream logs from
2039
+ */
2040
+ async streamProcessLogs(processId) {
2041
+ try {
2042
+ const url = `/api/process/${processId}/stream`;
2043
+ const response = await this.doFetch(url, { method: "GET" });
2044
+ const stream = await this.handleStreamResponse(response);
2045
+ this.logSuccess("Process log stream started", `ID: ${processId}`);
2046
+ return stream;
2047
+ } catch (error) {
2048
+ this.logError("streamProcessLogs", error);
2049
+ throw error;
2050
+ }
2051
+ }
2052
+ };
2053
+
2054
+ //#endregion
2055
+ //#region src/clients/utility-client.ts
2056
+ /**
2057
+ * Client for health checks and utility operations
2058
+ */
2059
+ var UtilityClient = class extends BaseHttpClient {
2060
+ /**
2061
+ * Ping the sandbox to check if it's responsive
2062
+ */
2063
+ async ping() {
2064
+ try {
2065
+ const response = await this.get("/api/ping");
2066
+ this.logSuccess("Ping successful", response.message);
2067
+ return response.message;
2068
+ } catch (error) {
2069
+ this.logError("ping", error);
2070
+ throw error;
2071
+ }
2072
+ }
2073
+ /**
2074
+ * Get list of available commands in the sandbox environment
2075
+ */
2076
+ async getCommands() {
2077
+ try {
2078
+ const response = await this.get("/api/commands");
2079
+ this.logSuccess("Commands retrieved", `${response.count} commands available`);
2080
+ return response.availableCommands;
2081
+ } catch (error) {
2082
+ this.logError("getCommands", error);
2083
+ throw error;
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Create a new execution session
2088
+ * @param options - Session configuration (id, env, cwd)
2089
+ */
2090
+ async createSession(options) {
2091
+ try {
2092
+ const response = await this.post("/api/session/create", options);
2093
+ this.logSuccess("Session created", `ID: ${options.id}`);
2094
+ return response;
2095
+ } catch (error) {
2096
+ this.logError("createSession", error);
2097
+ throw error;
2098
+ }
2099
+ }
2100
+ /**
2101
+ * Delete an execution session
2102
+ * @param sessionId - Session ID to delete
2103
+ */
2104
+ async deleteSession(sessionId) {
2105
+ try {
2106
+ const response = await this.post("/api/session/delete", { sessionId });
2107
+ this.logSuccess("Session deleted", `ID: ${sessionId}`);
2108
+ return response;
2109
+ } catch (error) {
2110
+ this.logError("deleteSession", error);
2111
+ throw error;
2112
+ }
2113
+ }
2114
+ /**
2115
+ * Get the container version
2116
+ * Returns the version embedded in the Docker image during build
2117
+ */
2118
+ async getVersion() {
2119
+ try {
2120
+ const response = await this.get("/api/version");
2121
+ this.logSuccess("Version retrieved", response.version);
2122
+ return response.version;
2123
+ } catch (error) {
2124
+ this.logger.debug("Failed to get container version (may be old container)", { error });
2125
+ return "unknown";
2126
+ }
2127
+ }
2128
+ };
2129
+
2130
+ //#endregion
2131
+ //#region src/clients/sandbox-client.ts
2132
+ /**
2133
+ * Main sandbox client that composes all domain-specific clients
2134
+ * Provides organized access to all sandbox functionality
2135
+ */
2136
+ var SandboxClient = class {
2137
+ commands;
2138
+ files;
2139
+ processes;
2140
+ ports;
2141
+ git;
2142
+ interpreter;
2143
+ utils;
2144
+ constructor(options) {
2145
+ const clientOptions = {
2146
+ baseUrl: "http://localhost:3000",
2147
+ ...options
2148
+ };
2149
+ this.commands = new CommandClient(clientOptions);
2150
+ this.files = new FileClient(clientOptions);
2151
+ this.processes = new ProcessClient(clientOptions);
2152
+ this.ports = new PortClient(clientOptions);
2153
+ this.git = new GitClient(clientOptions);
2154
+ this.interpreter = new InterpreterClient(clientOptions);
2155
+ this.utils = new UtilityClient(clientOptions);
2156
+ }
2157
+ };
2158
+
2159
+ //#endregion
2160
+ //#region src/security.ts
2161
+ /**
2162
+ * Security utilities for URL construction and input validation
2163
+ *
2164
+ * This module contains critical security functions to prevent:
2165
+ * - URL injection attacks
2166
+ * - SSRF (Server-Side Request Forgery) attacks
2167
+ * - DNS rebinding attacks
2168
+ * - Host header injection
2169
+ * - Open redirect vulnerabilities
2170
+ */
2171
+ var SecurityError = class extends Error {
2172
+ constructor(message, code) {
2173
+ super(message);
2174
+ this.code = code;
2175
+ this.name = "SecurityError";
2176
+ }
2177
+ };
2178
+ /**
2179
+ * Validates port numbers for sandbox services
2180
+ * Only allows non-system ports to prevent conflicts and security issues
2181
+ */
2182
+ function validatePort(port) {
2183
+ if (!Number.isInteger(port)) return false;
2184
+ if (port < 1024 || port > 65535) return false;
2185
+ if ([3e3, 8787].includes(port)) return false;
2186
+ return true;
2187
+ }
2188
+ /**
2189
+ * Sanitizes and validates sandbox IDs for DNS compliance and security
2190
+ * Only enforces critical requirements - allows maximum developer flexibility
2191
+ */
2192
+ function sanitizeSandboxId(id) {
2193
+ if (!id || id.length > 63) throw new SecurityError("Sandbox ID must be 1-63 characters long.", "INVALID_SANDBOX_ID_LENGTH");
2194
+ if (id.startsWith("-") || id.endsWith("-")) throw new SecurityError("Sandbox ID cannot start or end with hyphens (DNS requirement).", "INVALID_SANDBOX_ID_HYPHENS");
2195
+ const reservedNames = [
2196
+ "www",
2197
+ "api",
2198
+ "admin",
2199
+ "root",
2200
+ "system",
2201
+ "cloudflare",
2202
+ "workers"
2203
+ ];
2204
+ const lowerCaseId = id.toLowerCase();
2205
+ if (reservedNames.includes(lowerCaseId)) throw new SecurityError(`Reserved sandbox ID '${id}' is not allowed.`, "RESERVED_SANDBOX_ID");
2206
+ return id;
2207
+ }
2208
+ /**
2209
+ * Validates language for code interpreter
2210
+ * Only allows supported languages
2211
+ */
2212
+ function validateLanguage(language) {
2213
+ if (!language) return;
2214
+ const supportedLanguages = [
2215
+ "python",
2216
+ "python3",
2217
+ "javascript",
2218
+ "js",
2219
+ "node",
2220
+ "typescript",
2221
+ "ts"
2222
+ ];
2223
+ const normalized = language.toLowerCase();
2224
+ if (!supportedLanguages.includes(normalized)) throw new SecurityError(`Unsupported language '${language}'. Supported languages: python, javascript, typescript`, "INVALID_LANGUAGE");
2225
+ }
2226
+
2227
+ //#endregion
2228
+ //#region src/interpreter.ts
2229
+ var CodeInterpreter = class {
2230
+ interpreterClient;
2231
+ contexts = /* @__PURE__ */ new Map();
2232
+ constructor(sandbox) {
2233
+ this.interpreterClient = sandbox.client.interpreter;
2234
+ }
2235
+ /**
2236
+ * Create a new code execution context
2237
+ */
2238
+ async createCodeContext(options = {}) {
2239
+ validateLanguage(options.language);
2240
+ const context = await this.interpreterClient.createCodeContext(options);
2241
+ this.contexts.set(context.id, context);
2242
+ return context;
2243
+ }
2244
+ /**
2245
+ * Run code with optional context
2246
+ */
2247
+ async runCode(code, options = {}) {
2248
+ let context = options.context;
2249
+ if (!context) {
2250
+ const language = options.language || "python";
2251
+ context = await this.getOrCreateDefaultContext(language);
2252
+ }
2253
+ const execution = new Execution(code, context);
2254
+ await this.interpreterClient.runCodeStream(context.id, code, options.language, {
2255
+ onStdout: (output) => {
2256
+ execution.logs.stdout.push(output.text);
2257
+ if (options.onStdout) return options.onStdout(output);
2258
+ },
2259
+ onStderr: (output) => {
2260
+ execution.logs.stderr.push(output.text);
2261
+ if (options.onStderr) return options.onStderr(output);
2262
+ },
2263
+ onResult: async (result) => {
2264
+ execution.results.push(new ResultImpl(result));
2265
+ if (options.onResult) return options.onResult(result);
2266
+ },
2267
+ onError: (error) => {
2268
+ execution.error = error;
2269
+ if (options.onError) return options.onError(error);
2270
+ }
2271
+ });
2272
+ return execution;
2273
+ }
2274
+ /**
2275
+ * Run code and return a streaming response
2276
+ */
2277
+ async runCodeStream(code, options = {}) {
2278
+ let context = options.context;
2279
+ if (!context) {
2280
+ const language = options.language || "python";
2281
+ context = await this.getOrCreateDefaultContext(language);
2282
+ }
2283
+ const response = await this.interpreterClient.doFetch("/api/execute/code", {
2284
+ method: "POST",
2285
+ headers: {
2286
+ "Content-Type": "application/json",
2287
+ Accept: "text/event-stream"
2288
+ },
2289
+ body: JSON.stringify({
2290
+ context_id: context.id,
2291
+ code,
2292
+ language: options.language
2293
+ })
2294
+ });
2295
+ if (!response.ok) {
2296
+ const errorData = await response.json().catch(() => ({ error: "Unknown error" }));
2297
+ throw new Error(errorData.error || `Failed to execute code: ${response.status}`);
2298
+ }
2299
+ if (!response.body) throw new Error("No response body for streaming execution");
2300
+ return response.body;
2301
+ }
2302
+ /**
2303
+ * List all code contexts
2304
+ */
2305
+ async listCodeContexts() {
2306
+ const contexts = await this.interpreterClient.listCodeContexts();
2307
+ for (const context of contexts) this.contexts.set(context.id, context);
2308
+ return contexts;
2309
+ }
2310
+ /**
2311
+ * Delete a code context
2312
+ */
2313
+ async deleteCodeContext(contextId) {
2314
+ await this.interpreterClient.deleteCodeContext(contextId);
2315
+ this.contexts.delete(contextId);
2316
+ }
2317
+ async getOrCreateDefaultContext(language) {
2318
+ for (const context of this.contexts.values()) if (context.language === language) return context;
2319
+ return this.createCodeContext({ language });
2320
+ }
2321
+ };
2322
+
2323
+ //#endregion
2324
+ //#region src/request-handler.ts
2325
+ async function proxyToSandbox(request, env) {
2326
+ const logger = createLogger({
2327
+ component: "sandbox-do",
2328
+ traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
2329
+ operation: "proxy"
2330
+ });
2331
+ try {
2332
+ const url = new URL(request.url);
2333
+ const routeInfo = extractSandboxRoute(url);
2334
+ if (!routeInfo) return null;
2335
+ const { sandboxId, port, path, token } = routeInfo;
2336
+ const sandbox = getSandbox(env.Sandbox, sandboxId);
2337
+ if (port !== 3e3) {
2338
+ if (!await sandbox.validatePortToken(port, token)) {
2339
+ logger.warn("Invalid token access blocked", {
2340
+ port,
2341
+ sandboxId,
2342
+ path,
2343
+ hostname: url.hostname,
2344
+ url: request.url,
2345
+ method: request.method,
2346
+ userAgent: request.headers.get("User-Agent") || "unknown"
2347
+ });
2348
+ return new Response(JSON.stringify({
2349
+ error: `Access denied: Invalid token or port not exposed`,
2350
+ code: "INVALID_TOKEN"
2351
+ }), {
2352
+ status: 404,
2353
+ headers: { "Content-Type": "application/json" }
2354
+ });
2355
+ }
2356
+ }
2357
+ if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return await sandbox.fetch(switchPort(request, port));
2358
+ let proxyUrl;
2359
+ if (port !== 3e3) proxyUrl = `http://localhost:${port}${path}${url.search}`;
2360
+ else proxyUrl = `http://localhost:3000${path}${url.search}`;
2361
+ const proxyRequest = new Request(proxyUrl, {
2362
+ method: request.method,
2363
+ headers: {
2364
+ ...Object.fromEntries(request.headers),
2365
+ "X-Original-URL": request.url,
2366
+ "X-Forwarded-Host": url.hostname,
2367
+ "X-Forwarded-Proto": url.protocol.replace(":", ""),
2368
+ "X-Sandbox-Name": sandboxId
2369
+ },
2370
+ body: request.body,
2371
+ duplex: "half"
2372
+ });
2373
+ return await sandbox.containerFetch(proxyRequest, port);
2374
+ } catch (error) {
2375
+ logger.error("Proxy routing error", error instanceof Error ? error : new Error(String(error)));
2376
+ return new Response("Proxy routing error", { status: 500 });
2377
+ }
2378
+ }
2379
+ function extractSandboxRoute(url) {
2380
+ const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
2381
+ if (!subdomainMatch) return null;
2382
+ const portStr = subdomainMatch[1];
2383
+ const sandboxId = subdomainMatch[2];
2384
+ const token = subdomainMatch[3];
2385
+ subdomainMatch[4];
2386
+ const port = parseInt(portStr, 10);
2387
+ if (!validatePort(port)) return null;
2388
+ let sanitizedSandboxId;
2389
+ try {
2390
+ sanitizedSandboxId = sanitizeSandboxId(sandboxId);
2391
+ } catch (error) {
2392
+ return null;
2393
+ }
2394
+ if (sandboxId.length > 63) return null;
2395
+ return {
2396
+ port,
2397
+ sandboxId: sanitizedSandboxId,
2398
+ path: url.pathname || "/",
2399
+ token
2400
+ };
2401
+ }
2402
+ function isLocalhostPattern(hostname) {
2403
+ if (hostname.startsWith("[")) if (hostname.includes("]:")) return hostname.substring(0, hostname.indexOf("]:") + 1) === "[::1]";
2404
+ else return hostname === "[::1]";
2405
+ if (hostname === "::1") return true;
2406
+ const hostPart = hostname.split(":")[0];
2407
+ return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
2408
+ }
2409
+
2410
+ //#endregion
2411
+ //#region src/sse-parser.ts
2412
+ /**
2413
+ * Server-Sent Events (SSE) parser for streaming responses
2414
+ * Converts ReadableStream<Uint8Array> to typed AsyncIterable<T>
2415
+ */
2416
+ /**
2417
+ * Parse a ReadableStream of SSE events into typed AsyncIterable
2418
+ * @param stream - The ReadableStream from fetch response
2419
+ * @param signal - Optional AbortSignal for cancellation
2420
+ */
2421
+ async function* parseSSEStream(stream, signal) {
2422
+ const reader = stream.getReader();
2423
+ const decoder = new TextDecoder();
2424
+ let buffer = "";
2425
+ try {
2426
+ while (true) {
2427
+ if (signal?.aborted) throw new Error("Operation was aborted");
2428
+ const { done, value } = await reader.read();
2429
+ if (done) break;
2430
+ buffer += decoder.decode(value, { stream: true });
2431
+ const lines = buffer.split("\n");
2432
+ buffer = lines.pop() || "";
2433
+ for (const line of lines) {
2434
+ if (line.trim() === "") continue;
2435
+ if (line.startsWith("data: ")) {
2436
+ const data = line.substring(6);
2437
+ if (data === "[DONE]" || data.trim() === "") continue;
2438
+ try {
2439
+ yield JSON.parse(data);
2440
+ } catch {}
2441
+ }
2442
+ }
2443
+ }
2444
+ if (buffer.trim() && buffer.startsWith("data: ")) {
2445
+ const data = buffer.substring(6);
2446
+ if (data !== "[DONE]" && data.trim()) try {
2447
+ yield JSON.parse(data);
2448
+ } catch {}
2449
+ }
2450
+ } finally {
2451
+ reader.releaseLock();
2452
+ }
2453
+ }
2454
+ /**
2455
+ * Helper to convert a Response with SSE stream directly to AsyncIterable
2456
+ * @param response - Response object with SSE stream
2457
+ * @param signal - Optional AbortSignal for cancellation
2458
+ */
2459
+ async function* responseToAsyncIterable(response, signal) {
2460
+ if (!response.ok) throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
2461
+ if (!response.body) throw new Error("No response body");
2462
+ yield* parseSSEStream(response.body, signal);
2463
+ }
2464
+ /**
2465
+ * Create an SSE-formatted ReadableStream from an AsyncIterable
2466
+ * (Useful for Worker endpoints that need to forward AsyncIterable as SSE)
2467
+ * @param events - AsyncIterable of events
2468
+ * @param options - Stream options
2469
+ */
2470
+ function asyncIterableToSSEStream(events, options) {
2471
+ const encoder = new TextEncoder();
2472
+ const serialize = options?.serialize || JSON.stringify;
2473
+ return new ReadableStream({
2474
+ async start(controller) {
2475
+ try {
2476
+ for await (const event of events) {
2477
+ if (options?.signal?.aborted) {
2478
+ controller.error(/* @__PURE__ */ new Error("Operation was aborted"));
2479
+ break;
2480
+ }
2481
+ const sseEvent = `data: ${serialize(event)}\n\n`;
2482
+ controller.enqueue(encoder.encode(sseEvent));
2483
+ }
2484
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
2485
+ } catch (error) {
2486
+ controller.error(error);
2487
+ } finally {
2488
+ controller.close();
2489
+ }
2490
+ },
2491
+ cancel() {}
2492
+ });
2493
+ }
2494
+
2495
+ //#endregion
2496
+ //#region src/version.ts
2497
+ /**
2498
+ * SDK version - automatically synchronized with package.json by Changesets
2499
+ * This file is auto-updated by .github/changeset-version.ts during releases
2500
+ * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
2501
+ */
2502
+ const SDK_VERSION = "0.4.19";
2503
+
2504
+ //#endregion
2505
+ //#region src/sandbox.ts
2506
+ function getSandbox(ns, id, options) {
2507
+ const stub = getContainer(ns, id);
2508
+ stub.setSandboxName?.(id);
2509
+ if (options?.baseUrl) stub.setBaseUrl(options.baseUrl);
2510
+ if (options?.sleepAfter !== void 0) stub.setSleepAfter(options.sleepAfter);
2511
+ if (options?.keepAlive !== void 0) stub.setKeepAlive(options.keepAlive);
2512
+ return Object.assign(stub, { wsConnect: connect(stub) });
2513
+ }
2514
+ function connect(stub) {
2515
+ return async (request, port) => {
2516
+ if (!validatePort(port)) throw new SecurityError(`Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`);
2517
+ const portSwitchedRequest = switchPort(request, port);
2518
+ return await stub.fetch(portSwitchedRequest);
2519
+ };
2520
+ }
2521
+ var Sandbox = class extends Container {
2522
+ defaultPort = 3e3;
2523
+ sleepAfter = "10m";
2524
+ client;
2525
+ codeInterpreter;
2526
+ sandboxName = null;
2527
+ baseUrl = null;
2528
+ portTokens = /* @__PURE__ */ new Map();
2529
+ defaultSession = null;
2530
+ envVars = {};
2531
+ logger;
2532
+ keepAliveEnabled = false;
2533
+ constructor(ctx, env) {
2534
+ super(ctx, env);
2535
+ const envObj = env;
2536
+ ["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
2537
+ if (envObj?.[key]) this.envVars[key] = envObj[key];
2538
+ });
2539
+ this.logger = createLogger({
2540
+ component: "sandbox-do",
2541
+ sandboxId: this.ctx.id.toString()
2542
+ });
2543
+ this.client = new SandboxClient({
2544
+ logger: this.logger,
2545
+ port: 3e3,
2546
+ stub: this
2547
+ });
2548
+ this.codeInterpreter = new CodeInterpreter(this);
2549
+ this.ctx.blockConcurrencyWhile(async () => {
2550
+ this.sandboxName = await this.ctx.storage.get("sandboxName") || null;
2551
+ this.defaultSession = await this.ctx.storage.get("defaultSession") || null;
2552
+ const storedTokens = await this.ctx.storage.get("portTokens") || {};
2553
+ this.portTokens = /* @__PURE__ */ new Map();
2554
+ for (const [portStr, token] of Object.entries(storedTokens)) this.portTokens.set(parseInt(portStr, 10), token);
2555
+ });
2556
+ }
2557
+ async setSandboxName(name) {
2558
+ if (!this.sandboxName) {
2559
+ this.sandboxName = name;
2560
+ await this.ctx.storage.put("sandboxName", name);
2561
+ }
2562
+ }
2563
+ async setBaseUrl(baseUrl) {
2564
+ if (!this.baseUrl) {
2565
+ this.baseUrl = baseUrl;
2566
+ await this.ctx.storage.put("baseUrl", baseUrl);
2567
+ } else if (this.baseUrl !== baseUrl) throw new Error("Base URL already set and different from one previously provided");
2568
+ }
2569
+ async setSleepAfter(sleepAfter) {
2570
+ this.sleepAfter = sleepAfter;
2571
+ }
2572
+ async setKeepAlive(keepAlive) {
2573
+ this.keepAliveEnabled = keepAlive;
2574
+ if (keepAlive) this.logger.info("KeepAlive mode enabled - container will stay alive until explicitly destroyed");
2575
+ else this.logger.info("KeepAlive mode disabled - container will timeout normally");
2576
+ }
2577
+ async setEnvVars(envVars) {
2578
+ this.envVars = {
2579
+ ...this.envVars,
2580
+ ...envVars
2581
+ };
2582
+ if (this.defaultSession) for (const [key, value] of Object.entries(envVars)) {
2583
+ const exportCommand = `export ${key}='${value.replace(/'/g, "'\\''")}'`;
2584
+ const result = await this.client.commands.execute(exportCommand, this.defaultSession);
2585
+ if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
2586
+ }
2587
+ }
2588
+ /**
2589
+ * Cleanup and destroy the sandbox container
2590
+ */
2591
+ async destroy() {
2592
+ this.logger.info("Destroying sandbox container");
2593
+ await super.destroy();
2594
+ }
2595
+ onStart() {
2596
+ this.logger.debug("Sandbox started");
2597
+ this.checkVersionCompatibility().catch((error) => {
2598
+ this.logger.error("Version compatibility check failed", error instanceof Error ? error : new Error(String(error)));
2599
+ });
2600
+ }
2601
+ /**
2602
+ * Check if the container version matches the SDK version
2603
+ * Logs a warning if there's a mismatch
2604
+ */
2605
+ async checkVersionCompatibility() {
2606
+ try {
2607
+ const sdkVersion = SDK_VERSION;
2608
+ const containerVersion = await this.client.utils.getVersion();
2609
+ if (containerVersion === "unknown") {
2610
+ this.logger.warn("Container version check: Container version could not be determined. This may indicate an outdated container image. Please update your container to match SDK version " + sdkVersion);
2611
+ return;
2612
+ }
2613
+ if (containerVersion !== sdkVersion) {
2614
+ const message = `Version mismatch detected! SDK version (${sdkVersion}) does not match container version (${containerVersion}). This may cause compatibility issues. Please update your container image to version ${sdkVersion}`;
2615
+ this.logger.warn(message);
2616
+ } else this.logger.debug("Version check passed", {
2617
+ sdkVersion,
2618
+ containerVersion
2619
+ });
2620
+ } catch (error) {
2621
+ this.logger.debug("Version compatibility check encountered an error", { error: error instanceof Error ? error.message : String(error) });
2622
+ }
2623
+ }
2624
+ onStop() {
2625
+ this.logger.debug("Sandbox stopped");
2626
+ }
2627
+ onError(error) {
2628
+ this.logger.error("Sandbox error", error instanceof Error ? error : new Error(String(error)));
2629
+ }
2630
+ /**
2631
+ * Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
2632
+ * When keepAlive is disabled, calls parent implementation which stops the container
2633
+ */
2634
+ async onActivityExpired() {
2635
+ if (this.keepAliveEnabled) this.logger.debug("Activity expired but keepAlive is enabled - container will stay alive");
2636
+ else {
2637
+ this.logger.debug("Activity expired - stopping container");
2638
+ await super.onActivityExpired();
2639
+ }
2640
+ }
2641
+ async fetch(request) {
2642
+ const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
2643
+ const requestLogger = this.logger.child({
2644
+ traceId,
2645
+ operation: "fetch"
2646
+ });
2647
+ return await runWithLogger(requestLogger, async () => {
2648
+ const url = new URL(request.url);
2649
+ if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
2650
+ const name = request.headers.get("X-Sandbox-Name");
2651
+ this.sandboxName = name;
2652
+ await this.ctx.storage.put("sandboxName", name);
2653
+ }
2654
+ const upgradeHeader = request.headers.get("Upgrade");
2655
+ const connectionHeader = request.headers.get("Connection");
2656
+ if (upgradeHeader?.toLowerCase() === "websocket" && connectionHeader?.toLowerCase().includes("upgrade")) try {
2657
+ requestLogger.debug("WebSocket upgrade requested", {
2658
+ path: url.pathname,
2659
+ port: this.determinePort(url)
2660
+ });
2661
+ return await super.fetch(request);
2662
+ } catch (error) {
2663
+ requestLogger.error("WebSocket connection failed", error instanceof Error ? error : new Error(String(error)), { path: url.pathname });
2664
+ throw error;
2665
+ }
2666
+ const port = this.determinePort(url);
2667
+ return await this.containerFetch(request, port);
2668
+ });
2669
+ }
2670
+ wsConnect(request, port) {
2671
+ throw new Error("Not implemented here to avoid RPC serialization issues");
2672
+ }
2673
+ determinePort(url) {
2674
+ const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
2675
+ if (proxyMatch) return parseInt(proxyMatch[1], 10);
2676
+ return 3e3;
2677
+ }
2678
+ /**
2679
+ * Ensure default session exists - lazy initialization
2680
+ * This is called automatically by all public methods that need a session
2681
+ *
2682
+ * The session is persisted to Durable Object storage to survive hot reloads
2683
+ * during development. If a session already exists in the container after reload,
2684
+ * we reuse it instead of trying to create a new one.
2685
+ */
2686
+ async ensureDefaultSession() {
2687
+ if (!this.defaultSession) {
2688
+ const sessionId = `sandbox-${this.sandboxName || "default"}`;
2689
+ try {
2690
+ await this.client.utils.createSession({
2691
+ id: sessionId,
2692
+ env: this.envVars || {},
2693
+ cwd: "/workspace"
2694
+ });
2695
+ this.defaultSession = sessionId;
2696
+ await this.ctx.storage.put("defaultSession", sessionId);
2697
+ this.logger.debug("Default session initialized", { sessionId });
2698
+ } catch (error) {
2699
+ if (error?.message?.includes("already exists")) {
2700
+ this.logger.debug("Reusing existing session after reload", { sessionId });
2701
+ this.defaultSession = sessionId;
2702
+ await this.ctx.storage.put("defaultSession", sessionId);
2703
+ } else throw error;
2704
+ }
2705
+ }
2706
+ return this.defaultSession;
2707
+ }
2708
+ async exec(command, options) {
2709
+ const session = await this.ensureDefaultSession();
2710
+ return this.execWithSession(command, session, options);
2711
+ }
2712
+ /**
2713
+ * Internal session-aware exec implementation
2714
+ * Used by both public exec() and session wrappers
2715
+ */
2716
+ async execWithSession(command, sessionId, options) {
2717
+ const startTime = Date.now();
2718
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2719
+ try {
2720
+ if (options?.signal?.aborted) throw new Error("Operation was aborted");
2721
+ let result;
2722
+ if (options?.stream && options?.onOutput) result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
2723
+ else {
2724
+ const response = await this.client.commands.execute(command, sessionId);
2725
+ const duration = Date.now() - startTime;
2726
+ result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
2727
+ }
2728
+ if (options?.onComplete) options.onComplete(result);
2729
+ return result;
2730
+ } catch (error) {
2731
+ if (options?.onError && error instanceof Error) options.onError(error);
2732
+ throw error;
2733
+ }
2734
+ }
2735
+ async executeWithStreaming(command, sessionId, options, startTime, timestamp) {
2736
+ let stdout = "";
2737
+ let stderr = "";
2738
+ try {
2739
+ const stream = await this.client.commands.executeStream(command, sessionId);
2740
+ for await (const event of parseSSEStream(stream)) {
2741
+ if (options.signal?.aborted) throw new Error("Operation was aborted");
2742
+ switch (event.type) {
2743
+ case "stdout":
2744
+ case "stderr":
2745
+ if (event.data) {
2746
+ if (event.type === "stdout") stdout += event.data;
2747
+ if (event.type === "stderr") stderr += event.data;
2748
+ if (options.onOutput) options.onOutput(event.type, event.data);
2749
+ }
2750
+ break;
2751
+ case "complete": {
2752
+ const duration = Date.now() - startTime;
2753
+ return {
2754
+ success: (event.exitCode ?? 0) === 0,
2755
+ exitCode: event.exitCode ?? 0,
2756
+ stdout,
2757
+ stderr,
2758
+ command,
2759
+ duration,
2760
+ timestamp,
2761
+ sessionId
2762
+ };
2763
+ }
2764
+ case "error": throw new Error(event.data || "Command execution failed");
2765
+ }
2766
+ }
2767
+ throw new Error("Stream ended without completion event");
2768
+ } catch (error) {
2769
+ if (options.signal?.aborted) throw new Error("Operation was aborted");
2770
+ throw error;
2771
+ }
2772
+ }
2773
+ mapExecuteResponseToExecResult(response, duration, sessionId) {
2774
+ return {
2775
+ success: response.success,
2776
+ exitCode: response.exitCode,
2777
+ stdout: response.stdout,
2778
+ stderr: response.stderr,
2779
+ command: response.command,
2780
+ duration,
2781
+ timestamp: response.timestamp,
2782
+ sessionId
2783
+ };
2784
+ }
2785
+ /**
2786
+ * Create a Process domain object from HTTP client DTO
2787
+ * Centralizes process object creation with bound methods
2788
+ * This eliminates duplication across startProcess, listProcesses, getProcess, and session wrappers
2789
+ */
2790
+ createProcessFromDTO(data, sessionId) {
2791
+ return {
2792
+ id: data.id,
2793
+ pid: data.pid,
2794
+ command: data.command,
2795
+ status: data.status,
2796
+ startTime: typeof data.startTime === "string" ? new Date(data.startTime) : data.startTime,
2797
+ endTime: data.endTime ? typeof data.endTime === "string" ? new Date(data.endTime) : data.endTime : void 0,
2798
+ exitCode: data.exitCode,
2799
+ sessionId,
2800
+ kill: async (signal) => {
2801
+ await this.killProcess(data.id, signal);
2802
+ },
2803
+ getStatus: async () => {
2804
+ return (await this.getProcess(data.id))?.status || "error";
2805
+ },
2806
+ getLogs: async () => {
2807
+ const logs = await this.getProcessLogs(data.id);
2808
+ return {
2809
+ stdout: logs.stdout,
2810
+ stderr: logs.stderr
2811
+ };
2812
+ }
2813
+ };
2814
+ }
2815
+ async startProcess(command, options, sessionId) {
2816
+ try {
2817
+ const session = sessionId ?? await this.ensureDefaultSession();
2818
+ const response = await this.client.processes.startProcess(command, session, { processId: options?.processId });
2819
+ const processObj = this.createProcessFromDTO({
2820
+ id: response.processId,
2821
+ pid: response.pid,
2822
+ command: response.command,
2823
+ status: "running",
2824
+ startTime: /* @__PURE__ */ new Date(),
2825
+ endTime: void 0,
2826
+ exitCode: void 0
2827
+ }, session);
2828
+ if (options?.onStart) options.onStart(processObj);
2829
+ return processObj;
2830
+ } catch (error) {
2831
+ if (options?.onError && error instanceof Error) options.onError(error);
2832
+ throw error;
2833
+ }
2834
+ }
2835
+ async listProcesses(sessionId) {
2836
+ const session = sessionId ?? await this.ensureDefaultSession();
2837
+ return (await this.client.processes.listProcesses()).processes.map((processData) => this.createProcessFromDTO({
2838
+ id: processData.id,
2839
+ pid: processData.pid,
2840
+ command: processData.command,
2841
+ status: processData.status,
2842
+ startTime: processData.startTime,
2843
+ endTime: processData.endTime,
2844
+ exitCode: processData.exitCode
2845
+ }, session));
2846
+ }
2847
+ async getProcess(id, sessionId) {
2848
+ const session = sessionId ?? await this.ensureDefaultSession();
2849
+ const response = await this.client.processes.getProcess(id);
2850
+ if (!response.process) return null;
2851
+ const processData = response.process;
2852
+ return this.createProcessFromDTO({
2853
+ id: processData.id,
2854
+ pid: processData.pid,
2855
+ command: processData.command,
2856
+ status: processData.status,
2857
+ startTime: processData.startTime,
2858
+ endTime: processData.endTime,
2859
+ exitCode: processData.exitCode
2860
+ }, session);
2861
+ }
2862
+ async killProcess(id, signal, sessionId) {
2863
+ await this.client.processes.killProcess(id);
2864
+ }
2865
+ async killAllProcesses(sessionId) {
2866
+ return (await this.client.processes.killAllProcesses()).cleanedCount;
2867
+ }
2868
+ async cleanupCompletedProcesses(sessionId) {
2869
+ return 0;
2870
+ }
2871
+ async getProcessLogs(id, sessionId) {
2872
+ const response = await this.client.processes.getProcessLogs(id);
2873
+ return {
2874
+ stdout: response.stdout,
2875
+ stderr: response.stderr,
2876
+ processId: response.processId
2877
+ };
2878
+ }
2879
+ async execStream(command, options) {
2880
+ if (options?.signal?.aborted) throw new Error("Operation was aborted");
2881
+ const session = await this.ensureDefaultSession();
2882
+ return this.client.commands.executeStream(command, session);
2883
+ }
2884
+ /**
2885
+ * Internal session-aware execStream implementation
2886
+ */
2887
+ async execStreamWithSession(command, sessionId, options) {
2888
+ if (options?.signal?.aborted) throw new Error("Operation was aborted");
2889
+ return this.client.commands.executeStream(command, sessionId);
2890
+ }
2891
+ /**
2892
+ * Stream logs from a background process as a ReadableStream.
2893
+ */
2894
+ async streamProcessLogs(processId, options) {
2895
+ if (options?.signal?.aborted) throw new Error("Operation was aborted");
2896
+ return this.client.processes.streamProcessLogs(processId);
2897
+ }
2898
+ async gitCheckout(repoUrl, options) {
2899
+ const session = options.sessionId ?? await this.ensureDefaultSession();
2900
+ return this.client.git.checkout(repoUrl, session, {
2901
+ branch: options.branch,
2902
+ targetDir: options.targetDir
2903
+ });
2904
+ }
2905
+ async mkdir(path, options = {}) {
2906
+ const session = options.sessionId ?? await this.ensureDefaultSession();
2907
+ return this.client.files.mkdir(path, session, { recursive: options.recursive });
2908
+ }
2909
+ async writeFile(path, content, options = {}) {
2910
+ const session = options.sessionId ?? await this.ensureDefaultSession();
2911
+ return this.client.files.writeFile(path, content, session, { encoding: options.encoding });
2912
+ }
2913
+ async deleteFile(path, sessionId) {
2914
+ const session = sessionId ?? await this.ensureDefaultSession();
2915
+ return this.client.files.deleteFile(path, session);
2916
+ }
2917
+ async renameFile(oldPath, newPath, sessionId) {
2918
+ const session = sessionId ?? await this.ensureDefaultSession();
2919
+ return this.client.files.renameFile(oldPath, newPath, session);
2920
+ }
2921
+ async moveFile(sourcePath, destinationPath, sessionId) {
2922
+ const session = sessionId ?? await this.ensureDefaultSession();
2923
+ return this.client.files.moveFile(sourcePath, destinationPath, session);
2924
+ }
2925
+ async readFile(path, options = {}) {
2926
+ const session = options.sessionId ?? await this.ensureDefaultSession();
2927
+ return this.client.files.readFile(path, session, { encoding: options.encoding });
2928
+ }
2929
+ /**
2930
+ * Stream a file from the sandbox using Server-Sent Events
2931
+ * Returns a ReadableStream that can be consumed with streamFile() or collectFile() utilities
2932
+ * @param path - Path to the file to stream
2933
+ * @param options - Optional session ID
2934
+ */
2935
+ async readFileStream(path, options = {}) {
2936
+ const session = options.sessionId ?? await this.ensureDefaultSession();
2937
+ return this.client.files.readFileStream(path, session);
2938
+ }
2939
+ async listFiles(path, options) {
2940
+ const session = await this.ensureDefaultSession();
2941
+ return this.client.files.listFiles(path, session, options);
2942
+ }
2943
+ async exists(path, sessionId) {
2944
+ const session = sessionId ?? await this.ensureDefaultSession();
2945
+ return this.client.files.exists(path, session);
2946
+ }
2947
+ async exposePort(port, options) {
2948
+ if (options.hostname.endsWith(".workers.dev")) throw new CustomDomainRequiredError({
2949
+ code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
2950
+ message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
2951
+ context: { originalError: options.hostname },
2952
+ httpStatus: 400,
2953
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2954
+ });
2955
+ const sessionId = await this.ensureDefaultSession();
2956
+ await this.client.ports.exposePort(port, sessionId, options?.name);
2957
+ if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
2958
+ const token = this.generatePortToken();
2959
+ this.portTokens.set(port, token);
2960
+ await this.persistPortTokens();
2961
+ return {
2962
+ url: this.constructPreviewUrl(port, this.sandboxName, options.hostname, token),
2963
+ port,
2964
+ name: options?.name
2965
+ };
2966
+ }
2967
+ async unexposePort(port) {
2968
+ if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
2969
+ const sessionId = await this.ensureDefaultSession();
2970
+ await this.client.ports.unexposePort(port, sessionId);
2971
+ if (this.portTokens.has(port)) {
2972
+ this.portTokens.delete(port);
2973
+ await this.persistPortTokens();
2974
+ }
2975
+ }
2976
+ async getExposedPorts(hostname) {
2977
+ const sessionId = await this.ensureDefaultSession();
2978
+ const response = await this.client.ports.getExposedPorts(sessionId);
2979
+ if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
2980
+ return response.ports.map((port) => {
2981
+ const token = this.portTokens.get(port.port);
2982
+ if (!token) throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
2983
+ return {
2984
+ url: this.constructPreviewUrl(port.port, this.sandboxName, hostname, token),
2985
+ port: port.port,
2986
+ status: port.status
2987
+ };
2988
+ });
2989
+ }
2990
+ async isPortExposed(port) {
2991
+ try {
2992
+ const sessionId = await this.ensureDefaultSession();
2993
+ return (await this.client.ports.getExposedPorts(sessionId)).ports.some((exposedPort) => exposedPort.port === port);
2994
+ } catch (error) {
2995
+ this.logger.error("Error checking if port is exposed", error instanceof Error ? error : new Error(String(error)), { port });
2996
+ return false;
2997
+ }
2998
+ }
2999
+ async validatePortToken(port, token) {
3000
+ if (!await this.isPortExposed(port)) return false;
3001
+ const storedToken = this.portTokens.get(port);
3002
+ if (!storedToken) {
3003
+ this.logger.error("Port is exposed but has no token - bug detected", void 0, { port });
3004
+ return false;
3005
+ }
3006
+ return storedToken === token;
3007
+ }
3008
+ generatePortToken() {
3009
+ const array = new Uint8Array(12);
3010
+ crypto.getRandomValues(array);
3011
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
3012
+ }
3013
+ async persistPortTokens() {
3014
+ const tokensObj = {};
3015
+ for (const [port, token] of this.portTokens.entries()) tokensObj[port.toString()] = token;
3016
+ await this.ctx.storage.put("portTokens", tokensObj);
3017
+ }
3018
+ constructPreviewUrl(port, sandboxId, hostname, token) {
3019
+ if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
3020
+ const sanitizedSandboxId = sanitizeSandboxId(sandboxId);
3021
+ if (isLocalhostPattern(hostname)) {
3022
+ const [host, portStr] = hostname.split(":");
3023
+ const mainPort = portStr || "80";
3024
+ try {
3025
+ const baseUrl = new URL(`http://${host}:${mainPort}`);
3026
+ baseUrl.hostname = `${port}-${sanitizedSandboxId}-${token}.${host}`;
3027
+ return baseUrl.toString();
3028
+ } catch (error) {
3029
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
3030
+ }
3031
+ }
3032
+ try {
3033
+ const baseUrl = new URL(`https://${hostname}`);
3034
+ baseUrl.hostname = `${port}-${sanitizedSandboxId}-${token}.${hostname}`;
3035
+ return baseUrl.toString();
3036
+ } catch (error) {
3037
+ throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : "Unknown error"}`);
3038
+ }
3039
+ }
3040
+ /**
3041
+ * Create isolated execution session for advanced use cases
3042
+ * Returns ExecutionSession with full sandbox API bound to specific session
3043
+ */
3044
+ async createSession(options) {
3045
+ const sessionId = options?.id || `session-${Date.now()}`;
3046
+ await this.client.utils.createSession({
3047
+ id: sessionId,
3048
+ env: options?.env,
3049
+ cwd: options?.cwd
3050
+ });
3051
+ return this.getSessionWrapper(sessionId);
3052
+ }
3053
+ /**
3054
+ * Get an existing session by ID
3055
+ * Returns ExecutionSession wrapper bound to the specified session
3056
+ *
3057
+ * This is useful for retrieving sessions across different requests/contexts
3058
+ * without storing the ExecutionSession object (which has RPC lifecycle limitations)
3059
+ *
3060
+ * @param sessionId - The ID of an existing session
3061
+ * @returns ExecutionSession wrapper bound to the session
3062
+ */
3063
+ async getSession(sessionId) {
3064
+ return this.getSessionWrapper(sessionId);
3065
+ }
3066
+ /**
3067
+ * Delete an execution session
3068
+ * Cleans up session resources and removes it from the container
3069
+ * Note: Cannot delete the default session. To reset the default session,
3070
+ * use sandbox.destroy() to terminate the entire sandbox.
3071
+ *
3072
+ * @param sessionId - The ID of the session to delete
3073
+ * @returns Result with success status, sessionId, and timestamp
3074
+ * @throws Error if attempting to delete the default session
3075
+ */
3076
+ async deleteSession(sessionId) {
3077
+ if (this.defaultSession && sessionId === this.defaultSession) throw new Error(`Cannot delete default session '${sessionId}'. Use sandbox.destroy() to terminate the sandbox.`);
3078
+ const response = await this.client.utils.deleteSession(sessionId);
3079
+ return {
3080
+ success: response.success,
3081
+ sessionId: response.sessionId,
3082
+ timestamp: response.timestamp
3083
+ };
3084
+ }
3085
+ /**
3086
+ * Internal helper to create ExecutionSession wrapper for a given sessionId
3087
+ * Used by both createSession and getSession
3088
+ */
3089
+ getSessionWrapper(sessionId) {
3090
+ return {
3091
+ id: sessionId,
3092
+ exec: (command, options) => this.execWithSession(command, sessionId, options),
3093
+ execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
3094
+ startProcess: (command, options) => this.startProcess(command, options, sessionId),
3095
+ listProcesses: () => this.listProcesses(sessionId),
3096
+ getProcess: (id) => this.getProcess(id, sessionId),
3097
+ killProcess: (id, signal) => this.killProcess(id, signal),
3098
+ killAllProcesses: () => this.killAllProcesses(),
3099
+ cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
3100
+ getProcessLogs: (id) => this.getProcessLogs(id),
3101
+ streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
3102
+ writeFile: (path, content, options) => this.writeFile(path, content, {
3103
+ ...options,
3104
+ sessionId
3105
+ }),
3106
+ readFile: (path, options) => this.readFile(path, {
3107
+ ...options,
3108
+ sessionId
3109
+ }),
3110
+ readFileStream: (path) => this.readFileStream(path, { sessionId }),
3111
+ mkdir: (path, options) => this.mkdir(path, {
3112
+ ...options,
3113
+ sessionId
3114
+ }),
3115
+ deleteFile: (path) => this.deleteFile(path, sessionId),
3116
+ renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
3117
+ moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
3118
+ listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
3119
+ exists: (path) => this.exists(path, sessionId),
3120
+ gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, {
3121
+ ...options,
3122
+ sessionId
3123
+ }),
3124
+ setEnvVars: async (envVars) => {
3125
+ try {
3126
+ for (const [key, value] of Object.entries(envVars)) {
3127
+ const exportCommand = `export ${key}='${value.replace(/'/g, "'\\''")}'`;
3128
+ const result = await this.client.commands.execute(exportCommand, sessionId);
3129
+ if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
3130
+ }
3131
+ } catch (error) {
3132
+ this.logger.error("Failed to set environment variables", error instanceof Error ? error : new Error(String(error)), { sessionId });
3133
+ throw error;
3134
+ }
3135
+ },
3136
+ createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
3137
+ runCode: async (code, options) => {
3138
+ return (await this.codeInterpreter.runCode(code, options)).toJSON();
3139
+ },
3140
+ runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
3141
+ listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
3142
+ deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId)
3143
+ };
3144
+ }
3145
+ async createCodeContext(options) {
3146
+ return this.codeInterpreter.createCodeContext(options);
3147
+ }
3148
+ async runCode(code, options) {
3149
+ return (await this.codeInterpreter.runCode(code, options)).toJSON();
3150
+ }
3151
+ async runCodeStream(code, options) {
3152
+ return this.codeInterpreter.runCodeStream(code, options);
3153
+ }
3154
+ async listCodeContexts() {
3155
+ return this.codeInterpreter.listCodeContexts();
3156
+ }
3157
+ async deleteCodeContext(contextId) {
3158
+ return this.codeInterpreter.deleteCodeContext(contextId);
3159
+ }
3160
+ };
3161
+
3162
+ //#endregion
3163
+ //#region src/file-stream.ts
3164
+ /**
3165
+ * Parse SSE (Server-Sent Events) lines from a stream
3166
+ */
3167
+ async function* parseSSE(stream) {
3168
+ const reader = stream.getReader();
3169
+ const decoder = new TextDecoder();
3170
+ let buffer = "";
3171
+ try {
3172
+ while (true) {
3173
+ const { done, value } = await reader.read();
3174
+ if (done) break;
3175
+ buffer += decoder.decode(value, { stream: true });
3176
+ const lines = buffer.split("\n");
3177
+ buffer = lines.pop() || "";
3178
+ for (const line of lines) if (line.startsWith("data: ")) {
3179
+ const data = line.slice(6);
3180
+ try {
3181
+ yield JSON.parse(data);
3182
+ } catch {}
3183
+ }
3184
+ }
3185
+ } finally {
3186
+ reader.releaseLock();
3187
+ }
3188
+ }
3189
+ /**
3190
+ * Stream a file from the sandbox with automatic base64 decoding for binary files
3191
+ *
3192
+ * @param stream - The ReadableStream from readFileStream()
3193
+ * @returns AsyncGenerator that yields FileChunk (string for text, Uint8Array for binary)
3194
+ *
3195
+ * @example
3196
+ * ```ts
3197
+ * const stream = await sandbox.readFileStream('/path/to/file.png');
3198
+ * for await (const chunk of streamFile(stream)) {
3199
+ * if (chunk instanceof Uint8Array) {
3200
+ * // Binary chunk
3201
+ * console.log('Binary chunk:', chunk.length, 'bytes');
3202
+ * } else {
3203
+ * // Text chunk
3204
+ * console.log('Text chunk:', chunk);
3205
+ * }
3206
+ * }
3207
+ * ```
3208
+ */
3209
+ async function* streamFile(stream) {
3210
+ let metadata = null;
3211
+ for await (const event of parseSSE(stream)) switch (event.type) {
3212
+ case "metadata":
3213
+ metadata = {
3214
+ mimeType: event.mimeType,
3215
+ size: event.size,
3216
+ isBinary: event.isBinary,
3217
+ encoding: event.encoding
3218
+ };
3219
+ break;
3220
+ case "chunk":
3221
+ if (!metadata) throw new Error("Received chunk before metadata");
3222
+ if (metadata.isBinary && metadata.encoding === "base64") {
3223
+ const binaryString = atob(event.data);
3224
+ const bytes = new Uint8Array(binaryString.length);
3225
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
3226
+ yield bytes;
3227
+ } else yield event.data;
3228
+ break;
3229
+ case "complete":
3230
+ if (!metadata) throw new Error("Stream completed without metadata");
3231
+ return metadata;
3232
+ case "error": throw new Error(`File streaming error: ${event.error}`);
3233
+ }
3234
+ throw new Error("Stream ended unexpectedly");
3235
+ }
3236
+ /**
3237
+ * Collect an entire file into memory from a stream
3238
+ *
3239
+ * @param stream - The ReadableStream from readFileStream()
3240
+ * @returns Object containing the file content and metadata
3241
+ *
3242
+ * @example
3243
+ * ```ts
3244
+ * const stream = await sandbox.readFileStream('/path/to/file.txt');
3245
+ * const { content, metadata } = await collectFile(stream);
3246
+ * console.log('Content:', content);
3247
+ * console.log('MIME type:', metadata.mimeType);
3248
+ * ```
3249
+ */
3250
+ async function collectFile(stream) {
3251
+ const chunks = [];
3252
+ const generator = streamFile(stream);
3253
+ let result = await generator.next();
3254
+ while (!result.done) {
3255
+ chunks.push(result.value);
3256
+ result = await generator.next();
3257
+ }
3258
+ const metadata = result.value;
3259
+ if (!metadata) throw new Error("Failed to get file metadata");
3260
+ if (metadata.isBinary) {
3261
+ const totalLength = chunks.reduce((sum, chunk) => sum + (chunk instanceof Uint8Array ? chunk.length : 0), 0);
3262
+ const combined = new Uint8Array(totalLength);
3263
+ let offset = 0;
3264
+ for (const chunk of chunks) if (chunk instanceof Uint8Array) {
3265
+ combined.set(chunk, offset);
3266
+ offset += chunk.length;
3267
+ }
3268
+ return {
3269
+ content: combined,
3270
+ metadata
3271
+ };
3272
+ } else return {
3273
+ content: chunks.filter((c) => typeof c === "string").join(""),
3274
+ metadata
3275
+ };
3276
+ }
3277
+
3278
+ //#endregion
3279
+ export { CodeInterpreter, CommandClient, Execution, FileClient, GitClient, GitLogger, LogLevel as LogLevelEnum, PortClient, ProcessClient, ResultImpl, Sandbox, SandboxClient, TraceContext, UtilityClient, asyncIterableToSSEStream, collectFile, createLogger, createNoOpLogger, getLogger, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyToSandbox, redactCredentials, responseToAsyncIterable, runWithLogger, sanitizeGitData, streamFile };
55
3280
  //# sourceMappingURL=index.js.map