@cloudflare/sandbox 0.4.11 → 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 +44 -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 +370 -148
  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 +45 -6
  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 +220 -68
  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-FE4PJSRB.js +0 -7
  51. package/dist/chunk-FE4PJSRB.js.map +0 -1
  52. package/dist/chunk-JXZMAU2C.js +0 -559
  53. package/dist/chunk-JXZMAU2C.js.map +0 -1
  54. package/dist/chunk-SVWLTRHD.js +0 -2456
  55. package/dist/chunk-SVWLTRHD.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-DWQVgVTY.d.ts +0 -603
  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);
@@ -49,12 +45,33 @@ export function getSandbox(
49
45
  stub.setSleepAfter(options.sleepAfter);
50
46
  }
51
47
 
52
- return stub;
48
+ if (options?.keepAlive !== undefined) {
49
+ stub.setKeepAlive(options.keepAlive);
50
+ }
51
+
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
+ };
53
70
  }
54
71
 
55
72
  export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
56
73
  defaultPort = 3000; // Default port for the container's Bun server
57
- 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
58
75
 
59
76
  client: SandboxClient;
60
77
  private codeInterpreter: CodeInterpreter;
@@ -64,14 +81,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
64
81
  private defaultSession: string | null = null;
65
82
  envVars: Record<string, string> = {};
66
83
  private logger: ReturnType<typeof createLogger>;
84
+ private keepAliveEnabled: boolean = false;
67
85
 
68
- constructor(ctx: DurableObject['ctx'], env: Env) {
86
+ constructor(ctx: DurableObjectState<{}>, env: Env) {
69
87
  super(ctx, env);
70
88
 
71
89
  const envObj = env as any;
72
90
  // Set sandbox environment variables from env object
73
91
  const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
74
- sandboxEnvKeys.forEach(key => {
92
+ sandboxEnvKeys.forEach((key) => {
75
93
  if (envObj?.[key]) {
76
94
  this.envVars[key] = envObj[key];
77
95
  }
@@ -85,7 +103,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
85
103
  this.client = new SandboxClient({
86
104
  logger: this.logger,
87
105
  port: 3000, // Control plane port
88
- stub: this,
106
+ stub: this
89
107
  });
90
108
 
91
109
  // Initialize code interpreter - pass 'this' after client is ready
@@ -94,9 +112,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
94
112
 
95
113
  // Load the sandbox name, port tokens, and default session from storage on initialization
96
114
  this.ctx.blockConcurrencyWhile(async () => {
97
- this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
98
- this.defaultSession = await this.ctx.storage.get<string>('defaultSession') || null;
99
- 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
+ {};
100
122
 
101
123
  // Convert stored tokens back to Map
102
124
  this.portTokens = new Map();
@@ -120,8 +142,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
120
142
  this.baseUrl = baseUrl;
121
143
  await this.ctx.storage.put('baseUrl', baseUrl);
122
144
  } else {
123
- if(this.baseUrl !== baseUrl) {
124
- 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
+ );
125
149
  }
126
150
  }
127
151
  }
@@ -131,6 +155,20 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
131
155
  this.sleepAfter = sleepAfter;
132
156
  }
133
157
 
158
+ // RPC method to enable keepAlive mode
159
+ async setKeepAlive(keepAlive: boolean): Promise<void> {
160
+ this.keepAliveEnabled = keepAlive;
161
+ if (keepAlive) {
162
+ this.logger.info(
163
+ 'KeepAlive mode enabled - container will stay alive until explicitly destroyed'
164
+ );
165
+ } else {
166
+ this.logger.info(
167
+ 'KeepAlive mode disabled - container will timeout normally'
168
+ );
169
+ }
170
+ }
171
+
134
172
  // RPC method to set environment variables
135
173
  async setEnvVars(envVars: Record<string, string>): Promise<void> {
136
174
  // Update local state for new sessions
@@ -143,10 +181,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
143
181
  const escapedValue = value.replace(/'/g, "'\\''");
144
182
  const exportCommand = `export ${key}='${escapedValue}'`;
145
183
 
146
- const result = await this.client.commands.execute(exportCommand, this.defaultSession);
184
+ const result = await this.client.commands.execute(
185
+ exportCommand,
186
+ this.defaultSession
187
+ );
147
188
 
148
189
  if (result.exitCode !== 0) {
149
- 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
+ );
150
193
  }
151
194
  }
152
195
  }
@@ -164,8 +207,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
164
207
  this.logger.debug('Sandbox started');
165
208
 
166
209
  // Check version compatibility asynchronously (don't block startup)
167
- this.checkVersionCompatibility().catch(error => {
168
- 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
+ );
169
215
  });
170
216
  }
171
217
 
@@ -185,8 +231,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
185
231
  if (containerVersion === 'unknown') {
186
232
  this.logger.warn(
187
233
  'Container version check: Container version could not be determined. ' +
188
- 'This may indicate an outdated container image. ' +
189
- '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
190
237
  );
191
238
  return;
192
239
  }
@@ -202,7 +249,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
202
249
  // so we always use warning level as requested by the user
203
250
  this.logger.warn(message);
204
251
  } else {
205
- this.logger.debug('Version check passed', { sdkVersion, containerVersion });
252
+ this.logger.debug('Version check passed', {
253
+ sdkVersion,
254
+ containerVersion
255
+ });
206
256
  }
