@cloudflare/sandbox 0.11.0 → 0.12.0

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.
@@ -1,6 +1,6 @@
1
1
  import { _ as GitLogger, b as getEnvString, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as DEFAULT_GIT_CLONE_TIMEOUT_MS, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, p as logCanonicalEvent, r as isWSResponse, t as generateRequestId, u as createLogger, v as extractRepoName, x as partitionEnvVars, y as filterEnvVars } from "./dist-B_eXrP83.js";
2
- import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-COsTRno_.js";
3
- import { Container, getContainer, switchPort } from "@cloudflare/containers";
2
+ import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-aRUdk9K8.js";
3
+ import { Container, ContainerProxy, getContainer, switchPort } from "@cloudflare/containers";
4
4
  import { AwsClient } from "aws4fetch";
5
5
  import { RpcSession, RpcTarget } from "capnweb";
6
6
  import path from "node:path/posix";
@@ -629,42 +629,6 @@ var BackupRestoreError = class extends SandboxError {
629
629
  return this.context.backupId;
630
630
  }
631
631
  };
632
- var DesktopNotStartedError = class extends SandboxError {
633
- constructor(errorResponse) {
634
- super(errorResponse);
635
- this.name = "DesktopNotStartedError";
636
- }
637
- };
638
- var DesktopStartFailedError = class extends SandboxError {
639
- constructor(errorResponse) {
640
- super(errorResponse);
641
- this.name = "DesktopStartFailedError";
642
- }
643
- };
644
- var DesktopUnavailableError = class extends SandboxError {
645
- constructor(errorResponse) {
646
- super(errorResponse);
647
- this.name = "DesktopUnavailableError";
648
- }
649
- };
650
- var DesktopProcessCrashedError = class extends SandboxError {
651
- constructor(errorResponse) {
652
- super(errorResponse);
653
- this.name = "DesktopProcessCrashedError";
654
- }
655
- };
656
- var DesktopInvalidOptionsError = class extends SandboxError {
657
- constructor(errorResponse) {
658
- super(errorResponse);
659
- this.name = "DesktopInvalidOptionsError";
660
- }
661
- };
662
- var DesktopInvalidCoordinatesError = class extends SandboxError {
663
- constructor(errorResponse) {
664
- super(errorResponse);
665
- this.name = "DesktopInvalidCoordinatesError";
666
- }
667
- };
668
632
  /**
669
633
  * Raised when the capnweb WebSocket session itself fails on the SDK side.
670
634
  * Unlike the rest of the SandboxError tree, the container never produces
@@ -746,12 +710,6 @@ function createErrorFromResponse(errorResponse, options) {
746
710
  case ErrorCode.INTERPRETER_NOT_READY: return new InterpreterNotReadyError(errorResponse);
747
711
  case ErrorCode.CONTEXT_NOT_FOUND: return new ContextNotFoundError(errorResponse);
748
712
  case ErrorCode.CODE_EXECUTION_ERROR: return new CodeExecutionError(errorResponse);
749
- case ErrorCode.DESKTOP_NOT_STARTED: return new DesktopNotStartedError(errorResponse);
750
- case ErrorCode.DESKTOP_START_FAILED: return new DesktopStartFailedError(errorResponse);
751
- case ErrorCode.DESKTOP_UNAVAILABLE: return new DesktopUnavailableError(errorResponse);
752
- case ErrorCode.DESKTOP_PROCESS_CRASHED: return new DesktopProcessCrashedError(errorResponse);
753
- case ErrorCode.DESKTOP_INVALID_OPTIONS: return new DesktopInvalidOptionsError(errorResponse);
754
- case ErrorCode.DESKTOP_INVALID_COORDINATES: return new DesktopInvalidCoordinatesError(errorResponse);
755
713
  case ErrorCode.RPC_TRANSPORT_ERROR: return new RPCTransportError(errorResponse, options);
756
714
  case ErrorCode.VALIDATION_FAILED: return new ValidationFailedError(errorResponse);
757
715
  case ErrorCode.INVALID_JSON_RESPONSE:
@@ -1590,21 +1548,21 @@ var BaseHttpClient = class {
1590
1548
  body: JSON.stringify(data),
1591
1549
  ...requestOptions
1592
1550
  });
1593
- return this.handleResponse(response, responseHandler);
1551
+ return await this.handleResponse(response, responseHandler);
1594
1552
  }
1595
1553
  /**
1596
1554
  * Make a GET request
1597
1555
  */
1598
1556
  async get(endpoint, responseHandler) {
1599
1557
  const response = await this.doFetch(endpoint, { method: "GET" });
1600
- return this.handleResponse(response, responseHandler);
1558
+ return await this.handleResponse(response, responseHandler);
1601
1559
  }
1602
1560
  /**
1603
1561
  * Make a DELETE request
1604
1562
  */
1605
1563
  async delete(endpoint, responseHandler) {
1606
1564
  const response = await this.doFetch(endpoint, { method: "DELETE" });
1607
- return this.handleResponse(response, responseHandler);
1565
+ return await this.handleResponse(response, responseHandler);
1608
1566
  }
1609
1567
  /**
1610
1568
  * Handle HTTP response with error checking and parsing
@@ -1673,7 +1631,7 @@ var BaseHttpClient = class {
1673
1631
  headers: { "Content-Type": "application/json" },
1674
1632
  body: body && method === "POST" ? JSON.stringify(body) : void 0
1675
1633
  });
1676
- return this.handleStreamResponse(response);
1634
+ return await this.handleStreamResponse(response);
1677
1635
  }
1678
1636
  };
1679
1637
 
@@ -1782,240 +1740,6 @@ var CommandClient = class extends BaseHttpClient {
1782
1740
  }
1783
1741
  };
1784
1742
 
1785
- //#endregion
1786
- //#region src/clients/desktop-client.ts
1787
- /**
1788
- * Decode a base64-encoded screenshot payload into a Uint8Array.
1789
- * Shared with the RPC client wrapper, which receives base64 over the
1790
- * wire and needs the same `format: 'bytes'` convenience.
1791
- */
1792
- function base64ToBytes(data) {
1793
- const binaryString = atob(data);
1794
- const bytes = new Uint8Array(binaryString.length);
1795
- for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
1796
- return bytes;
1797
- }
1798
- /**
1799
- * Client for desktop environment lifecycle, input, and screen operations
1800
- */
1801
- var DesktopClient = class extends BaseHttpClient {
1802
- /**
1803
- * Start the desktop environment with optional resolution and DPI.
1804
- */
1805
- async start(options) {
1806
- try {
1807
- const data = {
1808
- ...options?.resolution !== void 0 && { resolution: options.resolution },
1809
- ...options?.dpi !== void 0 && { dpi: options.dpi }
1810
- };
1811
- return await this.post("/api/desktop/start", data);
1812
- } catch (error) {
1813
- this.options.onError?.(error instanceof Error ? error.message : String(error));
1814
- throw error;
1815
- }
1816
- }
1817
- /**
1818
- * Stop the desktop environment and all related processes.
1819
- */
1820
- async stop() {
1821
- try {
1822
- return await this.post("/api/desktop/stop", {});
1823
- } catch (error) {
1824
- this.options.onError?.(error instanceof Error ? error.message : String(error));
1825
- throw error;
1826
- }
1827
- }
1828
- /**
1829
- * Get desktop lifecycle and process health status.
1830
- */
1831
- async status() {
1832
- return await this.get("/api/desktop/status");
1833
- }
1834
- async screenshot(options) {
1835
- const wantsBytes = options?.format === "bytes";
1836
- const data = {
1837
- format: "base64",
1838
- ...options?.imageFormat !== void 0 && { imageFormat: options.imageFormat },
1839
- ...options?.quality !== void 0 && { quality: options.quality },
1840
- ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1841
- };
1842
- const response = await this.post("/api/desktop/screenshot", data);
1843
- if (wantsBytes) return {
1844
- ...response,
1845
- data: base64ToBytes(response.data)
1846
- };
1847
- return response;
1848
- }
1849
- async screenshotRegion(region, options) {
1850
- const wantsBytes = options?.format === "bytes";
1851
- const data = {
1852
- region,
1853
- format: "base64",
1854
- ...options?.imageFormat !== void 0 && { imageFormat: options.imageFormat },
1855
- ...options?.quality !== void 0 && { quality: options.quality },
1856
- ...options?.showCursor !== void 0 && { showCursor: options.showCursor }
1857
- };
1858
- const response = await this.post("/api/desktop/screenshot/region", data);
1859
- if (wantsBytes) return {
1860
- ...response,
1861
- data: base64ToBytes(response.data)
1862
- };
1863
- return response;
1864
- }
1865
- /**
1866
- * Single-click at the given coordinates.
1867
- */
1868
- async click(x, y, options) {
1869
- await this.post("/api/desktop/mouse/click", {
1870
- x,
1871
- y,
1872
- button: options?.button ?? "left",
1873
- clickCount: 1
1874
- });
1875
- }
1876
- /**
1877
- * Double-click at the given coordinates.
1878
- */
1879
- async doubleClick(x, y, options) {
1880
- await this.post("/api/desktop/mouse/click", {
1881
- x,
1882
- y,
1883
- button: options?.button ?? "left",
1884
- clickCount: 2
1885
- });
1886
- }
1887
- /**
1888
- * Triple-click at the given coordinates.
1889
- */
1890
- async tripleClick(x, y, options) {
1891
- await this.post("/api/desktop/mouse/click", {
1892
- x,
1893
- y,
1894
- button: options?.button ?? "left",
1895
- clickCount: 3
1896
- });
1897
- }
1898
- /**
1899
- * Right-click at the given coordinates.
1900
- */
1901
- async rightClick(x, y) {
1902
- await this.post("/api/desktop/mouse/click", {
1903
- x,
1904
- y,
1905
- button: "right",
1906
- clickCount: 1
1907
- });
1908
- }
1909
- /**
1910
- * Middle-click at the given coordinates.
1911
- */
1912
- async middleClick(x, y) {
1913
- await this.post("/api/desktop/mouse/click", {
1914
- x,
1915
- y,
1916
- button: "middle",
1917
- clickCount: 1
1918
- });
1919
- }
1920
- /**
1921
- * Press and hold a mouse button.
1922
- */
1923
- async mouseDown(x, y, options) {
1924
- await this.post("/api/desktop/mouse/down", {
1925
- ...x !== void 0 && { x },
1926
- ...y !== void 0 && { y },
1927
- button: options?.button ?? "left"
1928
- });
1929
- }
1930
- /**
1931
- * Release a held mouse button.
1932
- */
1933
- async mouseUp(x, y, options) {
1934
- await this.post("/api/desktop/mouse/up", {
1935
- ...x !== void 0 && { x },
1936
- ...y !== void 0 && { y },
1937
- button: options?.button ?? "left"
1938
- });
1939
- }
1940
- /**
1941
- * Move the mouse cursor to coordinates.
1942
- */
1943
- async moveMouse(x, y) {
1944
- await this.post("/api/desktop/mouse/move", {
1945
- x,
1946
- y
1947
- });
1948
- }
1949
- /**
1950
- * Drag from start coordinates to end coordinates.
1951
- */
1952
- async drag(startX, startY, endX, endY, options) {
1953
- await this.post("/api/desktop/mouse/drag", {
1954
- startX,
1955
- startY,
1956
- endX,
1957
- endY,
1958
- button: options?.button ?? "left"
1959
- });
1960
- }
1961
- /**
1962
- * Scroll at coordinates in the specified direction.
1963
- */
1964
- async scroll(x, y, direction, amount = 3) {
1965
- await this.post("/api/desktop/mouse/scroll", {
1966
- x,
1967
- y,
1968
- direction,
1969
- amount
1970
- });
1971
- }
1972
- /**
1973
- * Get the current cursor coordinates.
1974
- */
1975
- async getCursorPosition() {
1976
- return await this.get("/api/desktop/mouse/position");
1977
- }
1978
- /**
1979
- * Type text into the focused element.
1980
- */
1981
- async type(text, options) {
1982
- await this.post("/api/desktop/keyboard/type", {
1983
- text,
1984
- ...options?.delayMs !== void 0 && { delayMs: options.delayMs }
1985
- });
1986
- }
1987
- /**
1988
- * Press and release a key or key combination.
1989
- */
1990
- async press(key) {
1991
- await this.post("/api/desktop/keyboard/press", { key });
1992
- }
1993
- /**
1994
- * Press and hold a key.
1995
- */
1996
- async keyDown(key) {
1997
- await this.post("/api/desktop/keyboard/down", { key });
1998
- }
1999
- /**
2000
- * Release a held key.
2001
- */
2002
- async keyUp(key) {
2003
- await this.post("/api/desktop/keyboard/up", { key });
2004
- }
2005
- /**
2006
- * Get the active desktop screen size.
2007
- */
2008
- async getScreenSize() {
2009
- return await this.get("/api/desktop/screen/size");
2010
- }
2011
- /**
2012
- * Get health status for a specific desktop process.
2013
- */
2014
- async getProcessStatus(name) {
2015
- return this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
2016
- }
2017
- };
2018
-
2019
1743
  //#endregion
