@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.
- package/CHANGELOG.md +176 -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 +516 -145
- 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-U2M5GSMU.js +0 -2220
- package/dist/chunk-U2M5GSMU.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-Cyuj5F-M.d.ts +0 -579
- 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,50 +13,88 @@ 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
|
-
|
|
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;
|
|
34
41
|
|
|
35
42
|
// Store the name on first access
|
|
36
43
|
stub.setSandboxName?.(id);
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
if (options?.baseUrl) {
|
|
46
|
+
stub.setBaseUrl(options.baseUrl);
|
|
47
|
+
}
|
|
48
|
+
|
|
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
|
+
};
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
42
78
|
defaultPort = 3000; // Default port for the container's Bun server
|
|
43
|
-
sleepAfter =
|
|
79
|
+
sleepAfter: string | number = '10m'; // Sleep the sandbox if no requests are made in this timeframe
|
|
44
80
|
|
|
45
81
|
client: SandboxClient;
|
|
46
82
|
private codeInterpreter: CodeInterpreter;
|
|
47
83
|
private sandboxName: string | null = null;
|
|
84
|
+
private baseUrl: string | null = null;
|
|
48
85
|
private portTokens: Map<number, string> = new Map();
|
|
49
86
|
private defaultSession: string | null = null;
|
|
50
87
|
envVars: Record<string, string> = {};
|
|
51
88
|
private logger: ReturnType<typeof createLogger>;
|
|
89
|
+
private keepAliveEnabled: boolean = false;
|
|
52
90
|
|
|
53
|
-
constructor(ctx:
|
|
91
|
+
constructor(ctx: DurableObjectState<{}>, env: Env) {
|
|
54
92
|
super(ctx, env);
|
|
55
93
|
|
|
56
94
|
const envObj = env as any;
|
|
57
95
|
// Set sandbox environment variables from env object
|
|
58
96
|
const sandboxEnvKeys = ['SANDBOX_LOG_LEVEL', 'SANDBOX_LOG_FORMAT'] as const;
|
|
59
|
-
sandboxEnvKeys.forEach(key => {
|
|
97
|
+
sandboxEnvKeys.forEach((key) => {
|
|
60
98
|
if (envObj?.[key]) {
|
|
61
99
|
this.envVars[key] = envObj[key];
|
|
62
100
|
}
|
|
@@ -70,17 +108,22 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
70
108
|
this.client = new SandboxClient({
|
|
71
109
|
logger: this.logger,
|
|
72
110
|
port: 3000, // Control plane port
|
|
73
|
-
stub: this
|
|
111
|
+
stub: this
|
|
74
112
|
});
|
|
75
113
|
|
|
76
114
|
// Initialize code interpreter - pass 'this' after client is ready
|
|
77
115
|
// The CodeInterpreter extracts client.interpreter from the sandbox
|
|
78
116
|
this.codeInterpreter = new CodeInterpreter(this);
|
|
79
117
|
|
|
80
|
-
// Load the sandbox name
|
|
118
|
+
// Load the sandbox name, port tokens, and default session from storage on initialization
|
|
81
119
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
82
|
-
this.sandboxName =
|
|
83
|
-
|
|
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
|
+
{};
|
|
84
127
|
|
|
85
128
|
// Convert stored tokens back to Map
|
|
86
129
|
this.portTokens = new Map();
|
|
@@ -98,6 +141,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
98
141
|
}
|
|
99
142
|
}
|
|
100
143
|
|
|
144
|
+
// RPC method to set the base URL
|
|
145
|
+
async setBaseUrl(baseUrl: string): Promise<void> {
|
|
146
|
+
if (!this.baseUrl) {
|
|
147
|
+
this.baseUrl = baseUrl;
|
|
148
|
+
await this.ctx.storage.put('baseUrl', baseUrl);
|
|
149
|
+
} else {
|
|
150
|
+
if (this.baseUrl !== baseUrl) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Base URL already set and different from one previously provided'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
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
|
+
|
|
101
177
|
// RPC method to set environment variables
|
|
102
178
|
async setEnvVars(envVars: Record<string, string>): Promise<void> {
|
|
103
179
|
// Update local state for new sessions
|
|
@@ -110,10 +186,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
110
186
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
111
187
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
112
188
|
|
|
113
|
-
const result = await this.client.commands.execute(
|
|
189
|
+
const result = await this.client.commands.execute(
|
|
190
|
+
exportCommand,
|
|
191
|
+
this.defaultSession
|
|
192
|
+
);
|
|
114
193
|
|
|
115
194
|
if (result.exitCode !== 0) {
|
|
116
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
197
|
+
);
|
|
117
198
|
}
|
|
118
199
|
}
|
|
119
200
|
}
|
|
@@ -129,6 +210,61 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
129
210
|
|
|
130
211
|
override onStart() {
|
|
131
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
|
+
}
|
|
132
268
|
}
|
|
133
269
|
|
|
134
270
|
override onStop() {
|
|
@@ -136,13 +272,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
136
272
|
}
|
|
137
273
|
|
|
138
274
|
override onError(error: unknown) {
|
|
139
|
-
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
|
+
}
|
|
140
296
|
}
|
|
141
297
|
|
|
142
298
|
// Override fetch to route internal container requests to appropriate ports
|
|
143
299
|
override async fetch(request: Request): Promise<Response> {
|
|
144
300
|
// Extract or generate trace ID from request
|
|
145
|
-
const traceId =
|
|
301
|
+
const traceId =
|
|
302
|
+
TraceContext.fromHeaders(request.headers) || TraceContext.generate();
|
|
146
303
|
|
|
147
304
|
// Create request-specific logger with trace ID
|
|
148
305
|
const requestLogger = this.logger.child({ traceId, operation: 'fetch' });
|
|
@@ -157,7 +314,33 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
157
314
|
await this.ctx.storage.put('sandboxName', name);
|
|
158
315
|
}
|
|
159
316
|
|
|
160
|
-
//
|
|
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
|
|
161
344
|
const port = this.determinePort(url);
|
|
162
345
|
|
|
163
346
|
// Route to the appropriate port
|
|
@@ -165,6 +348,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
165
348
|
});
|
|
166
349
|
}
|
|
167
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
|
+
|
|
168
356
|
private determinePort(url: URL): number {
|
|
169
357
|
// Extract port from proxy requests (e.g., /proxy/8080/*)
|
|
170
358
|
const proxyMatch = url.pathname.match(/^\/proxy\/(\d+)/);
|
|
@@ -180,20 +368,41 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
180
368
|
/**
|
|
181
369
|
* Ensure default session exists - lazy initialization
|
|
182
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.
|
|
183
375
|
*/
|
|
184
376
|
private async ensureDefaultSession(): Promise<string> {
|
|
185
377
|
if (!this.defaultSession) {
|
|
186
378
|
const sessionId = `sandbox-${this.sandboxName || 'default'}`;
|
|
187
379
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
406
|
}
|
|
198
407
|
return this.defaultSession;
|
|
199
408
|
}
|
|
@@ -217,7 +426,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
217
426
|
const startTime = Date.now();
|
|
218
427
|
const timestamp = new Date().toISOString();
|
|
219
428
|
|
|
220
|
-
// Handle timeout
|
|
221
429
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
222
430
|
|
|
223
431
|
try {
|
|
@@ -230,13 +438,23 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
230
438
|
|
|
231
439
|
if (options?.stream && options?.onOutput) {
|
|
232
440
|
// Streaming with callbacks - we need to collect the final result
|
|
233
|
-
result = await this.executeWithStreaming(
|
|
441
|
+
result = await this.executeWithStreaming(
|
|
442
|
+
command,
|
|
443
|
+
sessionId,
|
|
444
|
+
options,
|
|
445
|
+
startTime,
|
|
446
|
+
timestamp
|
|
447
|
+
);
|
|
234
448
|
} else {
|
|
235
449
|
// Regular execution with session
|
|
236
450
|
const response = await this.client.commands.execute(command, sessionId);
|
|
237
451
|
|
|
238
452
|
const duration = Date.now() - startTime;
|
|
239
|
-
result = this.mapExecuteResponseToExecResult(
|
|
453
|
+
result = this.mapExecuteResponseToExecResult(
|
|
454
|
+
response,
|
|
455
|
+
duration,
|
|
456
|
+
sessionId
|
|
457
|
+
);
|
|
240
458
|
}
|
|
241
459
|
|
|
242
460
|
// Call completion callback if provided
|
|
@@ -268,7 +486,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
268
486
|
let stderr = '';
|
|
269
487
|
|
|
270
488
|
try {
|
|
271
|
-
const stream = await this.client.commands.executeStream(
|
|
489
|
+
const stream = await this.client.commands.executeStream(
|
|
490
|
+
command,
|
|
491
|
+
sessionId
|
|
492
|
+
);
|
|
272
493
|
|
|
273
494
|
for await (const event of parseSSEStream<ExecEvent>(stream)) {
|
|
274
495
|
// Check for cancellation
|
|
@@ -313,7 +534,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
313
534
|
|
|
314
535
|
// If we get here without a complete event, something went wrong
|
|
315
536
|
throw new Error('Stream ended without completion event');
|
|
316
|
-
|
|
317
537
|
} catch (error) {
|
|
318
538
|
if (options.signal?.aborted) {
|
|
319
539
|
throw new Error('Operation was aborted');
|
|
@@ -361,8 +581,15 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
361
581
|
pid: data.pid,
|
|
362
582
|
command: data.command,
|
|
363
583
|
status: data.status,
|
|
364
|
-
startTime:
|
|
365
|
-
|
|
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,
|
|
366
593
|
exitCode: data.exitCode,
|
|
367
594
|
sessionId,
|
|
368
595
|
|
|
@@ -382,25 +609,35 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
382
609
|
};
|
|
383
610
|
}
|
|
384
611
|
|
|
385
|
-
|
|
386
612
|
// Background process management
|
|
387
|
-
async startProcess(
|
|
613
|
+
async startProcess(
|
|
614
|
+
command: string,
|
|
615
|
+
options?: ProcessOptions,
|
|
616
|
+
sessionId?: string
|
|
617
|
+
): Promise<Process> {
|
|
388
618
|
// Use the new HttpClient method to start the process
|
|
389
619
|
try {
|
|
390
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
391
|
-
const response = await this.client.processes.startProcess(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
);
|
|
404
641
|
|
|
405
642
|
// Call onStart callback if provided
|
|
406
643
|
if (options?.onStart) {
|
|
@@ -408,7 +645,6 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
408
645
|
}
|
|
409
646
|
|
|
410
647
|
return processObj;
|
|
411
|
-
|
|
412
648
|
} catch (error) {
|
|
413
649
|
if (options?.onError && error instanceof Error) {
|
|
414
650
|
options.onError(error);
|
|
@@ -419,42 +655,52 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
419
655
|
}
|
|
420
656
|
|
|
421
657
|
async listProcesses(sessionId?: string): Promise<Process[]> {
|
|
422
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
658
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
423
659
|
const response = await this.client.processes.listProcesses();
|
|
424
660
|
|
|
425
|
-
return response.processes.map(processData =>
|
|
426
|
-
this.createProcessFromDTO(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
)
|
|
435
674
|
);
|
|
436
675
|
}
|
|
437
676
|
|
|
438
677
|
async getProcess(id: string, sessionId?: string): Promise<Process | null> {
|
|
439
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
678
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
440
679
|
const response = await this.client.processes.getProcess(id);
|
|
441
680
|
if (!response.process) {
|
|
442
681
|
return null;
|
|
443
682
|
}
|
|
444
683
|
|
|
445
684
|
const processData = response.process;
|
|
446
|
-
return this.createProcessFromDTO(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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> {
|
|
458
704
|
// Note: signal parameter is not currently supported by the HttpClient implementation
|
|
459
705
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
460
706
|
await this.client.processes.killProcess(id);
|
|
@@ -472,7 +718,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
472
718
|
return 0;
|
|
473
719
|
}
|
|
474
720
|
|
|
475
|
-
async getProcessLogs(
|
|
721
|
+
async getProcessLogs(
|
|
722
|
+
id: string,
|
|
723
|
+
sessionId?: string
|
|
724
|
+
): Promise<{ stdout: string; stderr: string; processId: string }> {
|
|
476
725
|
// The HTTP client already throws properly typed errors, so we just let them propagate
|
|
477
726
|
const response = await this.client.processes.getProcessLogs(id);
|
|
478
727
|
return {
|
|
@@ -482,9 +731,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
482
731
|
};
|
|
483
732
|
}
|
|
484
733
|
|
|
485
|
-
|
|
486
734
|
// Streaming methods - return ReadableStream for RPC compatibility
|
|
487
|
-
async execStream(
|
|
735
|
+
async execStream(
|
|
736
|
+
command: string,
|
|
737
|
+
options?: StreamOptions
|
|
738
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
488
739
|
// Check for cancellation
|
|
489
740
|
if (options?.signal?.aborted) {
|
|
490
741
|
throw new Error('Operation was aborted');
|
|
@@ -498,7 +749,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
498
749
|
/**
|
|
499
750
|
* Internal session-aware execStream implementation
|
|
500
751
|
*/
|
|
501
|
-
private async execStreamWithSession(
|
|
752
|
+
private async execStreamWithSession(
|
|
753
|
+
command: string,
|
|
754
|
+
sessionId: string,
|
|
755
|
+
options?: StreamOptions
|
|
756
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
502
757
|
// Check for cancellation
|
|
503
758
|
if (options?.signal?.aborted) {
|
|
504
759
|
throw new Error('Operation was aborted');
|
|
@@ -507,7 +762,13 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
507
762
|
return this.client.commands.executeStream(command, sessionId);
|
|
508
763
|
}
|
|
509
764
|
|
|
510
|
-
|
|
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>> {
|
|
511
772
|
// Check for cancellation
|
|
512
773
|
if (options?.signal?.aborted) {
|
|
513
774
|
throw new Error('Operation was aborted');
|
|
@@ -520,7 +781,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
520
781
|
repoUrl: string,
|
|
521
782
|
options: { branch?: string; targetDir?: string; sessionId?: string }
|
|
522
783
|
) {
|
|
523
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
784
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
524
785
|
return this.client.git.checkout(repoUrl, session, {
|
|
525
786
|
branch: options.branch,
|
|
526
787
|
targetDir: options.targetDir
|
|
@@ -531,8 +792,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
531
792
|
path: string,
|
|
532
793
|
options: { recursive?: boolean; sessionId?: string } = {}
|
|
533
794
|
) {
|
|
534
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
535
|
-
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
|
+
});
|
|
536
799
|
}
|
|
537
800
|
|
|
538
801
|
async writeFile(
|
|
@@ -540,21 +803,19 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
540
803
|
content: string,
|
|
541
804
|
options: { encoding?: string; sessionId?: string } = {}
|
|
542
805
|
) {
|
|
543
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
544
|
-
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
|
+
});
|
|
545
810
|
}
|
|
546
811
|
|
|
547
812
|
async deleteFile(path: string, sessionId?: string) {
|
|
548
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
813
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
549
814
|
return this.client.files.deleteFile(path, session);
|
|
550
815
|
}
|
|
551
816
|
|
|
552
|
-
async renameFile(
|
|
553
|
-
|
|
554
|
-
newPath: string,
|
|
555
|
-
sessionId?: string
|
|
556
|
-
) {
|
|
557
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
817
|
+
async renameFile(oldPath: string, newPath: string, sessionId?: string) {
|
|
818
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
558
819
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
559
820
|
}
|
|
560
821
|
|
|
@@ -563,7 +824,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
563
824
|
destinationPath: string,
|
|
564
825
|
sessionId?: string
|
|
565
826
|
) {
|
|
566
|
-
const session = sessionId ?? await this.ensureDefaultSession();
|
|
827
|
+
const session = sessionId ?? (await this.ensureDefaultSession());
|
|
567
828
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
568
829
|
}
|
|
569
830
|
|
|
@@ -571,8 +832,10 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
571
832
|
path: string,
|
|
572
833
|
options: { encoding?: string; sessionId?: string } = {}
|
|
573
834
|
) {
|
|
574
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
575
|
-
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
|
+
});
|
|
576
839
|
}
|
|
577
840
|
|
|
578
841
|
/**
|
|
@@ -585,7 +848,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
585
848
|
path: string,
|
|
586
849
|
options: { sessionId?: string } = {}
|
|
587
850
|
): Promise<ReadableStream<Uint8Array>> {
|
|
588
|
-
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
851
|
+
const session = options.sessionId ?? (await this.ensureDefaultSession());
|
|
589
852
|
return this.client.files.readFileStream(path, session);
|
|
590
853
|
}
|
|
591
854
|
|
|
@@ -597,6 +860,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
597
860
|
return this.client.files.listFiles(path, session, options);
|
|
598
861
|
}
|
|
599
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
|
+
|
|
600
868
|
async exposePort(port: number, options: { name?: string; hostname: string }) {
|
|
601
869
|
// Check if hostname is workers.dev domain (doesn't support wildcard subdomains)
|
|
602
870
|
if (options.hostname.endsWith('.workers.dev')) {
|
|
@@ -615,7 +883,9 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
615
883
|
|
|
616
884
|
// We need the sandbox name to construct preview URLs
|
|
617
885
|
if (!this.sandboxName) {
|
|
618
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
888
|
+
);
|
|
619
889
|
}
|
|
620
890
|
|
|
621
891
|
// Generate and store token for this port
|
|
@@ -623,18 +893,25 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
623
893
|
this.portTokens.set(port, token);
|
|
624
894
|
await this.persistPortTokens();
|
|
625
895
|
|
|
626
|
-
const url = this.constructPreviewUrl(
|
|
896
|
+
const url = this.constructPreviewUrl(
|
|
897
|
+
port,
|
|
898
|
+
this.sandboxName,
|
|
899
|
+
options.hostname,
|
|
900
|
+
token
|
|
901
|
+
);
|
|
627
902
|
|
|
628
903
|
return {
|
|
629
904
|
url,
|
|
630
905
|
port,
|
|
631
|
-
name: options?.name
|
|
906
|
+
name: options?.name
|
|
632
907
|
};
|
|
633
908
|
}
|
|
634
909
|
|
|
635
910
|
async unexposePort(port: number) {
|
|
636
911
|
if (!validatePort(port)) {
|
|
637
|
-
throw new SecurityError(
|
|
912
|
+
throw new SecurityError(
|
|
913
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
914
|
+
);
|
|
638
915
|
}
|
|
639
916
|
|
|
640
917
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -653,32 +930,44 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
653
930
|
|
|
654
931
|
// We need the sandbox name to construct preview URLs
|
|
655
932
|
if (!this.sandboxName) {
|
|
656
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
'Sandbox name not available. Ensure sandbox is accessed through getSandbox()'
|
|
935
|
+
);
|
|
657
936
|
}
|
|
658
937
|
|
|
659
|
-
return response.ports.map(port => {
|
|
938
|
+
return response.ports.map((port) => {
|
|
660
939
|
// Get token for this port - must exist for all exposed ports
|
|
661
940
|
const token = this.portTokens.get(port.port);
|
|
662
941
|
if (!token) {
|
|
663
|
-
throw new Error(
|
|
942
|
+
throw new Error(
|
|
943
|
+
`Port ${port.port} is exposed but has no token. This should not happen.`
|
|
944
|
+
);
|
|
664
945
|
}
|
|
665
946
|
|
|
666
947
|
return {
|
|
667
|
-
url: this.constructPreviewUrl(
|
|
948
|
+
url: this.constructPreviewUrl(
|
|
949
|
+
port.port,
|
|
950
|
+
this.sandboxName!,
|
|
951
|
+
hostname,
|
|
952
|
+
token
|
|
953
|
+
),
|
|
668
954
|
port: port.port,
|
|
669
|
-
status: port.status
|
|
955
|
+
status: port.status
|
|
670
956
|
};
|
|
671
957
|
});
|
|
672
958
|
}
|
|
673
959
|
|
|
674
|
-
|
|
675
960
|
async isPortExposed(port: number): Promise<boolean> {
|
|
676
961
|
try {
|
|
677
962
|
const sessionId = await this.ensureDefaultSession();
|
|
678
963
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
679
|
-
return response.ports.some(exposedPort => exposedPort.port === port);
|
|
964
|
+
return response.ports.some((exposedPort) => exposedPort.port === port);
|
|
680
965
|
} catch (error) {
|
|
681
|
-
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
|
+
);
|
|
682
971
|
return false;
|
|
683
972
|
}
|
|
684
973
|
}
|
|
@@ -694,7 +983,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
694
983
|
const storedToken = this.portTokens.get(port);
|
|
695
984
|
if (!storedToken) {
|
|
696
985
|
// This should not happen - all exposed ports must have tokens
|
|
697
|
-
this.logger.error(
|
|
986
|
+
this.logger.error(
|
|
987
|
+
'Port is exposed but has no token - bug detected',
|
|
988
|
+
undefined,
|
|
989
|
+
{ port }
|
|
990
|
+
);
|
|
698
991
|
return false;
|
|
699
992
|
}
|
|
700
993
|
|
|
@@ -710,7 +1003,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
710
1003
|
|
|
711
1004
|
// Convert to base64url format (URL-safe, no padding, lowercase)
|
|
712
1005
|
const base64 = btoa(String.fromCharCode(...array));
|
|
713
|
-
return base64
|
|
1006
|
+
return base64
|
|
1007
|
+
.replace(/\+/g, '-')
|
|
1008
|
+
.replace(/\//g, '_')
|
|
1009
|
+
.replace(/=/g, '')
|
|
1010
|
+
.toLowerCase();
|
|
714
1011
|
}
|
|
715
1012
|
|
|
716
1013
|
private async persistPortTokens(): Promise<void> {
|
|
@@ -722,9 +1019,16 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
722
1019
|
await this.ctx.storage.put('portTokens', tokensObj);
|
|
723
1020
|
}
|
|
724
1021
|
|
|
725
|
-
private constructPreviewUrl(
|
|
1022
|
+
private constructPreviewUrl(
|
|
1023
|
+
port: number,
|
|
1024
|
+
sandboxId: string,
|
|
1025
|
+
hostname: string,
|
|
1026
|
+
token: string
|
|
1027
|
+
): string {
|
|
726
1028
|
if (!validatePort(port)) {
|
|
727
|
-
throw new SecurityError(
|
|
1029
|
+
throw new SecurityError(
|
|
1030
|
+
`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`
|
|
1031
|
+
);
|
|
728
1032
|
}
|
|
729
1033
|
|
|
730
1034
|
// Validate sandbox ID (will throw SecurityError if invalid)
|
|
@@ -746,14 +1050,18 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
746
1050
|
|
|
747
1051
|
return baseUrl.toString();
|
|
748
1052
|
} catch (error) {
|
|
749
|
-
throw new SecurityError(
|
|
1053
|
+
throw new SecurityError(
|
|
1054
|
+
`Failed to construct preview URL: ${
|
|
1055
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1056
|
+
}`
|
|
1057
|
+
);
|
|
750
1058
|
}
|
|
751
1059
|
}
|
|
752
1060
|
|
|
753
1061
|
// Production subdomain logic - enforce HTTPS
|
|
754
1062
|
try {
|
|
755
1063
|
// Always use HTTPS for production (non-localhost)
|
|
756
|
-
const protocol =
|
|
1064
|
+
const protocol = 'https';
|
|
757
1065
|
const baseUrl = new URL(`${protocol}://${hostname}`);
|
|
758
1066
|
|
|
759
1067
|
// Construct subdomain safely with mandatory token
|
|
@@ -762,7 +1070,11 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
762
1070
|
|
|
763
1071
|
return baseUrl.toString();
|
|
764
1072
|
} catch (error) {
|
|
765
|
-
throw new SecurityError(
|
|
1073
|
+
throw new SecurityError(
|
|
1074
|
+
`Failed to construct preview URL: ${
|
|
1075
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
1076
|
+
}`
|
|
1077
|
+
);
|
|
766
1078
|
}
|
|
767
1079
|
}
|
|
768
1080
|
|
|
@@ -781,7 +1093,7 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
781
1093
|
await this.client.utils.createSession({
|
|
782
1094
|
id: sessionId,
|
|
783
1095
|
env: options?.env,
|
|
784
|
-
cwd: options?.cwd
|
|
1096
|
+
cwd: options?.cwd
|
|
785
1097
|
});
|
|
786
1098
|
|
|
787
1099
|
// Return wrapper that binds sessionId to all operations
|
|
@@ -803,6 +1115,34 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
803
1115
|
return this.getSessionWrapper(sessionId);
|
|
804
1116
|
}
|
|
805
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
|
+
|
|
806
1146
|
/**
|
|
807
1147
|
* Internal helper to create ExecutionSession wrapper for a given sessionId
|
|
808
1148
|
* Used by both createSession and getSession
|
|
@@ -812,31 +1152,42 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
812
1152
|
id: sessionId,
|
|
813
1153
|
|
|
814
1154
|
// Command execution - delegate to internal session-aware methods
|
|
815
|
-
exec: (command, options) =>
|
|
816
|
-
|
|
1155
|
+
exec: (command, options) =>
|
|
1156
|
+
this.execWithSession(command, sessionId, options),
|
|
1157
|
+
execStream: (command, options) =>
|
|
1158
|
+
this.execStreamWithSession(command, sessionId, options),
|
|
817
1159
|
|
|
818
1160
|
// Process management
|
|
819
|
-
startProcess: (command, options) =>
|
|
1161
|
+
startProcess: (command, options) =>
|
|
1162
|
+
this.startProcess(command, options, sessionId),
|
|
820
1163
|
listProcesses: () => this.listProcesses(sessionId),
|
|
821
1164
|
getProcess: (id) => this.getProcess(id, sessionId),
|
|
822
1165
|
killProcess: (id, signal) => this.killProcess(id, signal),
|
|
823
1166
|
killAllProcesses: () => this.killAllProcesses(),
|
|
824
1167
|
cleanupCompletedProcesses: () => this.cleanupCompletedProcesses(),
|
|
825
1168
|
getProcessLogs: (id) => this.getProcessLogs(id),
|
|
826
|
-
streamProcessLogs: (processId, options) =>
|
|
1169
|
+
streamProcessLogs: (processId, options) =>
|
|
1170
|
+
this.streamProcessLogs(processId, options),
|
|
827
1171
|
|
|
828
1172
|
// File operations - pass sessionId via options or parameter
|
|
829
|
-
writeFile: (path, content, options) =>
|
|
830
|
-
|
|
1173
|
+
writeFile: (path, content, options) =>
|
|
1174
|
+
this.writeFile(path, content, { ...options, sessionId }),
|
|
1175
|
+
readFile: (path, options) =>
|
|
1176
|
+
this.readFile(path, { ...options, sessionId }),
|
|
831
1177
|
readFileStream: (path) => this.readFileStream(path, { sessionId }),
|
|
832
1178
|
mkdir: (path, options) => this.mkdir(path, { ...options, sessionId }),
|
|
833
1179
|
deleteFile: (path) => this.deleteFile(path, sessionId),
|
|
834
|
-
renameFile: (oldPath, newPath) =>
|
|
835
|
-
|
|
836
|
-
|
|
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),
|
|
837
1187
|
|
|
838
1188
|
// Git operations
|
|
839
|
-
gitCheckout: (repoUrl, options) =>
|
|
1189
|
+
gitCheckout: (repoUrl, options) =>
|
|
1190
|
+
this.gitCheckout(repoUrl, { ...options, sessionId }),
|
|
840
1191
|
|
|
841
1192
|
// Environment management - needs special handling
|
|
842
1193
|
setEnvVars: async (envVars: Record<string, string>) => {
|
|
@@ -846,27 +1197,39 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
846
1197
|
const escapedValue = value.replace(/'/g, "'\\''");
|
|
847
1198
|
const exportCommand = `export ${key}='${escapedValue}'`;
|
|
848
1199
|
|
|
849
|
-
const result = await this.client.commands.execute(
|
|
1200
|
+
const result = await this.client.commands.execute(
|
|
1201
|
+
exportCommand,
|
|
1202
|
+
sessionId
|
|
1203
|
+
);
|
|
850
1204
|
|
|
851
1205
|
if (result.exitCode !== 0) {
|
|
852
|
-
throw new Error(
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Failed to set ${key}: ${result.stderr || 'Unknown error'}`
|
|
1208
|
+
);
|
|
853
1209
|
}
|
|
854
1210
|
}
|
|
855
1211
|
} catch (error) {
|
|
856
|
-
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
|
+
);
|
|
857
1217
|
throw error;
|
|
858
1218
|
}
|
|
859
1219
|
},
|
|
860
1220
|
|
|
861
1221
|
// Code interpreter methods - delegate to sandbox's code interpreter
|
|
862
|
-
createCodeContext: (options) =>
|
|
1222
|
+
createCodeContext: (options) =>
|
|
1223
|
+
this.codeInterpreter.createCodeContext(options),
|
|
863
1224
|
runCode: async (code, options) => {
|
|
864
1225
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
865
1226
|
return execution.toJSON();
|
|
866
1227
|
},
|
|
867
|
-
runCodeStream: (code, options) =>
|
|
1228
|
+
runCodeStream: (code, options) =>
|
|
1229
|
+
this.codeInterpreter.runCodeStream(code, options),
|
|
868
1230
|
listCodeContexts: () => this.codeInterpreter.listCodeContexts(),
|
|
869
|
-
deleteCodeContext: (contextId) =>
|
|
1231
|
+
deleteCodeContext: (contextId) =>
|
|
1232
|
+
this.codeInterpreter.deleteCodeContext(contextId)
|
|
870
1233
|
};
|
|
871
1234
|
}
|
|
872
1235
|
|
|
@@ -874,16 +1237,24 @@ export class Sandbox<Env = unknown> extends Container<Env> implements ISandbox {
|
|
|
874
1237
|
// Code interpreter methods - delegate to CodeInterpreter wrapper
|
|
875
1238
|
// ============================================================================
|
|
876
1239
|
|
|
877
|
-
async createCodeContext(
|
|
1240
|
+
async createCodeContext(
|
|
1241
|
+
options?: CreateContextOptions
|
|
1242
|
+
): Promise<CodeContext> {
|
|
878
1243
|
return this.codeInterpreter.createCodeContext(options);
|
|
879
1244
|
}
|
|
880
1245
|
|
|
881
|
-
async runCode(
|
|
1246
|
+
async runCode(
|
|
1247
|
+
code: string,
|
|
1248
|
+
options?: RunCodeOptions
|
|
1249
|
+
): Promise<ExecutionResult> {
|
|
882
1250
|
const execution = await this.codeInterpreter.runCode(code, options);
|
|
883
1251
|
return execution.toJSON();
|
|
884
1252
|
}
|
|
885
1253
|
|
|
886
|
-
async runCodeStream(
|
|
1254
|
+
async runCodeStream(
|
|
1255
|
+
code: string,
|
|
1256
|
+
options?: RunCodeOptions
|
|
1257
|
+
): Promise<ReadableStream> {
|
|
887
1258
|
return this.codeInterpreter.runCodeStream(code, options);
|
|
888
1259
|
}
|
|
889
1260
|
|