@cloudflare/sandbox 0.0.0-c5bd973 → 0.0.0-c77ae8b

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 +213 -0
  2. package/Dockerfile +130 -9
  3. package/README.md +147 -50
  4. package/dist/chunk-2P3MDMNJ.js +2367 -0
  5. package/dist/chunk-2P3MDMNJ.js.map +1 -0
  6. package/dist/chunk-BFVUNTP4.js +104 -0
  7. package/dist/chunk-BFVUNTP4.js.map +1 -0
  8. package/dist/chunk-EKSWCBCA.js +86 -0
  9. package/dist/chunk-EKSWCBCA.js.map +1 -0
  10. package/dist/chunk-JXZMAU2C.js +559 -0
  11. package/dist/chunk-JXZMAU2C.js.map +1 -0
  12. package/dist/chunk-Z532A7QC.js +78 -0
  13. package/dist/chunk-Z532A7QC.js.map +1 -0
  14. package/dist/file-stream.d.ts +43 -0
  15. package/dist/file-stream.js +9 -0
  16. package/dist/file-stream.js.map +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +66 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/interpreter.d.ts +33 -0
  21. package/dist/interpreter.js +8 -0
  22. package/dist/interpreter.js.map +1 -0
  23. package/dist/request-handler.d.ts +18 -0
  24. package/dist/request-handler.js +12 -0
  25. package/dist/request-handler.js.map +1 -0
  26. package/dist/sandbox-CZTMzV2R.d.ts +587 -0
  27. package/dist/sandbox.d.ts +4 -0
  28. package/dist/sandbox.js +12 -0
  29. package/dist/sandbox.js.map +1 -0
  30. package/dist/security.d.ts +31 -0
  31. package/dist/security.js +13 -0
  32. package/dist/security.js.map +1 -0
  33. package/dist/sse-parser.d.ts +28 -0
  34. package/dist/sse-parser.js +11 -0
  35. package/dist/sse-parser.js.map +1 -0
  36. package/package.json +13 -9
  37. package/src/clients/base-client.ts +280 -0
  38. package/src/clients/command-client.ts +115 -0
  39. package/src/clients/file-client.ts +269 -0
  40. package/src/clients/git-client.ts +92 -0
  41. package/src/clients/index.ts +63 -0
  42. package/src/clients/interpreter-client.ts +329 -0
  43. package/src/clients/port-client.ts +105 -0
  44. package/src/clients/process-client.ts +177 -0
  45. package/src/clients/sandbox-client.ts +41 -0
  46. package/src/clients/types.ts +84 -0
  47. package/src/clients/utility-client.ts +94 -0
  48. package/src/errors/adapter.ts +180 -0
  49. package/src/errors/classes.ts +469 -0
  50. package/src/errors/index.ts +105 -0
  51. package/src/file-stream.ts +164 -0
  52. package/src/index.ts +83 -119
  53. package/src/interpreter.ts +159 -0
  54. package/src/request-handler.ts +170 -0
  55. package/src/sandbox.ts +936 -0
  56. package/src/security.ts +104 -0
  57. package/src/sse-parser.ts +143 -0
  58. package/startup.sh +3 -0
  59. package/tests/base-client.test.ts +328 -0
  60. package/tests/command-client.test.ts +407 -0
  61. package/tests/file-client.test.ts +643 -0
  62. package/tests/file-stream.test.ts +306 -0
  63. package/tests/git-client.test.ts +328 -0
  64. package/tests/port-client.test.ts +301 -0
  65. package/tests/process-client.test.ts +658 -0
  66. package/tests/sandbox.test.ts +465 -0
  67. package/tests/sse-parser.test.ts +290 -0
  68. package/tests/utility-client.test.ts +266 -0
  69. package/tests/wrangler.jsonc +35 -0
  70. package/tsconfig.json +9 -1
  71. package/vitest.config.ts +31 -0
  72. package/container_src/index.ts +0 -2906
  73. package/container_src/package.json +0 -9
  74. package/src/client.ts +0 -1950
  75. package/tests/client.example.ts +0 -308
  76. package/tests/connection-test.ts +0 -81
  77. package/tests/simple-test.ts +0 -81
  78. package/tests/test1.ts +0 -281
  79. package/tests/test2.ts +0 -929