2020
1744
  //#region src/clients/file-client.ts
2021
1745
  /**
@@ -2618,7 +2342,6 @@ var SandboxClient = class {
2618
2342
  git;
2619
2343
  interpreter;
2620
2344
  utils;
2621
- desktop;
2622
2345
  watch;
2623
2346
  /**
2624
2347
  * Tunnels are RPC-only — the route-based transport does not implement them.
@@ -2651,7 +2374,6 @@ var SandboxClient = class {
2651
2374
  this.git = new GitClient(clientOptions);
2652
2375
  this.interpreter = new InterpreterClient(clientOptions);
2653
2376
  this.utils = new UtilityClient(clientOptions);
2654
- this.desktop = new DesktopClient(clientOptions);
2655
2377
  this.watch = new WatchClient(clientOptions);
2656
2378
  }
2657
2379
  /**
@@ -2672,7 +2394,6 @@ var SandboxClient = class {
2672
2394
  this.git.setRetryTimeoutMs(ms);
2673
2395
  this.interpreter.setRetryTimeoutMs(ms);
2674
2396
  this.utils.setRetryTimeoutMs(ms);
2675
- this.desktop.setRetryTimeoutMs(ms);
2676
2397
  this.watch.setRetryTimeoutMs(ms);
2677
2398
  }
2678
2399
  }
@@ -3301,32 +3022,6 @@ var ContainerControlClient = class {
3301
3022
  get backup() {
3302
3023
  return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
3303
3024
  }
3304
- get desktop() {
3305
- const stub = wrapStub(this.getConnection().rpc().desktop, this.renewActivity);
3306
- const wire = stub;
3307
- return new Proxy(stub, { get(target, prop, receiver) {
3308
- if (prop === "screenshot") return async (options) => {
3309
- const { format, ...rest } = options ?? {};
3310
- const result = await wire.screenshot(rest);
3311
- return format === "bytes" ? {
3312
- ...result,
3313
- data: base64ToBytes(result.data)
3314
- } : result;
3315
- };
3316
- if (prop === "screenshotRegion") return async (region, options) => {
3317
- const { format, ...rest } = options ?? {};
3318
- const result = await wire.screenshotRegion({
3319
- region,
3320
- ...rest
3321
- });
3322
- return format === "bytes" ? {
3323
- ...result,
3324
- data: base64ToBytes(result.data)
3325
- } : result;
3326
- };
3327
- return Reflect.get(target, prop, receiver);
3328
- } });
3329
- }
3330
3025
  get watch() {
3331
3026
  return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
3332
3027
  }
@@ -4775,6 +4470,413 @@ const r2EgressHandler = async (request, env$1, ctx) => {
4775
4470
  }
4776
4471
  };
4777
4472
 
4473
+ //#endregion
4474
+ //#region src/storage-mount/s3-credential-proxy-handler.ts
4475
+ const PER_MOUNT_SUFFIX = ".s3-credential-proxy.internal";
4476
+ const SELF_TEST_PATH = "/__sandbox_credential_proxy_self_test__";
4477
+ const DIAGNOSTICS_PATH = "/__sandbox_credential_proxy_diagnostics__";
4478
+ const DEFAULT_SLOW_REQUEST_MS = 1e3;
4479
+ const ERROR_RESPONSE_BODY_LIMIT = 2048;
4480
+ const MAX_DIAGNOSTIC_EVENTS = 500;
4481
+ const DUMMY_AUTH_HEADERS = new Set([
4482
+ "authorization",
4483
+ "x-amz-date",
4484
+ "x-amz-content-sha256",
4485
+ "x-amz-security-token",
4486
+ "x-goog-date",
4487
+ "x-goog-content-sha256"
4488
+ ]);
4489
+ const sigV4ClientCache = /* @__PURE__ */ new Map();
4490
+ const directoryMarkerCache = /* @__PURE__ */ new Map();
4491
+ const credentialProxyDiagnosticEvents = [];
4492
+ let credentialProxyDiagnosticEventCount = 0;
4493
+ function evictSigV4ClientCacheEntry(mountId) {
4494
+ sigV4ClientCache.delete(mountId);
4495
+ }
4496
+ function evictDirectoryMarkerCacheForMount(mountId) {
4497
+ const prefix = `${mountId}:`;
4498
+ for (const key of directoryMarkerCache.keys()) if (key.startsWith(prefix)) directoryMarkerCache.delete(key);
4499
+ }
4500
+ function toHex(buffer) {
4501
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
4502
+ }
4503
+ async function sha256Hex(data) {
4504
+ return toHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data)));
4505
+ }
4506
+ async function hmacSHA256(key, data) {
4507
+ const cryptoKey = await crypto.subtle.importKey("raw", key, {
4508
+ name: "HMAC",
4509
+ hash: "SHA-256"
4510
+ }, false, ["sign"]);
4511
+ return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data));
4512
+ }
4513
+ function detectS3Region(provider, endpoint) {
4514
+ if (provider === "r2") return "auto";
4515
+ try {
4516
+ const host = new URL(endpoint).hostname;
4517
+ const m = host.match(/s3[.-]([a-z0-9-]+)\.amazonaws\.com/);
4518
+ if (m && m[1] !== "amazonaws") return m[1];
4519
+ if (host === "s3.amazonaws.com") return "us-east-1";
4520
+ } catch {}
4521
+ return "auto";
4522
+ }
4523
+ function buildCleanHeaders(original) {
4524
+ const clean = new Headers();
4525
+ for (const [k, v] of original) {
4526
+ const lower = k.toLowerCase();
4527
+ if (!DUMMY_AUTH_HEADERS.has(lower) && lower !== "host") clean.set(k, v);
4528
+ }
4529
+ const contentSHA256 = original.get("x-amz-content-sha256");
4530
+ if (contentSHA256 && isValidContentSHA256(contentSHA256)) clean.set("x-amz-content-sha256", contentSHA256);
4531
+ return clean;
4532
+ }
4533
+ function isValidContentSHA256(value) {
4534
+ return value === "UNSIGNED-PAYLOAD" || /^[a-fA-F0-9]{64}$/.test(value);
4535
+ }
4536
+ function getCredentialProxyDebugConfig(env$1) {
4537
+ const envRecord = env$1;
4538
+ const enabled = envRecord.SANDBOX_CREDENTIAL_PROXY_DEBUG === "true";
4539
+ const diagnosticsEndpointEnabled = envRecord.SANDBOX_CREDENTIAL_PROXY_DIAGNOSTICS_ENDPOINT === "true";
4540
+ const configuredSlowRequestMs = Number(envRecord.SANDBOX_CREDENTIAL_PROXY_SLOW_REQUEST_MS);
4541
+ return {
4542
+ diagnosticsEndpointEnabled,
4543
+ enabled,
4544
+ slowRequestMs: Number.isFinite(configuredSlowRequestMs) && configuredSlowRequestMs >= 0 ? configuredSlowRequestMs : DEFAULT_SLOW_REQUEST_MS
4545
+ };
4546
+ }
4547
+ function recordCredentialProxyDiagnosticEvent(event) {
4548
+ credentialProxyDiagnosticEvents.push(event);
4549
+ credentialProxyDiagnosticEventCount++;
4550
+ while (credentialProxyDiagnosticEvents.length > MAX_DIAGNOSTIC_EVENTS) credentialProxyDiagnosticEvents.shift();
4551
+ }
4552
+ function getCredentialProxyDiagnosticsResponse(url, containerId) {
4553
+ const since = Number(url.searchParams.get("since") ?? "0");
4554
+ const bufferStartCount = credentialProxyDiagnosticEventCount - credentialProxyDiagnosticEvents.length;
4555
+ const events = credentialProxyDiagnosticEvents.filter((event, index) => {
4556
+ if (event.containerId !== containerId) return false;
4557
+ return !Number.isFinite(since) || bufferStartCount + index >= since;
4558
+ });
4559
+ return Response.json({
4560
+ nextCursor: credentialProxyDiagnosticEventCount,
4561
+ events
4562
+ });
4563
+ }
4564
+ async function withCredentialProxyDiagnostics(requestInfo, debugConfig, containerId, path$1, operation) {
4565
+ const started = Date.now();
4566
+ try {
4567
+ const response = await operation();
4568
+ const durationMs = Date.now() - started;
4569
+ if (debugConfig.enabled) recordCredentialProxyDiagnosticEvent({
4570
+ ...requestInfo,
4571
+ containerId,
4572
+ durationMs,
4573
+ ok: response.ok,
4574
+ path: path$1,
4575
+ status: response.status,
4576
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4577
+ });
4578
+ if (debugConfig.enabled || durationMs >= debugConfig.slowRequestMs) console.info("sandbox.s3_credential_proxy.request", {
4579
+ ...requestInfo,
4580
+ durationMs,
4581
+ ok: response.ok,
4582
+ status: response.status,
4583
+ responseContentLength: response.headers.get("content-length")
4584
+ });
4585
+ if (!response.ok) {
4586
+ const responseForLog = response.clone();
4587
+ const requestInfoSnapshot = { ...requestInfo };
4588
+ responseForLog.text().then((body) => {
4589
+ console.warn("sandbox.s3_credential_proxy.upstream_error", {
4590
+ ...requestInfoSnapshot,
4591
+ durationMs,
4592
+ status: response.status,
4593
+ statusText: response.statusText,
4594
+ errorBody: body.slice(0, ERROR_RESPONSE_BODY_LIMIT)
4595
+ });
4596
+ }).catch(() => {});
4597
+ }
4598
+ return response;
4599
+ } catch (error) {
4600
+ const durationMs = Date.now() - started;
4601
+ console.warn("sandbox.s3_credential_proxy.request_error", {
4602
+ ...requestInfo,
4603
+ durationMs,
4604
+ error: error instanceof Error ? error.message : String(error)
4605
+ });
4606
+ throw error;
4607
+ }
4608
+ }
4609
+ function getSigV4Client(mountId, endpoint, provider, credentials, region) {
4610
+ const cached = sigV4ClientCache.get(mountId);
4611
+ if (cached && cached.accessKeyId === credentials.accessKeyId && cached.secretAccessKey === credentials.secretAccessKey && cached.endpoint === endpoint && cached.provider === provider && cached.region === region) return cached.client;
4612
+ const client = new AwsClient({
4613
+ accessKeyId: credentials.accessKeyId,
4614
+ secretAccessKey: credentials.secretAccessKey,
4615
+ service: "s3",
4616
+ region,
4617
+ retries: 0
4618
+ });
4619
+ sigV4ClientCache.set(mountId, {
4620
+ client,
4621
+ accessKeyId: credentials.accessKeyId,
4622
+ secretAccessKey: credentials.secretAccessKey,
4623
+ endpoint,
4624
+ provider,
4625
+ region
4626
+ });
4627
+ return client;
4628
+ }
4629
+ function encodeCanonicalQueryPart(value) {
4630
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
4631
+ }
4632
+ function getCanonicalURI(url) {
4633
+ return url.pathname.split("/").map((segment) => segment.split(/(%[0-9A-Fa-f]{2})/g).map((part) => /^%[0-9A-Fa-f]{2}$/.test(part) ? part.toUpperCase() : encodeCanonicalQueryPart(part)).join("")).join("/");
4634
+ }
4635
+ function getCanonicalQueryString(url) {
4636
+ const query = url.search.startsWith("?") ? url.search.slice(1) : url.search;
4637
+ if (!query) return "";
4638
+ return query.split("&").map((part) => {
4639
+ const separatorIndex = part.indexOf("=");
4640
+ if (separatorIndex === -1) return [part, ""];
4641
+ return [part.slice(0, separatorIndex), part.slice(separatorIndex + 1)];
4642
+ }).map(([key, value]) => [encodeCanonicalQueryPart(decodeURIEncodedQueryPart(key)), encodeCanonicalQueryPart(decodeURIEncodedQueryPart(value))]).sort(([leftKey, leftValue], [rightKey, rightValue]) => {
4643
+ if (leftKey < rightKey) return -1;
4644
+ if (leftKey > rightKey) return 1;
4645
+ if (leftValue < rightValue) return -1;
4646
+ if (leftValue > rightValue) return 1;
4647
+ return 0;
4648
+ }).map(([key, value]) => `${key}=${value}`).join("&");
4649
+ }
4650
+ function decodeURIEncodedQueryPart(value) {
4651
+ try {
4652
+ return decodeURIComponent(value);
4653
+ } catch {
4654
+ return value;
4655
+ }
4656
+ }
4657
+ function getSigV4PayloadHash(headers) {
4658
+ const existingHash = headers.get("x-amz-content-sha256");
4659
+ if (existingHash && existingHash !== "UNSIGNED-PAYLOAD") return {
4660
+ hash: existingHash,
4661
+ mode: "signed"
4662
+ };
4663
+ return {
4664
+ hash: "UNSIGNED-PAYLOAD",
4665
+ mode: "unsigned"
4666
+ };
4667
+ }
4668
+ function isZeroLengthDirectoryMarkerPUT(request, realPath) {
4669
+ return request.method.toUpperCase() === "PUT" && request.headers.get("content-length") === "0" && realPath.endsWith("/");
4670
+ }
4671
+ function isDirectoryMarkerHEAD(request) {
4672
+ return request.method.toUpperCase() === "HEAD";
4673
+ }
4674
+ function getDirectoryMarkerCacheKey(mountId, realPath) {
4675
+ return `${mountId}:${realPath.replace(/\/+$/, "")}`;
4676
+ }
4677
+ function getDirectoryMarkerResponseHeaders(request) {
4678
+ const headers = [
4679
+ ["Accept-Ranges", "bytes"],
4680
+ ["Content-Length", "0"],
4681
+ ["ETag", "\"d41d8cd98f00b204e9800998ecf8427e\""],
4682
+ ["Last-Modified", (/* @__PURE__ */ new Date()).toUTCString()]
4683
+ ];
4684
+ const contentType = request.headers.get("content-type");
4685
+ if (contentType) headers.push(["Content-Type", contentType]);
4686
+ for (const [name, value] of request.headers) if (name.toLowerCase().startsWith("x-amz-meta-")) headers.push([name, value]);
4687
+ return headers;
4688
+ }
4689
+ function normalizePrefix(prefix) {
4690
+ if (!prefix) return void 0;
4691
+ return prefix.replace(/^\/+/, "").replace(/\/+$/, "");
4692
+ }
4693
+ function getObjectKeyForPath(realPath, bucket) {
4694
+ const pathSegments = realPath.split("/").filter(Boolean);
4695
+ if (pathSegments[0] !== bucket) return null;
4696
+ return pathSegments.slice(1).join("/");
4697
+ }
4698
+ function isObjectKeyWithinPrefix(objectKey, prefix) {
4699
+ const normalizedPrefix = normalizePrefix(prefix);
4700
+ if (!normalizedPrefix) return true;
4701
+ return objectKey === normalizedPrefix || objectKey.startsWith(`${normalizedPrefix}/`);
4702
+ }
4703
+ function isRequestWithinMountScope(realPath, url, bucket, prefix) {
4704
+ const objectKey = getObjectKeyForPath(realPath, bucket);
4705
+ if (objectKey === null) return false;
4706
+ const requestedPrefix = url.searchParams.get("prefix");
4707
+ if (objectKey !== "" && !isObjectKeyWithinPrefix(objectKey, prefix)) return false;
4708
+ if (objectKey === "" && normalizePrefix(prefix) !== void 0 && url.search !== "" && requestedPrefix === null) return false;
4709
+ if (requestedPrefix !== null) return isObjectKeyWithinPrefix(requestedPrefix, prefix);
4710
+ return true;
4711
+ }
4712
+ function isBucketRootProbe(request, realPath, url, bucket) {
4713
+ const method = request.method.toUpperCase();
4714
+ return (method === "GET" || method === "HEAD") && url.search === "" && getObjectKeyForPath(realPath, bucket) === "";
4715
+ }
4716
+ function deleteDirectoryMarkerCacheEntry(mountId, realPath) {
4717
+ directoryMarkerCache.delete(getDirectoryMarkerCacheKey(mountId, realPath));
4718
+ }
4719
+ function getContentLength(request) {
4720
+ const contentLength = request.headers.get("content-length");
4721
+ if (contentLength === null) return null;
4722
+ const parsed = Number(contentLength);
4723
+ if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
4724
+ return parsed;
4725
+ }
4726
+ function getSigV4ForwardInit(request) {
4727
+ const contentLength = getContentLength(request);
4728
+ if (contentLength === 0) return { body: new Uint8Array(0) };
4729
+ if (contentLength === null || request.body === null) return {};
4730
+ const { readable, writable } = new FixedLengthStream(contentLength);
4731
+ request.body.pipeTo(writable).catch((error) => {
4732
+ writable.abort(error).catch(() => {});
4733
+ });
4734
+ return { body: readable };
4735
+ }
4736
+ function getGCSHeaders(request) {
4737
+ const headers = new Headers();
4738
+ for (const [k, v] of request.headers) {
4739
+ const lower = k.toLowerCase();
4740
+ if (DUMMY_AUTH_HEADERS.has(lower) || lower === "host" || lower === "content-length" || lower === "expect") continue;
4741
+ if (lower.startsWith("x-amz-meta-")) {
4742
+ headers.set(`x-goog-meta-${lower.slice(11)}`, v);
4743
+ continue;
4744
+ }
4745
+ if (lower.startsWith("x-amz-")) continue;
4746
+ headers.set(k, v);
4747
+ }
4748
+ return headers;
4749
+ }
4750
+ async function signAndForwardSigV4(request, mountId, endpoint, provider, credentials, requestInfo) {
4751
+ const signingStarted = Date.now();
4752
+ const region = detectS3Region(provider, endpoint);
4753
+ const payload = getSigV4PayloadHash(request.headers);
4754
+ const client = getSigV4Client(mountId, endpoint, provider, credentials, region);
4755
+ requestInfo.payloadHashMode = payload.mode;
4756
+ requestInfo.clientSetupMs = Date.now() - signingStarted;
4757
+ const upstreamStarted = Date.now();
4758
+ const forwardInit = getSigV4ForwardInit(request);
4759
+ requestInfo.bodyPresent = forwardInit?.body !== void 0 || request.body !== null;
4760
+ const response = await client.fetch(request, forwardInit);
4761
+ requestInfo.upstreamMs = Date.now() - upstreamStarted;
4762
+ return response;
4763
+ }
4764
+ async function signAndForwardGCS(request, credentials, requestInfo) {
4765
+ const url = new URL(request.url);
4766
+ const gcsHeaders = getGCSHeaders(request);
4767
+ const dateStr = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:-]/g, "").replace(/\.\d+Z$/, "")}Z`;
4768
+ const dateOnly = dateStr.slice(0, 8);
4769
+ const location = "auto";
4770
+ const service = "storage";
4771
+ const credentialScope = `${dateOnly}/${location}/${service}/goog4_request`;
4772
+ const bodyHash = "UNSIGNED-PAYLOAD";
4773
+ const headerEntries = [
4774
+ ["host", url.host],
4775
+ ["x-goog-content-sha256", bodyHash],
4776
+ ["x-goog-date", dateStr]
4777
+ ];
4778
+ for (const [k, v] of gcsHeaders) headerEntries.push([k.toLowerCase(), v.trim()]);
4779
+ headerEntries.sort((a, b) => a[0].localeCompare(b[0]));
4780
+ const signedHeaders = headerEntries.map(([k]) => k).join(";");
4781
+ const canonicalHeaders = headerEntries.map(([k, v]) => `${k}:${v}\n`).join("");
4782
+ const stringToSign = [
4783
+ "GOOG4-HMAC-SHA256",
4784
+ dateStr,
4785
+ credentialScope,
4786
+ await sha256Hex([
4787
+ request.method,
4788
+ getCanonicalURI(url),
4789
+ getCanonicalQueryString(url),
4790
+ canonicalHeaders,
4791
+ signedHeaders,
4792
+ bodyHash
4793
+ ].join("\n"))
4794
+ ].join("\n");
4795
+ const signature = toHex(await hmacSHA256(await hmacSHA256(await hmacSHA256(await hmacSHA256(await hmacSHA256(new TextEncoder().encode(`GOOG4${credentials.secretAccessKey}`), dateOnly), location), service), "goog4_request"), stringToSign));
4796
+ const authorization = `GOOG4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
4797
+ const newHeaders = new Headers(gcsHeaders);
4798
+ newHeaders.set("x-goog-date", dateStr);
4799
+ newHeaders.set("x-goog-content-sha256", bodyHash);
4800
+ newHeaders.set("Authorization", authorization);
4801
+ const gcsBody = getContentLength(request) === 0 ? new Uint8Array(0) : request.body;
4802
+ const upstreamStarted = Date.now();
4803
+ const response = await fetch(new Request(request.url, {
4804
+ method: request.method,
4805
+ headers: newHeaders,
4806
+ body: gcsBody
4807
+ }));
4808
+ requestInfo.upstreamMs = Date.now() - upstreamStarted;
4809
+ return response;
4810
+ }
4811
+ const s3CredentialProxyHandler = async (request, env$1, ctx) => {
4812
+ const url = new URL(request.url);
4813
+ if (url.pathname === SELF_TEST_PATH) return new Response("OK", { status: 200 });
4814
+ const debugConfig = getCredentialProxyDebugConfig(env$1);
4815
+ if (url.pathname === DIAGNOSTICS_PATH) {
4816
+ if (!debugConfig.enabled || !debugConfig.diagnosticsEndpointEnabled) return new Response("Not Found", { status: 404 });
4817
+ return getCredentialProxyDiagnosticsResponse(url, ctx.containerId);
4818
+ }
4819
+ const segments = url.pathname.split("/").filter(Boolean);
4820
+ const hostname = url.hostname;
4821
+ let mountId;
4822
+ let realPath;
4823
+ if (hostname.endsWith(PER_MOUNT_SUFFIX)) {
4824
+ mountId = hostname.slice(0, -29);
4825
+ realPath = url.pathname;
4826
+ } else {
4827
+ mountId = segments[0] ?? null;
4828
+ realPath = mountId ? url.pathname.slice(`/${mountId}`.length) || "/" : "/";
4829
+ }
4830
+ if (!mountId) return new Response("Bad Request: missing mount ID", { status: 400 });
4831
+ const mount = ctx.params?.mounts[mountId];
4832
+ if (!mount) return new Response(`Forbidden: unknown mount ID "${mountId}"`, { status: 403 });
4833
+ if (mount.readOnly) {
4834
+ const method = request.method.toUpperCase();
4835
+ if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") return new Response("Forbidden: bucket mount is read-only", { status: 403 });
4836
+ }
4837
+ const realUrl = new URL(realPath + (url.search || ""), mount.endpoint);
4838
+ if (isBucketRootProbe(request, realPath, url, mount.bucket)) return new Response(null, { status: 200 });
4839
+ if (!isRequestWithinMountScope(realPath, url, mount.bucket, mount.prefix)) return new Response("Forbidden: request is outside mounted bucket scope", { status: 403 });
4840
+ const cleanHeaders = buildCleanHeaders(request.headers);
4841
+ const cleanRequest = new Request(realUrl.toString(), {
4842
+ method: request.method,
4843
+ headers: cleanHeaders,
4844
+ body: request.body
4845
+ });
4846
+ const requestInfo = {
4847
+ authStrategy: mount.authStrategy,
4848
+ bucket: mount.bucket,
4849
+ contentLength: request.headers.get("content-length"),
4850
+ method: request.method,
4851
+ mountId,
4852
+ query: [...url.searchParams.keys()].sort()
4853
+ };
4854
+ if (isZeroLengthDirectoryMarkerPUT(cleanRequest, realPath)) {
4855
+ const responseHeaders = getDirectoryMarkerResponseHeaders(cleanRequest);
4856
+ requestInfo.bodyPresent = request.body !== null;
4857
+ if (mount.authStrategy === "s3-sigv4") requestInfo.payloadHashMode = getSigV4PayloadHash(cleanRequest.headers).mode;
4858
+ return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, async () => {
4859
+ const response = mount.authStrategy === "gcs" ? await signAndForwardGCS(cleanRequest, mount.credentials, requestInfo) : await signAndForwardSigV4(cleanRequest, mountId, mount.endpoint, mount.provider, mount.credentials, requestInfo);
4860
+ if (response.ok) directoryMarkerCache.set(getDirectoryMarkerCacheKey(mountId, realPath), responseHeaders);
4861
+ return response;
4862
+ });
4863
+ }
4864
+ if (isDirectoryMarkerHEAD(cleanRequest)) {
4865
+ const responseHeaders = directoryMarkerCache.get(getDirectoryMarkerCacheKey(mountId, realPath));
4866
+ if (responseHeaders) {
4867
+ requestInfo.bodyPresent = false;
4868
+ if (mount.authStrategy === "s3-sigv4") requestInfo.payloadHashMode = getSigV4PayloadHash(cleanRequest.headers).mode;
4869
+ return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => Promise.resolve(new Response(null, {
4870
+ status: 200,
4871
+ headers: responseHeaders
4872
+ })));
4873
+ }
4874
+ }
4875
+ if (cleanRequest.method.toUpperCase() !== "HEAD") deleteDirectoryMarkerCacheEntry(mountId, realPath);
4876
+ if (mount.authStrategy === "gcs") return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => signAndForwardGCS(cleanRequest, mount.credentials, requestInfo));
4877
+ return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => signAndForwardSigV4(cleanRequest, mountId, mount.endpoint, mount.provider, mount.credentials, requestInfo));
4878
+ };
4879
+
4778
4880
  //#endregion
