@cloudflare/sandbox 0.0.0-d55b0f4 → 0.0.0-d81d2a5
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 +24 -0
- package/Dockerfile +36 -13
- package/README.md +784 -0
- package/container_src/bun.lock +122 -0
- package/container_src/handler/exec.ts +17 -14
- package/container_src/handler/process.ts +1 -1
- package/container_src/index.ts +171 -1
- package/container_src/jupyter-server.ts +336 -0
- package/container_src/mime-processor.ts +255 -0
- package/container_src/package.json +9 -0
- package/container_src/startup.sh +52 -0
- package/container_src/types.ts +8 -3
- package/package.json +1 -1
- package/src/client.ts +47 -64
- package/src/index.ts +13 -4
- package/src/interpreter-types.ts +383 -0
- package/src/interpreter.ts +150 -0
- package/src/jupyter-client.ts +266 -0
- package/src/sandbox.ts +281 -149
- package/src/types.ts +15 -0
package/src/sandbox.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
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";
|
|
4
11
|
import {
|
|
5
12
|
logSecurityEvent,
|
|
6
13
|
SecurityError,
|
|
7
14
|
sanitizeSandboxId,
|
|
8
|
-
validatePort
|
|
15
|
+
validatePort,
|
|
9
16
|
} from "./security";
|
|
17
|
+
import { parseSSEStream } from "./sse-parser";
|
|
10
18
|
import type {
|
|
11
19
|
ExecOptions,
|
|
12
20
|
ExecResult,
|
|
@@ -14,12 +22,9 @@ import type {
|
|
|
14
22
|
Process,
|
|
15
23
|
ProcessOptions,
|
|
16
24
|
ProcessStatus,
|
|
17
|
-
StreamOptions
|
|
18
|
-
} from "./types";
|
|
19
|
-
import {
|
|
20
|
-
ProcessNotFoundError,
|
|
21
|
-
SandboxError
|
|
25
|
+
StreamOptions,
|
|
22
26
|
} from "./types";
|
|
27
|
+
import { ProcessNotFoundError, SandboxError } from "./types";
|
|
23
28
|
|
|
24
29
|
export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
25
30
|
const stub = getContainer(ns, id);
|
|
@@ -33,21 +38,20 @@ export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string) {
|
|
|
33
38
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
34
39
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
35
40
|
sleepAfter = "3m"; // Sleep the sandbox if no requests are made in this timeframe
|
|
36
|
-
client:
|
|
41
|
+
client: JupyterClient;
|
|
37
42
|
private sandboxName: string | null = null;
|
|
43
|
+
private codeInterpreter: CodeInterpreter;
|
|
38
44
|
|
|
39
45
|
constructor(ctx: DurableObjectState, env: Env) {
|
|
40
46
|
super(ctx, env);
|
|
41
|
-
this.client = new
|
|
47
|
+
this.client = new JupyterClient({
|
|
42
48
|
onCommandComplete: (success, exitCode, _stdout, _stderr, command) => {
|
|
43
49
|
console.log(
|
|
44
50
|
`[Container] Command completed: ${command}, Success: ${success}, Exit code: ${exitCode}`
|
|
45
51
|
);
|
|
46
52
|
},
|
|
47
53
|
onCommandStart: (command) => {
|
|
48
|
-
console.log(
|
|
49
|
-
`[Container] Command started: ${command}`
|
|
50
|
-
);
|
|
54
|
+
console.log(`[Container] Command started: ${command}`);
|
|
51
55
|
},
|
|
52
56
|
onError: (error, _command) => {
|
|
53
57
|
console.error(`[Container] Command error: ${error}`);
|
|
@@ -59,9 +63,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
59
63
|
stub: this,
|
|
60
64
|
});
|
|
61
65
|
|
|
66
|
+
// Initialize code interpreter
|
|
67
|
+
this.codeInterpreter = new CodeInterpreter(this);
|
|
68
|
+
|
|
62
69
|
// Load the sandbox name from storage on initialization
|
|
63
70
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
64
|
-
this.sandboxName =
|
|
71
|
+
this.sandboxName =
|
|
72
|
+
(await this.ctx.storage.get<string>("sandboxName")) || null;
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -69,7 +77,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
69
77
|
async setSandboxName(name: string): Promise<void> {
|
|
70
78
|
if (!this.sandboxName) {
|
|
71
79
|
this.sandboxName = name;
|
|
72
|
-
await this.ctx.storage.put(
|
|
80
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
73
81
|
console.log(`[Sandbox] Stored sandbox name via RPC: ${name}`);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -100,10 +108,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
100
108
|
const url = new URL(request.url);
|
|
101
109
|
|
|
102
110
|
// Capture and store the sandbox name from the header if present
|
|
103
|
-
if (!this.sandboxName && request.headers.has(
|
|
104
|
-
const name = request.headers.get(
|
|
111
|
+
if (!this.sandboxName && request.headers.has("X-Sandbox-Name")) {
|
|
112
|
+
const name = request.headers.get("X-Sandbox-Name")!;
|
|
105
113
|
this.sandboxName = name;
|
|
106
|
-
await this.ctx.storage.put(
|
|
114
|
+
await this.ctx.storage.put("sandboxName", name);
|
|
107
115
|
console.log(`[Sandbox] Stored sandbox name: ${this.sandboxName}`);
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -138,23 +146,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
138
146
|
try {
|
|
139
147
|
// Handle cancellation
|
|
140
148
|
if (options?.signal?.aborted) {
|
|
141
|
-
throw new Error(
|
|
149
|
+
throw new Error("Operation was aborted");
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
let result: ExecResult;
|
|
145
153
|
|
|
146
154
|
if (options?.stream && options?.onOutput) {
|
|
147
155
|
// Streaming with callbacks - we need to collect the final result
|
|
148
|
-
result = await this.executeWithStreaming(
|
|
149
|
-
} else {
|
|
150
|
-
// Regular execution
|
|
151
|
-
const response = await this.client.execute(
|
|
156
|
+
result = await this.executeWithStreaming(
|
|
152
157
|
command,
|
|
153
|
-
options
|
|
158
|
+
options,
|
|
159
|
+
startTime,
|
|
160
|
+
timestamp
|
|
154
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
|
+
});
|
|
155
169
|
|
|
156
170
|
const duration = Date.now() - startTime;
|
|
157
|
-
result = this.mapExecuteResponseToExecResult(
|
|
171
|
+
result = this.mapExecuteResponseToExecResult(
|
|
172
|
+
response,
|
|
173
|
+
duration,
|
|
174
|
+
options?.sessionId
|
|
175
|
+
);
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
// Call completion callback if provided
|
|
@@ -181,26 +199,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
181
199
|
startTime: number,
|
|
182
200
|
timestamp: string
|
|
183
201
|
): Promise<ExecResult> {
|
|
184
|
-
let stdout =
|
|
185
|
-
let stderr =
|
|
202
|
+
let stdout = "";
|
|
203
|
+
let stderr = "";
|
|
186
204
|
|
|
187
205
|
try {
|
|
188
|
-
const stream = await this.client.executeCommandStream(
|
|
189
|
-
|
|
206
|
+
const stream = await this.client.executeCommandStream(
|
|
207
|
+
command,
|
|
208
|
+
options.sessionId
|
|
209
|
+
);
|
|
190
210
|
|
|
191
|
-
for await (const event of parseSSEStream<import(
|
|
211
|
+
for await (const event of parseSSEStream<import("./types").ExecEvent>(
|
|
212
|
+
stream
|
|
213
|
+
)) {
|
|
192
214
|
// Check for cancellation
|
|
193
215
|
if (options.signal?.aborted) {
|
|
194
|
-
throw new Error(
|
|
216
|
+
throw new Error("Operation was aborted");
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
switch (event.type) {
|
|
198
|
-
case
|
|
199
|
-
case
|
|
220
|
+
case "stdout":
|
|
221
|
+
case "stderr":
|
|
200
222
|
if (event.data) {
|
|
201
223
|
// Update accumulated output
|
|
202
|
-
if (event.type ===
|
|
203
|
-
if (event.type ===
|
|
224
|
+
if (event.type === "stdout") stdout += event.data;
|
|
225
|
+
if (event.type === "stderr") stderr += event.data;
|
|
204
226
|
|
|
205
227
|
// Call user's callback
|
|
206
228
|
if (options.onOutput) {
|
|
@@ -209,39 +231,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
209
231
|
}
|
|
210
232
|
break;
|
|
211
233
|
|
|
212
|
-
case
|
|
234
|
+
case "complete": {
|
|
213
235
|
// Use result from complete event if available
|
|
214
236
|
const duration = Date.now() - startTime;
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
);
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
case
|
|
228
|
-
throw new Error(event.error ||
|
|
251
|
+
case "error":
|
|
252
|
+
throw new Error(event.error || "Command execution failed");
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
255
|
|
|
232
256
|
// If we get here without a complete event, something went wrong
|
|
233
|
-
throw new Error(
|
|
234
|
-
|
|
257
|
+
throw new Error("Stream ended without completion event");
|
|
235
258
|
} catch (error) {
|
|
236
259
|
if (options.signal?.aborted) {
|
|
237
|
-
throw new Error(
|
|
260
|
+
throw new Error("Operation was aborted");
|
|
238
261
|
}
|
|
239
262
|
throw error;
|
|
240
263
|
}
|
|
241
264
|
}
|
|
242
265
|
|
|
243
266
|
private mapExecuteResponseToExecResult(
|
|
244
|
-
response: import(
|
|
267
|
+
response: import("./client").ExecuteResponse,
|
|
245
268
|
duration: number,
|
|
246
269
|
sessionId?: string
|
|
247
270
|
): ExecResult {
|
|
@@ -253,13 +276,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
253
276
|
command: response.command,
|
|
254
277
|
duration,
|
|
255
278
|
timestamp: response.timestamp,
|
|
256
|
-
sessionId
|
|
279
|
+
sessionId,
|
|
257
280
|
};
|
|
258
281
|
}
|
|
259
282
|
|
|
260
|
-
|
|
261
283
|
// Background process management
|
|
262
|
-
async startProcess(
|
|
284
|
+
async startProcess(
|
|
285
|
+
command: string,
|
|
286
|
+
options?: ProcessOptions
|
|
287
|
+
): Promise<Process> {
|
|
263
288
|
// Use the new HttpClient method to start the process
|
|
264
289
|
try {
|
|
265
290
|
const response = await this.client.startProcess(command, {
|
|
@@ -269,7 +294,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
269
294
|
env: options?.env,
|
|
270
295
|
cwd: options?.cwd,
|
|
271
296
|
encoding: options?.encoding,
|
|
272
|
-
autoCleanup: options?.autoCleanup
|
|
297
|
+
autoCleanup: options?.autoCleanup,
|
|
273
298
|
});
|
|
274
299
|
|
|
275
300
|
const process = response.process;
|
|
@@ -284,14 +309,14 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
284
309
|
sessionId: process.sessionId,
|
|
285
310
|
|
|
286
311
|
async kill(): Promise<void> {
|
|
287
|
-
throw new Error(
|
|
312
|
+
throw new Error("Method will be replaced");
|
|
288
313
|
},
|
|
289
314
|
async getStatus(): Promise<ProcessStatus> {
|
|
290
|
-
throw new Error(
|
|
315
|
+
throw new Error("Method will be replaced");
|
|
291
316
|
},
|
|
292
317
|
async getLogs(): Promise<{ stdout: string; stderr: string }> {
|
|
293
|
-
throw new Error(
|
|
294
|
-
}
|
|
318
|
+
throw new Error("Method will be replaced");
|
|
319
|
+
},
|
|
295
320
|
};
|
|
296
321
|
|
|
297
322
|
// Bind context properly
|
|
@@ -301,7 +326,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
301
326
|
|
|
302
327
|
processObj.getStatus = async () => {
|
|
303
328
|
const current = await this.getProcess(process.id);
|
|
304
|
-
return current?.status ||
|
|
329
|
+
return current?.status || "error";
|
|
305
330
|
};
|
|
306
331
|
|
|
307
332
|
processObj.getLogs = async () => {
|
|
@@ -315,7 +340,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
315
340
|
}
|
|
316
341
|
|
|
317
342
|
return processObj;
|
|
318
|
-
|
|
319
343
|
} catch (error) {
|
|
320
344
|
if (options?.onError && error instanceof Error) {
|
|
321
345
|
options.onError(error);
|
|
@@ -328,7 +352,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
328
352
|
async listProcesses(): Promise<Process[]> {
|
|
329
353
|
const response = await this.client.listProcesses();
|
|
330
354
|
|
|
331
|
-
return response.processes.map(processData => ({
|
|
355
|
+
return response.processes.map((processData) => ({
|
|
332
356
|
id: processData.id,
|
|
333
357
|
pid: processData.pid,
|
|
334
358
|
command: processData.command,
|
|
@@ -344,13 +368,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
344
368
|
|
|
345
369
|
getStatus: async () => {
|
|
346
370
|
const current = await this.getProcess(processData.id);
|
|
347
|
-
return current?.status ||
|
|
371
|
+
return current?.status || "error";
|
|
348
372
|
},
|
|
349
373
|
|
|
350
374
|
getLogs: async () => {
|
|
351
375
|
const logs = await this.getProcessLogs(processData.id);
|
|
352
376
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
353
|
-
}
|
|
377
|
+
},
|
|
354
378
|
}));
|
|
355
379
|
}
|
|
356
380
|
|
|
@@ -377,13 +401,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
377
401
|
|
|
378
402
|
getStatus: async () => {
|
|
379
403
|
const current = await this.getProcess(processData.id);
|
|
380
|
-
return current?.status ||
|
|
404
|
+
return current?.status || "error";
|
|
381
405
|
},
|
|
382
406
|
|
|
383
407
|
getLogs: async () => {
|
|
384
408
|
const logs = await this.getProcessLogs(processData.id);
|
|
385
409
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
386
|
-
}
|
|
410
|
+
},
|
|
387
411
|
};
|
|
388
412
|
}
|
|
389
413
|
|
|
@@ -392,12 +416,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
392
416
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
393
417
|
await this.client.killProcess(id);
|
|
394
418
|
} catch (error) {
|
|
395
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
error instanceof Error &&
|
|
421
|
+
error.message.includes("Process not found")
|
|
422
|
+
) {
|
|
396
423
|
throw new ProcessNotFoundError(id);
|
|
397
424
|
}
|
|
398
425
|
throw new SandboxError(
|
|
399
|
-
`Failed to kill process ${id}: ${
|
|
400
|
-
|
|
426
|
+
`Failed to kill process ${id}: ${
|
|
427
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
428
|
+
}`,
|
|
429
|
+
"KILL_PROCESS_FAILED"
|
|
401
430
|
);
|
|
402
431
|
}
|
|
403
432
|
}
|
|
@@ -414,40 +443,53 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
414
443
|
return 0;
|
|
415
444
|
}
|
|
416
445
|
|
|
417
|
-
async getProcessLogs(
|
|
446
|
+
async getProcessLogs(
|
|
447
|
+
id: string
|
|
448
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
418
449
|
try {
|
|
419
450
|
const response = await this.client.getProcessLogs(id);
|
|
420
451
|
return {
|
|
421
452
|
stdout: response.stdout,
|
|
422
|
-
stderr: response.stderr
|
|
453
|
+
stderr: response.stderr,
|
|
423
454
|
};
|
|
424
455
|
} catch (error) {
|
|
425
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
error instanceof Error &&
|
|
458
|
+
error.message.includes("Process not found")
|
|
459
|
+
) {
|
|
426
460
|
throw new ProcessNotFoundError(id);
|
|
427
461
|
}
|
|
428
462
|
throw error;
|
|
429
463
|
}
|
|
430
464
|
}
|
|
431
465
|
|
|
432
|
-
|
|
433
466
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
434
|
-
async execStream(
|
|
467
|
+
async execStream(
|
|
468
|
+
command: string,
|
|
469
|
+
options?: StreamOptions
|
|
470
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
435
471
|
// Check for cancellation
|
|
436
472
|
if (options?.signal?.aborted) {
|
|
437
|
-
throw new Error(
|
|
473
|
+
throw new Error("Operation was aborted");
|
|
438
474
|
}
|
|
439
475
|
|
|
440
476
|
// Get the stream from HttpClient (need to add this method)
|
|
441
|
-
const stream = await this.client.executeCommandStream(
|
|
477
|
+
const stream = await this.client.executeCommandStream(
|
|
478
|
+
command,
|
|
479
|
+
options?.sessionId
|
|
480
|
+
);
|
|
442
481
|
|
|
443
482
|
// Return the ReadableStream directly - can be converted to AsyncIterable by consumers
|
|
444
483
|
return stream;
|
|
445
484
|
}
|
|
446
485
|
|
|
447
|
-
async streamProcessLogs(
|
|
486
|
+
async streamProcessLogs(
|
|
487
|
+
processId: string,
|
|
488
|
+
options?: { signal?: AbortSignal }
|
|
489
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
448
490
|
// Check for cancellation
|
|
449
491
|
if (options?.signal?.aborted) {
|
|
450
|
-
throw new Error(
|
|
492
|
+
throw new Error("Operation was aborted");
|
|
451
493
|
}
|
|
452
494
|
|
|
453
495
|
// Get the stream from HttpClient
|
|
@@ -464,10 +506,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
464
506
|
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
465
507
|
}
|
|
466
508
|
|
|
467
|
-
async mkdir(
|
|
468
|
-
path: string,
|
|
469
|
-
options: { recursive?: boolean } = {}
|
|
470
|
-
) {
|
|
509
|
+
async mkdir(path: string, options: { recursive?: boolean } = {}) {
|
|
471
510
|
return this.client.mkdir(path, options.recursive);
|
|
472
511
|
}
|
|
473
512
|
|
|
@@ -483,24 +522,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
483
522
|
return this.client.deleteFile(path);
|
|
484
523
|
}
|
|
485
524
|
|
|
486
|
-
async renameFile(
|
|
487
|
-
oldPath: string,
|
|
488
|
-
newPath: string
|
|
489
|
-
) {
|
|
525
|
+
async renameFile(oldPath: string, newPath: string) {
|
|
490
526
|
return this.client.renameFile(oldPath, newPath);
|
|
491
527
|
}
|
|
492
528
|
|
|
493
|
-
async moveFile(
|
|
494
|
-
sourcePath: string,
|
|
495
|
-
destinationPath: string
|
|
496
|
-
) {
|
|
529
|
+
async moveFile(sourcePath: string, destinationPath: string) {
|
|
497
530
|
return this.client.moveFile(sourcePath, destinationPath);
|
|
498
531
|
}
|
|
499
532
|
|
|
500
|
-
async readFile(
|
|
501
|
-
path: string,
|
|
502
|
-
options: { encoding?: string } = {}
|
|
503
|
-
) {
|
|
533
|
+
async readFile(path: string, options: { encoding?: string } = {}) {
|
|
504
534
|
return this.client.readFile(path, options.encoding);
|
|
505
535
|
}
|
|
506
536
|
|
|
@@ -509,10 +539,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
509
539
|
|
|
510
540
|
// We need the sandbox name to construct preview URLs
|
|
511
541
|
if (!this.sandboxName) {
|
|
512
|
-
throw new Error(
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
544
|
+
);
|
|
513
545
|
}
|
|
514
546
|
|
|
515
|
-
const url = this.constructPreviewUrl(
|
|
547
|
+
const url = this.constructPreviewUrl(
|
|
548
|
+
port,
|
|
549
|
+
this.sandboxName,
|
|
550
|
+
options.hostname
|
|
551
|
+
);
|
|
516
552
|
|
|
517
553
|
return {
|
|
518
554
|
url,
|
|
@@ -523,17 +559,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
523
559
|
|
|
524
560
|
async unexposePort(port: number) {
|
|
525
561
|
if (!validatePort(port)) {
|
|
526
|
-
logSecurityEvent(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
);
|
|
530
572
|
}
|
|
531
573
|
|
|
532
574
|
await this.client.unexposePort(port);
|
|
533
575
|
|
|
534
|
-
logSecurityEvent(
|
|
535
|
-
|
|
536
|
-
|
|
576
|
+
logSecurityEvent(
|
|
577
|
+
"PORT_UNEXPOSED",
|
|
578
|
+
{
|
|
579
|
+
port,
|
|
580
|
+
},
|
|
581
|
+
"low"
|
|
582
|
+
);
|
|
537
583
|
}
|
|
538
584
|
|
|
539
585
|
async getExposedPorts(hostname: string) {
|
|
@@ -541,10 +587,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
541
587
|
|
|
542
588
|
// We need the sandbox name to construct preview URLs
|
|
543
589
|
if (!this.sandboxName) {
|
|
544
|
-
throw new Error(
|
|
590
|
+
throw new Error(
|
|
591
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
592
|
+
);
|
|
545
593
|
}
|
|
546
594
|
|
|
547
|
-
return response.ports.map(port => ({
|
|
595
|
+
return response.ports.map((port) => ({
|
|
548
596
|
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
549
597
|
port: port.port,
|
|
550
598
|
name: port.name,
|
|
@@ -552,27 +600,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
552
600
|
}));
|
|
553
601
|
}
|
|
554
602
|
|
|
555
|
-
|
|
556
|
-
|
|
603
|
+
private constructPreviewUrl(
|
|
604
|
+
port: number,
|
|
605
|
+
sandboxId: string,
|
|
606
|
+
hostname: string
|
|
607
|
+
): string {
|
|
557
608
|
if (!validatePort(port)) {
|
|
558
|
-
logSecurityEvent(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
+
);
|
|
564
621
|
}
|
|
565
622
|
|
|
566
623
|
let sanitizedSandboxId: string;
|
|
567
624
|
try {
|
|
568
625
|
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
569
626
|
} catch (error) {
|
|
570
|
-
logSecurityEvent(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
+
);
|
|
576
637
|
throw error;
|
|
577
638
|
}
|
|
578
639
|
|
|
@@ -580,8 +641,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
580
641
|
|
|
581
642
|
if (isLocalhost) {
|
|
582
643
|
// Unified subdomain approach for localhost (RFC 6761)
|
|
583
|
-
const [host, portStr] = hostname.split(
|
|
584
|
-
const mainPort = portStr ||
|
|
644
|
+
const [host, portStr] = hostname.split(":");
|
|
645
|
+
const mainPort = portStr || "80";
|
|
585
646
|
|
|
586
647
|
// Use URL constructor for safe URL building
|
|
587
648
|
try {
|
|
@@ -592,23 +653,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
592
653
|
|
|
593
654
|
const finalUrl = baseUrl.toString();
|
|
594
655
|
|
|
595
|
-
logSecurityEvent(
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
656
|
+
logSecurityEvent(
|
|
657
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
658
|
+
{
|
|
659
|
+
port,
|
|
660
|
+
sandboxId: sanitizedSandboxId,
|
|
661
|
+
hostname,
|
|
662
|
+
resultUrl: finalUrl,
|
|
663
|
+
environment: "localhost",
|
|
664
|
+
},
|
|
665
|
+
"low"
|
|
666
|
+
);
|
|
602
667
|
|
|
603
668
|
return finalUrl;
|
|
604
669
|
} catch (error) {
|
|
605
|
-
logSecurityEvent(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
+
);
|
|
612
685
|
}
|
|
613
686
|
}
|
|
614
687
|
|
|
@@ -624,23 +697,82 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
624
697
|
|
|
625
698
|
const finalUrl = baseUrl.toString();
|
|
626
699
|
|
|
627
|
-
logSecurityEvent(
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
700
|
+
logSecurityEvent(
|
|
701
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
702
|
+
{
|
|
703
|
+
port,
|
|
704
|
+
sandboxId: sanitizedSandboxId,
|
|
705
|
+
hostname,
|
|
706
|
+
resultUrl: finalUrl,
|
|
707
|
+
environment: "production",
|
|
708
|
+
},
|
|
709
|
+
"low"
|
|
710
|
+
);
|
|
634
711
|
|
|
635
712
|
return finalUrl;
|
|
636
713
|
} catch (error) {
|
|
637
|
-
logSecurityEvent(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
);
|
|
644
729
|
}
|
|
645
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);
|
|
777
|
+
}
|
|
646
778
|
}
|