@cloudflare/sandbox 0.4.11 → 0.4.14
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.
- package/.turbo/turbo-build.log +13 -47
- package/CHANGELOG.md +44 -16
- package/Dockerfile +15 -9
- package/README.md +0 -1
- package/dist/index.d.ts +1889 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3144 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +31 -26
- package/src/clients/git-client.ts +3 -4
- package/src/clients/index.ts +12 -16
- package/src/clients/interpreter-client.ts +51 -47
- package/src/clients/port-client.ts +10 -10
- package/src/clients/process-client.ts +11 -8
- package/src/clients/sandbox-client.ts +2 -4
- package/src/clients/types.ts +6 -2
- package/src/clients/utility-client.ts +10 -6
- package/src/errors/adapter.ts +90 -32
- package/src/errors/classes.ts +189 -64
- package/src/errors/index.ts +9 -5
- package/src/file-stream.ts +11 -6
- package/src/index.ts +22 -15
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +24 -21
- package/src/sandbox.ts +370 -148
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +309 -197
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +45 -6
- package/tests/git-client.test.ts +188 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +117 -65
- package/tests/sandbox.test.ts +220 -68
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +79 -72
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-BFVUNTP4.js +0 -104
- package/dist/chunk-BFVUNTP4.js.map +0 -1
- package/dist/chunk-EKSWCBCA.js +0 -86
- package/dist/chunk-EKSWCBCA.js.map +0 -1
- package/dist/chunk-FE4PJSRB.js +0 -7
- package/dist/chunk-FE4PJSRB.js.map +0 -1
- package/dist/chunk-JXZMAU2C.js +0 -559
- package/dist/chunk-JXZMAU2C.js.map +0 -1
- package/dist/chunk-SVWLTRHD.js +0 -2456
- package/dist/chunk-SVWLTRHD.js.map +0 -1
- package/dist/chunk-Z532A7QC.js +0 -78
- package/dist/chunk-Z532A7QC.js.map +0 -1
- package/dist/file-stream.d.ts +0 -43
- package/dist/file-stream.js +0 -9
- package/dist/file-stream.js.map +0 -1
- package/dist/interpreter.d.ts +0 -33
- package/dist/interpreter.js +0 -8
- package/dist/interpreter.js.map +0 -1
- package/dist/request-handler.d.ts +0 -18
- package/dist/request-handler.js +0 -13
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-DWQVgVTY.d.ts +0 -603
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -13
- package/dist/sandbox.js.map +0 -1
- package/dist/security.d.ts +0 -31
- package/dist/security.js +0 -13
- package/dist/security.js.map +0 -1
- package/dist/sse-parser.d.ts +0 -28
- package/dist/sse-parser.js +0 -11
- package/dist/sse-parser.js.map +0 -1
- package/dist/version.d.ts +0 -8
- package/dist/version.js +0 -7
- package/dist/version.js.map +0 -1
package/src/security.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export class SecurityError extends Error {
|
|
13
|
-
constructor(
|
|
13
|
+
constructor(
|
|
14
|
+
message: string,
|
|
15
|
+
public readonly code?: string
|
|
16
|
+
) {
|
|
14
17
|
super(message);
|
|
15
18
|
this.name = 'SecurityError';
|
|
16
19
|
}
|
|
@@ -34,7 +37,7 @@ export function validatePort(port: number): boolean {
|
|
|
34
37
|
// Exclude ports reserved by our system
|
|
35
38
|
const reservedPorts = [
|
|
36
39
|
3000, // Control plane port
|
|
37
|
-
8787
|
|
40
|
+
8787 // Common wrangler dev port
|
|
38
41
|
];
|
|
39
42
|
|
|
40
43
|
if (reservedPorts.includes(port)) {
|
|
@@ -67,8 +70,13 @@ export function sanitizeSandboxId(id: string): string {
|
|
|
67
70
|
|
|
68
71
|
// Prevent reserved names that cause technical conflicts
|
|
69
72
|
const reservedNames = [
|
|
70
|
-
'www',
|
|
71
|
-
'
|
|
73
|
+
'www',
|
|
74
|
+
'api',
|
|
75
|
+
'admin',
|
|
76
|
+
'root',
|
|
77
|
+
'system',
|
|
78
|
+
'cloudflare',
|
|
79
|
+
'workers'
|
|
72
80
|
];
|
|
73
81
|
|
|
74
82
|
const lowerCaseId = id.toLowerCase();
|
|
@@ -82,7 +90,6 @@ export function sanitizeSandboxId(id: string): string {
|
|
|
82
90
|
return id;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
|
|
86
93
|
/**
|
|
87
94
|
* Validates language for code interpreter
|
|
88
95
|
* Only allows supported languages
|
|
@@ -92,7 +99,15 @@ export function validateLanguage(language: string | undefined): void {
|
|
|
92
99
|
return; // undefined is valid, will default to python
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
const supportedLanguages = [
|
|
102
|
+
const supportedLanguages = [
|
|
103
|
+
'python',
|
|
104
|
+
'python3',
|
|
105
|
+
'javascript',
|
|
106
|
+
'js',
|
|
107
|
+
'node',
|
|
108
|
+
'typescript',
|
|
109
|
+
'ts'
|
|
110
|
+
];
|
|
96
111
|
const normalized = language.toLowerCase();
|
|
97
112
|
|
|
98
113
|
if (!supportedLanguages.includes(normalized)) {
|
package/src/sse-parser.ts
CHANGED
|
@@ -76,7 +76,6 @@ export async function* parseSSEStream<T>(
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
79
|
/**
|
|
81
80
|
* Helper to convert a Response with SSE stream directly to AsyncIterable
|
|
82
81
|
* @param response - Response object with SSE stream
|
|
@@ -87,7 +86,9 @@ export async function* responseToAsyncIterable<T>(
|
|
|
87
86
|
signal?: AbortSignal
|
|
88
87
|
): AsyncIterable<T> {
|
|
89
88
|
if (!response.ok) {
|
|
90
|
-
throw new Error(
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Response not ok: ${response.status} ${response.statusText}`
|
|
91
|
+
);
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
if (!response.body) {
|
|
@@ -140,4 +141,4 @@ export function asyncIterableToSSEStream<T>(
|
|
|
140
141
|
// Handle stream cancellation
|
|
141
142
|
}
|
|
142
143
|
});
|
|
143
|
-
}
|
|
144
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -31,11 +31,14 @@ class TestHttpClient extends BaseHttpClient {
|
|
|
31
31
|
super({
|
|
32
32
|
baseUrl: 'http://test.com',
|
|
33
33
|
port: 3000,
|
|
34
|
-
...options
|
|
34
|
+
...options
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
public async testRequest<T = BaseApiResponse>(
|
|
38
|
+
public async testRequest<T = BaseApiResponse>(
|
|
39
|
+
endpoint: string,
|
|
40
|
+
data?: Record<string, unknown>
|
|
41
|
+
): Promise<T> {
|
|
39
42
|
if (data) {
|
|
40
43
|
return this.post<T>(endpoint, data);
|
|
41
44
|
}
|
|
@@ -48,10 +51,9 @@ class TestHttpClient extends BaseHttpClient {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
public async testErrorHandling(errorResponse: ErrorResponse) {
|
|
51
|
-
const response = new Response(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
54
|
+
const response = new Response(JSON.stringify(errorResponse), {
|
|
55
|
+
status: errorResponse.httpStatus || 400
|
|
56
|
+
});
|
|
55
57
|
return this.handleErrorResponse(response);
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -63,15 +65,15 @@ describe('BaseHttpClient', () => {
|
|
|
63
65
|
|
|
64
66
|
beforeEach(() => {
|
|
65
67
|
vi.clearAllMocks();
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
mockFetch = vi.fn();
|
|
68
70
|
global.fetch = mockFetch as unknown as typeof fetch;
|
|
69
71
|
onError = vi.fn();
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
client = new TestHttpClient({
|
|
72
74
|
baseUrl: 'http://test.com',
|
|
73
75
|
port: 3000,
|
|
74
|
-
onError
|
|
76
|
+
onError
|
|
75
77
|
});
|
|
76
78
|
});
|
|
77
79
|
|
|
@@ -81,10 +83,12 @@ describe('BaseHttpClient', () => {
|
|
|
81
83
|
|
|
82
84
|
describe('core request functionality', () => {
|
|
83
85
|
it('should handle successful API requests', async () => {
|
|
84
|
-
mockFetch.mockResolvedValue(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
mockFetch.mockResolvedValue(
|
|
87
|
+
new Response(
|
|
88
|
+
JSON.stringify({ success: true, data: 'operation completed' }),
|
|
89
|
+
{ status: 200 }
|
|
90
|
+
)
|
|
91
|
+
);
|
|
88
92
|
|
|
89
93
|
const result = await client.testRequest<TestDataResponse>('/api/test');
|
|
90
94
|
|
|
@@ -94,12 +98,16 @@ describe('BaseHttpClient', () => {
|
|
|
94
98
|
|
|
95
99
|
it('should handle POST requests with data', async () => {
|
|
96
100
|
const requestData = { action: 'create', name: 'test-resource' };
|
|
97
|
-
mockFetch.mockResolvedValue(
|
|
98
|
-
JSON.stringify({ success: true, id: 'resource-123' }),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
mockFetch.mockResolvedValue(
|
|
102
|
+
new Response(JSON.stringify({ success: true, id: 'resource-123' }), {
|
|
103
|
+
status: 201
|
|
104
|
+
})
|
|
105
|
+
);
|
|
101
106
|
|
|
102
|
-
const result = await client.testRequest<TestResourceResponse>(
|
|
107
|
+
const result = await client.testRequest<TestResourceResponse>(
|
|
108
|
+
'/api/create',
|
|
109
|
+
requestData
|
|
110
|
+
);
|
|
103
111
|
|
|
104
112
|
expect(result.success).toBe(true);
|
|
105
113
|
expect(result.id).toBe('resource-123');
|
|
@@ -123,7 +131,7 @@ describe('BaseHttpClient', () => {
|
|
|
123
131
|
httpStatus: 404,
|
|
124
132
|
timestamp: new Date().toISOString()
|
|
125
133
|
},
|
|
126
|
-
expectedError: FileNotFoundError
|
|
134
|
+
expectedError: FileNotFoundError
|
|
127
135
|
},
|
|
128
136
|
{
|
|
129
137
|
containerError: {
|
|
@@ -133,7 +141,7 @@ describe('BaseHttpClient', () => {
|
|
|
133
141
|
httpStatus: 403,
|
|
134
142
|
timestamp: new Date().toISOString()
|
|
135
143
|
},
|
|
136
|
-
expectedError: PermissionDeniedError
|
|
144
|
+
expectedError: PermissionDeniedError
|
|
137
145
|
},
|
|
138
146
|
{
|
|
139
147
|
containerError: {
|
|
@@ -143,7 +151,7 @@ describe('BaseHttpClient', () => {
|
|
|
143
151
|
httpStatus: 400,
|
|
144
152
|
timestamp: new Date().toISOString()
|
|
145
153
|
},
|
|
146
|
-
expectedError: CommandError
|
|
154
|
+
expectedError: CommandError
|
|
147
155
|
},
|
|
148
156
|
{
|
|
149
157
|
containerError: {
|
|
@@ -153,7 +161,7 @@ describe('BaseHttpClient', () => {
|
|
|
153
161
|
httpStatus: 500,
|
|
154
162
|
timestamp: new Date().toISOString()
|
|
155
163
|
},
|
|
156
|
-
expectedError: FileSystemError
|
|
164
|
+
expectedError: FileSystemError
|
|
157
165
|
},
|
|
158
166
|
{
|
|
159
167
|
containerError: {
|
|
@@ -163,49 +171,56 @@ describe('BaseHttpClient', () => {
|
|
|
163
171
|
httpStatus: 500,
|
|
164
172
|
timestamp: new Date().toISOString()
|
|
165
173
|
},
|
|
166
|
-
expectedError: SandboxError
|
|
174
|
+
expectedError: SandboxError
|
|
167
175
|
}
|
|
168
176
|
];
|
|
169
177
|
|
|
170
178
|
for (const test of errorMappingTests) {
|
|
171
|
-
await expect(
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
await expect(
|
|
180
|
+
client.testErrorHandling(test.containerError as ErrorResponse)
|
|
181
|
+
).rejects.toThrow(test.expectedError);
|
|
182
|
+
|
|
183
|
+
expect(onError).toHaveBeenCalledWith(
|
|
184
|
+
test.containerError.message,
|
|
185
|
+
undefined
|
|
186
|
+
);
|
|
175
187
|
}
|
|
176
188
|
});
|
|
177
189
|
|
|
178
190
|
it('should handle malformed error responses', async () => {
|
|
179
|
-
mockFetch.mockResolvedValue(
|
|
180
|
-
'invalid json {',
|
|
181
|
-
|
|
182
|
-
));
|
|
191
|
+
mockFetch.mockResolvedValue(
|
|
192
|
+
new Response('invalid json {', { status: 500 })
|
|
193
|
+
);
|
|
183
194
|
|
|
184
|
-
await expect(client.testRequest('/api/test'))
|
|
185
|
-
|
|
195
|
+
await expect(client.testRequest('/api/test')).rejects.toThrow(
|
|
196
|
+
SandboxError
|
|
197
|
+
);
|
|
186
198
|
});
|
|
187
199
|
|
|
188
200
|
it('should handle network failures', async () => {
|
|
189
201
|
mockFetch.mockRejectedValue(new Error('Network connection timeout'));
|
|
190
202
|
|
|
191
|
-
await expect(client.testRequest('/api/test'))
|
|
192
|
-
|
|
203
|
+
await expect(client.testRequest('/api/test')).rejects.toThrow(
|
|
204
|
+
'Network connection timeout'
|
|
205
|
+
);
|
|
193
206
|
});
|
|
194
207
|
|
|
195
208
|
it('should handle server unavailable scenarios', async () => {
|
|
196
|
-
mockFetch.mockResolvedValue(
|
|
197
|
-
'Service Unavailable',
|
|
198
|
-
|
|
199
|
-
));
|
|
209
|
+
mockFetch.mockResolvedValue(
|
|
210
|
+
new Response('Service Unavailable', { status: 503 })
|
|
211
|
+
);
|
|
200
212
|
|
|
201
|
-
await expect(client.testRequest('/api/test'))
|
|
202
|
-
|
|
213
|
+
await expect(client.testRequest('/api/test')).rejects.toThrow(
|
|
214
|
+
SandboxError
|
|
215
|
+
);
|
|
203
216
|
|
|
204
|
-
expect(onError).toHaveBeenCalledWith(
|
|
217
|
+
expect(onError).toHaveBeenCalledWith(
|
|
218
|
+
'HTTP error! status: 503',
|
|
219
|
+
undefined
|
|
220
|
+
);
|
|
205
221
|
});
|
|
206
222
|
});
|
|
207
223
|
|
|
208
|
-
|
|
209
224
|
describe('streaming functionality', () => {
|
|
210
225
|
it('should handle streaming responses', async () => {
|
|
211
226
|
const streamData = 'data: {"type":"output","content":"stream data"}\n\n';
|
|
@@ -216,10 +231,12 @@ describe('BaseHttpClient', () => {
|
|
|
216
231
|
}
|
|
217
232
|
});
|
|
218
233
|
|
|
219
|
-
mockFetch.mockResolvedValue(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
mockFetch.mockResolvedValue(
|
|
235
|
+
new Response(mockStream, {
|
|
236
|
+
status: 200,
|
|
237
|
+
headers: { 'Content-Type': 'text/event-stream' }
|
|
238
|
+
})
|
|
239
|
+
);
|
|
223
240
|
|
|
224
241
|
const stream = await client.testStreamRequest('/api/stream');
|
|
225
242
|
|
|
@@ -236,41 +253,52 @@ describe('BaseHttpClient', () => {
|
|
|
236
253
|
});
|
|
237
254
|
|
|
238
255
|
it('should handle streaming errors', async () => {
|
|
239
|
-
mockFetch.mockResolvedValue(
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
mockFetch.mockResolvedValue(
|
|
257
|
+
new Response(
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
error: 'Stream initialization failed',
|
|
260
|
+
code: 'STREAM_ERROR'
|
|
261
|
+
}),
|
|
262
|
+
{ status: 400 }
|
|
263
|
+
)
|
|
264
|
+
);
|
|
243
265
|
|
|
244
|
-
await expect(client.testStreamRequest('/api/bad-stream'))
|
|
245
|
-
|
|
266
|
+
await expect(client.testStreamRequest('/api/bad-stream')).rejects.toThrow(
|
|
267
|
+
SandboxError
|
|
268
|
+
);
|
|
246
269
|
});
|
|
247
270
|
|
|
248
271
|
it('should handle missing stream body', async () => {
|
|
249
|
-
mockFetch.mockResolvedValue(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
272
|
+
mockFetch.mockResolvedValue(
|
|
273
|
+
new Response(null, {
|
|
274
|
+
status: 200,
|
|
275
|
+
headers: { 'Content-Type': 'text/event-stream' }
|
|
276
|
+
})
|
|
277
|
+
);
|
|
253
278
|
|
|
254
|
-
await expect(
|
|
255
|
-
.
|
|
279
|
+
await expect(
|
|
280
|
+
client.testStreamRequest('/api/empty-stream')
|
|
281
|
+
).rejects.toThrow('No response body for streaming');
|
|
256
282
|
});
|
|
257
283
|
});
|
|
258
284
|
|
|
259
285
|
describe('stub integration', () => {
|
|
260
286
|
it('should use stub when provided instead of fetch', async () => {
|
|
261
|
-
const stubFetch = vi.fn().mockResolvedValue(
|
|
262
|
-
JSON.stringify({ success: true, source: 'stub' }),
|
|
263
|
-
|
|
264
|
-
|
|
287
|
+
const stubFetch = vi.fn().mockResolvedValue(
|
|
288
|
+
new Response(JSON.stringify({ success: true, source: 'stub' }), {
|
|
289
|
+
status: 200
|
|
290
|
+
})
|
|
291
|
+
);
|
|
265
292
|
|
|
266
293
|
const stub = { containerFetch: stubFetch };
|
|
267
294
|
const stubClient = new TestHttpClient({
|
|
268
295
|
baseUrl: 'http://test.com',
|
|
269
296
|
port: 3000,
|
|
270
|
-
stub
|
|
297
|
+
stub
|
|
271
298
|
});
|
|
272
299
|
|
|
273
|
-
const result =
|
|
300
|
+
const result =
|
|
301
|
+
await stubClient.testRequest<TestSourceResponse>('/api/stub-test');
|
|
274
302
|
|
|
275
303
|
expect(result.success).toBe(true);
|
|
276
304
|
expect(result.source).toBe('stub');
|
|
@@ -283,16 +311,19 @@ describe('BaseHttpClient', () => {
|
|
|
283
311
|
});
|
|
284
312
|
|
|
285
313
|
it('should handle stub errors', async () => {
|
|
286
|
-
const stubFetch = vi
|
|
314
|
+
const stubFetch = vi
|
|
315
|
+
.fn()
|
|
316
|
+
.mockRejectedValue(new Error('Stub connection failed'));
|
|
287
317
|
const stub = { containerFetch: stubFetch };
|
|
288
318
|
const stubClient = new TestHttpClient({
|
|
289
319
|
baseUrl: 'http://test.com',
|
|
290
320
|
port: 3000,
|
|
291
|
-
stub
|
|
321
|
+
stub
|
|
292
322
|
});
|
|
293
323
|
|
|
294
|
-
await expect(stubClient.testRequest('/api/stub-error'))
|
|
295
|
-
|
|
324
|
+
await expect(stubClient.testRequest('/api/stub-error')).rejects.toThrow(
|
|
325
|
+
'Stub connection failed'
|
|
326
|
+
);
|
|
296
327
|
});
|
|
297
328
|
});
|
|
298
329
|
|
|
@@ -303,26 +334,31 @@ describe('BaseHttpClient', () => {
|
|
|
303
334
|
{ status: 202, shouldSucceed: true },
|
|
304
335
|
{ status: 409, shouldSucceed: false },
|
|
305
336
|
{ status: 422, shouldSucceed: false },
|
|
306
|
-
{ status: 429, shouldSucceed: false }
|
|
337
|
+
{ status: 429, shouldSucceed: false }
|
|
307
338
|
];
|
|
308
339
|
|
|
309
340
|
for (const test of unusualStatusTests) {
|
|
310
|
-
mockFetch.mockResolvedValueOnce(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
mockFetch.mockResolvedValueOnce(
|
|
342
|
+
new Response(
|
|
343
|
+
test.shouldSucceed
|
|
344
|
+
? JSON.stringify({ success: true, status: test.status })
|
|
345
|
+
: JSON.stringify({ error: `Status ${test.status}` }),
|
|
346
|
+
{ status: test.status }
|
|
347
|
+
)
|
|
348
|
+
);
|
|
316
349
|
|
|
317
350
|
if (test.shouldSucceed) {
|
|
318
|
-
const result = await client.testRequest<TestStatusResponse>(
|
|
351
|
+
const result = await client.testRequest<TestStatusResponse>(
|
|
352
|
+
'/api/unusual-status'
|
|
353
|
+
);
|
|
319
354
|
expect(result.success).toBe(true);
|
|
320
355
|
expect(result.status).toBe(test.status);
|
|
321
356
|
} else {
|
|
322
|
-
await expect(
|
|
323
|
-
.
|
|
357
|
+
await expect(
|
|
358
|
+
client.testRequest('/api/unusual-status')
|
|
359
|
+
).rejects.toThrow();
|
|
324
360
|
}
|
|
325
361
|
}
|
|
326
362
|
});
|
|
327
363
|
});
|
|
328
|
-
});
|
|
364
|
+
});
|