4779
4881
  //#region src/tunnels/credentials.ts
4780
4882
  /**
@@ -5731,16 +5833,47 @@ async function pruneTunnelsForRestart(storage) {
5731
5833
  * This file is auto-updated by .github/changeset-version.ts during releases
5732
5834
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
5733
5835
  */
5734
- const SDK_VERSION = "0.11.0";
5836
+ const SDK_VERSION = "0.12.0";
5735
5837
 
5736
5838
  //#endregion
5737
5839
  //#region src/sandbox.ts
5738
5840
  const PORT_TOKENS_STORAGE_KEY = "portTokens";
5739
5841
  const ACTIVE_PREVIEW_PORTS_STORAGE_KEY = "activePreviewPorts";
5740
- const R2_EGRESS_PROXY_TARGET_CLASS_NAME = "CloudflareSandboxR2EgressProxyTarget";
5741
- var R2EgressProxyTarget = class extends Container {};
5742
- Object.defineProperty(R2EgressProxyTarget, "name", { value: R2_EGRESS_PROXY_TARGET_CLASS_NAME });
5743
- R2EgressProxyTarget.outboundHandlers = { r2EgressMount: r2EgressHandler };
5842
+ const CONTAINER_PROXY_CLASS_NAME = "ContainerProxy";
5843
+ const S3_CREDENTIAL_PROXY_HOST = "s3-credential-proxy.internal";
5844
+ const S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST = "s3-credential-proxy.sandbox.test";
5845
+ var ContainerProxyOutboundTarget = class extends Container {};
5846
+ Object.defineProperty(ContainerProxyOutboundTarget, "name", { value: CONTAINER_PROXY_CLASS_NAME });
5847
+ ContainerProxyOutboundTarget.outboundHandlers = {
5848
+ r2EgressMount: r2EgressHandler,
5849
+ s3CredentialProxyMount: s3CredentialProxyHandler
5850
+ };
5851
+ /**
5852
+ * SDK-level ContainerProxy that directly dispatches SDK-internal mount hosts
5853
+ * (r2.internal, s3-credential-proxy.internal) without relying on
5854
+ * outboundHandlersRegistry lookups, which are NOT shared between the Durable
5855
+ * Object's execution context and the ContainerProxy WorkerEntrypoint context.
5856
+ *
5857
+ * Users must export this class from their Worker entrypoint so the Sandbox DO
5858
+ * can create outbound-interception fetchers that reference it.
5859
+ */
5860
+ var ContainerProxy$1 = class extends ContainerProxy {
5861
+ async fetch(request) {
5862
+ const hostname = new URL(request.url).hostname;
5863
+ const props = this.ctx.props;
5864
+ const override = props.outboundByHostOverrides?.[hostname];
5865
+ if (override) {
5866
+ const handlerCtx = {
5867
+ containerId: props.containerId ?? "",
5868
+ className: props.className ?? "",
5869
+ params: override.params
5870
+ };
5871
+ if (override.method === "r2EgressMount") return r2EgressHandler(request, this.env, handlerCtx);
5872
+ if (override.method === "s3CredentialProxyMount") return s3CredentialProxyHandler(request, this.env, handlerCtx);
5873
+ }
5874
+ return super.fetch(request);
5875
+ }
5876
+ };
5744
5877
  function isFetcher(value) {
5745
5878
  return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
5746
5879
  }