207
257
  } catch (error) {
208
258
  // Don't fail the sandbox initialization if version check fails
@@ -217,13 +267,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
217
267
  }
218
268
 
219
269
  override onError(error: unknown) {
220
- 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
+ );
274
+ }
275
+
276
+ /**
277
+ * Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
278
+ * When keepAlive is disabled, calls parent implementation which stops the container
279
+ */
280
+ override async onActivityExpired(): Promise<void> {
281
+ if (this.keepAliveEnabled) {
282
+ this.logger.debug(
283
+ 'Activity expired but keepAlive is enabled - container will stay alive'
284
+ );
285
+ // Do nothing - don't call stop(), container stays alive
286
+ } else {
287
+ // Default behavior: stop the container
288
+ this.logger.debug('Activity expired - stopping container');
289
+ await super.onActivityExpired();
290
+ }
221
291
  }
222
292
 
223
293
  // Override fetch to route internal container requests to appropriate ports
224
294
  override async fetch(request: Request): Promise<Response> {
225
295
  // Extract or generate trace ID from request
226
- const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
296
+ const traceId =
297
+ TraceContext.fromHeaders(request.headers) || TraceContext.generate();
227
298
 
228
299
  // Create request-specific logger with trace ID
229
300
  const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
@@ -238,14 +309,30 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
238
309
  await this.ctx.storage.put('sandboxName', name);
239
310
  }
240
311
 
241
- // Detect WebSocket upgrade request
312
+ // Detect WebSocket upgrade request (RFC 6455 compliant)
242
313
  const upgradeHeader = request.headers.get('Upgrade');
243
- const isWebSocket = upgradeHeader?.toLowerCase() === 'websocket';
314
+ const connectionHeader = request.headers.get('Connection');
315
+ const isWebSocket =
316
+ upgradeHeader?.toLowerCase() === 'websocket' &&
317
+ connectionHeader?.toLowerCase().includes('upgrade');
244
318
 
245
319
  if (isWebSocket) {
246
320
  // WebSocket path: Let parent Container class handle WebSocket proxying
247
321
  // This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
248
- 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
+ }
249
336
  }
250
337
 
251
338
  // Non-WebSocket: Use existing port determination and HTTP routing logic
@@ -256,6 +343,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
256
343
  });
257
344
  }
