@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
@@ -39,11 +39,13 @@
39
39
  */
40
40
 
41
41
  // Re-export context types for advanced usage
42
- export type {
42
+ export type {
43
43
  CodeExecutionContext,
44
44
  CommandErrorContext,
45
45
  CommandNotFoundContext,
46
- ContextNotFoundContext,ErrorCodeType, ErrorResponse,
46
+ ContextNotFoundContext,
47
+ ErrorCodeType,
48
+ ErrorResponse,
47
49
  FileExistsContext,
48
50
  FileNotFoundContext,
49
51
  FileSystemContext,
@@ -53,13 +55,15 @@ export type {
53
55
  GitRepositoryNotFoundContext,
54
56
  InternalErrorContext,
55
57
  InterpreterNotReadyContext,
56
- InvalidPortContext,OperationType,
58
+ InvalidPortContext,
59
+ OperationType,
57
60
  PortAlreadyExposedContext,
58
61
  PortErrorContext,
59
62
  PortNotExposedContext,
60
63
  ProcessErrorContext,
61
64
  ProcessNotFoundContext,
62
- ValidationFailedContext,} from '@repo/shared/errors';
65
+ ValidationFailedContext
66
+ } from '@repo/shared/errors';
63
67
  // Re-export shared types and constants
64
68
  export { ErrorCode, Operation } from '@repo/shared/errors';
65
69
 
@@ -101,5 +105,5 @@ export {
101
105
  SandboxError,
102
106
  ServiceNotRespondingError,
103
107
  // Validation Errors
104
- ValidationFailedError,
108
+ ValidationFailedError
105
109
  } from './classes';
@@ -3,7 +3,9 @@ import type { FileChunk, FileMetadata, FileStreamEvent } from '@repo/shared';
3
3
  /**
4
4
  * Parse SSE (Server-Sent Events) lines from a stream
5
5
  */
6
- async function* parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileStreamEvent> {
6
+ async function* parseSSE(
7
+ stream: ReadableStream<Uint8Array>
8
+ ): AsyncGenerator<FileStreamEvent> {
7
9
  const reader = stream.getReader();
8
10
  const decoder = new TextDecoder();
9
11
  let buffer = '';
@@ -59,7 +61,9 @@ async function* parseSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<Fil
59
61
  * }
60
62
  * ```
61
63
  */
62
- export async function* streamFile(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileChunk, FileMetadata> {
64
+ export async function* streamFile(
65
+ stream: ReadableStream<Uint8Array>
66
+ ): AsyncGenerator<FileChunk, FileMetadata> {
63
67
  let metadata: FileMetadata | null = null;
64
68
 
65
69
  for await (const event of parseSSE(stream)) {
@@ -69,7 +73,7 @@ export async function* streamFile(stream: ReadableStream<Uint8Array>): AsyncGene
69
73
  mimeType: event.mimeType,
70
74
  size: event.size,
71
75
  isBinary: event.isBinary,
72
- encoding: event.encoding,
76
+ encoding: event.encoding
73
77
  };
74
78
  break;
75
79
 
@@ -144,8 +148,9 @@ export async function collectFile(stream: ReadableStream<Uint8Array>): Promise<{
144
148
  // Combine chunks based on type
145
149
  if (metadata.isBinary) {
146
150
  // Binary file - combine Uint8Arrays
147
- const totalLength = chunks.reduce((sum, chunk) =>
148
- sum + (chunk instanceof Uint8Array ? chunk.length : 0), 0
151
+ const totalLength = chunks.reduce(
152
+ (sum, chunk) => sum + (chunk instanceof Uint8Array ? chunk.length : 0),
153
+ 0
149
154
  );
150
155
  const combined = new Uint8Array(totalLength);
151
156
  let offset = 0;
@@ -158,7 +163,7 @@ export async function collectFile(stream: ReadableStream<Uint8Array>): Promise<{
158
163
  return { content: combined, metadata };
159
164
  } else {
160
165
  // Text file - combine strings
161
- const combined = chunks.filter(c => typeof c === 'string').join('');
166
+ const combined = chunks.filter((c) => typeof c === 'string').join('');
162
167
  return { content: combined, metadata };
163
168
  }
164
169
  }
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  // Export the main Sandbox class and utilities
2
2
 
3
-
4
3
  // Export the new client architecture
5
4
  export {
6
5
  CommandClient,
@@ -10,8 +9,8 @@ export {
10
9
  ProcessClient,
11
10
  SandboxClient,
12
11
  UtilityClient
13
- } from "./clients";
14
- export { getSandbox, Sandbox } from "./sandbox";
12
+ } from './clients';
13
+ export { getSandbox, Sandbox } from './sandbox';
15
14
 
16
15
  // Legacy types are now imported from the new client architecture
17
16
 
@@ -20,21 +19,20 @@ export type {
20
19
  BaseExecOptions,
21
20
  ExecEvent,
22
21
  ExecOptions,
23
- ExecResult,FileChunk, FileMetadata, FileStreamEvent,
22
+ ExecResult,
23
+ FileChunk,
24
+ FileMetadata,
25
+ FileStreamEvent,
24
26
  ISandbox,
25
27
  LogEvent,
26
28
  Process,
27
29
  ProcessOptions,
28
30
  ProcessStatus,
29
- StreamOptions
30
- } from "@repo/shared";
31
+ StreamOptions
32
+ } from '@repo/shared';
31
33
  export * from '@repo/shared';
32
34
  // Export type guards for runtime validation
33
- export {
34
- isExecResult,
35
- isProcess,
36
- isProcessStatus
37
- } from "@repo/shared";
35
+ export { isExecResult, isProcess, isProcessStatus } from '@repo/shared';
38
36
  // Export all client types from new architecture
39
37
  export type {
40
38
  BaseApiResponse,
@@ -79,15 +77,24 @@ export type {
79
77
  StartProcessRequest,
80
78
  UnexposePortRequest,
81
79
  WriteFileRequest
82
- } from "./clients";
83
- export type { ExecutionCallbacks, InterpreterClient } from './clients/interpreter-client.js';
80
+ } from './clients';
81
+ export type {
82
+ ExecutionCallbacks,
83
+ InterpreterClient
84
+ } from './clients/interpreter-client.js';
84
85
  // Export file streaming utilities for binary file support
85
86
  export { collectFile, streamFile } from './file-stream';
86
87
  // Export interpreter functionality
87
88
  export { CodeInterpreter } from './interpreter.js';
88
89
  // Re-export request handler utilities
89
90
  export {
90
- proxyToSandbox, type RouteInfo, type SandboxEnv
91
+ proxyToSandbox,
92
+ type RouteInfo,
93
+ type SandboxEnv
91
94
  } from './request-handler';
92
95
  // Export SSE parser for converting ReadableStream to AsyncIterable
93
- export { asyncIterableToSSEStream, parseSSEStream, responseToAsyncIterable } from "./sse-parser";
96
+ export {
97
+ asyncIterableToSSEStream,
98
+ parseSSEStream,
99
+ responseToAsyncIterable
100
+ } from './sse-parser';
@@ -6,11 +6,11 @@ import {
6
6
  type OutputMessage,
7
7
  type Result,
8
8
  ResultImpl,
9
- type RunCodeOptions,
10
- } from "@repo/shared";
11
- import type { InterpreterClient } from "./clients/interpreter-client.js";
12
- import type { Sandbox } from "./sandbox.js";
13
- import { validateLanguage } from "./security.js";
9
+ type RunCodeOptions
10
+ } from '@repo/shared';
11
+ import type { InterpreterClient } from './clients/interpreter-client.js';
12
+ import type { Sandbox } from './sandbox.js';
13
+ import { validateLanguage } from './security.js';
14
14
 
15
15
  export class CodeInterpreter {
16
16
  private interpreterClient: InterpreterClient;
@@ -18,7 +18,8 @@ export class CodeInterpreter {
18
18
 
19
19
  constructor(sandbox: Sandbox) {
20
20
  // In init-testing architecture, client is a SandboxClient with an interpreter property
21
- this.interpreterClient = (sandbox.client as any).interpreter as InterpreterClient;
21
+ this.interpreterClient = (sandbox.client as any)
22
+ .interpreter as InterpreterClient;
22
23
  }
23
24
 
24
25
  /**
@@ -46,7 +47,7 @@ export class CodeInterpreter {
46
47
  let context = options.context;
47
48
  if (!context) {
48
49
  // Try to find or create a default context for the language
49
- const language = options.language || "python";
50
+ const language = options.language || 'python';
50
51
  context = await this.getOrCreateDefaultContext(language);
51
52
  }
52
53
 
@@ -54,24 +55,29 @@ export class CodeInterpreter {
54
55
  const execution = new Execution(code, context);
55
56
 
56
57
  // Stream execution
57
- await this.interpreterClient.runCodeStream(context.id, code, options.language, {
58
- onStdout: (output: OutputMessage) => {
59
- execution.logs.stdout.push(output.text);
60
- if (options.onStdout) return options.onStdout(output);
61
- },
62
- onStderr: (output: OutputMessage) => {
63
- execution.logs.stderr.push(output.text);
64
- if (options.onStderr) return options.onStderr(output);
65
- },
66
- onResult: async (result: Result) => {
67
- execution.results.push(new ResultImpl(result) as any);
68
- if (options.onResult) return options.onResult(result);
69
- },
70
- onError: (error: ExecutionError) => {
71
- execution.error = error;
72
- if (options.onError) return options.onError(error);
73
- },
74
- });
58
+ await this.interpreterClient.runCodeStream(
59
+ context.id,
60
+ code,
61
+ options.language,
62
+ {
63
+ onStdout: (output: OutputMessage) => {
64
+ execution.logs.stdout.push(output.text);
65
+ if (options.onStdout) return options.onStdout(output);
66
+ },
67
+ onStderr: (output: OutputMessage) => {
68
+ execution.logs.stderr.push(output.text);
69
+ if (options.onStderr) return options.onStderr(output);
70
+ },
71
+ onResult: async (result: Result) => {
72
+ execution.results.push(new ResultImpl(result) as any);
73
+ if (options.onResult) return options.onResult(result);
74
+ },
75
+ onError: (error: ExecutionError) => {
76
+ execution.error = error;
77
+ if (options.onError) return options.onError(error);
78
+ }
79
+ }
80
+ );
75
81
 
76
82
  return execution;
77
83
  }
@@ -86,36 +92,39 @@ export class CodeInterpreter {
86
92
  // Get or create context
87
93
  let context = options.context;
88
94
  if (!context) {
89
- const language = options.language || "python";
95
+ const language = options.language || 'python';
90
96
  context = await this.getOrCreateDefaultContext(language);
91
97
  }
92
98
 
93
99
  // Create streaming response
94
100
  // Note: doFetch is protected but we need direct access for raw stream response
95
- const response = await (this.interpreterClient as any).doFetch("/api/execute/code", {
96
- method: "POST",
97
- headers: {
98
- "Content-Type": "application/json",
99
- Accept: "text/event-stream",
100
- },
101
- body: JSON.stringify({
102
- context_id: context.id,
103
- code,
104
- language: options.language,
105
- }),
106
- });
101
+ const response = await (this.interpreterClient as any).doFetch(
102
+ '/api/execute/code',
103
+ {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/json',
107
+ Accept: 'text/event-stream'
108
+ },
109
+ body: JSON.stringify({
110
+ context_id: context.id,
111
+ code,
112
+ language: options.language
113
+ })
114
+ }
115
+ );
107
116
 
108
117
  if (!response.ok) {
109
118
  const errorData = (await response
110
119
  .json()
111
- .catch(() => ({ error: "Unknown error" }))) as { error?: string };
120
+ .catch(() => ({ error: 'Unknown error' }))) as { error?: string };
112
121
  throw new Error(
113
122
  errorData.error || `Failed to execute code: ${response.status}`
114
123
  );
115
124
  }
116
125
 
117
126
  if (!response.body) {
118
- throw new Error("No response body for streaming execution");
127
+ throw new Error('No response body for streaming execution');
119
128
  }
120
129
 
121
130
  return response.body;
@@ -144,7 +153,7 @@ export class CodeInterpreter {
144
153
  }
145
154
 
146
155
  private async getOrCreateDefaultContext(
147
- language: "python" | "javascript" | "typescript"
156
+ language: 'python' | 'javascript' | 'typescript'
148
157
  ): Promise<CodeContext> {
149
158
  // Check if we have a cached context for this language
150
159
  for (const context of this.contexts.values()) {
@@ -1,9 +1,7 @@
1
- import { createLogger, type LogContext, TraceContext } from "@repo/shared";
2
- import { getSandbox, type Sandbox } from "./sandbox";
3
- import {
4
- sanitizeSandboxId,
5
- validatePort
6
- } from "./security";
1
+ import { switchPort } from '@cloudflare/containers';
2
+ import { createLogger, type LogContext, TraceContext } from '@repo/shared';
3
+ import { getSandbox, type Sandbox } from './sandbox';
4
+ import { sanitizeSandboxId, validatePort } from './security';
7
5
 
8
6
  export interface SandboxEnv {
9
7
  Sandbox: DurableObjectNamespace<Sandbox>;
@@ -21,7 +19,8 @@ export async function proxyToSandbox<E extends SandboxEnv>(
21
19
  env: E
22
20
  ): Promise<Response | null> {
23
21
  // Create logger context for this request
24
- const traceId = TraceContext.fromHeaders(request.headers) || TraceContext.generate();
22
+ const traceId =
23
+ TraceContext.fromHeaders(request.headers) || TraceContext.generate();
25
24
  const logger = createLogger({
26
25
  component: 'sandbox-do',
27
26
  traceId,
@@ -70,6 +69,14 @@ export async function proxyToSandbox<E extends SandboxEnv>(
70
69
  }
71
70
  }
72
71
 
72
+ // Detect WebSocket upgrade request
73
+ const upgradeHeader = request.headers.get('Upgrade');
74
+ if (upgradeHeader?.toLowerCase() === 'websocket') {
75
+ // WebSocket path: Must use fetch() not containerFetch()
76
+ // This bypasses JSRPC serialization boundary which cannot handle WebSocket upgrades
77
+ return await sandbox.fetch(switchPort(request, port));
78
+ }
79
+
73
80
  // Build proxy request with proper headers
74
81
  let proxyUrl: string;
75
82
 
@@ -89,23 +96,29 @@ export async function proxyToSandbox<E extends SandboxEnv>(
89
96
  'X-Original-URL': request.url,
90
97
  'X-Forwarded-Host': url.hostname,
91
98
  'X-Forwarded-Proto': url.protocol.replace(':', ''),
92
- 'X-Sandbox-Name': sandboxId, // Pass the friendly name
99
+ 'X-Sandbox-Name': sandboxId // Pass the friendly name
93
100
  },
94
101
  body: request.body,
95
102
  // @ts-expect-error - duplex required for body streaming in modern runtimes
96
- duplex: 'half',
103
+ duplex: 'half'
97
104
  });
98
105
 
99
- return sandbox.containerFetch(proxyRequest, port);
106
+ return await sandbox.containerFetch(proxyRequest, port);
100
107
  } catch (error) {
101
- logger.error('Proxy routing error', error instanceof Error ? error : new Error(String(error)));
108
+ logger.error(
109
+ 'Proxy routing error',
110
+ error instanceof Error ? error : new Error(String(error))
111
+ );
102
112
  return new Response('Proxy routing error', { status: 500 });
103
113
  }
104
114
  }
105
115
 
106
116
  function extractSandboxRoute(url: URL): RouteInfo | null {
107
117
  // Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory)
108
- const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*[^.-]|[^.-])-([a-zA-Z0-9_-]{12,20})\.(.+)$/);
118
+ // Token is always exactly 16 chars (generated by generatePortToken)
119
+ const subdomainMatch = url.hostname.match(
120
+ /^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/
121
+ );
109
122
 
110
123
  if (!subdomainMatch) {
111
124
  return null;
@@ -136,8 +149,8 @@ function extractSandboxRoute(url: URL): RouteInfo | null {
136
149
  return {
137
150
  port,
138
151
  sandboxId: sanitizedSandboxId,
139
- path: url.pathname || "/",
140
- token,
152
+ path: url.pathname || '/',
153
+ token
141
154
  };
142
155
  }
143
156
 
@@ -153,18 +166,18 @@ export function isLocalhostPattern(hostname: string): boolean {
153
166
  return hostname === '[::1]';
154
167
  }
155
168
  }
156
-
169
+
157
170
  // Handle bare IPv6 without brackets
158
171
  if (hostname === '::1') {
159
172
  return true;
160
173
  }
161
-
174
+
162
175
  // For IPv4 and regular hostnames, split on colon to remove port
163
- const hostPart = hostname.split(":")[0];
164
-
176
+ const hostPart = hostname.split(':')[0];
177
+
165
178
  return (
166
- hostPart === "localhost" ||
167
- hostPart === "127.0.0.1" ||
168
- hostPart === "0.0.0.0"
179
+ hostPart === 'localhost' ||
180
+ hostPart === '127.0.0.1' ||
181
+ hostPart === '0.0.0.0'
169
182
  );
170
183
  }