@cloudflare/sandbox 0.0.0-02ee8fe → 0.0.0-04a60d8

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