@cloudflare/sandbox 0.4.12 → 0.4.14

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 (79) hide show
  1. package/.turbo/turbo-build.log +13 -47
  2. package/CHANGELOG.md +38 -16
  3. package/Dockerfile +15 -9
  4. package/README.md +0 -1
  5. package/dist/index.d.ts +1889 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3144 -65
  8. package/dist/index.js.map +1 -1
  9. package/package.json +5 -5
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +31 -26
  13. package/src/clients/git-client.ts +3 -4
  14. package/src/clients/index.ts +12 -16
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +10 -6
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +22 -15
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +24 -21
  28. package/src/sandbox.ts +339 -149
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +1 -1
  32. package/tests/base-client.test.ts +116 -80
  33. package/tests/command-client.test.ts +149 -112
  34. package/tests/file-client.test.ts +309 -197
  35. package/tests/file-stream.test.ts +24 -20
  36. package/tests/get-sandbox.test.ts +10 -10
  37. package/tests/git-client.test.ts +188 -101
  38. package/tests/port-client.test.ts +100 -108
  39. package/tests/process-client.test.ts +204 -179
  40. package/tests/request-handler.test.ts +117 -65
  41. package/tests/sandbox.test.ts +219 -67
  42. package/tests/sse-parser.test.ts +17 -16
  43. package/tests/utility-client.test.ts +79 -72
  44. package/tsdown.config.ts +12 -0
  45. package/vitest.config.ts +6 -6
  46. package/dist/chunk-BFVUNTP4.js +0 -104
  47. package/dist/chunk-BFVUNTP4.js.map +0 -1
  48. package/dist/chunk-EKSWCBCA.js +0 -86
  49. package/dist/chunk-EKSWCBCA.js.map +0 -1
  50. package/dist/chunk-JXZMAU2C.js +0 -559
  51. package/dist/chunk-JXZMAU2C.js.map +0 -1
  52. package/dist/chunk-UJ3TV4M6.js +0 -7
  53. package/dist/chunk-UJ3TV4M6.js.map +0 -1
  54. package/dist/chunk-YE265ASX.js +0 -2484
  55. package/dist/chunk-YE265ASX.js.map +0 -1
  56. package/dist/chunk-Z532A7QC.js +0 -78
  57. package/dist/chunk-Z532A7QC.js.map +0 -1
  58. package/dist/file-stream.d.ts +0 -43
  59. package/dist/file-stream.js +0 -9
  60. package/dist/file-stream.js.map +0 -1
  61. package/dist/interpreter.d.ts +0 -33
  62. package/dist/interpreter.js +0 -8
  63. package/dist/interpreter.js.map +0 -1
  64. package/dist/request-handler.d.ts +0 -18
  65. package/dist/request-handler.js +0 -13
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-CLZWpfGc.d.ts +0 -613
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -13
  70. package/dist/sandbox.js.map +0 -1
  71. package/dist/security.d.ts +0 -31
  72. package/dist/security.js +0 -13
  73. package/dist/security.js.map +0 -1
  74. package/dist/sse-parser.d.ts +0 -28
  75. package/dist/sse-parser.js +0 -11
  76. package/dist/sse-parser.js.map +0 -1
  77. package/dist/version.d.ts +0 -8
  78. package/dist/version.js +0 -7
  79. package/dist/version.js.map +0 -1
package/src/sandbox.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DurableObject } from 'cloudflare:workers';
2
- import { Container, getContainer } from "@cloudflare/containers";
2
+ import { Container, getContainer, switchPort } from '@cloudflare/containers';
3
3
  import type {
4
4
  CodeContext,
5
5
  CreateContextOptions,
@@ -16,27 +16,23 @@ import type {
16
16
  SandboxOptions,
17
17
  SessionOptions,
18
18
  StreamOptions
19
- } from "@repo/shared";
20
- import { createLogger, runWithLogger, TraceContext } from "@repo/shared";
21
- import { type ExecuteResponse, SandboxClient } from "./clients";
19
+ } from '@repo/shared';
20
+ import { createLogger, runWithLogger, TraceContext } from '@repo/shared';
21
+ import { type ExecuteResponse, SandboxClient } from './clients';
22
22
  import type { ErrorResponse } from './errors';
23
23
  import { CustomDomainRequiredError, ErrorCode } from './errors';
24
- import { CodeInterpreter } from "./interpreter";
25
- import { isLocalhostPattern } from "./request-handler";
26
- import {
27
- SecurityError,
28
- sanitizeSandboxId,
29
- validatePort
30
- } from "./security";
31
- import { parseSSEStream } from "./sse-parser";
32
- import { SDK_VERSION } from "./version";
24
+ import { CodeInterpreter } from './interpreter';
25
+ import { isLocalhostPattern } from './request-handler';
26
+ import { SecurityError, sanitizeSandboxId, validatePort } from './security';
27
+ import { parseSSEStream } from './sse-parser';
28
+ import { SDK_VERSION } from './version';
33
29
 