@@ -0,0 +1,92 @@
1
+ import type { GitCheckoutResult } from '@repo/shared';
2
+ import { BaseHttpClient } from './base-client';
3
+ import type { HttpClientOptions, SessionRequest } from './types';
4
+
5
+ // Re-export for convenience
6
+ export type { GitCheckoutResult };
7
+
8
+ /**
9
+ * Request interface for Git checkout operations
10
+ */
11
+ export interface GitCheckoutRequest extends SessionRequest {
12
+ repoUrl: string;
13
+ branch?: string;
14
+ targetDir?: string;
15
+ }
16
+
17
+ /**
18
+ * Client for Git repository operations
19
+ */
20
+ export class GitClient extends BaseHttpClient {
21
+
22
+ /**
23
+ * Clone a Git repository
24
+ * @param repoUrl - URL of the Git repository to clone
25
+ * @param sessionId - The session ID for this operation
26
+ * @param options - Optional settings (branch, targetDir)
27
+ */
28
+ async checkout(
29
+ repoUrl: string,
30
+ sessionId: string,
31
+ options?: {
32
+ branch?: string;
33
+ targetDir?: string;
34
+ }
35
+ ): Promise<GitCheckoutResult> {
36
+ try {
37
+ // Determine target directory - use provided path or generate from repo name
38
+ let targetDir = options?.targetDir;
39
+ if (!targetDir) {
40
+ const repoName = this.extractRepoName(repoUrl);
41
+ // Ensure absolute path in /workspace
42
+ targetDir = `/workspace/${repoName}`;
43
+ }
44
+
45
+ const data: GitCheckoutRequest = {
46
+ repoUrl,
47
+ sessionId,
48
+ targetDir,
49
+ };
50
+
51
+ // Only include branch if explicitly specified
52
+ // This allows Git to use the repository's default branch
53
+ if (options?.branch) {
54
+ data.branch = options.branch;
55
+ }
56
+
57
+ const response = await this.post<GitCheckoutResult>(
58
+ '/api/git/checkout',
59
+ data
60
+ );
61
+
62
+ this.logSuccess(
63
+ 'Repository cloned',
64
+ `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`
65
+ );
66
+
67
+ return response;
68
+ } catch (error) {
69
+ this.logError('checkout', error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extract repository name from URL for default directory name
76
+ */
77
+ private extractRepoName(repoUrl: string): string {
78
+ try {
79
+ const url = new URL(repoUrl);
80
+ const pathParts = url.pathname.split('/');
81
+ const repoName = pathParts[pathParts.length - 1];
82
+
83
+ // Remove .git extension if present
84
+ return repoName.replace(/\.git$/, '');
85
+ } catch {
86
+ // Fallback for invalid URLs
87
+ const parts = repoUrl.split('/');
88
+ const repoName = parts[parts.length - 1];
89
+ return repoName.replace(/\.git$/, '') || 'repo';
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,63 @@
1
+ // Main client exports
2
+
3
+
4
+ // Command client types
5
+ export type {
6
+ ExecuteRequest,
7
+ ExecuteResponse,
8
+ } from './command-client';
9
+
10
+ // Domain-specific clients
11
+ export { CommandClient } from './command-client';
12
+ // File client types
13
+ export type {
14
+ FileOperationRequest,
15
+ MkdirRequest,
16
+ ReadFileRequest,
17
+ WriteFileRequest,
18
+ } from './file-client';
19
+ export { FileClient } from './file-client';
20
+ // Git client types
21
+ export type {
22
+ GitCheckoutRequest,
23
+ GitCheckoutResult,
24
+ } from './git-client';
25
+ export { GitClient } from './git-client';
26
+ export { type ExecutionCallbacks, InterpreterClient } from './interpreter-client';
27
+ // Port client types
28
+ export type {
29
+ ExposePortRequest,
30
+ PortCloseResult,
31
+ PortExposeResult,
32
+ PortListResult,
33
+ UnexposePortRequest,
34
+ } from './port-client';
35
+ export { PortClient } from './port-client';
36
+ // Process client types
37
+ export type {
38
+ ProcessCleanupResult,
39
+ ProcessInfoResult,
40
+ ProcessKillResult,
41
+ ProcessListResult,
42
+ ProcessLogsResult,
43
+ ProcessStartResult,
44
+ StartProcessRequest,
45
+ } from './process-client';
46
+ export { ProcessClient } from './process-client';
47
+ export { SandboxClient } from './sandbox-client';
48
+ // Types and interfaces
49
+ export type {
50
+ BaseApiResponse,
51
+ ContainerStub,
52
+ ErrorResponse,
53
+ HttpClientOptions,
54
+ RequestConfig,
55
+ ResponseHandler,
56
+ SessionRequest,
57
+ } from './types';
58
+ // Utility client types
59
+ export type {
60
+ CommandsResponse,
61
+ PingResponse,
62
+ } from './utility-client';
63
+ export { UtilityClient } from './utility-client';
@@ -0,0 +1,329 @@
1
+ import {
2
+ type CodeContext,
3
+ type ContextCreateResult,
4
+ type ContextListResult,
5
+ type CreateContextOptions,
6
+ type ExecutionError,
7
+ type OutputMessage,
8
+ type Result,
9
+ ResultImpl,
10
+ } from '@repo/shared';
11
+ import type { ErrorResponse } from '../errors';
12
+ import { createErrorFromResponse, ErrorCode, InterpreterNotReadyError } from '../errors';
13
+ import { BaseHttpClient } from './base-client.js';
14
+ import type { HttpClientOptions } from './types.js';
15
+
16
+ // Streaming execution data from the server
17
+ interface StreamingExecutionData {
18
+ type: "result" | "stdout" | "stderr" | "error" | "execution_complete";
19
+ text?: string;
20
+ html?: string;
21
+ png?: string; // base64
22
+ jpeg?: string; // base64
23
+ svg?: string;
24
+ latex?: string;
25
+ markdown?: string;
26
+ javascript?: string;
27
+ json?: unknown;
28
+ chart?: {
29
+ type:
30
+ | "line"
31
+ | "bar"
32
+ | "scatter"
33
+ | "pie"
34
+ | "histogram"
35
+ | "heatmap"
36
+ | "unknown";
37
+ data: unknown;
38
+ options?: unknown;
39
+ };
40
+ data?: unknown;
41
+ metadata?: Record<string, unknown>;
42
+ execution_count?: number;
43
+ ename?: string;
44
+ evalue?: string;
45
+ traceback?: string[];
46
+ lineNumber?: number;
47
+ timestamp?: number;
48
+ }
49
+
50
+ export interface ExecutionCallbacks {
51
+ onStdout?: (output: OutputMessage) => void | Promise<void>;
52
+ onStderr?: (output: OutputMessage) => void | Promise<void>;
53
+ onResult?: (result: Result) => void | Promise<void>;
54
+ onError?: (error: ExecutionError) => void | Promise<void>;
55
+ }
56
+
57
+ export class InterpreterClient extends BaseHttpClient {
58
+ private readonly maxRetries = 3;
59
+ private readonly retryDelayMs = 1000;
60
+
61
+ async createCodeContext(
62
+ options: CreateContextOptions = {}
63
+ ): Promise<CodeContext> {
64
+ return this.executeWithRetry(async () => {
65
+ const response = await this.doFetch("/api/contexts", {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({
69
+ language: options.language || "python",
70
+ cwd: options.cwd || "/workspace",
71
+ env_vars: options.envVars,
72
+ }),
73
+ });
74
+
75
+ if (!response.ok) {
76
+ const error = await this.parseErrorResponse(response);
77
+ throw error;
78
+ }
79
+
80
+ const data = (await response.json()) as ContextCreateResult;
81
+ if (!data.success) {
82
+ throw new Error(`Failed to create context: ${JSON.stringify(data)}`);
83
+ }
84
+
85
+ return {
86
+ id: data.contextId,
87
+ language: data.language,
88
+ cwd: data.cwd || '/workspace',
89
+ createdAt: new Date(data.timestamp),
90
+ lastUsed: new Date(data.timestamp),
91
+ };
92
+ });
93
+ }
94
+
95
+ async runCodeStream(
96
+ contextId: string | undefined,
97
+ code: string,
98
+ language: string | undefined,
99
+ callbacks: ExecutionCallbacks,
100
+ timeoutMs?: number
101
+ ): Promise<void> {
102
+ return this.executeWithRetry(async () => {
103
+ const response = await this.doFetch("/api/execute/code", {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ Accept: "text/event-stream",
108
+ },
109
+ body: JSON.stringify({
110
+ context_id: contextId,
111
+ code,
112
+ language,
113
+ ...(timeoutMs !== undefined && { timeout_ms: timeoutMs })
114
+ }),
115
+ });
116
+
117
+ if (!response.ok) {
118
+ const error = await this.parseErrorResponse(response);
119
+ throw error;
120
+ }
121
+
122
+ if (!response.body) {
123
+ throw new Error("No response body for streaming execution");
124
+ }
125
+
126
+ // Process streaming response
127
+ for await (const chunk of this.readLines(response.body)) {
128
+ await this.parseExecutionResult(chunk, callbacks);
129
+ }
130
+ });
131
+ }
132
+
133
+ async listCodeContexts(): Promise<CodeContext[]> {
134
+ return this.executeWithRetry(async () => {
135
+ const response = await this.doFetch("/api/contexts", {
136
+ method: "GET",
137
+ headers: { "Content-Type": "application/json" },
138
+ });
139
+
140
+ if (!response.ok) {
141
+ const error = await this.parseErrorResponse(response);
142
+ throw error;
143
+ }
144
+
145
+ const data = (await response.json()) as ContextListResult;
146
+ if (!data.success) {
147
+ throw new Error(`Failed to list contexts: ${JSON.stringify(data)}`);
148
+ }
149
+
150
+ return data.contexts.map((ctx) => ({
151
+ id: ctx.id,
152
+ language: ctx.language,
153
+ cwd: ctx.cwd || '/workspace',
154
+ createdAt: new Date(data.timestamp),
155
+ lastUsed: new Date(data.timestamp),
156
+ }));
157
+ });
158
+ }
159
+
160
+ async deleteCodeContext(contextId: string): Promise<void> {
161
+ return this.executeWithRetry(async () => {
162
+ const response = await this.doFetch(`/api/contexts/${contextId}`, {
163
+ method: "DELETE",
164
+ headers: { "Content-Type": "application/json" },
165
+ });
166
+
167
+ if (!response.ok) {
168
+ const error = await this.parseErrorResponse(response);
169
+ throw error;
170
+ }
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Execute an operation with automatic retry for transient errors
176
+ */
177
+ private async executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
178
+ let lastError: Error | undefined;
179
+
180
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
181
+ try {
182
+ return await operation();
183
+ } catch (error) {
184
+ this.logError('executeWithRetry', error);
185
+ lastError = error as Error;
186
+
187
+ // Check if it's a retryable error (interpreter not ready)
188
+ if (this.isRetryableError(error)) {
189
+ // Don't retry on the last attempt
190
+ if (attempt < this.maxRetries - 1) {
191
+ // Exponential backoff with jitter
192
+ const delay =
193
+ this.retryDelayMs * 2 ** attempt + Math.random() * 1000;
194
+ await new Promise((resolve) => setTimeout(resolve, delay));
195
+ continue;
196
+ }
197
+ }
198
+
199
+ // Not retryable or last attempt - throw the error
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ throw lastError || new Error("Execution failed after retries");
205
+ }
206
+
207
+ private isRetryableError(error: unknown): boolean {
208
+ if (error instanceof InterpreterNotReadyError) {
209
+ return true;
210
+ }
211
+
212
+ if (error instanceof Error) {
213
+ return (
214
+ error.message.includes("not ready") ||
215
+ error.message.includes("initializing")
216
+ );
217
+ }
218
+
219
+ return false;
220
+ }
221
+
222
+ private async parseErrorResponse(response: Response): Promise<Error> {
223
+ try {
224
+ const errorData = await response.json() as ErrorResponse;
225
+ return createErrorFromResponse(errorData);
226
+ } catch {
227
+ // Fallback if response isn't JSON
228
+ const errorResponse: ErrorResponse = {
229
+ code: ErrorCode.INTERNAL_ERROR,
230
+ message: `HTTP ${response.status}: ${response.statusText}`,
231
+ context: {},
232
+ httpStatus: response.status,
233
+ timestamp: new Date().toISOString()
234
+ };
235
+ return createErrorFromResponse(errorResponse);
236
+ }
237
+ }
238
+
239
+ private async *readLines(
240
+ stream: ReadableStream<Uint8Array>
241
+ ): AsyncGenerator<string> {
242
+ const reader = stream.getReader();
243
+ let buffer = "";
244
+
245
+ try {
246
+ while (true) {
247
+ const { done, value } = await reader.read();
248
+ if (value) {
249
+ buffer += new TextDecoder().decode(value);
250
+ }
251
+ if (done) break;
252
+
253
+ let newlineIdx = buffer.indexOf("\n");
254
+ while (newlineIdx !== -1) {
255
+ yield buffer.slice(0, newlineIdx);
256
+ buffer = buffer.slice(newlineIdx + 1);
257
+ newlineIdx = buffer.indexOf("\n");
258
+ }
259
+ }
260
+
261
+ // Yield any remaining data
262
+ if (buffer.length > 0) {
263
+ yield buffer;
264
+ }
265
+ } finally {
266
+ reader.releaseLock();
267
+ }
268
+ }
269
+
270
+ private async parseExecutionResult(
271
+ line: string,
272
+ callbacks: ExecutionCallbacks
273
+ ) {
274
+ if (!line.trim()) return;
275
+
276
+ // Skip lines that don't start with "data: " (SSE format)
277
+ if (!line.startsWith('data: ')) return;
278
+
279
+ try {
280
+ // Strip "data: " prefix and parse JSON
281
+ const jsonData = line.substring(6); // "data: " is 6 characters
282
+ const data = JSON.parse(jsonData) as StreamingExecutionData;
283
+
284
+ switch (data.type) {
285
+ case "stdout":
286
+ if (callbacks.onStdout && data.text) {
287
+ await callbacks.onStdout({
288
+ text: data.text,
289
+ timestamp: data.timestamp || Date.now(),
290
+ });
291
+ }
292
+ break;
293
+
294
+ case "stderr":
295
+ if (callbacks.onStderr && data.text) {
296
+ await callbacks.onStderr({
297
+ text: data.text,
298
+ timestamp: data.timestamp || Date.now(),
299
+ });
300
+ }
301
+ break;
302
+
303
+ case "result":
304
+ if (callbacks.onResult) {
305
+ // Create a ResultImpl instance from the raw data
306
+ const result = new ResultImpl(data);
307
+ await callbacks.onResult(result);
308
+ }
309
+ break;
310
+
311
+ case "error":
312
+ if (callbacks.onError) {
313
+ await callbacks.onError({
314
+ name: data.ename || "Error",
315
+ message: data.evalue || "Unknown error",
316
+ traceback: data.traceback || [],
317
+ });
318
+ }
319
+ break;
320
+
321
+ case "execution_complete":
322
+ // Signal completion - callbacks can handle cleanup if needed
323
+ break;
324
+ }
325
+ } catch (error) {
326
+ this.logError('parseExecutionResult', error);
327
+ }
328
+ }
329
+ }
@@ -0,0 +1,105 @@
1
+ import type {
2
+ PortCloseResult,
3
+ PortExposeResult,
4
+ PortListResult,
5
+ } from '@repo/shared';
6
+ import { BaseHttpClient } from './base-client';
7
+ import type { HttpClientOptions } from './types';
8
+
9
+ // Re-export for convenience
10
+ export type {
11
+ PortExposeResult,
12
+ PortCloseResult,
13
+ PortListResult,
14
+ };
15
+
16
+ /**
17
+ * Request interface for exposing ports
18
+ */
19
+ export interface ExposePortRequest {
20
+ port: number;
21
+ name?: string;
22
+ }
23
+
24
+ /**
25
+ * Request interface for unexposing ports
26
+ */
27
+ export interface UnexposePortRequest {
28
+ port: number;
29
+ }
30
+
31
+ /**
32
+ * Client for port management and preview URL operations
33
+ */
34
+ export class PortClient extends BaseHttpClient {
35
+
36
+ /**
37
+ * Expose a port and get a preview URL
38
+ * @param port - Port number to expose
39
+ * @param sessionId - The session ID for this operation
40
+ * @param name - Optional name for the port
41
+ */
42
+ async exposePort(
43
+ port: number,
44
+ sessionId: string,
45
+ name?: string
46
+ ): Promise<PortExposeResult> {
47
+ try {
48
+ const data = { port, sessionId, name };
49
+
50
+ const response = await this.post<PortExposeResult>(
51
+ '/api/expose-port',
52
+ data
53
+ );
54
+
55
+ this.logSuccess(
56
+ 'Port exposed',
57
+ `${port} exposed at ${response.url}${name ? ` (${name})` : ''}`
58
+ );
59
+
60
+ return response;
61
+ } catch (error) {
62
+ this.logError('exposePort', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Unexpose a port and remove its preview URL
69
+ * @param port - Port number to unexpose
70
+ * @param sessionId - The session ID for this operation
71
+ */
72
+ async unexposePort(port: number, sessionId: string): Promise<PortCloseResult> {
73
+ try {
74
+ const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
75
+ const response = await this.delete<PortCloseResult>(url);
76
+
77
+ this.logSuccess('Port unexposed', `${port}`);
78
+ return response;
79
+ } catch (error) {
80
+ this.logError('unexposePort', error);
81
+ throw error;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all currently exposed ports
87
+ * @param sessionId - The session ID for this operation
88
+ */
89
+ async getExposedPorts(sessionId: string): Promise<PortListResult> {
90
+ try {
91
+ const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
92
+ const response = await this.get<PortListResult>(url);
93
+
94
+ this.logSuccess(
95
+ 'Exposed ports retrieved',
96
+ `${response.ports.length} ports exposed`
97
+ );
98
+
99
+ return response;
100
+ } catch (error) {
101
+ this.logError('getExposedPorts', error);
102
+ throw error;
103
+ }
104
+ }
105
+ }