@cloudflare/sandbox 0.1.4 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/Dockerfile +37 -11
  3. package/README.md +229 -5
  4. package/container_src/bun.lock +122 -0
  5. package/container_src/handler/exec.ts +4 -2
  6. package/container_src/handler/process.ts +1 -1
  7. package/container_src/index.ts +171 -1
  8. package/container_src/jupyter-server.ts +336 -0
  9. package/container_src/mime-processor.ts +255 -0
  10. package/container_src/package.json +9 -0
  11. package/container_src/startup.sh +52 -0
  12. package/dist/{chunk-YVZ3K26G.js → chunk-CUHYLCMT.js} +9 -21
  13. package/dist/chunk-CUHYLCMT.js.map +1 -0
  14. package/dist/chunk-EGC5IYXA.js +108 -0
  15. package/dist/chunk-EGC5IYXA.js.map +1 -0
  16. package/dist/chunk-FKBV7CZS.js +113 -0
  17. package/dist/chunk-FKBV7CZS.js.map +1 -0
  18. package/dist/{chunk-ZJN2PQOS.js → chunk-IATLC32Y.js} +173 -74
  19. package/dist/chunk-IATLC32Y.js.map +1 -0
  20. package/dist/{chunk-6THNBO4S.js → chunk-S5FFBU4Y.js} +1 -1
  21. package/dist/{chunk-6THNBO4S.js.map → chunk-S5FFBU4Y.js.map} +1 -1
  22. package/dist/chunk-SYMWNYWA.js +185 -0
  23. package/dist/chunk-SYMWNYWA.js.map +1 -0
  24. package/dist/{client-BXYlxy-j.d.ts → client-C7rKCYBD.d.ts} +42 -4
  25. package/dist/client.d.ts +2 -1
  26. package/dist/client.js +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +10 -4
  29. package/dist/interpreter-types.d.ts +259 -0
  30. package/dist/interpreter-types.js +9 -0
  31. package/dist/interpreter-types.js.map +1 -0
  32. package/dist/interpreter.d.ts +33 -0
  33. package/dist/interpreter.js +8 -0
  34. package/dist/interpreter.js.map +1 -0
  35. package/dist/jupyter-client.d.ts +4 -0
  36. package/dist/jupyter-client.js +8 -0
  37. package/dist/jupyter-client.js.map +1 -0
  38. package/dist/request-handler.d.ts +2 -1
  39. package/dist/request-handler.js +7 -3
  40. package/dist/sandbox.d.ts +2 -1
  41. package/dist/sandbox.js +7 -3
  42. package/dist/types.d.ts +8 -0
  43. package/dist/types.js +1 -1
  44. package/package.json +1 -1
  45. package/src/client.ts +37 -54
  46. package/src/index.ts +13 -4
  47. package/src/interpreter-types.ts +383 -0
  48. package/src/interpreter.ts +150 -0
  49. package/src/jupyter-client.ts +266 -0
  50. package/src/sandbox.ts +281 -153
  51. package/src/types.ts +15 -0
  52. package/dist/chunk-YVZ3K26G.js.map +0 -1
  53. 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 { HttpClient } from "./client";
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: HttpClient;
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 HttpClient({
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 = await this.ctx.storage.get<string>('sandboxName') || null;
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('sandboxName', name);
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('X-Sandbox-Name')) {
104
- const name = request.headers.get('X-Sandbox-Name')!;
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('sandboxName', name);
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('Operation was aborted');
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(command, options, startTime, timestamp);
149
- } else {
150
- // Regular execution
151
- const response = await this.client.execute(
156
+ result = await this.executeWithStreaming(
152
157
  command,
153
- {
154
- sessionId: options?.sessionId,
155
- cwd: options?.cwd,
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(response, duration, options?.sessionId);
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(command, options.sessionId);
193
- const { parseSSEStream } = await import('./sse-parser');
206
+ const stream = await this.client.executeCommandStream(
207
+ command,
208
+ options.sessionId
209
+ );
194
210
 
195
- for await (const event of parseSSEStream<import('./types').ExecEvent>(stream)) {
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('Operation was aborted');
216
+ throw new Error("Operation was aborted");
199
217
  }
200
218
 
201
219
  switch (event.type) {
202
- case 'stdout':
203
- case 'stderr':
220
+ case "stdout":
221
+ case "stderr":
204
222
  if (event.data) {
205
223
  // Update accumulated output
206
- if (event.type === 'stdout') stdout += event.data;
207
- if (event.type === 'stderr') stderr += event.data;
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 'complete': {
234
+ case "complete": {
217
235
  // Use result from complete event if available
218
236
  const duration = Date.now() - startTime;
219
- return event.result || {
220
- success: event.exitCode === 0,
221
- exitCode: event.exitCode || 0,
222
- stdout,
223
- stderr,
224
- command,
225
- duration,
226
- timestamp,
227
- sessionId: options.sessionId
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 'error':
232
- throw new Error(event.error || 'Command execution failed');
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('Stream ended without completion event');
238
-
257
+ throw new Error("Stream ended without completion event");
239
258
  } catch (error) {
240
259
  if (options.signal?.aborted) {
241
- throw new Error('Operation was aborted');
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('./client').ExecuteResponse,
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(command: string, options?: ProcessOptions): Promise<Process> {
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('Method will be replaced');
312
+ throw new Error("Method will be replaced");
292
313
  },
293
314
  async getStatus(): Promise<ProcessStatus> {
294
- throw new Error('Method will be replaced');
315
+ throw new Error("Method will be replaced");
295
316
  },
296
317
  async getLogs(): Promise<{ stdout: string; stderr: string }> {
297
- throw new Error('Method will be replaced');
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 || 'error';
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 || 'error';
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 || 'error';
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 (error instanceof Error && error.message.includes('Process not found')) {
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}: ${error instanceof Error ? error.message : 'Unknown error'}`,
404
- 'KILL_PROCESS_FAILED'
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(id: string): Promise<{ stdout: string; stderr: string }> {
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 (error instanceof Error && error.message.includes('Process not found')) {
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(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
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('Operation was aborted');
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(command, options?.sessionId);
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(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
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('Operation was aborted');
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('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
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(port, this.sandboxName, options.hostname);
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('INVALID_PORT_UNEXPOSE', {
531
- port
532
- }, 'high');
533
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
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('PORT_UNEXPOSED', {
539
- port
540
- }, 'low');
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('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
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
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string): string {
603
+ private constructPreviewUrl(
604
+ port: number,
605
+ sandboxId: string,
606
+ hostname: string
607
+ ): string {
561
608
  if (!validatePort(port)) {
562
- logSecurityEvent('INVALID_PORT_REJECTED', {
563
- port,
564
- sandboxId,
565
- hostname
566
- }, 'high');
567
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
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('INVALID_SANDBOX_ID_REJECTED', {
575
- sandboxId,
576
- port,
577
- hostname,
578
- error: error instanceof Error ? error.message : 'Unknown error'
579
- }, 'high');
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 || '80';
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('PREVIEW_URL_CONSTRUCTED', {
600
- port,
601
- sandboxId: sanitizedSandboxId,
602
- hostname,
603
- resultUrl: finalUrl,
604
- environment: 'localhost'
605
- }, 'low');
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('URL_CONSTRUCTION_FAILED', {
610
- port,
611
- sandboxId: sanitizedSandboxId,
612
- hostname,
613
- error: error instanceof Error ? error.message : 'Unknown error'
614
- }, 'high');
615
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown 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
+ );
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('PREVIEW_URL_CONSTRUCTED', {
632
- port,
633
- sandboxId: sanitizedSandboxId,
634
- hostname,
635
- resultUrl: finalUrl,
636
- environment: 'production'
637
- }, 'low');
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('URL_CONSTRUCTION_FAILED', {
642
- port,
643
- sandboxId: sanitizedSandboxId,
644
- hostname,
645
- error: error instanceof Error ? error.message : 'Unknown error'
646
- }, 'high');
647
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown 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
+ );
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
  }