@@ -5750,6 +5883,8 @@ const R2_DEFAULT_S3FS_OPTIONS = {
5750
5883
  enable_noobj_cache: true,
5751
5884
  multipart_size: "5"
5752
5885
  };
5886
+ const R2_DEFAULT_S3FS_OPTION_ENTRIES = Object.entries(R2_DEFAULT_S3FS_OPTIONS).map(([key, value]) => value === true ? key : `${key}=${value}`);
5887
+ const S3FS_DISABLE_EXPECT_HEADER_CONFIG = " Expect:\n";
5753
5888
  const BACKUP_DEFAULT_TTL_SECONDS = 259200;
5754
5889
  const BACKUP_MAX_NAME_LENGTH = 256;
5755
5890
  const BACKUP_CONTAINER_DIR = "/var/backups";
@@ -5941,10 +6076,6 @@ function getSandbox(ns, id, options) {
5941
6076
  }),
5942
6077
  terminal: (request, opts) => proxyTerminal(stub, defaultSessionId, request, opts),
5943
6078
  wsConnect: connect(stub),
5944
- desktop: new Proxy({}, { get(_, method) {
5945
- if (typeof method !== "string" || method === "then") return void 0;
5946
- return (...args) => stub.callDesktop(method, args);
5947
- } }),
5948
6079
  tunnels: new Proxy({}, { get: (_, method) => {
5949
6080
  if (typeof method !== "string" || method === "then") return void 0;
5950
6081
  return (...args) => stub.callTunnels(method, args);
@@ -5986,6 +6117,7 @@ var Sandbox = class Sandbox extends Container {
5986
6117
  logger;
5987
6118
  keepAliveEnabled = false;
5988
6119
  activeMounts = /* @__PURE__ */ new Map();
6120
+ mountOperationQueue = Promise.resolve();
5989
6121
  currentRuntime;
5990
6122
  transport = "http";
5991
6123
  /**
@@ -6052,56 +6184,6 @@ var Sandbox = class Sandbox extends Container {
6052
6184
  */
6053
6185
  hasStoredContainerTimeouts = false;
6054
6186
  /**
6055
- * Desktop environment operations.
6056
- * Within the DO, this getter provides direct access to DesktopClient.
6057
- * Over RPC, the getSandbox() proxy intercepts this property and routes
6058
- * calls through callDesktop() instead.
6059
- */
6060
- get desktop() {
6061
- return this.client.desktop;
6062
- }
6063
- /**
6064
- * Allowed desktop methods — derived from the Desktop interface.
6065
- * Restricts callDesktop() to a known set of operations.
6066
- */
6067
- static DESKTOP_METHODS = new Set([
6068
- "start",
6069
- "stop",
6070
- "status",
6071
- "screenshot",
6072
- "screenshotRegion",
6073
- "click",
6074
- "doubleClick",
6075
- "tripleClick",
6076
- "rightClick",
6077
- "middleClick",
6078
- "mouseDown",
6079
- "mouseUp",
6080
- "moveMouse",
6081
- "drag",
6082
- "scroll",
6083
- "getCursorPosition",
6084
- "type",
6085
- "press",
6086
- "keyDown",
6087
- "keyUp",
6088
- "getScreenSize",
6089
- "getProcessStatus"
6090
- ]);
6091
- /**
6092
- * Dispatch method for desktop operations.
6093
- * Called by the client-side proxy created in getSandbox() to provide
6094
- * the `sandbox.desktop.status()` API without relying on RPC pipelining
6095
- * through property getters which is broken when using vite-plugin.
6096
- */
6097
- async callDesktop(method, args) {
6098
- if (!Sandbox.DESKTOP_METHODS.has(method)) throw new Error(`Unknown desktop method: ${method}`);
6099
- const client = this.client.desktop;
6100
- const fn = client[method];
6101
- if (typeof fn !== "function") throw new Error(`sandbox.desktop missing method: ${method}`);
6102
- return fn.apply(client, args);
6103
- }
6104
- /**
6105
6187
  * Dispatch method for tunnel operations.
6106
6188
  * Called by the client-side proxy created in getSandbox() to provide
6107
6189
  * the `sandbox.tunnels` API without relying on RPC pipelining
@@ -6406,6 +6488,24 @@ var Sandbox = class Sandbox extends Container {
6406
6488
  * @throws InvalidMountConfigError if bucket name, mount path, or endpoint is invalid
6407
6489
  */
6408
6490
  async mountBucket(bucket, mountPath, options) {
6491
+ return this.runMountOperation(async () => {
6492
+ await this.mountBucketUnlocked(bucket, mountPath, options);
6493
+ });
6494
+ }
6495
+ async runMountOperation(operation) {
6496
+ const previous = this.mountOperationQueue;
6497
+ let release;
6498
+ this.mountOperationQueue = new Promise((resolve) => {
6499
+ release = resolve;
6500
+ });
6501
+ await previous.catch(() => {});
6502
+ try {
6503
+ await operation();
6504
+ } finally {
6505
+ release();
6506
+ }
6507
+ }
6508
+ async mountBucketUnlocked(bucket, mountPath, options) {
6409
6509
  if (options.prefix !== void 0) validatePrefix(options.prefix);
6410
6510
  if ("localBucket" in options && options.localBucket) {
6411
6511
  await this.mountBucketLocal(bucket, mountPath, options);
@@ -6445,6 +6545,7 @@ var Sandbox = class Sandbox extends Container {
6445
6545
  logger: this.logger
6446
6546
  });
6447
6547
  const mountInfo = {
6548
+ mountId: crypto.randomUUID(),
6448
6549
  mountType: "local-sync",
6449
6550
  bucket,
6450
6551
  mountPath,
@@ -6485,13 +6586,36 @@ var Sandbox = class Sandbox extends Container {
6485
6586
  };
6486
6587
  return { buckets };
6487
6588
  }
6488
- validateR2EgressS3fsOptions(options) {
6589
+ validateProtectedS3fsOptions(options, mountLabel, extraProtected = []) {
6489
6590
  if (!options) return;
6490
- const protectedOptions = new Set(["passwd_file", "url"]);
6591
+ const protectedOptions = new Set([
6592
+ "passwd_file",
6593
+ "url",
6594
+ ...extraProtected
6595
+ ]);
6491
6596
  for (const option of options) {
6492
6597
  const [key] = option.split("=");
6493
- if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for R2 binding mounts`);
6598
+ if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for ${mountLabel} mounts`);
6599
+ }
6600
+ }
6601
+ getS3CredentialProxyParams(options) {
6602
+ const mounts = {};
6603
+ for (const [, m] of this.activeMounts) if (m.mountType === "fuse" && m.credentialProxy) {
6604
+ if (m.mountId === options?.excludeMountId) continue;
6605
+ mounts[m.mountId] = {
6606
+ endpoint: m.credentialProxy.endpoint,
6607
+ bucket: m.credentialProxy.bucket,
6608
+ ...m.credentialProxy.prefix !== void 0 ? { prefix: m.credentialProxy.prefix } : {},
6609
+ credentials: m.credentialProxy.credentials,
6610
+ readOnly: m.credentialProxy.readOnly,
6611
+ provider: m.credentialProxy.provider,
6612
+ authStrategy: m.credentialProxy.authStrategy
6613
+ };
6494
6614
  }
6615
+ return { mounts };
6616
+ }
6617
+ resolveCredentialProxyAuthStrategy(provider) {
6618
+ return provider === "gcs" ? "gcs" : "s3-sigv4";
6495
6619
  }
6496
6620
  /**
6497
6621
  * Credential-less R2 mount: egress interception routes s3fs requests to the
@@ -6502,24 +6626,30 @@ var Sandbox = class Sandbox extends Container {
6502
6626
  const prefix = options.prefix;
6503
6627
  let mountOutcome = "error";
6504
6628
  let mountError;
6629
+ let passwordFilePath;
6630
+ let additionalHeaderFilePath;
6505
6631
  try {
6506
6632
  validateBucketBindingName(bucket, mountPath);
6507
6633
  this.validateMountPath(mountPath);
6508
- this.validateR2EgressS3fsOptions(options.s3fsOptions);
6634
+ this.validateProtectedS3fsOptions(options.s3fsOptions, "R2 binding");
6509
6635
  for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
6510
6636
  if (mountInfo$1.mountType === "r2-egress" && mountInfo$1.bucket === bucket && mountInfo$1.prefix !== prefix) throw new InvalidMountConfigError(`R2 binding "${bucket}" is already mounted at ${existingMountPath} with a different prefix. Mount the same binding only once, or use the same prefix for additional mounts.`);
6511
6637
  if (mountInfo$1.mountType === "r2-egress" && mountInfo$1.bucket === bucket && mountInfo$1.readOnly !== (options.readOnly ?? false)) throw new InvalidMountConfigError(`R2 binding "${bucket}" is already mounted at ${existingMountPath} with a different readOnly setting. Mount the same binding only once, or use the same readOnly value for additional mounts.`);
6512
6638
  }
6513
- const passwordFilePath = this.generatePasswordFilePath();
6639
+ passwordFilePath = this.generatePasswordFilePath();
6640
+ additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
6514
6641
  await this.createPasswordFile(passwordFilePath, bucket, {
6515
6642
  accessKeyId: "x",
6516
6643
  secretAccessKey: "x"
6517
6644
  });
6645
+ await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
6518
6646
  const mountInfo = {
6647
+ mountId: crypto.randomUUID(),
6519
6648
  mountType: "r2-egress",
6520
6649
  bucket,
6521
6650
  mountPath,
6522
6651
  passwordFilePath,
6652
+ additionalHeaderFilePath,
6523
6653
  mounted: false,
6524
6654
  prefix,
6525
6655
  readOnly: options.readOnly ?? false
@@ -6534,6 +6664,7 @@ var Sandbox = class Sandbox extends Container {
6534
6664
  ...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
6535
6665
  use_path_request_style: true,
6536
6666
  url: "http://r2.internal",
6667
+ ahbe_conf: additionalHeaderFilePath,
6537
6668
  ...options.readOnly ? { ro: true } : {}
6538
6669
  }));
6539
6670
  const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
@@ -6557,7 +6688,13 @@ var Sandbox = class Sandbox extends Container {
6557
6688
  mountError = error instanceof Error ? error : new Error(String(error));
6558
6689
  const failedMount = this.activeMounts.get(mountPath);
6559
6690
  this.activeMounts.delete(mountPath);
6560
- if (failedMount?.mountType === "r2-egress") await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
6691
+ if (failedMount?.mountType === "r2-egress") {
6692
+ await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
6693
+ if (failedMount.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(failedMount.additionalHeaderFilePath).catch(() => {});
6694
+ } else {
6695
+ if (passwordFilePath) await this.deletePasswordFile(passwordFilePath).catch(() => {});
6696
+ if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath).catch(() => {});
6697
+ }
6561
6698
  const remainingParams = this.getR2EgressParams();
6562
6699
  await this.configureR2EgressOutbound(remainingParams).catch(() => {});
6563
6700
  throw error;
@@ -6583,6 +6720,7 @@ var Sandbox = class Sandbox extends Container {
6583
6720
  let mountOutcome = "error";
6584
6721
  let mountError;
6585
6722
  let passwordFilePath;
6723
+ let additionalHeaderFilePath;
6586
6724
  let provider = null;
6587
6725
  let dirExisted = true;
6588
6726
  try {
@@ -6604,33 +6742,81 @@ var Sandbox = class Sandbox extends Container {
6604
6742
  R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
6605
6743
  ...this.envVars
6606
6744
  });
6745
+ const credentialProxyEnabled = options.credentialProxy === true;
6746
+ if (credentialProxyEnabled) this.validateProtectedS3fsOptions(options.s3fsOptions, "credential proxy", ["ahbe_conf", "use_path_request_style"]);
6607
6747
  passwordFilePath = this.generatePasswordFilePath();
6748
+ if (credentialProxyEnabled) additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
6749
+ const mountId = crypto.randomUUID();
6608
6750
  const mountInfo = {
6751
+ mountId,
6609
6752
  mountType: "fuse",
6610
6753
  bucket: s3fsSource,
6611
6754
  mountPath,
6612
6755
  endpoint: options.endpoint,
6613
6756
  provider,
6614
6757
  passwordFilePath,
6615
- mounted: false
6758
+ ...additionalHeaderFilePath ? { additionalHeaderFilePath } : {},
6759
+ mounted: false,
6760
+ ...credentialProxyEnabled ? { credentialProxy: {
6761
+ endpoint: options.endpoint,
6762
+ bucket,
6763
+ ...prefix !== void 0 ? { prefix } : {},
6764
+ credentials,
6765
+ readOnly: options.readOnly ?? false,
6766
+ provider,
6767
+ authStrategy: this.resolveCredentialProxyAuthStrategy(provider)
6768
+ } } : {}
6616
6769
  };
6617
6770
  this.activeMounts.set(mountPath, mountInfo);
6618
- await this.createPasswordFile(passwordFilePath, bucket, credentials);
6771
+ await this.createPasswordFile(passwordFilePath, bucket, credentialProxyEnabled ? {
6772
+ accessKeyId: "x",
6773
+ secretAccessKey: "x"
6774
+ } : credentials);
6775
+ if (credentialProxyEnabled) {
6776
+ if (additionalHeaderFilePath) await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
6777
+ await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams());
6778
+ }
6619
6779
  dirExisted = (await this.execInternal(`test -d ${shellEscape(mountPath)}`)).exitCode === 0;
6620
6780
  await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
6621
- await this.executeS3FSMount(s3fsSource, mountPath, options, provider, passwordFilePath);
6781
+ const effectiveOptions = credentialProxyEnabled ? {
6782
+ ...options,
6783
+ endpoint: `http://${S3_CREDENTIAL_PROXY_HOST}/${mountId}`,
6784
+ s3fsOptions: [
6785
+ ...provider === "r2" ? R2_DEFAULT_S3FS_OPTION_ENTRIES : [],
6786
+ ...options.s3fsOptions ?? [],
6787
+ ...additionalHeaderFilePath ? [`ahbe_conf=${additionalHeaderFilePath}`] : [],
6788
+ "use_path_request_style"
6789
+ ]
6790
+ } : options;
6791
+ await this.executeS3FSMount(s3fsSource, mountPath, effectiveOptions, provider, passwordFilePath);
6622
6792
  mountInfo.mounted = true;
6623
6793
  mountOutcome = "success";
6624
6794
  } catch (error) {
6625
6795
  mountError = error instanceof Error ? error : new Error(String(error));
6626
- if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
6627
6796
  try {
6628
6797
  await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && fusermount -u ${shellEscape(mountPath)}`);
6629
6798
  } catch {}
6799
+ if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
6800
+ if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath);
6630
6801
  if (!dirExisted) try {
6631
6802
  await this.execInternal(`rmdir ${shellEscape(mountPath)} 2>/dev/null`);
6632
6803
  } catch {}
6633
- this.activeMounts.delete(mountPath);
6804
+ const failedMount = this.activeMounts.get(mountPath);
6805
+ if (failedMount?.mountType === "fuse" && failedMount.credentialProxy) try {
6806
+ await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: failedMount.mountId }));
6807
+ this.activeMounts.delete(mountPath);
6808
+ evictSigV4ClientCacheEntry(failedMount.mountId);
6809
+ evictDirectoryMarkerCacheForMount(failedMount.mountId);
6810
+ } catch (cleanupError) {
6811
+ this.logger.warn("credential proxy cleanup failed", {
6812
+ mountPath,
6813
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
6814
+ });
6815
+ this.activeMounts.delete(mountPath);
6816
+ evictSigV4ClientCacheEntry(failedMount.mountId);
6817
+ evictDirectoryMarkerCacheForMount(failedMount.mountId);
6818
+ }
6819
+ else this.activeMounts.delete(mountPath);
6634
6820
  throw error;
6635
6821
  } finally {
6636
6822
  logCanonicalEvent(this.logger, {
@@ -6652,6 +6838,11 @@ var Sandbox = class Sandbox extends Container {
6652
6838
  * @throws InvalidMountConfigError if mount path doesn't exist or isn't mounted
6653
6839
  */
6654
6840
  async unmountBucket(mountPath) {
6841
+ return this.runMountOperation(async () => {
6842
+ await this.unmountBucketUnlocked(mountPath);
6843
+ });
6844
+ }
6845
+ async unmountBucketUnlocked(mountPath) {
6655
6846
  const unmountStartTime = Date.now();
6656
6847
  let unmountOutcome = "error";
6657
6848
  let unmountError;
@@ -6662,30 +6853,75 @@ var Sandbox = class Sandbox extends Container {
6662
6853
  await mountInfo.syncManager.stop();
6663
6854
  mountInfo.mounted = false;
6664
6855
  this.activeMounts.delete(mountPath);
6665
- } else try {
6666
- const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
6667
- if (result.exitCode !== 0) {
6668
- const stderr = result.stderr || "unknown error";
6669
- throw new BucketUnmountError(`fusermount -u failed (exit ${result.exitCode}): ${stderr}`);
6670
- }
6671
- mountInfo.mounted = false;
6672
- this.activeMounts.delete(mountPath);
6673
- if (mountInfo.mountType === "r2-egress") await this.configureR2EgressOutbound(this.getR2EgressParams());
6856
+ } else if (mountInfo.mountType === "fuse" && mountInfo.credentialProxy && !mountInfo.mounted) {
6674
6857
  try {
6675
- const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
6676
- if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
6677
- mountPath,
6678
- exitCode: cleanup.exitCode,
6679
- stderr: cleanup.stderr
6680
- });
6681
- } catch (err) {
6682
- this.logger.warn("mount directory removal failed", {
6858
+ await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: mountInfo.mountId }));
6859
+ } catch (cleanupError) {
6860
+ this.logger.warn("credential proxy outbound reconfiguration failed on unmount", {
6683
6861
  mountPath,
6684
- error: err instanceof Error ? err.message : String(err)
6862
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
6685
6863
  });
6686
6864
  }
6687
- } finally {
6688
- await this.deletePasswordFile(mountInfo.passwordFilePath);
6865
+ this.activeMounts.delete(mountPath);
6866
+ evictSigV4ClientCacheEntry(mountInfo.mountId);
6867
+ evictDirectoryMarkerCacheForMount(mountInfo.mountId);
6868
+ } else {
6869
+ let unmounted = false;
6870
+ try {
6871
+ const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
6872
+ if (result.exitCode !== 0) {
6873
+ const stderr = result.stderr || "unknown error";
6874
+ throw new BucketUnmountError(`fusermount -u failed (exit ${result.exitCode}): ${stderr}`);
6875
+ }
6876
+ unmounted = true;
6877
+ mountInfo.mounted = false;
6878
+ if (mountInfo.mountType === "r2-egress") {
6879
+ const remainingBuckets = {};
6880
+ for (const [, activeMount] of this.activeMounts) if (activeMount.mountType === "r2-egress" && activeMount.mountId !== mountInfo.mountId) remainingBuckets[activeMount.bucket] = {
6881
+ prefix: activeMount.prefix,
6882
+ readOnly: activeMount.readOnly
6883
+ };
6884
+ try {
6885
+ await this.configureR2EgressOutbound({ buckets: remainingBuckets });
6886
+ } catch (cleanupError) {
6887
+ this.logger.warn("r2 egress outbound reconfiguration failed on unmount", {
6888
+ mountPath,
6889
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
6890
+ });
6891
+ }
6892
+ this.activeMounts.delete(mountPath);
6893
+ } else if (mountInfo.mountType === "fuse" && mountInfo.credentialProxy) {
6894
+ try {
6895
+ await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: mountInfo.mountId }));
6896
+ } catch (cleanupError) {
6897
+ this.logger.warn("credential proxy outbound reconfiguration failed on unmount", {
6898
+ mountPath,
6899
+ error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
6900
+ });
6901
+ }
6902
+ this.activeMounts.delete(mountPath);
6903
+ evictSigV4ClientCacheEntry(mountInfo.mountId);
6904
+ evictDirectoryMarkerCacheForMount(mountInfo.mountId);
6905
+ } else this.activeMounts.delete(mountPath);
6906
+ try {
6907
+ const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
6908
+ if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
6909
+ mountPath,
6910
+ exitCode: cleanup.exitCode,
6911
+ stderr: cleanup.stderr
6912
+ });
6913
+ } catch (err) {
6914
+ this.logger.warn("mount directory removal failed", {
6915
+ mountPath,
6916
+ error: err instanceof Error ? err.message : String(err)
6917
+ });
6918
+ }
6919
+ } finally {
6920
+ if (unmounted) {
6921
+ await this.deletePasswordFile(mountInfo.passwordFilePath);
6922
+ if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
6923
+ }
6924
+ }
6689
6925
  }