34
30
  export function getSandbox(
35
31
  ns: DurableObjectNamespace<Sandbox>,
36
32
  id: string,
37
33
  options?: SandboxOptions
38
- ) {
39
- const stub = getContainer(ns, id);
34
+ ): Sandbox {
35
+ const stub = getContainer(ns, id) as unknown as Sandbox;
40
36
 
41
37
  // Store the name on first access
42
38
  stub.setSandboxName?.(id);
@@ -53,12 +49,29 @@ export function getSandbox(
53
49
  stub.setKeepAlive(options.keepAlive);
54
50
  }
55
51
 
56
- return stub;
52
+ return Object.assign(stub, {
53
+ wsConnect: connect(stub)
54
+ });
55
+ }
56
+
57
+ export function connect(
58
+ stub: { fetch: (request: Request) => Promise<Response> }
59
+ ) {
60
+ return async (request: Request, port: number) => {
61
+ // Validate port before routing
62
+ if (!validatePort(port)) {
63
+ throw new SecurityError(
64
+ `Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`
65
+ );
66
+ }
67
+ const portSwitchedRequest = switchPort(request, port);
68
+ return await stub.fetch(portSwitchedRequest);
69
+ };
57
70
  }
58
71
 
59
72
  export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
60
73
  defaultPort = 3000; // Default port for the container's Bun server
61
- sleepAfter: string | number = "10m"; // Sleep the sandbox if no requests are made in this timeframe
74
+ sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
62
75
 
63
76
  client: SandboxClient;
64
77
  private codeInterpreter: CodeInterpreter;
@@ -70,13 +83,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
70
83
  private logger: ReturnType<typeof createLogger>;
71
84
  private keepAliveEnabled: boolean = false;
72
85
 
