@cloudflare/sandbox 0.6.11 → 0.7.1

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { a as isExecResult, c as shellEscape, d as TraceContext, f as Execution, g as getEnvString, h as extractRepoName, i as isWSStreamChunk, l as createLogger, m as GitLogger, n as isWSError, o as isProcess, p as ResultImpl, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createNoOpLogger } from "./dist-c_xYW5i_.js";
2
- import { t as ErrorCode } from "./errors-BCXUmJUn.js";
1
+ import { _ as getEnvString, a as isExecResult, c as shellEscape, d as TraceContext, f as Execution, g as filterEnvVars, h as extractRepoName, i as isWSStreamChunk, l as createLogger, m as GitLogger, n as isWSError, o as isProcess, p as ResultImpl, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createNoOpLogger, v as partitionEnvVars } from "./dist-D9B_6gn_.js";
2
+ import { t as ErrorCode } from "./errors-Bzl0ZNia.js";
3
3
  import { Container, getContainer, switchPort } from "@cloudflare/containers";
4
4
 
5
5
  //#region src/errors/classes.ts
@@ -2216,6 +2216,19 @@ var CodeInterpreter = class {
2216
2216
  }
2217
2217
  };
2218
2218
 
2219
+ //#endregion
2220
+ //#region src/pty/proxy.ts
2221
+ async function proxyTerminal(stub, sessionId, request, options) {
2222
+ if (!sessionId || typeof sessionId !== "string") throw new Error("sessionId is required for terminal access");
2223
+ if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") throw new Error("terminal() requires a WebSocket upgrade request");
2224
+ const params = new URLSearchParams({ sessionId });
2225
+ if (options?.cols) params.set("cols", String(options.cols));
2226
+ if (options?.rows) params.set("rows", String(options.rows));
2227
+ const ptyUrl = `http://localhost/ws/pty?${params}`;
2228
+ const ptyRequest = new Request(ptyUrl, request);
2229
+ return stub.fetch(switchPort(ptyRequest, 3e3));
2230
+ }
2231
+
2219
2232
  //#endregion
2220
2233
  //#region src/request-handler.ts