6690
6926
  unmountOutcome = "success";
6691
6927
  } catch (error) {
@@ -6728,6 +6964,20 @@ var Sandbox = class Sandbox extends Container {
6728
6964
  return `/tmp/.passwd-s3fs-${crypto.randomUUID()}`;
6729
6965
  }
6730
6966
  /**
6967
+ * Generate unique ahbe_conf file path for s3fs additional header config
6968
+ */
6969
+ generateS3FSAdditionalHeaderFilePath() {
6970
+ return `/tmp/.s3fs-ahbe-${crypto.randomUUID()}.conf`;
6971
+ }
6972
+ /**
6973
+ * Create s3fs ahbe_conf file that suppresses the Expect: 100-continue header.
6974
+ * Restricted to 0600 so s3fs will accept it (same requirement as passwd files).
6975
+ */
6976
+ async createDisableExpectHeaderFile(headerFilePath) {
6977
+ await this.client.files.writeFile(headerFilePath, S3FS_DISABLE_EXPECT_HEADER_CONFIG, DISABLE_SESSION_TOKEN);
6978
+ await this.execInternal(`chmod 0600 ${shellEscape(headerFilePath)}`);
6979
+ }
6980
+ /**
6731
6981
  * Create password file with s3fs credentials
6732
6982
  * Format: bucket:accessKeyId:secretAccessKey
6733
6983
  */
@@ -6749,6 +6999,16 @@ var Sandbox = class Sandbox extends Container {
6749
6999
  });