73
- constructor(ctx: DurableObject['ctx'], env: Env) {
86
+ constructor(ctx: DurableObjectState<{}>, env: Env) {
74
87
  super(ctx, env);
75
88
 
76
89
  const envObj = env as any;
77
90
  // Set sandbox environment variables from env object
78
91
  const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
79
- sandboxEnvKeys.forEach(key => {
92
+ sandboxEnvKeys.forEach((key) => {
80
93
  if (envObj?.[key]) {
81
94
  this.envVars[key] = envObj[key];
82
95
  }
@@ -90,7 +103,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
90
103
  this.client = new SandboxClient({
91
104
  logger: this.logger,
92
105
  port: 3000, // Control plane port
93
- stub: this,
106
+ stub: this
94
107
  });
95
108
 
96
109
  // Initialize code interpreter - pass 'this' after client is ready
@@ -99,9 +112,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
99
112
 
100
113
  // Load the sandbox name, port tokens, and default session from storage on initialization
101
114
  this.ctx.blockConcurrencyWhile(async () => {
102
- this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
103
- this.defaultSession = await this.ctx.storage.get<string>('defaultSession') || null;
104
- const storedTokens = await this.ctx.storage.get<Record<string, string>>('portTokens') || {};
115
+ this.sandboxName =
116
+ (await this.ctx.storage.get<string>('sandboxName')) || null;
117
+ this.defaultSession =
118
+ (await this.ctx.storage.get<string>('defaultSession')) || null;
119
+ const storedTokens =
120
+ (await this.ctx.storage.get<Record<string, string>>('portTokens')) ||
121
+ {};
105
122
 
106
123
  // Convert stored tokens back to Map
107
124
  this.portTokens = new Map();
@@ -125,8 +142,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
125
142
  this.baseUrl = baseUrl;
126
143
  await this.ctx.storage.put('baseUrl', baseUrl);
127
144
  } else {
128
- if(this.baseUrl !== baseUrl) {
129
- throw new Error('Base URL already set and different from one previously provided');
145
+ if (this.baseUrl !== baseUrl) {
146
+ throw new Error(
147
+ 'Base URL already set and different from one previously provided'
148
+ );
130
149
  }
131
150
  }
132
151
  }
@@ -140,9 +159,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
140
159
  async setKeepAlive(keepAlive: boolean): Promise<void> {
141
160
  this.keepAliveEnabled = keepAlive;
142
161
  if (keepAlive) {
143
- this.logger.info('KeepAlive mode enabled - container will stay alive until explicitly destroyed');
162
+ this.logger.info(
163
+ 'KeepAlive mode enabled - container will stay alive until explicitly destroyed'
164
+ );
144
165
  } else {
145
- this.logger.info('KeepAlive mode disabled - container will timeout normally');
166
+ this.logger.info(
167
+ 'KeepAlive mode disabled - container will timeout normally'
168
+ );
146
169
  }
147
170
  }
148
171
 
@@ -158,10 +181,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
158
181
  const escapedValue = value.replace(/'/g, "'\\''");
159
182
  const exportCommand = `export ${key}='${escapedValue}'`;
160
183
 
161
- const result = await this.client.commands.execute(exportCommand, this.defaultSession);
184
+ const result = await this.client.commands.execute(
185
+ exportCommand,
186
+ this.defaultSession
187
+ );
162
188
 
163
189
  if (result.exitCode !== 0) {
164
- throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`);
190
+ throw new Error(
191
+ `Failed to set ${key}: ${result.stderr || 'Unknown error'}`
192
+ );
165
193
  }
166
194
  }
167
195
  }
@@ -179,8 +207,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
179
207
  this.logger.debug('Sandbox started');
180
208
 
181
209
  // Check version compatibility asynchronously (don't block startup)
182
- this.checkVersionCompatibility().catch(error => {
183
- this.logger.error('Version compatibility check failed', error instanceof Error ? error : new Error(String(error)));
210
+ this.checkVersionCompatibility().catch((error) => {
211
+ this.logger.error(
212
+ 'Version compatibility check failed',
213
+ error instanceof Error ? error : new Error(String(error))
214
+ );
184
215
  });
185
216
  }
186
217
 
@@ -200,8 +231,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
200
231
  if (containerVersion === 'unknown') {
201
232
  this.logger.warn(
202
233
  'Container version check: Container version could not be determined. ' +
203
- 'This may indicate an outdated container image. ' +
204
- 'Please update your container to match SDK version ' + sdkVersion
234
+ 'This may indicate an outdated container image. ' +
235
+ 'Please update your container to match SDK version ' +
236
+ sdkVersion
205
237
  );
206
238
  return;
207
239
  }
@@ -217,7 +249,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
217
249
  // so we always use warning level as requested by the user
218
250
  this.logger.warn(message);
219
251
  } else {
220
- this.logger.debug('Version check passed', { sdkVersion, containerVersion });
252
+ this.logger.debug('Version check passed', {
253
+ sdkVersion,
254
+ containerVersion
255
+ });
221
256
  }
222
257
  } catch (error) {
223
258
  // Don't fail the sandbox initialization if version check fails
@@ -232,7 +267,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
232
267
  }
233
268
 
234
269
  override onError(error: unknown) {
235
- this.logger.error('Sandbox error', error instanceof Error ? error : new Error(String(error)));
270
+ this.logger.error(
271
+ 'Sandbox error',
272
+ error instanceof Error ? error : new Error(String(error))
273
+ );
236
274
  }
237
275
 
238
276
  /**
@@ -241,7 +279,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
241
279
  */
242
280
  override async onActivityExpired(): Promise<void> {
243
281
  if (this.keepAliveEnabled) {
244
- this.logger.debug('Activity expired but keepAlive is enabled - container will stay alive');
282
+ this.logger.debug(
283
+ 'Activity expired but keepAlive is enabled - container will stay alive'
284
+ );
245
285
  // Do nothing - don't call stop(), container stays alive
246
286
  } else {
247
287
  // Default behavior: stop the container
@@ -250,11 +290,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
250
290
  }
251
291
  }
252
292
 
253
-
254
293
  // Override fetch to route internal container requests to appropriate ports
255
294
  override async fetch(request: Request): Promise<Response> {
256
295
  // Extract or generate trace ID from request
257
- const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
296
+ const traceId =
297
+ TraceContext.fromHeaders(request.headers) || TraceContext.generate();
258
298
 
259
299
  // Create request-specific logger with trace ID
260
300
  const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
@@ -269,14 +309,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
269
309
  await this.ctx.storage.put('sandboxName', name);
270
310
  }
271
311
 
272
- // Detect WebSocket upgrade request
312
+ // Detect WebSocket upgrade request (RFC 6455 compliant)
273
313
  const upgradeHeader = request.headers.get('Upgrade');
274
- const isWebSocket = upgradeHeader?.toLowerCase() === 'websocket';
314
+ const connectionHeader = request.headers.get('Connection');
315
+ const isWebSocket =
316
+ upgradeHeader?.toLowerCase() === 'websocket' &&
317
+ connectionHeader?.toLowerCase().includes('upgrade');
275
318
 
276
319
  if (isWebSocket) {
277
320
  // WebSocket path: Let parent Container class handle WebSocket proxying
278
321
  // This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
279
- return await super.fetch(request);
322
+ try {
323
+ requestLogger.debug('WebSocket upgrade requested', {
324
+ path: url.pathname,
325
+ port: this.determinePort(url)
326
+ });
327
+ return await super.fetch(request);
328
+ } catch (error) {
329
+ requestLogger.error(
330
+ 'WebSocket connection failed',
331
+ error instanceof Error ? error : new Error(String(error)),
332
+ { path: url.pathname }
333
+ );
334
+ throw error;
335
+ }
280
336
  }
281
337
 
282
338
  // Non-WebSocket: Use existing port determination and HTTP routing logic
@@ -287,6 +343,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
287
343
  });
288
344
  }
289
345
 
346
+ wsConnect(request: Request, port: number): Promise<Response> {
347
+ // Dummy implementation that will be overridden by the stub
348
+ throw new Error('Not implemented here to avoid RPC serialization issues');
349
+ }
350
+
290
351
  private determinePort(url: URL): number {
291
352
  // Extract port from proxy requests (e.g., /proxy/8080/*)
292
353
  const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
@@ -316,7 +377,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
316
377
  await this.client.utils.createSession({
317
378
  id: sessionId,
318
379
  env: this.envVars || {},
319
- cwd: '/workspace',
380
+ cwd: '/workspace'
320
381
  });
321
382
 
322
383
  this.defaultSession = sessionId;
@@ -326,7 +387,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
326
387
  } catch (error: any) {
327
388
  // If session already exists (e.g., after hot reload), reuse it
328
389
  if (error?.message?.includes('already exists')) {
329
- this.logger.debug('Reusing existing session after reload', { sessionId });
390
+ this.logger.debug('Reusing existing session after reload', {
391
+ sessionId
392
+ });
330
393
  this.defaultSession = sessionId;
331
394
  // Persist to storage in case it wasn't saved before
332
395
  await this.ctx.storage.put('defaultSession', sessionId);
@@ -370,13 +433,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
370
433
 
371
434
  if (options?.stream && options?.onOutput) {
372
435
  // Streaming with callbacks - we need to collect the final result
373
- result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
436
+ result = await this.executeWithStreaming(
437
+ command,
438
+ sessionId,
439
+ options,
440
+ startTime,
441
+ timestamp
442
+ );
374
443
  } else {
375
444
  // Regular execution with session
376
445
  const response = await this.client.commands.execute(command, sessionId);
377
446
 
378
447
  const duration = Date.now() - startTime;
379
- result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
448
+ result = this.mapExecuteResponseToExecResult(
449
+ response,
450
+ duration,
451
+ sessionId
452
+ );
380
453
  }
381
454
 
382
455
  // Call completion callback if provided
@@ -408,7 +481,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
408
481
  let stderr = '';
409
482
 
410
483
  try {
411
- const stream = await this.client.commands.executeStream(command, sessionId);
484
+ const stream = await this.client.commands.executeStream(
485
+ command,
486
+ sessionId
487
+ );
412
488
 
413
489
  for await (const event of parseSSEStream<ExecEvent>(stream)) {
414
490
  // Check for cancellation
@@ -453,7 +529,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
453
529
 
454
530
  // If we get here without a complete event, something went wrong
455
531
  throw new Error('Stream ended without completion event');
456
-
457
532
  } catch (error) {
458
533
  if (options.signal?.aborted) {
459
534
  throw new Error('Operation was aborted');
@@ -501,8 +576,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
501
576
  pid: data.pid,
502
577
  command: data.command,
503
578
  status: data.status,
504
- startTime: typeof data.startTime === 'string' ? new Date(data.startTime) : data.startTime,
505
- endTime: data.endTime ? (typeof data.endTime === 'string' ? new Date(data.endTime) : data.endTime) : undefined,
579
+ startTime:
580
+ typeof data.startTime === 'string'
581
+ ? new Date(data.startTime)
582
+ : data.startTime,
583
+ endTime: data.endTime
584
+ ? typeof data.endTime === 'string'
585
+ ? new Date(data.endTime)
586
+ : data.endTime
587
+ : undefined,
506
588
  exitCode: data.exitCode,
507
589
  sessionId,
508
590
 
@@ -522,25 +604,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
522
604
  };
523
605
  }
524
606
 
525
-
526
607
  // Background process management
527
- async startProcess(command: string, options?: ProcessOptions, sessionId?: string): Promise<Process> {
608
+ async startProcess(
609
+ command: string,
610
+ options?: ProcessOptions,
611
+ sessionId?: string
612
+ ): Promise<Process> {
528
613
  // Use the new HttpClient method to start the process
529
614
  try {
530
- const session = sessionId ?? await this.ensureDefaultSession();
531
- const response = await this.client.processes.startProcess(command, session, {
532
- processId: options?.processId
533
- });
534
-
535
- const processObj = this.createProcessFromDTO({
536
- id: response.processId,
537
- pid: response.pid,
538
- command: response.command,
539
- status: 'running' as ProcessStatus,
540
- startTime: new Date(),
541
- endTime: undefined,
542
- exitCode: undefined
543
- }, session);
615
+ const session = sessionId ?? (await this.ensureDefaultSession());
616
+ const response = await this.client.processes.startProcess(
617
+ command,
618
+ session,
619
+ {
620
+ processId: options?.processId
621
+ }
622
+ );
623
+
624
+ const processObj = this.createProcessFromDTO(
625
+ {
626
+ id: response.processId,
627
+ pid: response.pid,
628
+ command: response.command,
629
+ status: 'running' as ProcessStatus,
630
+ startTime: new Date(),
631
+ endTime: undefined,
632
+ exitCode: undefined
633
+ },
634
+ session
635
+ );
544
636
 
545
637
  // Call onStart callback if provided
546
638
  if (options?.onStart) {
@@ -548,7 +640,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
548
640
  }
549
641
 
550
642
  return processObj;
551
-
552
643
  } catch (error) {
553
644
  if (options?.onError && error instanceof Error) {
554
645
  options.onError(error);
@@ -559,42 +650,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
559
650
  }
560
651
 
561
652
  async listProcesses(sessionId?: string): Promise<Process[]> {
562
- const session = sessionId ?? await this.ensureDefaultSession();
653
+ const session = sessionId ?? (await this.ensureDefaultSession());
563
654
  const response = await this.client.processes.listProcesses();
564
655
 
565
- return response.processes.map(processData =>
566
- this.createProcessFromDTO({
567
- id: processData.id,
568
- pid: processData.pid,
569
- command: processData.command,
570
- status: processData.status,
571
- startTime: processData.startTime,
572
- endTime: processData.endTime,
573
- exitCode: processData.exitCode
574
- }, session)
656
+ return response.processes.map((processData) =>
657
+ this.createProcessFromDTO(
658
+ {
659
+ id: processData.id,
660
+ pid: processData.pid,
661
+ command: processData.command,
662
+ status: processData.status,
663
+ startTime: processData.startTime,
664
+ endTime: processData.endTime,
665
+ exitCode: processData.exitCode
666
+ },
667
+ session
668
+ )
575
669
  );
576
670
  }
577
671
 
578
672
  async getProcess(id: string, sessionId?: string): Promise<Process | null> {
579
- const session = sessionId ?? await this.ensureDefaultSession();
673
+ const session = sessionId ?? (await this.ensureDefaultSession());
580
674
  const response = await this.client.processes.getProcess(id);
581
675
  if (!response.process) {
582
676
  return null;
583
677
  }
584
678
 
585
679
  const processData = response.process;
586
- return this.createProcessFromDTO({
587
- id: processData.id,
588
- pid: processData.pid,
589
- command: processData.command,
590
- status: processData.status,
591
- startTime: processData.startTime,
592
- endTime: processData.endTime,
593
- exitCode: processData.exitCode
594
- }, session);
680
+ return this.createProcessFromDTO(
681
+ {
682
+ id: processData.id,
683
+ pid: processData.pid,
684
+ command: processData.command,
685
+ status: processData.status,
686
+ startTime: processData.startTime,
687
+ endTime: processData.endTime,
688
+ exitCode: processData.exitCode
689
+ },
690
+ session
691
+ );
595
692
  }
596
693
 
597
- async killProcess(id: string, signal?: string, sessionId?: string): Promise<void> {
694
+ async killProcess(
695
+ id: string,
696
+ signal?: string,
697
+ sessionId?: string
698
+ ): Promise<void> {
598
699
  // Note: signal parameter is not currently supported by the HttpClient implementation
599
700
  // The HTTP client already throws properly typed errors, so we just let them propagate
600
701
  await this.client.processes.killProcess(id);
@@ -612,7 +713,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
612
713
  return 0;
613
714
  }
614
715
 
615
- async getProcessLogs(id: string, sessionId?: string): Promise<{ stdout: string; stderr: string; processId: string }> {
716
+ async getProcessLogs(
717
+ id: string,
718
+ sessionId?: string
719
+ ): Promise<{ stdout: string; stderr: string; processId: string }> {
616
720
  // The HTTP client already throws properly typed errors, so we just let them propagate
617
721
  const response = await this.client.processes.getProcessLogs(id);
618
722
  return {
@@ -622,8 +726,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
622
726
  };
623
727
  }
624
728
 
625
- // Streaming methods - return ReadableStream for RPC compatibility
626
- async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
729
+ // Streaming methods - return ReadableStream for RPC compatibility
730
+ async execStream(
731
+ command: string,
732
+ options?: StreamOptions
733
+ ): Promise<ReadableStream<Uint8Array>> {
627
734
  // Check for cancellation
628
735
  if (options?.signal?.aborted) {
629
736
  throw new Error('Operation was aborted');
@@ -637,7 +744,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
637
744
  /**
638
745
  * Internal session-aware execStream implementation
639
746
  */
640
- private async execStreamWithSession(command: string, sessionId: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
747
+ private async execStreamWithSession(
748
+ command: string,
749
+ sessionId: string,
750
+ options?: StreamOptions
751
+ ): Promise<ReadableStream<Uint8Array>> {
641
752
  // Check for cancellation
642
753
  if (options?.signal?.aborted) {
643
754
  throw new Error('Operation was aborted');
@@ -649,7 +760,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
649
760
  /**
650
761
  * Stream logs from a background process as a ReadableStream.
651
762
  */
652
- async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
763
+ async streamProcessLogs(
764
+ processId: string,
765
+ options?: { signal?: AbortSignal }
766
+ ): Promise<ReadableStream<Uint8Array>> {
653
767
  // Check for cancellation
654
768
  if (options?.signal?.aborted) {
655
769
  throw new Error('Operation was aborted');
@@ -662,7 +776,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
662
776
  repoUrl: string,
663
777
  options: { branch?: string; targetDir?: string; sessionId?: string }
664
778
  ) {
665
- const session = options.sessionId ?? await this.ensureDefaultSession();
779
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
666
780
  return this.client.git.checkout(repoUrl, session, {
667
781
  branch: options.branch,
668
782
  targetDir: options.targetDir
@@ -673,8 +787,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
673
787
  path: string,
674
788
  options: { recursive?: boolean; sessionId?: string } = {}
675
789
  ) {
676
- const session = options.sessionId ?? await this.ensureDefaultSession();
677
- return this.client.files.mkdir(path, session, { recursive: options.recursive });
790
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
791
+ return this.client.files.mkdir(path, session, {
792
+ recursive: options.recursive
793
+ });
678
794
  }
679
795
 
680
796
  async writeFile(
@@ -682,21 +798,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
682
798
  content: string,
683
799
  options: { encoding?: string; sessionId?: string } = {}
684
800
  ) {
685
- const session = options.sessionId ?? await this.ensureDefaultSession();
686
- return this.client.files.writeFile(path, content, session, { encoding: options.encoding });
801
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
802
+ return this.client.files.writeFile(path, content, session, {
803
+ encoding: options.encoding
804
+ });
687
805
  }
688
806
 
689
807
  async deleteFile(path: string, sessionId?: string) {
690
- const session = sessionId ?? await this.ensureDefaultSession();
808
+ const session = sessionId ?? (await this.ensureDefaultSession());
691
809
  return this.client.files.deleteFile(path, session);
692
810
  }
693
811
 
694
- async renameFile(
695
- oldPath: string,
696
- newPath: string,
697
- sessionId?: string
698
- ) {
699
- const session = sessionId ?? await this.ensureDefaultSession();
812
+ async renameFile(oldPath: string, newPath: string, sessionId?: string) {
813
+ const session = sessionId ?? (await this.ensureDefaultSession());
700
814
  return this.client.files.renameFile(oldPath, newPath, session);
701
815
  }
702
816
 
@@ -705,7 +819,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
705
819
  destinationPath: string,
706
820
  sessionId?: string
707
821
  ) {
708
- const session = sessionId ?? await this.ensureDefaultSession();
822
+ const session = sessionId ?? (await this.ensureDefaultSession());
709
823
  return this.client.files.moveFile(sourcePath, destinationPath, session);
710
824
  }
711
825
 
@@ -713,8 +827,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
713
827
  path: string,
714
828
  options: { encoding?: string; sessionId?: string } = {}
715
829
  ) {
716
- const session = options.sessionId ?? await this.ensureDefaultSession();
717
- return this.client.files.readFile(path, session, { encoding: options.encoding });
830
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
831
+ return this.client.files.readFile(path, session, {
832
+ encoding: options.encoding
833
+ });
718
834
  }
719
835
 
720
836
  /**
@@ -727,7 +843,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
727
843
  path: string,
728
844
  options: { sessionId?: string } = {}
729
845
  ): Promise<ReadableStream<Uint8Array>> {
730
- const session = options.sessionId ?? await this.ensureDefaultSession();
846
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
731
847
  return this.client.files.readFileStream(path, session);
732
848
  }
733
849
 
@@ -740,7 +856,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
740
856
  }
741
857
 
742
858
  async exists(path: string, sessionId?: string) {
743
- const session = sessionId ?? await this.ensureDefaultSession();
859
+ const session = sessionId ?? (await this.ensureDefaultSession());
744
860
  return this.client.files.exists(path, session);
745
861
  }
746
862
 
@@ -762,7 +878,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
762
878
 
763
879
  // We need the sandbox name to construct preview URLs
764
880
  if (!this.sandboxName) {
765
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
881
+ throw new Error(
882
+ 'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
883
+ );
766
884
  }
767
885
 
768
886
  // Generate and store token for this port
@@ -770,18 +888,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
770
888
  this.portTokens.set(port, token);
771
889
  await this.persistPortTokens();
772
890
 
773
- const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
891
+ const url = this.constructPreviewUrl(
892
+ port,
893
+ this.sandboxName,
894
+ options.hostname,
895
+ token
896
+ );
774
897
 
775
898
  return {
776
899
  url,
777
900
  port,
778
- name: options?.name,
901
+ name: options?.name
779
902
  };
780
903
  }
781
904
 
782
905
  async unexposePort(port: number) {
783
906
  if (!validatePort(port)) {
784
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
907
+ throw new SecurityError(
908
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
909
+ );
785
910
  }
786
911
 
787
912
  const sessionId = await this.ensureDefaultSession();
@@ -800,32 +925,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
800
925
 
801
926
  // We need the sandbox name to construct preview URLs
802
927
  if (!this.sandboxName) {
803
- throw new Error('Sandbox name not available. Ensure sandbox is accessed through getSandbox()');
928
+ throw new Error(
929
+ 'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
930
+ );
804
931
  }
805
932
 
806
- return response.ports.map(port => {
933
+ return response.ports.map((port) => {
807
934
  // Get token for this port - must exist for all exposed ports
808
935
  const token = this.portTokens.get(port.port);
809
936
  if (!token) {
810
- throw new Error(`Port ${port.port} is exposed but has no token. This should not happen.`);
937
+ throw new Error(
938
+ `Port ${port.port} is exposed but has no token. This should not happen.`
939
+ );
811
940
  }
812
941
 
813
942
  return {
814
- url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname, token),
943
+ url: this.constructPreviewUrl(
944
+ port.port,
945
+ this.sandboxName!,
946
+ hostname,
947
+ token
948
+ ),
815
949
  port: port.port,
816
- status: port.status,
950
+ status: port.status
817
951
  };
818
952
  });
819
953
  }
820
954
 
821
-
822
955
  async isPortExposed(port: number): Promise<boolean> {
823
956
  try {
824
957
  const sessionId = await this.ensureDefaultSession();
825
958
  const response = await this.client.ports.getExposedPorts(sessionId);
826
- return response.ports.some(exposedPort => exposedPort.port === port);
959
+ return response.ports.some((exposedPort) => exposedPort.port === port);
827
960
  } catch (error) {
828
- this.logger.error('Error checking if port is exposed', error instanceof Error ? error : new Error(String(error)), { port });
961
+ this.logger.error(
962
+ 'Error checking if port is exposed',
963
+ error instanceof Error ? error : new Error(String(error)),
964
+ { port }
965
+ );
829
966
  return false;
830
967
  }
831
968
  }
@@ -841,7 +978,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
841
978
  const storedToken = this.portTokens.get(port);
842
979
  if (!storedToken) {
843
980
  // This should not happen - all exposed ports must have tokens
844
- this.logger.error('Port is exposed but has no token - bug detected', undefined, { port });
981
+ this.logger.error(
982
+ 'Port is exposed but has no token - bug detected',
983
+ undefined,
984
+ { port }
985
+ );
845
986
  return false;
846
987
  }
847
988
 
@@ -857,7 +998,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
857
998
 
858
999
  // Convert to base64url format (URL-safe, no padding, lowercase)
859
1000
  const base64 = btoa(String.fromCharCode(...array));
860
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').toLowerCase();
1001
+ return base64
1002
+ .replace(/\+/g, '-')
1003
+ .replace(/\//g, '_')
1004
+ .replace(/=/g, '')
1005
+ .toLowerCase();
861
1006
  }
862
1007
 
863
1008
  private async persistPortTokens(): Promise<void> {
@@ -869,9 +1014,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
869
1014
  await this.ctx.storage.put('portTokens', tokensObj);
870
1015
  }
871
1016
 
872
- private constructPreviewUrl(port: number, sandboxId: string, hostname: string, token: string): string {
1017
+ private constructPreviewUrl(
1018
+ port: number,
1019
+ sandboxId: string,
1020
+ hostname: string,
1021
+ token: string
1022
+ ): string {
873
1023
  if (!validatePort(port)) {
874
- throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
1024
+ throw new SecurityError(
1025
+ `Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
1026
+ );
875
1027
  }
876
1028
 
877
1029
  // Validate sandbox ID (will throw SecurityError if invalid)
@@ -893,14 +1045,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
893
1045
 
894
1046
  return baseUrl.toString();
895
1047
  } catch (error) {
896
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
1048
+ throw new SecurityError(
1049
+ `Failed to construct preview URL: ${
1050
+ error instanceof Error ? error.message : 'Unknown error'
1051
+ }`
1052
+ );
897
1053
  }
898
1054
  }
899
1055
 
900
1056
  // Production subdomain logic - enforce HTTPS
901
1057
  try {
902
1058
  // Always use HTTPS for production (non-localhost)
903
- const protocol = "https";
1059
+ const protocol = 'https';
904
1060
  const baseUrl = new URL(`${protocol}://${hostname}`);
905
1061
 
906
1062
  // Construct subdomain safely with mandatory token
@@ -909,7 +1065,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
909
1065
 
910
1066
  return baseUrl.toString();
911
1067
  } catch (error) {
912
- throw new SecurityError(`Failed to construct preview URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
1068
+ throw new SecurityError(
1069
+ `Failed to construct preview URL: ${
1070
+ error instanceof Error ? error.message : 'Unknown error'
1071
+ }`
1072
+ );
913
1073
  }
914
1074
  }
915
1075
 
@@ -928,7 +1088,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
928
1088
  await this.client.utils.createSession({
929
1089
  id: sessionId,
930
1090
  env: options?.env,
931
- cwd: options?.cwd,
1091
+ cwd: options?.cwd
932
1092
  });
933
1093
 
934
1094
  // Return wrapper that binds sessionId to all operations
@@ -959,32 +1119,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
959
1119
  id: sessionId,
960
1120
 
961
1121
  // Command execution - delegate to internal session-aware methods
962
- exec: (command, options) => this.execWithSession(command, sessionId, options),
963
- execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
1122
+ exec: (command, options) =>
1123
+ this.execWithSession(command, sessionId, options),
1124
+ execStream: (command, options) =>
1125
+ this.execStreamWithSession(command, sessionId, options),
964
1126
 
965
1127
  // Process management
966
- startProcess: (command, options) => this.startProcess(command, options, sessionId),
1128
+ startProcess: (command, options) =>
1129
+ this.startProcess(command, options, sessionId),
967
1130
  listProcesses: () => this.listProcesses(sessionId),
968
1131
  getProcess: (id) => this.getProcess(id, sessionId),
969
1132
  killProcess: (id, signal) => this.killProcess(id, signal),
970
1133
  killAllProcesses: () => this.killAllProcesses(),
971
1134
  cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
972
1135
  getProcessLogs: (id) => this.getProcessLogs(id),
973
- streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
1136
+ streamProcessLogs: (processId, options) =>
1137
+ this.streamProcessLogs(processId, options),
974
1138
 
975
1139
  // File operations - pass sessionId via options or parameter
976
- writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
977
- readFile: (path, options) => this.readFile(path, { ...options, sessionId }),
1140
+ writeFile: (path, content, options) =>
1141
+ this.writeFile(path, content, { ...options, sessionId }),
1142
+ readFile: (path, options) =>
1143
+ this.readFile(path, { ...options, sessionId }),
978
1144
  readFileStream: (path) => this.readFileStream(path, { sessionId }),
979
1145
  mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
980
1146
  deleteFile: (path) => this.deleteFile(path, sessionId),
981
- renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
982
- moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
983
- listFiles: (path, options) => this.client.files.listFiles(path, sessionId, options),
1147
+ renameFile: (oldPath, newPath) =>
1148
+ this.renameFile(oldPath, newPath, sessionId),
1149
+ moveFile: (sourcePath, destPath) =>
1150
+ this.moveFile(sourcePath, destPath, sessionId),
1151
+ listFiles: (path, options) =>
1152
+ this.client.files.listFiles(path, sessionId, options),
984
1153
  exists: (path) => this.exists(path, sessionId),
985
1154
 
986
1155
  // Git operations
987
- gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
1156
+ gitCheckout: (repoUrl, options) =>
1157
+ this.gitCheckout(repoUrl, { ...options, sessionId }),
988
1158
 
989
1159
  // Environment management - needs special handling
990
1160
  setEnvVars: async (envVars: Record<string, string>) => {
@@ -994,27 +1164,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
994
1164
  const escapedValue = value.replace(/'/g, "'\\''");
995
1165
  const exportCommand = `export ${key}='${escapedValue}'`;
996
1166
 
997
- const result = await this.client.commands.execute(exportCommand, sessionId);
1167
+ const result = await this.client.commands.execute(
1168
+ exportCommand,
1169
+ sessionId
1170
+ );
998
1171
 
999
1172
  if (result.exitCode !== 0) {
1000
- throw new Error(`Failed to set ${key}: ${result.stderr || 'Unknown error'}`);
1173
+ throw new Error(
1174
+ `Failed to set ${key}: ${result.stderr || 'Unknown error'}`
1175
+ );
1001
1176
  }
1002
1177
  }
1003
1178
  } catch (error) {
1004
- this.logger.error('Failed to set environment variables', error instanceof Error ? error : new Error(String(error)), { sessionId });
1179
+ this.logger.error(
1180
+ 'Failed to set environment variables',
1181
+ error instanceof Error ? error : new Error(String(error)),
1182
+ { sessionId }
1183
+ );
1005
1184
  throw error;
1006
1185
  }
1007
1186
  },
1008
1187
 
1009
1188
  // Code interpreter methods - delegate to sandbox's code interpreter
1010
- createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
1189
+ createCodeContext: (options) =>
1190
+ this.codeInterpreter.createCodeContext(options),
1011
1191
  runCode: async (code, options) => {
1012
1192
  const execution = await this.codeInterpreter.runCode(code, options);
1013
1193
  return execution.toJSON();
1014
1194
  },
1015
- runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
1195
+ runCodeStream: (code, options) =>
1196
+ this.codeInterpreter.runCodeStream(code, options),
1016
1197
  listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
1017
- deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId),
1198
+ deleteCodeContext: (contextId) =>
1199
+ this.codeInterpreter.deleteCodeContext(contextId)
1018
1200
  };
1019
1201
  }
1020
1202
 
@@ -1022,16 +1204,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
1022
1204
  // Code interpreter methods - delegate to CodeInterpreter wrapper
1023
1205
  // ============================================================================
1024
1206
 
1025
- async createCodeContext(options?: CreateContextOptions): Promise<CodeContext> {
1207
+ async createCodeContext(
1208
+ options?: CreateContextOptions
1209
+ ): Promise<CodeContext> {
1026
1210
  return this.codeInterpreter.createCodeContext(options);
1027
1211
  }
1028
1212
 
1029
- async runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult> {
1213
+ async runCode(
1214
+ code: string,
1215
+ options?: RunCodeOptions
1216
+ ): Promise<ExecutionResult> {
1030
1217
  const execution = await this.codeInterpreter.runCode(code, options);
1031
1218
  return execution.toJSON();
1032
1219
  }
1033
1220
 
1034
- async runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream> {
1221
+ async runCodeStream(
1222
+ code: string,
1223
+ options?: RunCodeOptions
1224
+ ): Promise<ReadableStream> {
1035
1225
  return this.codeInterpreter.runCodeStream(code, options);
1036
1226
  }
1037
1227