@cloudflare/sandbox 0.5.4 → 0.6.0

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 (57) hide show
  1. package/Dockerfile +54 -59
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +13 -8
  8. package/.turbo/turbo-build.log +0 -23
  9. package/CHANGELOG.md +0 -441
  10. package/src/clients/base-client.ts +0 -356
  11. package/src/clients/command-client.ts +0 -133
  12. package/src/clients/file-client.ts +0 -300
  13. package/src/clients/git-client.ts +0 -98
  14. package/src/clients/index.ts +0 -64
  15. package/src/clients/interpreter-client.ts +0 -333
  16. package/src/clients/port-client.ts +0 -105
  17. package/src/clients/process-client.ts +0 -198
  18. package/src/clients/sandbox-client.ts +0 -39
  19. package/src/clients/types.ts +0 -88
  20. package/src/clients/utility-client.ts +0 -156
  21. package/src/errors/adapter.ts +0 -238
  22. package/src/errors/classes.ts +0 -594
  23. package/src/errors/index.ts +0 -109
  24. package/src/file-stream.ts +0 -169
  25. package/src/index.ts +0 -121
  26. package/src/interpreter.ts +0 -168
  27. package/src/openai/index.ts +0 -465
  28. package/src/request-handler.ts +0 -184
  29. package/src/sandbox.ts +0 -1937
  30. package/src/security.ts +0 -119
  31. package/src/sse-parser.ts +0 -144
  32. package/src/storage-mount/credential-detection.ts +0 -41
  33. package/src/storage-mount/errors.ts +0 -51
  34. package/src/storage-mount/index.ts +0 -17
  35. package/src/storage-mount/provider-detection.ts +0 -93
  36. package/src/storage-mount/types.ts +0 -17
  37. package/src/version.ts +0 -6
  38. package/tests/base-client.test.ts +0 -582
  39. package/tests/command-client.test.ts +0 -444
  40. package/tests/file-client.test.ts +0 -831
  41. package/tests/file-stream.test.ts +0 -310
  42. package/tests/get-sandbox.test.ts +0 -172
  43. package/tests/git-client.test.ts +0 -455
  44. package/tests/openai-shell-editor.test.ts +0 -434
  45. package/tests/port-client.test.ts +0 -283
  46. package/tests/process-client.test.ts +0 -649
  47. package/tests/request-handler.test.ts +0 -292
  48. package/tests/sandbox.test.ts +0 -890
  49. package/tests/sse-parser.test.ts +0 -291
  50. package/tests/storage-mount/credential-detection.test.ts +0 -119
  51. package/tests/storage-mount/provider-detection.test.ts +0 -77
  52. package/tests/utility-client.test.ts +0 -339
  53. package/tests/version.test.ts +0 -16
  54. package/tests/wrangler.jsonc +0 -35
  55. package/tsconfig.json +0 -11
  56. package/tsdown.config.ts +0 -13
  57. package/vitest.config.ts +0 -31