6750
7000
  }
6751
7001
  }
7002
+ async deleteAdditionalHeaderFile(headerFilePath) {
7003
+ try {
7004
+ await this.execInternal(`rm -f ${shellEscape(headerFilePath)}`);
7005
+ } catch (error) {
7006
+ this.logger.warn("s3fs additional header file cleanup failed", {
7007
+ headerFilePath,
7008
+ error: error instanceof Error ? error.message : String(error)
7009
+ });
7010
+ }
7011
+ }
6752
7012
  /**
6753
7013
  * Execute S3FS mount command
6754
7014
  */
@@ -6836,9 +7096,6 @@ var Sandbox = class Sandbox extends Container {
6836
7096
  await this.ctx.storage.delete(PORT_TOKENS_STORAGE_KEY);
6837
7097
  await this.clearActivePreviewPorts();
6838
7098
  await this.currentRuntime.clear();
6839
- if (this.ctx.container?.running) try {
6840
- await this.client.desktop.stop();
6841
- } catch {}
6842
7099
  for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
6843
7100
  mountsProcessed++;
6844
7101
  if (mountInfo.mountType === "local-sync") try {
@@ -6858,6 +7115,7 @@ var Sandbox = class Sandbox extends Container {
6858
7115
  this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
6859
7116
  }
6860
7117
  await this.deletePasswordFile(mountInfo.passwordFilePath);
7118
+ if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
6861
7119
  }
6862
7120
  }
