@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.
- package/Dockerfile +0 -55
- 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-D3N9M5EI.d.ts} +43 -377
- package/dist/sandbox-D3N9M5EI.d.ts.map +1 -0
- package/dist/{sandbox-DQxTkLyY.js → sandbox-Duj2gvUC.js} +724 -441
- package/dist/sandbox-Duj2gvUC.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:
|
|
@@ -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.
|
|
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
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
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
|
-
|
|
6589
|
+
validateProtectedS3fsOptions(options, mountLabel, extraProtected = []) {
|
|
6489
6590
|
if (!options) return;
|
|
6490
|
-
const protectedOptions = new Set([
|
|
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
|
|
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.
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
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:
|
|
6862
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
6685
6863
|
});
|
|
6686
6864
|
}
|
|
6687
|
-
|
|
6688
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
9704
|
-
//# sourceMappingURL=sandbox-
|
|
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
|