@cloudflare/sandbox 0.11.0 → 0.12.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/Dockerfile +0 -55
- package/README.md +1 -0
- package/dist/bridge/index.js +9 -2
- package/dist/bridge/index.js.map +1 -1
- package/dist/{contexts-XHAo64dB.d.ts → contexts-B0_bcx9f.d.ts} +2 -29
- package/dist/contexts-B0_bcx9f.d.ts.map +1 -0
- package/dist/{errors-COsTRno_.js → errors-aRUdk9K8.js} +1 -19
- package/dist/errors-aRUdk9K8.js.map +1 -0
- package/dist/index.d.ts +3 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +2 -2
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-B9LOT0cg.d.ts → sandbox-C8l-pMlL.d.ts} +51 -384
- package/dist/sandbox-C8l-pMlL.d.ts.map +1 -0
- package/dist/{sandbox-DQxTkLyY.js → sandbox-DKG3H156.js} +801 -517
- package/dist/sandbox-DKG3H156.js.map +1 -0
- package/package.json +1 -1
- package/dist/contexts-XHAo64dB.d.ts.map +0 -1
- package/dist/errors-COsTRno_.js.map +0 -1
- package/dist/sandbox-B9LOT0cg.d.ts.map +0 -1
- package/dist/sandbox-DQxTkLyY.js.map +0 -1
|
@@ -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-
|
|
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:
|
|
@@ -761,6 +719,53 @@ function createErrorFromResponse(errorResponse, options) {
|
|
|
761
719
|
}
|
|
762
720
|
}
|
|
763
721
|
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/response-retry.ts
|
|
724
|
+
const DEFAULT_INITIAL_RETRY_DELAY_MS = 3e3;
|
|
725
|
+
const DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
|
|
726
|
+
const RETRYABLE_WEBSOCKET_UPGRADE_STATUSES = new Set([
|
|
727
|
+
500,
|
|
728
|
+
502,
|
|
729
|
+
503,
|
|
730
|
+
504
|
|
731
|
+
]);
|
|
732
|
+
function isRetryableWebSocketUpgradeResponse(response) {
|
|
733
|
+
return RETRYABLE_WEBSOCKET_UPGRADE_STATUSES.has(response.status);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Retry Response-returning operations while their response remains retryable.
|
|
737
|
+
* The retry budget covers the whole operation; each attempt owns any
|
|
738
|
+
* per-request timeout inside the caller-provided `fetchResponse` function.
|
|
739
|
+
*/
|
|
740
|
+
async function fetchWithResponseRetry(fetchResponse, options) {
|
|
741
|
+
const startTime = Date.now();
|
|
742
|
+
let attempt = 0;
|
|
743
|
+
while (true) {
|
|
744
|
+
const response = await fetchResponse();
|
|
745
|
+
if (!options.shouldRetry(response)) return response;
|
|
746
|
+
const elapsed = Date.now() - startTime;
|
|
747
|
+
const remaining = options.retryTimeoutMs - elapsed;
|
|
748
|
+
if (remaining <= options.minTimeForRetryMs) {
|
|
749
|
+
options.onRetryExhausted?.({
|
|
750
|
+
attempts: attempt + 1,
|
|
751
|
+
elapsedMs: elapsed,
|
|
752
|
+
response
|
|
753
|
+
});
|
|
754
|
+
return response;
|
|
755
|
+
}
|
|
756
|
+
const delay = Math.min(DEFAULT_INITIAL_RETRY_DELAY_MS * 2 ** attempt, DEFAULT_MAX_RETRY_DELAY_MS);
|
|
757
|
+
options.logger.info(options.retryLogMessage, {
|
|
758
|
+
status: response.status,
|
|
759
|
+
attempt: attempt + 1,
|
|
760
|
+
delayMs: delay,
|
|
761
|
+
remainingSec: Math.floor(remaining / 1e3),
|
|
762
|
+
...options.getRetryLogContext?.(response)
|
|
763
|
+
});
|
|
764
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
765
|
+
attempt++;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
764
769
|
//#endregion
|
|
765
770
|
//#region src/clients/transport/base-transport.ts
|
|
766
771
|
/**
|
|
@@ -796,30 +801,17 @@ var BaseTransport = class {
|
|
|
796
801
|
* transport-specific doFetch() with retry logic for container startup.
|
|
797
802
|
*/
|
|
798
803
|
async fetch(path$1, options) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
this.logger.info("Container not ready, retrying", {
|
|
809
|
-
status: response.status,
|
|
810
|
-
attempt: attempt + 1,
|
|
811
|
-
delayMs: delay,
|
|
812
|
-
remainingSec: Math.floor(remaining / 1e3),
|
|
813
|
-
mode: this.getMode()
|
|
814
|
-
});
|
|
815
|
-
await this.sleep(delay);
|
|
816
|
-
attempt++;
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempt + 1} attempts over ${Math.floor(elapsed / 1e3)}s`));
|
|
804
|
+
return fetchWithResponseRetry(() => this.doFetch(path$1, options), {
|
|
805
|
+
retryTimeoutMs: this.retryTimeoutMs,
|
|
806
|
+
minTimeForRetryMs: MIN_TIME_FOR_RETRY_MS$1,
|
|
807
|
+
logger: this.logger,
|
|
808
|
+
retryLogMessage: "Container not ready, retrying",
|
|
809
|
+
shouldRetry: (response) => response.status === 503,
|
|
810
|
+
getRetryLogContext: () => ({ mode: this.getMode() }),
|
|
811
|
+
onRetryExhausted: ({ attempts, elapsedMs }) => {
|
|
812
|
+
this.logger.error("Container failed to become ready", /* @__PURE__ */ new Error(`Failed after ${attempts} attempts over ${Math.floor(elapsedMs / 1e3)}s`));
|
|
820
813
|
}
|
|
821
|
-
|
|
822
|
-
}
|
|
814
|
+
});
|
|
823
815
|
}
|
|
824
816
|
/**
|
|
825
817
|
* Build a URL targeting the container's HTTP server.
|
|
@@ -859,12 +851,6 @@ var BaseTransport = class {
|
|
|
859
851
|
if (!response.body) throw new Error("No response body for streaming");
|
|
860
852
|
return response.body;
|
|
861
853
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Sleep utility for retry delays
|
|
864
|
-
*/
|
|
865
|
-
sleep(ms) {
|
|
866
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
867
|
-
}
|
|
868
854
|
};
|
|
869
855
|
|
|
870
856
|
//#endregion
|
|
@@ -1033,24 +1019,13 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1033
1019
|
else await this.connectViaWebSocket();
|
|
1034
1020
|
}
|
|
1035
1021
|
async fetchUpgradeWithRetry(attemptUpgrade) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (remaining <= MIN_TIME_FOR_CONNECT_RETRY_MS) return response;
|
|
1044
|
-
const delay = Math.min(3e3 * 2 ** attempt, 3e4);
|
|
1045
|
-
this.logger.info("WebSocket container not ready, retrying", {
|
|
1046
|
-
status: response.status,
|
|
1047
|
-
attempt: attempt + 1,
|
|
1048
|
-
delayMs: delay,
|
|
1049
|
-
remainingSec: Math.floor(remaining / 1e3)
|
|
1050
|
-
});
|
|
1051
|
-
await this.sleep(delay);
|
|
1052
|
-
attempt++;
|
|
1053
|
-
}
|
|
1022
|
+
return fetchWithResponseRetry(attemptUpgrade, {
|
|
1023
|
+
retryTimeoutMs: this.getRetryTimeoutMs(),
|
|
1024
|
+
minTimeForRetryMs: MIN_TIME_FOR_CONNECT_RETRY_MS,
|
|
1025
|
+
logger: this.logger,
|
|
1026
|
+
retryLogMessage: "WebSocket upgrade returned retryable status, retrying",
|
|
1027
|
+
shouldRetry: isRetryableWebSocketUpgradeResponse
|
|
1028
|
+
});
|
|
1054
1029
|
}
|
|
1055
1030
|
/**
|
|
1056
1031
|
* Connect using fetch-based WebSocket (Cloudflare Workers style)
|
|
@@ -1590,21 +1565,21 @@ var BaseHttpClient = class {
|
|
|
1590
1565
|
body: JSON.stringify(data),
|
|
1591
1566
|
...requestOptions
|
|
1592
1567
|
});
|
|
1593
|
-
return this.handleResponse(response, responseHandler);
|
|
1568
|
+
return await this.handleResponse(response, responseHandler);
|
|
1594
1569
|
}
|
|
1595
1570
|
/**
|
|
1596
1571
|
* Make a GET request
|
|
1597
1572
|
*/
|
|
1598
1573
|
async get(endpoint, responseHandler) {
|
|
1599
1574
|
const response = await this.doFetch(endpoint, { method: "GET" });
|
|
1600
|
-
return this.handleResponse(response, responseHandler);
|
|
1575
|
+
return await this.handleResponse(response, responseHandler);
|
|
1601
1576
|
}
|
|
1602
1577
|
/**
|
|
1603
1578
|
* Make a DELETE request
|
|
1604
1579
|
*/
|
|
1605
1580
|
async delete(endpoint, responseHandler) {
|
|
1606
1581
|
const response = await this.doFetch(endpoint, { method: "DELETE" });
|
|
1607
|
-
return this.handleResponse(response, responseHandler);
|
|
1582
|
+
return await this.handleResponse(response, responseHandler);
|
|
1608
1583
|
}
|
|
1609
1584
|
/**
|
|
1610
1585
|
* Handle HTTP response with error checking and parsing
|
|
@@ -1673,7 +1648,7 @@ var BaseHttpClient = class {
|
|
|
1673
1648
|
headers: { "Content-Type": "application/json" },
|
|
1674
1649
|
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
1675
1650
|
});
|
|
1676
|
-
return this.handleStreamResponse(response);
|
|
1651
|
+
return await this.handleStreamResponse(response);
|
|
1677
1652
|
}
|
|
1678
1653
|
};
|
|
1679
1654
|
|
|
@@ -1782,240 +1757,6 @@ var CommandClient = class extends BaseHttpClient {
|
|
|
1782
1757
|
}
|
|
1783
1758
|
};
|
|
1784
1759
|
|
|
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
1760
|
//#endregion
|
|
2020
1761
|
//#region src/clients/file-client.ts
|
|
2021
1762
|
/**
|
|
@@ -2618,7 +2359,6 @@ var SandboxClient = class {
|
|
|
2618
2359
|
git;
|
|
2619
2360
|
interpreter;
|
|
2620
2361
|
utils;
|
|
2621
|
-
desktop;
|
|
2622
2362
|
watch;
|
|
2623
2363
|
/**
|
|
2624
2364
|
* Tunnels are RPC-only — the route-based transport does not implement them.
|
|
@@ -2651,11 +2391,10 @@ var SandboxClient = class {
|
|
|
2651
2391
|
this.git = new GitClient(clientOptions);
|
|
2652
2392
|
this.interpreter = new InterpreterClient(clientOptions);
|
|
2653
2393
|
this.utils = new UtilityClient(clientOptions);
|
|
2654
|
-
this.desktop = new DesktopClient(clientOptions);
|
|
2655
2394
|
this.watch = new WatchClient(clientOptions);
|
|
2656
2395
|
}
|
|
2657
2396
|
/**
|
|
2658
|
-
* Update the
|
|
2397
|
+
* Update the transport retry budget without recreating the client.
|
|
2659
2398
|
*
|
|
2660
2399
|
* In WebSocket mode a single shared transport is used, so one update covers
|
|
2661
2400
|
* every sub-client. In HTTP mode each sub-client owns its own transport, so
|
|
@@ -2672,7 +2411,6 @@ var SandboxClient = class {
|
|
|
2672
2411
|
this.git.setRetryTimeoutMs(ms);
|
|
2673
2412
|
this.interpreter.setRetryTimeoutMs(ms);
|
|
2674
2413
|
this.utils.setRetryTimeoutMs(ms);
|
|
2675
|
-
this.desktop.setRetryTimeoutMs(ms);
|
|
2676
2414
|
this.watch.setRetryTimeoutMs(ms);
|
|
2677
2415
|
}
|
|
2678
2416
|
}
|
|
@@ -2743,7 +2481,6 @@ const DISABLE_SESSION_TOKEN = "__DISABLE_SESSION__";
|
|
|
2743
2481
|
const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
2744
2482
|
const DEFAULT_RETRY_TIMEOUT_MS = 12e4;
|
|
2745
2483
|
const MIN_TIME_FOR_RETRY_MS = 15e3;
|
|
2746
|
-
const MAX_RETRY_BACKOFF_MS = 3e4;
|
|
2747
2484
|
/**
|
|
2748
2485
|
* Manages a capnweb WebSocket RPC session to the container.
|
|
2749
2486
|
*
|
|
@@ -2821,7 +2558,7 @@ var ContainerControlConnection = class {
|
|
|
2821
2558
|
this.connectPromise = null;
|
|
2822
2559
|
}
|
|
2823
2560
|
/**
|
|
2824
|
-
* Update the
|
|
2561
|
+
* Update the upgrade retry budget without recreating the connection. Takes
|
|
2825
2562
|
* effect on the next `connect()`; an in-flight connect uses the value
|
|
2826
2563
|
* captured at start. Mirrors `WebSocketTransport.setRetryTimeoutMs`.
|
|
2827
2564
|
*/
|
|
@@ -2885,30 +2622,18 @@ var ContainerControlConnection = class {
|
|
|
2885
2622
|
}
|
|
2886
2623
|
}
|
|
2887
2624
|
/**
|
|
2888
|
-
* Issue WebSocket upgrade fetches, retrying transient
|
|
2889
|
-
*
|
|
2890
|
-
*
|
|
2891
|
-
* runs out. Mirrors `WebSocketTransport.fetchUpgradeWithRetry` so both
|
|
2892
|
-
* transports behave the same way during container startup.
|
|
2625
|
+
* Issue WebSocket upgrade fetches, retrying transient control-plane
|
|
2626
|
+
* unavailability responses until either the upgrade succeeds, a
|
|
2627
|
+
* non-retryable status is returned, or the retry budget runs out.
|
|
2893
2628
|
*/
|
|
2894
2629
|
async fetchUpgradeWithRetry() {
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
if (remaining <= MIN_TIME_FOR_RETRY_MS) return response;
|
|
2903
|
-
const delay = Math.min(3e3 * 2 ** attempt, MAX_RETRY_BACKOFF_MS);
|
|
2904
|
-
this.logger.info("ContainerControlConnection upgrade returned 503, retrying", {
|
|
2905
|
-
attempt: attempt + 1,
|
|
2906
|
-
delayMs: delay,
|
|
2907
|
-
remainingSec: Math.floor(remaining / 1e3)
|
|
2908
|
-
});
|
|
2909
|
-
await this.sleep(delay);
|
|
2910
|
-
attempt++;
|
|
2911
|
-
}
|
|
2630
|
+
return fetchWithResponseRetry(() => this.fetchUpgradeAttempt(), {
|
|
2631
|
+
retryTimeoutMs: this.retryTimeoutMs,
|
|
2632
|
+
minTimeForRetryMs: MIN_TIME_FOR_RETRY_MS,
|
|
2633
|
+
logger: this.logger,
|
|
2634
|
+
retryLogMessage: "ContainerControlConnection upgrade returned retryable status, retrying",
|
|
2635
|
+
shouldRetry: isRetryableWebSocketUpgradeResponse
|
|
2636
|
+
});
|
|
2912
2637
|
}
|
|
2913
2638
|
/**
|
|
2914
2639
|
* Single WebSocket-upgrade fetch attempt. Owns its own AbortController so
|
|
@@ -2932,9 +2657,6 @@ var ContainerControlConnection = class {
|
|
|
2932
2657
|
clearTimeout(timeout);
|
|
2933
2658
|
}
|
|
2934
2659
|
}
|
|
2935
|
-
sleep(ms) {
|
|
2936
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2937
|
-
}
|
|
2938
2660
|
};
|
|
2939
2661
|
/**
|
|
2940
2662
|
* RPC transport that queues sends and blocks receives until a WebSocket
|
|
@@ -3301,32 +3023,6 @@ var ContainerControlClient = class {
|
|
|
3301
3023
|
get backup() {
|
|
3302
3024
|
return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
|
|
3303
3025
|
}
|
|
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
3026
|
get watch() {
|
|
3331
3027
|
return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
|
|
3332
3028
|
}
|
|
@@ -3337,7 +3033,7 @@ var ContainerControlClient = class {
|
|
|
3337
3033
|
return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
|
|
3338
3034
|
}
|
|
3339
3035
|
/**
|
|
3340
|
-
* Update the
|
|
3036
|
+
* Update the upgrade retry budget. Applies to the current connection
|
|
3341
3037
|
* (if any) and is remembered for any future connections created after the
|
|
3342
3038
|
* client is torn down and reconnected.
|
|
3343
3039
|
*/
|
|
@@ -4775,6 +4471,413 @@ const r2EgressHandler = async (request, env$1, ctx) => {
|
|
|
4775
4471
|
}
|
|
4776
4472
|
};
|
|
4777
4473
|
|
|
4474
|
+
//#endregion
|
|
4475
|
+
//#region src/storage-mount/s3-credential-proxy-handler.ts
|
|
4476
|
+
const PER_MOUNT_SUFFIX = ".s3-credential-proxy.internal";
|
|
4477
|
+
const SELF_TEST_PATH = "/__sandbox_credential_proxy_self_test__";
|
|
4478
|
+
const DIAGNOSTICS_PATH = "/__sandbox_credential_proxy_diagnostics__";
|
|
4479
|
+
const DEFAULT_SLOW_REQUEST_MS = 1e3;
|
|
4480
|
+
const ERROR_RESPONSE_BODY_LIMIT = 2048;
|
|
4481
|
+
const MAX_DIAGNOSTIC_EVENTS = 500;
|
|
4482
|
+
const DUMMY_AUTH_HEADERS = new Set([
|
|
4483
|
+
"authorization",
|
|
4484
|
+
"x-amz-date",
|
|
4485
|
+
"x-amz-content-sha256",
|
|
4486
|
+
"x-amz-security-token",
|
|
4487
|
+
"x-goog-date",
|
|
4488
|
+
"x-goog-content-sha256"
|
|
4489
|
+
]);
|
|
4490
|
+
const sigV4ClientCache = /* @__PURE__ */ new Map();
|
|
4491
|
+
const directoryMarkerCache = /* @__PURE__ */ new Map();
|
|
4492
|
+
const credentialProxyDiagnosticEvents = [];
|
|
4493
|
+
let credentialProxyDiagnosticEventCount = 0;
|
|
4494
|
+
function evictSigV4ClientCacheEntry(mountId) {
|
|
4495
|
+
sigV4ClientCache.delete(mountId);
|
|
4496
|
+
}
|
|
4497
|
+
function evictDirectoryMarkerCacheForMount(mountId) {
|
|
4498
|
+
const prefix = `${mountId}:`;
|
|
4499
|
+
for (const key of directoryMarkerCache.keys()) if (key.startsWith(prefix)) directoryMarkerCache.delete(key);
|
|
4500
|
+
}
|
|
4501
|
+
function toHex(buffer) {
|
|
4502
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4503
|
+
}
|
|
4504
|
+
async function sha256Hex(data) {
|
|
4505
|
+
return toHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data)));
|
|
4506
|
+
}
|
|
4507
|
+
async function hmacSHA256(key, data) {
|
|
4508
|
+
const cryptoKey = await crypto.subtle.importKey("raw", key, {
|
|
4509
|
+
name: "HMAC",
|
|
4510
|
+
hash: "SHA-256"
|
|
4511
|
+
}, false, ["sign"]);
|
|
4512
|
+
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data));
|
|
4513
|
+
}
|
|
4514
|
+
function detectS3Region(provider, endpoint) {
|
|
4515
|
+
if (provider === "r2") return "auto";
|
|
4516
|
+
try {
|
|
4517
|
+
const host = new URL(endpoint).hostname;
|
|
4518
|
+
const m = host.match(/s3[.-]([a-z0-9-]+)\.amazonaws\.com/);
|
|
4519
|
+
if (m && m[1] !== "amazonaws") return m[1];
|
|
4520
|
+
if (host === "s3.amazonaws.com") return "us-east-1";
|
|
4521
|
+
} catch {}
|
|
4522
|
+
return "auto";
|
|
4523
|
+
}
|
|
4524
|
+
function buildCleanHeaders(original) {
|
|
4525
|
+
const clean = new Headers();
|
|
4526
|
+
for (const [k, v] of original) {
|
|
4527
|
+
const lower = k.toLowerCase();
|
|
4528
|
+
if (!DUMMY_AUTH_HEADERS.has(lower) && lower !== "host") clean.set(k, v);
|
|
4529
|
+
}
|
|
4530
|
+
const contentSHA256 = original.get("x-amz-content-sha256");
|
|
4531
|
+
if (contentSHA256 && isValidContentSHA256(contentSHA256)) clean.set("x-amz-content-sha256", contentSHA256);
|
|
4532
|
+
return clean;
|
|
4533
|
+
}
|
|
4534
|
+
function isValidContentSHA256(value) {
|
|
4535
|
+
return value === "UNSIGNED-PAYLOAD" || /^[a-fA-F0-9]{64}$/.test(value);
|
|
4536
|
+
}
|
|
4537
|
+
function getCredentialProxyDebugConfig(env$1) {
|
|
4538
|
+
const envRecord = env$1;
|
|
4539
|
+
const enabled = envRecord.SANDBOX_CREDENTIAL_PROXY_DEBUG === "true";
|
|
4540
|
+
const diagnosticsEndpointEnabled = envRecord.SANDBOX_CREDENTIAL_PROXY_DIAGNOSTICS_ENDPOINT === "true";
|
|
4541
|
+
const configuredSlowRequestMs = Number(envRecord.SANDBOX_CREDENTIAL_PROXY_SLOW_REQUEST_MS);
|
|
4542
|
+
return {
|
|
4543
|
+
diagnosticsEndpointEnabled,
|
|
4544
|
+
enabled,
|
|
4545
|
+
slowRequestMs: Number.isFinite(configuredSlowRequestMs) && configuredSlowRequestMs >= 0 ? configuredSlowRequestMs : DEFAULT_SLOW_REQUEST_MS
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4548
|
+
function recordCredentialProxyDiagnosticEvent(event) {
|
|
4549
|
+
credentialProxyDiagnosticEvents.push(event);
|
|
4550
|
+
credentialProxyDiagnosticEventCount++;
|
|
4551
|
+
while (credentialProxyDiagnosticEvents.length > MAX_DIAGNOSTIC_EVENTS) credentialProxyDiagnosticEvents.shift();
|
|
4552
|
+
}
|
|
4553
|
+
function getCredentialProxyDiagnosticsResponse(url, containerId) {
|
|
4554
|
+
const since = Number(url.searchParams.get("since") ?? "0");
|
|
4555
|
+
const bufferStartCount = credentialProxyDiagnosticEventCount - credentialProxyDiagnosticEvents.length;
|
|
4556
|
+
const events = credentialProxyDiagnosticEvents.filter((event, index) => {
|
|
4557
|
+
if (event.containerId !== containerId) return false;
|
|
4558
|
+
return !Number.isFinite(since) || bufferStartCount + index >= since;
|
|
4559
|
+
});
|
|
4560
|
+
return Response.json({
|
|
4561
|
+
nextCursor: credentialProxyDiagnosticEventCount,
|
|
4562
|
+
events
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
async function withCredentialProxyDiagnostics(requestInfo, debugConfig, containerId, path$1, operation) {
|
|
4566
|
+
const started = Date.now();
|
|
4567
|
+
try {
|
|
4568
|
+
const response = await operation();
|
|
4569
|
+
const durationMs = Date.now() - started;
|
|
4570
|
+
if (debugConfig.enabled) recordCredentialProxyDiagnosticEvent({
|
|
4571
|
+
...requestInfo,
|
|
4572
|
+
containerId,
|
|
4573
|
+
durationMs,
|
|
4574
|
+
ok: response.ok,
|
|
4575
|
+
path: path$1,
|
|
4576
|
+
status: response.status,
|
|
4577
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4578
|
+
});
|
|
4579
|
+
if (debugConfig.enabled || durationMs >= debugConfig.slowRequestMs) console.info("sandbox.s3_credential_proxy.request", {
|
|
4580
|
+
...requestInfo,
|
|
4581
|
+
durationMs,
|
|
4582
|
+
ok: response.ok,
|
|
4583
|
+
status: response.status,
|
|
4584
|
+
responseContentLength: response.headers.get("content-length")
|
|
4585
|
+
});
|
|
4586
|
+
if (!response.ok) {
|
|
4587
|
+
const responseForLog = response.clone();
|
|
4588
|
+
const requestInfoSnapshot = { ...requestInfo };
|
|
4589
|
+
responseForLog.text().then((body) => {
|
|
4590
|
+
console.warn("sandbox.s3_credential_proxy.upstream_error", {
|
|
4591
|
+
...requestInfoSnapshot,
|
|
4592
|
+
durationMs,
|
|
4593
|
+
status: response.status,
|
|
4594
|
+
statusText: response.statusText,
|
|
4595
|
+
errorBody: body.slice(0, ERROR_RESPONSE_BODY_LIMIT)
|
|
4596
|
+
});
|
|
4597
|
+
}).catch(() => {});
|
|
4598
|
+
}
|
|
4599
|
+
return response;
|
|
4600
|
+
} catch (error) {
|
|
4601
|
+
const durationMs = Date.now() - started;
|
|
4602
|
+
console.warn("sandbox.s3_credential_proxy.request_error", {
|
|
4603
|
+
...requestInfo,
|
|
4604
|
+
durationMs,
|
|
4605
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4606
|
+
});
|
|
4607
|
+
throw error;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
function getSigV4Client(mountId, endpoint, provider, credentials, region) {
|
|
4611
|
+
const cached = sigV4ClientCache.get(mountId);
|
|
4612
|
+
if (cached && cached.accessKeyId === credentials.accessKeyId && cached.secretAccessKey === credentials.secretAccessKey && cached.endpoint === endpoint && cached.provider === provider && cached.region === region) return cached.client;
|
|
4613
|
+
const client = new AwsClient({
|
|
4614
|
+
accessKeyId: credentials.accessKeyId,
|
|
4615
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
4616
|
+
service: "s3",
|
|
4617
|
+
region,
|
|
4618
|
+
retries: 0
|
|
4619
|
+
});
|
|
4620
|
+
sigV4ClientCache.set(mountId, {
|
|
4621
|
+
client,
|
|
4622
|
+
accessKeyId: credentials.accessKeyId,
|
|
4623
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
4624
|
+
endpoint,
|
|
4625
|
+
provider,
|
|
4626
|
+
region
|
|
4627
|
+
});
|
|
4628
|
+
return client;
|
|
4629
|
+
}
|
|
4630
|
+
function encodeCanonicalQueryPart(value) {
|
|
4631
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
4632
|
+
}
|
|
4633
|
+
function getCanonicalURI(url) {
|
|
4634
|
+
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("/");
|
|
4635
|
+
}
|
|
4636
|
+
function getCanonicalQueryString(url) {
|
|
4637
|
+
const query = url.search.startsWith("?") ? url.search.slice(1) : url.search;
|
|
4638
|
+
if (!query) return "";
|
|
4639
|
+
return query.split("&").map((part) => {
|
|
4640
|
+
const separatorIndex = part.indexOf("=");
|
|
4641
|
+
if (separatorIndex === -1) return [part, ""];
|
|
4642
|
+
return [part.slice(0, separatorIndex), part.slice(separatorIndex + 1)];
|
|
4643
|
+
}).map(([key, value]) => [encodeCanonicalQueryPart(decodeURIEncodedQueryPart(key)), encodeCanonicalQueryPart(decodeURIEncodedQueryPart(value))]).sort(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
4644
|
+
if (leftKey < rightKey) return -1;
|
|
4645
|
+
if (leftKey > rightKey) return 1;
|
|
4646
|
+
if (leftValue < rightValue) return -1;
|
|
4647
|
+
if (leftValue > rightValue) return 1;
|
|
4648
|
+
return 0;
|
|
4649
|
+
}).map(([key, value]) => `${key}=${value}`).join("&");
|
|
4650
|
+
}
|
|
4651
|
+
function decodeURIEncodedQueryPart(value) {
|
|
4652
|
+
try {
|
|
4653
|
+
return decodeURIComponent(value);
|
|
4654
|
+
} catch {
|
|
4655
|
+
return value;
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
function getSigV4PayloadHash(headers) {
|
|
4659
|
+
const existingHash = headers.get("x-amz-content-sha256");
|
|
4660
|
+
if (existingHash && existingHash !== "UNSIGNED-PAYLOAD") return {
|
|
4661
|
+
hash: existingHash,
|
|
4662
|
+
mode: "signed"
|
|
4663
|
+
};
|
|
4664
|
+
return {
|
|
4665
|
+
hash: "UNSIGNED-PAYLOAD",
|
|
4666
|
+
mode: "unsigned"
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
function isZeroLengthDirectoryMarkerPUT(request, realPath) {
|
|
4670
|
+
return request.method.toUpperCase() === "PUT" && request.headers.get("content-length") === "0" && realPath.endsWith("/");
|
|
4671
|
+
}
|
|
4672
|
+
function isDirectoryMarkerHEAD(request) {
|
|
4673
|
+
return request.method.toUpperCase() === "HEAD";
|
|
4674
|
+
}
|
|
4675
|
+
function getDirectoryMarkerCacheKey(mountId, realPath) {
|
|
4676
|
+
return `${mountId}:${realPath.replace(/\/+$/, "")}`;
|
|
4677
|
+
}
|
|
4678
|
+
function getDirectoryMarkerResponseHeaders(request) {
|
|
4679
|
+
const headers = [
|
|
4680
|
+
["Accept-Ranges", "bytes"],
|
|
4681
|
+
["Content-Length", "0"],
|
|
4682
|
+
["ETag", "\"d41d8cd98f00b204e9800998ecf8427e\""],
|
|
4683
|
+
["Last-Modified", (/* @__PURE__ */ new Date()).toUTCString()]
|
|
4684
|
+
];
|
|
4685
|
+
const contentType = request.headers.get("content-type");
|
|
4686
|
+
if (contentType) headers.push(["Content-Type", contentType]);
|
|
4687
|
+
for (const [name, value] of request.headers) if (name.toLowerCase().startsWith("x-amz-meta-")) headers.push([name, value]);
|
|
4688
|
+
return headers;
|
|
4689
|
+
}
|
|
4690
|
+
function normalizePrefix(prefix) {
|
|
4691
|
+
if (!prefix) return void 0;
|
|
4692
|
+
return prefix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
4693
|
+
}
|
|
4694
|
+
function getObjectKeyForPath(realPath, bucket) {
|
|
4695
|
+
const pathSegments = realPath.split("/").filter(Boolean);
|
|
4696
|
+
if (pathSegments[0] !== bucket) return null;
|
|
4697
|
+
return pathSegments.slice(1).join("/");
|
|
4698
|
+
}
|
|
4699
|
+
function isObjectKeyWithinPrefix(objectKey, prefix) {
|
|
4700
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
4701
|
+
if (!normalizedPrefix) return true;
|
|
4702
|
+
return objectKey === normalizedPrefix || objectKey.startsWith(`${normalizedPrefix}/`);
|
|
4703
|
+
}
|
|
4704
|
+
function isRequestWithinMountScope(realPath, url, bucket, prefix) {
|
|
4705
|
+
const objectKey = getObjectKeyForPath(realPath, bucket);
|
|
4706
|
+
if (objectKey === null) return false;
|
|
4707
|
+
const requestedPrefix = url.searchParams.get("prefix");
|
|
4708
|
+
if (objectKey !== "" && !isObjectKeyWithinPrefix(objectKey, prefix)) return false;
|
|
4709
|
+
if (objectKey === "" && normalizePrefix(prefix) !== void 0 && url.search !== "" && requestedPrefix === null) return false;
|
|
4710
|
+
if (requestedPrefix !== null) return isObjectKeyWithinPrefix(requestedPrefix, prefix);
|
|
4711
|
+
return true;
|
|
4712
|
+
}
|
|
4713
|
+
function isBucketRootProbe(request, realPath, url, bucket) {
|
|
4714
|
+
const method = request.method.toUpperCase();
|
|
4715
|
+
return (method === "GET" || method === "HEAD") && url.search === "" && getObjectKeyForPath(realPath, bucket) === "";
|
|
4716
|
+
}
|
|
4717
|
+
function deleteDirectoryMarkerCacheEntry(mountId, realPath) {
|
|
4718
|
+
directoryMarkerCache.delete(getDirectoryMarkerCacheKey(mountId, realPath));
|
|
4719
|
+
}
|
|
4720
|
+
function getContentLength(request) {
|
|
4721
|
+
const contentLength = request.headers.get("content-length");
|
|
4722
|
+
if (contentLength === null) return null;
|
|
4723
|
+
const parsed = Number(contentLength);
|
|
4724
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) return null;
|
|
4725
|
+
return parsed;
|
|
4726
|
+
}
|
|
4727
|
+
function getSigV4ForwardInit(request) {
|
|
4728
|
+
const contentLength = getContentLength(request);
|
|
4729
|
+
if (contentLength === 0) return { body: new Uint8Array(0) };
|
|
4730
|
+
if (contentLength === null || request.body === null) return {};
|
|
4731
|
+
const { readable, writable } = new FixedLengthStream(contentLength);
|
|
4732
|
+
request.body.pipeTo(writable).catch((error) => {
|
|
4733
|
+
writable.abort(error).catch(() => {});
|
|
4734
|
+
});
|
|
4735
|
+
return { body: readable };
|
|
4736
|
+
}
|
|
4737
|
+
function getGCSHeaders(request) {
|
|
4738
|
+
const headers = new Headers();
|
|
4739
|
+
for (const [k, v] of request.headers) {
|
|
4740
|
+
const lower = k.toLowerCase();
|
|
4741
|
+
if (DUMMY_AUTH_HEADERS.has(lower) || lower === "host" || lower === "content-length" || lower === "expect") continue;
|
|
4742
|
+
if (lower.startsWith("x-amz-meta-")) {
|
|
4743
|
+
headers.set(`x-goog-meta-${lower.slice(11)}`, v);
|
|
4744
|
+
continue;
|
|
4745
|
+
}
|
|
4746
|
+
if (lower.startsWith("x-amz-")) continue;
|
|
4747
|
+
headers.set(k, v);
|
|
4748
|
+
}
|
|
4749
|
+
return headers;
|
|
4750
|
+
}
|
|
4751
|
+
async function signAndForwardSigV4(request, mountId, endpoint, provider, credentials, requestInfo) {
|
|
4752
|
+
const signingStarted = Date.now();
|
|
4753
|
+
const region = detectS3Region(provider, endpoint);
|
|
4754
|
+
const payload = getSigV4PayloadHash(request.headers);
|
|
4755
|
+
const client = getSigV4Client(mountId, endpoint, provider, credentials, region);
|
|
4756
|
+
requestInfo.payloadHashMode = payload.mode;
|
|
4757
|
+
requestInfo.clientSetupMs = Date.now() - signingStarted;
|
|
4758
|
+
const upstreamStarted = Date.now();
|
|
4759
|
+
const forwardInit = getSigV4ForwardInit(request);
|
|
4760
|
+
requestInfo.bodyPresent = forwardInit?.body !== void 0 || request.body !== null;
|
|
4761
|
+
const response = await client.fetch(request, forwardInit);
|
|
4762
|
+
requestInfo.upstreamMs = Date.now() - upstreamStarted;
|
|
4763
|
+
return response;
|
|
4764
|
+
}
|
|
4765
|
+
async function signAndForwardGCS(request, credentials, requestInfo) {
|
|
4766
|
+
const url = new URL(request.url);
|
|
4767
|
+
const gcsHeaders = getGCSHeaders(request);
|
|
4768
|
+
const dateStr = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:-]/g, "").replace(/\.\d+Z$/, "")}Z`;
|
|
4769
|
+
const dateOnly = dateStr.slice(0, 8);
|
|
4770
|
+
const location = "auto";
|
|
4771
|
+
const service = "storage";
|
|
4772
|
+
const credentialScope = `${dateOnly}/${location}/${service}/goog4_request`;
|
|
4773
|
+
const bodyHash = "UNSIGNED-PAYLOAD";
|
|
4774
|
+
const headerEntries = [
|
|
4775
|
+
["host", url.host],
|
|
4776
|
+
["x-goog-content-sha256", bodyHash],
|
|
4777
|
+
["x-goog-date", dateStr]
|
|
4778
|
+
];
|
|
4779
|
+
for (const [k, v] of gcsHeaders) headerEntries.push([k.toLowerCase(), v.trim()]);
|
|
4780
|
+
headerEntries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
4781
|
+
const signedHeaders = headerEntries.map(([k]) => k).join(";");
|
|
4782
|
+
const canonicalHeaders = headerEntries.map(([k, v]) => `${k}:${v}\n`).join("");
|
|
4783
|
+
const stringToSign = [
|
|
4784
|
+
"GOOG4-HMAC-SHA256",
|
|
4785
|
+
dateStr,
|
|
4786
|
+
credentialScope,
|
|
4787
|
+
await sha256Hex([
|
|
4788
|
+
request.method,
|
|
4789
|
+
getCanonicalURI(url),
|
|
4790
|
+
getCanonicalQueryString(url),
|
|
4791
|
+
canonicalHeaders,
|
|
4792
|
+
signedHeaders,
|
|
4793
|
+
bodyHash
|
|
4794
|
+
].join("\n"))
|
|
4795
|
+
].join("\n");
|
|
4796
|
+
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));
|
|
4797
|
+
const authorization = `GOOG4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
4798
|
+
const newHeaders = new Headers(gcsHeaders);
|
|
4799
|
+
newHeaders.set("x-goog-date", dateStr);
|
|
4800
|
+
newHeaders.set("x-goog-content-sha256", bodyHash);
|
|
4801
|
+
newHeaders.set("Authorization", authorization);
|
|
4802
|
+
const gcsBody = getContentLength(request) === 0 ? new Uint8Array(0) : request.body;
|
|
4803
|
+
const upstreamStarted = Date.now();
|
|
4804
|
+
const response = await fetch(new Request(request.url, {
|
|
4805
|
+
method: request.method,
|
|
4806
|
+
headers: newHeaders,
|
|
4807
|
+
body: gcsBody
|
|
4808
|
+
}));
|
|
4809
|
+
requestInfo.upstreamMs = Date.now() - upstreamStarted;
|
|
4810
|
+
return response;
|
|
4811
|
+
}
|
|
4812
|
+
const s3CredentialProxyHandler = async (request, env$1, ctx) => {
|
|
4813
|
+
const url = new URL(request.url);
|
|
4814
|
+
if (url.pathname === SELF_TEST_PATH) return new Response("OK", { status: 200 });
|
|
4815
|
+
const debugConfig = getCredentialProxyDebugConfig(env$1);
|
|
4816
|
+
if (url.pathname === DIAGNOSTICS_PATH) {
|
|
4817
|
+
if (!debugConfig.enabled || !debugConfig.diagnosticsEndpointEnabled) return new Response("Not Found", { status: 404 });
|
|
4818
|
+
return getCredentialProxyDiagnosticsResponse(url, ctx.containerId);
|
|
4819
|
+
}
|
|
4820
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
4821
|
+
const hostname = url.hostname;
|
|
4822
|
+
let mountId;
|
|
4823
|
+
let realPath;
|
|
4824
|
+
if (hostname.endsWith(PER_MOUNT_SUFFIX)) {
|
|
4825
|
+
mountId = hostname.slice(0, -29);
|
|
4826
|
+
realPath = url.pathname;
|
|
4827
|
+
} else {
|
|
4828
|
+
mountId = segments[0] ?? null;
|
|
4829
|
+
realPath = mountId ? url.pathname.slice(`/${mountId}`.length) || "/" : "/";
|
|
4830
|
+
}
|
|
4831
|
+
if (!mountId) return new Response("Bad Request: missing mount ID", { status: 400 });
|
|
4832
|
+
const mount = ctx.params?.mounts[mountId];
|
|
4833
|
+
if (!mount) return new Response(`Forbidden: unknown mount ID "${mountId}"`, { status: 403 });
|
|
4834
|
+
if (mount.readOnly) {
|
|
4835
|
+
const method = request.method.toUpperCase();
|
|
4836
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") return new Response("Forbidden: bucket mount is read-only", { status: 403 });
|
|
4837
|
+
}
|
|
4838
|
+
const realUrl = new URL(realPath + (url.search || ""), mount.endpoint);
|
|
4839
|
+
if (isBucketRootProbe(request, realPath, url, mount.bucket)) return new Response(null, { status: 200 });
|
|
4840
|
+
if (!isRequestWithinMountScope(realPath, url, mount.bucket, mount.prefix)) return new Response("Forbidden: request is outside mounted bucket scope", { status: 403 });
|
|
4841
|
+
const cleanHeaders = buildCleanHeaders(request.headers);
|
|
4842
|
+
const cleanRequest = new Request(realUrl.toString(), {
|
|
4843
|
+
method: request.method,
|
|
4844
|
+
headers: cleanHeaders,
|
|
4845
|
+
body: request.body
|
|
4846
|
+
});
|
|
4847
|
+
const requestInfo = {
|
|
4848
|
+
authStrategy: mount.authStrategy,
|
|
4849
|
+
bucket: mount.bucket,
|
|
4850
|
+
contentLength: request.headers.get("content-length"),
|
|
4851
|
+
method: request.method,
|
|
4852
|
+
mountId,
|
|
4853
|
+
query: [...url.searchParams.keys()].sort()
|
|
4854
|
+
};
|
|
4855
|
+
if (isZeroLengthDirectoryMarkerPUT(cleanRequest, realPath)) {
|
|
4856
|
+
const responseHeaders = getDirectoryMarkerResponseHeaders(cleanRequest);
|
|
4857
|
+
requestInfo.bodyPresent = request.body !== null;
|
|
4858
|
+
if (mount.authStrategy === "s3-sigv4") requestInfo.payloadHashMode = getSigV4PayloadHash(cleanRequest.headers).mode;
|
|
4859
|
+
return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, async () => {
|
|
4860
|
+
const response = mount.authStrategy === "gcs" ? await signAndForwardGCS(cleanRequest, mount.credentials, requestInfo) : await signAndForwardSigV4(cleanRequest, mountId, mount.endpoint, mount.provider, mount.credentials, requestInfo);
|
|
4861
|
+
if (response.ok) directoryMarkerCache.set(getDirectoryMarkerCacheKey(mountId, realPath), responseHeaders);
|
|
4862
|
+
return response;
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
if (isDirectoryMarkerHEAD(cleanRequest)) {
|
|
4866
|
+
const responseHeaders = directoryMarkerCache.get(getDirectoryMarkerCacheKey(mountId, realPath));
|
|
4867
|
+
if (responseHeaders) {
|
|
4868
|
+
requestInfo.bodyPresent = false;
|
|
4869
|
+
if (mount.authStrategy === "s3-sigv4") requestInfo.payloadHashMode = getSigV4PayloadHash(cleanRequest.headers).mode;
|
|
4870
|
+
return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => Promise.resolve(new Response(null, {
|
|
4871
|
+
status: 200,
|
|
4872
|
+
headers: responseHeaders
|
|
4873
|
+
})));
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
if (cleanRequest.method.toUpperCase() !== "HEAD") deleteDirectoryMarkerCacheEntry(mountId, realPath);
|
|
4877
|
+
if (mount.authStrategy === "gcs") return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => signAndForwardGCS(cleanRequest, mount.credentials, requestInfo));
|
|
4878
|
+
return withCredentialProxyDiagnostics(requestInfo, debugConfig, ctx.containerId, realPath, () => signAndForwardSigV4(cleanRequest, mountId, mount.endpoint, mount.provider, mount.credentials, requestInfo));
|
|
4879
|
+
};
|
|
4880
|
+
|
|
4778
4881
|
//#endregion
|
|
4779
4882
|
//#region src/tunnels/credentials.ts
|
|
4780
4883
|
/**
|
|
@@ -5731,16 +5834,47 @@ async function pruneTunnelsForRestart(storage) {
|
|
|
5731
5834
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
5732
5835
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
5733
5836
|
*/
|
|
5734
|
-
const SDK_VERSION = "0.
|
|
5837
|
+
const SDK_VERSION = "0.12.1";
|
|
5735
5838
|
|
|
5736
5839
|
//#endregion
|
|
5737
5840
|
//#region src/sandbox.ts
|
|
5738
5841
|
const PORT_TOKENS_STORAGE_KEY = "portTokens";
|
|
5739
5842
|
const ACTIVE_PREVIEW_PORTS_STORAGE_KEY = "activePreviewPorts";
|
|
5740
|
-
const
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5843
|
+
const CONTAINER_PROXY_CLASS_NAME = "ContainerProxy";
|
|
5844
|
+
const S3_CREDENTIAL_PROXY_HOST = "s3-credential-proxy.internal";
|
|
5845
|
+
const S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST = "s3-credential-proxy.sandbox.test";
|
|
5846
|
+
var ContainerProxyOutboundTarget = class extends Container {};
|
|
5847
|
+
Object.defineProperty(ContainerProxyOutboundTarget, "name", { value: CONTAINER_PROXY_CLASS_NAME });
|
|
5848
|
+
ContainerProxyOutboundTarget.outboundHandlers = {
|
|
5849
|
+
r2EgressMount: r2EgressHandler,
|
|
5850
|
+
s3CredentialProxyMount: s3CredentialProxyHandler
|
|
5851
|
+
};
|
|
5852
|
+
/**
|
|
5853
|
+
* SDK-level ContainerProxy that directly dispatches SDK-internal mount hosts
|
|
5854
|
+
* (r2.internal, s3-credential-proxy.internal) without relying on
|
|
5855
|
+
* outboundHandlersRegistry lookups, which are NOT shared between the Durable
|
|
5856
|
+
* Object's execution context and the ContainerProxy WorkerEntrypoint context.
|
|
5857
|
+
*
|
|
5858
|
+
* Users must export this class from their Worker entrypoint so the Sandbox DO
|
|
5859
|
+
* can create outbound-interception fetchers that reference it.
|
|
5860
|
+
*/
|
|
5861
|
+
var ContainerProxy$1 = class extends ContainerProxy {
|
|
5862
|
+
async fetch(request) {
|
|
5863
|
+
const hostname = new URL(request.url).hostname;
|
|
5864
|
+
const props = this.ctx.props;
|
|
5865
|
+
const override = props.outboundByHostOverrides?.[hostname];
|
|
5866
|
+
if (override) {
|
|
5867
|
+
const handlerCtx = {
|
|
5868
|
+
containerId: props.containerId ?? "",
|
|
5869
|
+
className: props.className ?? "",
|
|
5870
|
+
params: override.params
|
|
5871
|
+
};
|
|
5872
|
+
if (override.method === "r2EgressMount") return r2EgressHandler(request, this.env, handlerCtx);
|
|
5873
|
+
if (override.method === "s3CredentialProxyMount") return s3CredentialProxyHandler(request, this.env, handlerCtx);
|
|
5874
|
+
}
|
|
5875
|
+
return super.fetch(request);
|
|
5876
|
+
}
|
|
5877
|
+
};
|
|
5744
5878
|
function isFetcher(value) {
|
|
5745
5879
|
return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
|
|
5746
5880
|
}
|
|
@@ -5750,6 +5884,8 @@ const R2_DEFAULT_S3FS_OPTIONS = {
|
|
|
5750
5884
|
enable_noobj_cache: true,
|
|
5751
5885
|
multipart_size: "5"
|
|
5752
5886
|
};
|
|
5887
|
+
const R2_DEFAULT_S3FS_OPTION_ENTRIES = Object.entries(R2_DEFAULT_S3FS_OPTIONS).map(([key, value]) => value === true ? key : `${key}=${value}`);
|
|
5888
|
+
const S3FS_DISABLE_EXPECT_HEADER_CONFIG = " Expect:\n";
|
|
5753
5889
|
const BACKUP_DEFAULT_TTL_SECONDS = 259200;
|
|
5754
5890
|
const BACKUP_MAX_NAME_LENGTH = 256;
|
|
5755
5891
|
const BACKUP_CONTAINER_DIR = "/var/backups";
|
|
@@ -5941,10 +6077,6 @@ function getSandbox(ns, id, options) {
|
|
|
5941
6077
|
}),
|
|
5942
6078
|
terminal: (request, opts) => proxyTerminal(stub, defaultSessionId, request, opts),
|
|
5943
6079
|
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
6080
|
tunnels: new Proxy({}, { get: (_, method) => {
|
|
5949
6081
|
if (typeof method !== "string" || method === "then") return void 0;
|
|
5950
6082
|
return (...args) => stub.callTunnels(method, args);
|
|
@@ -5986,6 +6118,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5986
6118
|
logger;
|
|
5987
6119
|
keepAliveEnabled = false;
|
|
5988
6120
|
activeMounts = /* @__PURE__ */ new Map();
|
|
6121
|
+
mountOperationQueue = Promise.resolve();
|
|
5989
6122
|
currentRuntime;
|
|
5990
6123
|
transport = "http";
|
|
5991
6124
|
/**
|
|
@@ -6052,56 +6185,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6052
6185
|
*/
|
|
6053
6186
|
hasStoredContainerTimeouts = false;
|
|
6054
6187
|
/**
|
|
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
6188
|
* Dispatch method for tunnel operations.
|
|
6106
6189
|
* Called by the client-side proxy created in getSandbox() to provide
|
|
6107
6190
|
* the `sandbox.tunnels` API without relying on RPC pipelining
|
|
@@ -6406,6 +6489,24 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6406
6489
|
* @throws InvalidMountConfigError if bucket name, mount path, or endpoint is invalid
|
|
6407
6490
|
*/
|
|
6408
6491
|
async mountBucket(bucket, mountPath, options) {
|
|
6492
|
+
return this.runMountOperation(async () => {
|
|
6493
|
+
await this.mountBucketUnlocked(bucket, mountPath, options);
|
|
6494
|
+
});
|
|
6495
|
+
}
|
|
6496
|
+
async runMountOperation(operation) {
|
|
6497
|
+
const previous = this.mountOperationQueue;
|
|
6498
|
+
let release;
|
|
6499
|
+
this.mountOperationQueue = new Promise((resolve) => {
|
|
6500
|
+
release = resolve;
|
|
6501
|
+
});
|
|
6502
|
+
await previous.catch(() => {});
|
|
6503
|
+
try {
|
|
6504
|
+
await operation();
|
|
6505
|
+
} finally {
|
|
6506
|
+
release();
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
async mountBucketUnlocked(bucket, mountPath, options) {
|
|
6409
6510
|
if (options.prefix !== void 0) validatePrefix(options.prefix);
|
|
6410
6511
|
if ("localBucket" in options && options.localBucket) {
|
|
6411
6512
|
await this.mountBucketLocal(bucket, mountPath, options);
|
|
@@ -6445,6 +6546,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6445
6546
|
logger: this.logger
|
|
6446
6547
|
});
|
|
6447
6548
|
const mountInfo = {
|
|
6549
|
+
mountId: crypto.randomUUID(),
|
|
6448
6550
|
mountType: "local-sync",
|
|
6449
6551
|
bucket,
|
|
6450
6552
|
mountPath,
|
|
@@ -6485,13 +6587,36 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6485
6587
|
};
|
|
6486
6588
|
return { buckets };
|
|
6487
6589
|
}
|
|
6488
|
-
|
|
6590
|
+
validateProtectedS3fsOptions(options, mountLabel, extraProtected = []) {
|
|
6489
6591
|
if (!options) return;
|
|
6490
|
-
const protectedOptions = new Set([
|
|
6592
|
+
const protectedOptions = new Set([
|
|
6593
|
+
"passwd_file",
|
|
6594
|
+
"url",
|
|
6595
|
+
...extraProtected
|
|
6596
|
+
]);
|
|
6491
6597
|
for (const option of options) {
|
|
6492
6598
|
const [key] = option.split("=");
|
|
6493
|
-
if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for
|
|
6599
|
+
if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for ${mountLabel} mounts`);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
getS3CredentialProxyParams(options) {
|
|
6603
|
+
const mounts = {};
|
|
6604
|
+
for (const [, m] of this.activeMounts) if (m.mountType === "fuse" && m.credentialProxy) {
|
|
6605
|
+
if (m.mountId === options?.excludeMountId) continue;
|
|
6606
|
+
mounts[m.mountId] = {
|
|
6607
|
+
endpoint: m.credentialProxy.endpoint,
|
|
6608
|
+
bucket: m.credentialProxy.bucket,
|
|
6609
|
+
...m.credentialProxy.prefix !== void 0 ? { prefix: m.credentialProxy.prefix } : {},
|
|
6610
|
+
credentials: m.credentialProxy.credentials,
|
|
6611
|
+
readOnly: m.credentialProxy.readOnly,
|
|
6612
|
+
provider: m.credentialProxy.provider,
|
|
6613
|
+
authStrategy: m.credentialProxy.authStrategy
|
|
6614
|
+
};
|
|
6494
6615
|
}
|
|
6616
|
+
return { mounts };
|
|
6617
|
+
}
|
|
6618
|
+
resolveCredentialProxyAuthStrategy(provider) {
|
|
6619
|
+
return provider === "gcs" ? "gcs" : "s3-sigv4";
|
|
6495
6620
|
}
|
|
6496
6621
|
/**
|
|
6497
6622
|
* Credential-less R2 mount: egress interception routes s3fs requests to the
|
|
@@ -6502,24 +6627,30 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6502
6627
|
const prefix = options.prefix;
|
|
6503
6628
|
let mountOutcome = "error";
|
|
6504
6629
|
let mountError;
|
|
6630
|
+
let passwordFilePath;
|
|
6631
|
+
let additionalHeaderFilePath;
|
|
6505
6632
|
try {
|
|
6506
6633
|
validateBucketBindingName(bucket, mountPath);
|
|
6507
6634
|
this.validateMountPath(mountPath);
|
|
6508
|
-
this.
|
|
6635
|
+
this.validateProtectedS3fsOptions(options.s3fsOptions, "R2 binding");
|
|
6509
6636
|
for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
|
|
6510
6637
|
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
6638
|
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
6639
|
}
|
|
6513
|
-
|
|
6640
|
+
passwordFilePath = this.generatePasswordFilePath();
|
|
6641
|
+
additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
|
|
6514
6642
|
await this.createPasswordFile(passwordFilePath, bucket, {
|
|
6515
6643
|
accessKeyId: "x",
|
|
6516
6644
|
secretAccessKey: "x"
|
|
6517
6645
|
});
|
|
6646
|
+
await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
|
|
6518
6647
|
const mountInfo = {
|
|
6648
|
+
mountId: crypto.randomUUID(),
|
|
6519
6649
|
mountType: "r2-egress",
|
|
6520
6650
|
bucket,
|
|
6521
6651
|
mountPath,
|
|
6522
6652
|
passwordFilePath,
|
|
6653
|
+
additionalHeaderFilePath,
|
|
6523
6654
|
mounted: false,
|
|
6524
6655
|
prefix,
|
|
6525
6656
|
readOnly: options.readOnly ?? false
|
|
@@ -6534,6 +6665,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6534
6665
|
...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
|
|
6535
6666
|
use_path_request_style: true,
|
|
6536
6667
|
url: "http://r2.internal",
|
|
6668
|
+
ahbe_conf: additionalHeaderFilePath,
|
|
6537
6669
|
...options.readOnly ? { ro: true } : {}
|
|
6538
6670
|
}));
|
|
6539
6671
|
const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
@@ -6557,7 +6689,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6557
6689
|
mountError = error instanceof Error ? error : new Error(String(error));
|
|
6558
6690
|
const failedMount = this.activeMounts.get(mountPath);
|
|
6559
6691
|
this.activeMounts.delete(mountPath);
|
|
6560
|
-
if (failedMount?.mountType === "r2-egress")
|
|
6692
|
+
if (failedMount?.mountType === "r2-egress") {
|
|
6693
|
+
await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
|
|
6694
|
+
if (failedMount.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(failedMount.additionalHeaderFilePath).catch(() => {});
|
|
6695
|
+
} else {
|
|
6696
|
+
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath).catch(() => {});
|
|
6697
|
+
if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath).catch(() => {});
|
|
6698
|
+
}
|
|
6561
6699
|
const remainingParams = this.getR2EgressParams();
|
|
6562
6700
|
await this.configureR2EgressOutbound(remainingParams).catch(() => {});
|
|
6563
6701
|
throw error;
|
|
@@ -6583,6 +6721,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6583
6721
|
let mountOutcome = "error";
|
|
6584
6722
|
let mountError;
|
|
6585
6723
|
let passwordFilePath;
|
|
6724
|
+
let additionalHeaderFilePath;
|
|
6586
6725
|
let provider = null;
|
|
6587
6726
|
let dirExisted = true;
|
|
6588
6727
|
try {
|
|
@@ -6604,33 +6743,81 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6604
6743
|
R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
|
|
6605
6744
|
...this.envVars
|
|
6606
6745
|
});
|
|
6746
|
+
const credentialProxyEnabled = options.credentialProxy === true;
|
|
6747
|
+
if (credentialProxyEnabled) this.validateProtectedS3fsOptions(options.s3fsOptions, "credential proxy", ["ahbe_conf", "use_path_request_style"]);
|
|
6607
6748
|
passwordFilePath = this.generatePasswordFilePath();
|
|
6749
|
+
if (credentialProxyEnabled) additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
|
|
6750
|
+
const mountId = crypto.randomUUID();
|
|
6608
6751
|
const mountInfo = {
|
|
6752
|
+
mountId,
|
|
6609
6753
|
mountType: "fuse",
|
|
6610
6754
|
bucket: s3fsSource,
|
|
6611
6755
|
mountPath,
|
|
6612
6756
|
endpoint: options.endpoint,
|
|
6613
6757
|
provider,
|
|
6614
6758
|
passwordFilePath,
|
|
6615
|
-
|
|
6759
|
+
...additionalHeaderFilePath ? { additionalHeaderFilePath } : {},
|
|
6760
|
+
mounted: false,
|
|
6761
|
+
...credentialProxyEnabled ? { credentialProxy: {
|
|
6762
|
+
endpoint: options.endpoint,
|
|
6763
|
+
bucket,
|
|
6764
|
+
...prefix !== void 0 ? { prefix } : {},
|
|
6765
|
+
credentials,
|
|
6766
|
+
readOnly: options.readOnly ?? false,
|
|
6767
|
+
provider,
|
|
6768
|
+
authStrategy: this.resolveCredentialProxyAuthStrategy(provider)
|
|
6769
|
+
} } : {}
|
|
6616
6770
|
};
|
|
6617
6771
|
this.activeMounts.set(mountPath, mountInfo);
|
|
6618
|
-
await this.createPasswordFile(passwordFilePath, bucket,
|
|
6772
|
+
await this.createPasswordFile(passwordFilePath, bucket, credentialProxyEnabled ? {
|
|
6773
|
+
accessKeyId: "x",
|
|
6774
|
+
secretAccessKey: "x"
|
|
6775
|
+
} : credentials);
|
|
6776
|
+
if (credentialProxyEnabled) {
|
|
6777
|
+
if (additionalHeaderFilePath) await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
|
|
6778
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams());
|
|
6779
|
+
}
|
|
6619
6780
|
dirExisted = (await this.execInternal(`test -d ${shellEscape(mountPath)}`)).exitCode === 0;
|
|
6620
6781
|
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
6621
|
-
|
|
6782
|
+
const effectiveOptions = credentialProxyEnabled ? {
|
|
6783
|
+
...options,
|
|
6784
|
+
endpoint: `http://${S3_CREDENTIAL_PROXY_HOST}/${mountId}`,
|
|
6785
|
+
s3fsOptions: [
|
|
6786
|
+
...provider === "r2" ? R2_DEFAULT_S3FS_OPTION_ENTRIES : [],
|
|
6787
|
+
...options.s3fsOptions ?? [],
|
|
6788
|
+
...additionalHeaderFilePath ? [`ahbe_conf=${additionalHeaderFilePath}`] : [],
|
|
6789
|
+
"use_path_request_style"
|
|
6790
|
+
]
|
|
6791
|
+
} : options;
|
|
6792
|
+
await this.executeS3FSMount(s3fsSource, mountPath, effectiveOptions, provider, passwordFilePath);
|
|
6622
6793
|
mountInfo.mounted = true;
|
|
6623
6794
|
mountOutcome = "success";
|
|
6624
6795
|
} catch (error) {
|
|
6625
6796
|
mountError = error instanceof Error ? error : new Error(String(error));
|
|
6626
|
-
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
|
|
6627
6797
|
try {
|
|
6628
6798
|
await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && fusermount -u ${shellEscape(mountPath)}`);
|
|
6629
6799
|
} catch {}
|
|
6800
|
+
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
|
|
6801
|
+
if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath);
|
|
6630
6802
|
if (!dirExisted) try {
|
|
6631
6803
|
await this.execInternal(`rmdir ${shellEscape(mountPath)} 2>/dev/null`);
|
|
6632
6804
|
} catch {}
|
|
6633
|
-
this.activeMounts.
|
|
6805
|
+
const failedMount = this.activeMounts.get(mountPath);
|
|
6806
|
+
if (failedMount?.mountType === "fuse" && failedMount.credentialProxy) try {
|
|
6807
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: failedMount.mountId }));
|
|
6808
|
+
this.activeMounts.delete(mountPath);
|
|
6809
|
+
evictSigV4ClientCacheEntry(failedMount.mountId);
|
|
6810
|
+
evictDirectoryMarkerCacheForMount(failedMount.mountId);
|
|
6811
|
+
} catch (cleanupError) {
|
|
6812
|
+
this.logger.warn("credential proxy cleanup failed", {
|
|
6813
|
+
mountPath,
|
|
6814
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
6815
|
+
});
|
|
6816
|
+
this.activeMounts.delete(mountPath);
|
|
6817
|
+
evictSigV4ClientCacheEntry(failedMount.mountId);
|
|
6818
|
+
evictDirectoryMarkerCacheForMount(failedMount.mountId);
|
|
6819
|
+
}
|
|
6820
|
+
else this.activeMounts.delete(mountPath);
|
|
6634
6821
|
throw error;
|
|
6635
6822
|
} finally {
|
|
6636
6823
|
logCanonicalEvent(this.logger, {
|
|
@@ -6652,6 +6839,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6652
6839
|
* @throws InvalidMountConfigError if mount path doesn't exist or isn't mounted
|
|
6653
6840
|
*/
|
|
6654
6841
|
async unmountBucket(mountPath) {
|
|
6842
|
+
return this.runMountOperation(async () => {
|
|
6843
|
+
await this.unmountBucketUnlocked(mountPath);
|
|
6844
|
+
});
|
|
6845
|
+
}
|
|
6846
|
+
async unmountBucketUnlocked(mountPath) {
|
|
6655
6847
|
const unmountStartTime = Date.now();
|
|
6656
6848
|
let unmountOutcome = "error";
|
|
6657
6849
|
let unmountError;
|
|
@@ -6662,30 +6854,75 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6662
6854
|
await mountInfo.syncManager.stop();
|
|
6663
6855
|
mountInfo.mounted = false;
|
|
6664
6856
|
this.activeMounts.delete(mountPath);
|
|
6665
|
-
} else
|
|
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());
|
|
6857
|
+
} else if (mountInfo.mountType === "fuse" && mountInfo.credentialProxy && !mountInfo.mounted) {
|
|
6674
6858
|
try {
|
|
6675
|
-
|
|
6676
|
-
|
|
6859
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: mountInfo.mountId }));
|
|
6860
|
+
} catch (cleanupError) {
|
|
6861
|
+
this.logger.warn("credential proxy outbound reconfiguration failed on unmount", {
|
|
6677
6862
|
mountPath,
|
|
6678
|
-
|
|
6679
|
-
stderr: cleanup.stderr
|
|
6680
|
-
});
|
|
6681
|
-
} catch (err) {
|
|
6682
|
-
this.logger.warn("mount directory removal failed", {
|
|
6683
|
-
mountPath,
|
|
6684
|
-
error: err instanceof Error ? err.message : String(err)
|
|
6863
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
6685
6864
|
});
|
|
6686
6865
|
}
|
|
6687
|
-
|
|
6688
|
-
|
|
6866
|
+
this.activeMounts.delete(mountPath);
|
|
6867
|
+
evictSigV4ClientCacheEntry(mountInfo.mountId);
|
|
6868
|
+
evictDirectoryMarkerCacheForMount(mountInfo.mountId);
|
|
6869
|
+
} else {
|
|
6870
|
+
let unmounted = false;
|
|
6871
|
+
try {
|
|
6872
|
+
const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
6873
|
+
if (result.exitCode !== 0) {
|
|
6874
|
+
const stderr = result.stderr || "unknown error";
|
|
6875
|
+
throw new BucketUnmountError(`fusermount -u failed (exit ${result.exitCode}): ${stderr}`);
|
|
6876
|
+
}
|
|
6877
|
+
unmounted = true;
|
|
6878
|
+
mountInfo.mounted = false;
|
|
6879
|
+
if (mountInfo.mountType === "r2-egress") {
|
|
6880
|
+
const remainingBuckets = {};
|
|
6881
|
+
for (const [, activeMount] of this.activeMounts) if (activeMount.mountType === "r2-egress" && activeMount.mountId !== mountInfo.mountId) remainingBuckets[activeMount.bucket] = {
|
|
6882
|
+
prefix: activeMount.prefix,
|
|
6883
|
+
readOnly: activeMount.readOnly
|
|
6884
|
+
};
|
|
6885
|
+
try {
|
|
6886
|
+
await this.configureR2EgressOutbound({ buckets: remainingBuckets });
|
|
6887
|
+
} catch (cleanupError) {
|
|
6888
|
+
this.logger.warn("r2 egress outbound reconfiguration failed on unmount", {
|
|
6889
|
+
mountPath,
|
|
6890
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
6891
|
+
});
|
|
6892
|
+
}
|
|
6893
|
+
this.activeMounts.delete(mountPath);
|
|
6894
|
+
} else if (mountInfo.mountType === "fuse" && mountInfo.credentialProxy) {
|
|
6895
|
+
try {
|
|
6896
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: mountInfo.mountId }));
|
|
6897
|
+
} catch (cleanupError) {
|
|
6898
|
+
this.logger.warn("credential proxy outbound reconfiguration failed on unmount", {
|
|
6899
|
+
mountPath,
|
|
6900
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
6901
|
+
});
|
|
6902
|
+
}
|
|
6903
|
+
this.activeMounts.delete(mountPath);
|
|
6904
|
+
evictSigV4ClientCacheEntry(mountInfo.mountId);
|
|
6905
|
+
evictDirectoryMarkerCacheForMount(mountInfo.mountId);
|
|
6906
|
+
} else this.activeMounts.delete(mountPath);
|
|
6907
|
+
try {
|
|
6908
|
+
const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
|
|
6909
|
+
if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
|
|
6910
|
+
mountPath,
|
|
6911
|
+
exitCode: cleanup.exitCode,
|
|
6912
|
+
stderr: cleanup.stderr
|
|
6913
|
+
});
|
|
6914
|
+
} catch (err) {
|
|
6915
|
+
this.logger.warn("mount directory removal failed", {
|
|
6916
|
+
mountPath,
|
|
6917
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6918
|
+
});
|
|
6919
|
+
}
|
|
6920
|
+
} finally {
|
|
6921
|
+
if (unmounted) {
|
|
6922
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
6923
|
+
if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6689
6926
|
}
|
|
6690
6927
|
unmountOutcome = "success";
|
|
6691
6928
|
} catch (error) {
|
|
@@ -6728,6 +6965,20 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6728
6965
|
return `/tmp/.passwd-s3fs-${crypto.randomUUID()}`;
|
|
6729
6966
|
}
|
|
6730
6967
|
/**
|
|
6968
|
+
* Generate unique ahbe_conf file path for s3fs additional header config
|
|
6969
|
+
*/
|
|
6970
|
+
generateS3FSAdditionalHeaderFilePath() {
|
|
6971
|
+
return `/tmp/.s3fs-ahbe-${crypto.randomUUID()}.conf`;
|
|
6972
|
+
}
|
|
6973
|
+
/**
|
|
6974
|
+
* Create s3fs ahbe_conf file that suppresses the Expect: 100-continue header.
|
|
6975
|
+
* Restricted to 0600 so s3fs will accept it (same requirement as passwd files).
|
|
6976
|
+
*/
|
|
6977
|
+
async createDisableExpectHeaderFile(headerFilePath) {
|
|
6978
|
+
await this.client.files.writeFile(headerFilePath, S3FS_DISABLE_EXPECT_HEADER_CONFIG, DISABLE_SESSION_TOKEN);
|
|
6979
|
+
await this.execInternal(`chmod 0600 ${shellEscape(headerFilePath)}`);
|
|
6980
|
+
}
|
|
6981
|
+
/**
|
|
6731
6982
|
* Create password file with s3fs credentials
|
|
6732
6983
|
* Format: bucket:accessKeyId:secretAccessKey
|
|
6733
6984
|
*/
|
|
@@ -6749,6 +7000,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6749
7000
|
});
|
|
6750
7001
|
}
|
|
6751
7002
|
}
|
|
7003
|
+
async deleteAdditionalHeaderFile(headerFilePath) {
|
|
7004
|
+
try {
|
|
7005
|
+
await this.execInternal(`rm -f ${shellEscape(headerFilePath)}`);
|
|
7006
|
+
} catch (error) {
|
|
7007
|
+
this.logger.warn("s3fs additional header file cleanup failed", {
|
|
7008
|
+
headerFilePath,
|
|
7009
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7010
|
+
});
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
6752
7013
|
/**
|
|
6753
7014
|
* Execute S3FS mount command
|
|
6754
7015
|
*/
|
|
@@ -6836,9 +7097,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6836
7097
|
await this.ctx.storage.delete(PORT_TOKENS_STORAGE_KEY);
|
|
6837
7098
|
await this.clearActivePreviewPorts();
|
|
6838
7099
|
await this.currentRuntime.clear();
|
|
6839
|
-
if (this.ctx.container?.running) try {
|
|
6840
|
-
await this.client.desktop.stop();
|
|
6841
|
-
} catch {}
|
|
6842
7100
|
for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
|
|
6843
7101
|
mountsProcessed++;
|
|
6844
7102
|
if (mountInfo.mountType === "local-sync") try {
|
|
@@ -6858,6 +7116,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6858
7116
|
this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
|
|
6859
7117
|
}
|
|
6860
7118
|
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
7119
|
+
if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
|
|
6861
7120
|
}
|
|
6862
7121
|
}
|
|
6863
7122
|
try {
|
|
@@ -6975,9 +7234,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6975
7234
|
}
|
|
6976
7235
|
this.client.disconnect();
|
|
6977
7236
|
let hadR2EgressMount = false;
|
|
7237
|
+
let hadCredentialProxyMount = false;
|
|
6978
7238
|
for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
|
|
6979
7239
|
else if (m.mountType === "r2-egress") hadR2EgressMount = true;
|
|
7240
|
+
else if (m.mountType === "fuse" && m.credentialProxy) {
|
|
7241
|
+
hadCredentialProxyMount = true;
|
|
7242
|
+
evictSigV4ClientCacheEntry(m.mountId);
|
|
7243
|
+
evictDirectoryMarkerCacheForMount(m.mountId);
|
|
7244
|
+
}
|
|
6980
7245
|
if (hadR2EgressMount) await this.configureR2EgressOutbound({ buckets: {} }).catch(() => {});
|
|
7246
|
+
if (hadCredentialProxyMount) await this.configureS3CredentialProxyOutbound({ mounts: {} }).catch(() => {});
|
|
6981
7247
|
this.activeMounts.clear();
|
|
6982
7248
|
await this.ctx.storage.delete("defaultSession");
|
|
6983
7249
|
}
|
|
@@ -8015,33 +8281,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8015
8281
|
return this.client.files.exists(path$1, session);
|
|
8016
8282
|
}
|
|
8017
8283
|
/**
|
|
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
8284
|
* Watch a directory for file system changes using native inotify.
|
|
8046
8285
|
*
|
|
8047
8286
|
* The returned promise resolves only after the watcher is established on the
|
|
@@ -9097,10 +9336,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9097
9336
|
* create-archive → read → upload (or mount → extract) flow
|
|
9098
9337
|
* is not interleaved with another backup operation on the same directory.
|
|
9099
9338
|
*/
|
|
9100
|
-
enqueueBackupOp(fn) {
|
|
9101
|
-
|
|
9339
|
+
async enqueueBackupOp(fn) {
|
|
9340
|
+
try {
|
|
9341
|
+
await this.backupInProgress;
|
|
9342
|
+
} catch {}
|
|
9343
|
+
const next = fn();
|
|
9102
9344
|
this.backupInProgress = next.catch(() => {});
|
|
9103
|
-
return next;
|
|
9345
|
+
return await next;
|
|
9104
9346
|
}
|
|
9105
9347
|
/**
|
|
9106
9348
|
* Create a backup of a directory and upload it to R2.
|
|
@@ -9124,9 +9366,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9124
9366
|
* under the `backups/` prefix after the desired retention period.
|
|
9125
9367
|
*/
|
|
9126
9368
|
async createBackup(options) {
|
|
9127
|
-
if (options.localBucket) return this.enqueueBackupOp(() => this.doCreateBackupLocal(options));
|
|
9369
|
+
if (options.localBucket) return await this.enqueueBackupOp(() => this.doCreateBackupLocal(options));
|
|
9128
9370
|
this.requireBackupBucket();
|
|
9129
|
-
return this.enqueueBackupOp(() => this.doCreateBackup(options));
|
|
9371
|
+
return await this.enqueueBackupOp(() => this.doCreateBackup(options));
|
|
9130
9372
|
}
|
|
9131
9373
|
async doCreateBackup(options) {
|
|
9132
9374
|
const bucket = this.requireBackupBucket();
|
|
@@ -9423,9 +9665,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9423
9665
|
* Concurrent backup/restore calls on the same sandbox are serialized.
|
|
9424
9666
|
*/
|
|
9425
9667
|
async restoreBackup(backup) {
|
|
9426
|
-
if (backup.localBucket) return this.enqueueBackupOp(() => this.doRestoreBackupLocal(backup));
|
|
9668
|
+
if (backup.localBucket) return await this.enqueueBackupOp(() => this.doRestoreBackupLocal(backup));
|
|
9427
9669
|
this.requireBackupBucket();
|
|
9428
|
-
return this.enqueueBackupOp(() => this.doRestoreBackup(backup));
|
|
9670
|
+
return await this.enqueueBackupOp(() => this.doRestoreBackup(backup));
|
|
9429
9671
|
}
|
|
9430
9672
|
async doRestoreBackup(backup) {
|
|
9431
9673
|
const restoreStartTime = Date.now();
|
|
@@ -9685,10 +9927,18 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9685
9927
|
const ctx = this.ctx;
|
|
9686
9928
|
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
|
|
9687
9929
|
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
9930
|
+
this.constructor.outboundHandlers = { r2EgressMount: r2EgressHandler };
|
|
9931
|
+
if (Object.keys(params.buckets).length > 0) await this.setOutboundByHost("r2.internal", "r2EgressMount", params);
|
|
9932
|
+
else await this.removeOutboundByHost("r2.internal");
|
|
9933
|
+
this.logger.debug("r2 egress: registering host interception", {
|
|
9934
|
+
host: "r2.internal",
|
|
9935
|
+
method: "r2EgressMount",
|
|
9936
|
+
targetClassName: CONTAINER_PROXY_CLASS_NAME
|
|
9937
|
+
});
|
|
9688
9938
|
const fetcher = ctx.exports.ContainerProxy({ props: {
|
|
9689
9939
|
enableInternet: this.enableInternet,
|
|
9690
9940
|
containerId: this.ctx.id.toString(),
|
|
9691
|
-
className:
|
|
9941
|
+
className: CONTAINER_PROXY_CLASS_NAME,
|
|
9692
9942
|
outboundByHostOverrides: { "r2.internal": {
|
|
9693
9943
|
method: "r2EgressMount",
|
|
9694
9944
|
params
|
|
@@ -9697,8 +9947,42 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9697
9947
|
if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
|
|
9698
9948
|
await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
|
|
9699
9949
|
}
|
|
9950
|
+
async configureS3CredentialProxyOutbound(params) {
|
|
9951
|
+
const ctx = this.ctx;
|
|
9952
|
+
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("Credential proxy bucket mounts require container outbound interception support");
|
|
9953
|
+
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("Credential proxy bucket mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
9954
|
+
const hosts = [S3_CREDENTIAL_PROXY_HOST, S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST];
|
|
9955
|
+
this.constructor.outboundHandlers = { s3CredentialProxyMount: s3CredentialProxyHandler };
|
|
9956
|
+
if (Object.keys(params.mounts).length > 0) for (const host of hosts) await this.setOutboundByHost(host, "s3CredentialProxyMount", params);
|
|
9957
|
+
else for (const host of hosts) await this.removeOutboundByHost(host);
|
|
9958
|
+
const hostOverrides = {};
|
|
9959
|
+
for (const host of hosts) hostOverrides[host] = {
|
|
9960
|
+
method: "s3CredentialProxyMount",
|
|
9961
|
+
params
|
|
9962
|
+
};
|
|
9963
|
+
this.logger.debug("s3 credential proxy: registering host interception", {
|
|
9964
|
+
hosts,
|
|
9965
|
+
method: "s3CredentialProxyMount",
|
|
9966
|
+
targetClassName: CONTAINER_PROXY_CLASS_NAME
|
|
9967
|
+
});
|
|
9968
|
+
const fetcher = ctx.exports.ContainerProxy({ props: {
|
|
9969
|
+
enableInternet: this.enableInternet,
|
|
9970
|
+
containerId: this.ctx.id.toString(),
|
|
9971
|
+
className: CONTAINER_PROXY_CLASS_NAME,
|
|
9972
|
+
outboundByHostOverrides: hostOverrides
|
|
9973
|
+
} });
|
|
9974
|
+
if (!isFetcher(fetcher)) throw new InvalidMountConfigError("Credential proxy bucket mounts require ContainerProxy to return a valid Fetcher");
|
|
9975
|
+
try {
|
|
9976
|
+
const selfTest = await fetcher.fetch(new Request(`http://${S3_CREDENTIAL_PROXY_HOST}${SELF_TEST_PATH}`));
|
|
9977
|
+
await selfTest.text();
|
|
9978
|
+
this.logger.debug("s3 credential proxy: fetcher self-test complete", { status: selfTest.status });
|
|
9979
|
+
} catch (error) {
|
|
9980
|
+
this.logger.warn("s3 credential proxy: fetcher self-test failed", { error: error instanceof Error ? error.message : String(error) });
|
|
9981
|
+
}
|
|
9982
|
+
for (const host of hosts) await ctx.container.interceptOutboundHttp(host, fetcher);
|
|
9983
|
+
}
|
|
9700
9984
|
};
|
|
9701
9985
|
|
|
9702
9986
|
//#endregion
|
|
9703
|
-
export {
|
|
9704
|
-
//# sourceMappingURL=sandbox-
|
|
9987
|
+
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 };
|
|
9988
|
+
//# sourceMappingURL=sandbox-DKG3H156.js.map
|