@cloudflare/sandbox 0.2.0 → 0.2.1
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 +6 -0
- package/Dockerfile +31 -7
- package/README.md +226 -2
- package/container_src/bun.lock +122 -0
- 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/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
- package/dist/chunk-CUHYLCMT.js.map +1 -0
- package/dist/chunk-EGC5IYXA.js +108 -0
- package/dist/chunk-EGC5IYXA.js.map +1 -0
- package/dist/chunk-FKBV7CZS.js +113 -0
- package/dist/chunk-FKBV7CZS.js.map +1 -0
- package/dist/{chunk-ZJN2PQOS.js → chunk-IATLC32Y.js} +173 -74
- package/dist/chunk-IATLC32Y.js.map +1 -0
- package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
- package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
- package/dist/chunk-SYMWNYWA.js +185 -0
- package/dist/chunk-SYMWNYWA.js.map +1 -0
- package/dist/{client-BXYlxy-j.d.ts → client-C7rKCYBD.d.ts} +42 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +10 -4
- package/dist/interpreter-types.d.ts +259 -0
- package/dist/interpreter-types.js +9 -0
- package/dist/interpreter-types.js.map +1 -0
- package/dist/interpreter.d.ts +33 -0
- package/dist/interpreter.js +8 -0
- package/dist/interpreter.js.map +1 -0
- package/dist/jupyter-client.d.ts +4 -0
- package/dist/jupyter-client.js +8 -0
- package/dist/jupyter-client.js.map +1 -0
- package/dist/request-handler.d.ts +2 -1
- package/dist/request-handler.js +7 -3
- package/dist/sandbox.d.ts +2 -1
- package/dist/sandbox.js +7 -3
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/client.ts +37 -54
- 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 -153
- package/src/types.ts +15 -0
- package/dist/chunk-YVZ3K26G.js.map +0 -1
- package/dist/chunk-ZJN2PQOS.js.map +0 -1
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,27 +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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
env: options?.env,
|
|
157
|
-
}
|
|
158
|
+
options,
|
|
159
|
+
startTime,
|
|
160
|
+
timestamp
|
|
158
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
|
+
});
|
|
159
169
|
|
|
160
170
|
const duration = Date.now() - startTime;
|
|
161
|
-
result = this.mapExecuteResponseToExecResult(
|
|
171
|
+
result = this.mapExecuteResponseToExecResult(
|
|
172
|
+
response,
|
|
173
|
+
duration,
|
|
174
|
+
options?.sessionId
|
|
175
|
+
);
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
// Call completion callback if provided
|
|
@@ -185,26 +199,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
185
199
|
startTime: number,
|
|
186
200
|
timestamp: string
|
|
187
201
|
): Promise<ExecResult> {
|
|
188
|
-
let stdout =
|
|
189
|
-
let stderr =
|
|
202
|
+
let stdout = "";
|
|
203
|
+
let stderr = "";
|
|
190
204
|
|
|
191
205
|
try {
|
|
192
|
-
const stream = await this.client.executeCommandStream(
|
|
193
|
-
|
|
206
|
+
const stream = await this.client.executeCommandStream(
|
|
207
|
+
command,
|
|
208
|
+
options.sessionId
|
|
209
|
+
);
|
|
194
210
|
|
|
195
|
-
for await (const event of parseSSEStream<import(
|
|
211
|
+
for await (const event of parseSSEStream<import("./types").ExecEvent>(
|
|
212
|
+
stream
|
|
213
|
+
)) {
|
|
196
214
|
// Check for cancellation
|
|
197
215
|
if (options.signal?.aborted) {
|
|
198
|
-
throw new Error(
|
|
216
|
+
throw new Error("Operation was aborted");
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
switch (event.type) {
|
|
202
|
-
case
|
|
203
|
-
case
|
|
220
|
+
case "stdout":
|
|
221
|
+
case "stderr":
|
|
204
222
|
if (event.data) {
|
|
205
223
|
// Update accumulated output
|
|
206
|
-
if (event.type ===
|
|
207
|
-
if (event.type ===
|
|
224
|
+
if (event.type === "stdout") stdout += event.data;
|
|
225
|
+
if (event.type === "stderr") stderr += event.data;
|
|
208
226
|
|
|
209
227
|
// Call user's callback
|
|
210
228
|
if (options.onOutput) {
|
|
@@ -213,39 +231,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
213
231
|
}
|
|
214
232
|
break;
|
|
215
233
|
|
|
216
|
-
case
|
|
234
|
+
case "complete": {
|
|
217
235
|
// Use result from complete event if available
|
|
218
236
|
const duration = Date.now() - startTime;
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
);
|
|
229
249
|
}
|
|
230
250
|
|
|
231
|
-
case
|
|
232
|
-
throw new Error(event.error ||
|
|
251
|
+
case "error":
|
|
252
|
+
throw new Error(event.error || "Command execution failed");
|
|
233
253
|
}
|
|
234
254
|
}
|
|
235
255
|
|
|
236
256
|
// If we get here without a complete event, something went wrong
|
|
237
|
-
throw new Error(
|
|
238
|
-
|
|
257
|
+
throw new Error("Stream ended without completion event");
|
|
239
258
|
} catch (error) {
|
|
240
259
|
if (options.signal?.aborted) {
|
|
241
|
-
throw new Error(
|
|
260
|
+
throw new Error("Operation was aborted");
|
|
242
261
|
}
|
|
243
262
|
throw error;
|
|
244
263
|
}
|
|
245
264
|
}
|
|
246
265
|
|
|
247
266
|
private mapExecuteResponseToExecResult(
|
|
248
|
-
response: import(
|
|
267
|
+
response: import("./client").ExecuteResponse,
|
|
249
268
|
duration: number,
|
|
250
269
|
sessionId?: string
|
|
251
270
|
): ExecResult {
|
|
@@ -257,13 +276,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
257
276
|
command: response.command,
|
|
258
277
|
duration,
|
|
259
278
|
timestamp: response.timestamp,
|
|
260
|
-
sessionId
|
|
279
|
+
sessionId,
|
|
261
280
|
};
|
|
262
281
|
}
|
|
263
282
|
|
|
264
|
-
|
|
265
283
|
// Background process management
|
|
266
|
-
async startProcess(
|
|
284
|
+
async startProcess(
|
|
285
|
+
command: string,
|
|
286
|
+
options?: ProcessOptions
|
|
287
|
+
): Promise<Process> {
|
|
267
288
|
// Use the new HttpClient method to start the process
|
|
268
289
|
try {
|
|
269
290
|
const response = await this.client.startProcess(command, {
|
|
@@ -273,7 +294,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
273
294
|
env: options?.env,
|
|
274
295
|
cwd: options?.cwd,
|
|
275
296
|
encoding: options?.encoding,
|
|
276
|
-
autoCleanup: options?.autoCleanup
|
|
297
|
+
autoCleanup: options?.autoCleanup,
|
|
277
298
|
});
|
|
278
299
|
|
|
279
300
|
const process = response.process;
|
|
@@ -288,14 +309,14 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
288
309
|
sessionId: process.sessionId,
|
|
289
310
|
|
|
290
311
|
async kill(): Promise<void> {
|
|
291
|
-
throw new Error(
|
|
312
|
+
throw new Error("Method will be replaced");
|
|
292
313
|
},
|
|
293
314
|
async getStatus(): Promise<ProcessStatus> {
|
|
294
|
-
throw new Error(
|
|
315
|
+
throw new Error("Method will be replaced");
|
|
295
316
|
},
|
|
296
317
|
async getLogs(): Promise<{ stdout: string; stderr: string }> {
|
|
297
|
-
throw new Error(
|
|
298
|
-
}
|
|
318
|
+
throw new Error("Method will be replaced");
|
|
319
|
+
},
|
|
299
320
|
};
|
|
300
321
|
|
|
301
322
|
// Bind context properly
|
|
@@ -305,7 +326,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
305
326
|
|
|
306
327
|
processObj.getStatus = async () => {
|
|
307
328
|
const current = await this.getProcess(process.id);
|
|
308
|
-
return current?.status ||
|
|
329
|
+
return current?.status || "error";
|
|
309
330
|
};
|
|
310
331
|
|
|
311
332
|
processObj.getLogs = async () => {
|
|
@@ -319,7 +340,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
319
340
|
}
|
|
320
341
|
|
|
321
342
|
return processObj;
|
|
322
|
-
|
|
323
343
|
} catch (error) {
|
|
324
344
|
if (options?.onError && error instanceof Error) {
|
|
325
345
|
options.onError(error);
|
|
@@ -332,7 +352,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
332
352
|
async listProcesses(): Promise<Process[]> {
|
|
333
353
|
const response = await this.client.listProcesses();
|
|
334
354
|
|
|
335
|
-
return response.processes.map(processData => ({
|
|
355
|
+
return response.processes.map((processData) => ({
|
|
336
356
|
id: processData.id,
|
|
337
357
|
pid: processData.pid,
|
|
338
358
|
command: processData.command,
|
|
@@ -348,13 +368,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
348
368
|
|
|
349
369
|
getStatus: async () => {
|
|
350
370
|
const current = await this.getProcess(processData.id);
|
|
351
|
-
return current?.status ||
|
|
371
|
+
return current?.status || "error";
|
|
352
372
|
},
|
|
353
373
|
|
|
354
374
|
getLogs: async () => {
|
|
355
375
|
const logs = await this.getProcessLogs(processData.id);
|
|
356
376
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
357
|
-
}
|
|
377
|
+
},
|
|
358
378
|
}));
|
|
359
379
|
}
|
|
360
380
|
|
|
@@ -381,13 +401,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
381
401
|
|
|
382
402
|
getStatus: async () => {
|
|
383
403
|
const current = await this.getProcess(processData.id);
|
|
384
|
-
return current?.status ||
|
|
404
|
+
return current?.status || "error";
|
|
385
405
|
},
|
|
386
406
|
|
|
387
407
|
getLogs: async () => {
|
|
388
408
|
const logs = await this.getProcessLogs(processData.id);
|
|
389
409
|
return { stdout: logs.stdout, stderr: logs.stderr };
|
|
390
|
-
}
|
|
410
|
+
},
|
|
391
411
|
};
|
|
392
412
|
}
|
|
393
413
|
|
|
@@ -396,12 +416,17 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
396
416
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
397
417
|
await this.client.killProcess(id);
|
|
398
418
|
} catch (error) {
|
|
399
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
error instanceof Error &&
|
|
421
|
+
error.message.includes("Process not found")
|
|
422
|
+
) {
|
|
400
423
|
throw new ProcessNotFoundError(id);
|
|
401
424
|
}
|
|
402
425
|
throw new SandboxError(
|
|
403
|
-
`Failed to kill process ${id}: ${
|
|
404
|
-
|
|
426
|
+
`Failed to kill process ${id}: ${
|
|
427
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
428
|
+
}`,
|
|
429
|
+
"KILL_PROCESS_FAILED"
|
|
405
430
|
);
|
|
406
431
|
}
|
|
407
432
|
}
|
|
@@ -418,40 +443,53 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
418
443
|
return 0;
|
|
419
444
|
}
|
|
420
445
|
|
|
421
|
-
async getProcessLogs(
|
|
446
|
+
async getProcessLogs(
|
|
447
|
+
id: string
|
|
448
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
422
449
|
try {
|
|
423
450
|
const response = await this.client.getProcessLogs(id);
|
|
424
451
|
return {
|
|
425
452
|
stdout: response.stdout,
|
|
426
|
-
stderr: response.stderr
|
|
453
|
+
stderr: response.stderr,
|
|
427
454
|
};
|
|
428
455
|
} catch (error) {
|
|
429
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
error instanceof Error &&
|
|
458
|
+
error.message.includes("Process not found")
|
|
459
|
+
) {
|
|
430
460
|
throw new ProcessNotFoundError(id);
|
|
431
461
|
}
|
|
432
462
|
throw error;
|
|
433
463
|
}
|
|
434
464
|
}
|
|
435
465
|
|
|
436
|
-
|
|
437
466
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
438
|
-
async execStream(
|
|
467
|
+
async execStream(
|
|
468
|
+
command: string,
|
|
469
|
+
options?: StreamOptions
|
|
470
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
439
471
|
// Check for cancellation
|
|
440
472
|
if (options?.signal?.aborted) {
|
|
441
|
-
throw new Error(
|
|
473
|
+
throw new Error("Operation was aborted");
|
|
442
474
|
}
|
|
443
475
|
|
|
444
476
|
// Get the stream from HttpClient (need to add this method)
|
|
445
|
-
const stream = await this.client.executeCommandStream(
|
|
477
|
+
const stream = await this.client.executeCommandStream(
|
|
478
|
+
command,
|
|
479
|
+
options?.sessionId
|
|
480
|
+
);
|
|
446
481
|
|
|
447
482
|
// Return the ReadableStream directly - can be converted to AsyncIterable by consumers
|
|
448
483
|
return stream;
|
|
449
484
|
}
|
|
450
485
|
|
|
451
|
-
async streamProcessLogs(
|
|
486
|
+
async streamProcessLogs(
|
|
487
|
+
processId: string,
|
|
488
|
+
options?: { signal?: AbortSignal }
|
|
489
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
452
490
|
// Check for cancellation
|
|
453
491
|
if (options?.signal?.aborted) {
|
|
454
|
-
throw new Error(
|
|
492
|
+
throw new Error("Operation was aborted");
|
|
455
493
|
}
|
|
456
494
|
|
|
457
495
|
// Get the stream from HttpClient
|
|
@@ -468,10 +506,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
468
506
|
return this.client.gitCheckout(repoUrl, options.branch, options.targetDir);
|
|
469
507
|
}
|
|
470
508
|
|
|
471
|
-
async mkdir(
|
|
472
|
-
path: string,
|
|
473
|
-
options: { recursive?: boolean } = {}
|
|
474
|
-
) {
|
|
509
|
+
async mkdir(path: string, options: { recursive?: boolean } = {}) {
|
|
475
510
|
return this.client.mkdir(path, options.recursive);
|
|
476
511
|
}
|
|
477
512
|
|
|
@@ -487,24 +522,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
487
522
|
return this.client.deleteFile(path);
|
|
488
523
|
}
|
|
489
524
|
|
|
490
|
-
async renameFile(
|
|
491
|
-
oldPath: string,
|
|
492
|
-
newPath: string
|
|
493
|
-
) {
|
|
525
|
+
async renameFile(oldPath: string, newPath: string) {
|
|
494
526
|
return this.client.renameFile(oldPath, newPath);
|
|
495
527
|
}
|
|
496
528
|
|
|
497
|
-
async moveFile(
|
|
498
|
-
sourcePath: string,
|
|
499
|
-
destinationPath: string
|
|
500
|
-
) {
|
|
529
|
+
async moveFile(sourcePath: string, destinationPath: string) {
|
|
501
530
|
return this.client.moveFile(sourcePath, destinationPath);
|
|
502
531
|
}
|
|
503
532
|
|
|
504
|
-
async readFile(
|
|
505
|
-
path: string,
|
|
506
|
-
options: { encoding?: string } = {}
|
|
507
|
-
) {
|
|
533
|
+
async readFile(path: string, options: { encoding?: string } = {}) {
|
|
508
534
|
return this.client.readFile(path, options.encoding);
|
|
509
535
|
}
|
|
510
536
|
|
|
@@ -513,10 +539,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
513
539
|
|
|
514
540
|
// We need the sandbox name to construct preview URLs
|
|
515
541
|
if (!this.sandboxName) {
|
|
516
|
-
throw new Error(
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
544
|
+
);
|
|
517
545
|
}
|
|
518
546
|
|
|
519
|
-
const url = this.constructPreviewUrl(
|
|
547
|
+
const url = this.constructPreviewUrl(
|
|
548
|
+
port,
|
|
549
|
+
this.sandboxName,
|
|
550
|
+
options.hostname
|
|
551
|
+
);
|
|
520
552
|
|
|
521
553
|
return {
|
|
522
554
|
url,
|
|
@@ -527,17 +559,27 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
527
559
|
|
|
528
560
|
async unexposePort(port: number) {
|
|
529
561
|
if (!validatePort(port)) {
|
|
530
|
-
logSecurityEvent(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
+
);
|
|
534
572
|
}
|
|
535
573
|
|
|
536
574
|
await this.client.unexposePort(port);
|
|
537
575
|
|
|
538
|
-
logSecurityEvent(
|
|
539
|
-
|
|
540
|
-
|
|
576
|
+
logSecurityEvent(
|
|
577
|
+
"PORT_UNEXPOSED",
|
|
578
|
+
{
|
|
579
|
+
port,
|
|
580
|
+
},
|
|
581
|
+
"low"
|
|
582
|
+
);
|
|
541
583
|
}
|
|
542
584
|
|
|
543
585
|
async getExposedPorts(hostname: string) {
|
|
@@ -545,10 +587,12 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
545
587
|
|
|
546
588
|
// We need the sandbox name to construct preview URLs
|
|
547
589
|
if (!this.sandboxName) {
|
|
548
|
-
throw new Error(
|
|
590
|
+
throw new Error(
|
|
591
|
+
"Sandbox name not available. Ensure sandbox is accessed through getSandbox()"
|
|
592
|
+
);
|
|
549
593
|
}
|
|
550
594
|
|
|
551
|
-
return response.ports.map(port => ({
|
|
595
|
+
return response.ports.map((port) => ({
|
|
552
596
|
url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname),
|
|
553
597
|
port: port.port,
|
|
554
598
|
name: port.name,
|
|
@@ -556,27 +600,40 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
556
600
|
}));
|
|
557
601
|
}
|
|
558
602
|
|
|
559
|
-
|
|
560
|
-
|
|
603
|
+
private constructPreviewUrl(
|
|
604
|
+
port: number,
|
|
605
|
+
sandboxId: string,
|
|
606
|
+
hostname: string
|
|
607
|
+
): string {
|
|
561
608
|
if (!validatePort(port)) {
|
|
562
|
-
logSecurityEvent(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
);
|
|
568
621
|
}
|
|
569
622
|
|
|
570
623
|
let sanitizedSandboxId: string;
|
|
571
624
|
try {
|
|
572
625
|
sanitizedSandboxId = sanitizeSandboxId(sandboxId);
|
|
573
626
|
} catch (error) {
|
|
574
|
-
logSecurityEvent(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
+
);
|
|
580
637
|
throw error;
|
|
581
638
|
}
|
|
582
639
|
|
|
@@ -584,8 +641,8 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
584
641
|
|
|
585
642
|
if (isLocalhost) {
|
|
586
643
|
// Unified subdomain approach for localhost (RFC 6761)
|
|
587
|
-
const [host, portStr] = hostname.split(
|
|
588
|
-
const mainPort = portStr ||
|
|
644
|
+
const [host, portStr] = hostname.split(":");
|
|
645
|
+
const mainPort = portStr || "80";
|
|
589
646
|
|
|
590
647
|
// Use URL constructor for safe URL building
|
|
591
648
|
try {
|
|
@@ -596,23 +653,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
596
653
|
|
|
597
654
|
const finalUrl = baseUrl.toString();
|
|
598
655
|
|
|
599
|
-
logSecurityEvent(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
656
|
+
logSecurityEvent(
|
|
657
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
658
|
+
{
|
|
659
|
+
port,
|
|
660
|
+
sandboxId: sanitizedSandboxId,
|
|
661
|
+
hostname,
|
|
662
|
+
resultUrl: finalUrl,
|
|
663
|
+
environment: "localhost",
|
|
664
|
+
},
|
|
665
|
+
"low"
|
|
666
|
+
);
|
|
606
667
|
|
|
607
668
|
return finalUrl;
|
|
608
669
|
} catch (error) {
|
|
609
|
-
logSecurityEvent(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
+
);
|
|
616
685
|
}
|
|
617
686
|
}
|
|
618
687
|
|
|
@@ -628,23 +697,82 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
628
697
|
|
|
629
698
|
const finalUrl = baseUrl.toString();
|
|
630
699
|
|
|
631
|
-
logSecurityEvent(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
700
|
+
logSecurityEvent(
|
|
701
|
+
"PREVIEW_URL_CONSTRUCTED",
|
|
702
|
+
{
|
|
703
|
+
port,
|
|
704
|
+
sandboxId: sanitizedSandboxId,
|
|
705
|
+
hostname,
|
|
706
|
+
resultUrl: finalUrl,
|
|
707
|
+
environment: "production",
|
|
708
|
+
},
|
|
709
|
+
"low"
|
|
710
|
+
);
|
|
638
711
|
|
|
639
712
|
return finalUrl;
|
|
640
713
|
} catch (error) {
|
|
641
|
-
logSecurityEvent(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
);
|
|
648
729
|
}
|
|
649
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
|
+
}
|
|
650
778
|
}
|