@@ -1,356 +0,0 @@
1
- import type { Logger } from '@repo/shared';
2
- import { createNoOpLogger } from '@repo/shared';
3
- import { getHttpStatus } from '@repo/shared/errors';
4
- import type { ErrorResponse as NewErrorResponse } from '../errors';
5
- import { createErrorFromResponse, ErrorCode } from '../errors';
6
- import type { SandboxError } from '../errors/classes';
7
- import type { HttpClientOptions, ResponseHandler } from './types';
8
-
9
- // Container startup retry configuration
10
- const TIMEOUT_MS = 120_000; // 2 minutes total retry budget
11
- const MIN_TIME_FOR_RETRY_MS = 15_000; // Need at least 15s remaining to retry (allows for longer container startups)
12
-
13
- /**
14
- * Abstract base class providing common HTTP functionality for all domain clients
15
- */
16
- export abstract class BaseHttpClient {
17
- protected baseUrl: string;
18
- protected options: HttpClientOptions;
19
- protected logger: Logger;
20
-
21
- constructor(options: HttpClientOptions = {}) {
22
- this.options = options;
23
- this.logger = options.logger ?? createNoOpLogger();
24
- this.baseUrl = this.options.baseUrl!;
25
- }
26
-
27
- /**
28
- * Core HTTP request method with automatic retry for container startup delays
29
- * Retries both 503 (provisioning) and 500 (startup failure) errors when they're container-related
30
- */
31
- protected async doFetch(
32
- path: string,
33
- options?: RequestInit
34
- ): Promise<Response> {
35
- const startTime = Date.now();
36
- let attempt = 0;
37
-
38
- while (true) {
39
- const response = await this.executeFetch(path, options);
40
-
41
- // Check if this is a retryable container error (both 500 and 503)
42
- const shouldRetry = await this.isRetryableContainerError(response);
43
-
44
- if (shouldRetry) {
45
- const elapsed = Date.now() - startTime;
46
- const remaining = TIMEOUT_MS - elapsed;
47
-
48
- // Check if we have enough time for another attempt
49
- if (remaining > MIN_TIME_FOR_RETRY_MS) {
50
- // Exponential backoff with longer delays for container ops: 3s, 6s, 12s, 24s, 30s
51
- const delay = Math.min(3000 * 2 ** attempt, 30000);
52
-
53
- this.logger.info('Container not ready, retrying', {
54
- status: response.status,
55
- attempt: attempt + 1,
56
- delayMs: delay,
57
- remainingSec: Math.floor(remaining / 1000)
58
- });
59
-
60
- await new Promise((resolve) => setTimeout(resolve, delay));
61
- attempt++;
62
- continue;
63
- }
64
-
65
- // Timeout exhausted
66
- this.logger.error(
67
- 'Container failed to become ready',
68
- new Error(
69
- `Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1000)}s`
70
- )
71
- );
72
- return response;
73
- }
74
-
75
- // Not a retryable error or request succeeded
76
- return response;
77
- }
78
- }
79
-
80
- /**
81
- * Make a POST request with JSON body
82
- */
83
- protected async post<T>(
84
- endpoint: string,
85
- data: unknown,
86
- responseHandler?: ResponseHandler<T>
87
- ): Promise<T> {
88
- const response = await this.doFetch(endpoint, {
89
- method: 'POST',
90
- headers: {
91
- 'Content-Type': 'application/json'
92
- },
93
- body: JSON.stringify(data)
94
- });
95
-
96
- return this.handleResponse(response, responseHandler);
97
- }
98
-
99
- /**
100
- * Make a GET request
101
- */
102
- protected async get<T>(
103
- endpoint: string,
104
- responseHandler?: ResponseHandler<T>
105
- ): Promise<T> {
106
- const response = await this.doFetch(endpoint, {
107
- method: 'GET'
108
- });
109
-
110
- return this.handleResponse(response, responseHandler);
111
- }
112
-
113
- /**
114
- * Make a DELETE request
115
- */
116
- protected async delete<T>(
117
- endpoint: string,
118
- responseHandler?: ResponseHandler<T>
119
- ): Promise<T> {
120
- const response = await this.doFetch(endpoint, {
121
- method: 'DELETE'
122
- });
123
-
124
- return this.handleResponse(response, responseHandler);
125
- }
126
-
127
- /**
128
- * Handle HTTP response with error checking and parsing
129
- */
130
- protected async handleResponse<T>(
131
- response: Response,
132
- customHandler?: ResponseHandler<T>
133
- ): Promise<T> {
134
- if (!response.ok) {
135
- await this.handleErrorResponse(response);
136
- }
137
-
138
- if (customHandler) {
139
- return customHandler(response);
140
- }
141
-
142
- try {
143
- return await response.json();
144
- } catch (error) {
145
- // Handle malformed JSON responses gracefully
146
- const errorResponse: NewErrorResponse = {
147
- code: ErrorCode.INVALID_JSON_RESPONSE,
148
- message: `Invalid JSON response: ${
149
- error instanceof Error ? error.message : 'Unknown parsing error'
150
- }`,
151
- context: {},
152
- httpStatus: response.status,
153
- timestamp: new Date().toISOString()
154
- };
155
- throw createErrorFromResponse(errorResponse);
156
- }
157
- }
158
-
159
- /**
160
- * Handle error responses with consistent error throwing
161
- */
162
- protected async handleErrorResponse(response: Response): Promise<never> {
163
- let errorData: NewErrorResponse;
164
-
165
- try {
166
- errorData = await response.json();
167
- } catch {
168
- // Fallback if response isn't JSON or parsing fails
169
- errorData = {
170
- code: ErrorCode.INTERNAL_ERROR,
171
- message: `HTTP error! status: ${response.status}`,
172
- context: { statusText: response.statusText },
173
- httpStatus: response.status,
174
- timestamp: new Date().toISOString()
175
- };
176
- }
177
-
178
- // Convert ErrorResponse to appropriate Error class
179
- const error = createErrorFromResponse(errorData);
180
-
181
- // Call error callback if provided
182
- this.options.onError?.(errorData.message, undefined);
183
-
184
- throw error;
185
- }
186
-
187
- /**
188
- * Create a streaming response handler for Server-Sent Events
189
- */
190
- protected async handleStreamResponse(
191
- response: Response
192
- ): Promise<ReadableStream<Uint8Array>> {
193
- if (!response.ok) {
194
- await this.handleErrorResponse(response);
195
- }
196
-
197
- if (!response.body) {
198
- throw new Error('No response body for streaming');
199
- }
200
-
201
- return response.body;
202
- }
203
-
204
- /**
205
- * Utility method to log successful operations
206
- */
207
- protected logSuccess(operation: string, details?: string): void {
208
- this.logger.info(
209
- `${operation} completed successfully`,
210
- details ? { details } : undefined
211
- );
212
- }
213
-
214
- /**
215
- * Utility method to log errors intelligently
216
- * Only logs unexpected errors (5xx), not expected errors (4xx)
217
- *
218
- * - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
219
- * - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
220
- */
221
- protected logError(operation: string, error: unknown): void {
222
- // Check if it's a SandboxError with HTTP status
223
- if (error && typeof error === 'object' && 'httpStatus' in error) {
224
- const httpStatus = (error as SandboxError).httpStatus;
225
-
226
- // Only log server errors (5xx), not client errors (4xx)
227
- if (httpStatus >= 500) {
228
- this.logger.error(
229
- `Unexpected error in ${operation}`,
230
- error instanceof Error ? error : new Error(String(error)),
231
- { httpStatus }
232
- );
233
- }
234
- // 4xx errors are expected (validation, not found, etc.) - don't log
235
- } else {
236
- // Non-SandboxError (unexpected) - log it
237
- this.logger.error(
238
- `Error in ${operation}`,
239
- error instanceof Error ? error : new Error(String(error))
240
- );
241
- }
242
- }
243
-
244
- /**
245
- * Check if response indicates a retryable container error
246
- * Uses fail-safe strategy: only retry known transient errors
247
- *
248
- * TODO: This relies on string matching error messages, which is brittle.
249
- * Ideally, the container API should return structured errors with a
250
- * `retryable: boolean` field to avoid coupling to error message format.
251
- *
252
- * @param response - HTTP response to check
253
- * @returns true if error is retryable container error, false otherwise
254
- */
255
- private async isRetryableContainerError(
256
- response: Response
257
- ): Promise<boolean> {
258
- // Only consider 500 and 503 status codes
259
- if (response.status !== 500 && response.status !== 503) {
260
- return false;
261
- }
262
-
263
- try {
264
- const cloned = response.clone();
265
- const text = await cloned.text();
266
- const textLower = text.toLowerCase();
267
-
268
- // Step 1: Check for permanent errors (fail fast)
269
- const permanentErrors = [
270
- 'no such image', // Missing Docker image
271
- 'container already exists', // Name collision
272
- 'malformed containerinspect' // Docker API issue
273
- ];
274
-
275
- if (permanentErrors.some((err) => textLower.includes(err))) {
276
- this.logger.debug('Detected permanent error, not retrying', { text });
277
- return false; // Don't retry
278
- }
279
-
280
- // Step 2: Check for known transient errors (do retry)
281
- const transientErrors = [
282
- // Platform provisioning (503)
283
- 'no container instance available',
284
- 'currently provisioning',
285
-
286
- // Port mapping race conditions (500)
287
- 'container port not found',
288
- 'connection refused: container port',
289
-
290
- // Application startup delays (500)
291
- 'the container is not listening',
292
- 'failed to verify port',
293
- 'container did not start',
294
-
295
- // Network transients (500)
296
- 'network connection lost',
297
- 'container suddenly disconnected',
298
-
299
- // Monitor race conditions (500)
300
- 'monitor failed to find container',
301
-
302
- // General timeouts (500)
303
- 'timed out',
304
- 'timeout'
305
- ];
306
-
307
- const shouldRetry = transientErrors.some((err) =>
308
- textLower.includes(err)
309
- );
310
-
311
- if (!shouldRetry) {
312
- this.logger.debug('Unknown error pattern, not retrying', {
313
- status: response.status,
314
- text: text.substring(0, 200) // Log first 200 chars
315
- });
316
- }
317
-
318
- return shouldRetry;
319
- } catch (error) {
320
- this.logger.error(
321
- 'Error checking if response is retryable',
322
- error instanceof Error ? error : new Error(String(error))
323
- );
324
- // If we can't read response, don't retry (fail fast)
325
- return false;
326
- }
327
- }
328
-
329
- private async executeFetch(
330
- path: string,
331
- options?: RequestInit
332
- ): Promise<Response> {
333
- const url = this.options.stub
334
- ? `http://localhost:${this.options.port}${path}`
335
- : `${this.baseUrl}${path}`;
336
-
337
- try {
338
- if (this.options.stub) {
339
- return await this.options.stub.containerFetch(
340
- url,
341
- options || {},
342
- this.options.port
343
- );
344
- } else {
345
- return await fetch(url, options);
346
- }
347
- } catch (error) {
348
- this.logger.error(
349
- 'HTTP request error',
350
- error instanceof Error ? error : new Error(String(error)),
351
- { method: options?.method || 'GET', url }
352
- );
353
- throw error;
354
- }
355
- }
356
- }
@@ -1,133 +0,0 @@
1
- import type { ExecuteRequest } from '@repo/shared';
2
- import { BaseHttpClient } from './base-client';
3
- import type { BaseApiResponse } from './types';
4
-
5
- /**
6
- * Request interface for command execution
7
- */
8
- export type { ExecuteRequest };
9
-
10
- /**
11
- * Response interface for command execution
12
- */
13
- export interface ExecuteResponse extends BaseApiResponse {
14
- stdout: string;
15
- stderr: string;
16
- exitCode: number;
17
- command: string;
18
- }
19
-
20
- /**
21
- * Client for command execution operations
22
- */
23
- export class CommandClient extends BaseHttpClient {
24
- /**
25
- * Execute a command and return the complete result
26
- * @param command - The command to execute
27
- * @param sessionId - The session ID for this command execution
28
- * @param timeoutMs - Optional timeout in milliseconds (unlimited by default)
29
- * @param env - Optional environment variables for this command
30
- * @param cwd - Optional working directory for this command
31
- */
32
- async execute(
33
- command: string,
34
- sessionId: string,
35
- options?: {
36
- timeoutMs?: number;
37
- env?: Record<string, string>;
38
- cwd?: string;
39
- }
40
- ): Promise<ExecuteResponse> {
41
- try {
42
- const data: ExecuteRequest = {
43
- command,
44
- sessionId,
45
- ...(options?.timeoutMs !== undefined && {
46
- timeoutMs: options.timeoutMs
47
- }),
48
- ...(options?.env !== undefined && { env: options.env }),
49
- ...(options?.cwd !== undefined && { cwd: options.cwd })
50
- };
51
-
52
- const response = await this.post<ExecuteResponse>('/api/execute', data);
53
-
54
- this.logSuccess(
55
- 'Command executed',
56
- `${command}, Success: ${response.success}`
57
- );
58
-
59
- // Call the callback if provided
60
- this.options.onCommandComplete?.(
61
- response.success,
62
- response.exitCode,
63
- response.stdout,
64
- response.stderr,
65
- response.command
66
- );
67
-
68
- return response;
69
- } catch (error) {
70
- this.logError('execute', error);
71
-
72
- // Call error callback if provided
73
- this.options.onError?.(
74
- error instanceof Error ? error.message : String(error),
75
- command
76
- );
77
-
78
- throw error;
79
- }
80
- }
81
-
82
- /**
83
- * Execute a command and return a stream of events
84
- * @param command - The command to execute
85
- * @param sessionId - The session ID for this command execution
86
- * @param options - Optional per-command execution settings
87
- */
88
- async executeStream(
89
- command: string,
90
- sessionId: string,
91
- options?: {
92
- timeoutMs?: number;
93
- env?: Record<string, string>;
94
- cwd?: string;
95
- }
96
- ): Promise<ReadableStream<Uint8Array>> {
97
- try {
98
- const data = {
99
- command,
100
- sessionId,
101
- ...(options?.timeoutMs !== undefined && {
102
- timeoutMs: options.timeoutMs
103
- }),
104
- ...(options?.env !== undefined && { env: options.env }),
105
- ...(options?.cwd !== undefined && { cwd: options.cwd })
106
- };
107
-
108
- const response = await this.doFetch('/api/execute/stream', {
109
- method: 'POST',
110
- headers: {
111
- 'Content-Type': 'application/json'
112
- },
113
- body: JSON.stringify(data)
114
- });
115
-
116
- const stream = await this.handleStreamResponse(response);
117
-
118
- this.logSuccess('Command stream started', command);
119
-
120
- return stream;
121
- } catch (error) {
122
- this.logError('executeStream', error);
123
-
124
- // Call error callback if provided
125
- this.options.onError?.(
126
- error instanceof Error ? error.message : String(error),
127
- command
128
- );
129
-
130
- throw error;
131
- }
132
- }
133
- }