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