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