@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/src/sandbox.ts CHANGED
@@ -1,6 +1,30 @@
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";
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: HttpClient;
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 HttpClient({
23
- onCommandComplete: (success, exitCode, _stdout, _stderr, command, _args) => {
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, args) => {
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, _args) => {
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 = await this.ctx.storage.get<string>('sandboxName') || null;
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('sandboxName', name);
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 capture the hostname and route to appropriate ports
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('X-Sandbox-Name')) {
91
- 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")!;
92
113
  this.sandboxName = name;
93
- await this.ctx.storage.put('sandboxName', name);
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
- async exec(command: string, args: string[], options?: { stream?: boolean; background?: boolean }) {
117
- if (options?.stream) {
118
- return this.client.executeStream(command, args, undefined, options?.background);
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
- return this.client.execute(command, args, undefined, options?.background);
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; stream?: boolean }
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; stream?: boolean } = {}
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, options: { stream?: boolean } = {}) {
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?: { name?: string }) {
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('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
+ );
203
545
  }
204
546
 
205
- const hostname = this.getHostname();
206
- const url = this.constructPreviewUrl(port, this.sandboxName, hostname);
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('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
+ );
225
593
  }
226
594
 
227
- const hostname = this.getHostname();
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 getHostname(): string {
238
- // Use the captured hostname or fall back to localhost for development
239
- return this.workerHostname || "localhost:8787";
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
- // For local development, we need to use a different approach
248
- // Since subdomains don't work with localhost, we'll use the base URL
249
- // with a note that the user needs to handle routing differently
250
- return `http://${hostname}/preview/${port}/${sandboxId}`;
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
- // For all other domains (workers.dev, custom domains, etc.)
254
- // Use subdomain-based routing pattern
255
- const protocol = hostname.includes(":") ? "http" : "https";
256
- return `${protocol}://${port}-${sandboxId}.${hostname}`;
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
  }