6863
7121
  try {
@@ -6975,9 +7233,16 @@ var Sandbox = class Sandbox extends Container {
6975
7233
  }
6976
7234
  this.client.disconnect();
6977
7235
  let hadR2EgressMount = false;
7236
+ let hadCredentialProxyMount = false;
6978
7237
  for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
6979
7238
  else if (m.mountType === "r2-egress") hadR2EgressMount = true;
7239
+ else if (m.mountType === "fuse" && m.credentialProxy) {
7240
+ hadCredentialProxyMount = true;
7241
+ evictSigV4ClientCacheEntry(m.mountId);
7242
+ evictDirectoryMarkerCacheForMount(m.mountId);
7243
+ }
6980
7244
  if (hadR2EgressMount) await this.configureR2EgressOutbound({ buckets: {} }).catch(() => {});
7245
+ if (hadCredentialProxyMount) await this.configureS3CredentialProxyOutbound({ mounts: {} }).catch(() => {});
6981
7246
  this.activeMounts.clear();
6982
7247
  await this.ctx.storage.delete("defaultSession");
6983
7248
  }
@@ -8015,33 +8280,6 @@ var Sandbox = class Sandbox extends Container {
8015
8280
  return this.client.files.exists(path$1, session);
8016
8281
  }
8017
8282
  /**
8018
- * Get the noVNC preview URL for browser-based desktop viewing.
8019
- * Confirms desktop is active, then uses exposePort() to generate
8020
- * a token-authenticated preview URL for the noVNC port (6080).
8021
- *
8022
- * @param hostname - The custom domain hostname for preview URLs
8023
- * (e.g., 'preview.example.com'). Required because preview URLs
8024
- * use subdomain patterns that .workers.dev doesn't support.
8025
- * @param options - Optional settings
8026
- * @param options.token - Reuse an existing token instead of generating a new one
8027
- * @returns The authenticated noVNC preview URL
8028
- */
8029
- async getDesktopStreamUrl(hostname, options) {
8030
- if ((await this.client.desktop.status()).status === "inactive") throw new Error("Desktop is not running. Call sandbox.desktop.start() first.");
8031
- const url = (await this.exposePort(6080, {
8032
- hostname,
8033
- token: options?.token
8034
- })).url;
8035
- try {
8036
- await this.waitForPort({
8037
- portToCheck: 6080,
8038
- retries: 30,
8039
- waitInterval: 500
8040
- });
8041
- } catch {}
8042
- return { url };
8043
- }
8044
- /**
8045
8283
  * Watch a directory for file system changes using native inotify.
8046
8284
  *
8047
8285
  * The returned promise resolves only after the watcher is established on the
@@ -9097,10 +9335,13 @@ var Sandbox = class Sandbox extends Container {
9097
9335
  * create-archive → read → upload (or mount → extract) flow
9098
9336
  * is not interleaved with another backup operation on the same directory.
9099
9337
  */
