@cloudflare/sandbox 0.0.0-0b4cc05 → 0.0.0-102fc4f

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 (76) hide show
  1. package/CHANGELOG.md +176 -15
  2. package/Dockerfile +88 -71
  3. package/LICENSE +176 -0
  4. package/README.md +10 -5
  5. package/dist/index.d.ts +1953 -9
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3278 -53
  8. package/dist/index.js.map +1 -1
  9. package/package.json +11 -9
  10. package/src/clients/base-client.ts +39 -24
  11. package/src/clients/command-client.ts +8 -8
  12. package/src/clients/file-client.ts +51 -20
  13. package/src/clients/git-client.ts +9 -3
  14. package/src/clients/index.ts +16 -15
  15. package/src/clients/interpreter-client.ts +51 -47
  16. package/src/clients/port-client.ts +10 -10
  17. package/src/clients/process-client.ts +11 -8
  18. package/src/clients/sandbox-client.ts +2 -4
  19. package/src/clients/types.ts +6 -2
  20. package/src/clients/utility-client.ts +67 -5
  21. package/src/errors/adapter.ts +90 -32
  22. package/src/errors/classes.ts +189 -64
  23. package/src/errors/index.ts +9 -5
  24. package/src/file-stream.ts +11 -6
  25. package/src/index.ts +28 -17
  26. package/src/interpreter.ts +50 -41
  27. package/src/request-handler.ts +34 -21
  28. package/src/sandbox.ts +516 -145
  29. package/src/security.ts +21 -6
  30. package/src/sse-parser.ts +4 -3
  31. package/src/version.ts +6 -0
  32. package/startup.sh +1 -1
  33. package/tests/base-client.test.ts +116 -80
  34. package/tests/command-client.test.ts +149 -112
  35. package/tests/file-client.test.ts +373 -185
  36. package/tests/file-stream.test.ts +24 -20
  37. package/tests/get-sandbox.test.ts +149 -0
  38. package/tests/git-client.test.ts +260 -101
  39. package/tests/port-client.test.ts +100 -108
  40. package/tests/process-client.test.ts +204 -179
  41. package/tests/request-handler.test.ts +292 -0
  42. package/tests/sandbox.test.ts +336 -62
  43. package/tests/sse-parser.test.ts +17 -16
  44. package/tests/utility-client.test.ts +129 -56
  45. package/tests/version.test.ts +16 -0
  46. package/tsdown.config.ts +12 -0
  47. package/vitest.config.ts +6 -6
  48. package/dist/chunk-BCJ7SF3Q.js +0 -117
  49. package/dist/chunk-BCJ7SF3Q.js.map +0 -1
  50. package/dist/chunk-BFVUNTP4.js +0 -104
  51. package/dist/chunk-BFVUNTP4.js.map +0 -1
  52. package/dist/chunk-EKSWCBCA.js +0 -86
  53. package/dist/chunk-EKSWCBCA.js.map +0 -1
  54. package/dist/chunk-U2M5GSMU.js +0 -2220
  55. package/dist/chunk-U2M5GSMU.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 -12
  66. package/dist/request-handler.js.map +0 -1
  67. package/dist/sandbox-Cyuj5F-M.d.ts +0 -579
  68. package/dist/sandbox.d.ts +0 -4
  69. package/dist/sandbox.js +0 -12
  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
@@ -1,17 +1,14 @@
1
- import type { Logger } from "@repo/shared";
2
- import { createNoOpLogger } from "@repo/shared";
1
+ import type { Logger } from '@repo/shared';
2
+ import { createNoOpLogger } from '@repo/shared';
3
3
  import { getHttpStatus } from '@repo/shared/errors';
4
4
  import type { ErrorResponse as NewErrorResponse } from '../errors';
5
5
  import { createErrorFromResponse, ErrorCode } from '../errors';
6
6
  import type { SandboxError } from '../errors/classes';
7
- import type {
8
- HttpClientOptions,
9
- ResponseHandler
10
- } from './types';
7
+ import type { HttpClientOptions, ResponseHandler } from './types';
11
8
 
12
9
  // Container provisioning retry configuration
13
- const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
14
- const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
10
+ const TIMEOUT_MS = 60_000; // 60 seconds total timeout budget
11
+ const MIN_TIME_FOR_RETRY_MS = 10_000; // Need at least 10s remaining to retry (8s Container + 2s delay)
15
12
 
