@cloudflare/sandbox 0.0.0-6704961 → 0.0.0-68d9bc5
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/CHANGELOG.md +62 -0
- package/Dockerfile +40 -31
- package/README.md +743 -24
- package/container_src/bun.lock +122 -0
- package/container_src/circuit-breaker.ts +121 -0
- package/container_src/handler/exec.ts +340 -0
- package/container_src/handler/file.ts +844 -0
- package/container_src/handler/git.ts +182 -0
- package/container_src/handler/ports.ts +314 -0
- package/container_src/handler/process.ts +640 -0
- package/container_src/index.ts +377 -2917
- package/container_src/jupyter-server.ts +579 -0
- package/container_src/jupyter-service.ts +448 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +83 -0
- package/container_src/types.ts +108 -0
- package/package.json +5 -9
- package/src/client.ts +254 -1322
- package/src/errors.ts +218 -0
- package/src/index.ts +47 -7
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +349 -0
- package/src/request-handler.ts +69 -20
- package/src/sandbox.ts +616 -96
- package/src/security.ts +113 -0
- package/src/sse-parser.ts +147 -0
- package/src/types.ts +401 -0
- package/tsconfig.json +1 -1
- package/tests/client.example.ts +0 -308
- package/tests/connection-test.ts +0 -81
- package/tests/simple-test.ts +0 -81
- package/tests/test1.ts +0 -281
- package/tests/test2.ts +0 -929
package/src/sandbox.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { Container, getContainer } from "@cloudflare/containers";
|
|
2
|
-
import {
|
|
2
|
+
import { CodeInterpreter } from "./interpreter";
|
|
3
|
+
import type {
|
|
4
|
+
CodeContext,
|
|
5
|
+
CreateContextOptions,
|
|
6
|
+
ExecutionResult,
|
|
7
|
+
RunCodeOptions,
|
|
8
|
+
} from "./interpreter-types";
|
|
9
|
+
import { JupyterClient } from "./jupyter-client";
|
|
3
10
|
import { isLocalhostPattern } from "./request-handler";
|
|
11
|
+
import {
|
|
12
|
+
logSecurityEvent,
|
|
13
|
+
SecurityError,
|
|
14
|
+
sanitizeSandboxId,
|
|
15
|
+
validatePort,
|
|
16
|
+
} from "./security";
|
|
17
|
+
import { parseSSEStream } from "./sse-parser";
|
|
18
|
+
import type {
|
|
19
|
+
ExecOptions,
|
|
20
|
+
ExecResult,
|
|
21
|
+
ISandbox,
|
|
22
|
+
Process,
|
|
23
|
+
ProcessOptions,
|
|
24
|
+
ProcessStatus,
|
|
25
|
+
StreamOptions,
|
|
26
|
+
} from "./types";
|
|
27
|
+
import { ProcessNotFoundError, SandboxError } from "./types";
|
|
4
28
|
|
|
5
29
|
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
6
30
|
const stub = getContainer(ns, id);
|
|
@@ -11,26 +35,25 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
11
35
|
return stub;
|
|
12
36
|
}
|
|
13
37
|
|
|
14
|
-
export class Sandbox<Env = unknown> extends Container<Env> {
|
|
38
|
+
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
39
|
+
defaultPort = 3000; // Default port for the container's Bun server
|
|
15
40
|
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
16
|
-
client:
|
|
17
|
-
private workerHostname: string | null = null;
|
|
41
|
+
client: JupyterClient;
|
|
18
42
|
private sandboxName: string | null = null;
|
|
43
|
+
private codeInterpreter: CodeInterpreter;
|
|
19
44
|
|
|
20
45
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
21
46
|
super(ctx, env);
|
|
22
|
-
this.client = new
|
|
23
|
-
onCommandComplete: (success, exitCode, _stdout, _stderr, command
|
|
47
|
+
this.client = new JupyterClient({
|
|
48
|
+
onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
|
|
24
49
|
console.log(
|
|
25
50
|
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
26
51
|
);
|
|
27
52
|
},
|
|
28
|
-
onCommandStart: (command
|
|
29
|
-
console.log(
|
|
30
|
-
`[Container] Command started: ${command} ${args.join(" ")}`
|
|
31
|
-
);
|
|
53
|
+
onCommandStart: (command) => {
|
|
54
|
+
console.log(`[Container] Command started: ${command}`);
|
|
32
55
|
},
|
|
33
|
-
onError: (error, _command
|
|
56
|
+
onError: (error, _command) => {
|
|
34
57
|
console.error(`[Container] Command error: ${error}`);
|
|
35
58
|
},
|
|
36
59
|
onOutput: (stream, data, _command) => {
|
|
@@ -40,9 +63,13 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
40
63
|
stub: this,
|
|
41
64
|
});
|
|
42
65
|
|
|
66
|
+
// Initialize code interpreter
|
|
67
|
+
this.codeInterpreter = new CodeInterpreter(this);
|
|
68
|
+
|
|
43
69
|
// Load the sandbox name from storage on initialization
|
|
44
70
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
45
|
-
this.sandboxName =
|
|
71
|
+
this.sandboxName =
|
|
72
|
+
(await this.ctx.storage.get<string>("sandboxName")) || null;
|
|
46
73
|
});
|
|
47
74
|
}
|
|
48
75
|
|
|
@@ -50,7 +77,7 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
50
77
|
async setSandboxName(name: string): Promise<void> {
|
|
51
78
|
if (!this.sandboxName) {
|
|
52
79
|
this.sandboxName = name;
|
|
53
|
-
await this.ctx.storage.put(
|
|
80
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
54
81
|
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
55
82
|
}
|
|
56
83
|
}
|
|
@@ -76,21 +103,15 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
76
103
|
console.log("Sandbox error:", error);
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
// Override fetch to
|
|
106
|
+
// Override fetch to route internal container requests to appropriate ports
|
|
80
107
|
override async fetch(request: Request): Promise<Response> {
|
|
81
108
|
const url = new URL(request.url);
|
|
82
109
|
|
|
83
|
-
// Capture the hostname from the first request
|
|
84
|
-
if (!this.workerHostname) {
|
|
85
|
-
this.workerHostname = url.hostname;
|
|
86
|
-
console.log(`[Sandbox] Captured hostname: ${this.workerHostname}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
110
|
// Capture and store the sandbox name from the header if present
|
|
90
|
-
if (!this.sandboxName && request.headers.has(
|
|
91
|
-
const name = request.headers.get(
|
|
111
|
+
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
112
|
+
const name = request.headers.get("X-Sandbox-Name")!;
|
|
92
113
|
this.sandboxName = name;
|
|
93
|
-
await this.ctx.storage.put(
|
|
114
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
94
115
|
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
95
116
|
}
|
|
96
117
|
|
|
@@ -113,97 +134,421 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
113
134
|
return 3000;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
// Enhanced exec method - always returns ExecResult with optional streaming
|
|
138
|
+
// This replaces the old exec method to match ISandbox interface
|
|
139
|
+
async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
140
|
+
const startTime = Date.now();
|
|
141
|
+
const timestamp = new Date().toISOString();
|
|
142
|
+
|
|
143
|
+
// Handle timeout
|
|
144
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Handle cancellation
|
|
148
|
+
if (options?.signal?.aborted) {
|
|
149
|
+
throw new Error("Operation was aborted");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let result: ExecResult;
|
|
153
|
+
|
|
154
|
+
if (options?.stream && options?.onOutput) {
|
|
155
|
+
// Streaming with callbacks - we need to collect the final result
|
|
156
|
+
result = await this.executeWithStreaming(
|
|
157
|
+
command,
|
|
158
|
+
options,
|
|
159
|
+
startTime,
|
|
160
|
+
timestamp
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
// Regular execution
|
|
164
|
+
const response = await this.client.execute(command, {
|
|
165
|
+
sessionId: options?.sessionId,
|
|
166
|
+
cwd: options?.cwd,
|
|
167
|
+
env: options?.env,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const duration = Date.now() - startTime;
|
|
171
|
+
result = this.mapExecuteResponseToExecResult(
|
|
172
|
+
response,
|
|
173
|
+
duration,
|
|
174
|
+
options?.sessionId
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Call completion callback if provided
|
|
179
|
+
if (options?.onComplete) {
|
|
180
|
+
options.onComplete(result);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (options?.onError && error instanceof Error) {
|
|
186
|
+
options.onError(error);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
} finally {
|
|
190
|
+
if (timeoutId) {
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private async executeWithStreaming(
|
|
197
|
+
command: string,
|
|
198
|
+
options: ExecOptions,
|
|
199
|
+
startTime: number,
|
|
200
|
+
timestamp: string
|
|
201
|
+
): Promise<ExecResult> {
|
|
202
|
+
let stdout = "";
|
|
203
|
+
let stderr = "";
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const stream = await this.client.executeCommandStream(
|
|
207
|
+
command,
|
|
208
|
+
options.sessionId
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
for await (const event of parseSSEStream<import("./types").ExecEvent>(
|
|
212
|
+
stream
|
|
213
|
+
)) {
|
|
214
|
+
// Check for cancellation
|
|
215
|
+
if (options.signal?.aborted) {
|
|
216
|
+
throw new Error("Operation was aborted");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
switch (event.type) {
|
|
220
|
+
case "stdout":
|
|
221
|
+
case "stderr":
|
|
222
|
+
if (event.data) {
|
|
223
|
+
// Update accumulated output
|
|
224
|
+
if (event.type === "stdout") stdout += event.data;
|
|
225
|
+
if (event.type === "stderr") stderr += event.data;
|
|
226
|
+
|
|
227
|
+
// Call user's callback
|
|
228
|
+
if (options.onOutput) {
|
|
229
|
+
options.onOutput(event.type, event.data);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case "complete": {
|
|
235
|
+
// Use result from complete event if available
|
|
236
|
+
const duration = Date.now() - startTime;
|
|
237
|
+
return (
|
|
238
|
+
event.result || {
|
|
239
|
+
success: event.exitCode === 0,
|
|
240
|
+
exitCode: event.exitCode || 0,
|
|
241
|
+
stdout,
|
|
242
|
+
stderr,
|
|
243
|
+
command,
|
|
244
|
+
duration,
|
|
245
|
+
timestamp,
|
|
246
|
+
sessionId: options.sessionId,
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
case "error":
|
|
252
|
+
throw new Error(event.error || "Command execution failed");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If we get here without a complete event, something went wrong
|
|
257
|
+
throw new Error("Stream ended without completion event");
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (options.signal?.aborted) {
|
|
260
|
+
throw new Error("Operation was aborted");
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private mapExecuteResponseToExecResult(
|
|
267
|
+
response: import("./client").ExecuteResponse,
|
|
268
|
+
duration: number,
|
|
269
|
+
sessionId?: string
|
|
270
|
+
): ExecResult {
|
|
271
|
+
return {
|
|
272
|
+
success: response.success,
|
|
273
|
+
exitCode: response.exitCode,
|
|
274
|
+
stdout: response.stdout,
|
|
275
|
+
stderr: response.stderr,
|
|
276
|
+
command: response.command,
|
|
277
|
+
duration,
|
|
278
|
+
timestamp: response.timestamp,
|
|
279
|
+
sessionId,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Background process management
|
|
284
|
+
async startProcess(
|
|
285
|
+
command: string,
|
|
286
|
+
options?: ProcessOptions
|
|
287
|
+
): Promise<Process> {
|
|
288
|
+
// Use the new HttpClient method to start the process
|
|
289
|
+
try {
|
|
290
|
+
const response = await this.client.startProcess(command, {
|
|
291
|
+
processId: options?.processId,
|
|
292
|
+
sessionId: options?.sessionId,
|
|
293
|
+
timeout: options?.timeout,
|
|
294
|
+
env: options?.env,
|
|
295
|
+
cwd: options?.cwd,
|
|
296
|
+
encoding: options?.encoding,
|
|
297
|
+
autoCleanup: options?.autoCleanup,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const process = response.process;
|
|
301
|
+
const processObj: Process = {
|
|
302
|
+
id: process.id,
|
|
303
|
+
pid: process.pid,
|
|
304
|
+
command: process.command,
|
|
305
|
+
status: process.status as ProcessStatus,
|
|
306
|
+
startTime: new Date(process.startTime),
|
|
307
|
+
endTime: undefined,
|
|
308
|
+
exitCode: undefined,
|
|
309
|
+
sessionId: process.sessionId,
|
|
310
|
+
|
|
311
|
+
async kill(): Promise<void> {
|
|
312
|
+
throw new Error("Method will be replaced");
|
|
313
|
+
},
|
|
314
|
+
async getStatus(): Promise<ProcessStatus> {
|
|
315
|
+
throw new Error("Method will be replaced");
|
|
316
|
+
},
|
|
317
|
+
async getLogs(): Promise<{ stdout: string; stderr: string }> {
|
|
318
|
+
throw new Error("Method will be replaced");
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Bind context properly
|
|
323
|
+
processObj.kill = async (signal?: string) => {
|
|
324
|
+
await this.killProcess(process.id, signal);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
processObj.getStatus = async () => {
|
|
328
|
+
const current = await this.getProcess(process.id);
|
|
329
|
+
return current?.status || "error";
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
processObj.getLogs = async () => {
|
|
333
|
+
const logs = await this.getProcessLogs(process.id);
|
|
334
|
+
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Call onStart callback if provided
|
|
338
|
+
if (options?.onStart) {
|
|
339
|
+
options.onStart(processObj);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return processObj;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (options?.onError && error instanceof Error) {
|
|
345
|
+
options.onError(error);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async listProcesses(): Promise<Process[]> {
|
|
353
|
+
const response = await this.client.listProcesses();
|
|
354
|
+
|
|
355
|
+
return response.processes.map((processData) => ({
|
|
356
|
+
id: processData.id,
|
|
357
|
+
pid: processData.pid,
|
|
358
|
+
command: processData.command,
|
|
359
|
+
status: processData.status,
|
|
360
|
+
startTime: new Date(processData.startTime),
|
|
361
|
+
endTime: processData.endTime ? new Date(processData.endTime) : undefined,
|
|
362
|
+
exitCode: processData.exitCode,
|
|
363
|
+
sessionId: processData.sessionId,
|
|
364
|
+
|
|
365
|
+
kill: async (signal?: string) => {
|
|
366
|
+
await this.killProcess(processData.id, signal);
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
getStatus: async () => {
|
|
370
|
+
const current = await this.getProcess(processData.id);
|
|
371
|
+
return current?.status || "error";
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
getLogs: async () => {
|
|
375
|
+
const logs = await this.getProcessLogs(processData.id);
|
|
376
|
+
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
377
|
+
},
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async getProcess(id: string): Promise<Process | null> {
|
|
382
|
+
const response = await this.client.getProcess(id);
|
|
383
|
+
if (!response.process) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const processData = response.process;
|
|
388
|
+
return {
|
|
389
|
+
id: processData.id,
|
|
390
|
+
pid: processData.pid,
|
|
391
|
+
command: processData.command,
|
|
392
|
+
status: processData.status,
|
|
393
|
+
startTime: new Date(processData.startTime),
|
|
394
|
+
endTime: processData.endTime ? new Date(processData.endTime) : undefined,
|
|
395
|
+
exitCode: processData.exitCode,
|
|
396
|
+
sessionId: processData.sessionId,
|
|
397
|
+
|
|
398
|
+
kill: async (signal?: string) => {
|
|
399
|
+
await this.killProcess(processData.id, signal);
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
getStatus: async () => {
|
|
403
|
+
const current = await this.getProcess(processData.id);
|
|
404
|
+
return current?.status || "error";
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
getLogs: async () => {
|
|
408
|
+
const logs = await this.getProcessLogs(processData.id);
|
|
409
|
+
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async killProcess(id: string, _signal?: string): Promise<void> {
|
|
415
|
+
try {
|
|
416
|
+
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
417
|
+
await this.client.killProcess(id);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (
|
|
420
|
+
error instanceof Error &&
|
|
421
|
+
error.message.includes("Process not found")
|
|
422
|
+
) {
|
|
423
|
+
throw new ProcessNotFoundError(id);
|
|
424
|
+
}
|
|
425
|
+
throw new SandboxError(
|
|
426
|
+
`Failed to kill process ${id}: ${
|
|
427
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
428
|
+
}`,
|
|
429
|
+
"KILL_PROCESS_FAILED"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async killAllProcesses(): Promise<number> {
|
|
435
|
+
const response = await this.client.killAllProcesses();
|
|
436
|
+
return response.killedCount;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async cleanupCompletedProcesses(): Promise<number> {
|
|
440
|
+
// For now, this would need to be implemented as a container endpoint
|
|
441
|
+
// as we no longer maintain local process storage
|
|
442
|
+
// We'll return 0 as a placeholder until the container endpoint is added
|
|
443
|
+
return 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async getProcessLogs(
|
|
447
|
+
id: string
|
|
448
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
449
|
+
try {
|
|
450
|
+
const response = await this.client.getProcessLogs(id);
|
|
451
|
+
return {
|
|
452
|
+
stdout: response.stdout,
|
|
453
|
+
stderr: response.stderr,
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (
|
|
457
|
+
error instanceof Error &&
|
|
458
|
+
error.message.includes("Process not found")
|
|
459
|
+
) {
|
|
460
|
+
throw new ProcessNotFoundError(id);
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Streaming methods - return ReadableStream for RPC compatibility
|
|
467
|
+
async execStream(
|
|
468
|
+
command: string,
|
|
469
|
+
options?: StreamOptions
|
|
470
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
471
|
+
// Check for cancellation
|
|
472
|
+
if (options?.signal?.aborted) {
|
|
473
|
+
throw new Error("Operation was aborted");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Get the stream from HttpClient (need to add this method)
|
|
477
|
+
const stream = await this.client.executeCommandStream(
|
|
478
|
+
command,
|
|
479
|
+
options?.sessionId
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
// Return the ReadableStream directly - can be converted to AsyncIterable by consumers
|
|
483
|
+
return stream;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async streamProcessLogs(
|
|
487
|
+
processId: string,
|
|
488
|
+
options?: { signal?: AbortSignal }
|
|
489
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
490
|
+
// Check for cancellation
|
|
491
|
+
if (options?.signal?.aborted) {
|
|
492
|
+
throw new Error("Operation was aborted");
|
|
119
493
|
}
|
|
120
|
-
|
|
494
|
+
|
|
495
|
+
// Get the stream from HttpClient
|
|
496
|
+
const stream = await this.client.streamProcessLogs(processId);
|
|
497
|
+
|
|
498
|
+
// Return the ReadableStream directly - can be converted to AsyncIterable by consumers
|
|
499
|
+
return stream;
|
|
121
500
|
}
|
|
122
501
|
|
|
123
502
|
async gitCheckout(
|
|
124
503
|
repoUrl: string,
|
|
125
|
-
options: { branch?: string; targetDir?: string
|
|
504
|
+
options: { branch?: string; targetDir?: string }
|
|
126
505
|
) {
|
|
127
|
-
if (options?.stream) {
|
|
128
|
-
return this.client.gitCheckoutStream(
|
|
129
|
-
repoUrl,
|
|
130
|
-
options.branch,
|
|
131
|
-
options.targetDir
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
506
|
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
135
507
|
}
|
|
136
508
|
|
|
137
|
-
async mkdir(
|
|
138
|
-
path: string,
|
|
139
|
-
options: { recursive?: boolean; stream?: boolean } = {}
|
|
140
|
-
) {
|
|
141
|
-
if (options?.stream) {
|
|
142
|
-
return this.client.mkdirStream(path, options.recursive);
|
|
143
|
-
}
|
|
509
|
+
async mkdir(path: string, options: { recursive?: boolean } = {}) {
|
|
144
510
|
return this.client.mkdir(path, options.recursive);
|
|
145
511
|
}
|
|
146
512
|
|
|
147
513
|
async writeFile(
|
|
148
514
|
path: string,
|
|
149
515
|
content: string,
|
|
150
|
-
options: { encoding?: string
|
|
516
|
+
options: { encoding?: string } = {}
|
|
151
517
|
) {
|
|
152
|
-
if (options?.stream) {
|
|
153
|
-
return this.client.writeFileStream(path, content, options.encoding);
|
|
154
|
-
}
|
|
155
518
|
return this.client.writeFile(path, content, options.encoding);
|
|
156
519
|
}
|
|
157
520
|
|
|
158
|
-
async deleteFile(path: string
|
|
159
|
-
if (options?.stream) {
|
|
160
|
-
return this.client.deleteFileStream(path);
|
|
161
|
-
}
|
|
521
|
+
async deleteFile(path: string) {
|
|
162
522
|
return this.client.deleteFile(path);
|
|
163
523
|
}
|
|
164
524
|
|
|
165
|
-
async renameFile(
|
|
166
|
-
oldPath: string,
|
|
167
|
-
newPath: string,
|
|
168
|
-
options: { stream?: boolean } = {}
|
|
169
|
-
) {
|
|
170
|
-
if (options?.stream) {
|
|
171
|
-
return this.client.renameFileStream(oldPath, newPath);
|
|
172
|
-
}
|
|
525
|
+
async renameFile(oldPath: string, newPath: string) {
|
|
173
526
|
return this.client.renameFile(oldPath, newPath);
|
|
174
527
|
}
|
|
175
528
|
|
|
176
|
-
async moveFile(
|
|
177
|
-
sourcePath: string,
|
|
178
|
-
destinationPath: string,
|
|
179
|
-
options: { stream?: boolean } = {}
|
|
180
|
-
) {
|
|
181
|
-
if (options?.stream) {
|
|
182
|
-
return this.client.moveFileStream(sourcePath, destinationPath);
|
|
183
|
-
}
|
|
529
|
+
async moveFile(sourcePath: string, destinationPath: string) {
|
|
184
530
|
return this.client.moveFile(sourcePath, destinationPath);
|
|
185
531
|
}
|
|
186
532
|
|
|
187
|
-
async readFile(
|
|
188
|
-
path: string,
|
|
189
|
-
options: { encoding?: string; stream?: boolean } = {}
|
|
190
|
-
) {
|
|
191
|
-
if (options?.stream) {
|
|
192
|
-
return this.client.readFileStream(path, options.encoding);
|
|
193
|
-
}
|
|
533
|
+
async readFile(path: string, options: { encoding?: string } = {}) {
|
|
194
534
|
return this.client.readFile(path, options.encoding);
|
|
195
535
|
}
|
|
196
536
|
|
|
197
|
-
async exposePort(port: number, options
|
|
537
|
+
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
198
538
|
await this.client.exposePort(port, options?.name);
|
|
199
539
|
|
|
200
540
|
// We need the sandbox name to construct preview URLs
|
|
201
541
|
if (!this.sandboxName) {
|
|
202
|
-
throw new Error(
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
544
|
+
);
|
|
203
545
|
}
|
|
204
546
|
|
|
205
|
-
const
|
|
206
|
-
|
|
547
|
+
const url = this.constructPreviewUrl(
|
|
548
|
+
port,
|
|
549
|
+
this.sandboxName,
|
|
550
|
+
options.hostname
|
|
551
|
+
);
|
|
207
552
|
|
|
208
553
|
return {
|
|
209
554
|
url,
|
|
@@ -213,20 +558,41 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
213
558
|
}
|
|
214
559
|
|
|
215
560
|
async unexposePort(port: number) {
|
|
561
|
+
if (!validatePort(port)) {
|
|
562
|
+
logSecurityEvent(
|
|
563
|
+
"INVALID_PORT_UNEXPOSE",
|
|
564
|
+
{
|
|
565
|
+
port,
|
|
566
|
+
},
|
|
567
|
+
"high"
|
|
568
|
+
);
|
|
569
|
+
throw new SecurityError(
|
|
570
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
216
574
|
await this.client.unexposePort(port);
|
|
575
|
+
|
|
576
|
+
logSecurityEvent(
|
|
577
|
+
"PORT_UNEXPOSED",
|
|
578
|
+
{
|
|
579
|
+
port,
|
|
580
|
+
},
|
|
581
|
+
"low"
|
|
582
|
+
);
|
|
217
583
|
}
|
|
218
584
|
|
|
219
|
-
async getExposedPorts() {
|
|
585
|
+
async getExposedPorts(hostname: string) {
|
|
220
586
|
const response = await this.client.getExposedPorts();
|
|
221
587
|
|
|
222
588
|
// We need the sandbox name to construct preview URLs
|
|
223
589
|
if (!this.sandboxName) {
|
|
224
|
-
throw new Error(
|
|
590
|
+
throw new Error(
|
|
591
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
592
|
+
);
|
|
225
593
|
}
|
|
226
594
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return response.ports.map(port => ({
|
|
595
|
+
return response.ports.map((port) => ({
|
|
230
596
|
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
231
597
|
port: port.port,
|
|
232
598
|
name: port.name,
|
|
@@ -234,25 +600,179 @@ export class Sandbox<Env = unknown> extends Container<Env> {
|
|
|
234
600
|
}));
|
|
235
601
|
}
|
|
236
602
|
|
|
237
|
-
private
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
603
|
+
private constructPreviewUrl(
|
|
604
|
+
port: number,
|
|
605
|
+
sandboxId: string,
|
|
606
|
+
hostname: string
|
|
607
|
+
): string {
|
|
608
|
+
if (!validatePort(port)) {
|
|
609
|
+
logSecurityEvent(
|
|
610
|
+
"INVALID_PORT_REJECTED",
|
|
611
|
+
{
|
|
612
|
+
port,
|
|
613
|
+
sandboxId,
|
|
614
|
+
hostname,
|
|
615
|
+
},
|
|
616
|
+
"high"
|
|
617
|
+
);
|
|
618
|
+
throw new SecurityError(
|
|
619
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let sanitizedSandboxId: string;
|
|
624
|
+
try {
|
|
625
|
+
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
logSecurityEvent(
|
|
628
|
+
"INVALID_SANDBOX_ID_REJECTED",
|
|
629
|
+
{
|
|
630
|
+
sandboxId,
|
|
631
|
+
port,
|
|
632
|
+
hostname,
|
|
633
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
634
|
+
},
|
|
635
|
+
"high"
|
|
636
|
+
);
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
241
639
|
|
|
242
|
-
private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
|
|
243
|
-
// Check if this is a localhost pattern
|
|
244
640
|
const isLocalhost = isLocalhostPattern(hostname);
|
|
245
641
|
|
|
246
642
|
if (isLocalhost) {
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
643
|
+
// Unified subdomain approach for localhost (RFC 6761)
|
|
644
|
+
const [host, portStr] = hostname.split(":");
|
|
645
|
+
const mainPort = portStr || "80";
|
|
646
|
+
|
|
647
|
+
// Use URL constructor for safe URL building
|
|
648
|
+
try {
|
|
649
|
+
const baseUrl = new URL(`http://${host}:${mainPort}`);
|
|
650
|
+
// Construct subdomain safely
|
|
651
|
+
const subdomainHost = `${port}-${sanitizedSandboxId}.${host}`;
|
|
652
|
+
baseUrl.hostname = subdomainHost;
|
|
653
|
+
|
|
654
|
+
const finalUrl = baseUrl.toString();
|
|
655
|
+
|
|
656
|
+
logSecurityEvent(
|
|
657
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
658
|
+
{
|
|
659
|
+
port,
|
|
660
|
+
sandboxId: sanitizedSandboxId,
|
|
661
|
+
hostname,
|
|
662
|
+
resultUrl: finalUrl,
|
|
663
|
+
environment: "localhost",
|
|
664
|
+
},
|
|
665
|
+
"low"
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
return finalUrl;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
logSecurityEvent(
|
|
671
|
+
"URL_CONSTRUCTION_FAILED",
|
|
672
|
+
{
|
|
673
|
+
port,
|
|
674
|
+
sandboxId: sanitizedSandboxId,
|
|
675
|
+
hostname,
|
|
676
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
677
|
+
},
|
|
678
|
+
"high"
|
|
679
|
+
);
|
|
680
|
+
throw new SecurityError(
|
|
681
|
+
`Failed to construct preview URL: ${
|
|
682
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
683
|
+
}`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
251
686
|
}
|
|
252
687
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
688
|
+
// Production subdomain logic - enforce HTTPS
|
|
689
|
+
try {
|
|
690
|
+
// Always use HTTPS for production (non-localhost)
|
|
691
|
+
const protocol = "https";
|
|
692
|
+
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
693
|
+
|
|
694
|
+
// Construct subdomain safely
|
|
695
|
+
const subdomainHost = `${port}-${sanitizedSandboxId}.${hostname}`;
|
|
696
|
+
baseUrl.hostname = subdomainHost;
|
|
697
|
+
|
|
698
|
+
const finalUrl = baseUrl.toString();
|
|
699
|
+
|
|
700
|
+
logSecurityEvent(
|
|
701
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
702
|
+
{
|
|
703
|
+
port,
|
|
704
|
+
sandboxId: sanitizedSandboxId,
|
|
705
|
+
hostname,
|
|
706
|
+
resultUrl: finalUrl,
|
|
707
|
+
environment: "production",
|
|
708
|
+
},
|
|
709
|
+
"low"
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
return finalUrl;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
logSecurityEvent(
|
|
715
|
+
"URL_CONSTRUCTION_FAILED",
|
|
716
|
+
{
|
|
717
|
+
port,
|
|
718
|
+
sandboxId: sanitizedSandboxId,
|
|
719
|
+
hostname,
|
|
720
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
721
|
+
},
|
|
722
|
+
"high"
|
|
723
|
+
);
|
|
724
|
+
throw new SecurityError(
|
|
725
|
+
`Failed to construct preview URL: ${
|
|
726
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
727
|
+
}`
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Code Interpreter Methods
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Create a new code execution context
|
|
736
|
+
*/
|
|
737
|
+
async createCodeContext(
|
|
738
|
+
options?: CreateContextOptions
|
|
739
|
+
): Promise<CodeContext> {
|
|
740
|
+
return this.codeInterpreter.createCodeContext(options);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Run code with streaming callbacks
|
|
745
|
+
*/
|
|
746
|
+
async runCode(
|
|
747
|
+
code: string,
|
|
748
|
+
options?: RunCodeOptions
|
|
749
|
+
): Promise<ExecutionResult> {
|
|
750
|
+
const execution = await this.codeInterpreter.runCode(code, options);
|
|
751
|
+
// Convert to plain object for RPC serialization
|
|
752
|
+
return execution.toJSON();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Run code and return a streaming response
|
|
757
|
+
*/
|
|
758
|
+
async runCodeStream(
|
|
759
|
+
code: string,
|
|
760
|
+
options?: RunCodeOptions
|
|
761
|
+
): Promise<ReadableStream> {
|
|
762
|
+
return this.codeInterpreter.runCodeStream(code, options);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* List all code contexts
|
|
767
|
+
*/
|
|
768
|
+
async listCodeContexts(): Promise<CodeContext[]> {
|
|
769
|
+
return this.codeInterpreter.listCodeContexts();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Delete a code context
|
|
774
|
+
*/
|
|
775
|
+
async deleteCodeContext(contextId: string): Promise<void> {
|
|
776
|
+
return this.codeInterpreter.deleteCodeContext(contextId);
|
|
257
777
|
}
|
|
258
778
|
}
|