@cloudflare/sandbox 0.4.18 → 0.5.2
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 +17 -9
- package/CHANGELOG.md +64 -0
- package/Dockerfile +5 -1
- package/LICENSE +176 -0
- package/README.md +1 -1
- package/dist/dist-gVyG2H2h.js +612 -0
- package/dist/dist-gVyG2H2h.js.map +1 -0
- package/dist/index.d.ts +94 -1834
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +489 -678
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +67 -0
- package/dist/openai/index.d.ts.map +1 -0
- package/dist/openai/index.js +362 -0
- package/dist/openai/index.js.map +1 -0
- package/dist/sandbox-B3vJ541e.d.ts +1729 -0
- package/dist/sandbox-B3vJ541e.d.ts.map +1 -0
- package/package.json +16 -2
- package/src/clients/base-client.ts +107 -46
- package/src/index.ts +19 -2
- package/src/openai/index.ts +465 -0
- package/src/request-handler.ts +2 -1
- package/src/sandbox.ts +684 -62
- package/src/storage-mount/credential-detection.ts +41 -0
- package/src/storage-mount/errors.ts +51 -0
- package/src/storage-mount/index.ts +17 -0
- package/src/storage-mount/provider-detection.ts +93 -0
- package/src/storage-mount/types.ts +17 -0
- package/src/version.ts +1 -1
- package/tests/base-client.test.ts +218 -0
- package/tests/get-sandbox.test.ts +24 -1
- package/tests/git-client.test.ts +7 -39
- package/tests/openai-shell-editor.test.ts +434 -0
- package/tests/port-client.test.ts +25 -35
- package/tests/process-client.test.ts +73 -107
- package/tests/sandbox.test.ts +128 -1
- package/tests/storage-mount/credential-detection.test.ts +119 -0
- package/tests/storage-mount/provider-detection.test.ts +77 -0
- package/tsconfig.json +2 -2
- package/tsdown.config.ts +3 -2
package/dist/index.js
CHANGED
|
@@ -1,625 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as createLogger, c as Execution, d as getEnvString, i as shellEscape, l as ResultImpl, n as isProcess, o as createNoOpLogger, r as isProcessStatus, s as TraceContext, t as isExecResult, u as GitLogger } from "./dist-gVyG2H2h.js";
|
|
2
2
|
import { Container, getContainer, switchPort } from "@cloudflare/containers";
|
|
3
3
|
|
|
4
|
-
//#region ../shared/dist/git.js
|
|
5
|
-
/**
|
|
6
|
-
* Redact credentials from URLs for secure logging
|
|
7
|
-
*
|
|
8
|
-
* Replaces any credentials (username:password, tokens, etc.) embedded
|
|
9
|
-
* in URLs with ****** to prevent sensitive data exposure in logs.
|
|
10
|
-
* Works with URLs embedded in text (e.g., "Error: https://token@github.com/repo.git failed")
|
|
11
|
-
*
|
|
12
|
-
* @param text - String that may contain URLs with credentials
|
|
13
|
-
* @returns String with credentials redacted from any URLs
|
|
14
|
-
*/
|
|
15
|
-
function redactCredentials(text) {
|
|
16
|
-
let result = text;
|
|
17
|
-
let pos = 0;
|
|
18
|
-
while (pos < result.length) {
|
|
19
|
-
const httpPos = result.indexOf("http://", pos);
|
|
20
|
-
const httpsPos = result.indexOf("https://", pos);
|
|
21
|
-
let protocolPos = -1;
|
|
22
|
-
let protocolLen = 0;
|
|
23
|
-
if (httpPos === -1 && httpsPos === -1) break;
|
|
24
|
-
if (httpPos !== -1 && (httpsPos === -1 || httpPos < httpsPos)) {
|
|
25
|
-
protocolPos = httpPos;
|
|
26
|
-
protocolLen = 7;
|
|
27
|
-
} else {
|
|
28
|
-
protocolPos = httpsPos;
|
|
29
|
-
protocolLen = 8;
|
|
30
|
-
}
|
|
31
|
-
const searchStart = protocolPos + protocolLen;
|
|
32
|
-
const atPos = result.indexOf("@", searchStart);
|
|
33
|
-
let urlEnd = searchStart;
|
|
34
|
-
while (urlEnd < result.length) {
|
|
35
|
-
const char = result[urlEnd];
|
|
36
|
-
if (/[\s"'`<>,;{}[\]]/.test(char)) break;
|
|
37
|
-
urlEnd++;
|
|
38
|
-
}
|
|
39
|
-
if (atPos !== -1 && atPos < urlEnd) {
|
|
40
|
-
result = `${result.substring(0, searchStart)}******${result.substring(atPos)}`;
|
|
41
|
-
pos = searchStart + 6;
|
|
42
|
-
} else pos = protocolPos + protocolLen;
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Sanitize data by redacting credentials from any strings
|
|
48
|
-
* Recursively processes objects and arrays to ensure credentials are never leaked
|
|
49
|
-
*/
|
|
50
|
-
function sanitizeGitData(data) {
|
|
51
|
-
if (typeof data === "string") return redactCredentials(data);
|
|
52
|
-
if (data === null || data === void 0) return data;
|
|
53
|
-
if (Array.isArray(data)) return data.map((item) => sanitizeGitData(item));
|
|
54
|
-
if (typeof data === "object") {
|
|
55
|
-
const result = {};
|
|
56
|
-
for (const [key, value] of Object.entries(data)) result[key] = sanitizeGitData(value);
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
return data;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Logger wrapper that automatically sanitizes git credentials
|
|
63
|
-
*/
|
|
64
|
-
var GitLogger = class GitLogger {
|
|
65
|
-
baseLogger;
|
|
66
|
-
constructor(baseLogger) {
|
|
67
|
-
this.baseLogger = baseLogger;
|
|
68
|
-
}
|
|
69
|
-
sanitizeContext(context) {
|
|
70
|
-
return context ? sanitizeGitData(context) : context;
|
|
71
|
-
}
|
|
72
|
-
sanitizeError(error) {
|
|
73
|
-
if (!error) return error;
|
|
74
|
-
const sanitized = new Error(redactCredentials(error.message));
|
|
75
|
-
sanitized.name = error.name;
|
|
76
|
-
if (error.stack) sanitized.stack = redactCredentials(error.stack);
|
|
77
|
-
const sanitizedRecord = sanitized;
|
|
78
|
-
const errorRecord = error;
|
|
79
|
-
for (const key of Object.keys(error)) if (key !== "message" && key !== "stack" && key !== "name") sanitizedRecord[key] = sanitizeGitData(errorRecord[key]);
|
|
80
|
-
return sanitized;
|
|
81
|
-
}
|
|
82
|
-
debug(message, context) {
|
|
83
|
-
this.baseLogger.debug(message, this.sanitizeContext(context));
|
|
84
|
-
}
|
|
85
|
-
info(message, context) {
|
|
86
|
-
this.baseLogger.info(message, this.sanitizeContext(context));
|
|
87
|
-
}
|
|
88
|
-
warn(message, context) {
|
|
89
|
-
this.baseLogger.warn(message, this.sanitizeContext(context));
|
|
90
|
-
}
|
|
91
|
-
error(message, error, context) {
|
|
92
|
-
this.baseLogger.error(message, this.sanitizeError(error), this.sanitizeContext(context));
|
|
93
|
-
}
|
|
94
|
-
child(context) {
|
|
95
|
-
const sanitized = sanitizeGitData(context);
|
|
96
|
-
return new GitLogger(this.baseLogger.child(sanitized));
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region ../shared/dist/interpreter-types.js
|
|
102
|
-
var Execution = class {
|
|
103
|
-
code;
|
|
104
|
-
context;
|
|
105
|
-
/**
|
|
106
|
-
* All results from the execution
|
|
107
|
-
*/
|
|
108
|
-
results = [];
|
|
109
|
-
/**
|
|
110
|
-
* Accumulated stdout and stderr
|
|
111
|
-
*/
|
|
112
|
-
logs = {
|
|
113
|
-
stdout: [],
|
|
114
|
-
stderr: []
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* Execution error if any
|
|
118
|
-
*/
|
|
119
|
-
error;
|
|
120
|
-
/**
|
|
121
|
-
* Execution count (for interpreter)
|
|
122
|
-
*/
|
|
123
|
-
executionCount;
|
|
124
|
-
constructor(code, context) {
|
|
125
|
-
this.code = code;
|
|
126
|
-
this.context = context;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Convert to a plain object for serialization
|
|
130
|
-
*/
|
|
131
|
-
toJSON() {
|
|
132
|
-
return {
|
|
133
|
-
code: this.code,
|
|
134
|
-
logs: this.logs,
|
|
135
|
-
error: this.error,
|
|
136
|
-
executionCount: this.executionCount,
|
|
137
|
-
results: this.results.map((result) => ({
|
|
138
|
-
text: result.text,
|
|
139
|
-
html: result.html,
|
|
140
|
-
png: result.png,
|
|
141
|
-
jpeg: result.jpeg,
|
|
142
|
-
svg: result.svg,
|
|
143
|
-
latex: result.latex,
|
|
144
|
-
markdown: result.markdown,
|
|
145
|
-
javascript: result.javascript,
|
|
146
|
-
json: result.json,
|
|
147
|
-
chart: result.chart,
|
|
148
|
-
data: result.data
|
|
149
|
-
}))
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
var ResultImpl = class {
|
|
154
|
-
raw;
|
|
155
|
-
constructor(raw) {
|
|
156
|
-
this.raw = raw;
|
|
157
|
-
}
|
|
158
|
-
get text() {
|
|
159
|
-
return this.raw.text || this.raw.data?.["text/plain"];
|
|
160
|
-
}
|
|
161
|
-
get html() {
|
|
162
|
-
return this.raw.html || this.raw.data?.["text/html"];
|
|
163
|
-
}
|
|
164
|
-
get png() {
|
|
165
|
-
return this.raw.png || this.raw.data?.["image/png"];
|
|
166
|
-
}
|
|
167
|
-
get jpeg() {
|
|
168
|
-
return this.raw.jpeg || this.raw.data?.["image/jpeg"];
|
|
169
|
-
}
|
|
170
|
-
get svg() {
|
|
171
|
-
return this.raw.svg || this.raw.data?.["image/svg+xml"];
|
|
172
|
-
}
|
|
173
|
-
get latex() {
|
|
174
|
-
return this.raw.latex || this.raw.data?.["text/latex"];
|
|
175
|
-
}
|
|
176
|
-
get markdown() {
|
|
177
|
-
return this.raw.markdown || this.raw.data?.["text/markdown"];
|
|
178
|
-
}
|
|
179
|
-
get javascript() {
|
|
180
|
-
return this.raw.javascript || this.raw.data?.["application/javascript"];
|
|
181
|
-
}
|
|
182
|
-
get json() {
|
|
183
|
-
return this.raw.json || this.raw.data?.["application/json"];
|
|
184
|
-
}
|
|
185
|
-
get chart() {
|
|
186
|
-
return this.raw.chart;
|
|
187
|
-
}
|
|
188
|
-
get data() {
|
|
189
|
-
return this.raw.data;
|
|
190
|
-
}
|
|
191
|
-
formats() {
|
|
192
|
-
const formats = [];
|
|
193
|
-
if (this.text) formats.push("text");
|
|
194
|
-
if (this.html) formats.push("html");
|
|
195
|
-
if (this.png) formats.push("png");
|
|
196
|
-
if (this.jpeg) formats.push("jpeg");
|
|
197
|
-
if (this.svg) formats.push("svg");
|
|
198
|
-
if (this.latex) formats.push("latex");
|
|
199
|
-
if (this.markdown) formats.push("markdown");
|
|
200
|
-
if (this.javascript) formats.push("javascript");
|
|
201
|
-
if (this.json) formats.push("json");
|
|
202
|
-
if (this.chart) formats.push("chart");
|
|
203
|
-
return formats;
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region ../shared/dist/logger/types.js
|
|
209
|
-
/**
|
|
210
|
-
* Logger types for Cloudflare Sandbox SDK
|
|
211
|
-
*
|
|
212
|
-
* Provides structured, trace-aware logging across Worker, Durable Object, and Container.
|
|
213
|
-
*/
|
|
214
|
-
/**
|
|
215
|
-
* Log levels (from most to least verbose)
|
|
216
|
-
*/
|
|
217
|
-
var LogLevel;
|
|
218
|
-
(function(LogLevel$1) {
|
|
219
|
-
LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
|
|
220
|
-
LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
|
|
221
|
-
LogLevel$1[LogLevel$1["WARN"] = 2] = "WARN";
|
|
222
|
-
LogLevel$1[LogLevel$1["ERROR"] = 3] = "ERROR";
|
|
223
|
-
})(LogLevel || (LogLevel = {}));
|
|
224
|
-
|
|
225
|
-
//#endregion
|
|
226
|
-
//#region ../shared/dist/logger/logger.js
|
|
227
|
-
/**
|
|
228
|
-
* ANSI color codes for terminal output
|
|
229
|
-
*/
|
|
230
|
-
const COLORS = {
|
|
231
|
-
reset: "\x1B[0m",
|
|
232
|
-
debug: "\x1B[36m",
|
|
233
|
-
info: "\x1B[32m",
|
|
234
|
-
warn: "\x1B[33m",
|
|
235
|
-
error: "\x1B[31m",
|
|
236
|
-
dim: "\x1B[2m"
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* CloudflareLogger implements structured logging with support for
|
|
240
|
-
* both JSON output (production) and pretty printing (development).
|
|
241
|
-
*/
|
|
242
|
-
var CloudflareLogger = class CloudflareLogger {
|
|
243
|
-
baseContext;
|
|
244
|
-
minLevel;
|
|
245
|
-
pretty;
|
|
246
|
-
/**
|
|
247
|
-
* Create a new CloudflareLogger
|
|
248
|
-
*
|
|
249
|
-
* @param baseContext Base context included in all log entries
|
|
250
|
-
* @param minLevel Minimum log level to output (default: INFO)
|
|
251
|
-
* @param pretty Enable pretty printing for human-readable output (default: false)
|
|
252
|
-
*/
|
|
253
|
-
constructor(baseContext, minLevel = LogLevel.INFO, pretty = false) {
|
|
254
|
-
this.baseContext = baseContext;
|
|
255
|
-
this.minLevel = minLevel;
|
|
256
|
-
this.pretty = pretty;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Log debug-level message
|
|
260
|
-
*/
|
|
261
|
-
debug(message, context) {
|
|
262
|
-
if (this.shouldLog(LogLevel.DEBUG)) {
|
|
263
|
-
const logData = this.buildLogData("debug", message, context);
|
|
264
|
-
this.output(console.log, logData);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Log info-level message
|
|
269
|
-
*/
|
|
270
|
-
info(message, context) {
|
|
271
|
-
if (this.shouldLog(LogLevel.INFO)) {
|
|
272
|
-
const logData = this.buildLogData("info", message, context);
|
|
273
|
-
this.output(console.log, logData);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Log warning-level message
|
|
278
|
-
*/
|
|
279
|
-
warn(message, context) {
|
|
280
|
-
if (this.shouldLog(LogLevel.WARN)) {
|
|
281
|
-
const logData = this.buildLogData("warn", message, context);
|
|
282
|
-
this.output(console.warn, logData);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Log error-level message
|
|
287
|
-
*/
|
|
288
|
-
error(message, error, context) {
|
|
289
|
-
if (this.shouldLog(LogLevel.ERROR)) {
|
|
290
|
-
const logData = this.buildLogData("error", message, context, error);
|
|
291
|
-
this.output(console.error, logData);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Create a child logger with additional context
|
|
296
|
-
*/
|
|
297
|
-
child(context) {
|
|
298
|
-
return new CloudflareLogger({
|
|
299
|
-
...this.baseContext,
|
|
300
|
-
...context
|
|
301
|
-
}, this.minLevel, this.pretty);
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Check if a log level should be output
|
|
305
|
-
*/
|
|
306
|
-
shouldLog(level) {
|
|
307
|
-
return level >= this.minLevel;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Build log data object
|
|
311
|
-
*/
|
|
312
|
-
buildLogData(level, message, context, error) {
|
|
313
|
-
const logData = {
|
|
314
|
-
level,
|
|
315
|
-
msg: message,
|
|
316
|
-
...this.baseContext,
|
|
317
|
-
...context,
|
|
318
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
319
|
-
};
|
|
320
|
-
if (error) logData.error = {
|
|
321
|
-
message: error.message,
|
|
322
|
-
stack: error.stack,
|
|
323
|
-
name: error.name
|
|
324
|
-
};
|
|
325
|
-
return logData;
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Output log data to console (pretty or JSON)
|
|
329
|
-
*/
|
|
330
|
-
output(consoleFn, data) {
|
|
331
|
-
if (this.pretty) this.outputPretty(consoleFn, data);
|
|
332
|
-
else this.outputJson(consoleFn, data);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Output as JSON (production)
|
|
336
|
-
*/
|
|
337
|
-
outputJson(consoleFn, data) {
|
|
338
|
-
consoleFn(JSON.stringify(data));
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Output as pretty-printed, colored text (development)
|
|
342
|
-
*
|
|
343
|
-
* Format: LEVEL [component] message (trace: tr_...) {context}
|
|
344
|
-
* Example: INFO [sandbox-do] Command started (trace: tr_7f3a9b2c) {commandId: "cmd-123"}
|
|
345
|
-
*/
|
|
346
|
-
outputPretty(consoleFn, data) {
|
|
347
|
-
const { level, msg, timestamp, traceId, component, sandboxId, sessionId, processId, commandId, operation, duration, error,...rest } = data;
|
|
348
|
-
const levelStr = String(level || "INFO").toUpperCase();
|
|
349
|
-
const levelColor = this.getLevelColor(levelStr);
|
|
350
|
-
const componentBadge = component ? `[${component}]` : "";
|
|
351
|
-
const traceIdShort = traceId ? String(traceId).substring(0, 12) : "";
|
|
352
|
-
let logLine = `${levelColor}${levelStr.padEnd(5)}${COLORS.reset} ${componentBadge} ${msg}`;
|
|
353
|
-
if (traceIdShort) logLine += ` ${COLORS.dim}(trace: ${traceIdShort})${COLORS.reset}`;
|
|
354
|
-
const contextFields = [];
|
|
355
|
-
if (operation) contextFields.push(`operation: ${operation}`);
|
|
356
|
-
if (commandId) contextFields.push(`commandId: ${String(commandId).substring(0, 12)}`);
|
|
357
|
-
if (sandboxId) contextFields.push(`sandboxId: ${sandboxId}`);
|
|
358
|
-
if (sessionId) contextFields.push(`sessionId: ${String(sessionId).substring(0, 12)}`);
|
|
359
|
-
if (processId) contextFields.push(`processId: ${processId}`);
|
|
360
|
-
if (duration !== void 0) contextFields.push(`duration: ${duration}ms`);
|
|
361
|
-
if (contextFields.length > 0) logLine += ` ${COLORS.dim}{${contextFields.join(", ")}}${COLORS.reset}`;
|
|
362
|
-
consoleFn(logLine);
|
|
363
|
-
if (error && typeof error === "object") {
|
|
364
|
-
const errorObj = error;
|
|
365
|
-
if (errorObj.message) consoleFn(` ${COLORS.error}Error: ${errorObj.message}${COLORS.reset}`);
|
|
366
|
-
if (errorObj.stack) consoleFn(` ${COLORS.dim}${errorObj.stack}${COLORS.reset}`);
|
|
367
|
-
}
|
|
368
|
-
if (Object.keys(rest).length > 0) consoleFn(` ${COLORS.dim}${JSON.stringify(rest, null, 2)}${COLORS.reset}`);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Get ANSI color code for log level
|
|
372
|
-
*/
|
|
373
|
-
getLevelColor(level) {
|
|
374
|
-
switch (level.toLowerCase()) {
|
|
375
|
-
case "debug": return COLORS.debug;
|
|
376
|
-
case "info": return COLORS.info;
|
|
377
|
-
case "warn": return COLORS.warn;
|
|
378
|
-
case "error": return COLORS.error;
|
|
379
|
-
default: return COLORS.reset;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
//#endregion
|
|
385
|
-
//#region ../shared/dist/logger/trace-context.js
|
|
386
|
-
/**
|
|
387
|
-
* Trace context utilities for request correlation
|
|
388
|
-
*
|
|
389
|
-
* Trace IDs enable correlating logs across distributed components:
|
|
390
|
-
* Worker → Durable Object → Container → back
|
|
391
|
-
*
|
|
392
|
-
* The trace ID is propagated via the X-Trace-Id HTTP header.
|
|
393
|
-
*/
|
|
394
|
-
/**
|
|
395
|
-
* Utility for managing trace context across distributed components
|
|
396
|
-
*/
|
|
397
|
-
var TraceContext = class TraceContext {
|
|
398
|
-
/**
|
|
399
|
-
* HTTP header name for trace ID propagation
|
|
400
|
-
*/
|
|
401
|
-
static TRACE_HEADER = "X-Trace-Id";
|
|
402
|
-
/**
|
|
403
|
-
* Generate a new trace ID
|
|
404
|
-
*
|
|
405
|
-
* Format: "tr_" + 16 random hex characters
|
|
406
|
-
* Example: "tr_7f3a9b2c4e5d6f1a"
|
|
407
|
-
*
|
|
408
|
-
* @returns Newly generated trace ID
|
|
409
|
-
*/
|
|
410
|
-
static generate() {
|
|
411
|
-
return `tr_${crypto.randomUUID().replace(/-/g, "").substring(0, 16)}`;
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Extract trace ID from HTTP request headers
|
|
415
|
-
*
|
|
416
|
-
* @param headers Request headers
|
|
417
|
-
* @returns Trace ID if present, null otherwise
|
|
418
|
-
*/
|
|
419
|
-
static fromHeaders(headers) {
|
|
420
|
-
return headers.get(TraceContext.TRACE_HEADER);
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Create headers object with trace ID for outgoing requests
|
|
424
|
-
*
|
|
425
|
-
* @param traceId Trace ID to include
|
|
426
|
-
* @returns Headers object with X-Trace-Id set
|
|
427
|
-
*/
|
|
428
|
-
static toHeaders(traceId) {
|
|
429
|
-
return { [TraceContext.TRACE_HEADER]: traceId };
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Get the header name used for trace ID propagation
|
|
433
|
-
*
|
|
434
|
-
* @returns Header name ("X-Trace-Id")
|
|
435
|
-
*/
|
|
436
|
-
static getHeaderName() {
|
|
437
|
-
return TraceContext.TRACE_HEADER;
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
//#endregion
|
|
442
|
-
//#region ../shared/dist/logger/index.js
|
|
443
|
-
/**
|
|
444
|
-
* Create a no-op logger for testing
|
|
445
|
-
*
|
|
446
|
-
* Returns a logger that implements the Logger interface but does nothing.
|
|
447
|
-
* Useful for tests that don't need actual logging output.
|
|
448
|
-
*
|
|
449
|
-
* @returns No-op logger instance
|
|
450
|
-
*
|
|
451
|
-
* @example
|
|
452
|
-
* ```typescript
|
|
453
|
-
* // In tests
|
|
454
|
-
* const client = new HttpClient({
|
|
455
|
-
* baseUrl: 'http://test.com',
|
|
456
|
-
* logger: createNoOpLogger() // Optional - tests can enable real logging if needed
|
|
457
|
-
* });
|
|
458
|
-
* ```
|
|
459
|
-
*/
|
|
460
|
-
function createNoOpLogger() {
|
|
461
|
-
return {
|
|
462
|
-
debug: () => {},
|
|
463
|
-
info: () => {},
|
|
464
|
-
warn: () => {},
|
|
465
|
-
error: () => {},
|
|
466
|
-
child: () => createNoOpLogger()
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* AsyncLocalStorage for logger context
|
|
471
|
-
*
|
|
472
|
-
* Enables implicit logger propagation throughout the call stack without
|
|
473
|
-
* explicit parameter passing. The logger is stored per async context.
|
|
474
|
-
*/
|
|
475
|
-
const loggerStorage = new AsyncLocalStorage();
|
|
476
|
-
/**
|
|
477
|
-
* Get the current logger from AsyncLocalStorage
|
|
478
|
-
*
|
|
479
|
-
* @throws Error if no logger is initialized in the current async context
|
|
480
|
-
* @returns Current logger instance
|
|
481
|
-
*
|
|
482
|
-
* @example
|
|
483
|
-
* ```typescript
|
|
484
|
-
* function someHelperFunction() {
|
|
485
|
-
* const logger = getLogger(); // Automatically has all context!
|
|
486
|
-
* logger.info('Helper called');
|
|
487
|
-
* }
|
|
488
|
-
* ```
|
|
489
|
-
*/
|
|
490
|
-
function getLogger() {
|
|
491
|
-
const logger = loggerStorage.getStore();
|
|
492
|
-
if (!logger) throw new Error("Logger not initialized in async context. Ensure runWithLogger() is called at the entry point (e.g., fetch handler).");
|
|
493
|
-
return logger;
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Run a function with a logger stored in AsyncLocalStorage
|
|
497
|
-
*
|
|
498
|
-
* The logger is available to all code within the function via getLogger().
|
|
499
|
-
* This is typically called at request entry points (fetch handler) and when
|
|
500
|
-
* creating child loggers with additional context.
|
|
501
|
-
*
|
|
502
|
-
* @param logger Logger instance to store in context
|
|
503
|
-
* @param fn Function to execute with logger context
|
|
504
|
-
* @returns Result of the function
|
|
505
|
-
*
|
|
506
|
-
* @example
|
|
507
|
-
* ```typescript
|
|
508
|
-
* // At request entry point
|
|
509
|
-
* async fetch(request: Request): Promise<Response> {
|
|
510
|
-
* const logger = createLogger({ component: 'sandbox-do', traceId: 'tr_abc' });
|
|
511
|
-
* return runWithLogger(logger, async () => {
|
|
512
|
-
* return await this.handleRequest(request);
|
|
513
|
-
* });
|
|
514
|
-
* }
|
|
515
|
-
*
|
|
516
|
-
* // When adding operation context
|
|
517
|
-
* async exec(command: string) {
|
|
518
|
-
* const logger = getLogger().child({ operation: 'exec', commandId: 'cmd-123' });
|
|
519
|
-
* return runWithLogger(logger, async () => {
|
|
520
|
-
* logger.info('Command started');
|
|
521
|
-
* await this.executeCommand(command); // Nested calls get the child logger
|
|
522
|
-
* logger.info('Command completed');
|
|
523
|
-
* });
|
|
524
|
-
* }
|
|
525
|
-
* ```
|
|
526
|
-
*/
|
|
527
|
-
function runWithLogger(logger, fn) {
|
|
528
|
-
return loggerStorage.run(logger, fn);
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Create a new logger instance
|
|
532
|
-
*
|
|
533
|
-
* @param context Base context for the logger. Must include 'component'.
|
|
534
|
-
* TraceId will be auto-generated if not provided.
|
|
535
|
-
* @returns New logger instance
|
|
536
|
-
*
|
|
537
|
-
* @example
|
|
538
|
-
* ```typescript
|
|
539
|
-
* // In Durable Object
|
|
540
|
-
* const logger = createLogger({
|
|
541
|
-
* component: 'sandbox-do',
|
|
542
|
-
* traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
|
|
543
|
-
* sandboxId: this.id
|
|
544
|
-
* });
|
|
545
|
-
*
|
|
546
|
-
* // In Container
|
|
547
|
-
* const logger = createLogger({
|
|
548
|
-
* component: 'container',
|
|
549
|
-
* traceId: TraceContext.fromHeaders(request.headers)!,
|
|
550
|
-
* sessionId: this.id
|
|
551
|
-
* });
|
|
552
|
-
* ```
|
|
553
|
-
*/
|
|
554
|
-
function createLogger(context) {
|
|
555
|
-
const minLevel = getLogLevelFromEnv();
|
|
556
|
-
const pretty = isPrettyPrintEnabled();
|
|
557
|
-
return new CloudflareLogger({
|
|
558
|
-
...context,
|
|
559
|
-
traceId: context.traceId || TraceContext.generate(),
|
|
560
|
-
component: context.component
|
|
561
|
-
}, minLevel, pretty);
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Get log level from environment variable
|
|
565
|
-
*
|
|
566
|
-
* Checks SANDBOX_LOG_LEVEL env var, falls back to default based on environment.
|
|
567
|
-
* Default: 'debug' for development, 'info' for production
|
|
568
|
-
*/
|
|
569
|
-
function getLogLevelFromEnv() {
|
|
570
|
-
switch ((getEnvVar("SANDBOX_LOG_LEVEL") || "info").toLowerCase()) {
|
|
571
|
-
case "debug": return LogLevel.DEBUG;
|
|
572
|
-
case "info": return LogLevel.INFO;
|
|
573
|
-
case "warn": return LogLevel.WARN;
|
|
574
|
-
case "error": return LogLevel.ERROR;
|
|
575
|
-
default: return LogLevel.INFO;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Check if pretty printing should be enabled
|
|
580
|
-
*
|
|
581
|
-
* Checks SANDBOX_LOG_FORMAT env var, falls back to auto-detection:
|
|
582
|
-
* - Local development: pretty (colored, human-readable)
|
|
583
|
-
* - Production: json (structured)
|
|
584
|
-
*/
|
|
585
|
-
function isPrettyPrintEnabled() {
|
|
586
|
-
const format = getEnvVar("SANDBOX_LOG_FORMAT");
|
|
587
|
-
if (format) return format.toLowerCase() === "pretty";
|
|
588
|
-
return false;
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Get environment variable value
|
|
592
|
-
*
|
|
593
|
-
* Supports both Node.js (process.env) and Bun (Bun.env)
|
|
594
|
-
*/
|
|
595
|
-
function getEnvVar(name) {
|
|
596
|
-
if (typeof process !== "undefined" && process.env) return process.env[name];
|
|
597
|
-
if (typeof Bun !== "undefined") {
|
|
598
|
-
const bunEnv = Bun.env;
|
|
599
|
-
if (bunEnv) return bunEnv[name];
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
//#endregion
|
|
604
|
-
//#region ../shared/dist/types.js
|
|
605
|
-
function isExecResult(value) {
|
|
606
|
-
return value && typeof value.success === "boolean" && typeof value.exitCode === "number" && typeof value.stdout === "string" && typeof value.stderr === "string";
|
|
607
|
-
}
|
|
608
|
-
function isProcess(value) {
|
|
609
|
-
return value && typeof value.id === "string" && typeof value.command === "string" && typeof value.status === "string";
|
|
610
|
-
}
|
|
611
|
-
function isProcessStatus(value) {
|
|
612
|
-
return [
|
|
613
|
-
"starting",
|
|
614
|
-
"running",
|
|
615
|
-
"completed",
|
|
616
|
-
"failed",
|
|
617
|
-
"killed",
|
|
618
|
-
"error"
|
|
619
|
-
].includes(value);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
//#endregion
|
|
623
4
|
//#region ../shared/dist/errors/codes.js
|
|
624
5
|
/**
|
|
625
6
|
* Centralized error code registry
|
|
@@ -662,6 +43,10 @@ const ErrorCode = {
|
|
|
662
43
|
GIT_CLONE_FAILED: "GIT_CLONE_FAILED",
|
|
663
44
|
GIT_CHECKOUT_FAILED: "GIT_CHECKOUT_FAILED",
|
|
664
45
|
GIT_OPERATION_FAILED: "GIT_OPERATION_FAILED",
|
|
46
|
+
BUCKET_MOUNT_ERROR: "BUCKET_MOUNT_ERROR",
|
|
47
|
+
S3FS_MOUNT_ERROR: "S3FS_MOUNT_ERROR",
|
|
48
|
+
MISSING_CREDENTIALS: "MISSING_CREDENTIALS",
|
|
49
|
+
INVALID_MOUNT_CONFIG: "INVALID_MOUNT_CONFIG",
|
|
665
50
|
INTERPRETER_NOT_READY: "INTERPRETER_NOT_READY",
|
|
666
51
|
CONTEXT_NOT_FOUND: "CONTEXT_NOT_FOUND",
|
|
667
52
|
CODE_EXECUTION_ERROR: "CODE_EXECUTION_ERROR",
|
|
@@ -695,6 +80,8 @@ const ERROR_STATUS_MAP = {
|
|
|
695
80
|
[ErrorCode.INVALID_JSON_RESPONSE]: 400,
|
|
696
81
|
[ErrorCode.NAME_TOO_LONG]: 400,
|
|
697
82
|
[ErrorCode.VALIDATION_FAILED]: 400,
|
|
83
|
+
[ErrorCode.MISSING_CREDENTIALS]: 400,
|
|
84
|
+
[ErrorCode.INVALID_MOUNT_CONFIG]: 400,
|
|
698
85
|
[ErrorCode.GIT_AUTH_FAILED]: 401,
|
|
699
86
|
[ErrorCode.PERMISSION_DENIED]: 403,
|
|
700
87
|
[ErrorCode.COMMAND_PERMISSION_DENIED]: 403,
|
|
@@ -719,6 +106,8 @@ const ERROR_STATUS_MAP = {
|
|
|
719
106
|
[ErrorCode.GIT_CHECKOUT_FAILED]: 500,
|
|
720
107
|
[ErrorCode.GIT_OPERATION_FAILED]: 500,
|
|
721
108
|
[ErrorCode.CODE_EXECUTION_ERROR]: 500,
|
|
109
|
+
[ErrorCode.BUCKET_MOUNT_ERROR]: 500,
|
|
110
|
+
[ErrorCode.S3FS_MOUNT_ERROR]: 500,
|
|
722
111
|
[ErrorCode.UNKNOWN_ERROR]: 500,
|
|
723
112
|
[ErrorCode.INTERNAL_ERROR]: 500
|
|
724
113
|
};
|
|
@@ -1237,8 +626,8 @@ function createErrorFromResponse(errorResponse) {
|
|
|
1237
626
|
|
|
1238
627
|
//#endregion
|
|
1239
628
|
//#region src/clients/base-client.ts
|
|
1240
|
-
const TIMEOUT_MS =
|
|
1241
|
-
const MIN_TIME_FOR_RETRY_MS =
|
|
629
|
+
const TIMEOUT_MS = 12e4;
|
|
630
|
+
const MIN_TIME_FOR_RETRY_MS = 15e3;
|
|
1242
631
|
/**
|
|
1243
632
|
* Abstract base class providing common HTTP functionality for all domain clients
|
|
1244
633
|
*/
|
|
@@ -1252,31 +641,31 @@ var BaseHttpClient = class {
|
|
|
1252
641
|
this.baseUrl = this.options.baseUrl;
|
|
1253
642
|
}
|
|
1254
643
|
/**
|
|
1255
|
-
* Core HTTP request method with automatic retry for container
|
|
644
|
+
* Core HTTP request method with automatic retry for container startup delays
|
|
645
|
+
* Retries both 503 (provisioning) and 500 (startup failure) errors when they're container-related
|
|
1256
646
|
*/
|
|
1257
647
|
async doFetch(path, options) {
|
|
1258
648
|
const startTime = Date.now();
|
|
1259
649
|
let attempt = 0;
|
|
1260
650
|
while (true) {
|
|
1261
651
|
const response = await this.executeFetch(path, options);
|
|
1262
|
-
if (response
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.logger.error("Container failed to provision after multiple attempts", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over 60s`));
|
|
1277
|
-
return response;
|
|
1278
|
-
}
|
|
652
|
+
if (await this.isRetryableContainerError(response)) {
|
|
653
|
+
const elapsed = Date.now() - startTime;
|
|
654
|
+
const remaining = TIMEOUT_MS - elapsed;
|
|
655
|
+
if (remaining > MIN_TIME_FOR_RETRY_MS) {
|
|
656
|
+
const delay = Math.min(3e3 * 2 ** attempt, 3e4);
|
|
657
|
+
this.logger.info("Container not ready, retrying", {
|
|
658
|
+
status: response.status,
|
|
659
|
+
attempt: attempt + 1,
|
|
660
|
+
delayMs: delay,
|
|
661
|
+
remainingSec: Math.floor(remaining / 1e3)
|
|
662
|
+
});
|
|
663
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
664
|
+
attempt++;
|
|
665
|
+
continue;
|
|
1279
666
|
}
|
|
667
|
+
this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1e3)}s`));
|
|
668
|
+
return response;
|
|
1280
669
|
}
|
|
1281
670
|
return response;
|
|
1282
671
|
}
|
|
@@ -1372,14 +761,50 @@ var BaseHttpClient = class {
|
|
|
1372
761
|
} else this.logger.error(`Error in ${operation}`, error instanceof Error ? error : new Error(String(error)));
|
|
1373
762
|
}
|
|
1374
763
|
/**
|
|
1375
|
-
* Check if
|
|
1376
|
-
*
|
|
764
|
+
* Check if response indicates a retryable container error
|
|
765
|
+
* Uses fail-safe strategy: only retry known transient errors
|
|
766
|
+
*
|
|
767
|
+
* TODO: This relies on string matching error messages, which is brittle.
|
|
768
|
+
* Ideally, the container API should return structured errors with a
|
|
769
|
+
* `retryable: boolean` field to avoid coupling to error message format.
|
|
770
|
+
*
|
|
771
|
+
* @param response - HTTP response to check
|
|
772
|
+
* @returns true if error is retryable container error, false otherwise
|
|
1377
773
|
*/
|
|
1378
|
-
async
|
|
774
|
+
async isRetryableContainerError(response) {
|
|
775
|
+
if (response.status !== 500 && response.status !== 503) return false;
|
|
1379
776
|
try {
|
|
1380
|
-
|
|
777
|
+
const text = await response.clone().text();
|
|
778
|
+
const textLower = text.toLowerCase();
|
|
779
|
+
if ([
|
|
780
|
+
"no such image",
|
|
781
|
+
"container already exists",
|
|
782
|
+
"malformed containerinspect"
|
|
783
|
+
].some((err) => textLower.includes(err))) {
|
|
784
|
+
this.logger.debug("Detected permanent error, not retrying", { text });
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
const shouldRetry = [
|
|
788
|
+
"no container instance available",
|
|
789
|
+
"currently provisioning",
|
|
790
|
+
"container port not found",
|
|
791
|
+
"connection refused: container port",
|
|
792
|
+
"the container is not listening",
|
|
793
|
+
"failed to verify port",
|
|
794
|
+
"container did not start",
|
|
795
|
+
"network connection lost",
|
|
796
|
+
"container suddenly disconnected",
|
|
797
|
+
"monitor failed to find container",
|
|
798
|
+
"timed out",
|
|
799
|
+
"timeout"
|
|
800
|
+
].some((err) => textLower.includes(err));
|
|
801
|
+
if (!shouldRetry) this.logger.debug("Unknown error pattern, not retrying", {
|
|
802
|
+
status: response.status,
|
|
803
|
+
text: text.substring(0, 200)
|
|
804
|
+
});
|
|
805
|
+
return shouldRetry;
|
|
1381
806
|
} catch (error) {
|
|
1382
|
-
this.logger.error("Error checking response
|
|
807
|
+
this.logger.error("Error checking if response is retryable", error instanceof Error ? error : new Error(String(error)));
|
|
1383
808
|
return false;
|
|
1384
809
|
}
|
|
1385
810
|
}
|
|
@@ -2333,7 +1758,7 @@ async function proxyToSandbox(request, env) {
|
|
|
2333
1758
|
const routeInfo = extractSandboxRoute(url);
|
|
2334
1759
|
if (!routeInfo) return null;
|
|
2335
1760
|
const { sandboxId, port, path, token } = routeInfo;
|
|
2336
|
-
const sandbox = getSandbox(env.Sandbox, sandboxId);
|
|
1761
|
+
const sandbox = getSandbox(env.Sandbox, sandboxId, { normalizeId: true });
|
|
2337
1762
|
if (port !== 3e3) {
|
|
2338
1763
|
if (!await sandbox.validatePortToken(port, token)) {
|
|
2339
1764
|
logger.warn("Invalid token access blocked", {
|
|
@@ -2492,6 +1917,124 @@ function asyncIterableToSSEStream(events, options) {
|
|
|
2492
1917
|
});
|
|
2493
1918
|
}
|
|
2494
1919
|
|
|
1920
|
+
//#endregion
|
|
1921
|
+
//#region src/storage-mount/errors.ts
|
|
1922
|
+
/**
|
|
1923
|
+
* Bucket mounting error classes
|
|
1924
|
+
*
|
|
1925
|
+
* These are SDK-side validation errors that follow the same pattern as SecurityError.
|
|
1926
|
+
* They are thrown before any container interaction occurs.
|
|
1927
|
+
*/
|
|
1928
|
+
/**
|
|
1929
|
+
* Base error for bucket mounting operations
|
|
1930
|
+
*/
|
|
1931
|
+
var BucketMountError = class extends Error {
|
|
1932
|
+
code;
|
|
1933
|
+
constructor(message, code = ErrorCode.BUCKET_MOUNT_ERROR) {
|
|
1934
|
+
super(message);
|
|
1935
|
+
this.name = "BucketMountError";
|
|
1936
|
+
this.code = code;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
/**
|
|
1940
|
+
* Thrown when S3FS mount command fails
|
|
1941
|
+
*/
|
|
1942
|
+
var S3FSMountError = class extends BucketMountError {
|
|
1943
|
+
constructor(message) {
|
|
1944
|
+
super(message, ErrorCode.S3FS_MOUNT_ERROR);
|
|
1945
|
+
this.name = "S3FSMountError";
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
/**
|
|
1949
|
+
* Thrown when no credentials found in environment
|
|
1950
|
+
*/
|
|
1951
|
+
var MissingCredentialsError = class extends BucketMountError {
|
|
1952
|
+
constructor(message) {
|
|
1953
|
+
super(message, ErrorCode.MISSING_CREDENTIALS);
|
|
1954
|
+
this.name = "MissingCredentialsError";
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
/**
|
|
1958
|
+
* Thrown when bucket name, mount path, or options are invalid
|
|
1959
|
+
*/
|
|
1960
|
+
var InvalidMountConfigError = class extends BucketMountError {
|
|
1961
|
+
constructor(message) {
|
|
1962
|
+
super(message, ErrorCode.INVALID_MOUNT_CONFIG);
|
|
1963
|
+
this.name = "InvalidMountConfigError";
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
|
|
1967
|
+
//#endregion
|
|
1968
|
+
//#region src/storage-mount/credential-detection.ts
|
|
1969
|
+
/**
|
|
1970
|
+
* Detect credentials for bucket mounting from environment variables
|
|
1971
|
+
* Priority order:
|
|
1972
|
+
* 1. Explicit options.credentials
|
|
1973
|
+
* 2. Standard AWS env vars: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
|
|
1974
|
+
* 3. Error: no credentials found
|
|
1975
|
+
*
|
|
1976
|
+
* @param options - Mount options
|
|
1977
|
+
* @param envVars - Environment variables
|
|
1978
|
+
* @returns Detected credentials
|
|
1979
|
+
* @throws MissingCredentialsError if no credentials found
|
|
1980
|
+
*/
|
|
1981
|
+
function detectCredentials(options, envVars) {
|
|
1982
|
+
if (options.credentials) return options.credentials;
|
|
1983
|
+
const awsAccessKeyId = envVars.AWS_ACCESS_KEY_ID;
|
|
1984
|
+
const awsSecretAccessKey = envVars.AWS_SECRET_ACCESS_KEY;
|
|
1985
|
+
if (awsAccessKeyId && awsSecretAccessKey) return {
|
|
1986
|
+
accessKeyId: awsAccessKeyId,
|
|
1987
|
+
secretAccessKey: awsSecretAccessKey
|
|
1988
|
+
};
|
|
1989
|
+
throw new MissingCredentialsError("No credentials found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or pass explicit credentials in options.");
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/storage-mount/provider-detection.ts
|
|
1994
|
+
/**
|
|
1995
|
+
* Detect provider from endpoint URL using pattern matching
|
|
1996
|
+
*/
|
|
1997
|
+
function detectProviderFromUrl(endpoint) {
|
|
1998
|
+
try {
|
|
1999
|
+
const hostname = new URL(endpoint).hostname.toLowerCase();
|
|
2000
|
+
if (hostname.endsWith(".r2.cloudflarestorage.com")) return "r2";
|
|
2001
|
+
if (hostname.endsWith(".amazonaws.com") || hostname === "s3.amazonaws.com") return "s3";
|
|
2002
|
+
if (hostname === "storage.googleapis.com") return "gcs";
|
|
2003
|
+
return null;
|
|
2004
|
+
} catch {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Get s3fs flags for a given provider
|
|
2010
|
+
*
|
|
2011
|
+
* Based on s3fs-fuse wiki recommendations:
|
|
2012
|
+
* https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3
|
|
2013
|
+
*/
|
|
2014
|
+
function getProviderFlags(provider) {
|
|
2015
|
+
if (!provider) return ["use_path_request_style"];
|
|
2016
|
+
switch (provider) {
|
|
2017
|
+
case "r2": return ["nomixupload"];
|
|
2018
|
+
case "s3": return [];
|
|
2019
|
+
case "gcs": return [];
|
|
2020
|
+
default: return ["use_path_request_style"];
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Resolve s3fs options by combining provider defaults with user overrides
|
|
2025
|
+
*/
|
|
2026
|
+
function resolveS3fsOptions(provider, userOptions) {
|
|
2027
|
+
const providerFlags = getProviderFlags(provider);
|
|
2028
|
+
if (!userOptions || userOptions.length === 0) return providerFlags;
|
|
2029
|
+
const allFlags = [...providerFlags, ...userOptions];
|
|
2030
|
+
const flagMap = /* @__PURE__ */ new Map();
|
|
2031
|
+
for (const flag of allFlags) {
|
|
2032
|
+
const [flagName] = flag.split("=");
|
|
2033
|
+
flagMap.set(flagName, flag);
|
|
2034
|
+
}
|
|
2035
|
+
return Array.from(flagMap.values());
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2495
2038
|
//#endregion
|
|
2496
2039
|
//#region src/version.ts
|
|
2497
2040
|
/**
|
|
@@ -2499,16 +2042,21 @@ function asyncIterableToSSEStream(events, options) {
|
|
|
2499
2042
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
2500
2043
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
2501
2044
|
*/
|
|
2502
|
-
const SDK_VERSION = "0.
|
|
2045
|
+
const SDK_VERSION = "0.5.2";
|
|
2503
2046
|
|
|
2504
2047
|
//#endregion
|
|
2505
2048
|
//#region src/sandbox.ts
|
|
2506
2049
|
function getSandbox(ns, id, options) {
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2050
|
+
const sanitizedId = sanitizeSandboxId(id);
|
|
2051
|
+
const effectiveId = options?.normalizeId ? sanitizedId.toLowerCase() : sanitizedId;
|
|
2052
|
+
const hasUppercase = /[A-Z]/.test(sanitizedId);
|
|
2053
|
+
if (!options?.normalizeId && hasUppercase) createLogger({ component: "sandbox-do" }).warn(`Sandbox ID "${sanitizedId}" contains uppercase letters, which causes issues with preview URLs (hostnames are case-insensitive). normalizeId will default to true in a future version to prevent this. Use lowercase IDs or pass { normalizeId: true } to prepare.`);
|
|
2054
|
+
const stub = getContainer(ns, effectiveId);
|
|
2055
|
+
stub.setSandboxName?.(effectiveId, options?.normalizeId);
|
|
2509
2056
|
if (options?.baseUrl) stub.setBaseUrl(options.baseUrl);
|
|
2510
2057
|
if (options?.sleepAfter !== void 0) stub.setSleepAfter(options.sleepAfter);
|
|
2511
2058
|
if (options?.keepAlive !== void 0) stub.setKeepAlive(options.keepAlive);
|
|
2059
|
+
if (options?.containerTimeouts) stub.setContainerTimeouts(options.containerTimeouts);
|
|
2512
2060
|
return Object.assign(stub, { wsConnect: connect(stub) });
|
|
2513
2061
|
}
|
|
2514
2062
|
function connect(stub) {
|
|
@@ -2524,18 +2072,35 @@ var Sandbox = class extends Container {
|
|
|
2524
2072
|
client;
|
|
2525
2073
|
codeInterpreter;
|
|
2526
2074
|
sandboxName = null;
|
|
2075
|
+
normalizeId = false;
|
|
2527
2076
|
baseUrl = null;
|
|
2528
2077
|
portTokens = /* @__PURE__ */ new Map();
|
|
2529
2078
|
defaultSession = null;
|
|
2530
2079
|
envVars = {};
|
|
2531
2080
|
logger;
|
|
2532
2081
|
keepAliveEnabled = false;
|
|
2082
|
+
activeMounts = /* @__PURE__ */ new Map();
|
|
2083
|
+
/**
|
|
2084
|
+
* Default container startup timeouts (conservative for production)
|
|
2085
|
+
* Based on Cloudflare docs: "Containers take several minutes to provision"
|
|
2086
|
+
*/
|
|
2087
|
+
DEFAULT_CONTAINER_TIMEOUTS = {
|
|
2088
|
+
instanceGetTimeoutMS: 3e4,
|
|
2089
|
+
portReadyTimeoutMS: 9e4,
|
|
2090
|
+
waitIntervalMS: 1e3
|
|
2091
|
+
};
|
|
2092
|
+
/**
|
|
2093
|
+
* Active container timeout configuration
|
|
2094
|
+
* Can be set via options, env vars, or defaults
|
|
2095
|
+
*/
|
|
2096
|
+
containerTimeouts = { ...this.DEFAULT_CONTAINER_TIMEOUTS };
|
|
2533
2097
|
constructor(ctx, env) {
|
|
2534
2098
|
super(ctx, env);
|
|
2535
2099
|
const envObj = env;
|
|
2536
2100
|
["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
|
|
2537
|
-
if (envObj?.[key]) this.envVars[key] = envObj[key];
|
|
2101
|
+
if (envObj?.[key]) this.envVars[key] = String(envObj[key]);
|
|
2538
2102
|
});
|
|
2103
|
+
this.containerTimeouts = this.getDefaultTimeouts(envObj);
|
|
2539
2104
|
this.logger = createLogger({
|
|
2540
2105
|
component: "sandbox-do",
|
|
2541
2106
|
sandboxId: this.ctx.id.toString()
|
|
@@ -2548,16 +2113,24 @@ var Sandbox = class extends Container {
|
|
|
2548
2113
|
this.codeInterpreter = new CodeInterpreter(this);
|
|
2549
2114
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
2550
2115
|
this.sandboxName = await this.ctx.storage.get("sandboxName") || null;
|
|
2116
|
+
this.normalizeId = await this.ctx.storage.get("normalizeId") || false;
|
|
2551
2117
|
this.defaultSession = await this.ctx.storage.get("defaultSession") || null;
|
|
2552
2118
|
const storedTokens = await this.ctx.storage.get("portTokens") || {};
|
|
2553
2119
|
this.portTokens = /* @__PURE__ */ new Map();
|
|
2554
2120
|
for (const [portStr, token] of Object.entries(storedTokens)) this.portTokens.set(parseInt(portStr, 10), token);
|
|
2121
|
+
const storedTimeouts = await this.ctx.storage.get("containerTimeouts");
|
|
2122
|
+
if (storedTimeouts) this.containerTimeouts = {
|
|
2123
|
+
...this.containerTimeouts,
|
|
2124
|
+
...storedTimeouts
|
|
2125
|
+
};
|
|
2555
2126
|
});
|
|
2556
2127
|
}
|
|
2557
|
-
async setSandboxName(name) {
|
|
2128
|
+
async setSandboxName(name, normalizeId) {
|
|
2558
2129
|
if (!this.sandboxName) {
|
|
2559
2130
|
this.sandboxName = name;
|
|
2131
|
+
this.normalizeId = normalizeId || false;
|
|
2560
2132
|
await this.ctx.storage.put("sandboxName", name);
|
|
2133
|
+
await this.ctx.storage.put("normalizeId", this.normalizeId);
|
|
2561
2134
|
}
|
|
2562
2135
|
}
|
|
2563
2136
|
async setBaseUrl(baseUrl) {
|
|
@@ -2586,10 +2159,183 @@ var Sandbox = class extends Container {
|
|
|
2586
2159
|
}
|
|
2587
2160
|
}
|
|
2588
2161
|
/**
|
|
2162
|
+
* RPC method to configure container startup timeouts
|
|
2163
|
+
*/
|
|
2164
|
+
async setContainerTimeouts(timeouts) {
|
|
2165
|
+
const validated = { ...this.containerTimeouts };
|
|
2166
|
+
if (timeouts.instanceGetTimeoutMS !== void 0) validated.instanceGetTimeoutMS = this.validateTimeout(timeouts.instanceGetTimeoutMS, "instanceGetTimeoutMS", 5e3, 3e5);
|
|
2167
|
+
if (timeouts.portReadyTimeoutMS !== void 0) validated.portReadyTimeoutMS = this.validateTimeout(timeouts.portReadyTimeoutMS, "portReadyTimeoutMS", 1e4, 6e5);
|
|
2168
|
+
if (timeouts.waitIntervalMS !== void 0) validated.waitIntervalMS = this.validateTimeout(timeouts.waitIntervalMS, "waitIntervalMS", 100, 5e3);
|
|
2169
|
+
this.containerTimeouts = validated;
|
|
2170
|
+
await this.ctx.storage.put("containerTimeouts", this.containerTimeouts);
|
|
2171
|
+
this.logger.debug("Container timeouts updated", this.containerTimeouts);
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Validate a timeout value is within acceptable range
|
|
2175
|
+
* Throws error if invalid - used for user-provided values
|
|
2176
|
+
*/
|
|
2177
|
+
validateTimeout(value, name, min, max) {
|
|
2178
|
+
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) throw new Error(`${name} must be a valid finite number, got ${value}`);
|
|
2179
|
+
if (value < min || value > max) throw new Error(`${name} must be between ${min}-${max}ms, got ${value}ms`);
|
|
2180
|
+
return value;
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Get default timeouts with env var fallbacks and validation
|
|
2184
|
+
* Precedence: SDK defaults < Env vars < User config
|
|
2185
|
+
*/
|
|
2186
|
+
getDefaultTimeouts(env) {
|
|
2187
|
+
const parseAndValidate = (envVar, name, min, max) => {
|
|
2188
|
+
const defaultValue = this.DEFAULT_CONTAINER_TIMEOUTS[name];
|
|
2189
|
+
if (envVar === void 0) return defaultValue;
|
|
2190
|
+
const parsed = parseInt(envVar, 10);
|
|
2191
|
+
if (Number.isNaN(parsed)) {
|
|
2192
|
+
this.logger.warn(`Invalid ${name}: "${envVar}" is not a number. Using default: ${defaultValue}ms`);
|
|
2193
|
+
return defaultValue;
|
|
2194
|
+
}
|
|
2195
|
+
if (parsed < min || parsed > max) {
|
|
2196
|
+
this.logger.warn(`Invalid ${name}: ${parsed}ms. Must be ${min}-${max}ms. Using default: ${defaultValue}ms`);
|
|
2197
|
+
return defaultValue;
|
|
2198
|
+
}
|
|
2199
|
+
return parsed;
|
|
2200
|
+
};
|
|
2201
|
+
return {
|
|
2202
|
+
instanceGetTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
|
|
2203
|
+
portReadyTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
|
|
2204
|
+
waitIntervalMS: parseAndValidate(getEnvString(env, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
async mountBucket(bucket, mountPath, options) {
|
|
2208
|
+
this.logger.info(`Mounting bucket ${bucket} to ${mountPath}`);
|
|
2209
|
+
this.validateMountOptions(bucket, mountPath, options);
|
|
2210
|
+
const provider = options.provider || detectProviderFromUrl(options.endpoint);
|
|
2211
|
+
this.logger.debug(`Detected provider: ${provider || "unknown"}`, { explicitProvider: options.provider });
|
|
2212
|
+
const credentials = detectCredentials(options, this.envVars);
|
|
2213
|
+
const passwordFilePath = this.generatePasswordFilePath();
|
|
2214
|
+
this.activeMounts.set(mountPath, {
|
|
2215
|
+
bucket,
|
|
2216
|
+
mountPath,
|
|
2217
|
+
endpoint: options.endpoint,
|
|
2218
|
+
provider,
|
|
2219
|
+
passwordFilePath,
|
|
2220
|
+
mounted: false
|
|
2221
|
+
});
|
|
2222
|
+
try {
|
|
2223
|
+
await this.createPasswordFile(passwordFilePath, bucket, credentials);
|
|
2224
|
+
await this.exec(`mkdir -p ${shellEscape(mountPath)}`);
|
|
2225
|
+
await this.executeS3FSMount(bucket, mountPath, options, provider, passwordFilePath);
|
|
2226
|
+
this.activeMounts.set(mountPath, {
|
|
2227
|
+
bucket,
|
|
2228
|
+
mountPath,
|
|
2229
|
+
endpoint: options.endpoint,
|
|
2230
|
+
provider,
|
|
2231
|
+
passwordFilePath,
|
|
2232
|
+
mounted: true
|
|
2233
|
+
});
|
|
2234
|
+
this.logger.info(`Successfully mounted bucket ${bucket} to ${mountPath}`);
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
await this.deletePasswordFile(passwordFilePath);
|
|
2237
|
+
this.activeMounts.delete(mountPath);
|
|
2238
|
+
throw error;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Manually unmount a bucket filesystem
|
|
2243
|
+
*
|
|
2244
|
+
* @param mountPath - Absolute path where the bucket is mounted
|
|
2245
|
+
* @throws InvalidMountConfigError if mount path doesn't exist or isn't mounted
|
|
2246
|
+
*/
|
|
2247
|
+
async unmountBucket(mountPath) {
|
|
2248
|
+
this.logger.info(`Unmounting bucket from ${mountPath}`);
|
|
2249
|
+
const mountInfo = this.activeMounts.get(mountPath);
|
|
2250
|
+
if (!mountInfo) throw new InvalidMountConfigError(`No active mount found at path: ${mountPath}`);
|
|
2251
|
+
try {
|
|
2252
|
+
await this.exec(`fusermount -u ${shellEscape(mountPath)}`);
|
|
2253
|
+
mountInfo.mounted = false;
|
|
2254
|
+
this.activeMounts.delete(mountPath);
|
|
2255
|
+
} finally {
|
|
2256
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
2257
|
+
}
|
|
2258
|
+
this.logger.info(`Successfully unmounted bucket from ${mountPath}`);
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Validate mount options
|
|
2262
|
+
*/
|
|
2263
|
+
validateMountOptions(bucket, mountPath, options) {
|
|
2264
|
+
if (!options.endpoint) throw new InvalidMountConfigError("Endpoint is required. Provide the full S3-compatible endpoint URL.");
|
|
2265
|
+
try {
|
|
2266
|
+
new URL(options.endpoint);
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
throw new InvalidMountConfigError(`Invalid endpoint URL: "${options.endpoint}". Must be a valid HTTP(S) URL.`);
|
|
2269
|
+
}
|
|
2270
|
+
if (!/^[a-z0-9]([a-z0-9.-]{0,61}[a-z0-9])?$/.test(bucket)) throw new InvalidMountConfigError(`Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, dots, or hyphens, and cannot start/end with dots or hyphens.`);
|
|
2271
|
+
if (!mountPath.startsWith("/")) throw new InvalidMountConfigError(`Mount path must be absolute (start with /): "${mountPath}"`);
|
|
2272
|
+
if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path "${mountPath}" is already in use by bucket "${this.activeMounts.get(mountPath)?.bucket}". Unmount the existing bucket first or use a different mount path.`);
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Generate unique password file path for s3fs credentials
|
|
2276
|
+
*/
|
|
2277
|
+
generatePasswordFilePath() {
|
|
2278
|
+
return `/tmp/.passwd-s3fs-${crypto.randomUUID()}`;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Create password file with s3fs credentials
|
|
2282
|
+
* Format: bucket:accessKeyId:secretAccessKey
|
|
2283
|
+
*/
|
|
2284
|
+
async createPasswordFile(passwordFilePath, bucket, credentials) {
|
|
2285
|
+
const content = `${bucket}:${credentials.accessKeyId}:${credentials.secretAccessKey}`;
|
|
2286
|
+
await this.writeFile(passwordFilePath, content);
|
|
2287
|
+
await this.exec(`chmod 0600 ${shellEscape(passwordFilePath)}`);
|
|
2288
|
+
this.logger.debug(`Created password file: ${passwordFilePath}`);
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* Delete password file
|
|
2292
|
+
*/
|
|
2293
|
+
async deletePasswordFile(passwordFilePath) {
|
|
2294
|
+
try {
|
|
2295
|
+
await this.exec(`rm -f ${shellEscape(passwordFilePath)}`);
|
|
2296
|
+
this.logger.debug(`Deleted password file: ${passwordFilePath}`);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
this.logger.warn(`Failed to delete password file ${passwordFilePath}`, { error: error instanceof Error ? error.message : String(error) });
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Execute S3FS mount command
|
|
2303
|
+
*/
|
|
2304
|
+
async executeS3FSMount(bucket, mountPath, options, provider, passwordFilePath) {
|
|
2305
|
+
const resolvedOptions = resolveS3fsOptions(provider, options.s3fsOptions);
|
|
2306
|
+
const s3fsArgs = [];
|
|
2307
|
+
s3fsArgs.push(`passwd_file=${passwordFilePath}`);
|
|
2308
|
+
s3fsArgs.push(...resolvedOptions);
|
|
2309
|
+
if (options.readOnly) s3fsArgs.push("ro");
|
|
2310
|
+
s3fsArgs.push(`url=${options.endpoint}`);
|
|
2311
|
+
const optionsStr = shellEscape(s3fsArgs.join(","));
|
|
2312
|
+
const mountCmd = `s3fs ${shellEscape(bucket)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
2313
|
+
this.logger.debug("Executing s3fs mount", {
|
|
2314
|
+
bucket,
|
|
2315
|
+
mountPath,
|
|
2316
|
+
provider,
|
|
2317
|
+
resolvedOptions
|
|
2318
|
+
});
|
|
2319
|
+
const result = await this.exec(mountCmd);
|
|
2320
|
+
if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
|
|
2321
|
+
this.logger.debug("Mount command executed successfully");
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2589
2324
|
* Cleanup and destroy the sandbox container
|
|
2590
2325
|
*/
|
|
2591
2326
|
async destroy() {
|
|
2592
2327
|
this.logger.info("Destroying sandbox container");
|
|
2328
|
+
for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
|
|
2329
|
+
if (mountInfo.mounted) try {
|
|
2330
|
+
this.logger.info(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
|
|
2331
|
+
await this.exec(`fusermount -u ${shellEscape(mountPath)}`);
|
|
2332
|
+
mountInfo.mounted = false;
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2335
|
+
this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
|
|
2336
|
+
}
|
|
2337
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
2338
|
+
}
|
|
2593
2339
|
await super.destroy();
|
|
2594
2340
|
}
|
|
2595
2341
|
onStart() {
|
|
@@ -2621,13 +2367,75 @@ var Sandbox = class extends Container {
|
|
|
2621
2367
|
this.logger.debug("Version compatibility check encountered an error", { error: error instanceof Error ? error.message : String(error) });
|
|
2622
2368
|
}
|
|
2623
2369
|
}
|
|
2624
|
-
onStop() {
|
|
2370
|
+
async onStop() {
|
|
2625
2371
|
this.logger.debug("Sandbox stopped");
|
|
2372
|
+
this.portTokens.clear();
|
|
2373
|
+
this.defaultSession = null;
|
|
2374
|
+
this.activeMounts.clear();
|
|
2375
|
+
await Promise.all([this.ctx.storage.delete("portTokens"), this.ctx.storage.delete("defaultSession")]);
|
|
2626
2376
|
}
|
|
2627
2377
|
onError(error) {
|
|
2628
2378
|
this.logger.error("Sandbox error", error instanceof Error ? error : new Error(String(error)));
|
|
2629
2379
|
}
|
|
2630
2380
|
/**
|
|
2381
|
+
* Override Container.containerFetch to use production-friendly timeouts
|
|
2382
|
+
* Automatically starts container with longer timeouts if not running
|
|
2383
|
+
*/
|
|
2384
|
+
async containerFetch(requestOrUrl, portOrInit, portParam) {
|
|
2385
|
+
const { request, port } = this.parseContainerFetchArgs(requestOrUrl, portOrInit, portParam);
|
|
2386
|
+
if ((await this.getState()).status !== "healthy") try {
|
|
2387
|
+
this.logger.debug("Starting container with configured timeouts", {
|
|
2388
|
+
instanceTimeout: this.containerTimeouts.instanceGetTimeoutMS,
|
|
2389
|
+
portTimeout: this.containerTimeouts.portReadyTimeoutMS
|
|
2390
|
+
});
|
|
2391
|
+
await this.startAndWaitForPorts({
|
|
2392
|
+
ports: port,
|
|
2393
|
+
cancellationOptions: {
|
|
2394
|
+
instanceGetTimeoutMS: this.containerTimeouts.instanceGetTimeoutMS,
|
|
2395
|
+
portReadyTimeoutMS: this.containerTimeouts.portReadyTimeoutMS,
|
|
2396
|
+
waitInterval: this.containerTimeouts.waitIntervalMS,
|
|
2397
|
+
abort: request.signal
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
} catch (e) {
|
|
2401
|
+
if (this.isNoInstanceError(e)) return new Response("Container is currently provisioning. This can take several minutes on first deployment. Please retry in a moment.", {
|
|
2402
|
+
status: 503,
|
|
2403
|
+
headers: { "Retry-After": "10" }
|
|
2404
|
+
});
|
|
2405
|
+
this.logger.error("Container startup failed", e instanceof Error ? e : new Error(String(e)));
|
|
2406
|
+
return new Response(`Failed to start container: ${e instanceof Error ? e.message : String(e)}`, { status: 500 });
|
|
2407
|
+
}
|
|
2408
|
+
return await super.containerFetch(requestOrUrl, portOrInit, portParam);
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Helper: Check if error is "no container instance available"
|
|
2412
|
+
*/
|
|
2413
|
+
isNoInstanceError(error) {
|
|
2414
|
+
return error instanceof Error && error.message.toLowerCase().includes("no container instance");
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Helper: Parse containerFetch arguments (supports multiple signatures)
|
|
2418
|
+
*/
|
|
2419
|
+
parseContainerFetchArgs(requestOrUrl, portOrInit, portParam) {
|
|
2420
|
+
let request;
|
|
2421
|
+
let port;
|
|
2422
|
+
if (requestOrUrl instanceof Request) {
|
|
2423
|
+
request = requestOrUrl;
|
|
2424
|
+
port = typeof portOrInit === "number" ? portOrInit : void 0;
|
|
2425
|
+
} else {
|
|
2426
|
+
const url = typeof requestOrUrl === "string" ? requestOrUrl : requestOrUrl.toString();
|
|
2427
|
+
const init = typeof portOrInit === "number" ? {} : portOrInit || {};
|
|
2428
|
+
port = typeof portOrInit === "number" ? portOrInit : typeof portParam === "number" ? portParam : void 0;
|
|
2429
|
+
request = new Request(url, init);
|
|
2430
|
+
}
|
|
2431
|
+
port ??= this.defaultPort;
|
|
2432
|
+
if (port === void 0) throw new Error("No port specified for container fetch");
|
|
2433
|
+
return {
|
|
2434
|
+
request,
|
|
2435
|
+
port
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2631
2439
|
* Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
|
|
2632
2440
|
* When keepAlive is disabled, calls parent implementation which stops the container
|
|
2633
2441
|
*/
|
|
@@ -2644,28 +2452,26 @@ var Sandbox = class extends Container {
|
|
|
2644
2452
|
traceId,
|
|
2645
2453
|
operation: "fetch"
|
|
2646
2454
|
});
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
return await this.containerFetch(request, port);
|
|
2668
|
-
});
|
|
2455
|
+
const url = new URL(request.url);
|
|
2456
|
+
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
2457
|
+
const name = request.headers.get("X-Sandbox-Name");
|
|
2458
|
+
this.sandboxName = name;
|
|
2459
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
2460
|
+
}
|
|
2461
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
2462
|
+
const connectionHeader = request.headers.get("Connection");
|
|
2463
|
+
if (upgradeHeader?.toLowerCase() === "websocket" && connectionHeader?.toLowerCase().includes("upgrade")) try {
|
|
2464
|
+
requestLogger.debug("WebSocket upgrade requested", {
|
|
2465
|
+
path: url.pathname,
|
|
2466
|
+
port: this.determinePort(url)
|
|
2467
|
+
});
|
|
2468
|
+
return await super.fetch(request);
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
requestLogger.error("WebSocket connection failed", error instanceof Error ? error : new Error(String(error)), { path: url.pathname });
|
|
2471
|
+
throw error;
|
|
2472
|
+
}
|
|
2473
|
+
const port = this.determinePort(url);
|
|
2474
|
+
return await this.containerFetch(request, port);
|
|
2669
2475
|
}
|
|
2670
2476
|
wsConnect(request, port) {
|
|
2671
2477
|
throw new Error("Not implemented here to avoid RPC serialization issues");
|
|
@@ -2696,7 +2502,7 @@ var Sandbox = class extends Container {
|
|
|
2696
2502
|
await this.ctx.storage.put("defaultSession", sessionId);
|
|
2697
2503
|
this.logger.debug("Default session initialized", { sessionId });
|
|
2698
2504
|
} catch (error) {
|
|
2699
|
-
if (error
|
|
2505
|
+
if (error instanceof Error && error.message.includes("already exists")) {
|
|
2700
2506
|
this.logger.debug("Reusing existing session after reload", { sessionId });
|
|
2701
2507
|
this.defaultSession = sessionId;
|
|
2702
2508
|
await this.ctx.storage.put("defaultSession", sessionId);
|
|
@@ -3017,7 +2823,10 @@ var Sandbox = class extends Container {
|
|
|
3017
2823
|
}
|
|
3018
2824
|
constructPreviewUrl(port, sandboxId, hostname, token) {
|
|
3019
2825
|
if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
|
|
3020
|
-
const
|
|
2826
|
+
const effectiveId = this.sandboxName || sandboxId;
|
|
2827
|
+
const hasUppercase = /[A-Z]/.test(effectiveId);
|
|
2828
|
+
if (!this.normalizeId && hasUppercase) throw new SecurityError(`Preview URLs require lowercase sandbox IDs. Your ID "${effectiveId}" contains uppercase letters.\n\nTo fix this:\n1. Create a new sandbox with: getSandbox(ns, "${effectiveId}", { normalizeId: true })\n2. This will create a sandbox with ID: "${effectiveId.toLowerCase()}"\n\nNote: Due to DNS case-insensitivity, IDs with uppercase letters cannot be used with preview URLs.`);
|
|
2829
|
+
const sanitizedSandboxId = sanitizeSandboxId(sandboxId).toLowerCase();
|
|
3021
2830
|
if (isLocalhostPattern(hostname)) {
|
|
3022
2831
|
const [host, portStr] = hostname.split(":");
|
|
3023
2832
|
const mainPort = portStr || "80";
|
|
@@ -3139,7 +2948,9 @@ var Sandbox = class extends Container {
|
|
|
3139
2948
|
},
|
|
3140
2949
|
runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
|
|
3141
2950
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
3142
|
-
deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId)
|
|
2951
|
+
deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId),
|
|
2952
|
+
mountBucket: (bucket, mountPath, options) => this.mountBucket(bucket, mountPath, options),
|
|
2953
|
+
unmountBucket: (mountPath) => this.unmountBucket(mountPath)
|
|
3143
2954
|
};
|
|
3144
2955
|
}
|
|
3145
2956
|
async createCodeContext(options) {
|
|
@@ -3276,5 +3087,5 @@ async function collectFile(stream) {
|
|
|
3276
3087
|
}
|
|
3277
3088
|
|
|
3278
3089
|
//#endregion
|
|
3279
|
-
export { CodeInterpreter, CommandClient,
|
|
3090
|
+
export { BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyToSandbox, responseToAsyncIterable, streamFile };
|
|
3280
3091
|
//# sourceMappingURL=index.js.map
|