16
13
  /**
17
14
  * Abstract base class providing common HTTP functionality for all domain clients
@@ -42,7 +39,8 @@ export abstract class BaseHttpClient {
42
39
 
43
40
  // Only retry container provisioning 503s, not user app 503s
44
41
  if (response.status === 503) {
45
- const isContainerProvisioning = await this.isContainerProvisioningError(response);
42
+ const isContainerProvisioning =
43
+ await this.isContainerProvisioningError(response);
46
44
 
47
45
  if (isContainerProvisioning) {
48
46
  const elapsed = Date.now() - startTime;
@@ -60,13 +58,16 @@ export abstract class BaseHttpClient {
60
58
  remainingSec: Math.floor(remaining / 1000)
61
59
  });
62
60
 
63
- await new Promise(resolve => setTimeout(resolve, delay));
61
+ await new Promise((resolve) => setTimeout(resolve, delay));
64
62
  attempt++;
65
63
  continue;
66
64
  } else {
67
65
  // Exhausted retries - log error and return response
68
66
  // Let existing error handling convert to proper error
69
- this.logger.error('Container failed to provision after multiple attempts', new Error(`Failed after ${attempt + 1} attempts over 60s`));
67
+ this.logger.error(
68
+ 'Container failed to provision after multiple attempts',
69
+ new Error(`Failed after ${attempt + 1} attempts over 60s`)
70
+ );
70
71
  return response;
71
72
  }
72
73
  }
@@ -87,9 +88,9 @@ export abstract class BaseHttpClient {
87
88
  const response = await this.doFetch(endpoint, {
88
89
  method: 'POST',
89
90
  headers: {
90
- 'Content-Type': 'application/json',
91
+ 'Content-Type': 'application/json'
91
92
  },
92
- body: JSON.stringify(data),
93
+ body: JSON.stringify(data)
93
94
  });
94
95
 
95
96
  return this.handleResponse(response, responseHandler);
@@ -103,7 +104,7 @@ export abstract class BaseHttpClient {
103
104
  responseHandler?: ResponseHandler<T>
104
105
  ): Promise<T> {
105
106
  const response = await this.doFetch(endpoint, {
106
- method: 'GET',
107
+ method: 'GET'
107
108
  });
108
109
 
109
110
  return this.handleResponse(response, responseHandler);
@@ -117,13 +118,12 @@ export abstract class BaseHttpClient {
117
118
  responseHandler?: ResponseHandler<T>
118
119
  ): Promise<T> {
119
120
  const response = await this.doFetch(endpoint, {
120
- method: 'DELETE',
121
+ method: 'DELETE'
121
122
  });
122
123
 
123
124
  return this.handleResponse(response, responseHandler);
124
125
  }
125
126
 
126
-
127
127
  /**
128
128
  * Handle HTTP response with error checking and parsing
129
129
  */
