@cloudflare/sandbox 0.10.0 → 0.10.2
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 +32 -0
- package/README.md +46 -0
- package/dist/bridge/index.d.ts +0 -6
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +75 -23
- package/dist/bridge/index.js.map +1 -1
- package/dist/{errors-CBi-O-pF.js → errors-8Hvune8K.js} +2 -2
- package/dist/{errors-CBi-O-pF.js.map → errors-8Hvune8K.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-2bHZZmy5.js → sandbox-BcEq4aUF.js} +911 -102
- package/dist/sandbox-BcEq4aUF.js.map +1 -0
- package/dist/{sandbox-C-AzrX_L.d.ts → sandbox-KdzTTnWq.d.ts} +269 -67
- package/dist/sandbox-KdzTTnWq.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/sandbox-2bHZZmy5.js.map +0 -1
- package/dist/sandbox-C-AzrX_L.d.ts.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
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-
|
|
2
|
+
import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-8Hvune8K.js";
|
|
3
3
|
import { Container, getContainer, switchPort } from "@cloudflare/containers";
|
|
4
4
|
import { AwsClient } from "aws4fetch";
|
|
5
|
-
import { RpcSession } from "capnweb";
|
|
5
|
+
import { RpcSession, RpcTarget } from "capnweb";
|
|
6
6
|
import path from "node:path/posix";
|
|
7
|
+
import { RpcTarget as RpcTarget$1 } from "cloudflare:workers";
|
|
7
8
|
|
|
8
9
|
//#region src/errors/classes.ts
|
|
9
10
|
/**
|
|
@@ -1784,6 +1785,17 @@ var CommandClient = class extends BaseHttpClient {
|
|
|
1784
1785
|
//#endregion
|
|
1785
1786
|
//#region src/clients/desktop-client.ts
|
|
1786
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
|
+
/**
|
|
1787
1799
|
* Client for desktop environment lifecycle, input, and screen operations
|
|
1788
1800
|
*/
|
|
1789
1801
|
var DesktopClient = class extends BaseHttpClient {
|
|
@@ -1828,15 +1840,10 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1828
1840
|
...options?.showCursor !== void 0 && { showCursor: options.showCursor }
|
|
1829
1841
|
};
|
|
1830
1842
|
const response = await this.post("/api/desktop/screenshot", data);
|
|
1831
|
-
if (wantsBytes) {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
return {
|
|
1836
|
-
...response,
|
|
1837
|
-
data: bytes
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1843
|
+
if (wantsBytes) return {
|
|
1844
|
+
...response,
|
|
1845
|
+
data: base64ToBytes(response.data)
|
|
1846
|
+
};
|
|
1840
1847
|
return response;
|
|
1841
1848
|
}
|
|
1842
1849
|
async screenshotRegion(region, options) {
|
|
@@ -1849,15 +1856,10 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1849
1856
|
...options?.showCursor !== void 0 && { showCursor: options.showCursor }
|
|
1850
1857
|
};
|
|
1851
1858
|
const response = await this.post("/api/desktop/screenshot/region", data);
|
|
1852
|
-
if (wantsBytes) {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
return {
|
|
1857
|
-
...response,
|
|
1858
|
-
data: bytes
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1859
|
+
if (wantsBytes) return {
|
|
1860
|
+
...response,
|
|
1861
|
+
data: base64ToBytes(response.data)
|
|
1862
|
+
};
|
|
1861
1863
|
return response;
|
|
1862
1864
|
}
|
|
1863
1865
|
/**
|
|
@@ -2010,7 +2012,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
2010
2012
|
* Get health status for a specific desktop process.
|
|
2011
2013
|
*/
|
|
2012
2014
|
async getProcessStatus(name) {
|
|
2013
|
-
return
|
|
2015
|
+
return this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
|
|
2014
2016
|
}
|
|
2015
2017
|
};
|
|
2016
2018
|
|
|
@@ -2050,13 +2052,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2050
2052
|
};
|
|
2051
2053
|
return await this.post("/api/write", data);
|
|
2052
2054
|
}
|
|
2053
|
-
/**
|
|
2054
|
-
* Read content from a file
|
|
2055
|
-
* @param path - File path to read from
|
|
2056
|
-
* @param sessionId - The session ID for this operation
|
|
2057
|
-
* @param options - Optional settings (encoding)
|
|
2058
|
-
*/
|
|
2059
2055
|
async readFile(path$1, sessionId, options) {
|
|
2056
|
+
if (options?.encoding === "none") throw new Error("readFile with encoding: 'none' requires the rpc transport. Set SANDBOX_TRANSPORT=rpc.");
|
|
2060
2057
|
const data = {
|
|
2061
2058
|
path: path$1,
|
|
2062
2059
|
sessionId,
|
|
@@ -2143,6 +2140,13 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2143
2140
|
};
|
|
2144
2141
|
return await this.post("/api/exists", data);
|
|
2145
2142
|
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Write a file via a raw binary stream over the RPC transport.
|
|
2145
|
+
* Throws on HTTP and WebSocket transports — use writeFile() with a string instead.
|
|
2146
|
+
*/
|
|
2147
|
+
writeFileStream(_path, _content, _sessionId) {
|
|
2148
|
+
throw new Error("writeFileStream requires the rpc transport. Set SANDBOX_TRANSPORT=rpc.");
|
|
2149
|
+
}
|
|
2146
2150
|
};
|
|
2147
2151
|
|
|
2148
2152
|
//#endregion
|
|
@@ -2521,6 +2525,9 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2521
2525
|
return "unknown";
|
|
2522
2526
|
}
|
|
2523
2527
|
}
|
|
2528
|
+
listSessions() {
|
|
2529
|
+
throw new Error("listSessions requires the RPC transport. Set SANDBOX_TRANSPORT=rpc.");
|
|
2530
|
+
}
|
|
2524
2531
|
};
|
|
2525
2532
|
|
|
2526
2533
|
//#endregion
|
|
@@ -2644,6 +2651,13 @@ var SandboxClient = class {
|
|
|
2644
2651
|
utils;
|
|
2645
2652
|
desktop;
|
|
2646
2653
|
watch;
|
|
2654
|
+
/**
|
|
2655
|
+
* Tunnels are RPC-only — the route-based transport does not implement them.
|
|
2656
|
+
* This getter exists so the `PublicKeys<SandboxClient> satisfies
|
|
2657
|
+
* PublicKeys<SandboxAPI>` compile-time check holds. Calling any method on
|
|
2658
|
+
* the returned proxy throws a clear `RPC transport required` error.
|
|
2659
|
+
*/
|
|
2660
|
+
tunnels = createTunnelsNotImplemented();
|
|
2647
2661
|
transport = null;
|
|
2648
2662
|
constructor(options) {
|
|
2649
2663
|
if (options.transportMode === "websocket" && options.wsUrl) this.transport = createTransport({
|
|
@@ -2706,16 +2720,6 @@ var SandboxClient = class {
|
|
|
2706
2720
|
return this.transport?.isConnected() ?? false;
|
|
2707
2721
|
}
|
|
2708
2722
|
/**
|
|
2709
|
-
* Stream a file directly to the container over a binary RPC channel.
|
|
2710
|
-
*
|
|
2711
|
-
* Requires the container-control path (`transport: 'rpc'`). Calling this
|
|
2712
|
-
* method with the HTTP or WebSocket route transports throws an error because
|
|
2713
|
-
* those transports do not support binary streaming.
|
|
2714
|
-
*/
|
|
2715
|
-
writeFileStream(_path, _content, _sessionId) {
|
|
2716
|
-
throw new Error("writeFileStream requires the RPC transport. Enable it with transport: \"rpc\" in sandbox options.");
|
|
2717
|
-
}
|
|
2718
|
-
/**
|
|
2719
2723
|
* Connect WebSocket transport (no-op in HTTP mode)
|
|
2720
2724
|
* Called automatically on first request, but can be called explicitly
|
|
2721
2725
|
* to establish connection upfront.
|
|
@@ -2731,6 +2735,14 @@ var SandboxClient = class {
|
|
|
2731
2735
|
if (this.transport) this.transport.disconnect();
|
|
2732
2736
|
}
|
|
2733
2737
|
};
|
|
2738
|
+
function createTunnelsNotImplemented() {
|
|
2739
|
+
const message = "sandbox.tunnels.* requires the RPC transport. Enable it with transport: \"rpc\" in sandbox options.";
|
|
2740
|
+
return new Proxy({}, { get() {
|
|
2741
|
+
return () => {
|
|
2742
|
+
throw new Error(message);
|
|
2743
|
+
};
|
|
2744
|
+
} });
|
|
2745
|
+
}
|
|
2734
2746
|
|
|
2735
2747
|
//#endregion
|
|
2736
2748
|
//#region ../shared/src/backup.ts
|
|
@@ -2777,13 +2789,15 @@ var ContainerControlConnection = class {
|
|
|
2777
2789
|
port;
|
|
2778
2790
|
logger;
|
|
2779
2791
|
retryTimeoutMs;
|
|
2792
|
+
onClose;
|
|
2780
2793
|
constructor(options) {
|
|
2781
2794
|
this.containerStub = options.stub;
|
|
2782
2795
|
this.port = options.port ?? 3e3;
|
|
2783
2796
|
this.logger = options.logger ?? createNoOpLogger();
|
|
2784
2797
|
this.retryTimeoutMs = options.retryTimeoutMs ?? DEFAULT_RETRY_TIMEOUT_MS;
|
|
2798
|
+
this.onClose = options.onClose;
|
|
2785
2799
|
this.transport = new DeferredTransport();
|
|
2786
|
-
this.session = new RpcSession(this.transport);
|
|
2800
|
+
this.session = new RpcSession(this.transport, options.localMain);
|
|
2787
2801
|
this.stub = this.session.getRemoteMain();
|
|
2788
2802
|
}
|
|
2789
2803
|
/**
|
|
@@ -2823,6 +2837,8 @@ var ContainerControlConnection = class {
|
|
|
2823
2837
|
this.stub[Symbol.dispose]?.();
|
|
2824
2838
|
} catch {}
|
|
2825
2839
|
if (this.ws) {
|
|
2840
|
+
this.ws.removeEventListener("close", this.onWebSocketClose);
|
|
2841
|
+
this.ws.removeEventListener("error", this.onWebSocketError);
|
|
2826
2842
|
try {
|
|
2827
2843
|
this.ws.close();
|
|
2828
2844
|
} catch {}
|
|
@@ -2839,6 +2855,42 @@ var ContainerControlConnection = class {
|
|
|
2839
2855
|
setRetryTimeoutMs(ms) {
|
|
2840
2856
|
this.retryTimeoutMs = ms;
|
|
2841
2857
|
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Run the owner-provided `onClose` callback exactly once per call,
|
|
2860
|
+
* swallowing any errors so a buggy listener can't keep the connection
|
|
2861
|
+
* object in a half-torn-down state.
|
|
2862
|
+
*/
|
|
2863
|
+
fireOnClose() {
|
|
2864
|
+
if (!this.onClose) return;
|
|
2865
|
+
try {
|
|
2866
|
+
this.onClose();
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
this.logger.warn("ContainerControlConnection onClose handler threw", { error: err instanceof Error ? err.message : String(err) });
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* WebSocket `close` listener. Defined as a bound arrow field so the
|
|
2873
|
+
* same reference can be passed to both `addEventListener` and
|
|
2874
|
+
* `removeEventListener` — a fresh anonymous lambda would silently
|
|
2875
|
+
* fail to unbind.
|
|
2876
|
+
*/
|
|
2877
|
+
onWebSocketClose = () => {
|
|
2878
|
+
const wasConnected = this.connected;
|
|
2879
|
+
this.connected = false;
|
|
2880
|
+
this.ws = null;
|
|
2881
|
+
this.logger.debug("ContainerControlConnection WebSocket closed");
|
|
2882
|
+
if (wasConnected) this.fireOnClose();
|
|
2883
|
+
};
|
|
2884
|
+
/**
|
|
2885
|
+
* WebSocket `error` listener. Same field-form rationale as
|
|
2886
|
+
* {@link onWebSocketClose}.
|
|
2887
|
+
*/
|
|
2888
|
+
onWebSocketError = () => {
|
|
2889
|
+
const wasConnected = this.connected;
|
|
2890
|
+
this.connected = false;
|
|
2891
|
+
this.ws = null;
|
|
2892
|
+
if (wasConnected) this.fireOnClose();
|
|
2893
|
+
};
|
|
2842
2894
|
async doConnect() {
|
|
2843
2895
|
try {
|
|
2844
2896
|
const response = await this.fetchUpgradeWithRetry();
|
|
@@ -2846,15 +2898,8 @@ var ContainerControlConnection = class {
|
|
|
2846
2898
|
const ws = response.webSocket;
|
|
2847
2899
|
if (!ws) throw new Error("No WebSocket in upgrade response");
|
|
2848
2900
|
ws.accept();
|
|
2849
|
-
ws.addEventListener("close",
|
|
2850
|
-
|
|
2851
|
-
this.ws = null;
|
|
2852
|
-
this.logger.debug("ContainerControlConnection WebSocket closed");
|
|
2853
|
-
});
|
|
2854
|
-
ws.addEventListener("error", () => {
|
|
2855
|
-
this.connected = false;
|
|
2856
|
-
this.ws = null;
|
|
2857
|
-
});
|
|
2901
|
+
ws.addEventListener("close", this.onWebSocketClose);
|
|
2902
|
+
ws.addEventListener("error", this.onWebSocketError);
|
|
2858
2903
|
this.ws = ws;
|
|
2859
2904
|
this.transport.activate(ws);
|
|
2860
2905
|
this.connected = true;
|
|
@@ -3134,19 +3179,16 @@ var ContainerControlClient = class {
|
|
|
3134
3179
|
busyPollTimer = null;
|
|
3135
3180
|
/** Tracks whether we currently believe the session is busy. */
|
|
3136
3181
|
busy = false;
|
|
3137
|
-
/**
|
|
3138
|
-
* Set the first time the poller observes `conn.isConnected() === true`,
|
|
3139
|
-
* cleared in `destroyConnection()`. Lets us distinguish "the WebSocket
|
|
3140
|
-
* upgrade is still in progress" (don't tear down) from "we were
|
|
3141
|
-
* connected and the peer went away" (do tear down).
|
|
3142
|
-
*/
|
|
3143
|
-
wasEverConnected = false;
|
|
3144
3182
|
constructor(options) {
|
|
3145
3183
|
this.connOptions = {
|
|
3146
3184
|
stub: options.stub,
|
|
3147
3185
|
port: options.port,
|
|
3186
|
+
localMain: options.localMain,
|
|
3148
3187
|
logger: options.logger,
|
|
3149
|
-
retryTimeoutMs: options.retryTimeoutMs
|
|
3188
|
+
retryTimeoutMs: options.retryTimeoutMs,
|
|
3189
|
+
onClose: () => {
|
|
3190
|
+
if (this.conn) this.destroyConnection();
|
|
3191
|
+
}
|
|
3150
3192
|
};
|
|
3151
3193
|
this.idleDisconnectMs = options.idleDisconnectMs ?? DEFAULT_IDLE_DISCONNECT_MS;
|
|
3152
3194
|
this.busyPollIntervalMs = options.busyPollIntervalMs ?? BUSY_POLL_INTERVAL_MS;
|
|
@@ -3188,11 +3230,7 @@ var ContainerControlClient = class {
|
|
|
3188
3230
|
pollBusyState = () => {
|
|
3189
3231
|
const conn = this.conn;
|
|
3190
3232
|
if (!conn) return;
|
|
3191
|
-
if (!conn.isConnected())
|
|
3192
|
-
if (this.wasEverConnected) this.destroyConnection();
|
|
3193
|
-
return;
|
|
3194
|
-
}
|
|
3195
|
-
this.wasEverConnected = true;
|
|
3233
|
+
if (!conn.isConnected()) return;
|
|
3196
3234
|
const { imports, exports } = conn.getStats();
|
|
3197
3235
|
if (imports > IDLE_IMPORT_THRESHOLD || exports > IDLE_EXPORT_THRESHOLD) {
|
|
3198
3236
|
if (!this.busy) {
|
|
@@ -3225,7 +3263,7 @@ var ContainerControlClient = class {
|
|
|
3225
3263
|
if (!conn || !conn.isConnected()) return;
|
|
3226
3264
|
const { imports, exports } = conn.getStats();
|
|
3227
3265
|
if (imports <= IDLE_IMPORT_THRESHOLD && exports <= IDLE_EXPORT_THRESHOLD) {
|
|
3228
|
-
this.logger.debug("Disconnecting idle
|
|
3266
|
+
this.logger.debug("Disconnecting idle RPC connection");
|
|
3229
3267
|
this.destroyConnection();
|
|
3230
3268
|
}
|
|
3231
3269
|
}, this.idleDisconnectMs);
|
|
@@ -3247,7 +3285,6 @@ var ContainerControlClient = class {
|
|
|
3247
3285
|
this.conn.disconnect();
|
|
3248
3286
|
this.conn = null;
|
|
3249
3287
|
}
|
|
3250
|
-
this.wasEverConnected = false;
|
|
3251
3288
|
}
|
|
3252
3289
|
get commands() {
|
|
3253
3290
|
return wrapStub(this.getConnection().rpc().commands, this.renewActivity);
|
|
@@ -3271,11 +3308,37 @@ var ContainerControlClient = class {
|
|
|
3271
3308
|
return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
|
|
3272
3309
|
}
|
|
3273
3310
|
get desktop() {
|
|
3274
|
-
|
|
3311
|
+
const stub = wrapStub(this.getConnection().rpc().desktop, this.renewActivity);
|
|
3312
|
+
const wire = stub;
|
|
3313
|
+
return new Proxy(stub, { get(target, prop, receiver) {
|
|
3314
|
+
if (prop === "screenshot") return async (options) => {
|
|
3315
|
+
const { format, ...rest } = options ?? {};
|
|
3316
|
+
const result = await wire.screenshot(rest);
|
|
3317
|
+
return format === "bytes" ? {
|
|
3318
|
+
...result,
|
|
3319
|
+
data: base64ToBytes(result.data)
|
|
3320
|
+
} : result;
|
|
3321
|
+
};
|
|
3322
|
+
if (prop === "screenshotRegion") return async (region, options) => {
|
|
3323
|
+
const { format, ...rest } = options ?? {};
|
|
3324
|
+
const result = await wire.screenshotRegion({
|
|
3325
|
+
region,
|
|
3326
|
+
...rest
|
|
3327
|
+
});
|
|
3328
|
+
return format === "bytes" ? {
|
|
3329
|
+
...result,
|
|
3330
|
+
data: base64ToBytes(result.data)
|
|
3331
|
+
} : result;
|
|
3332
|
+
};
|
|
3333
|
+
return Reflect.get(target, prop, receiver);
|
|
3334
|
+
} });
|
|
3275
3335
|
}
|
|
3276
3336
|
get watch() {
|
|
3277
3337
|
return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
|
|
3278
3338
|
}
|
|
3339
|
+
get tunnels() {
|
|
3340
|
+
return wrapStub(this.getConnection().rpc().tunnels, this.renewActivity);
|
|
3341
|
+
}
|
|
3279
3342
|
get interpreter() {
|
|
3280
3343
|
return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
|
|
3281
3344
|
}
|
|
@@ -3300,9 +3363,6 @@ var ContainerControlClient = class {
|
|
|
3300
3363
|
disconnect() {
|
|
3301
3364
|
this.destroyConnection();
|
|
3302
3365
|
}
|
|
3303
|
-
async writeFileStream(path$1, stream, sessionId) {
|
|
3304
|
-
return this.files.writeFileStream(path$1, stream, sessionId);
|
|
3305
|
-
}
|
|
3306
3366
|
};
|
|
3307
3367
|
|
|
3308
3368
|
//#endregion
|
|
@@ -3819,6 +3879,13 @@ function resolveS3fsOptions(provider, userOptions) {
|
|
|
3819
3879
|
|
|
3820
3880
|
//#endregion
|
|
3821
3881
|
//#region src/storage-mount/validation.ts
|
|
3882
|
+
/**
|
|
3883
|
+
* Type guard for R2Bucket binding.
|
|
3884
|
+
* Checks for the minimal R2Bucket interface methods we use.
|
|
3885
|
+
*/
|
|
3886
|
+
function isR2Bucket(value) {
|
|
3887
|
+
return typeof value === "object" && value !== null && "put" in value && typeof value.put === "function" && "get" in value && typeof value.get === "function" && "head" in value && typeof value.head === "function" && "delete" in value && typeof value.delete === "function" && "list" in value && typeof value.list === "function";
|
|
3888
|
+
}
|
|
3822
3889
|
function validatePrefix(prefix) {
|
|
3823
3890
|
if (!prefix.startsWith("/")) throw new InvalidMountConfigError(`Prefix must start with '/': "${prefix}"`);
|
|
3824
3891
|
}
|
|
@@ -3829,6 +3896,13 @@ function validateBucketName(bucket, mountPath) {
|
|
|
3829
3896
|
}
|
|
3830
3897
|
if (!/^[a-z0-9]([a-z0-9.-]{0,61}[a-z0-9])?$/.test(bucket)) throw new InvalidMountConfigError(`Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, dots, or hyphens, and cannot start/end with dots or hyphens.`);
|
|
3831
3898
|
}
|
|
3899
|
+
function validateBucketBindingName(bucketBinding, mountPath) {
|
|
3900
|
+
if (bucketBinding.includes(":")) {
|
|
3901
|
+
const [bucketName, prefixPart] = bucketBinding.split(":");
|
|
3902
|
+
throw new InvalidMountConfigError(`Bucket name cannot contain ':'. To mount a prefix, use the 'prefix' option:\n mountBucket('${bucketName}', '${mountPath}', { ...options, prefix: '${prefixPart}' })`);
|
|
3903
|
+
}
|
|
3904
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(bucketBinding)) throw new InvalidMountConfigError(`Invalid R2 binding name: "${bucketBinding}". Binding names must start with a letter or underscore and contain only letters, numbers, or underscores.`);
|
|
3905
|
+
}
|
|
3832
3906
|
/**
|
|
3833
3907
|
* Builds the s3fs source string from bucket name and optional prefix.
|
|
3834
3908
|
* Format: "bucket" or "bucket:/prefix/" for subdirectory mounts.
|
|
@@ -4153,7 +4227,7 @@ async function proxyTerminal(stub, sessionId, request, options) {
|
|
|
4153
4227
|
|
|
4154
4228
|
//#endregion
|
|
4155
4229
|
//#region src/request-handler.ts
|
|
4156
|
-
async function proxyToSandbox(request, env) {
|
|
4230
|
+
async function proxyToSandbox(request, env$1) {
|
|
4157
4231
|
const logger = createLogger({
|
|
4158
4232
|
component: "sandbox-do",
|
|
4159
4233
|
traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
|
|
@@ -4164,7 +4238,7 @@ async function proxyToSandbox(request, env) {
|
|
|
4164
4238
|
const routeInfo = extractSandboxRoute(url);
|
|
4165
4239
|
if (!routeInfo) return null;
|
|
4166
4240
|
const { sandboxId, port, path: path$1, token } = routeInfo;
|
|
4167
|
-
const sandbox = getSandbox(env.Sandbox, sandboxId, { normalizeId: true });
|
|
4241
|
+
const sandbox = getSandbox(env$1.Sandbox, sandboxId, { normalizeId: true });
|
|
4168
4242
|
if (port !== 3e3) {
|
|
4169
4243
|
if (!await sandbox.validatePortToken(port, token)) {
|
|
4170
4244
|
logger.warn("Invalid token access blocked", {
|
|
@@ -4250,6 +4324,513 @@ function isLocalhostPattern(hostname) {
|
|
|
4250
4324
|
return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
|
|
4251
4325
|
}
|
|
4252
4326
|
|
|
4327
|
+
//#endregion
|
|
4328
|
+
//#region src/storage-mount/r2-egress-handler.ts
|
|
4329
|
+
const XML_NS = "xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"";
|
|
4330
|
+
const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
4331
|
+
function escapeXML(s) {
|
|
4332
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4333
|
+
}
|
|
4334
|
+
function xmlResponse(body, status = 200) {
|
|
4335
|
+
return new Response(XML_DECL + body, {
|
|
4336
|
+
status,
|
|
4337
|
+
headers: { "Content-Type": "application/xml" }
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
function normalizeObjectKey(value) {
|
|
4341
|
+
return value.replace(/^\/+/, "");
|
|
4342
|
+
}
|
|
4343
|
+
function trimTrailingSlashes(s) {
|
|
4344
|
+
let end = s.length;
|
|
4345
|
+
while (end > 0 && s[end - 1] === "/") end--;
|
|
4346
|
+
return s.slice(0, end);
|
|
4347
|
+
}
|
|
4348
|
+
function parsePath(pathname) {
|
|
4349
|
+
const stripped = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
4350
|
+
if (!stripped) return null;
|
|
4351
|
+
const slash = stripped.indexOf("/");
|
|
4352
|
+
if (slash === -1) return {
|
|
4353
|
+
bucket: stripped,
|
|
4354
|
+
key: ""
|
|
4355
|
+
};
|
|
4356
|
+
return {
|
|
4357
|
+
bucket: stripped.slice(0, slash),
|
|
4358
|
+
key: normalizeObjectKey(stripped.slice(slash + 1))
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
function resolveR2Bucket(env$1, name) {
|
|
4362
|
+
if (typeof env$1 !== "object" || env$1 === null) return null;
|
|
4363
|
+
const val = env$1[name];
|
|
4364
|
+
return isR2Bucket(val) ? val : null;
|
|
4365
|
+
}
|
|
4366
|
+
function parseRange(header) {
|
|
4367
|
+
if (!header) return void 0;
|
|
4368
|
+
const m = header.match(/^bytes=(\d*)-(\d*)$/);
|
|
4369
|
+
if (!m) return void 0;
|
|
4370
|
+
const start = m[1] ? parseInt(m[1], 10) : void 0;
|
|
4371
|
+
const end = m[2] ? parseInt(m[2], 10) : void 0;
|
|
4372
|
+
if (start === void 0 && end !== void 0) return { suffix: end };
|
|
4373
|
+
if (start !== void 0 && end !== void 0) return {
|
|
4374
|
+
offset: start,
|
|
4375
|
+
length: end - start + 1
|
|
4376
|
+
};
|
|
4377
|
+
if (start !== void 0) return { offset: start };
|
|
4378
|
+
}
|
|
4379
|
+
function buildListObjectsV2Xml(bucketName, prefix, delimiter, maxKeys, result) {
|
|
4380
|
+
const contents = result.objects.map((obj) => `<Contents><Key>${escapeXML(obj.key)}</Key><LastModified>${obj.uploaded.toISOString()}</LastModified><ETag>${escapeXML(obj.httpEtag)}</ETag><Size>${obj.size}</Size><StorageClass>STANDARD</StorageClass></Contents>`).join("");
|
|
4381
|
+
const commonPrefixes = result.delimitedPrefixes.map((p) => `<CommonPrefixes><Prefix>${escapeXML(p)}</Prefix></CommonPrefixes>`).join("");
|
|
4382
|
+
const nextToken = result.truncated && result.cursor ? `<NextContinuationToken>${escapeXML(result.cursor)}</NextContinuationToken>` : "";
|
|
4383
|
+
const keyCount = result.objects.length + result.delimitedPrefixes.length;
|
|
4384
|
+
return `<ListBucketResult ${XML_NS}><Name>${escapeXML(bucketName)}</Name><Prefix>${escapeXML(prefix)}</Prefix><KeyCount>${keyCount}</KeyCount><MaxKeys>${maxKeys}</MaxKeys>` + (delimiter ? `<Delimiter>${escapeXML(delimiter)}</Delimiter>` : "") + `<IsTruncated>${result.truncated}</IsTruncated>` + nextToken + contents + commonPrefixes + `</ListBucketResult>`;
|
|
4385
|
+
}
|
|
4386
|
+
function buildLocationXml() {
|
|
4387
|
+
return `<LocationConstraint ${XML_NS}/>`;
|
|
4388
|
+
}
|
|
4389
|
+
function buildInitiateMultipartUploadXml(bucketName, key, uploadId) {
|
|
4390
|
+
return `<InitiateMultipartUploadResult ${XML_NS}><Bucket>${escapeXML(bucketName)}</Bucket><Key>${escapeXML(key)}</Key><UploadId>${escapeXML(uploadId)}</UploadId></InitiateMultipartUploadResult>`;
|
|
4391
|
+
}
|
|
4392
|
+
function buildCompleteMultipartUploadXml(bucketName, key, etag) {
|
|
4393
|
+
return `<CompleteMultipartUploadResult ${XML_NS}><Location>http://r2.internal/${escapeXML(bucketName)}/${escapeXML(key)}</Location><Bucket>${escapeXML(bucketName)}</Bucket><Key>${escapeXML(key)}</Key><ETag>${escapeXML(etag)}</ETag></CompleteMultipartUploadResult>`;
|
|
4394
|
+
}
|
|
4395
|
+
function buildCopyObjectXml(etag, uploaded) {
|
|
4396
|
+
return `<CopyObjectResult ${XML_NS}><LastModified>${uploaded.toISOString()}</LastModified><ETag>${escapeXML(etag)}</ETag></CopyObjectResult>`;
|
|
4397
|
+
}
|
|
4398
|
+
function extractXmlTagContent(segment, tagName) {
|
|
4399
|
+
const openTag = `<${tagName}>`;
|
|
4400
|
+
const closeTag = `</${tagName}>`;
|
|
4401
|
+
const start = segment.indexOf(openTag);
|
|
4402
|
+
if (start === -1) return null;
|
|
4403
|
+
const contentStart = start + openTag.length;
|
|
4404
|
+
const end = segment.indexOf(closeTag, contentStart);
|
|
4405
|
+
if (end === -1) return null;
|
|
4406
|
+
return segment.slice(contentStart, end);
|
|
4407
|
+
}
|
|
4408
|
+
function parseCompleteMultipartUploadBody(body) {
|
|
4409
|
+
const parts = [];
|
|
4410
|
+
let pos = 0;
|
|
4411
|
+
while (pos < body.length) {
|
|
4412
|
+
const start = body.indexOf("<Part>", pos);
|
|
4413
|
+
if (start === -1) break;
|
|
4414
|
+
const end = body.indexOf("</Part>", start + 6);
|
|
4415
|
+
if (end === -1) break;
|
|
4416
|
+
const segment = body.slice(start, end + 7);
|
|
4417
|
+
pos = end + 7;
|
|
4418
|
+
const partNumberText = extractXmlTagContent(segment, "PartNumber");
|
|
4419
|
+
const etagText = extractXmlTagContent(segment, "ETag");
|
|
4420
|
+
const partNumber = partNumberText ? parseInt(partNumberText, 10) : NaN;
|
|
4421
|
+
if (Number.isFinite(partNumber) && etagText) parts.push({
|
|
4422
|
+
partNumber,
|
|
4423
|
+
etag: etagText.replace(/^"|"$/g, "")
|
|
4424
|
+
});
|
|
4425
|
+
}
|
|
4426
|
+
return parts;
|
|
4427
|
+
}
|
|
4428
|
+
function buildResponseHeaders(obj) {
|
|
4429
|
+
const headers = new Headers();
|
|
4430
|
+
headers.set("ETag", obj.httpEtag);
|
|
4431
|
+
headers.set("Content-Length", String(obj.size));
|
|
4432
|
+
headers.set("Last-Modified", obj.uploaded.toUTCString());
|
|
4433
|
+
headers.set("Accept-Ranges", "bytes");
|
|
4434
|
+
if (obj.httpMetadata?.contentType) headers.set("Content-Type", obj.httpMetadata.contentType);
|
|
4435
|
+
if (obj.httpMetadata?.contentDisposition) headers.set("Content-Disposition", obj.httpMetadata.contentDisposition);
|
|
4436
|
+
if (obj.httpMetadata?.contentEncoding) headers.set("Content-Encoding", obj.httpMetadata.contentEncoding);
|
|
4437
|
+
if (obj.httpMetadata?.contentLanguage) headers.set("Content-Language", obj.httpMetadata.contentLanguage);
|
|
4438
|
+
if (obj.httpMetadata?.cacheControl) headers.set("Cache-Control", obj.httpMetadata.cacheControl);
|
|
4439
|
+
return headers;
|
|
4440
|
+
}
|
|
4441
|
+
function buildContentRange(range, totalSize) {
|
|
4442
|
+
if ("suffix" in range) return `bytes ${Math.max(0, totalSize - range.suffix)}-${totalSize - 1}/${totalSize}`;
|
|
4443
|
+
const start = range.offset ?? 0;
|
|
4444
|
+
return `bytes ${start}-${range.length !== void 0 ? start + range.length - 1 : totalSize - 1}/${totalSize}`;
|
|
4445
|
+
}
|
|
4446
|
+
function getRangeContentLength(range, totalSize) {
|
|
4447
|
+
if ("suffix" in range) return Math.min(range.suffix, totalSize);
|
|
4448
|
+
const start = range.offset ?? 0;
|
|
4449
|
+
if (start >= totalSize) return 0;
|
|
4450
|
+
const requestedLength = range.length !== void 0 ? range.length : totalSize - start;
|
|
4451
|
+
return Math.min(requestedLength, totalSize - start);
|
|
4452
|
+
}
|
|
4453
|
+
function extractHttpMetadata(request) {
|
|
4454
|
+
const meta = {};
|
|
4455
|
+
const ct = request.headers.get("Content-Type");
|
|
4456
|
+
if (ct) meta.contentType = ct;
|
|
4457
|
+
const cd = request.headers.get("Content-Disposition");
|
|
4458
|
+
if (cd) meta.contentDisposition = cd;
|
|
4459
|
+
const ce = request.headers.get("Content-Encoding");
|
|
4460
|
+
if (ce) meta.contentEncoding = ce;
|
|
4461
|
+
const cl = request.headers.get("Content-Language");
|
|
4462
|
+
if (cl) meta.contentLanguage = cl;
|
|
4463
|
+
const cc = request.headers.get("Cache-Control");
|
|
4464
|
+
if (cc) meta.cacheControl = cc;
|
|
4465
|
+
return meta;
|
|
4466
|
+
}
|
|
4467
|
+
function parseCopySource(header) {
|
|
4468
|
+
const sourcePath = header.split("?")[0] ?? "";
|
|
4469
|
+
if (!sourcePath) return null;
|
|
4470
|
+
const decoded = decodeURIComponent(sourcePath);
|
|
4471
|
+
const parsed = parsePath(decoded.startsWith("/") ? decoded : `/${decoded}`);
|
|
4472
|
+
return parsed ? {
|
|
4473
|
+
bucket: parsed.bucket,
|
|
4474
|
+
key: normalizeObjectKey(parsed.key)
|
|
4475
|
+
} : null;
|
|
4476
|
+
}
|
|
4477
|
+
function normalizeStorageClass(storageClass) {
|
|
4478
|
+
if (storageClass === "Standard" || storageClass === "InfrequentAccess") return storageClass;
|
|
4479
|
+
}
|
|
4480
|
+
async function putRequestBody(r2, key, request, options) {
|
|
4481
|
+
const contentLength = request.headers.get("Content-Length");
|
|
4482
|
+
const length = contentLength ? Number.parseInt(contentLength, 10) : NaN;
|
|
4483
|
+
if (!Number.isFinite(length) || length < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
|
|
4484
|
+
if (length === 0) return r2.put(key, new Uint8Array(0), options);
|
|
4485
|
+
if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
|
|
4486
|
+
const { readable, writable } = new FixedLengthStream(length);
|
|
4487
|
+
const pipe = request.body.pipeTo(writable);
|
|
4488
|
+
const result = await r2.put(key, readable, options);
|
|
4489
|
+
await pipe;
|
|
4490
|
+
return result;
|
|
4491
|
+
}
|
|
4492
|
+
async function handleListObjects(r2, bucketName, url, mountPrefix) {
|
|
4493
|
+
const queryPrefix = normalizeObjectKey(url.searchParams.get("prefix") ?? "");
|
|
4494
|
+
const delimiter = url.searchParams.get("delimiter") ?? "";
|
|
4495
|
+
const maxKeys = Math.min(parseInt(url.searchParams.get("max-keys") ?? "1000", 10) || 1e3, 1e3);
|
|
4496
|
+
const continuationToken = url.searchParams.get("continuation-token") ?? void 0;
|
|
4497
|
+
const listOpts = {
|
|
4498
|
+
prefix: (mountPrefix ? `${mountPrefix}/${queryPrefix}` : queryPrefix) || void 0,
|
|
4499
|
+
delimiter: delimiter || void 0,
|
|
4500
|
+
limit: maxKeys,
|
|
4501
|
+
cursor: continuationToken
|
|
4502
|
+
};
|
|
4503
|
+
const result = await r2.list(listOpts);
|
|
4504
|
+
const stripKey = mountPrefix ? (k) => k.startsWith(`${mountPrefix}/`) ? k.slice(mountPrefix.length + 1) : k : (k) => k;
|
|
4505
|
+
return xmlResponse(buildListObjectsV2Xml(bucketName, queryPrefix, delimiter, maxKeys, {
|
|
4506
|
+
objects: result.objects.map((obj) => ({
|
|
4507
|
+
key: stripKey(obj.key),
|
|
4508
|
+
uploaded: obj.uploaded,
|
|
4509
|
+
httpEtag: obj.httpEtag,
|
|
4510
|
+
size: obj.size
|
|
4511
|
+
})),
|
|
4512
|
+
delimitedPrefixes: result.delimitedPrefixes.map(stripKey),
|
|
4513
|
+
truncated: result.truncated,
|
|
4514
|
+
cursor: result.truncated ? result.cursor : void 0
|
|
4515
|
+
}));
|
|
4516
|
+
}
|
|
4517
|
+
async function handleHeadObject(r2, key) {
|
|
4518
|
+
const obj = await r2.head(key);
|
|
4519
|
+
if (!obj) return new Response(null, { status: 404 });
|
|
4520
|
+
return new Response(null, {
|
|
4521
|
+
status: 200,
|
|
4522
|
+
headers: buildResponseHeaders(obj)
|
|
4523
|
+
});
|
|
4524
|
+
}
|
|
4525
|
+
async function handleGetObject(r2, key, request) {
|
|
4526
|
+
const range = parseRange(request.headers.get("Range"));
|
|
4527
|
+
if (!range) {
|
|
4528
|
+
const obj = await r2.get(key);
|
|
4529
|
+
if (!obj) return new Response(null, { status: 404 });
|
|
4530
|
+
return new Response(obj.body, {
|
|
4531
|
+
status: 200,
|
|
4532
|
+
headers: buildResponseHeaders(obj)
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
const [headObj, rangeObj] = await Promise.all([r2.head(key), r2.get(key, { range })]);
|
|
4536
|
+
if (!headObj || !rangeObj) return new Response(null, { status: 404 });
|
|
4537
|
+
const headers = buildResponseHeaders(rangeObj);
|
|
4538
|
+
headers.set("Content-Range", buildContentRange(range, headObj.size));
|
|
4539
|
+
headers.set("Content-Length", String(getRangeContentLength(range, headObj.size)));
|
|
4540
|
+
return new Response(rangeObj.body, {
|
|
4541
|
+
status: 206,
|
|
4542
|
+
headers
|
|
4543
|
+
});
|
|
4544
|
+
}
|
|
4545
|
+
async function handlePutObject(r2, bucketName, key, request, env$1, permitted, mountPrefix) {
|
|
4546
|
+
const copySourceHeader = request.headers.get("x-amz-copy-source");
|
|
4547
|
+
if (copySourceHeader) {
|
|
4548
|
+
const copySource = parseCopySource(copySourceHeader);
|
|
4549
|
+
if (!copySource || !copySource.key) return new Response("Bad Request: invalid x-amz-copy-source", { status: 400 });
|
|
4550
|
+
if (!permitted.has(copySource.bucket)) return new Response(`Access to R2 bucket "${copySource.bucket}" is not permitted. Call mountBucket() with this bucket before accessing it.`, { status: 403 });
|
|
4551
|
+
const sourceBucket = copySource.bucket === bucketName ? r2 : resolveR2Bucket(env$1, copySource.bucket);
|
|
4552
|
+
if (!sourceBucket) return new Response(`R2 binding "${copySource.bucket}" not found in Worker env. Ensure the binding name matches the bucket name passed to mountBucket().`, { status: 500 });
|
|
4553
|
+
const sourceKey = mountPrefix && copySource.bucket === bucketName ? `${mountPrefix}/${copySource.key}` : copySource.key;
|
|
4554
|
+
const sourceObject = await sourceBucket.get(sourceKey);
|
|
4555
|
+
if (!sourceObject) return new Response(null, { status: 404 });
|
|
4556
|
+
const httpMetadata = request.headers.get("x-amz-metadata-directive")?.toUpperCase() === "REPLACE" ? extractHttpMetadata(request) : sourceObject.httpMetadata;
|
|
4557
|
+
const result$1 = await r2.put(key, sourceObject.body, {
|
|
4558
|
+
httpMetadata,
|
|
4559
|
+
customMetadata: sourceObject.customMetadata,
|
|
4560
|
+
storageClass: normalizeStorageClass(sourceObject.storageClass)
|
|
4561
|
+
});
|
|
4562
|
+
return xmlResponse(buildCopyObjectXml(result$1.httpEtag, result$1.uploaded));
|
|
4563
|
+
}
|
|
4564
|
+
const result = await putRequestBody(r2, key, request, { httpMetadata: extractHttpMetadata(request) });
|
|
4565
|
+
if (result instanceof Response) return result;
|
|
4566
|
+
const headers = new Headers();
|
|
4567
|
+
headers.set("ETag", result.httpEtag);
|
|
4568
|
+
return new Response(null, {
|
|
4569
|
+
status: 200,
|
|
4570
|
+
headers
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
async function handleDeleteObject(r2, key) {
|
|
4574
|
+
await r2.delete(key);
|
|
4575
|
+
return new Response(null, { status: 204 });
|
|
4576
|
+
}
|
|
4577
|
+
async function handleCreateMultipartUpload(r2, bucketName, key, request) {
|
|
4578
|
+
const httpMetadata = extractHttpMetadata(request);
|
|
4579
|
+
return xmlResponse(buildInitiateMultipartUploadXml(bucketName, key, (await r2.createMultipartUpload(key, { httpMetadata })).uploadId));
|
|
4580
|
+
}
|
|
4581
|
+
async function handleUploadPart(r2, key, url, request) {
|
|
4582
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4583
|
+
const partNumber = parseInt(url.searchParams.get("partNumber") ?? "0", 10);
|
|
4584
|
+
if (!uploadId || !partNumber) return new Response("Bad Request: missing uploadId or partNumber", { status: 400 });
|
|
4585
|
+
if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
|
|
4586
|
+
const contentLength = request.headers.get("Content-Length");
|
|
4587
|
+
const partLength = contentLength ? Number.parseInt(contentLength, 10) : NaN;
|
|
4588
|
+
if (!Number.isFinite(partLength) || partLength < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
|
|
4589
|
+
const upload = r2.resumeMultipartUpload(key, uploadId);
|
|
4590
|
+
let part;
|
|
4591
|
+
if (partLength === 0) part = await upload.uploadPart(partNumber, new Uint8Array(0));
|
|
4592
|
+
else {
|
|
4593
|
+
const { readable, writable } = new FixedLengthStream(partLength);
|
|
4594
|
+
const pipe = request.body.pipeTo(writable);
|
|
4595
|
+
part = await upload.uploadPart(partNumber, readable);
|
|
4596
|
+
await pipe;
|
|
4597
|
+
}
|
|
4598
|
+
const headers = new Headers();
|
|
4599
|
+
headers.set("ETag", `"${part.etag}"`);
|
|
4600
|
+
return new Response(null, {
|
|
4601
|
+
status: 200,
|
|
4602
|
+
headers
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
async function handleCompleteMultipartUpload(r2, bucketName, key, url, request) {
|
|
4606
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4607
|
+
if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
|
|
4608
|
+
const r2Parts = parseCompleteMultipartUploadBody(await request.text()).map((p) => ({
|
|
4609
|
+
partNumber: p.partNumber,
|
|
4610
|
+
etag: p.etag
|
|
4611
|
+
}));
|
|
4612
|
+
return xmlResponse(buildCompleteMultipartUploadXml(bucketName, key, (await r2.resumeMultipartUpload(key, uploadId).complete(r2Parts)).httpEtag));
|
|
4613
|
+
}
|
|
4614
|
+
async function handleAbortMultipartUpload(r2, key, url) {
|
|
4615
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4616
|
+
if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
|
|
4617
|
+
await r2.resumeMultipartUpload(key, uploadId).abort();
|
|
4618
|
+
return new Response(null, { status: 204 });
|
|
4619
|
+
}
|
|
4620
|
+
const r2EgressHandler = async (request, env$1, ctx) => {
|
|
4621
|
+
const url = new URL(request.url);
|
|
4622
|
+
const parsed = parsePath(url.pathname);
|
|
4623
|
+
if (!parsed) return new Response("Bad Request: empty path", { status: 400 });
|
|
4624
|
+
const { bucket: bucketName, key } = parsed;
|
|
4625
|
+
if (!ctx.params?.buckets || !(bucketName in ctx.params.buckets)) return new Response(`Access to R2 bucket "${bucketName}" is not permitted. Call mountBucket() with this bucket before accessing it.`, { status: 403 });
|
|
4626
|
+
const bucketParams = ctx.params.buckets[bucketName];
|
|
4627
|
+
const rawPrefix = bucketParams.prefix;
|
|
4628
|
+
const mountPrefix = rawPrefix ? trimTrailingSlashes(normalizeObjectKey(rawPrefix)) : void 0;
|
|
4629
|
+
const readOnly = bucketParams.readOnly ?? false;
|
|
4630
|
+
const r2 = resolveR2Bucket(env$1, bucketName);
|
|
4631
|
+
if (!r2) return new Response(`R2 binding "${bucketName}" not found in Worker env. Ensure the binding name matches the bucket name passed to mountBucket().`, { status: 500 });
|
|
4632
|
+
const { method } = request;
|
|
4633
|
+
if (!key) {
|
|
4634
|
+
if (method === "GET" && url.searchParams.has("location")) return xmlResponse(buildLocationXml());
|
|
4635
|
+
if (method === "GET" && url.searchParams.get("list-type") === "2") return handleListObjects(r2, bucketName, url, mountPrefix);
|
|
4636
|
+
if (method === "GET") return handleListObjects(r2, bucketName, url, mountPrefix);
|
|
4637
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
4638
|
+
}
|
|
4639
|
+
const fullKey = mountPrefix ? `${mountPrefix}/${key}` : key;
|
|
4640
|
+
const permitted = new Set(Object.keys(ctx.params.buckets));
|
|
4641
|
+
if (readOnly && (method === "PUT" || method === "DELETE" || method === "POST" && (url.searchParams.has("uploads") || url.searchParams.has("uploadId")))) return new Response("Forbidden: bucket mount is read-only", { status: 403 });
|
|
4642
|
+
if (method === "POST" && url.searchParams.has("uploads")) return handleCreateMultipartUpload(r2, bucketName, fullKey, request);
|
|
4643
|
+
if (method === "POST" && url.searchParams.has("uploadId")) return handleCompleteMultipartUpload(r2, bucketName, fullKey, url, request);
|
|
4644
|
+
if (method === "PUT" && url.searchParams.has("partNumber") && url.searchParams.has("uploadId")) return handleUploadPart(r2, fullKey, url, request);
|
|
4645
|
+
if (method === "DELETE" && url.searchParams.has("uploadId")) return handleAbortMultipartUpload(r2, fullKey, url);
|
|
4646
|
+
switch (method) {
|
|
4647
|
+
case "HEAD": return handleHeadObject(r2, fullKey);
|
|
4648
|
+
case "GET": return handleGetObject(r2, fullKey, request);
|
|
4649
|
+
case "PUT": return handlePutObject(r2, bucketName, fullKey, request, env$1, permitted, mountPrefix);
|
|
4650
|
+
case "DELETE": return handleDeleteObject(r2, fullKey);
|
|
4651
|
+
default: return new Response("Method Not Allowed", { status: 405 });
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
|
|
4655
|
+
//#endregion
|
|
4656
|
+
//#region src/tunnels/sandbox-control-callback.ts
|
|
4657
|
+
var SandboxControlCallbackImpl = class extends RpcTarget {
|
|
4658
|
+
constructor(getHandler, logger) {
|
|
4659
|
+
super();
|
|
4660
|
+
this.getHandler = getHandler;
|
|
4661
|
+
this.logger = logger;
|
|
4662
|
+
}
|
|
4663
|
+
async onTunnelExit(id, port, exitCode) {
|
|
4664
|
+
const handler = this.getHandler();
|
|
4665
|
+
if (!handler) {
|
|
4666
|
+
this.logger.debug("onTunnelExit: no handler bound; ignoring", {
|
|
4667
|
+
id,
|
|
4668
|
+
port,
|
|
4669
|
+
exitCode
|
|
4670
|
+
});
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
await handler(id, port, exitCode);
|
|
4674
|
+
}
|
|
4675
|
+
};
|
|
4676
|
+
|
|
4677
|
+
//#endregion
|
|
4678
|
+
//#region src/tunnels/tunnels-handler.ts
|
|
4679
|
+
/**
|
|
4680
|
+
* Tunnels namespace handler. Created once per Sandbox DO instance via
|
|
4681
|
+
* `createTunnelsHandler(host)` and exposed as `sandbox.tunnels`.
|
|
4682
|
+
*
|
|
4683
|
+
* Storage is the source of truth. The DO holds a `Record<portString, TunnelInfo>`
|
|
4684
|
+
* under the `tunnels` storage key. `Sandbox.onStart()` clears the key on every
|
|
4685
|
+
* container restart so any record in storage is by construction backed by a
|
|
4686
|
+
* running `cloudflared` process; the handler never needs to verify that
|
|
4687
|
+
* separately against the container.
|
|
4688
|
+
*/
|
|
4689
|
+
/** DO storage key for the `port → TunnelInfo` map. */
|
|
4690
|
+
const STORAGE_KEY = "tunnels";
|
|
4691
|
+
function validateTunnelPort(port) {
|
|
4692
|
+
if (!validatePort(port)) throw new SandboxSecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding reserved ports.`);
|
|
4693
|
+
}
|
|
4694
|
+
/** 8-char hex id derived from `crypto.getRandomValues`. Unique per sandbox. */
|
|
4695
|
+
function shortId() {
|
|
4696
|
+
const buf = new Uint8Array(4);
|
|
4697
|
+
crypto.getRandomValues(buf);
|
|
4698
|
+
return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4699
|
+
}
|
|
4700
|
+
function isTunnelNotFoundError(error) {
|
|
4701
|
+
return (error instanceof Error ? error.message : String(error)).includes("TUNNEL_NOT_FOUND");
|
|
4702
|
+
}
|
|
4703
|
+
async function readMap(storage) {
|
|
4704
|
+
return await storage.get(STORAGE_KEY) ?? {};
|
|
4705
|
+
}
|
|
4706
|
+
/**
|
|
4707
|
+
* Concrete `TunnelsHandler` implementation. Extends `RpcTarget` so it
|
|
4708
|
+
* can cross the Workers RPC boundary: the Sandbox DO is reachable from
|
|
4709
|
+
* Workers via Workers RPC (`stub.tunnels.get(port)`), and only
|
|
4710
|
+
* `RpcTarget` instances are passed by reference across that boundary.
|
|
4711
|
+
*/
|
|
4712
|
+
var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
4713
|
+
#host;
|
|
4714
|
+
#withPortLock;
|
|
4715
|
+
constructor(host, withPortLock) {
|
|
4716
|
+
super();
|
|
4717
|
+
this.#host = host;
|
|
4718
|
+
this.#withPortLock = withPortLock;
|
|
4719
|
+
}
|
|
4720
|
+
async get(port) {
|
|
4721
|
+
const startTime = Date.now();
|
|
4722
|
+
let outcome = "error";
|
|
4723
|
+
let cacheState = "miss";
|
|
4724
|
+
let caughtError;
|
|
4725
|
+
try {
|
|
4726
|
+
validateTunnelPort(port);
|
|
4727
|
+
const info = await this.#withPortLock(port, async () => {
|
|
4728
|
+
const existing = (await readMap(this.#host.storage))[port.toString()];
|
|
4729
|
+
if (existing) {
|
|
4730
|
+
cacheState = "hit";
|
|
4731
|
+
return existing;
|
|
4732
|
+
}
|
|
4733
|
+
const id = `quick-${shortId()}`;
|
|
4734
|
+
const spawned = await this.#host.client.tunnels.runQuickTunnel(id, port);
|
|
4735
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
4736
|
+
const nextMap = await readMap(txn);
|
|
4737
|
+
nextMap[port.toString()] = spawned;
|
|
4738
|
+
await txn.put(STORAGE_KEY, nextMap);
|
|
4739
|
+
});
|
|
4740
|
+
return spawned;
|
|
4741
|
+
});
|
|
4742
|
+
outcome = "success";
|
|
4743
|
+
return info;
|
|
4744
|
+
} catch (error) {
|
|
4745
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
4746
|
+
throw error;
|
|
4747
|
+
} finally {
|
|
4748
|
+
logCanonicalEvent(this.#host.logger, {
|
|
4749
|
+
event: "tunnel.get",
|
|
4750
|
+
outcome,
|
|
4751
|
+
port,
|
|
4752
|
+
cacheState,
|
|
4753
|
+
durationMs: Date.now() - startTime,
|
|
4754
|
+
error: caughtError
|
|
4755
|
+
});
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
async destroy(portOrInfo) {
|
|
4759
|
+
const port = typeof portOrInfo === "number" ? portOrInfo : portOrInfo.port;
|
|
4760
|
+
const startTime = Date.now();
|
|
4761
|
+
let outcome = "error";
|
|
4762
|
+
let caughtError;
|
|
4763
|
+
let tunnelId;
|
|
4764
|
+
try {
|
|
4765
|
+
await this.#withPortLock(port, async () => {
|
|
4766
|
+
const existing = (await readMap(this.#host.storage))[port.toString()];
|
|
4767
|
+
if (!existing) return;
|
|
4768
|
+
tunnelId = existing.id;
|
|
4769
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
4770
|
+
const current = await readMap(txn);
|
|
4771
|
+
delete current[port.toString()];
|
|
4772
|
+
await txn.put(STORAGE_KEY, current);
|
|
4773
|
+
});
|
|
4774
|
+
try {
|
|
4775
|
+
await this.#host.client.tunnels.destroyTunnel(existing.id);
|
|
4776
|
+
} catch (error) {
|
|
4777
|
+
if (!isTunnelNotFoundError(error)) throw error;
|
|
4778
|
+
}
|
|
4779
|
+
});
|
|
4780
|
+
outcome = "success";
|
|
4781
|
+
} catch (error) {
|
|
4782
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
4783
|
+
throw error;
|
|
4784
|
+
} finally {
|
|
4785
|
+
logCanonicalEvent(this.#host.logger, {
|
|
4786
|
+
event: "tunnel.destroy",
|
|
4787
|
+
outcome,
|
|
4788
|
+
port,
|
|
4789
|
+
tunnelId,
|
|
4790
|
+
durationMs: Date.now() - startTime,
|
|
4791
|
+
error: caughtError
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
async list() {
|
|
4796
|
+
const map = await readMap(this.#host.storage);
|
|
4797
|
+
return Object.values(map);
|
|
4798
|
+
}
|
|
4799
|
+
};
|
|
4800
|
+
function createTunnelsHandler(host) {
|
|
4801
|
+
const portLocks = /* @__PURE__ */ new Map();
|
|
4802
|
+
const withPortLock = (port, fn) => {
|
|
4803
|
+
const next = (portLocks.get(port) ?? Promise.resolve()).then(fn, fn);
|
|
4804
|
+
portLocks.set(port, next.catch(() => void 0));
|
|
4805
|
+
return next;
|
|
4806
|
+
};
|
|
4807
|
+
const tunnels = new TunnelsRpcTarget(host, withPortLock);
|
|
4808
|
+
const handleTunnelExit = async (id, port, exitCode) => {
|
|
4809
|
+
const startTime = Date.now();
|
|
4810
|
+
await withPortLock(port, async () => {
|
|
4811
|
+
await host.storage.transaction(async (txn) => {
|
|
4812
|
+
const map = await readMap(txn);
|
|
4813
|
+
if (map[port.toString()]?.id === id) {
|
|
4814
|
+
delete map[port.toString()];
|
|
4815
|
+
await txn.put(STORAGE_KEY, map);
|
|
4816
|
+
}
|
|
4817
|
+
});
|
|
4818
|
+
logCanonicalEvent(host.logger, {
|
|
4819
|
+
event: "tunnel.exit",
|
|
4820
|
+
outcome: "success",
|
|
4821
|
+
port,
|
|
4822
|
+
tunnelId: id,
|
|
4823
|
+
exitCode: exitCode ?? void 0,
|
|
4824
|
+
durationMs: Date.now() - startTime
|
|
4825
|
+
});
|
|
4826
|
+
});
|
|
4827
|
+
};
|
|
4828
|
+
return {
|
|
4829
|
+
tunnels,
|
|
4830
|
+
handleTunnelExit
|
|
4831
|
+
};
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4253
4834
|
//#endregion
|
|
4254
4835
|
//#region src/version.ts
|
|
4255
4836
|
/**
|
|
@@ -4257,11 +4838,23 @@ function isLocalhostPattern(hostname) {
|
|
|
4257
4838
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
4258
4839
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
4259
4840
|
*/
|
|
4260
|
-
const SDK_VERSION = "0.10.
|
|
4841
|
+
const SDK_VERSION = "0.10.2";
|
|
4261
4842
|
|
|
4262
4843
|
//#endregion
|
|
4263
4844
|
//#region src/sandbox.ts
|
|
4845
|
+
const R2_EGRESS_PROXY_TARGET_CLASS_NAME = "CloudflareSandboxR2EgressProxyTarget";
|
|
4846
|
+
var R2EgressProxyTarget = class extends Container {};
|
|
4847
|
+
Object.defineProperty(R2EgressProxyTarget, "name", { value: R2_EGRESS_PROXY_TARGET_CLASS_NAME });
|
|
4848
|
+
R2EgressProxyTarget.outboundHandlers = { r2EgressMount: r2EgressHandler };
|
|
4849
|
+
function isFetcher(value) {
|
|
4850
|
+
return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
|
|
4851
|
+
}
|
|
4264
4852
|
const sandboxConfigurationCache = /* @__PURE__ */ new WeakMap();
|
|
4853
|
+
const R2_DEFAULT_S3FS_OPTIONS = {
|
|
4854
|
+
stat_cache_expire: "60",
|
|
4855
|
+
enable_noobj_cache: true,
|
|
4856
|
+
multipart_size: "5"
|
|
4857
|
+
};
|
|
4265
4858
|
const BACKUP_DEFAULT_TTL_SECONDS = 259200;
|
|
4266
4859
|
const BACKUP_MAX_NAME_LENGTH = 256;
|
|
4267
4860
|
const BACKUP_CONTAINER_DIR = "/var/backups";
|
|
@@ -4407,6 +5000,10 @@ function getSandbox(ns, id, options) {
|
|
|
4407
5000
|
desktop: new Proxy({}, { get(_, method) {
|
|
4408
5001
|
if (typeof method !== "string" || method === "then") return void 0;
|
|
4409
5002
|
return (...args) => stub.callDesktop(method, args);
|
|
5003
|
+
} }),
|
|
5004
|
+
tunnels: new Proxy({}, { get: (_, method) => {
|
|
5005
|
+
if (typeof method !== "string" || method === "then") return void 0;
|
|
5006
|
+
return (...args) => stub.callTunnels(method, args);
|
|
4410
5007
|
} })
|
|
4411
5008
|
};
|
|
4412
5009
|
return new Proxy(stub, { get(target, prop) {
|
|
@@ -4427,19 +5024,15 @@ function connect(stub) {
|
|
|
4427
5024
|
return await stub.fetch(portSwitchedRequest);
|
|
4428
5025
|
};
|
|
4429
5026
|
}
|
|
4430
|
-
/**
|
|
4431
|
-
* Type guard for R2Bucket binding.
|
|
4432
|
-
* Checks for the minimal R2Bucket interface methods we use.
|
|
4433
|
-
*/
|
|
4434
|
-
function isR2Bucket(value) {
|
|
4435
|
-
return typeof value === "object" && value !== null && "put" in value && typeof value.put === "function" && "get" in value && typeof value.get === "function" && "head" in value && typeof value.head === "function" && "delete" in value && typeof value.delete === "function";
|
|
4436
|
-
}
|
|
4437
5027
|
var Sandbox = class Sandbox extends Container {
|
|
4438
5028
|
defaultPort = 3e3;
|
|
4439
5029
|
sleepAfter = "10m";
|
|
4440
5030
|
client;
|
|
4441
5031
|
codeInterpreter;
|
|
4442
5032
|
sandboxName = null;
|
|
5033
|
+
tunnelsHandler = null;
|
|
5034
|
+
tunnelExitHandler = null;
|
|
5035
|
+
controlCallback;
|
|
4443
5036
|
normalizeId = false;
|
|
4444
5037
|
defaultSession = null;
|
|
4445
5038
|
containerGeneration = 0;
|
|
@@ -4538,13 +5131,30 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4538
5131
|
* Dispatch method for desktop operations.
|
|
4539
5132
|
* Called by the client-side proxy created in getSandbox() to provide
|
|
4540
5133
|
* the `sandbox.desktop.status()` API without relying on RPC pipelining
|
|
4541
|
-
* through property getters.
|
|
5134
|
+
* through property getters which is broken when using vite-plugin.
|
|
4542
5135
|
*/
|
|
4543
5136
|
async callDesktop(method, args) {
|
|
4544
5137
|
if (!Sandbox.DESKTOP_METHODS.has(method)) throw new Error(`Unknown desktop method: ${method}`);
|
|
4545
5138
|
const client = this.client.desktop;
|
|
4546
5139
|
const fn = client[method];
|
|
4547
|
-
if (typeof fn !== "function") throw new Error(`
|
|
5140
|
+
if (typeof fn !== "function") throw new Error(`sandbox.desktop missing method: ${method}`);
|
|
5141
|
+
return fn.apply(client, args);
|
|
5142
|
+
}
|
|
5143
|
+
/**
|
|
5144
|
+
* Dispatch method for tunnel operations.
|
|
5145
|
+
* Called by the client-side proxy created in getSandbox() to provide
|
|
5146
|
+
* the `sandbox.tunnels` API without relying on RPC pipelining
|
|
5147
|
+
* through property getters which is broken when using vite-plugin.
|
|
5148
|
+
*/
|
|
5149
|
+
async callTunnels(method, args) {
|
|
5150
|
+
if (![
|
|
5151
|
+
"get",
|
|
5152
|
+
"list",
|
|
5153
|
+
"destroy"
|
|
5154
|
+
].includes(method)) throw new Error(`Unknown tunnels method: ${method}`);
|
|
5155
|
+
const client = this.tunnels;
|
|
5156
|
+
const fn = client[method];
|
|
5157
|
+
if (typeof fn !== "function") throw new Error(`sandbox.tunnels missing method: ${method}`);
|
|
4548
5158
|
return fn.apply(client, args);
|
|
4549
5159
|
}
|
|
4550
5160
|
/**
|
|
@@ -4590,6 +5200,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4590
5200
|
port: 3e3,
|
|
4591
5201
|
logger: this.logger,
|
|
4592
5202
|
retryTimeoutMs: this.computeRetryTimeoutMs(),
|
|
5203
|
+
localMain: this.controlCallback,
|
|
4593
5204
|
onActivity: () => {
|
|
4594
5205
|
this.renewActivityTimeout();
|
|
4595
5206
|
},
|
|
@@ -4604,9 +5215,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4604
5215
|
}
|
|
4605
5216
|
return this.createSandboxClient();
|
|
4606
5217
|
}
|
|
4607
|
-
constructor(ctx, env) {
|
|
4608
|
-
super(ctx, env);
|
|
4609
|
-
const envObj = env;
|
|
5218
|
+
constructor(ctx, env$1) {
|
|
5219
|
+
super(ctx, env$1);
|
|
5220
|
+
const envObj = env$1;
|
|
4610
5221
|
["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
|
|
4611
5222
|
if (envObj?.[key]) this.envVars[key] = String(envObj[key]);
|
|
4612
5223
|
});
|
|
@@ -4629,6 +5240,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4629
5240
|
accessKeyId: this.r2AccessKeyId,
|
|
4630
5241
|
secretAccessKey: this.r2SecretAccessKey
|
|
4631
5242
|
});
|
|
5243
|
+
this.controlCallback = new SandboxControlCallbackImpl(() => this.tunnelExitHandler, this.logger);
|
|
4632
5244
|
this.client = this.createClientForTransport(this.transport);
|
|
4633
5245
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
4634
5246
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
@@ -4656,6 +5268,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4656
5268
|
const previousClient = this.client;
|
|
4657
5269
|
this.client = this.createClientForTransport(storedTransport);
|
|
4658
5270
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
5271
|
+
this.tunnelsHandler = null;
|
|
5272
|
+
this.tunnelExitHandler = null;
|
|
4659
5273
|
previousClient.disconnect();
|
|
4660
5274
|
}
|
|
4661
5275
|
if (storedTransport) this.hasStoredTransport = true;
|
|
@@ -4747,6 +5361,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4747
5361
|
this.hasStoredTransport = true;
|
|
4748
5362
|
this.client = this.createClientForTransport(transport);
|
|
4749
5363
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
5364
|
+
this.tunnelsHandler = null;
|
|
5365
|
+
this.tunnelExitHandler = null;
|
|
4750
5366
|
previousClient.disconnect();
|
|
4751
5367
|
this.renewActivityTimeout();
|
|
4752
5368
|
this.logger.debug("Transport updated", { transport });
|
|
@@ -4764,7 +5380,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4764
5380
|
* Get default timeouts with env var fallbacks and validation
|
|
4765
5381
|
* Precedence: SDK defaults < Env vars < User config
|
|
4766
5382
|
*/
|
|
4767
|
-
getDefaultTimeouts(env) {
|
|
5383
|
+
getDefaultTimeouts(env$1) {
|
|
4768
5384
|
const parseAndValidate = (envVar, name, min, max) => {
|
|
4769
5385
|
const defaultValue = this.DEFAULT_CONTAINER_TIMEOUTS[name];
|
|
4770
5386
|
if (envVar === void 0) return defaultValue;
|
|
@@ -4780,9 +5396,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4780
5396
|
return parsed;
|
|
4781
5397
|
};
|
|
4782
5398
|
return {
|
|
4783
|
-
instanceGetTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
|
|
4784
|
-
portReadyTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
|
|
4785
|
-
waitIntervalMS: parseAndValidate(getEnvString(env, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
|
|
5399
|
+
instanceGetTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
|
|
5400
|
+
portReadyTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
|
|
5401
|
+
waitIntervalMS: parseAndValidate(getEnvString(env$1, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
|
|
4786
5402
|
};
|
|
4787
5403
|
}
|
|
4788
5404
|
/**
|
|
@@ -4804,7 +5420,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4804
5420
|
await this.mountBucketLocal(bucket, mountPath, options);
|
|
4805
5421
|
return;
|
|
4806
5422
|
}
|
|
4807
|
-
|
|
5423
|
+
const remoteOptions = options;
|
|
5424
|
+
if (remoteOptions.endpoint === void 0) {
|
|
5425
|
+
const binding = this.env[bucket];
|
|
5426
|
+
if (isR2Bucket(binding)) {
|
|
5427
|
+
await this.mountBucketR2Egress(bucket, mountPath, options);
|
|
5428
|
+
return;
|
|
5429
|
+
}
|
|
5430
|
+
throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in Worker env. Ensure the binding name matches the bucket binding configured in wrangler.jsonc.`);
|
|
5431
|
+
}
|
|
5432
|
+
await this.mountBucketFuse(bucket, mountPath, remoteOptions);
|
|
4808
5433
|
}
|
|
4809
5434
|
/**
|
|
4810
5435
|
* Local dev mount: bidirectional sync via R2 binding + file/watch APIs
|
|
@@ -4861,12 +5486,109 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4861
5486
|
});
|
|
4862
5487
|
}
|
|
4863
5488
|
}
|
|
5489
|
+
getR2EgressParams() {
|
|
5490
|
+
const buckets = {};
|
|
5491
|
+
for (const [, m] of this.activeMounts) if (m.mountType === "r2-egress") buckets[m.bucket] = {
|
|
5492
|
+
prefix: m.prefix,
|
|
5493
|
+
readOnly: m.readOnly
|
|
5494
|
+
};
|
|
5495
|
+
return { buckets };
|
|
5496
|
+
}
|
|
5497
|
+
validateR2EgressS3fsOptions(options) {
|
|
5498
|
+
if (!options) return;
|
|
5499
|
+
const protectedOptions = new Set(["passwd_file", "url"]);
|
|
5500
|
+
for (const option of options) {
|
|
5501
|
+
const [key] = option.split("=");
|
|
5502
|
+
if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for R2 binding mounts`);
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
/**
|
|
5506
|
+
* Credential-less R2 mount: egress interception routes s3fs requests to the
|
|
5507
|
+
* R2 binding. No S3 credentials are needed in the container or Worker env.
|
|
5508
|
+
*/
|
|
5509
|
+
async mountBucketR2Egress(bucket, mountPath, options) {
|
|
5510
|
+
const mountStartTime = Date.now();
|
|
5511
|
+
const prefix = options.prefix;
|
|
5512
|
+
let mountOutcome = "error";
|
|
5513
|
+
let mountError;
|
|
5514
|
+
try {
|
|
5515
|
+
validateBucketBindingName(bucket, mountPath);
|
|
5516
|
+
this.validateMountPath(mountPath);
|
|
5517
|
+
this.validateR2EgressS3fsOptions(options.s3fsOptions);
|
|
5518
|
+
for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
|
|
5519
|
+
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.`);
|
|
5520
|
+
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.`);
|
|
5521
|
+
}
|
|
5522
|
+
const passwordFilePath = this.generatePasswordFilePath();
|
|
5523
|
+
await this.createPasswordFile(passwordFilePath, bucket, {
|
|
5524
|
+
accessKeyId: "x",
|
|
5525
|
+
secretAccessKey: "x"
|
|
5526
|
+
});
|
|
5527
|
+
const mountInfo = {
|
|
5528
|
+
mountType: "r2-egress",
|
|
5529
|
+
bucket,
|
|
5530
|
+
mountPath,
|
|
5531
|
+
passwordFilePath,
|
|
5532
|
+
mounted: false,
|
|
5533
|
+
prefix,
|
|
5534
|
+
readOnly: options.readOnly ?? false
|
|
5535
|
+
};
|
|
5536
|
+
this.activeMounts.set(mountPath, mountInfo);
|
|
5537
|
+
await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
5538
|
+
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
5539
|
+
const s3fsSource = bucket;
|
|
5540
|
+
const optionsStr = shellEscape(serializeS3fsOptions({
|
|
5541
|
+
passwd_file: passwordFilePath,
|
|
5542
|
+
...R2_DEFAULT_S3FS_OPTIONS,
|
|
5543
|
+
...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
|
|
5544
|
+
use_path_request_style: true,
|
|
5545
|
+
url: "http://r2.internal",
|
|
5546
|
+
...options.readOnly ? { ro: true } : {}
|
|
5547
|
+
}));
|
|
5548
|
+
const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
5549
|
+
this.logger.debug("r2-egress: running s3fs", { mountCmd });
|
|
5550
|
+
const result = await this.execInternal(mountCmd);
|
|
5551
|
+
this.logger.debug("r2-egress: s3fs exited", {
|
|
5552
|
+
exitCode: result.exitCode,
|
|
5553
|
+
stdout: result.stdout,
|
|
5554
|
+
stderr: result.stderr
|
|
5555
|
+
});
|
|
5556
|
+
if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
|
|
5557
|
+
const mountpointCheck = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && echo 'FUSE_MOUNTED' || echo 'NOT_FUSE_MOUNTED'`);
|
|
5558
|
+
this.logger.debug("r2-egress: mountpoint check", {
|
|
5559
|
+
stdout: mountpointCheck.stdout.trim(),
|
|
5560
|
+
exitCode: mountpointCheck.exitCode
|
|
5561
|
+
});
|
|
5562
|
+
if (mountpointCheck.stdout.trim() !== "FUSE_MOUNTED") throw new S3FSMountError(`s3fs exited 0 but mount was not established at ${mountPath}`);
|
|
5563
|
+
mountInfo.mounted = true;
|
|
5564
|
+
mountOutcome = "success";
|
|
5565
|
+
} catch (error) {
|
|
5566
|
+
mountError = error instanceof Error ? error : new Error(String(error));
|
|
5567
|
+
const failedMount = this.activeMounts.get(mountPath);
|
|
5568
|
+
this.activeMounts.delete(mountPath);
|
|
5569
|
+
if (failedMount?.mountType === "r2-egress") await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
|
|
5570
|
+
const remainingParams = this.getR2EgressParams();
|
|
5571
|
+
await this.configureR2EgressOutbound(remainingParams).catch(() => {});
|
|
5572
|
+
throw error;
|
|
5573
|
+
} finally {
|
|
5574
|
+
logCanonicalEvent(this.logger, {
|
|
5575
|
+
event: "bucket.mount",
|
|
5576
|
+
outcome: mountOutcome,
|
|
5577
|
+
durationMs: Date.now() - mountStartTime,
|
|
5578
|
+
bucket,
|
|
5579
|
+
mountPath,
|
|
5580
|
+
provider: "r2",
|
|
5581
|
+
prefix,
|
|
5582
|
+
error: mountError
|
|
5583
|
+
});
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
4864
5586
|
/**
|
|
4865
5587
|
* Production mount: S3FS-FUSE inside the container
|
|
4866
5588
|
*/
|
|
4867
5589
|
async mountBucketFuse(bucket, mountPath, options) {
|
|
4868
5590
|
const mountStartTime = Date.now();
|
|
4869
|
-
const prefix = options.prefix
|
|
5591
|
+
const prefix = options.prefix;
|
|
4870
5592
|
let mountOutcome = "error";
|
|
4871
5593
|
let mountError;
|
|
4872
5594
|
let passwordFilePath;
|
|
@@ -4957,6 +5679,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4957
5679
|
}
|
|
4958
5680
|
mountInfo.mounted = false;
|
|
4959
5681
|
this.activeMounts.delete(mountPath);
|
|
5682
|
+
if (mountInfo.mountType === "r2-egress") await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
4960
5683
|
try {
|
|
4961
5684
|
const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
|
|
4962
5685
|
if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
|
|
@@ -4989,7 +5712,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4989
5712
|
}
|
|
4990
5713
|
}
|
|
4991
5714
|
/**
|
|
4992
|
-
*
|
|
5715
|
+
* Shared validation for mount path (absolute, not already in use).
|
|
5716
|
+
*/
|
|
5717
|
+
validateMountPath(mountPath) {
|
|
5718
|
+
if (!mountPath.startsWith("/")) throw new InvalidMountConfigError(`Mount path must be absolute (start with /): "${mountPath}"`);
|
|
5719
|
+
if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path "${mountPath}" is already in use by bucket "${this.activeMounts.get(mountPath)?.bucket}". Unmount the existing bucket first or use a different mount path.`);
|
|
5720
|
+
}
|
|
5721
|
+
/**
|
|
5722
|
+
* Validate mount options for remote (FUSE) mounts
|
|
4993
5723
|
*/
|
|
4994
5724
|
validateMountOptions(bucket, mountPath, options) {
|
|
4995
5725
|
try {
|
|
@@ -4998,8 +5728,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4998
5728
|
throw new InvalidMountConfigError(`Invalid endpoint URL: "${options.endpoint}". Must be a valid HTTP(S) URL.`);
|
|
4999
5729
|
}
|
|
5000
5730
|
validateBucketName(bucket, mountPath);
|
|
5001
|
-
|
|
5002
|
-
if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path "${mountPath}" is already in use by bucket "${this.activeMounts.get(mountPath)?.bucket}". Unmount the existing bucket first or use a different mount path.`);
|
|
5731
|
+
this.validateMountPath(mountPath);
|
|
5003
5732
|
}
|
|
5004
5733
|
/**
|
|
5005
5734
|
* Generate unique password file path for s3fs credentials
|
|
@@ -5059,6 +5788,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5059
5788
|
if (result.exitCode === 2) throw new S3FSMountError(`S3FS mount failed: ${detail || "Unknown error"}`);
|
|
5060
5789
|
throw new S3FSMountError(`S3FS mount failed: FUSE filesystem never appeared at ${mountPath}. ${detail ? `s3fs log: ${detail}` : "No s3fs log output captured. The s3fs daemon may have exited before writing logs."}`);
|
|
5061
5790
|
}
|
|
5791
|
+
async unmountTrackedFuseMount(mountPath, mountInfo) {
|
|
5792
|
+
if (!mountInfo.mounted) return;
|
|
5793
|
+
this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
|
|
5794
|
+
const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
5795
|
+
if (result.exitCode !== 0) throw new Error(`fusermount -u failed (exit ${result.exitCode}): ${result.stderr || "unknown error"}`);
|
|
5796
|
+
mountInfo.mounted = false;
|
|
5797
|
+
}
|
|
5062
5798
|
/**
|
|
5063
5799
|
* In-flight `destroy()` promise. While set, concurrent callers coalesce
|
|
5064
5800
|
* onto the same teardown instead of triggering a second one. Cleared when
|
|
@@ -5120,10 +5856,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5120
5856
|
this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
|
|
5121
5857
|
}
|
|
5122
5858
|
else {
|
|
5123
|
-
|
|
5124
|
-
this.
|
|
5125
|
-
await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
5126
|
-
mountInfo.mounted = false;
|
|
5859
|
+
try {
|
|
5860
|
+
await this.unmountTrackedFuseMount(mountPath, mountInfo);
|
|
5127
5861
|
} catch (error) {
|
|
5128
5862
|
mountFailures++;
|
|
5129
5863
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -5133,6 +5867,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5133
5867
|
}
|
|
5134
5868
|
}
|
|
5135
5869
|
await this.ctx.storage.delete("portTokens");
|
|
5870
|
+
await this.ctx.storage.delete("tunnels");
|
|
5136
5871
|
this.client.disconnect();
|
|
5137
5872
|
outcome = "success";
|
|
5138
5873
|
await super.destroy();
|
|
@@ -5160,6 +5895,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5160
5895
|
} catch (error) {
|
|
5161
5896
|
this.logger.error("Failed to restore exposed ports after container start", error instanceof Error ? error : new Error(String(error)));
|
|
5162
5897
|
}
|
|
5898
|
+
try {
|
|
5899
|
+
await this.ctx.storage.delete("tunnels");
|
|
5900
|
+
} catch (error) {
|
|
5901
|
+
this.logger.error("Failed to clear tunnel storage after container start", error instanceof Error ? error : new Error(String(error)));
|
|
5902
|
+
}
|
|
5163
5903
|
}
|
|
5164
5904
|
/**
|
|
5165
5905
|
* Re-expose ports on the container runtime using tokens persisted in DO
|
|
@@ -5262,7 +6002,10 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5262
6002
|
this.defaultSession = null;
|
|
5263
6003
|
this.defaultSessionInit = null;
|
|
5264
6004
|
this.client.disconnect();
|
|
6005
|
+
let hadR2EgressMount = false;
|
|
5265
6006
|
for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
|
|
6007
|
+
else if (m.mountType === "r2-egress") hadR2EgressMount = true;
|
|
6008
|
+
if (hadR2EgressMount) await this.configureR2EgressOutbound({ buckets: {} }).catch(() => {});
|
|
5266
6009
|
this.activeMounts.clear();
|
|
5267
6010
|
await this.ctx.storage.delete("defaultSession");
|
|
5268
6011
|
}
|
|
@@ -6009,8 +6752,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6009
6752
|
}
|
|
6010
6753
|
async getProcess(id, sessionId) {
|
|
6011
6754
|
const session = sessionId ?? await this.ensureDefaultSession();
|
|
6012
|
-
const response = await this.client.processes.getProcess(id)
|
|
6013
|
-
|
|
6755
|
+
const response = await this.client.processes.getProcess(id).catch((e) => {
|
|
6756
|
+
if (e instanceof ProcessNotFoundError) return null;
|
|
6757
|
+
throw e;
|
|
6758
|
+
});
|
|
6759
|
+
if (!response?.process) return null;
|
|
6014
6760
|
const processData = response.process;
|
|
6015
6761
|
return this.createProcessFromDTO({
|
|
6016
6762
|
id: processData.id,
|
|
@@ -6081,7 +6827,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6081
6827
|
}
|
|
6082
6828
|
async writeFile(path$1, content, options = {}) {
|
|
6083
6829
|
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
6084
|
-
if (content instanceof ReadableStream) return this.client.writeFileStream(path$1, content, session);
|
|
6830
|
+
if (content instanceof ReadableStream) return this.client.files.writeFileStream(path$1, content, session);
|
|
6085
6831
|
return this.client.files.writeFile(path$1, content, session, { encoding: options.encoding });
|
|
6086
6832
|
}
|
|
6087
6833
|
async deleteFile(path$1, sessionId) {
|
|
@@ -6098,6 +6844,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6098
6844
|
}
|
|
6099
6845
|
async readFile(path$1, options = {}) {
|
|
6100
6846
|
const session = options.sessionId ?? await this.ensureDefaultSession();
|
|
6847
|
+
if (options.encoding === "none") return this.client.files.readFile(path$1, session, { encoding: "none" });
|
|
6101
6848
|
return this.client.files.readFile(path$1, session, { encoding: options.encoding });
|
|
6102
6849
|
}
|
|
6103
6850
|
/**
|
|
@@ -6326,6 +7073,45 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6326
7073
|
}];
|
|
6327
7074
|
});
|
|
6328
7075
|
}
|
|
7076
|
+
/**
|
|
7077
|
+
* Namespaced tunnel API. Quick tunnels are zero-config preview URLs
|
|
7078
|
+
* backed by Cloudflare's trycloudflare service.
|
|
7079
|
+
*
|
|
7080
|
+
* - `tunnels.get(port)` — idempotent. Returns the cached tunnel for
|
|
7081
|
+
* `port` if one exists in DO storage, otherwise spawns a fresh
|
|
7082
|
+
* cloudflared process and persists the record.
|
|
7083
|
+
* - `tunnels.list()` — records currently known to this sandbox, from
|
|
7084
|
+
* DO storage.
|
|
7085
|
+
* - `tunnels.destroy(portOrInfo)` — tear down by port number or by
|
|
7086
|
+
* the record returned from `get()`.
|
|
7087
|
+
*
|
|
7088
|
+
* Storage is cleared on container restart (`onStart`), so URLs do
|
|
7089
|
+
* not survive a container restart — the next `get(port)` call will
|
|
7090
|
+
* spawn a fresh tunnel with a new URL.
|
|
7091
|
+
*
|
|
7092
|
+
* Requires the RPC transport. Calling this on a route-based transport
|
|
7093
|
+
* throws "RPC transport required".
|
|
7094
|
+
*/
|
|
7095
|
+
get tunnels() {
|
|
7096
|
+
this.ensureTunnelsBuilt();
|
|
7097
|
+
return this.tunnelsHandler;
|
|
7098
|
+
}
|
|
7099
|
+
/**
|
|
7100
|
+
* Lazily construct both the public tunnels handler and its sibling
|
|
7101
|
+
* exit-handler callback. Called from the `tunnels` getter on first
|
|
7102
|
+
* access and on every access after a transport swap clears both
|
|
7103
|
+
* fields.
|
|
7104
|
+
*/
|
|
7105
|
+
ensureTunnelsBuilt() {
|
|
7106
|
+
if (this.tunnelsHandler) return;
|
|
7107
|
+
const built = createTunnelsHandler({
|
|
7108
|
+
client: this.client,
|
|
7109
|
+
storage: this.ctx.storage,
|
|
7110
|
+
logger: this.logger
|
|
7111
|
+
});
|
|
7112
|
+
this.tunnelsHandler = built.tunnels;
|
|
7113
|
+
this.tunnelExitHandler = built.handleTunnelExit;
|
|
7114
|
+
}
|
|
6329
7115
|
async isPortExposed(port) {
|
|
6330
7116
|
try {
|
|
6331
7117
|
const sessionId = await this.ensureDefaultSession();
|
|
@@ -6472,9 +7258,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6472
7258
|
...options,
|
|
6473
7259
|
sessionId
|
|
6474
7260
|
}),
|
|
6475
|
-
readFile: (path$1, options) =>
|
|
6476
|
-
|
|
6477
|
-
|
|
7261
|
+
readFile: ((path$1, options) => {
|
|
7262
|
+
const encoding = options?.encoding;
|
|
7263
|
+
if (encoding === "none") return this.readFile(path$1, {
|
|
7264
|
+
encoding: "none",
|
|
7265
|
+
sessionId
|
|
7266
|
+
});
|
|
7267
|
+
return this.readFile(path$1, {
|
|
7268
|
+
encoding,
|
|
7269
|
+
sessionId
|
|
7270
|
+
});
|
|
6478
7271
|
}),
|
|
6479
7272
|
readFileStream: (path$1) => this.readFileStream(path$1, { sessionId }),
|
|
6480
7273
|
watch: (path$1, options) => this.watch(path$1, {
|
|
@@ -7534,7 +8327,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7534
8327
|
},
|
|
7535
8328
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7536
8329
|
});
|
|
7537
|
-
await this.client.writeFileStream(archivePath, body, backupSession);
|
|
8330
|
+
await this.client.files.writeFileStream(archivePath, body, backupSession);
|
|
7538
8331
|
} else {
|
|
7539
8332
|
const archiveBuffer = await archiveObject.arrayBuffer();
|
|
7540
8333
|
const base64Content = Buffer.from(archiveBuffer).toString("base64");
|
|
@@ -7588,8 +8381,24 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7588
8381
|
});
|
|
7589
8382
|
}
|
|
7590
8383
|
}
|
|
8384
|
+
async configureR2EgressOutbound(params) {
|
|
8385
|
+
const ctx = this.ctx;
|
|
8386
|
+
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
|
|
8387
|
+
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
8388
|
+
const fetcher = ctx.exports.ContainerProxy({ props: {
|
|
8389
|
+
enableInternet: this.enableInternet,
|
|
8390
|
+
containerId: this.ctx.id.toString(),
|
|
8391
|
+
className: R2_EGRESS_PROXY_TARGET_CLASS_NAME,
|
|
8392
|
+
outboundByHostOverrides: { "r2.internal": {
|
|
8393
|
+
method: "r2EgressMount",
|
|
8394
|
+
params
|
|
8395
|
+
} }
|
|
8396
|
+
} });
|
|
8397
|
+
if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
|
|
8398
|
+
await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
|
|
8399
|
+
}
|
|
7591
8400
|
};
|
|
7592
8401
|
|
|
7593
8402
|
//#endregion
|
|
7594
8403
|
export { DesktopInvalidOptionsError as A, CommandClient as C, BackupNotFoundError as D, BackupExpiredError as E, InvalidBackupConfigError as F, ProcessExitedBeforeReadyError as I, ProcessReadyTimeoutError as L, DesktopProcessCrashedError as M, DesktopStartFailedError as N, BackupRestoreError as O, DesktopUnavailableError as P, RPCTransportError as R, DesktopClient as S, BackupCreateError as T, UtilityClient as _, BucketMountError as a, GitClient as b, MissingCredentialsError as c, parseSSEStream as d, responseToAsyncIterable as f, SandboxClient as g, streamFile as h, proxyTerminal as i, DesktopNotStartedError as j, DesktopInvalidCoordinatesError as k, S3FSMountError as l, collectFile as m, getSandbox as n, BucketUnmountError as o, CodeInterpreter as p, proxyToSandbox as r, InvalidMountConfigError as s, Sandbox as t, asyncIterableToSSEStream as u, ProcessClient as v, BackupClient as w, FileClient as x, PortClient as y, SessionTerminatedError as z };
|
|
7595
|
-
//# sourceMappingURL=sandbox-
|
|
8404
|
+
//# sourceMappingURL=sandbox-BcEq4aUF.js.map
|