9100
- enqueueBackupOp(fn) {
9101
- const next = this.backupInProgress.then(fn, () => fn());
9338
+ async enqueueBackupOp(fn) {
9339
+ try {
9340
+ await this.backupInProgress;
9341
+ } catch {}
9342
+ const next = fn();
9102
9343
  this.backupInProgress = next.catch(() => {});
9103
- return next;
9344
+ return await next;
9104
9345
  }
9105
9346
  /**
9106
9347
  * Create a backup of a directory and upload it to R2.
@@ -9124,9 +9365,9 @@ var Sandbox = class Sandbox extends Container {
9124
9365
  * under the `backups/` prefix after the desired retention period.
9125
9366
  */
9126
9367
  async createBackup(options) {
9127
- if (options.localBucket) return this.enqueueBackupOp(() => this.doCreateBackupLocal(options));
9368
+ if (options.localBucket) return await this.enqueueBackupOp(() => this.doCreateBackupLocal(options));
9128
9369
  this.requireBackupBucket();
9129
- return this.enqueueBackupOp(() => this.doCreateBackup(options));
9370
+ return await this.enqueueBackupOp(() => this.doCreateBackup(options));
9130
9371
  }
9131
9372
  async doCreateBackup(options) {
9132
9373
  const bucket = this.requireBackupBucket();
@@ -9423,9 +9664,9 @@ var Sandbox = class Sandbox extends Container {
9423
9664
  * Concurrent backup/restore calls on the same sandbox are serialized.
9424
9665
  */
9425
9666
  async restoreBackup(backup) {
9426
- if (backup.localBucket) return this.enqueueBackupOp(() => this.doRestoreBackupLocal(backup));
9667
+ if (backup.localBucket) return await this.enqueueBackupOp(() => this.doRestoreBackupLocal(backup));
9427
9668
  this.requireBackupBucket();
9428
- return this.enqueueBackupOp(() => this.doRestoreBackup(backup));
9669
+ return await this.enqueueBackupOp(() => this.doRestoreBackup(backup));
9429
9670
  }
9430
9671
  async doRestoreBackup(backup) {
9431
9672
  const restoreStartTime = Date.now();
@@ -9685,10 +9926,18 @@ var Sandbox = class Sandbox extends Container {
9685
9926
  const ctx = this.ctx;
9686
9927
  if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
9687
9928
  if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
9929
+ this.constructor.outboundHandlers = { r2EgressMount: r2EgressHandler };
9930
+ if (Object.keys(params.buckets).length > 0) await this.setOutboundByHost("r2.internal", "r2EgressMount", params);
9931
+ else await this.removeOutboundByHost("r2.internal");
9932
+ this.logger.debug("r2 egress: registering host interception", {
9933
+ host: "r2.internal",
9934
+ method: "r2EgressMount",
9935
+ targetClassName: CONTAINER_PROXY_CLASS_NAME
9936
+ });
9688
9937
  const fetcher = ctx.exports.ContainerProxy({ props: {
9689
9938
  enableInternet: this.enableInternet,
9690
9939
  containerId: this.ctx.id.toString(),
9691
- className: R2_EGRESS_PROXY_TARGET_CLASS_NAME,
9940
+ className: CONTAINER_PROXY_CLASS_NAME,
9692
9941
  outboundByHostOverrides: { "r2.internal": {
9693
9942
  method: "r2EgressMount",
9694
9943
  params
@@ -9697,8 +9946,42 @@ var Sandbox = class Sandbox extends Container {
9697
9946
  if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
9698
9947
  await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
9699
9948
  }
9949
+ async configureS3CredentialProxyOutbound(params) {
9950
+ const ctx = this.ctx;
9951
+ if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("Credential proxy bucket mounts require container outbound interception support");
9952
+ if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("Credential proxy bucket mounts require exporting ContainerProxy from the Worker entrypoint");
9953
+ const hosts = [S3_CREDENTIAL_PROXY_HOST, S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST];
9954
+ this.constructor.outboundHandlers = { s3CredentialProxyMount: s3CredentialProxyHandler };
9955
+ if (Object.keys(params.mounts).length > 0) for (const host of hosts) await this.setOutboundByHost(host, "s3CredentialProxyMount", params);
9956
+ else for (const host of hosts) await this.removeOutboundByHost(host);
9957
+ const hostOverrides = {};
9958
+ for (const host of hosts) hostOverrides[host] = {
9959
+ method: "s3CredentialProxyMount",
9960
+ params
9961
+ };
9962
+ this.logger.debug("s3 credential proxy: registering host interception", {
9963
+ hosts,
9964
+ method: "s3CredentialProxyMount",
9965
+ targetClassName: CONTAINER_PROXY_CLASS_NAME
9966
+ });
9967
+ const fetcher = ctx.exports.ContainerProxy({ props: {
9968
+ enableInternet: this.enableInternet,
9969
+ containerId: this.ctx.id.toString(),
9970
+ className: CONTAINER_PROXY_CLASS_NAME,
9971
+ outboundByHostOverrides: hostOverrides
9972
+ } });
9973
+ if (!isFetcher(fetcher)) throw new InvalidMountConfigError("Credential proxy bucket mounts require ContainerProxy to return a valid Fetcher");
9974
+ try {
9975
+ const selfTest = await fetcher.fetch(new Request(`http://${S3_CREDENTIAL_PROXY_HOST}${SELF_TEST_PATH}`));
9976
+ await selfTest.text();
9977
+ this.logger.debug("s3 credential proxy: fetcher self-test complete", { status: selfTest.status });
9978
+ } catch (error) {
9979
+ this.logger.warn("s3 credential proxy: fetcher self-test failed", { error: error instanceof Error ? error.message : String(error) });
9980
+ }
9981
+ for (const host of hosts) await ctx.container.interceptOutboundHttp(host, fetcher);
9982
+ }
9700
9983
  };
9701
9984
 
9702
9985
  //#endregion
9703
- export { DesktopClient as A, DesktopProcessCrashedError as B, streamFile as C, PortClient as D, ProcessClient as E, BackupNotFoundError as F, ProcessReadyTimeoutError as G, DesktopUnavailableError as H, BackupRestoreError as I, RPCTransportError as K, DesktopInvalidCoordinatesError as L, BackupClient as M, BackupCreateError as N, GitClient as O, BackupExpiredError as P, DesktopInvalidOptionsError as R, collectFile as S, UtilityClient as T, InvalidBackupConfigError as U, DesktopStartFailedError as V, ProcessExitedBeforeReadyError as W, CodeInterpreter as _, PREVIEW_PROXY_HEADERS as a, validatePort as b, PREVIEW_PROXY_TOKEN_HEADER as c, InvalidMountConfigError as d, MissingCredentialsError as f, responseToAsyncIterable as g, parseSSEStream as h, PREVIEW_PROXY_HEADER as i, CommandClient as j, FileClient as k, BucketMountError as l, asyncIterableToSSEStream as m, getSandbox as n, PREVIEW_PROXY_PORT_HEADER as o, S3FSMountError as p, SessionTerminatedError as q, proxyTerminal as r, PREVIEW_PROXY_SANDBOX_ID_HEADER as s, Sandbox as t, BucketUnmountError as u, SandboxSecurityError as v, SandboxClient as w, validateTunnelName as x, sanitizeSandboxId as y, DesktopNotStartedError as z };
9704
- //# sourceMappingURL=sandbox-DQxTkLyY.js.map
9986
+ export { FileClient as A, RPCTransportError as B, collectFile as C, ProcessClient as D, UtilityClient as E, BackupNotFoundError as F, BackupRestoreError as I, InvalidBackupConfigError as L, BackupClient as M, BackupCreateError as N, PortClient as O, BackupExpiredError as P, ProcessExitedBeforeReadyError as R, validateTunnelName as S, SandboxClient as T, SessionTerminatedError as V, responseToAsyncIterable as _, PREVIEW_PROXY_HEADER as a, sanitizeSandboxId as b, PREVIEW_PROXY_SANDBOX_ID_HEADER as c, BucketUnmountError as d, InvalidMountConfigError as f, parseSSEStream as g, asyncIterableToSSEStream as h, proxyTerminal as i, CommandClient as j, GitClient as k, PREVIEW_PROXY_TOKEN_HEADER as l, S3FSMountError as m, Sandbox as n, PREVIEW_PROXY_HEADERS as o, MissingCredentialsError as p, getSandbox as r, PREVIEW_PROXY_PORT_HEADER as s, ContainerProxy$1 as t, BucketMountError as u, CodeInterpreter as v, streamFile as w, validatePort as x, SandboxSecurityError as y, ProcessReadyTimeoutError as z };
9987
+ //# sourceMappingURL=sandbox-Duj2gvUC.js.map