@@ -145,7 +145,9 @@ export abstract class BaseHttpClient {
145
145
  // Handle malformed JSON responses gracefully
146
146
  const errorResponse: NewErrorResponse = {
147
147
  code: ErrorCode.INVALID_JSON_RESPONSE,
148
- message: `Invalid JSON response: ${error instanceof Error ? error.message : 'Unknown parsing error'}`,
148
+ message: `Invalid JSON response: ${
149
+ error instanceof Error ? error.message : 'Unknown parsing error'
150
+ }`,
149
151
  context: {},
150
152
  httpStatus: response.status,
151
153
  timestamp: new Date().toISOString()
@@ -182,8 +184,6 @@ export abstract class BaseHttpClient {
182
184
  throw error;
183
185
  }
184
186
 
185
-
186
-
187
187
  /**
188
188
  * Create a streaming response handler for Server-Sent Events
189
189
  */
@@ -205,7 +205,10 @@ export abstract class BaseHttpClient {
205
205
  * Utility method to log successful operations
206
206
  */
207
207
  protected logSuccess(operation: string, details?: string): void {
208
- this.logger.info(`${operation} completed successfully`, details ? { details } : undefined);
208
+ this.logger.info(
209
+ `${operation} completed successfully`,
210
+ details ? { details } : undefined
211
+ );
209
212
  }
210
213
 
211
214
  /**
@@ -242,7 +245,9 @@ export abstract class BaseHttpClient {
242
245
  * Check if 503 response is from container provisioning (retryable)
243
246
  * vs user application (not retryable)
244
247
  */
245
- private async isContainerProvisioningError(response: Response): Promise<boolean> {
248
+ private async isContainerProvisioningError(
249
+ response: Response
250
+ ): Promise<boolean> {
246
251
  try {
247
252
  // Clone response so we don't consume the original body
248
253
  const cloned = response.clone();
@@ -251,13 +256,19 @@ export abstract class BaseHttpClient {
251
256
  // Container package returns specific message for provisioning errors
252
257
  return text.includes('There is no Container instance available');
253
258
  } catch (error) {
254
- this.logger.error('Error checking response body', error instanceof Error ? error : new Error(String(error)));
259
+ this.logger.error(
260
+ 'Error checking response body',
261
+ error instanceof Error ? error : new Error(String(error))
262
+ );
255
263
  // If we can't read the body, don't retry to be safe
256
264
  return false;
257
265
  }
258
266
  }
259
267
 
260
- private async executeFetch(path: string, options?: RequestInit): Promise<Response> {
268
+ private async executeFetch(
269
+ path: string,
270
+ options?: RequestInit
271
+ ): Promise<Response> {
261
272
  const url = this.options.stub
262
273
  ? `http://localhost:${this.options.port}${path}`
263
274
  : `${this.baseUrl}${path}`;
@@ -273,7 +284,11 @@ export abstract class BaseHttpClient {
273
284
  return await fetch(url, options);
274
285
  }
275
286
  } catch (error) {
276
- this.logger.error('HTTP request error', error instanceof Error ? error : new Error(String(error)), { method: options?.method || 'GET', url });
287
+ this.logger.error(
288
+ 'HTTP request error',
289
+ error instanceof Error ? error : new Error(String(error)),
290
+ { method: options?.method || 'GET', url }
291
+ );
277
292
  throw error;
278
293
  }
279
294
  }
@@ -1,5 +1,9 @@
1
1
  import { BaseHttpClient } from './base-client';
2
- import type { BaseApiResponse, HttpClientOptions, SessionRequest } from './types';
2
+ import type {
3
+ BaseApiResponse,
4
+ HttpClientOptions,
5
+ SessionRequest
6
+ } from './types';
3
7
 
4
8
  /**
5
9
  * Request interface for command execution
@@ -23,7 +27,6 @@ export interface ExecuteResponse extends BaseApiResponse {
23
27
  * Client for command execution operations
24
28
  */
25
29
  export class CommandClient extends BaseHttpClient {
26
-
27
30
  /**
28
31
  * Execute a command and return the complete result
29
32
  * @param command - The command to execute
@@ -42,10 +45,7 @@ export class CommandClient extends BaseHttpClient {
42
45
  ...(timeoutMs !== undefined && { timeoutMs })
43
46
  };
44
47
 
45
- const response = await this.post<ExecuteResponse>(
46
- '/api/execute',
47
- data
48
- );
48
+ const response = await this.post<ExecuteResponse>('/api/execute', data);
49
49
 
50
50
  this.logSuccess(
51
51
  'Command executed',
@@ -90,9 +90,9 @@ export class CommandClient extends BaseHttpClient {
90
90
  const response = await this.doFetch('/api/execute/stream', {
91
91
  method: 'POST',
92
92
  headers: {
93
- 'Content-Type': 'application/json',
93
+ 'Content-Type': 'application/json'
94
94
  },
95
- body: JSON.stringify(data),
95
+ body: JSON.stringify(data)
96
96
  });
97
97
 
98
98
  const stream = await this.handleStreamResponse(response);
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  DeleteFileResult,
3
+ FileExistsResult,
3
4
  ListFilesOptions,
4
5
  ListFilesResult,
5
6
  MkdirResult,
@@ -48,7 +49,6 @@ export interface FileOperationRequest extends SessionRequest {
48
49
  * Client for file system operations
49
50
  */
50
51
  export class FileClient extends BaseHttpClient {
51
-
52
52
  /**
53
53
  * Create a directory
54
54
  * @param path - Directory path to create
@@ -64,12 +64,15 @@ export class FileClient extends BaseHttpClient {
64
64
  const data = {
65
65
  path,
66
66
  sessionId,
67
- recursive: options?.recursive ?? false,
67
+ recursive: options?.recursive ?? false
68
68
  };
69
69
 
70
70
  const response = await this.post<MkdirResult>('/api/mkdir', data);
71
-
72
- this.logSuccess('Directory created', `${path} (recursive: ${data.recursive})`);
71
+
72
+ this.logSuccess(
73
+ 'Directory created',
74
+ `${path} (recursive: ${data.recursive})`
75
+ );
73
76
  return response;
74
77
  } catch (error) {
75
78
  this.logError('mkdir', error);
@@ -95,11 +98,11 @@ export class FileClient extends BaseHttpClient {
95
98
  path,
96
99
  content,
97
100
  sessionId,
98
- encoding: options?.encoding ?? 'utf8',
101
+ encoding: options?.encoding
99
102
  };
100
103
 
101
104
  const response = await this.post<WriteFileResult>('/api/write', data);
102
-
105
+
103
106
  this.logSuccess('File written', `${path} (${content.length} chars)`);
104
107
  return response;
105
108
  } catch (error) {
@@ -123,12 +126,15 @@ export class FileClient extends BaseHttpClient {
123
126
  const data = {
124
127
  path,
125
128
  sessionId,
126
- encoding: options?.encoding ?? 'utf8',
129
+ encoding: options?.encoding
127
130
  };
128
131
 
129
132
  const response = await this.post<ReadFileResult>('/api/read', data);
130
133
 
131
- this.logSuccess('File read', `${path} (${response.content.length} chars)`);
134
+ this.logSuccess(
135
+ 'File read',
136
+ `${path} (${response.content.length} chars)`
137
+ );
132
138
  return response;
133
139
  } catch (error) {
134
140
  this.logError('readFile', error);
@@ -149,15 +155,15 @@ export class FileClient extends BaseHttpClient {
149
155
  try {
150
156
  const data = {
151
157
  path,
152
- sessionId,
158
+ sessionId
153
159
  };
154
160
 
155
161
  const response = await this.doFetch('/api/read/stream', {
156
162
  method: 'POST',
157
163
  headers: {
158
- 'Content-Type': 'application/json',
164
+ 'Content-Type': 'application/json'
159
165
  },
160
- body: JSON.stringify(data),
166
+ body: JSON.stringify(data)
161
167
  });
162
168
 
163
169
  const stream = await this.handleStreamResponse(response);
@@ -174,15 +180,12 @@ export class FileClient extends BaseHttpClient {
174
180
  * @param path - File path to delete
175
181
  * @param sessionId - The session ID for this operation
176
182
  */
177
- async deleteFile(
178
- path: string,
179
- sessionId: string
180
- ): Promise<DeleteFileResult> {
183
+ async deleteFile(path: string, sessionId: string): Promise<DeleteFileResult> {
181
184
  try {
182
185
  const data = { path, sessionId };
183
186
 
184
187
  const response = await this.post<DeleteFileResult>('/api/delete', data);
185
-
188
+
186
189
  this.logSuccess('File deleted', path);
187
190
  return response;
188
191
  } catch (error) {
@@ -206,7 +209,7 @@ export class FileClient extends BaseHttpClient {
206
209
  const data = { oldPath: path, newPath, sessionId };
207
210
 
208
211
  const response = await this.post<RenameFileResult>('/api/rename', data);
209
-
212
+
210
213
  this.logSuccess('File renamed', `${path} -> ${newPath}`);
211
214
  return response;
212
215
  } catch (error) {
@@ -254,10 +257,13 @@ export class FileClient extends BaseHttpClient {
254
257
  const data = {
255
258
  path,
256
259
  sessionId,
257
- options: options || {},
260
+ options: options || {}
258
261
  };
259
262
 
260
- const response = await this.post<ListFilesResult>('/api/list-files', data);
263
+ const response = await this.post<ListFilesResult>(
264
+ '/api/list-files',
265
+ data
266
+ );
261
267
 
262
268
  this.logSuccess('Files listed', `${path} (${response.count} files)`);
263
269
  return response;
@@ -266,4 +272,29 @@ export class FileClient extends BaseHttpClient {
266
272
  throw error;
267
273
  }
268
274
  }
269
- }
275
+
276
+ /**
277
+ * Check if a file or directory exists
278
+ * @param path - Path to check
279
+ * @param sessionId - The session ID for this operation
280
+ */
281
+ async exists(path: string, sessionId: string): Promise<FileExistsResult> {
282
+ try {
283
+ const data = {
284
+ path,
285
+ sessionId
286
+ };
287
+
288
+ const response = await this.post<FileExistsResult>('/api/exists', data);
289
+
290
+ this.logSuccess(
291
+ 'Path existence checked',
292
+ `${path} (exists: ${response.exists})`
293
+ );
294
+ return response;
295
+ } catch (error) {
296
+ this.logError('exists', error);
297
+ throw error;
298
+ }
299
+ }
300
+ }
@@ -1,4 +1,5 @@
1
1
  import type { GitCheckoutResult } from '@repo/shared';
2
+ import { GitLogger } from '@repo/shared';
2
3
  import { BaseHttpClient } from './base-client';
3
4
  import type { HttpClientOptions, SessionRequest } from './types';
4
5
 
@@ -18,6 +19,11 @@ export interface GitCheckoutRequest extends SessionRequest {
18
19
  * Client for Git repository operations
19
20
  */
20
21
  export class GitClient extends BaseHttpClient {
22
+ constructor(options: HttpClientOptions = {}) {
23
+ super(options);
24
+ // Wrap logger with GitLogger to auto-redact credentials
25
+ this.logger = new GitLogger(this.logger);
26
+ }
21
27
 
22
28
  /**
23
29
  * Clone a Git repository
@@ -45,7 +51,7 @@ export class GitClient extends BaseHttpClient {
45
51
  const data: GitCheckoutRequest = {
46
52
  repoUrl,
47
53
  sessionId,
48
- targetDir,
54
+ targetDir
49
55
  };
50
56
 
51
57
  // Only include branch if explicitly specified
@@ -79,7 +85,7 @@ export class GitClient extends BaseHttpClient {
79
85
  const url = new URL(repoUrl);
80
86
  const pathParts = url.pathname.split('/');
81
87
  const repoName = pathParts[pathParts.length - 1];
82
-
88
+
83
89
  // Remove .git extension if present
84
90
  return repoName.replace(/\.git$/, '');
85
91
  } catch {
@@ -89,4 +95,4 @@ export class GitClient extends BaseHttpClient {
89
95
  return repoName.replace(/\.git$/, '') || 'repo';
90
96
  }
91
97
  }
92
- }
98
+ }
@@ -1,11 +1,7 @@
1
1
  // Main client exports
2
2
 
3
-
4
3
  // Command client types
5
- export type {
6
- ExecuteRequest,
7
- ExecuteResponse,
8
- } from './command-client';
4
+ export type { ExecuteRequest, ExecuteResponse } from './command-client';
9
5
 
10
6
  // Domain-specific clients
11
7
  export { CommandClient } from './command-client';
@@ -14,23 +10,23 @@ export type {
14
10
  FileOperationRequest,
15
11
  MkdirRequest,
16
12
  ReadFileRequest,
17
- WriteFileRequest,
13
+ WriteFileRequest
18
14
  } from './file-client';
19
15
  export { FileClient } from './file-client';
20
16
  // Git client types
21
- export type {
22
- GitCheckoutRequest,
23
- GitCheckoutResult,
24
- } from './git-client';
17
+ export type { GitCheckoutRequest, GitCheckoutResult } from './git-client';
25
18
  export { GitClient } from './git-client';
26
- export { type ExecutionCallbacks, InterpreterClient } from './interpreter-client';
19
+ export {
20
+ type ExecutionCallbacks,
21
+ InterpreterClient
22
+ } from './interpreter-client';
27
23
  // Port client types
28
24
  export type {
29
25
  ExposePortRequest,
30
26
  PortCloseResult,
31
27
  PortExposeResult,
32
28
  PortListResult,
33
- UnexposePortRequest,
29
+ UnexposePortRequest
34
30
  } from './port-client';
35
31
  export { PortClient } from './port-client';
36
32
  // Process client types
@@ -41,7 +37,7 @@ export type {
41
37
  ProcessListResult,
42
38
  ProcessLogsResult,
43
39
  ProcessStartResult,
44
- StartProcessRequest,
40
+ StartProcessRequest
45
41
  } from './process-client';
46
42
  export { ProcessClient } from './process-client';
47
43
  export { SandboxClient } from './sandbox-client';
@@ -53,11 +49,16 @@ export type {
53
49
  HttpClientOptions,
54
50
  RequestConfig,
55
51
  ResponseHandler,
56
- SessionRequest,
52
+ SessionRequest
57
53
  } from './types';
58
54
  // Utility client types
59
55
  export type {
60
56
  CommandsResponse,
57
+ CreateSessionRequest,
58
+ CreateSessionResponse,
59
+ DeleteSessionRequest,
60
+ DeleteSessionResponse,
61
61
  PingResponse,
62
+ VersionResponse
62
63
  } from './utility-client';
63
- export { UtilityClient } from './utility-client';
64
+ export { UtilityClient } from './utility-client';