@cloudflare/sandbox 0.0.0-aa00a75 → 0.0.0-aeba44f
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/CHANGELOG.md +158 -15
- package/Dockerfile +88 -71
- package/LICENSE +176 -0
- package/README.md +10 -5
- package/dist/index.d.ts +1953 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3278 -53
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/src/clients/base-client.ts +39 -24
- package/src/clients/command-client.ts +8 -8
- package/src/clients/file-client.ts +51 -20
- package/src/clients/git-client.ts +9 -3
- package/src/clients/index.ts +16 -15
- 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 +67 -5
- 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 +28 -17
- package/src/interpreter.ts +50 -41
- package/src/request-handler.ts +34 -21
- package/src/sandbox.ts +502 -151
- package/src/security.ts +21 -6
- package/src/sse-parser.ts +4 -3
- package/src/version.ts +6 -0
- package/startup.sh +1 -1
- package/tests/base-client.test.ts +116 -80
- package/tests/command-client.test.ts +149 -112
- package/tests/file-client.test.ts +373 -185
- package/tests/file-stream.test.ts +24 -20
- package/tests/get-sandbox.test.ts +149 -0
- package/tests/git-client.test.ts +260 -101
- package/tests/port-client.test.ts +100 -108
- package/tests/process-client.test.ts +204 -179
- package/tests/request-handler.test.ts +292 -0
- package/tests/sandbox.test.ts +336 -62
- package/tests/sse-parser.test.ts +17 -16
- package/tests/utility-client.test.ts +129 -56
- package/tests/version.test.ts +16 -0
- package/tsdown.config.ts +12 -0
- package/vitest.config.ts +6 -6
- package/dist/chunk-BCJ7SF3Q.js +0 -117
- package/dist/chunk-BCJ7SF3Q.js.map +0 -1
- 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-HGF554LH.js +0 -2236
- package/dist/chunk-HGF554LH.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 -12
- package/dist/request-handler.js.map +0 -1
- package/dist/sandbox-D9K2ypln.d.ts +0 -583
- package/dist/sandbox.d.ts +0 -4
- package/dist/sandbox.js +0 -12
- 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/src/sandbox.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DurableObject } from 'cloudflare:workers';
|
|
2
|
-
import { Container, getContainer } from
|
|
2
|
+
import { Container, getContainer, switchPort } from '@cloudflare/containers';
|
|
3
3
|
import type {
|
|
4
4
|
CodeContext,
|
|
5
5
|
CreateContextOptions,
|
|
@@ -13,40 +13,70 @@ import type {
|
|
|
13
13
|
ProcessOptions,
|
|
14
14
|
ProcessStatus,
|
|
15
15
|
RunCodeOptions,
|
|
16
|
+
SandboxOptions,
|
|
16
17
|
SessionOptions,
|
|
17
18
|
StreamOptions
|
|
18
|
-
} from
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
} from '@repo/shared';
|
|
20
|
+
import {
|
|
21
|
+
createLogger,
|
|
22
|
+
runWithLogger,
|
|
23
|
+
type SessionDeleteResult,
|
|
24
|
+
TraceContext
|
|
25
|
+
} from '@repo/shared';
|
|
26
|
+
import { type ExecuteResponse, SandboxClient } from './clients';
|
|
21
27
|
import type { ErrorResponse } from './errors';
|
|
22
28
|
import { CustomDomainRequiredError, ErrorCode } from './errors';
|
|
23
|
-
import { CodeInterpreter } from
|
|
24
|
-
import { isLocalhostPattern } from
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const stub = getContainer(ns, id);
|
|
29
|
+
import { CodeInterpreter } from './interpreter';
|
|
30
|
+
import { isLocalhostPattern } from './request-handler';
|
|
31
|
+
import { SecurityError, sanitizeSandboxId, validatePort } from './security';
|
|
32
|
+
import { parseSSEStream } from './sse-parser';
|
|
33
|
+
import { SDK_VERSION } from './version';
|
|
34
|
+
|
|
35
|
+
export function getSandbox(
|
|
36
|
+
ns: DurableObjectNamespace<Sandbox>,
|
|
37
|
+
id: string,
|
|
38
|
+
options?: SandboxOptions
|
|
39
|
+
): Sandbox {
|
|
40
|
+
const stub = getContainer(ns, id) as unknown as Sandbox;
|
|
36
41
|
|
|
37
42
|
// Store the name on first access
|
|
38
43
|
stub.setSandboxName?.(id);
|
|
39
44
|
|
|
40
|
-
if(options?.baseUrl) {
|
|
45
|
+
if (options?.baseUrl) {
|
|
41
46
|
stub.setBaseUrl(options.baseUrl);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
if (options?.sleepAfter !== undefined) {
|
|
50
|
+
stub.setSleepAfter(options.sleepAfter);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options?.keepAlive !== undefined) {
|
|
54
|
+
stub.setKeepAlive(options.keepAlive);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Object.assign(stub, {
|
|
58
|
+
wsConnect: connect(stub)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function connect(stub: {
|
|
63
|
+
fetch: (request: Request) => Promise<Response>;
|
|
64
|
+
}) {
|
|
65
|
+
return async (request: Request, port: number) => {
|
|
66
|
+
// Validate port before routing
|
|
67
|
+
if (!validatePort(port)) {
|
|
68
|
+
throw new SecurityError(
|
|
69
|
+
`Invalid or restricted port: ${port}. Ports must be in range 1024-65535 and not reserved.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const portSwitchedRequest = switchPort(request, port);
|
|
73
|
+
return await stub.fetch(portSwitchedRequest);
|
|
74
|
+
};
|
|
45
75
|
}
|
|
46
76
|
|
|
47
77
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
48
78
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
49
|
-
sleepAfter =
|
|
79
|
+
sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
|
|
50
80
|
|
|
51
81
|
client: SandboxClient;
|
|
52
82
|
private codeInterpreter: CodeInterpreter;
|
|
@@ -56,14 +86,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
56
86
|
private defaultSession: string | null = null;
|
|
57
87
|
envVars: Record<string, string> = {};
|
|
58
88
|
private logger: ReturnType<typeof createLogger>;
|
|
89
|
+
private keepAliveEnabled: boolean = false;
|
|
59
90
|
|
|
60
|
-
constructor(ctx:
|
|
91
|
+
constructor(ctx: DurableObjectState<{}>, env: Env) {
|
|
61
92
|
super(ctx, env);
|
|
62
93
|
|
|
63
94
|
const envObj = env as any;
|
|
64
95
|
// Set sandbox environment variables from env object
|
|
65
96
|
const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
|
|
66
|
-
sandboxEnvKeys.forEach(key => {
|
|
97
|
+
sandboxEnvKeys.forEach((key) => {
|
|
67
98
|
if (envObj?.[key]) {
|
|
68
99
|
this.envVars[key] = envObj[key];
|
|
69
100
|
}
|
|
@@ -77,17 +108,22 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
77
108
|
this.client = new SandboxClient({
|
|
78
109
|
logger: this.logger,
|
|
79
110
|
port: 3000, // Control plane port
|
|
80
|
-
stub: this
|
|
111
|
+
stub: this
|
|
81
112
|
});
|
|
82
113
|
|
|
83
114
|
// Initialize code interpreter - pass 'this' after client is ready
|
|
84
115
|
// The CodeInterpreter extracts client.interpreter from the sandbox
|
|
85
116
|
this.codeInterpreter = new CodeInterpreter(this);
|
|
86
117
|
|
|
87
|
-
// Load the sandbox name
|
|
118
|
+
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
88
119
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
89
|
-
this.sandboxName =
|
|
90
|
-
|
|
120
|
+
this.sandboxName =
|
|
121
|
+
(await this.ctx.storage.get<string>('sandboxName')) || null;
|
|
122
|
+
this.defaultSession =
|
|
123
|
+
(await this.ctx.storage.get<string>('defaultSession')) || null;
|
|
124
|
+
const storedTokens =
|
|
125
|
+
(await this.ctx.storage.get<Record<string, string>>('portTokens')) ||
|
|
126
|
+
{};
|
|
91
127
|
|
|
92
128
|
// Convert stored tokens back to Map
|
|
93
129
|
this.portTokens = new Map();
|
|
@@ -110,14 +146,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
110
146
|
if (!this.baseUrl) {
|
|
111
147
|
this.baseUrl = baseUrl;
|
|
112
148
|
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
113
|
-
console.log(`[Sandbox] Stored base URL: ${baseUrl}`);
|
|
114
149
|
} else {
|
|
115
|
-
if(this.baseUrl !== baseUrl) {
|
|
116
|
-
throw new Error(
|
|
150
|
+
if (this.baseUrl !== baseUrl) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Base URL already set and different from one previously provided'
|
|
153
|
+
);
|
|
117
154
|
}
|
|
118
155
|
}
|
|
119
156
|
}
|
|
120
157
|
|
|
158
|
+
// RPC method to set the sleep timeout
|
|
159
|
+
async setSleepAfter(sleepAfter: string | number): Promise<void> {
|
|
160
|
+
this.sleepAfter = sleepAfter;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// RPC method to enable keepAlive mode
|
|
164
|
+
async setKeepAlive(keepAlive: boolean): Promise<void> {
|
|
165
|
+
this.keepAliveEnabled = keepAlive;
|
|
166
|
+
if (keepAlive) {
|
|
167
|
+
this.logger.info(
|
|
168
|
+
'KeepAlive mode enabled - container will stay alive until explicitly destroyed'
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
this.logger.info(
|
|
172
|
+
'KeepAlive mode disabled - container will timeout normally'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
121
177
|
// RPC method to set environment variables
|
|
122
178
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
123
179
|
// Update local state for new sessions
|
|
@@ -130,10 +186,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
130
186
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
131
187
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
132
188
|
|
|
133
|
-
const result = await this.client.commands.execute(
|
|
189
|
+
const result = await this.client.commands.execute(
|
|
190
|
+
exportCommand,
|
|
191
|
+
this.defaultSession
|
|
192
|
+
);
|
|
134
193
|
|
|
135
194
|
if (result.exitCode !== 0) {
|
|
136
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
197
|
+
);
|
|
137
198
|
}
|
|
138
199
|
}
|
|
139
200
|
}
|
|
@@ -149,6 +210,61 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
149
210
|
|
|
150
211
|
override onStart() {
|
|
151
212
|
this.logger.debug('Sandbox started');
|
|
213
|
+
|
|
214
|
+
// Check version compatibility asynchronously (don't block startup)
|
|
215
|
+
this.checkVersionCompatibility().catch((error) => {
|
|
216
|
+
this.logger.error(
|
|
217
|
+
'Version compatibility check failed',
|
|
218
|
+
error instanceof Error ? error : new Error(String(error))
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if the container version matches the SDK version
|
|
225
|
+
* Logs a warning if there's a mismatch
|
|
226
|
+
*/
|
|
227
|
+
private async checkVersionCompatibility(): Promise<void> {
|
|
228
|
+
try {
|
|
229
|
+
// Get the SDK version (imported from version.ts)
|
|
230
|
+
const sdkVersion = SDK_VERSION;
|
|
231
|
+
|
|
232
|
+
// Get container version
|
|
233
|
+
const containerVersion = await this.client.utils.getVersion();
|
|
234
|
+
|
|
235
|
+
// If container version is unknown, it's likely an old container without the endpoint
|
|
236
|
+
if (containerVersion === 'unknown') {
|
|
237
|
+
this.logger.warn(
|
|
238
|
+
'Container version check: Container version could not be determined. ' +
|
|
239
|
+
'This may indicate an outdated container image. ' +
|
|
240
|
+
'Please update your container to match SDK version ' +
|
|
241
|
+
sdkVersion
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check if versions match
|
|
247
|
+
if (containerVersion !== sdkVersion) {
|
|
248
|
+
const message =
|
|
249
|
+
`Version mismatch detected! SDK version (${sdkVersion}) does not match ` +
|
|
250
|
+
`container version (${containerVersion}). This may cause compatibility issues. ` +
|
|
251
|
+
`Please update your container image to version ${sdkVersion}`;
|
|
252
|
+
|
|
253
|
+
// Log warning - we can't reliably detect dev vs prod environment in Durable Objects
|
|
254
|
+
// so we always use warning level as requested by the user
|
|
255
|
+
this.logger.warn(message);
|
|
256
|
+
} else {
|
|
257
|
+
this.logger.debug('Version check passed', {
|
|
258
|
+
sdkVersion,
|
|
259
|
+
containerVersion
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// Don't fail the sandbox initialization if version check fails
|
|
264
|
+
this.logger.debug('Version compatibility check encountered an error', {
|
|
265
|
+
error: error instanceof Error ? error.message : String(error)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
152
268
|
}
|
|
153
269
|
|
|
154
270
|
override onStop() {
|
|
@@ -156,13 +272,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
156
272
|
}
|
|
157
273
|
|
|
158
274
|
override onError(error: unknown) {
|
|
159
|
-
this.logger.error(
|
|
275
|
+
this.logger.error(
|
|
276
|
+
'Sandbox error',
|
|
277
|
+
error instanceof Error ? error : new Error(String(error))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Override onActivityExpired to prevent automatic shutdown when keepAlive is enabled
|
|
283
|
+
* When keepAlive is disabled, calls parent implementation which stops the container
|
|
284
|
+
*/
|
|
285
|
+
override async onActivityExpired(): Promise<void> {
|
|
286
|
+
if (this.keepAliveEnabled) {
|
|
287
|
+
this.logger.debug(
|
|
288
|
+
'Activity expired but keepAlive is enabled - container will stay alive'
|
|
289
|
+
);
|
|
290
|
+
// Do nothing - don't call stop(), container stays alive
|
|
291
|
+
} else {
|
|
292
|
+
// Default behavior: stop the container
|
|
293
|
+
this.logger.debug('Activity expired - stopping container');
|
|
294
|
+
await super.onActivityExpired();
|
|
295
|
+
}
|
|
160
296
|
}
|
|
161
297
|
|
|
162
298
|
// Override fetch to route internal container requests to appropriate ports
|
|
163
299
|
override async fetch(request: Request): Promise<Response> {
|
|
164
300
|
// Extract or generate trace ID from request
|
|
165
|
-
const traceId =
|
|
301
|
+
const traceId =
|
|
302
|
+
TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
166
303
|
|
|
167
304
|
// Create request-specific logger with trace ID
|
|
168
305
|
const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
|
|
@@ -177,7 +314,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
177
314
|
await this.ctx.storage.put('sandboxName', name);
|
|
178
315
|
}
|
|
179
316
|
|
|
180
|
-
//
|
|
317
|
+
// Detect WebSocket upgrade request (RFC 6455 compliant)
|
|
318
|
+
const upgradeHeader = request.headers.get('Upgrade');
|
|
319
|
+
const connectionHeader = request.headers.get('Connection');
|
|
320
|
+
const isWebSocket =
|
|
321
|
+
upgradeHeader?.toLowerCase() === 'websocket' &&
|
|
322
|
+
connectionHeader?.toLowerCase().includes('upgrade');
|
|
323
|
+
|
|
324
|
+
if (isWebSocket) {
|
|
325
|
+
// WebSocket path: Let parent Container class handle WebSocket proxying
|
|
326
|
+
// This bypasses containerFetch() which uses JSRPC and cannot handle WebSocket upgrades
|
|
327
|
+
try {
|
|
328
|
+
requestLogger.debug('WebSocket upgrade requested', {
|
|
329
|
+
path: url.pathname,
|
|
330
|
+
port: this.determinePort(url)
|
|
331
|
+
});
|
|
332
|
+
return await super.fetch(request);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
requestLogger.error(
|
|
335
|
+
'WebSocket connection failed',
|
|
336
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
337
|
+
{ path: url.pathname }
|
|
338
|
+
);
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Non-WebSocket: Use existing port determination and HTTP routing logic
|
|
181
344
|
const port = this.determinePort(url);
|
|
182
345
|
|
|
183
346
|
// Route to the appropriate port
|
|
@@ -185,6 +348,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
185
348
|
});
|
|
186
349
|
}
|
|
187
350
|
|
|
351
|
+
wsConnect(request: Request, port: number): Promise<Response> {
|
|
352
|
+
// Dummy implementation that will be overridden by the stub
|
|
353
|
+
throw new Error('Not implemented here to avoid RPC serialization issues');
|
|
354
|
+
}
|
|
355
|
+
|
|
188
356
|
private determinePort(url: URL): number {
|
|
189
357
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
190
358
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -200,20 +368,41 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
200
368
|
/**
|
|
201
369
|
* Ensure default session exists - lazy initialization
|
|
202
370
|
* This is called automatically by all public methods that need a session
|
|
371
|
+
*
|
|
372
|
+
* The session is persisted to Durable Object storage to survive hot reloads
|
|
373
|
+
* during development. If a session already exists in the container after reload,
|
|
374
|
+
* we reuse it instead of trying to create a new one.
|
|
203
375
|
*/
|
|
204
376
|
private async ensureDefaultSession(): Promise<string> {
|
|
205
377
|
if (!this.defaultSession) {
|
|
206
378
|
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
|
|
207
379
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
380
|
+
try {
|
|
381
|
+
// Try to create session in container
|
|
382
|
+
await this.client.utils.createSession({
|
|
383
|
+
id: sessionId,
|
|
384
|
+
env: this.envVars || {},
|
|
385
|
+
cwd: '/workspace'
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
this.defaultSession = sessionId;
|
|
389
|
+
// Persist to storage so it survives hot reloads
|
|
390
|
+
await this.ctx.storage.put('defaultSession', sessionId);
|
|
391
|
+
this.logger.debug('Default session initialized', { sessionId });
|
|
392
|
+
} catch (error: any) {
|
|
393
|
+
// If session already exists (e.g., after hot reload), reuse it
|
|
394
|
+
if (error?.message?.includes('already exists')) {
|
|
395
|
+
this.logger.debug('Reusing existing session after reload', {
|
|
396
|
+
sessionId
|
|
397
|
+
});
|
|
398
|
+
this.defaultSession = sessionId;
|
|
399
|
+
// Persist to storage in case it wasn't saved before
|
|
400
|
+
await this.ctx.storage.put('defaultSession', sessionId);
|
|
401
|
+
} else {
|
|
402
|
+
// Re-throw other errors
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
217
406
|
}
|
|
218
407
|
return this.defaultSession;
|
|
219
408
|
}
|
|
@@ -237,7 +426,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
237
426
|
const startTime = Date.now();
|
|
238
427
|
const timestamp = new Date().toISOString();
|
|
239
428
|
|
|
240
|
-
// Handle timeout
|
|
241
429
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
242
430
|
|
|
243
431
|
try {
|
|
@@ -250,13 +438,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
250
438
|
|
|
251
439
|
if (options?.stream && options?.onOutput) {
|
|
252
440
|
// Streaming with callbacks - we need to collect the final result
|
|
253
|
-
result = await this.executeWithStreaming(
|
|
441
|
+
result = await this.executeWithStreaming(
|
|
442
|
+
command,
|
|
443
|
+
sessionId,
|
|
444
|
+
options,
|
|
445
|
+
startTime,
|
|
446
|
+
timestamp
|
|
447
|
+
);
|
|
254
448
|
} else {
|
|
255
449
|
// Regular execution with session
|
|
256
450
|
const response = await this.client.commands.execute(command, sessionId);
|
|
257
451
|
|
|
258
452
|
const duration = Date.now() - startTime;
|
|
259
|
-
result = this.mapExecuteResponseToExecResult(
|
|
453
|
+
result = this.mapExecuteResponseToExecResult(
|
|
454
|
+
response,
|
|
455
|
+
duration,
|
|
456
|
+
sessionId
|
|
457
|
+
);
|
|
260
458
|
}
|
|
261
459
|
|
|
262
460
|
// Call completion callback if provided
|
|
@@ -288,7 +486,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
288
486
|
let stderr = '';
|
|
289
487
|
|
|
290
488
|
try {
|
|
291
|
-
const stream = await this.client.commands.executeStream(
|
|
489
|
+
const stream = await this.client.commands.executeStream(
|
|
490
|
+
command,
|
|
491
|
+
sessionId
|
|
492
|
+
);
|
|
292
493
|
|
|
293
494
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
294
495
|
// Check for cancellation
|
|
@@ -333,7 +534,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
333
534
|
|
|
334
535
|
// If we get here without a complete event, something went wrong
|
|
335
536
|
throw new Error('Stream ended without completion event');
|
|
336
|
-
|
|
337
537
|
} catch (error) {
|
|
338
538
|
if (options.signal?.aborted) {
|
|
339
539
|
throw new Error('Operation was aborted');
|
|
@@ -381,8 +581,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
381
581
|
pid: data.pid,
|
|
382
582
|
command: data.command,
|
|
383
583
|
status: data.status,
|
|
384
|
-
startTime:
|
|
385
|
-
|
|
584
|
+
startTime:
|
|
585
|
+
typeof data.startTime === 'string'
|
|
586
|
+
? new Date(data.startTime)
|
|
587
|
+
: data.startTime,
|
|
588
|
+
endTime: data.endTime
|
|
589
|
+
? typeof data.endTime === 'string'
|
|
590
|
+
? new Date(data.endTime)
|
|
591
|
+
: data.endTime
|
|
592
|
+
: undefined,
|
|
386
593
|
exitCode: data.exitCode,
|
|
387
594
|
sessionId,
|
|
388
595
|
|
|
@@ -402,25 +609,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
402
609
|
};
|
|
403
610
|
}
|
|
404
611
|
|
|
405
|
-
|
|
406
612
|
// Background process management
|
|
407
|
-
async startProcess(
|
|
613
|
+
async startProcess(
|
|
614
|
+
command: string,
|
|
615
|
+
options?: ProcessOptions,
|
|
616
|
+
sessionId?: string
|
|
617
|
+
): Promise<Process> {
|
|
408
618
|
// Use the new HttpClient method to start the process
|
|
409
619
|
try {
|
|
410
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
411
|
-
const response = await this.client.processes.startProcess(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
620
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
621
|
+
const response = await this.client.processes.startProcess(
|
|
622
|
+
command,
|
|
623
|
+
session,
|
|
624
|
+
{
|
|
625
|
+
processId: options?.processId
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const processObj = this.createProcessFromDTO(
|
|
630
|
+
{
|
|
631
|
+
id: response.processId,
|
|
632
|
+
pid: response.pid,
|
|
633
|
+
command: response.command,
|
|
634
|
+
status: 'running' as ProcessStatus,
|
|
635
|
+
startTime: new Date(),
|
|
636
|
+
endTime: undefined,
|
|
637
|
+
exitCode: undefined
|
|
638
|
+
},
|
|
639
|
+
session
|
|
640
|
+
);
|
|
424
641
|
|
|
425
642
|
// Call onStart callback if provided
|
|
426
643
|
if (options?.onStart) {
|
|
@@ -428,7 +645,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
428
645
|
}
|
|
429
646
|
|
|
430
647
|
return processObj;
|
|
431
|
-
|
|
432
648
|
} catch (error) {
|
|
433
649
|
if (options?.onError && error instanceof Error) {
|
|
434
650
|
options.onError(error);
|
|
@@ -439,42 +655,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
439
655
|
}
|
|
440
656
|
|
|
441
657
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
442
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
658
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
443
659
|
const response = await this.client.processes.listProcesses();
|
|
444
660
|
|
|
445
|
-
return response.processes.map(processData =>
|
|
446
|
-
this.createProcessFromDTO(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
661
|
+
return response.processes.map((processData) =>
|
|
662
|
+
this.createProcessFromDTO(
|
|
663
|
+
{
|
|
664
|
+
id: processData.id,
|
|
665
|
+
pid: processData.pid,
|
|
666
|
+
command: processData.command,
|
|
667
|
+
status: processData.status,
|
|
668
|
+
startTime: processData.startTime,
|
|
669
|
+
endTime: processData.endTime,
|
|
670
|
+
exitCode: processData.exitCode
|
|
671
|
+
},
|
|
672
|
+
session
|
|
673
|
+
)
|
|
455
674
|
);
|
|
456
675
|
}
|
|
457
676
|
|
|
458
677
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
459
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
678
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
460
679
|
const response = await this.client.processes.getProcess(id);
|
|
461
680
|
if (!response.process) {
|
|
462
681
|
return null;
|
|
463
682
|
}
|
|
464
683
|
|
|
465
684
|
const processData = response.process;
|
|
466
|
-
return this.createProcessFromDTO(
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
685
|
+
return this.createProcessFromDTO(
|
|
686
|
+
{
|
|
687
|
+
id: processData.id,
|
|
688
|
+
pid: processData.pid,
|
|
689
|
+
command: processData.command,
|
|
690
|
+
status: processData.status,
|
|
691
|
+
startTime: processData.startTime,
|
|
692
|
+
endTime: processData.endTime,
|
|
693
|
+
exitCode: processData.exitCode
|
|
694
|
+
},
|
|
695
|
+
session
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async killProcess(
|
|
700
|
+
id: string,
|
|
701
|
+
signal?: string,
|
|
702
|
+
sessionId?: string
|
|
703
|
+
): Promise<void> {
|
|
478
704
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
479
705
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
480
706
|
await this.client.processes.killProcess(id);
|
|
@@ -492,7 +718,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
492
718
|
return 0;
|
|
493
719
|
}
|
|
494
720
|
|
|
495
|
-
async getProcessLogs(
|
|
721
|
+
async getProcessLogs(
|
|
722
|
+
id: string,
|
|
723
|
+
sessionId?: string
|
|
724
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
496
725
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
497
726
|
const response = await this.client.processes.getProcessLogs(id);
|
|
498
727
|
return {
|
|
@@ -502,9 +731,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
502
731
|
};
|
|
503
732
|
}
|
|
504
733
|
|
|
505
|
-
|
|
506
734
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
507
|
-
async execStream(
|
|
735
|
+
async execStream(
|
|
736
|
+
command: string,
|
|
737
|
+
options?: StreamOptions
|
|
738
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
508
739
|
// Check for cancellation
|
|
509
740
|
if (options?.signal?.aborted) {
|
|
510
741
|
throw new Error('Operation was aborted');
|
|
@@ -518,7 +749,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
518
749
|
/**
|
|
519
750
|
* Internal session-aware execStream implementation
|
|
520
751
|
*/
|
|
521
|
-
private async execStreamWithSession(
|
|
752
|
+
private async execStreamWithSession(
|
|
753
|
+
command: string,
|
|
754
|
+
sessionId: string,
|
|
755
|
+
options?: StreamOptions
|
|
756
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
522
757
|
// Check for cancellation
|
|
523
758
|
if (options?.signal?.aborted) {
|
|
524
759
|
throw new Error('Operation was aborted');
|
|
@@ -527,7 +762,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
527
762
|
return this.client.commands.executeStream(command, sessionId);
|
|
528
763
|
}
|
|
529
764
|
|
|
530
|
-
|
|
765
|
+
/**
|
|
766
|
+
* Stream logs from a background process as a ReadableStream.
|
|
767
|
+
*/
|
|
768
|
+
async streamProcessLogs(
|
|
769
|
+
processId: string,
|
|
770
|
+
options?: { signal?: AbortSignal }
|
|
771
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
531
772
|
// Check for cancellation
|
|
532
773
|
if (options?.signal?.aborted) {
|
|
533
774
|
throw new Error('Operation was aborted');
|
|
@@ -540,7 +781,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
540
781
|
repoUrl: string,
|
|
541
782
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
542
783
|
) {
|
|
543
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
784
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
544
785
|
return this.client.git.checkout(repoUrl, session, {
|
|
545
786
|
branch: options.branch,
|
|
546
787
|
targetDir: options.targetDir
|
|
@@ -551,8 +792,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
551
792
|
path: string,
|
|
552
793
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
553
794
|
) {
|
|
554
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
555
|
-
return this.client.files.mkdir(path, session, {
|
|
795
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
796
|
+
return this.client.files.mkdir(path, session, {
|
|
797
|
+
recursive: options.recursive
|
|
798
|
+
});
|
|
556
799
|
}
|
|
557
800
|
|
|
558
801
|
async writeFile(
|
|
@@ -560,21 +803,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
560
803
|
content: string,
|
|
561
804
|
options: { encoding?: string; sessionId?: string } = {}
|
|
562
805
|
) {
|
|
563
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
564
|
-
return this.client.files.writeFile(path, content, session, {
|
|
806
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
807
|
+
return this.client.files.writeFile(path, content, session, {
|
|
808
|
+
encoding: options.encoding
|
|
809
|
+
});
|
|
565
810
|
}
|
|
566
811
|
|
|
567
812
|
async deleteFile(path: string, sessionId?: string) {
|
|
568
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
569
814
|
return this.client.files.deleteFile(path, session);
|
|
570
815
|
}
|
|
571
816
|
|
|
572
|
-
async renameFile(
|
|
573
|
-
|
|
574
|
-
newPath: string,
|
|
575
|
-
sessionId?: string
|
|
576
|
-
) {
|
|
577
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
817
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
818
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
578
819
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
579
820
|
}
|
|
580
821
|
|
|
@@ -583,7 +824,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
583
824
|
destinationPath: string,
|
|
584
825
|
sessionId?: string
|
|
585
826
|
) {
|
|
586
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
827
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
587
828
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
588
829
|
}
|
|
589
830
|
|
|
@@ -591,8 +832,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
591
832
|
path: string,
|
|
592
833
|
options: { encoding?: string; sessionId?: string } = {}
|
|
593
834
|
) {
|
|
594
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
595
|
-
return this.client.files.readFile(path, session, {
|
|
835
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
836
|
+
return this.client.files.readFile(path, session, {
|
|
837
|
+
encoding: options.encoding
|
|
838
|
+
});
|
|
596
839
|
}
|
|
597
840
|
|
|
598
841
|
/**
|
|
@@ -605,7 +848,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
605
848
|
path: string,
|
|
606
849
|
options: { sessionId?: string } = {}
|
|
607
850
|
): Promise<ReadableStream<Uint8Array>> {
|
|
608
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
851
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
609
852
|
return this.client.files.readFileStream(path, session);
|
|
610
853
|
}
|
|
611
854
|
|
|
@@ -617,6 +860,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
617
860
|
return this.client.files.listFiles(path, session, options);
|
|
618
861
|
}
|
|
619
862
|
|
|
863
|
+
async exists(path: string, sessionId?: string) {
|
|
864
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
865
|
+
return this.client.files.exists(path, session);
|
|
866
|
+
}
|
|
867
|
+
|
|
620
868
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
621
869
|
// Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
|
|
622
870
|
if (options.hostname.endsWith('.workers.dev')) {
|
|
@@ -635,7 +883,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
635
883
|
|
|
636
884
|
// We need the sandbox name to construct preview URLs
|
|
637
885
|
if (!this.sandboxName) {
|
|
638
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
888
|
+
);
|
|
639
889
|
}
|
|
640
890
|
|
|
641
891
|
// Generate and store token for this port
|
|
@@ -643,18 +893,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
643
893
|
this.portTokens.set(port, token);
|
|
644
894
|
await this.persistPortTokens();
|
|
645
895
|
|
|
646
|
-
const url = this.constructPreviewUrl(
|
|
896
|
+
const url = this.constructPreviewUrl(
|
|
897
|
+
port,
|
|
898
|
+
this.sandboxName,
|
|
899
|
+
options.hostname,
|
|
900
|
+
token
|
|
901
|
+
);
|
|
647
902
|
|
|
648
903
|
return {
|
|
649
904
|
url,
|
|
650
905
|
port,
|
|
651
|
-
name: options?.name
|
|
906
|
+
name: options?.name
|
|
652
907
|
};
|
|
653
908
|
}
|
|
654
909
|
|
|
655
910
|
async unexposePort(port: number) {
|
|
656
911
|
if (!validatePort(port)) {
|
|
657
|
-
throw new SecurityError(
|
|
912
|
+
throw new SecurityError(
|
|
913
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
914
|
+
);
|
|
658
915
|
}
|
|
659
916
|
|
|
660
917
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -673,32 +930,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
673
930
|
|
|
674
931
|
// We need the sandbox name to construct preview URLs
|
|
675
932
|
if (!this.sandboxName) {
|
|
676
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
935
|
+
);
|
|
677
936
|
}
|
|
678
937
|
|
|
679
|
-
return response.ports.map(port => {
|
|
938
|
+
return response.ports.map((port) => {
|
|
680
939
|
// Get token for this port - must exist for all exposed ports
|
|
681
940
|
const token = this.portTokens.get(port.port);
|
|
682
941
|
if (!token) {
|
|
683
|
-
throw new Error(
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
944
|
+
);
|
|
684
945
|
}
|
|
685
946
|
|
|
686
947
|
return {
|
|
687
|
-
url: this.constructPreviewUrl(
|
|
948
|
+
url: this.constructPreviewUrl(
|
|
949
|
+
port.port,
|
|
950
|
+
this.sandboxName!,
|
|
951
|
+
hostname,
|
|
952
|
+
token
|
|
953
|
+
),
|
|
688
954
|
port: port.port,
|
|
689
|
-
status: port.status
|
|
955
|
+
status: port.status
|
|
690
956
|
};
|
|
691
957
|
});
|
|
692
958
|
}
|
|
693
959
|
|
|
694
|
-
|
|
695
960
|
async isPortExposed(port: number): Promise<boolean> {
|
|
696
961
|
try {
|
|
697
962
|
const sessionId = await this.ensureDefaultSession();
|
|
698
963
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
699
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
964
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
700
965
|
} catch (error) {
|
|
701
|
-
this.logger.error(
|
|
966
|
+
this.logger.error(
|
|
967
|
+
'Error checking if port is exposed',
|
|
968
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
969
|
+
{ port }
|
|
970
|
+
);
|
|
702
971
|
return false;
|
|
703
972
|
}
|
|
704
973
|
}
|
|
@@ -714,7 +983,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
714
983
|
const storedToken = this.portTokens.get(port);
|
|
715
984
|
if (!storedToken) {
|
|
716
985
|
// This should not happen - all exposed ports must have tokens
|
|
717
|
-
this.logger.error(
|
|
986
|
+
this.logger.error(
|
|
987
|
+
'Port is exposed but has no token - bug detected',
|
|
988
|
+
undefined,
|
|
989
|
+
{ port }
|
|
990
|
+
);
|
|
718
991
|
return false;
|
|
719
992
|
}
|
|
720
993
|
|
|
@@ -730,7 +1003,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
730
1003
|
|
|
731
1004
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
732
1005
|
const base64 = btoa(String.fromCharCode(...array));
|
|
733
|
-
return base64
|
|
1006
|
+
return base64
|
|
1007
|
+
.replace(/\+/g, '-')
|
|
1008
|
+
.replace(/\//g, '_')
|
|
1009
|
+
.replace(/=/g, '')
|
|
1010
|
+
.toLowerCase();
|
|
734
1011
|
}
|
|
735
1012
|
|
|
736
1013
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -742,9 +1019,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
742
1019
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
743
1020
|
}
|
|
744
1021
|
|
|
745
|
-
private constructPreviewUrl(
|
|
1022
|
+
private constructPreviewUrl(
|
|
1023
|
+
port: number,
|
|
1024
|
+
sandboxId: string,
|
|
1025
|
+
hostname: string,
|
|
1026
|
+
token: string
|
|
1027
|
+
): string {
|
|
746
1028
|
if (!validatePort(port)) {
|
|
747
|
-
throw new SecurityError(
|
|
1029
|
+
throw new SecurityError(
|
|
1030
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1031
|
+
);
|
|
748
1032
|
}
|
|
749
1033
|
|
|
750
1034
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -766,14 +1050,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
766
1050
|
|
|
767
1051
|
return baseUrl.toString();
|
|
768
1052
|
} catch (error) {
|
|
769
|
-
throw new SecurityError(
|
|
1053
|
+
throw new SecurityError(
|
|
1054
|
+
`Failed to construct preview URL: ${
|
|
1055
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1056
|
+
}`
|
|
1057
|
+
);
|
|
770
1058
|
}
|
|
771
1059
|
}
|
|
772
1060
|
|
|
773
1061
|
// Production subdomain logic - enforce HTTPS
|
|
774
1062
|
try {
|
|
775
1063
|
// Always use HTTPS for production (non-localhost)
|
|
776
|
-
const protocol =
|
|
1064
|
+
const protocol = 'https';
|
|
777
1065
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
778
1066
|
|
|
779
1067
|
// Construct subdomain safely with mandatory token
|
|
@@ -782,7 +1070,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
782
1070
|
|
|
783
1071
|
return baseUrl.toString();
|
|
784
1072
|
} catch (error) {
|
|
785
|
-
throw new SecurityError(
|
|
1073
|
+
throw new SecurityError(
|
|
1074
|
+
`Failed to construct preview URL: ${
|
|
1075
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1076
|
+
}`
|
|
1077
|
+
);
|
|
786
1078
|
}
|
|
787
1079
|
}
|
|
788
1080
|
|
|
@@ -801,7 +1093,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
801
1093
|
await this.client.utils.createSession({
|
|
802
1094
|
id: sessionId,
|
|
803
1095
|
env: options?.env,
|
|
804
|
-
cwd: options?.cwd
|
|
1096
|
+
cwd: options?.cwd
|
|
805
1097
|
});
|
|
806
1098
|
|
|
807
1099
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -823,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
823
1115
|
return this.getSessionWrapper(sessionId);
|
|
824
1116
|
}
|
|
825
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Delete an execution session
|
|
1120
|
+
* Cleans up session resources and removes it from the container
|
|
1121
|
+
* Note: Cannot delete the default session. To reset the default session,
|
|
1122
|
+
* use sandbox.destroy() to terminate the entire sandbox.
|
|
1123
|
+
*
|
|
1124
|
+
* @param sessionId - The ID of the session to delete
|
|
1125
|
+
* @returns Result with success status, sessionId, and timestamp
|
|
1126
|
+
* @throws Error if attempting to delete the default session
|
|
1127
|
+
*/
|
|
1128
|
+
async deleteSession(sessionId: string): Promise<SessionDeleteResult> {
|
|
1129
|
+
// Prevent deletion of default session
|
|
1130
|
+
if (this.defaultSession && sessionId === this.defaultSession) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`Cannot delete default session '${sessionId}'. Use sandbox.destroy() to terminate the sandbox.`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const response = await this.client.utils.deleteSession(sessionId);
|
|
1137
|
+
|
|
1138
|
+
// Map HTTP response to result type
|
|
1139
|
+
return {
|
|
1140
|
+
success: response.success,
|
|
1141
|
+
sessionId: response.sessionId,
|
|
1142
|
+
timestamp: response.timestamp
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
826
1146
|
/**
|
|
827
1147
|
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
828
1148
|
* Used by both createSession and getSession
|
|
@@ -832,31 +1152,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
832
1152
|
id: sessionId,
|
|
833
1153
|
|
|
834
1154
|
// Command execution - delegate to internal session-aware methods
|
|
835
|
-
exec: (command, options) =>
|
|
836
|
-
|
|
1155
|
+
exec: (command, options) =>
|
|
1156
|
+
this.execWithSession(command, sessionId, options),
|
|
1157
|
+
execStream: (command, options) =>
|
|
1158
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
837
1159
|
|
|
838
1160
|
// Process management
|
|
839
|
-
startProcess: (command, options) =>
|
|
1161
|
+
startProcess: (command, options) =>
|
|
1162
|
+
this.startProcess(command, options, sessionId),
|
|
840
1163
|
listProcesses: () => this.listProcesses(sessionId),
|
|
841
1164
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
842
1165
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
843
1166
|
killAllProcesses: () => this.killAllProcesses(),
|
|
844
1167
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
845
1168
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
846
|
-
streamProcessLogs: (processId, options) =>
|
|
1169
|
+
streamProcessLogs: (processId, options) =>
|
|
1170
|
+
this.streamProcessLogs(processId, options),
|
|
847
1171
|
|
|
848
1172
|
// File operations - pass sessionId via options or parameter
|
|
849
|
-
writeFile: (path, content, options) =>
|
|
850
|
-
|
|
1173
|
+
writeFile: (path, content, options) =>
|
|
1174
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1175
|
+
readFile: (path, options) =>
|
|
1176
|
+
this.readFile(path, { ...options, sessionId }),
|
|
851
1177
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
852
1178
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
853
1179
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
854
|
-
renameFile: (oldPath, newPath) =>
|
|
855
|
-
|
|
856
|
-
|
|
1180
|
+
renameFile: (oldPath, newPath) =>
|
|
1181
|
+
this.renameFile(oldPath, newPath, sessionId),
|
|
1182
|
+
moveFile: (sourcePath, destPath) =>
|
|
1183
|
+
this.moveFile(sourcePath, destPath, sessionId),
|
|
1184
|
+
listFiles: (path, options) =>
|
|
1185
|
+
this.client.files.listFiles(path, sessionId, options),
|
|
1186
|
+
exists: (path) => this.exists(path, sessionId),
|
|
857
1187
|
|
|
858
1188
|
// Git operations
|
|
859
|
-
gitCheckout: (repoUrl, options) =>
|
|
1189
|
+
gitCheckout: (repoUrl, options) =>
|
|
1190
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
860
1191
|
|
|
861
1192
|
// Environment management - needs special handling
|
|
862
1193
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -866,27 +1197,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
866
1197
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
867
1198
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
868
1199
|
|
|
869
|
-
const result = await this.client.commands.execute(
|
|
1200
|
+
const result = await this.client.commands.execute(
|
|
1201
|
+
exportCommand,
|
|
1202
|
+
sessionId
|
|
1203
|
+
);
|
|
870
1204
|
|
|
871
1205
|
if (result.exitCode !== 0) {
|
|
872
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1208
|
+
);
|
|
873
1209
|
}
|
|
874
1210
|
}
|
|
875
1211
|
} catch (error) {
|
|
876
|
-
this.logger.error(
|
|
1212
|
+
this.logger.error(
|
|
1213
|
+
'Failed to set environment variables',
|
|
1214
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1215
|
+
{ sessionId }
|
|
1216
|
+
);
|
|
877
1217
|
throw error;
|
|
878
1218
|
}
|
|
879
1219
|
},
|
|
880
1220
|
|
|
881
1221
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
882
|
-
createCodeContext: (options) =>
|
|
1222
|
+
createCodeContext: (options) =>
|
|
1223
|
+
this.codeInterpreter.createCodeContext(options),
|
|
883
1224
|
runCode: async (code, options) => {
|
|
884
1225
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
885
1226
|
return execution.toJSON();
|
|
886
1227
|
},
|
|
887
|
-
runCodeStream: (code, options) =>
|
|
1228
|
+
runCodeStream: (code, options) =>
|
|
1229
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
888
1230
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
889
|
-
deleteCodeContext: (contextId) =>
|
|
1231
|
+
deleteCodeContext: (contextId) =>
|
|
1232
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
890
1233
|
};
|
|
891
1234
|
}
|
|
892
1235
|
|
|
@@ -894,16 +1237,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
894
1237
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
895
1238
|
// ============================================================================
|
|
896
1239
|
|
|
897
|
-
async createCodeContext(
|
|
1240
|
+
async createCodeContext(
|
|
1241
|
+
options?: CreateContextOptions
|
|
1242
|
+
): Promise<CodeContext> {
|
|
898
1243
|
return this.codeInterpreter.createCodeContext(options);
|
|
899
1244
|
}
|
|
900
1245
|
|
|
901
|
-
async runCode(
|
|
1246
|
+
async runCode(
|
|
1247
|
+
code: string,
|
|
1248
|
+
options?: RunCodeOptions
|
|
1249
|
+
): Promise<ExecutionResult> {
|
|
902
1250
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
903
1251
|
return execution.toJSON();
|
|
904
1252
|
}
|
|
905
1253
|
|
|
906
|
-
async runCodeStream(
|
|
1254
|
+
async runCodeStream(
|
|
1255
|
+
code: string,
|
|
1256
|
+
options?: RunCodeOptions
|
|
1257
|
+
): Promise<ReadableStream> {
|
|
907
1258
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
908
1259
|
}
|
|
909
1260
|
|