2221
2234
  async function proxyToSandbox(request, env) {
@@ -2254,15 +2267,18 @@ async function proxyToSandbox(request, env) {
2254
2267
  let proxyUrl;
2255
2268
  if (port !== 3e3) proxyUrl = `http://localhost:${port}${path}${url.search}`;
2256
2269
  else proxyUrl = `http://localhost:3000${path}${url.search}`;
2270
+ const headers = {
2271
+ "X-Original-URL": request.url,
2272
+ "X-Forwarded-Host": url.hostname,
2273
+ "X-Forwarded-Proto": url.protocol.replace(":", ""),
2274
+ "X-Sandbox-Name": sandboxId
2275
+ };
2276
+ request.headers.forEach((value, key) => {
2277
+ headers[key] = value;
2278
+ });
2257
2279
  const proxyRequest = new Request(proxyUrl, {
2258
2280
  method: request.method,
2259
- headers: {
2260
- ...Object.fromEntries(request.headers),
2261
- "X-Original-URL": request.url,
2262
- "X-Forwarded-Host": url.hostname,
2263
- "X-Forwarded-Proto": url.protocol.replace(":", ""),
2264
- "X-Sandbox-Name": sandboxId
2265
- },
2281
+ headers,
2266
2282
  body: request.body,
2267
2283
  duplex: "half"
2268
2284
  });
@@ -2273,21 +2289,29 @@ async function proxyToSandbox(request, env) {
2273
2289
  }
2274
2290
  }
2275
2291
  function extractSandboxRoute(url) {
2276
- const subdomainMatch = url.hostname.match(/^(\d{4,5})-([^.-][^.]*?[^.-]|[^.-])-([a-z0-9_-]{16})\.(.+)$/);
2277
- if (!subdomainMatch) return null;
2278
- const portStr = subdomainMatch[1];
2279
- const sandboxId = subdomainMatch[2];
2280
- const token = subdomainMatch[3];
2281
- subdomainMatch[4];
2292
+ const dotIndex = url.hostname.indexOf(".");
2293
+ if (dotIndex === -1) return null;
2294
+ const subdomain = url.hostname.slice(0, dotIndex);
2295
+ url.hostname.slice(dotIndex + 1);
2296
+ const firstHyphen = subdomain.indexOf("-");
2297
+ if (firstHyphen === -1) return null;
2298
+ const portStr = subdomain.slice(0, firstHyphen);
2299
+ if (!/^\d{4,5}$/.test(portStr)) return null;
2282
2300
  const port = parseInt(portStr, 10);
2283
2301
  if (!validatePort(port)) return null;
2302
+ const rest = subdomain.slice(firstHyphen + 1);
2303
+ const lastHyphen = rest.lastIndexOf("-");
2304
+ if (lastHyphen === -1) return null;
2305
+ const sandboxId = rest.slice(0, lastHyphen);
2306
+ const token = rest.slice(lastHyphen + 1);
2307
+ if (!/^[a-z0-9_]+$/.test(token) || token.length === 0 || token.length > 63) return null;
2308
+ if (sandboxId.length === 0 || sandboxId.length > 63) return null;
2284
2309
  let sanitizedSandboxId;
2285
2310
  try {
2286
2311
  sanitizedSandboxId = sanitizeSandboxId(sandboxId);
2287
- } catch (error) {
2312
+ } catch {
2288
2313
  return null;
2289
2314
  }
2290
- if (sandboxId.length > 63) return null;
2291
2315
  return {
2292
2316
  port,
2293
2317
  sandboxId: sanitizedSandboxId,
@@ -2540,7 +2564,7 @@ function buildS3fsSource(bucket, prefix) {
2540
2564
  * This file is auto-updated by .github/changeset-version.ts during releases
2541
2565
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
2542
2566
  */
2543
- const SDK_VERSION = "0.6.11";
2567
+ const SDK_VERSION = "0.7.1";
2544
2568
 
2545
2569
  //#endregion
2546
2570
  //#region src/sandbox.ts
@@ -2555,7 +2579,27 @@ function getSandbox(ns, id, options) {
2555
2579
  if (options?.sleepAfter !== void 0) stub.setSleepAfter(options.sleepAfter);
2556
2580
  if (options?.keepAlive !== void 0) stub.setKeepAlive(options.keepAlive);
2557
2581
  if (options?.containerTimeouts) stub.setContainerTimeouts(options.containerTimeouts);
2558
- return Object.assign(stub, { wsConnect: connect(stub) });
2582
+ const defaultSessionId = `sandbox-${effectiveId}`;
2583
+ const enhancedMethods = {
2584
+ createSession: async (opts) => {
2585
+ return enhanceSession(stub, await stub.createSession(opts));
2586
+ },
2587
+ getSession: async (sessionId) => {
2588
+ return enhanceSession(stub, await stub.getSession(sessionId));
2589
+ },
2590
+ terminal: (request, opts) => proxyTerminal(stub, defaultSessionId, request, opts),
2591
+ wsConnect: connect(stub)
2592
+ };
2593
+ return new Proxy(stub, { get(target, prop) {
2594
+ if (typeof prop === "string" && prop in enhancedMethods) return enhancedMethods[prop];
2595
+ return target[prop];
2596
+ } });
2597
+ }
2598
+ function enhanceSession(stub, rpcSession) {
2599
+ return {
2600
+ ...rpcSession,
2601
+ terminal: (request, opts) => proxyTerminal(stub, rpcSession.id, request, opts)
2602
+ };
2559
2603
  }
2560
2604
  function connect(stub) {
2561
2605
  return async (request, port) => {
@@ -2657,14 +2701,23 @@ var Sandbox = class extends Container {
2657
2701
  await this.ctx.storage.put("keepAliveEnabled", keepAlive);
2658
2702
  }
2659
2703
  async setEnvVars(envVars) {
2704
+ const { toSet, toUnset } = partitionEnvVars(envVars);
2705
+ for (const key of toUnset) delete this.envVars[key];
2660
2706
  this.envVars = {
2661
2707
  ...this.envVars,
2662
- ...envVars
2708
+ ...toSet
2663
2709
  };
2664
- if (this.defaultSession) for (const [key, value] of Object.entries(envVars)) {
2665
- const exportCommand = `export ${key}='${value.replace(/'/g, "'\\''")}'`;
2666
- const result = await this.client.commands.execute(exportCommand, this.defaultSession);
2667
- if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
2710
+ if (this.defaultSession) {
2711
+ for (const key of toUnset) {
2712
+ const unsetCommand = `unset ${key}`;
2713
+ const result = await this.client.commands.execute(unsetCommand, this.defaultSession);
2714
+ if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
2715
+ }
2716
+ for (const [key, value] of Object.entries(toSet)) {
2717
+ const exportCommand = `export ${key}=${shellEscape(value)}`;
2718
+ const result = await this.client.commands.execute(exportCommand, this.defaultSession);
2719
+ if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
2720
+ }
2668
2721
  }
2669
2722
  }
2670
2723
  /**
@@ -3422,7 +3475,7 @@ var Sandbox = class extends Container {
3422
3475
  const requestOptions = {
3423
3476
  ...options?.processId !== void 0 && { processId: options.processId },
3424
3477
  ...options?.timeout !== void 0 && { timeoutMs: options.timeout },
3425
- ...options?.env !== void 0 && { env: options.env },
3478
+ ...options?.env !== void 0 && { env: filterEnvVars(options.env) },
3426
3479
  ...options?.cwd !== void 0 && { cwd: options.cwd },
3427
3480
  ...options?.encoding !== void 0 && { encoding: options.encoding },
3428
3481
  ...options?.autoCleanup !== void 0 && { autoCleanup: options.autoCleanup }
@@ -3590,6 +3643,30 @@ var Sandbox = class extends Container {
3590
3643
  const session = sessionId ?? await this.ensureDefaultSession();
3591
3644
  return this.client.files.exists(path, session);
3592
3645
  }
3646
+ /**
3647
+ * Expose a port and get a preview URL for accessing services running in the sandbox
3648
+ *
3649
+ * @param port - Port number to expose (1024-65535)
3650
+ * @param options - Configuration options
3651
+ * @param options.hostname - Your Worker's domain name (required for preview URL construction)
3652
+ * @param options.name - Optional friendly name for the port
3653
+ * @param options.token - Optional custom token for the preview URL (1-16 characters: lowercase letters, numbers, hyphens, underscores)
3654
+ * If not provided, a random 16-character token will be generated automatically
3655
+ * @returns Preview URL information including the full URL, port number, and optional name
3656
+ *
3657
+ * @example
3658
+ * // With auto-generated token
3659
+ * const { url } = await sandbox.exposePort(8080, { hostname: 'example.com' });
3660
+ * // url: https://8080-sandbox-id-abc123random4567.example.com
3661
+ *
3662
+ * @example
3663
+ * // With custom token for stable URLs across deployments
3664
+ * const { url } = await sandbox.exposePort(8080, {
3665
+ * hostname: 'example.com',
3666
+ * token: 'my-token-v1'
3667
+ * });
3668
+ * // url: https://8080-sandbox-id-my-token-v1.example.com
3669
+ */
3593
3670
  async exposePort(port, options) {
3594
3671
  if (options.hostname.endsWith(".workers.dev")) throw new CustomDomainRequiredError({
3595
3672
  code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
@@ -3598,11 +3675,17 @@ var Sandbox = class extends Container {
3598
3675
  httpStatus: 400,
3599
3676
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3600
3677
  });
3601
- const sessionId = await this.ensureDefaultSession();
3602
- await this.client.ports.exposePort(port, sessionId, options?.name);
3603
3678
  if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
3604
- const token = this.generatePortToken();
3679
+ let token;
3680
+ if (options.token !== void 0) {
3681
+ this.validateCustomToken(options.token);
3682
+ token = options.token;
3683
+ } else token = this.generatePortToken();
3605
3684
  const tokens = await this.ctx.storage.get("portTokens") || {};
3685
+ const existingPort = Object.entries(tokens).find(([p, t]) => t === token && p !== port.toString());
3686
+ if (existingPort) throw new SecurityError(`Token '${token}' is already in use by port ${existingPort[0]}. Please use a different token.`);
3687
+ const sessionId = await this.ensureDefaultSession();
3688
+ await this.client.ports.exposePort(port, sessionId, options?.name);
3606
3689
  tokens[port.toString()] = token;
3607
3690
  await this.ctx.storage.put("portTokens", tokens);
3608
3691
  return {
@@ -3652,12 +3735,21 @@ var Sandbox = class extends Container {
3652
3735
  this.logger.error("Port is exposed but has no token - bug detected", void 0, { port });
3653
3736
  return false;
3654
3737
  }
3655
- return storedToken === token;
3738
+ if (storedToken.length !== token.length) return false;
3739
+ const encoder = new TextEncoder();
3740
+ const a = encoder.encode(storedToken);
3741
+ const b = encoder.encode(token);
3742
+ return crypto.subtle.timingSafeEqual(a, b);
3743
+ }
3744
+ validateCustomToken(token) {
3745
+ if (token.length === 0) throw new SecurityError(`Custom token cannot be empty.`);
3746
+ if (token.length > 16) throw new SecurityError(`Custom token too long. Maximum 16 characters allowed. Received: ${token.length} characters.`);
3747
+ if (!/^[a-z0-9_]+$/.test(token)) throw new SecurityError(`Custom token must contain only lowercase letters (a-z), numbers (0-9), and underscores (_). Invalid token provided.`);
3656
3748
  }
3657
3749
  generatePortToken() {
3658
3750
  const array = new Uint8Array(12);
3659
3751
  crypto.getRandomValues(array);
3660
- return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
3752
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "_").replace(/\//g, "_").replace(/=/g, "").toLowerCase();
3661
3753
  }
3662
3754
  constructPreviewUrl(port, sandboxId, hostname, token) {
3663
3755
  if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be between 1024-65535 and not reserved.`);
@@ -3690,11 +3782,11 @@ var Sandbox = class extends Container {
3690
3782
  */
3691
3783
  async createSession(options) {
3692
3784
  const sessionId = options?.id || `session-${Date.now()}`;
3693
- const mergedEnv = {
3785
+ const filteredEnv = filterEnvVars({
3694
3786
  ...this.envVars,
3695
3787
  ...options?.env ?? {}
3696
- };
3697
- const envPayload = Object.keys(mergedEnv).length > 0 ? mergedEnv : void 0;
3788
+ });
3789
+ const envPayload = Object.keys(filteredEnv).length > 0 ? filteredEnv : void 0;
3698
3790
  await this.client.utils.createSession({
3699
3791
  id: sessionId,
3700
3792
  ...envPayload && { env: envPayload },
@@ -3734,13 +3826,10 @@ var Sandbox = class extends Container {
3734
3826
  timestamp: response.timestamp
3735
3827
  };
3736
3828
  }
3737
- /**
3738
- * Internal helper to create ExecutionSession wrapper for a given sessionId
3739
- * Used by both createSession and getSession
3740
- */
3741
3829
  getSessionWrapper(sessionId) {
3742
3830
  return {
3743
3831
  id: sessionId,
3832
+ terminal: null,
3744
3833
  exec: (command, options) => this.execWithSession(command, sessionId, options),
3745
3834
  execStream: (command, options) => this.execStreamWithSession(command, sessionId, options),
3746
3835
  startProcess: (command, options) => this.startProcess(command, options, sessionId),
@@ -3774,9 +3863,15 @@ var Sandbox = class extends Container {
3774
3863
  sessionId
3775
3864
  }),
3776
3865
  setEnvVars: async (envVars) => {
3866
+ const { toSet, toUnset } = partitionEnvVars(envVars);
3777
3867
  try {
3778
- for (const [key, value] of Object.entries(envVars)) {
3779
- const exportCommand = `export ${key}='${value.replace(/'/g, "'\\''")}'`;
3868
+ for (const key of toUnset) {
3869
+ const unsetCommand = `unset ${key}`;
3870
+ const result = await this.client.commands.execute(unsetCommand, sessionId);
3871
+ if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
3872
+ }
3873
+ for (const [key, value] of Object.entries(toSet)) {
3874
+ const exportCommand = `export ${key}=${shellEscape(value)}`;
3780
3875
  const result = await this.client.commands.execute(exportCommand, sessionId);
3781
3876
  if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
3782
3877
  }
@@ -3933,5 +4028,5 @@ async function collectFile(stream) {
3933
4028
  }
3934
4029
 
3935
4030
  //#endregion
3936
- export { BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyToSandbox, responseToAsyncIterable, streamFile };
4031
+ export { BucketMountError, CodeInterpreter, CommandClient, FileClient, GitClient, InvalidMountConfigError, MissingCredentialsError, PortClient, ProcessClient, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, Sandbox, SandboxClient, UtilityClient, asyncIterableToSSEStream, collectFile, getSandbox, isExecResult, isProcess, isProcessStatus, parseSSEStream, proxyTerminal, proxyToSandbox, responseToAsyncIterable, streamFile };
3937
4032
  //# sourceMappingURL=index.js.map