@cloudflare/sandbox 0.0.0-fddccfd → 0.0.0-ff2fa91

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 (74) hide show
  1. package/CHANGELOG.md +102 -15
  2. package/Dockerfile +84 -31
  3. package/README.md +9 -2
  4. package/dist/index.d.ts +1889 -9
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +3144 -64
  7. package/dist/index.js.map +1 -1
  8. package/package.json +9 -9
  9. package/src/clients/base-client.ts +39 -24
  10. package/src/clients/command-client.ts +8 -8
  11. package/src/clients/file-client.ts +51 -20
  12. package/src/clients/git-client.ts +3 -4
  13. package/src/clients/index.ts +12 -15
  14. package/src/clients/interpreter-client.ts +51 -47
  15. package/src/clients/port-client.ts +10 -10
  16. package/src/clients/process-client.ts +11 -8
  17. package/src/clients/sandbox-client.ts +2 -4
  18. package/src/clients/types.ts +6 -2
  19. package/src/clients/utility-client.ts +34 -5
  20. package/src/errors/adapter.ts +90 -32
  21. package/src/errors/classes.ts +189 -64
  22. package/src/errors/index.ts +9 -5
  23. package/src/file-stream.ts +11 -6
  24. package/src/index.ts +22 -15
  25. package/src/interpreter.ts +50 -41
  26. package/src/request-handler.ts +34 -21
  27. package/src/sandbox.ts +443 -144
  28. package/src/security.ts +21 -6
  29. package/src/sse-parser.ts +4 -3
  30. package/src/version.ts +6 -0
  31. package/tests/base-client.test.ts +116 -80
  32. package/tests/command-client.test.ts +149 -112
  33. package/tests/file-client.test.ts +373 -185
  34. package/tests/file-stream.test.ts +24 -20
  35. package/tests/get-sandbox.test.ts +149 -0
  36. package/tests/git-client.test.ts +188 -101
  37. package/tests/port-client.test.ts +100 -108
  38. package/tests/process-client.test.ts +204 -179
  39. package/tests/request-handler.test.ts +292 -0
  40. package/tests/sandbox.test.ts +303 -62
  41. package/tests/sse-parser.test.ts +17 -16
  42. package/tests/utility-client.test.ts +129 -56
  43. package/tests/version.test.ts +16 -0
  44. package/tsdown.config.ts +12 -0
  45. package/vitest.config.ts +6 -6
  46. package/dist/chunk-2P3MDMNJ.js +0 -2367
  47. package/dist/chunk-2P3MDMNJ.js.map +0 -1
  48. package/dist/chunk-BFVUNTP4.js +0 -104
  49. package/dist/chunk-BFVUNTP4.js.map +0 -1
  50. package/dist/chunk-EKSWCBCA.js +0 -86
  51. package/dist/chunk-EKSWCBCA.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-Z532A7QC.js +0 -78
  55. package/dist/chunk-Z532A7QC.js.map +0 -1
  56. package/dist/file-stream.d.ts +0 -43
  57. package/dist/file-stream.js +0 -9
  58. package/dist/file-stream.js.map +0 -1
  59. package/dist/interpreter.d.ts +0 -33
  60. package/dist/interpreter.js +0 -8
  61. package/dist/interpreter.js.map +0 -1
  62. package/dist/request-handler.d.ts +0 -18
  63. package/dist/request-handler.js +0 -12
  64. package/dist/request-handler.js.map +0 -1
  65. package/dist/sandbox-CZTMzV2R.d.ts +0 -587
  66. package/dist/sandbox.d.ts +0 -4
  67. package/dist/sandbox.js +0 -12
  68. package/dist/sandbox.js.map +0 -1
  69. package/dist/security.d.ts +0 -31
  70. package/dist/security.js +0 -13
  71. package/dist/security.js.map +0 -1
  72. package/dist/sse-parser.d.ts +0 -28
  73. package/dist/sse-parser.js +0 -11
  74. package/dist/sse-parser.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,