258
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
+
259
351
  private determinePort(url: URL): number {
260
352
  // Extract port from proxy requests (e.g., /proxy/8080/*)
261
353
  const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
@@ -285,7 +377,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
285
377
  await this.client.utils.createSession({
286
378
  id: sessionId,
287
379
  env: this.envVars || {},
288
- cwd: '/workspace',
380
+ cwd: '/workspace'
289
381
  });
290
382
 
291
383
  this.defaultSession = sessionId;
@@ -295,7 +387,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
295
387
  } catch (error: any) {
296
388
  // If session already exists (e.g., after hot reload), reuse it
297
389
  if (error?.message?.includes('already exists')) {
298
- this.logger.debug('Reusing existing session after reload', { sessionId });
390
+ this.logger.debug('Reusing existing session after reload', {
391
+ sessionId
392
+ });
299
393
  this.defaultSession = sessionId;
300
394
  // Persist to storage in case it wasn't saved before
301
395
  await this.ctx.storage.put('defaultSession', sessionId);
@@ -327,7 +421,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
327
421
  const startTime = Date.now();
328
422
  const timestamp = new Date().toISOString();
329
423
 
330
- // Handle timeout
331
424
  let timeoutId: NodeJS.Timeout | undefined;
332
425
 
333
426
  try {
@@ -340,13 +433,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
340
433
 
341
434
  if (options?.stream && options?.onOutput) {
342
435
  // Streaming with callbacks - we need to collect the final result
343
- 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
+ );
344
443
  } else {
345
444
  // Regular execution with session
346
445
  const response = await this.client.commands.execute(command, sessionId);
347
446
 
348
447
  const duration = Date.now() - startTime;
349
- result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
448
+ result = this.mapExecuteResponseToExecResult(
449
+ response,
450
+ duration,
451
+ sessionId
452
+ );
350
453
  }
351
454
 
352
455
  // Call completion callback if provided
@@ -378,7 +481,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
378
481
  let stderr = '';
379
482
 
380
483
  try {
381
- const stream = await this.client.commands.executeStream(command, sessionId);
484
+ const stream = await this.client.commands.executeStream(
485
+ command,
486
+ sessionId
487
+ );
382
488
 
383
489
  for await (const event of parseSSEStream<ExecEvent>(stream)) {
384
490
  // Check for cancellation
@@ -423,7 +529,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
423
529
 
424
530
  // If we get here without a complete event, something went wrong
425
531
  throw new Error('Stream ended without completion event');
426
-
427
532
  } catch (error) {
428
533
  if (options.signal?.aborted) {
429
534
  throw new Error('Operation was aborted');
@@ -471,8 +576,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
471
576
  pid: data.pid,
472
577
  command: data.command,
473
578
  status: data.status,
474
- startTime: typeof data.startTime === 'string' ? new Date(data.startTime) : data.startTime,
475
- 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,
476
588
  exitCode: data.exitCode,
477
589
  sessionId,
478
590
 
@@ -492,25 +604,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
492
604
  };
493
605
  }
494
606
 
495
-
496
607
  // Background process management
497
- 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> {
498
613
  // Use the new HttpClient method to start the process
499
614
  try {
500
- const session = sessionId ?? await this.ensureDefaultSession();
501
- const response = await this.client.processes.startProcess(command, session, {
502
- processId: options?.processId
503
- });
504
-
505
- const processObj = this.createProcessFromDTO({
506
- id: response.processId,
507
- pid: response.pid,
508
- command: response.command,
509
- status: 'running' as ProcessStatus,
510
- startTime: new Date(),
511
- endTime: undefined,
512
- exitCode: undefined
513
- }, 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
+ );
514
636
 
515
637
  // Call onStart callback if provided
516
638
  if (options?.onStart) {
@@ -518,7 +640,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
518
640
  }
519
641
 
520
642
  return processObj;
521
-
522
643
  } catch (error) {
523
644
  if (options?.onError && error instanceof Error) {
524
645
  options.onError(error);
@@ -529,42 +650,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
529
650
  }
530
651
 
531
652
  async listProcesses(sessionId?: string): Promise<Process[]> {
532
- const session = sessionId ?? await this.ensureDefaultSession();
653
+ const session = sessionId ?? (await this.ensureDefaultSession());
533
654
  const response = await this.client.processes.listProcesses();
534
655
 
535
- return response.processes.map(processData =>
536
- this.createProcessFromDTO({
537
- id: processData.id,
538
- pid: processData.pid,
539
- command: processData.command,
540
- status: processData.status,
541
- startTime: processData.startTime,
542
- endTime: processData.endTime,
543
- exitCode: processData.exitCode
544
- }, 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
+ )
545
669
  );
546
670
  }
547
671
 
548
672
  async getProcess(id: string, sessionId?: string): Promise<Process | null> {
549
- const session = sessionId ?? await this.ensureDefaultSession();
673
+ const session = sessionId ?? (await this.ensureDefaultSession());
550
674
  const response = await this.client.processes.getProcess(id);
551
675
  if (!response.process) {
552
676
  return null;
553
677
  }
554
678
 
555
679
  const processData = response.process;
556
- return this.createProcessFromDTO({
557
- id: processData.id,
558
- pid: processData.pid,
559
- command: processData.command,
560
- status: processData.status,
561
- startTime: processData.startTime,
562
- endTime: processData.endTime,
563
- exitCode: processData.exitCode
564
- }, session);
565
- }
566
-
567
- async killProcess(id: string, signal?: string, sessionId?: string): Promise<void> {
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
+ );
692
+ }
693
+
694
+ async killProcess(
695
+ id: string,
696
+ signal?: string,
697
+ sessionId?: string
698
+ ): Promise<void> {
568
699
  // Note: signal parameter is not currently supported by the HttpClient implementation
569
700
  // The HTTP client already throws properly typed errors, so we just let them propagate
570
701
  await this.client.processes.killProcess(id);
@@ -582,7 +713,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
582
713
  return 0;
583
714
  }
584
715
 
585
- 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 }> {
586
720
  // The HTTP client already throws properly typed errors, so we just let them propagate
587
721
  const response = await this.client.processes.getProcessLogs(id);
588
722
  return {
@@ -592,9 +726,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
592
726
  };
593
727
  }
594
728
 
595
-
596
729
  // Streaming methods - return ReadableStream for RPC compatibility
597
- async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
730
+ async execStream(
731
+ command: string,
732
+ options?: StreamOptions
733
+ ): Promise<ReadableStream<Uint8Array>> {
598
734
  // Check for cancellation
599
735
  if (options?.signal?.aborted) {
600
736
  throw new Error('Operation was aborted');
@@ -608,7 +744,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
608
744
  /**
609
745
  * Internal session-aware execStream implementation
610
746
  */
611
- 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>> {
612
752
  // Check for cancellation
613
753
  if (options?.signal?.aborted) {
614
754
  throw new Error('Operation was aborted');
@@ -617,7 +757,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
617
757
  return this.client.commands.executeStream(command, sessionId);
618
758
  }
619
759
 
620
- async streamProcessLogs(processId: string, options?: { signal?: AbortSignal }): Promise<ReadableStream<Uint8Array>> {
760
+ /**
761
+ * Stream logs from a background process as a ReadableStream.
762
+ */
763
+ async streamProcessLogs(
764
+ processId: string,
765
+ options?: { signal?: AbortSignal }
766
+ ): Promise<ReadableStream<Uint8Array>> {
621
767
  // Check for cancellation
622
768
  if (options?.signal?.aborted) {
623
769
  throw new Error('Operation was aborted');
@@ -630,7 +776,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
630
776
  repoUrl: string,
631
777
  options: { branch?: string; targetDir?: string; sessionId?: string }
632
778
  ) {
633
- const session = options.sessionId ?? await this.ensureDefaultSession();
779
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
634
780
  return this.client.git.checkout(repoUrl, session, {
635
781
  branch: options.branch,
636
782
  targetDir: options.targetDir
@@ -641,8 +787,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
641
787
  path: string,
642
788
  options: { recursive?: boolean; sessionId?: string } = {}
643
789
  ) {
644
- const session = options.sessionId ?? await this.ensureDefaultSession();
645
- 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
+ });
646
794
  }
647
795
 
648
796
  async writeFile(
@@ -650,21 +798,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
650
798
  content: string,
651
799
  options: { encoding?: string; sessionId?: string } = {}
652
800
  ) {
653
- const session = options.sessionId ?? await this.ensureDefaultSession();
654
- 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
+ });
655
805
  }
656
806
 
657
807
  async deleteFile(path: string, sessionId?: string) {
658
- const session = sessionId ?? await this.ensureDefaultSession();
808
+ const session = sessionId ?? (await this.ensureDefaultSession());
659
809
  return this.client.files.deleteFile(path, session);
660
810
  }
661
811
 
662
- async renameFile(
663
- oldPath: string,
664
- newPath: string,
665
- sessionId?: string
666
- ) {
667
- const session = sessionId ?? await this.ensureDefaultSession();
812
+ async renameFile(oldPath: string, newPath: string, sessionId?: string) {
813
+ const session = sessionId ?? (await this.ensureDefaultSession());
668
814
  return this.client.files.renameFile(oldPath, newPath, session);
669
815
  }
670
816
 
@@ -673,7 +819,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
673
819
  destinationPath: string,
674
820
  sessionId?: string
675
821
  ) {
676
- const session = sessionId ?? await this.ensureDefaultSession();
822
+ const session = sessionId ?? (await this.ensureDefaultSession());
677
823
  return this.client.files.moveFile(sourcePath, destinationPath, session);
678
824
  }
679
825
 
@@ -681,8 +827,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
681
827
  path: string,
682
828
  options: { encoding?: string; sessionId?: string } = {}
683
829
  ) {
684
- const session = options.sessionId ?? await this.ensureDefaultSession();
685
- 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
+ });
686
834
  }
687
835
 
688
836
  /**
@@ -695,7 +843,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
695
843
  path: string,
696
844
  options: { sessionId?: string } = {}
697
845
  ): Promise<ReadableStream<Uint8Array>> {
698
- const session = options.sessionId ?? await this.ensureDefaultSession();
846
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
699
847
  return this.client.files.readFileStream(path, session);
700
848
  }
701
849
 
@@ -708,7 +856,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
708
856
  }
709
857
 
710
858
  async exists(path: string, sessionId?: string) {
711
- const session = sessionId ?? await this.ensureDefaultSession();
859
+ const session = sessionId ?? (await this.ensureDefaultSession());
712
860
  return this.client.files.exists(path, session);
713
861
  }
714
862
 
@@ -730,7 +878,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
730
878
 
731
879
  // We need the sandbox name to construct preview URLs
732
880
  if (!this.sandboxName) {
733
- 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
+ );
734
884
  }
735
885
 
736
886
  // Generate and store token for this port
@@ -738,18 +888,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
738
888
  this.portTokens.set(port, token);
739
889
  await this.persistPortTokens();
740
890
 
741
- 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
+ );
742
897
 
743
898
  return {
744
899
  url,
745
900
  port,
746
- name: options?.name,
901
+ name: options?.name
747
902
  };
748
903
  }
749
904
 
750
905
  async unexposePort(port: number) {
751
906
  if (!validatePort(port)) {
752
- 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
+ );
753
910
  }
754
911
 
755
912
  const sessionId = await this.ensureDefaultSession();
@@ -768,32 +925,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
768
925
 
769
926
  // We need the sandbox name to construct preview URLs
770
927
  if (!this.sandboxName) {
771
- 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
+ );
772
931
  }
773
932
 
774
- return response.ports.map(port => {
933
+ return response.ports.map((port) => {
775
934
  // Get token for this port - must exist for all exposed ports
776
935
  const token = this.portTokens.get(port.port);
777
936
  if (!token) {
778
- 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
+ );
779
940
  }
780
941
 
781
942
  return {
782
- url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname, token),
943
+ url: this.constructPreviewUrl(
944
+ port.port,
945
+ this.sandboxName!,
946
+ hostname,
947
+ token
948
+ ),
783
949
  port: port.port,
784
- status: port.status,
950
+ status: port.status
785
951
  };
786
952
  });
787
953
  }
788
954
 
789
-
790
955
  async isPortExposed(port: number): Promise<boolean> {
791
956
  try {
792
957
  const sessionId = await this.ensureDefaultSession();
793
958
  const response = await this.client.ports.getExposedPorts(sessionId);
794
- return response.ports.some(exposedPort => exposedPort.port === port);
959
+ return response.ports.some((exposedPort) => exposedPort.port === port);
795
960
  } catch (error) {
796
- 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
+ );
797
966
  return false;
798
967
  }
799
968
  }
@@ -809,7 +978,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
809
978
  const storedToken = this.portTokens.get(port);
810
979
  if (!storedToken) {
811
980
  // This should not happen - all exposed ports must have tokens
812
- 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
+ );
813
986
  return false;
814
987
  }
815
988
 
@@ -825,7 +998,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
825
998
 
826
999
  // Convert to base64url format (URL-safe, no padding, lowercase)
827
1000
  const base64 = btoa(String.fromCharCode(...array));
828
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').toLowerCase();
1001
+ return base64
1002
+ .replace(/\+/g, '-')
1003
+ .replace(/\//g, '_')
1004
+ .replace(/=/g, '')
1005
+ .toLowerCase();
829
1006
  }
830
1007
 
831
1008
  private async persistPortTokens(): Promise<void> {
@@ -837,9 +1014,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
837
1014
  await this.ctx.storage.put('portTokens', tokensObj);
838
1015
  }
839
1016
 
840
- 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 {
841
1023
  if (!validatePort(port)) {
842
- 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
+ );
843
1027
  }
844
1028
 
845
1029
  // Validate sandbox ID (will throw SecurityError if invalid)
@@ -861,14 +1045,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
861
1045
 
862
1046
  return baseUrl.toString();
863
1047
  } catch (error) {
864
- 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
+ );
865
1053
  }
866
1054
  }
867
1055
 
868
1056
  // Production subdomain logic - enforce HTTPS
869
1057
  try {
870
1058
  // Always use HTTPS for production (non-localhost)
871
- const protocol = "https";
1059
+ const protocol = 'https';
872
1060
  const baseUrl = new URL(`${protocol}://${hostname}`);
873
1061
 
874
1062
  // Construct subdomain safely with mandatory token
@@ -877,7 +1065,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
877
1065
 
878
1066
  return baseUrl.toString();
879
1067
  } catch (error) {
880
- 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
+ );
881
1073
  }
882
1074
  }
883
1075
 
@@ -896,7 +1088,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
896
1088
  await this.client.utils.createSession({
897
1089
  id: sessionId,
898
1090
  env: options?.env,
899
- cwd: options?.cwd,
1091
+ cwd: options?.cwd
900
1092
  });
901
1093
 
902
1094
  // Return wrapper that binds sessionId to all operations
@@ -927,32 +1119,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
927
1119
  id: sessionId,
928
1120
 
929
1121
  // Command execution - delegate to internal session-aware methods
930
- exec: (command, options) => this.execWithSession(command, sessionId, options),
931
- 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),
932
1126
 
933
1127
  // Process management
934
- startProcess: (command, options) => this.startProcess(command, options, sessionId),
1128
+ startProcess: (command, options) =>
1129
+ this.startProcess(command, options, sessionId),
935
1130
  listProcesses: () => this.listProcesses(sessionId),
936
1131
  getProcess: (id) => this.getProcess(id, sessionId),
937
1132
  killProcess: (id, signal) => this.killProcess(id, signal),
938
1133
  killAllProcesses: () => this.killAllProcesses(),
939
1134
  cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
940
1135
  getProcessLogs: (id) => this.getProcessLogs(id),
941
- streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
1136
+ streamProcessLogs: (processId, options) =>
1137
+ this.streamProcessLogs(processId, options),
942
1138
 
943
1139
  // File operations - pass sessionId via options or parameter
944
- writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
945
- 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 }),
946
1144
  readFileStream: (path) => this.readFileStream(path, { sessionId }),
947
1145
  mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
948
1146
  deleteFile: (path) => this.deleteFile(path, sessionId),
949
- renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
950
- moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
951
- 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),
952
1153
  exists: (path) => this.exists(path, sessionId),
953
1154
 
954
1155
  // Git operations
955
- gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
1156
+ gitCheckout: (repoUrl, options) =>
1157
+ this.gitCheckout(repoUrl, { ...options, sessionId }),
956
1158
 
957
1159
  // Environment management - needs special handling
958
1160
  setEnvVars: async (envVars: Record<string, string>) => {
@@ -962,27 +1164,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
962
1164
  const escapedValue = value.replace(/'/g, "'\\''");
963
1165
  const exportCommand = `export ${key}='${escapedValue}'`;
964
1166
 
965
- const result = await this.client.commands.execute(exportCommand, sessionId);
1167
+ const result = await this.client.commands.execute(
1168
+ exportCommand,
1169
+ sessionId
1170
+ );
966
1171
 
967
1172
  if (result.exitCode !== 0) {
968
- 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
+ );
969
1176
  }
970
1177
  }
971
1178
  } catch (error) {
972
- 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
+ );
973
1184
  throw error;
974
1185
  }
975
1186
  },
976
1187
 
977
1188
  // Code interpreter methods - delegate to sandbox's code interpreter
978
- createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
1189
+ createCodeContext: (options) =>
1190
+ this.codeInterpreter.createCodeContext(options),
979
1191
  runCode: async (code, options) => {
980
1192
  const execution = await this.codeInterpreter.runCode(code, options);
981
1193
  return execution.toJSON();
982
1194
  },
983
- runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
1195
+ runCodeStream: (code, options) =>
1196
+ this.codeInterpreter.runCodeStream(code, options),
984
1197
  listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
985
- deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId),
1198
+ deleteCodeContext: (contextId) =>
1199
+ this.codeInterpreter.deleteCodeContext(contextId)
986
1200
  };
987
1201
  }
988
1202
 
@@ -990,16 +1204,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
990
1204
  // Code interpreter methods - delegate to CodeInterpreter wrapper
991
1205
  // ============================================================================
992
1206
 
993
- async createCodeContext(options?: CreateContextOptions): Promise<CodeContext> {
1207
+ async createCodeContext(
1208
+ options?: CreateContextOptions
1209
+ ): Promise<CodeContext> {
994
1210
  return this.codeInterpreter.createCodeContext(options);
995
1211
  }
996
1212
 
997
- async runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult> {
1213
+ async runCode(
1214
+ code: string,
1215
+ options?: RunCodeOptions
1216
+ ): Promise<ExecutionResult> {
998
1217
  const execution = await this.codeInterpreter.runCode(code, options);
999
1218
  return execution.toJSON();
1000
1219
  }
1001
1220
 
1002
- async runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream> {
1221
+ async runCodeStream(
1222
+ code: string,
1223
+ options?: RunCodeOptions
1224
+ ): Promise<ReadableStream> {
1003
1225
  return this.codeInterpreter.runCodeStream(code, options);
1004
1226
  }
1005
1227