@@ -13,40 +13,65 @@ import type {
13
13
  ProcessOptions,
14
14
  ProcessStatus,
15
15
  RunCodeOptions,
16
+ SandboxOptions,
16
17
  SessionOptions,
17
18
  StreamOptions
18
- } from "@repo/shared";
19
- import { createLogger, runWithLogger, TraceContext } from "@repo/shared";
20
- 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';
21
22
  import type { ErrorResponse } from './errors';
22
23
  import { CustomDomainRequiredError, ErrorCode } from './errors';
23
- import { CodeInterpreter } from "./interpreter";
24
- import { isLocalhostPattern } from "./request-handler";
25
- import {
26
- SecurityError,
27
- sanitizeSandboxId,
28
- validatePort
29
- } from "./security";
30
- import { parseSSEStream } from "./sse-parser";
31
-
32
- export function getSandbox(ns: DurableObjectNamespace<Sandbox>, id: string, options?: {
33
- baseUrl: string
34
- }) {
35
- const stub = getContainer(ns, id);
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';
29
+
30
+ export function getSandbox(
31
+ ns: DurableObjectNamespace<Sandbox>,
32
+ id: string,
33
+ options?: SandboxOptions
34
+ ): Sandbox {
35
+ const stub = getContainer(ns, id) as unknown as Sandbox;
36
36
 
37
37
  // Store the name on first access
38
38
  stub.setSandboxName?.(id);
39
39
 
40
- if(options?.baseUrl) {
40
+ if (options?.baseUrl) {
41
41
  stub.setBaseUrl(options.baseUrl);
42
42
  }
43
43
 
44
- return stub;
44
+ if (options?.sleepAfter !== undefined) {
45
+ stub.setSleepAfter(options.sleepAfter);
46
+ }
47
+
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
+ };
45
70
  }
46
71
 
47
72
  export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
48
73
  defaultPort = 3000; // Default port for the container's Bun server
49
- sleepAfter = "3m"; // 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
50
75
 
51
76
  client: SandboxClient;
52
77
  private codeInterpreter: CodeInterpreter;
@@ -56,14 +81,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
56
81
  private defaultSession: string | null = null;
57
82
  envVars: Record<string, string> = {};
58
83
  private logger: ReturnType<typeof createLogger>;
84
+ private keepAliveEnabled: boolean = false;
59
85
 
60
- constructor(ctx: DurableObject['ctx'], env: Env) {
86
+ constructor(ctx: DurableObjectState<{}>, env: Env) {
61
87
  super(ctx, env);
62
88
 
63
89
  const envObj = env as any;
64
90
  // Set sandbox environment variables from env object
65
91
  const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
66
- sandboxEnvKeys.forEach(key => {
92
+ sandboxEnvKeys.forEach((key) => {
67
93
  if (envObj?.[key]) {
68
94
  this.envVars[key] = envObj[key];
69
95
  }
@@ -77,7 +103,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
77
103
  this.client = new SandboxClient({
78
104
  logger: this.logger,
79
105
  port: 3000, // Control plane port
80
- stub: this,
106
+ stub: this
81
107
  });
82
108
 
83
109
  // Initialize code interpreter - pass 'this' after client is ready
@@ -86,9 +112,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
86
112
 
87
113
  // Load the sandbox name, port tokens, and default session from storage on initialization
88
114
  this.ctx.blockConcurrencyWhile(async () => {
89
- this.sandboxName = await this.ctx.storage.get<string>('sandboxName') || null;
90
- this.defaultSession = await this.ctx.storage.get<string>('defaultSession') || null;
91
- 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
+ {};
92
122
 
93
123
  // Convert stored tokens back to Map
94
124
  this.portTokens = new Map();
@@ -112,12 +142,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
112
142
  this.baseUrl = baseUrl;
113
143
  await this.ctx.storage.put('baseUrl', baseUrl);
114
144
  } else {
115
- if(this.baseUrl !== baseUrl) {
116
- 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
+ );
117
149
  }
118
150
  }
119
151
  }
120
152
 
153
+ // RPC method to set the sleep timeout
154
+ async setSleepAfter(sleepAfter: string | number): Promise<void> {
155
+ this.sleepAfter = sleepAfter;
156
+ }
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
+
121
172
  // RPC method to set environment variables
122
173
  async setEnvVars(envVars: Record<string, string>): Promise<void> {
123
174
  // Update local state for new sessions
@@ -130,10 +181,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
130
181
  const escapedValue = value.replace(/'/g, "'\\''");
131
182
  const exportCommand = `export ${key}='${escapedValue}'`;
132
183
 
133
- const result = await this.client.commands.execute(exportCommand, this.defaultSession);
184
+ const result = await this.client.commands.execute(
185
+ exportCommand,
186
+ this.defaultSession
187
+ );
134
188
 
135
189
  if (result.exitCode !== 0) {
136
- 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
+ );
137
193
  }
138
194
  }
139
195
  }
@@ -149,6 +205,61 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
149
205
 
150
206
  override onStart() {
151
207
  this.logger.debug('Sandbox started');
208
+
209
+ // Check version compatibility asynchronously (don't block startup)
210
+ this.checkVersionCompatibility().catch((error) => {
211
+ this.logger.error(
212
+ 'Version compatibility check failed',
213
+ error instanceof Error ? error : new Error(String(error))
214
+ );
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Check if the container version matches the SDK version
220
+ * Logs a warning if there's a mismatch
221
+ */
222
+ private async checkVersionCompatibility(): Promise<void> {
223
+ try {
224
+ // Get the SDK version (imported from version.ts)
225
+ const sdkVersion = SDK_VERSION;
226
+
227
+ // Get container version
228
+ const containerVersion = await this.client.utils.getVersion();
229
+
230
+ // If container version is unknown, it's likely an old container without the endpoint
231
+ if (containerVersion === 'unknown') {
232
+ this.logger.warn(
233
+ 'Container version check: Container version could not be determined. ' +
234
+ 'This may indicate an outdated container image. ' +
235
+ 'Please update your container to match SDK version ' +
236
+ sdkVersion
237
+ );
238
+ return;
239
+ }
240
+
241
+ // Check if versions match
242
+ if (containerVersion !== sdkVersion) {
243
+ const message =
244
+ `Version mismatch detected! SDK version (${sdkVersion}) does not match ` +
245
+ `container version (${containerVersion}). This may cause compatibility issues. ` +
246
+ `Please update your container image to version ${sdkVersion}`;
247
+
248
+ // Log warning - we can't reliably detect dev vs prod environment in Durable Objects
249
+ // so we always use warning level as requested by the user
250
+ this.logger.warn(message);
251
+ } else {
252
+ this.logger.debug('Version check passed', {
253
+ sdkVersion,
254
+ containerVersion
255
+ });
256
+ }
257
+ } catch (error) {
258
+ // Don't fail the sandbox initialization if version check fails
259
+ this.logger.debug('Version compatibility check encountered an error', {
260
+ error: error instanceof Error ? error.message : String(error)
261
+ });
262
+ }
152
263
  }
153
264
 
154
265
  override onStop() {
@@ -156,13 +267,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
156
267
  }
157
268
 
158
269
  override onError(error: unknown) {
159
- 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
+ }
160
291
  }
161
292
 
162
293
  // Override fetch to route internal container requests to appropriate ports
163
294
  override async fetch(request: Request): Promise<Response> {
164
295
  // Extract or generate trace ID from request
165
- const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
296
+ const traceId =
297
+ TraceContext.fromHeaders(request.headers) || TraceContext.generate();
166
298
 
167
299
  // Create request-specific logger with trace ID
168
300
  const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
@@ -177,7 +309,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
177
309
  await this.ctx.storage.put('sandboxName', name);
178
310
  }
179
311
 
180
- // Determine which port to route to
312
+ // Detect WebSocket upgrade request (RFC 6455 compliant)
313
+ const upgradeHeader = request.headers.get('Upgrade');
314
+ const connectionHeader = request.headers.get('Connection');
315
+ const isWebSocket =
316
+ upgradeHeader?.toLowerCase() === 'websocket' &&
317
+ connectionHeader?.toLowerCase().includes('upgrade');
318
+
319
+ if (isWebSocket) {
320
+ // WebSocket path: Let parent Container class handle WebSocket proxying
321
+ // This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
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
+ }
336
+ }
337
+
338
+ // Non-WebSocket: Use existing port determination and HTTP routing logic
181
339
  const port = this.determinePort(url);
182
340
 
183
341
  // Route to the appropriate port
@@ -185,6 +343,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
185
343
  });
186
344
  }
187
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
+
188
351
  private determinePort(url: URL): number {
189
352
  // Extract port from proxy requests (e.g., /proxy/8080/*)
190
353
  const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
@@ -214,7 +377,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
214
377
  await this.client.utils.createSession({
215
378
  id: sessionId,
216
379
  env: this.envVars || {},
217
- cwd: '/workspace',
380
+ cwd: '/workspace'
218
381
  });
219
382
 
220
383
  this.defaultSession = sessionId;
@@ -223,8 +386,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
223
386
  this.logger.debug('Default session initialized', { sessionId });
224
387
  } catch (error: any) {
225
388
  // If session already exists (e.g., after hot reload), reuse it
226
- if (error?.message?.includes('already exists') || error?.message?.includes('Session')) {
227
- this.logger.debug('Reusing existing session after reload', { sessionId });
389
+ if (error?.message?.includes('already exists')) {
390
+ this.logger.debug('Reusing existing session after reload', {
391
+ sessionId
392
+ });
228
393
  this.defaultSession = sessionId;
229
394
  // Persist to storage in case it wasn't saved before
230
395
  await this.ctx.storage.put('defaultSession', sessionId);
@@ -256,7 +421,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
256
421
  const startTime = Date.now();
257
422
  const timestamp = new Date().toISOString();
258
423
 
259
- // Handle timeout
260
424
  let timeoutId: NodeJS.Timeout | undefined;
261
425
 
262
426
  try {
@@ -269,13 +433,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
269
433
 
270
434
  if (options?.stream && options?.onOutput) {
271
435
  // Streaming with callbacks - we need to collect the final result
272
- 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
+ );
273
443
  } else {
274
444
  // Regular execution with session
275
445
  const response = await this.client.commands.execute(command, sessionId);
276
446
 
277
447
  const duration = Date.now() - startTime;
278
- result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
448
+ result = this.mapExecuteResponseToExecResult(
449
+ response,
450
+ duration,
451
+ sessionId
452
+ );
279
453
  }
280
454
 
281
455
  // Call completion callback if provided
@@ -307,7 +481,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
307
481
  let stderr = '';
308
482
 
309
483
  try {
310
- const stream = await this.client.commands.executeStream(command, sessionId);
484
+ const stream = await this.client.commands.executeStream(
485
+ command,
486
+ sessionId
487
+ );
311
488
 
312
489
  for await (const event of parseSSEStream<ExecEvent>(stream)) {
313
490
  // Check for cancellation
@@ -352,7 +529,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
352
529
 
353
530
  // If we get here without a complete event, something went wrong
354
531
  throw new Error('Stream ended without completion event');
355
-
356
532
  } catch (error) {
357
533
  if (options.signal?.aborted) {
358
534
  throw new Error('Operation was aborted');
@@ -400,8 +576,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
400
576
  pid: data.pid,
401
577
  command: data.command,
402
578
  status: data.status,
403
- startTime: typeof data.startTime === 'string' ? new Date(data.startTime) : data.startTime,
404
- 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,
405
588
  exitCode: data.exitCode,
406
589
  sessionId,
407
590
 
@@ -421,25 +604,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
421
604
  };
422
605
  }
423
606
 
424
-
425
607
  // Background process management
426
- 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> {
427
613
  // Use the new HttpClient method to start the process
428
614
  try {
429
- const session = sessionId ?? await this.ensureDefaultSession();
430
- const response = await this.client.processes.startProcess(command, session, {
431
- processId: options?.processId
432
- });
433
-
434
- const processObj = this.createProcessFromDTO({
435
- id: response.processId,
436
- pid: response.pid,
437
- command: response.command,
438
- status: 'running' as ProcessStatus,
439
- startTime: new Date(),
440
- endTime: undefined,
441
- exitCode: undefined
442
- }, 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
+ );
443
636
 
444
637
  // Call onStart callback if provided
445
638
  if (options?.onStart) {
@@ -447,7 +640,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
447
640
  }
448
641
 
449
642
  return processObj;
450
-
451
643
  } catch (error) {
452
644
  if (options?.onError && error instanceof Error) {
453
645
  options.onError(error);
@@ -458,42 +650,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
458
650
  }
459
651
 
460
652
  async listProcesses(sessionId?: string): Promise<Process[]> {
461
- const session = sessionId ?? await this.ensureDefaultSession();
653
+ const session = sessionId ?? (await this.ensureDefaultSession());
462
654
  const response = await this.client.processes.listProcesses();
463
655
 
464
- return response.processes.map(processData =>
465
- this.createProcessFromDTO({
466
- id: processData.id,
467
- pid: processData.pid,
468
- command: processData.command,
469
- status: processData.status,
470
- startTime: processData.startTime,
471
- endTime: processData.endTime,
472
- exitCode: processData.exitCode
473
- }, 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
+ )
474
669
  );
475
670
  }
476
671
 
477
672
  async getProcess(id: string, sessionId?: string): Promise<Process | null> {
478
- const session = sessionId ?? await this.ensureDefaultSession();
673
+ const session = sessionId ?? (await this.ensureDefaultSession());
479
674
  const response = await this.client.processes.getProcess(id);
480
675
  if (!response.process) {
481
676
  return null;
482
677
  }
483
678
 
484
679
  const processData = response.process;
485
- return this.createProcessFromDTO({
486
- id: processData.id,
487
- pid: processData.pid,
488
- command: processData.command,
489
- status: processData.status,
490
- startTime: processData.startTime,
491
- endTime: processData.endTime,
492
- exitCode: processData.exitCode
493
- }, session);
494
- }
495
-
496
- 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> {
497
699
  // Note: signal parameter is not currently supported by the HttpClient implementation
498
700
  // The HTTP client already throws properly typed errors, so we just let them propagate
499
701
  await this.client.processes.killProcess(id);
@@ -511,7 +713,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
511
713
  return 0;
512
714
  }
513
715
 
514
- 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 }> {
515
720
  // The HTTP client already throws properly typed errors, so we just let them propagate
516
721
  const response = await this.client.processes.getProcessLogs(id);
517
722
  return {
@@ -521,9 +726,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
521
726
  };
522
727
  }
523
728
 
524
-
525
729
  // Streaming methods - return ReadableStream for RPC compatibility
526
- async execStream(command: string, options?: StreamOptions): Promise<ReadableStream<Uint8Array>> {
730
+ async execStream(
731
+ command: string,
732
+ options?: StreamOptions
733
+ ): Promise<ReadableStream<Uint8Array>> {
527
734
  // Check for cancellation
528
735
  if (options?.signal?.aborted) {
529
736
  throw new Error('Operation was aborted');
@@ -537,7 +744,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
537
744
  /**
538
745
  * Internal session-aware execStream implementation
539
746
  */
540
- 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>> {
541
752
  // Check for cancellation
542
753
  if (options?.signal?.aborted) {
543
754
  throw new Error('Operation was aborted');
@@ -546,7 +757,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
546
757
  return this.client.commands.executeStream(command, sessionId);
547
758
  }
548
759
 
549
- 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>> {
550
767
  // Check for cancellation
551
768
  if (options?.signal?.aborted) {
552
769
  throw new Error('Operation was aborted');
@@ -559,7 +776,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
559
776
  repoUrl: string,
560
777
  options: { branch?: string; targetDir?: string; sessionId?: string }
561
778
  ) {
562
- const session = options.sessionId ?? await this.ensureDefaultSession();
779
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
563
780
  return this.client.git.checkout(repoUrl, session, {
564
781
  branch: options.branch,
565
782
  targetDir: options.targetDir
@@ -570,8 +787,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
570
787
  path: string,
571
788
  options: { recursive?: boolean; sessionId?: string } = {}
572
789
  ) {
573
- const session = options.sessionId ?? await this.ensureDefaultSession();
574
- 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
+ });
575
794
  }
576
795
 
577
796
  async writeFile(
@@ -579,21 +798,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
579
798
  content: string,
580
799
  options: { encoding?: string; sessionId?: string } = {}
581
800
  ) {
582
- const session = options.sessionId ?? await this.ensureDefaultSession();
583
- 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
+ });
584
805
  }
585
806
 
586
807
  async deleteFile(path: string, sessionId?: string) {
587
- const session = sessionId ?? await this.ensureDefaultSession();
808
+ const session = sessionId ?? (await this.ensureDefaultSession());
588
809
  return this.client.files.deleteFile(path, session);
589
810
  }
590
811
 
591
- async renameFile(
592
- oldPath: string,
593
- newPath: string,
594
- sessionId?: string
595
- ) {
596
- const session = sessionId ?? await this.ensureDefaultSession();
812
+ async renameFile(oldPath: string, newPath: string, sessionId?: string) {
813
+ const session = sessionId ?? (await this.ensureDefaultSession());
597
814
  return this.client.files.renameFile(oldPath, newPath, session);
598
815
  }
599
816
 
@@ -602,7 +819,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
602
819
  destinationPath: string,
603
820
  sessionId?: string
604
821
  ) {
605
- const session = sessionId ?? await this.ensureDefaultSession();
822
+ const session = sessionId ?? (await this.ensureDefaultSession());
606
823
  return this.client.files.moveFile(sourcePath, destinationPath, session);
607
824
  }
608
825
 
@@ -610,8 +827,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
610
827
  path: string,
611
828
  options: { encoding?: string; sessionId?: string } = {}
612
829
  ) {
613
- const session = options.sessionId ?? await this.ensureDefaultSession();
614
- 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
+ });
615
834
  }
616
835
 
617
836
  /**
@@ -624,7 +843,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
624
843
  path: string,
625
844
  options: { sessionId?: string } = {}
626
845
  ): Promise<ReadableStream<Uint8Array>> {
627
- const session = options.sessionId ?? await this.ensureDefaultSession();
846
+ const session = options.sessionId ?? (await this.ensureDefaultSession());
628
847
  return this.client.files.readFileStream(path, session);
629
848
  }
630
849
 
@@ -636,6 +855,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
636
855
  return this.client.files.listFiles(path, session, options);
637
856
  }
638
857
 
858
+ async exists(path: string, sessionId?: string) {
859
+ const session = sessionId ?? (await this.ensureDefaultSession());
860
+ return this.client.files.exists(path, session);
861
+ }
862
+
639
863
  async exposePort(port: number, options: { name?: string; hostname: string }) {
640
864
  // Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
641
865
  if (options.hostname.endsWith('.workers.dev')) {
@@ -654,7 +878,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
654
878
 
655
879
  // We need the sandbox name to construct preview URLs
656
880
  if (!this.sandboxName) {
657
- 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
+ );
658
884
  }
659
885
 
660
886
  // Generate and store token for this port
@@ -662,18 +888,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
662
888
  this.portTokens.set(port, token);
663
889
  await this.persistPortTokens();
664
890
 
665
- 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
+ );
666
897
 
667
898
  return {
668
899
  url,
669
900
  port,
670
- name: options?.name,
901
+ name: options?.name
671
902
  };
672
903
  }
673
904
 
674
905
  async unexposePort(port: number) {
675
906
  if (!validatePort(port)) {
676
- 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
+ );
677
910
  }
678
911
 
679
912
  const sessionId = await this.ensureDefaultSession();
@@ -692,32 +925,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
692
925
 
693
926
  // We need the sandbox name to construct preview URLs
694
927
  if (!this.sandboxName) {
695
- 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
+ );
696
931
  }
697
932
 
698
- return response.ports.map(port => {
933
+ return response.ports.map((port) => {
699
934
  // Get token for this port - must exist for all exposed ports
700
935
  const token = this.portTokens.get(port.port);
701
936
  if (!token) {
702
- 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
+ );
703
940
  }
704
941
 
705
942
  return {
706
- url: this.constructPreviewUrl(port.port, this.sandboxName!, hostname, token),
943
+ url: this.constructPreviewUrl(
944
+ port.port,
945
+ this.sandboxName!,
946
+ hostname,
947
+ token
948
+ ),
707
949
  port: port.port,
708
- status: port.status,
950
+ status: port.status
709
951
  };
710
952
  });
711
953
  }
712
954
 
713
-
714
955
  async isPortExposed(port: number): Promise<boolean> {
715
956
  try {
716
957
  const sessionId = await this.ensureDefaultSession();
717
958
  const response = await this.client.ports.getExposedPorts(sessionId);
718
- return response.ports.some(exposedPort => exposedPort.port === port);
959
+ return response.ports.some((exposedPort) => exposedPort.port === port);
719
960
  } catch (error) {
720
- 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
+ );
721
966
  return false;
722
967
  }
723
968
  }
@@ -733,7 +978,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
733
978
  const storedToken = this.portTokens.get(port);
734
979
  if (!storedToken) {
735
980
  // This should not happen - all exposed ports must have tokens
736
- 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
+ );
737
986
  return false;
738
987
  }
739
988
 
@@ -749,7 +998,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
749
998
 
750
999
  // Convert to base64url format (URL-safe, no padding, lowercase)
751
1000
  const base64 = btoa(String.fromCharCode(...array));
752
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '').toLowerCase();
1001
+ return base64
1002
+ .replace(/\+/g, '-')
1003
+ .replace(/\//g, '_')
1004
+ .replace(/=/g, '')
1005
+ .toLowerCase();
753
1006
  }
754
1007
 
755
1008
  private async persistPortTokens(): Promise<void> {
@@ -761,9 +1014,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
761
1014
  await this.ctx.storage.put('portTokens', tokensObj);
762
1015
  }
763
1016
 
764
- 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 {
765
1023
  if (!validatePort(port)) {
766
- 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
+ );
767
1027
  }
768
1028
 
769
1029
  // Validate sandbox ID (will throw SecurityError if invalid)
@@ -785,14 +1045,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
785
1045
 
786
1046
  return baseUrl.toString();
787
1047
  } catch (error) {
788
- 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
+ );
789
1053
  }
790
1054
  }
791
1055
 
792
1056
  // Production subdomain logic - enforce HTTPS
793
1057
  try {
794
1058
  // Always use HTTPS for production (non-localhost)
795
- const protocol = "https";
1059
+ const protocol = 'https';
796
1060
  const baseUrl = new URL(`${protocol}://${hostname}`);
797
1061
 
798
1062
  // Construct subdomain safely with mandatory token
@@ -801,7 +1065,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
801
1065
 
802
1066
  return baseUrl.toString();
803
1067
  } catch (error) {
804
- 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
+ );
805
1073
  }
806
1074
  }
807
1075
 
@@ -820,7 +1088,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
820
1088
  await this.client.utils.createSession({
821
1089
  id: sessionId,
822
1090
  env: options?.env,
823
- cwd: options?.cwd,
1091
+ cwd: options?.cwd
824
1092
  });
825
1093
 
826
1094
  // Return wrapper that binds sessionId to all operations
@@ -851,31 +1119,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
851
1119
  id: sessionId,
852
1120
 
853
1121
  // Command execution - delegate to internal session-aware methods
854
- exec: (command, options) => this.execWithSession(command, sessionId, options),
855
- 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),
856
1126
 
857
1127
  // Process management
858
- startProcess: (command, options) => this.startProcess(command, options, sessionId),
1128
+ startProcess: (command, options) =>
1129
+ this.startProcess(command, options, sessionId),
859
1130
  listProcesses: () => this.listProcesses(sessionId),
860
1131
  getProcess: (id) => this.getProcess(id, sessionId),
861
1132
  killProcess: (id, signal) => this.killProcess(id, signal),
862
1133
  killAllProcesses: () => this.killAllProcesses(),
863
1134
  cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
864
1135
  getProcessLogs: (id) => this.getProcessLogs(id),
865
- streamProcessLogs: (processId, options) => this.streamProcessLogs(processId, options),
1136
+ streamProcessLogs: (processId, options) =>
1137
+ this.streamProcessLogs(processId, options),
866
1138
 
867
1139
  // File operations - pass sessionId via options or parameter
868
- writeFile: (path, content, options) => this.writeFile(path, content, { ...options, sessionId }),
869
- 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 }),
870
1144
  readFileStream: (path) => this.readFileStream(path, { sessionId }),
871
1145
  mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
872
1146
  deleteFile: (path) => this.deleteFile(path, sessionId),
873
- renameFile: (oldPath, newPath) => this.renameFile(oldPath, newPath, sessionId),
874
- moveFile: (sourcePath, destPath) => this.moveFile(sourcePath, destPath, sessionId),
875
- 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),
1153
+ exists: (path) => this.exists(path, sessionId),
876
1154
 
877
1155
  // Git operations
878
- gitCheckout: (repoUrl, options) => this.gitCheckout(repoUrl, { ...options, sessionId }),
1156
+ gitCheckout: (repoUrl, options) =>
1157
+ this.gitCheckout(repoUrl, { ...options, sessionId }),
879
1158
 
880
1159
  // Environment management - needs special handling
881
1160
  setEnvVars: async (envVars: Record<string, string>) => {
@@ -885,27 +1164,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
885
1164
  const escapedValue = value.replace(/'/g, "'\\''");
886
1165
  const exportCommand = `export ${key}='${escapedValue}'`;
887
1166
 
888
- const result = await this.client.commands.execute(exportCommand, sessionId);
1167
+ const result = await this.client.commands.execute(
1168
+ exportCommand,
1169
+ sessionId
1170
+ );
889
1171
 
890
1172
  if (result.exitCode !== 0) {
891
- 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
+ );
892
1176
  }
893
1177
  }
894
1178
  } catch (error) {
895
- 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
+ );
896
1184
  throw error;
897
1185
  }
898
1186
  },
899
1187
 
900
1188
  // Code interpreter methods - delegate to sandbox's code interpreter
901
- createCodeContext: (options) => this.codeInterpreter.createCodeContext(options),
1189
+ createCodeContext: (options) =>
1190
+ this.codeInterpreter.createCodeContext(options),
902
1191
  runCode: async (code, options) => {
903
1192
  const execution = await this.codeInterpreter.runCode(code, options);
904
1193
  return execution.toJSON();
905
1194
  },
906
- runCodeStream: (code, options) => this.codeInterpreter.runCodeStream(code, options),
1195
+ runCodeStream: (code, options) =>
1196
+ this.codeInterpreter.runCodeStream(code, options),
907
1197
  listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
908
- deleteCodeContext: (contextId) => this.codeInterpreter.deleteCodeContext(contextId),
1198
+ deleteCodeContext: (contextId) =>
1199
+ this.codeInterpreter.deleteCodeContext(contextId)
909
1200
  };
910
1201
  }
911
1202
 
@@ -913,16 +1204,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
913
1204
  // Code interpreter methods - delegate to CodeInterpreter wrapper
914
1205
  // ============================================================================
915
1206
 
916
- async createCodeContext(options?: CreateContextOptions): Promise<CodeContext> {
1207
+ async createCodeContext(
1208
+ options?: CreateContextOptions
1209
+ ): Promise<CodeContext> {
917
1210
  return this.codeInterpreter.createCodeContext(options);
918
1211
  }
919
1212
 
920
- async runCode(code: string, options?: RunCodeOptions): Promise<ExecutionResult> {
1213
+ async runCode(
1214
+ code: string,
1215
+ options?: RunCodeOptions
1216
+ ): Promise<ExecutionResult> {
921
1217
  const execution = await this.codeInterpreter.runCode(code, options);
922
1218
  return execution.toJSON();
923
1219
  }
924
1220
 
925
- async runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream> {
1221
+ async runCodeStream(
1222
+ code: string,
1223
+ options?: RunCodeOptions
1224
+ ): Promise<ReadableStream> {
926
1225
  return this.codeInterpreter.runCodeStream(code, options);
927
1226
  }
928
1227