@cloudflare/sandbox 0.10.1 → 0.10.3
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 +63 -0
- package/README.md +44 -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.d.ts.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-uC1vzWtG.js → sandbox-B-MUmsli.js} +1111 -160
- package/dist/sandbox-B-MUmsli.js.map +1 -0
- package/dist/{sandbox-BVgScWy9.d.ts → sandbox-CwcSm_60.d.ts} +256 -73
- package/dist/sandbox-CwcSm_60.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/sandbox-BVgScWy9.d.ts.map +0 -1
- package/dist/sandbox-uC1vzWtG.js.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
|
|
|
@@ -2523,6 +2525,9 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2523
2525
|
return "unknown";
|
|
2524
2526
|
}
|
|
2525
2527
|
}
|
|
2528
|
+
listSessions() {
|
|
2529
|
+
throw new Error("listSessions requires the RPC transport. Set SANDBOX_TRANSPORT=rpc.");
|
|
2530
|
+
}
|
|
2526
2531
|
};
|
|
2527
2532
|
|
|
2528
2533
|
//#endregion
|
|
@@ -2646,6 +2651,13 @@ var SandboxClient = class {
|
|
|
2646
2651
|
utils;
|
|
2647
2652
|
desktop;
|
|
2648
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();
|
|
2649
2661
|
transport = null;
|
|
2650
2662
|
constructor(options) {
|
|
2651
2663
|
if (options.transportMode === "websocket" && options.wsUrl) this.transport = createTransport({
|
|
@@ -2723,6 +2735,14 @@ var SandboxClient = class {
|
|
|
2723
2735
|
if (this.transport) this.transport.disconnect();
|
|
2724
2736
|
}
|
|
2725
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
|
+
}
|
|
2726
2746
|
|
|
2727
2747
|
//#endregion
|
|
2728
2748
|
//#region ../shared/src/backup.ts
|
|
@@ -2745,6 +2765,10 @@ function normalizeBackupExcludePattern(pattern) {
|
|
|
2745
2765
|
return normalized;
|
|
2746
2766
|
}
|
|
2747
2767
|
|
|
2768
|
+
//#endregion
|
|
2769
|
+
//#region ../shared/src/internal.ts
|
|
2770
|
+
const DISABLE_SESSION_TOKEN = "__DISABLE_SESSION__";
|
|
2771
|
+
|
|
2748
2772
|
//#endregion
|
|
2749
2773
|
//#region src/container-control/connection.ts
|
|
2750
2774
|
const DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -2769,13 +2793,15 @@ var ContainerControlConnection = class {
|
|
|
2769
2793
|
port;
|
|
2770
2794
|
logger;
|
|
2771
2795
|
retryTimeoutMs;
|
|
2796
|
+
onClose;
|
|
2772
2797
|
constructor(options) {
|
|
2773
2798
|
this.containerStub = options.stub;
|
|
2774
2799
|
this.port = options.port ?? 3e3;
|
|
2775
2800
|
this.logger = options.logger ?? createNoOpLogger();
|
|
2776
2801
|
this.retryTimeoutMs = options.retryTimeoutMs ?? DEFAULT_RETRY_TIMEOUT_MS;
|
|
2802
|
+
this.onClose = options.onClose;
|
|
2777
2803
|
this.transport = new DeferredTransport();
|
|
2778
|
-
this.session = new RpcSession(this.transport);
|
|
2804
|
+
this.session = new RpcSession(this.transport, options.localMain);
|
|
2779
2805
|
this.stub = this.session.getRemoteMain();
|
|
2780
2806
|
}
|
|
2781
2807
|
/**
|
|
@@ -2815,6 +2841,8 @@ var ContainerControlConnection = class {
|
|
|
2815
2841
|
this.stub[Symbol.dispose]?.();
|
|
2816
2842
|
} catch {}
|
|
2817
2843
|
if (this.ws) {
|
|
2844
|
+
this.ws.removeEventListener("close", this.onWebSocketClose);
|
|
2845
|
+
this.ws.removeEventListener("error", this.onWebSocketError);
|
|
2818
2846
|
try {
|
|
2819
2847
|
this.ws.close();
|
|
2820
2848
|
} catch {}
|
|
@@ -2831,6 +2859,42 @@ var ContainerControlConnection = class {
|
|
|
2831
2859
|
setRetryTimeoutMs(ms) {
|
|
2832
2860
|
this.retryTimeoutMs = ms;
|
|
2833
2861
|
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Run the owner-provided `onClose` callback exactly once per call,
|
|
2864
|
+
* swallowing any errors so a buggy listener can't keep the connection
|
|
2865
|
+
* object in a half-torn-down state.
|
|
2866
|
+
*/
|
|
2867
|
+
fireOnClose() {
|
|
2868
|
+
if (!this.onClose) return;
|
|
2869
|
+
try {
|
|
2870
|
+
this.onClose();
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
this.logger.warn("ContainerControlConnection onClose handler threw", { error: err instanceof Error ? err.message : String(err) });
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* WebSocket `close` listener. Defined as a bound arrow field so the
|
|
2877
|
+
* same reference can be passed to both `addEventListener` and
|
|
2878
|
+
* `removeEventListener` — a fresh anonymous lambda would silently
|
|
2879
|
+
* fail to unbind.
|
|
2880
|
+
*/
|
|
2881
|
+
onWebSocketClose = () => {
|
|
2882
|
+
const wasConnected = this.connected;
|
|
2883
|
+
this.connected = false;
|
|
2884
|
+
this.ws = null;
|
|
2885
|
+
this.logger.debug("ContainerControlConnection WebSocket closed");
|
|
2886
|
+
if (wasConnected) this.fireOnClose();
|
|
2887
|
+
};
|
|
2888
|
+
/**
|
|
2889
|
+
* WebSocket `error` listener. Same field-form rationale as
|
|
2890
|
+
* {@link onWebSocketClose}.
|
|
2891
|
+
*/
|
|
2892
|
+
onWebSocketError = () => {
|
|
2893
|
+
const wasConnected = this.connected;
|
|
2894
|
+
this.connected = false;
|
|
2895
|
+
this.ws = null;
|
|
2896
|
+
if (wasConnected) this.fireOnClose();
|
|
2897
|
+
};
|
|
2834
2898
|
async doConnect() {
|
|
2835
2899
|
try {
|
|
2836
2900
|
const response = await this.fetchUpgradeWithRetry();
|
|
@@ -2838,15 +2902,8 @@ var ContainerControlConnection = class {
|
|
|
2838
2902
|
const ws = response.webSocket;
|
|
2839
2903
|
if (!ws) throw new Error("No WebSocket in upgrade response");
|
|
2840
2904
|
ws.accept();
|
|
2841
|
-
ws.addEventListener("close",
|
|
2842
|
-
|
|
2843
|
-
this.ws = null;
|
|
2844
|
-
this.logger.debug("ContainerControlConnection WebSocket closed");
|
|
2845
|
-
});
|
|
2846
|
-
ws.addEventListener("error", () => {
|
|
2847
|
-
this.connected = false;
|
|
2848
|
-
this.ws = null;
|
|
2849
|
-
});
|
|
2905
|
+
ws.addEventListener("close", this.onWebSocketClose);
|
|
2906
|
+
ws.addEventListener("error", this.onWebSocketError);
|
|
2850
2907
|
this.ws = ws;
|
|
2851
2908
|
this.transport.activate(ws);
|
|
2852
2909
|
this.connected = true;
|
|
@@ -3004,12 +3061,33 @@ const IDLE_EXPORT_THRESHOLD = 1;
|
|
|
3004
3061
|
/**
|
|
3005
3062
|
* Translate a capnweb-propagated error into a typed SandboxError.
|
|
3006
3063
|
*
|
|
3007
|
-
*
|
|
3008
|
-
*
|
|
3009
|
-
*
|
|
3064
|
+
* Two wire formats are supported for backward compatibility with older
|
|
3065
|
+
* container images:
|
|
3066
|
+
*
|
|
3067
|
+
* 1. Propagated error properties (capnweb >= 0.8.0). The container throws a
|
|
3068
|
+
* `ServiceError`-shaped object with own enumerable `code` and `details`
|
|
3069
|
+
* properties. capnweb walks `Object.keys()` and reconstructs those fields
|
|
3070
|
+
* on the SDK side.
|
|
3071
|
+
* 2. Legacy JSON-encoded message. Older containers encoded the structured
|
|
3072
|
+
* payload as a JSON string in `error.message`.
|
|
3073
|
+
*
|
|
3074
|
+
* The JSON-fallback branch can be removed once all older container images are
|
|
3075
|
+
* no longer in service.
|
|
3010
3076
|
*/
|
|
3011
3077
|
function translateRPCError(error) {
|
|
3012
3078
|
if (error instanceof Error) {
|
|
3079
|
+
const propagated = error;
|
|
3080
|
+
if (typeof propagated.code === "string" && Object.hasOwn(ErrorCode, propagated.code)) {
|
|
3081
|
+
const code = propagated.code;
|
|
3082
|
+
const context = propagated.details && typeof propagated.details === "object" ? propagated.details : {};
|
|
3083
|
+
throw createErrorFromResponse({
|
|
3084
|
+
code,
|
|
3085
|
+
message: error.message,
|
|
3086
|
+
context,
|
|
3087
|
+
httpStatus: getHttpStatus(code),
|
|
3088
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3013
3091
|
let payload;
|
|
3014
3092
|
try {
|
|
3015
3093
|
payload = JSON.parse(error.message);
|
|
@@ -3126,19 +3204,16 @@ var ContainerControlClient = class {
|
|
|
3126
3204
|
busyPollTimer = null;
|
|
3127
3205
|
/** Tracks whether we currently believe the session is busy. */
|
|
3128
3206
|
busy = false;
|
|
3129
|
-
/**
|
|
3130
|
-
* Set the first time the poller observes `conn.isConnected() === true`,
|
|
3131
|
-
* cleared in `destroyConnection()`. Lets us distinguish "the WebSocket
|
|
3132
|
-
* upgrade is still in progress" (don't tear down) from "we were
|
|
3133
|
-
* connected and the peer went away" (do tear down).
|
|
3134
|
-
*/
|
|
3135
|
-
wasEverConnected = false;
|
|
3136
3207
|
constructor(options) {
|
|
3137
3208
|
this.connOptions = {
|
|
3138
3209
|
stub: options.stub,
|
|
3139
3210
|
port: options.port,
|
|
3211
|
+
localMain: options.localMain,
|
|
3140
3212
|
logger: options.logger,
|
|
3141
|
-
retryTimeoutMs: options.retryTimeoutMs
|
|
3213
|
+
retryTimeoutMs: options.retryTimeoutMs,
|
|
3214
|
+
onClose: () => {
|
|
3215
|
+
if (this.conn) this.destroyConnection();
|
|
3216
|
+
}
|
|
3142
3217
|
};
|
|
3143
3218
|
this.idleDisconnectMs = options.idleDisconnectMs ?? DEFAULT_IDLE_DISCONNECT_MS;
|
|
3144
3219
|
this.busyPollIntervalMs = options.busyPollIntervalMs ?? BUSY_POLL_INTERVAL_MS;
|
|
@@ -3180,11 +3255,7 @@ var ContainerControlClient = class {
|
|
|
3180
3255
|
pollBusyState = () => {
|
|
3181
3256
|
const conn = this.conn;
|
|
3182
3257
|
if (!conn) return;
|
|
3183
|
-
if (!conn.isConnected())
|
|
3184
|
-
if (this.wasEverConnected) this.destroyConnection();
|
|
3185
|
-
return;
|
|
3186
|
-
}
|
|
3187
|
-
this.wasEverConnected = true;
|
|
3258
|
+
if (!conn.isConnected()) return;
|
|
3188
3259
|
const { imports, exports } = conn.getStats();
|
|
3189
3260
|
if (imports > IDLE_IMPORT_THRESHOLD || exports > IDLE_EXPORT_THRESHOLD) {
|
|
3190
3261
|
if (!this.busy) {
|
|
@@ -3217,7 +3288,7 @@ var ContainerControlClient = class {
|
|
|
3217
3288
|
if (!conn || !conn.isConnected()) return;
|
|
3218
3289
|
const { imports, exports } = conn.getStats();
|
|
3219
3290
|
if (imports <= IDLE_IMPORT_THRESHOLD && exports <= IDLE_EXPORT_THRESHOLD) {
|
|
3220
|
-
this.logger.debug("Disconnecting idle
|
|
3291
|
+
this.logger.debug("Disconnecting idle RPC connection");
|
|
3221
3292
|
this.destroyConnection();
|
|
3222
3293
|
}
|
|
3223
3294
|
}, this.idleDisconnectMs);
|
|
@@ -3239,7 +3310,6 @@ var ContainerControlClient = class {
|
|
|
3239
3310
|
this.conn.disconnect();
|
|
3240
3311
|
this.conn = null;
|
|
3241
3312
|
}
|
|
3242
|
-
this.wasEverConnected = false;
|
|
3243
3313
|
}
|
|
3244
3314
|
get commands() {
|
|
3245
3315
|
return wrapStub(this.getConnection().rpc().commands, this.renewActivity);
|
|
@@ -3263,11 +3333,37 @@ var ContainerControlClient = class {
|
|
|
3263
3333
|
return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
|
|
3264
3334
|
}
|
|
3265
3335
|
get desktop() {
|
|
3266
|
-
|
|
3336
|
+
const stub = wrapStub(this.getConnection().rpc().desktop, this.renewActivity);
|
|
3337
|
+
const wire = stub;
|
|
3338
|
+
return new Proxy(stub, { get(target, prop, receiver) {
|
|
3339
|
+
if (prop === "screenshot") return async (options) => {
|
|
3340
|
+
const { format, ...rest } = options ?? {};
|
|
3341
|
+
const result = await wire.screenshot(rest);
|
|
3342
|
+
return format === "bytes" ? {
|
|
3343
|
+
...result,
|
|
3344
|
+
data: base64ToBytes(result.data)
|
|
3345
|
+
} : result;
|
|
3346
|
+
};
|
|
3347
|
+
if (prop === "screenshotRegion") return async (region, options) => {
|
|
3348
|
+
const { format, ...rest } = options ?? {};
|
|
3349
|
+
const result = await wire.screenshotRegion({
|
|
3350
|
+
region,
|
|
3351
|
+
...rest
|
|
3352
|
+
});
|
|
3353
|
+
return format === "bytes" ? {
|
|
3354
|
+
...result,
|
|
3355
|
+
data: base64ToBytes(result.data)
|
|
3356
|
+
} : result;
|
|
3357
|
+
};
|
|
3358
|
+
return Reflect.get(target, prop, receiver);
|
|
3359
|
+
} });
|
|
3267
3360
|
}
|
|
3268
3361
|
get watch() {
|
|
3269
3362
|
return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
|
|
3270
3363
|
}
|
|
3364
|
+
get tunnels() {
|
|
3365
|
+
return wrapStub(this.getConnection().rpc().tunnels, this.renewActivity);
|
|
3366
|
+
}
|
|
3271
3367
|
get interpreter() {
|
|
3272
3368
|
return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
|
|
3273
3369
|
}
|
|
@@ -3808,6 +3904,13 @@ function resolveS3fsOptions(provider, userOptions) {
|
|
|
3808
3904
|
|
|
3809
3905
|
//#endregion
|
|
3810
3906
|
//#region src/storage-mount/validation.ts
|
|
3907
|
+
/**
|
|
3908
|
+
* Type guard for R2Bucket binding.
|
|
3909
|
+
* Checks for the minimal R2Bucket interface methods we use.
|
|
3910
|
+
*/
|
|
3911
|
+
function isR2Bucket(value) {
|
|
3912
|
+
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";
|
|
3913
|
+
}
|
|
3811
3914
|
function validatePrefix(prefix) {
|
|
3812
3915
|
if (!prefix.startsWith("/")) throw new InvalidMountConfigError(`Prefix must start with '/': "${prefix}"`);
|
|
3813
3916
|
}
|
|
@@ -3818,6 +3921,13 @@ function validateBucketName(bucket, mountPath) {
|
|
|
3818
3921
|
}
|
|
3819
3922
|
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.`);
|
|
3820
3923
|
}
|
|
3924
|
+
function validateBucketBindingName(bucketBinding, mountPath) {
|
|
3925
|
+
if (bucketBinding.includes(":")) {
|
|
3926
|
+
const [bucketName, prefixPart] = bucketBinding.split(":");
|
|
3927
|
+
throw new InvalidMountConfigError(`Bucket name cannot contain ':'. To mount a prefix, use the 'prefix' option:\n mountBucket('${bucketName}', '${mountPath}', { ...options, prefix: '${prefixPart}' })`);
|
|
3928
|
+
}
|
|
3929
|
+
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.`);
|
|
3930
|
+
}
|
|
3821
3931
|
/**
|
|
3822
3932
|
* Builds the s3fs source string from bucket name and optional prefix.
|
|
3823
3933
|
* Format: "bucket" or "bucket:/prefix/" for subdirectory mounts.
|
|
@@ -4142,7 +4252,7 @@ async function proxyTerminal(stub, sessionId, request, options) {
|
|
|
4142
4252
|
|
|
4143
4253
|
//#endregion
|
|
4144
4254
|
//#region src/request-handler.ts
|
|
4145
|
-
async function proxyToSandbox(request, env) {
|
|
4255
|
+
async function proxyToSandbox(request, env$1) {
|
|
4146
4256
|
const logger = createLogger({
|
|
4147
4257
|
component: "sandbox-do",
|
|
4148
4258
|
traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
|
|
@@ -4153,7 +4263,7 @@ async function proxyToSandbox(request, env) {
|
|
|
4153
4263
|
const routeInfo = extractSandboxRoute(url);
|
|
4154
4264
|
if (!routeInfo) return null;
|
|
4155
4265
|
const { sandboxId, port, path: path$1, token } = routeInfo;
|
|
4156
|
-
const sandbox = getSandbox(env.Sandbox, sandboxId, { normalizeId: true });
|
|
4266
|
+
const sandbox = getSandbox(env$1.Sandbox, sandboxId, { normalizeId: true });
|
|
4157
4267
|
if (port !== 3e3) {
|
|
4158
4268
|
if (!await sandbox.validatePortToken(port, token)) {
|
|
4159
4269
|
logger.warn("Invalid token access blocked", {
|
|
@@ -4239,6 +4349,513 @@ function isLocalhostPattern(hostname) {
|
|
|
4239
4349
|
return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
|
|
4240
4350
|
}
|
|
4241
4351
|
|
|
4352
|
+
//#endregion
|
|
4353
|
+
//#region src/storage-mount/r2-egress-handler.ts
|
|
4354
|
+
const XML_NS = "xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"";
|
|
4355
|
+
const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
|
4356
|
+
function escapeXML(s) {
|
|
4357
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4358
|
+
}
|
|
4359
|
+
function xmlResponse(body, status = 200) {
|
|
4360
|
+
return new Response(XML_DECL + body, {
|
|
4361
|
+
status,
|
|
4362
|
+
headers: { "Content-Type": "application/xml" }
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
function normalizeObjectKey(value) {
|
|
4366
|
+
return value.replace(/^\/+/, "");
|
|
4367
|
+
}
|
|
4368
|
+
function trimTrailingSlashes(s) {
|
|
4369
|
+
let end = s.length;
|
|
4370
|
+
while (end > 0 && s[end - 1] === "/") end--;
|
|
4371
|
+
return s.slice(0, end);
|
|
4372
|
+
}
|
|
4373
|
+
function parsePath(pathname) {
|
|
4374
|
+
const stripped = pathname.startsWith("/") ? pathname.slice(1) : pathname;
|
|
4375
|
+
if (!stripped) return null;
|
|
4376
|
+
const slash = stripped.indexOf("/");
|
|
4377
|
+
if (slash === -1) return {
|
|
4378
|
+
bucket: stripped,
|
|
4379
|
+
key: ""
|
|
4380
|
+
};
|
|
4381
|
+
return {
|
|
4382
|
+
bucket: stripped.slice(0, slash),
|
|
4383
|
+
key: normalizeObjectKey(stripped.slice(slash + 1))
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
function resolveR2Bucket(env$1, name) {
|
|
4387
|
+
if (typeof env$1 !== "object" || env$1 === null) return null;
|
|
4388
|
+
const val = env$1[name];
|
|
4389
|
+
return isR2Bucket(val) ? val : null;
|
|
4390
|
+
}
|
|
4391
|
+
function parseRange(header) {
|
|
4392
|
+
if (!header) return void 0;
|
|
4393
|
+
const m = header.match(/^bytes=(\d*)-(\d*)$/);
|
|
4394
|
+
if (!m) return void 0;
|
|
4395
|
+
const start = m[1] ? parseInt(m[1], 10) : void 0;
|
|
4396
|
+
const end = m[2] ? parseInt(m[2], 10) : void 0;
|
|
4397
|
+
if (start === void 0 && end !== void 0) return { suffix: end };
|
|
4398
|
+
if (start !== void 0 && end !== void 0) return {
|
|
4399
|
+
offset: start,
|
|
4400
|
+
length: end - start + 1
|
|
4401
|
+
};
|
|
4402
|
+
if (start !== void 0) return { offset: start };
|
|
4403
|
+
}
|
|
4404
|
+
function buildListObjectsV2Xml(bucketName, prefix, delimiter, maxKeys, result) {
|
|
4405
|
+
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("");
|
|
4406
|
+
const commonPrefixes = result.delimitedPrefixes.map((p) => `<CommonPrefixes><Prefix>${escapeXML(p)}</Prefix></CommonPrefixes>`).join("");
|
|
4407
|
+
const nextToken = result.truncated && result.cursor ? `<NextContinuationToken>${escapeXML(result.cursor)}</NextContinuationToken>` : "";
|
|
4408
|
+
const keyCount = result.objects.length + result.delimitedPrefixes.length;
|
|
4409
|
+
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>`;
|
|
4410
|
+
}
|
|
4411
|
+
function buildLocationXml() {
|
|
4412
|
+
return `<LocationConstraint ${XML_NS}/>`;
|
|
4413
|
+
}
|
|
4414
|
+
function buildInitiateMultipartUploadXml(bucketName, key, uploadId) {
|
|
4415
|
+
return `<InitiateMultipartUploadResult ${XML_NS}><Bucket>${escapeXML(bucketName)}</Bucket><Key>${escapeXML(key)}</Key><UploadId>${escapeXML(uploadId)}</UploadId></InitiateMultipartUploadResult>`;
|
|
4416
|
+
}
|
|
4417
|
+
function buildCompleteMultipartUploadXml(bucketName, key, etag) {
|
|
4418
|
+
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>`;
|
|
4419
|
+
}
|
|
4420
|
+
function buildCopyObjectXml(etag, uploaded) {
|
|
4421
|
+
return `<CopyObjectResult ${XML_NS}><LastModified>${uploaded.toISOString()}</LastModified><ETag>${escapeXML(etag)}</ETag></CopyObjectResult>`;
|
|
4422
|
+
}
|
|
4423
|
+
function extractXmlTagContent(segment, tagName) {
|
|
4424
|
+
const openTag = `<${tagName}>`;
|
|
4425
|
+
const closeTag = `</${tagName}>`;
|
|
4426
|
+
const start = segment.indexOf(openTag);
|
|
4427
|
+
if (start === -1) return null;
|
|
4428
|
+
const contentStart = start + openTag.length;
|
|
4429
|
+
const end = segment.indexOf(closeTag, contentStart);
|
|
4430
|
+
if (end === -1) return null;
|
|
4431
|
+
return segment.slice(contentStart, end);
|
|
4432
|
+
}
|
|
4433
|
+
function parseCompleteMultipartUploadBody(body) {
|
|
4434
|
+
const parts = [];
|
|
4435
|
+
let pos = 0;
|
|
4436
|
+
while (pos < body.length) {
|
|
4437
|
+
const start = body.indexOf("<Part>", pos);
|
|
4438
|
+
if (start === -1) break;
|
|
4439
|
+
const end = body.indexOf("</Part>", start + 6);
|
|
4440
|
+
if (end === -1) break;
|
|
4441
|
+
const segment = body.slice(start, end + 7);
|
|
4442
|
+
pos = end + 7;
|
|
4443
|
+
const partNumberText = extractXmlTagContent(segment, "PartNumber");
|
|
4444
|
+
const etagText = extractXmlTagContent(segment, "ETag");
|
|
4445
|
+
const partNumber = partNumberText ? parseInt(partNumberText, 10) : NaN;
|
|
4446
|
+
if (Number.isFinite(partNumber) && etagText) parts.push({
|
|
4447
|
+
partNumber,
|
|
4448
|
+
etag: etagText.replace(/^"|"$/g, "")
|
|
4449
|
+
});
|
|
4450
|
+
}
|
|
4451
|
+
return parts;
|
|
4452
|
+
}
|
|
4453
|
+
function buildResponseHeaders(obj) {
|
|
4454
|
+
const headers = new Headers();
|
|
4455
|
+
headers.set("ETag", obj.httpEtag);
|
|
4456
|
+
headers.set("Content-Length", String(obj.size));
|
|
4457
|
+
headers.set("Last-Modified", obj.uploaded.toUTCString());
|
|
4458
|
+
headers.set("Accept-Ranges", "bytes");
|
|
4459
|
+
if (obj.httpMetadata?.contentType) headers.set("Content-Type", obj.httpMetadata.contentType);
|
|
4460
|
+
if (obj.httpMetadata?.contentDisposition) headers.set("Content-Disposition", obj.httpMetadata.contentDisposition);
|
|
4461
|
+
if (obj.httpMetadata?.contentEncoding) headers.set("Content-Encoding", obj.httpMetadata.contentEncoding);
|
|
4462
|
+
if (obj.httpMetadata?.contentLanguage) headers.set("Content-Language", obj.httpMetadata.contentLanguage);
|
|
4463
|
+
if (obj.httpMetadata?.cacheControl) headers.set("Cache-Control", obj.httpMetadata.cacheControl);
|
|
4464
|
+
return headers;
|
|
4465
|
+
}
|
|
4466
|
+
function buildContentRange(range, totalSize) {
|
|
4467
|
+
if ("suffix" in range) return `bytes ${Math.max(0, totalSize - range.suffix)}-${totalSize - 1}/${totalSize}`;
|
|
4468
|
+
const start = range.offset ?? 0;
|
|
4469
|
+
return `bytes ${start}-${range.length !== void 0 ? start + range.length - 1 : totalSize - 1}/${totalSize}`;
|
|
4470
|
+
}
|
|
4471
|
+
function getRangeContentLength(range, totalSize) {
|
|
4472
|
+
if ("suffix" in range) return Math.min(range.suffix, totalSize);
|
|
4473
|
+
const start = range.offset ?? 0;
|
|
4474
|
+
if (start >= totalSize) return 0;
|
|
4475
|
+
const requestedLength = range.length !== void 0 ? range.length : totalSize - start;
|
|
4476
|
+
return Math.min(requestedLength, totalSize - start);
|
|
4477
|
+
}
|
|
4478
|
+
function extractHttpMetadata(request) {
|
|
4479
|
+
const meta = {};
|
|
4480
|
+
const ct = request.headers.get("Content-Type");
|
|
4481
|
+
if (ct) meta.contentType = ct;
|
|
4482
|
+
const cd = request.headers.get("Content-Disposition");
|
|
4483
|
+
if (cd) meta.contentDisposition = cd;
|
|
4484
|
+
const ce = request.headers.get("Content-Encoding");
|
|
4485
|
+
if (ce) meta.contentEncoding = ce;
|
|
4486
|
+
const cl = request.headers.get("Content-Language");
|
|
4487
|
+
if (cl) meta.contentLanguage = cl;
|
|
4488
|
+
const cc = request.headers.get("Cache-Control");
|
|
4489
|
+
if (cc) meta.cacheControl = cc;
|
|
4490
|
+
return meta;
|
|
4491
|
+
}
|
|
4492
|
+
function parseCopySource(header) {
|
|
4493
|
+
const sourcePath = header.split("?")[0] ?? "";
|
|
4494
|
+
if (!sourcePath) return null;
|
|
4495
|
+
const decoded = decodeURIComponent(sourcePath);
|
|
4496
|
+
const parsed = parsePath(decoded.startsWith("/") ? decoded : `/${decoded}`);
|
|
4497
|
+
return parsed ? {
|
|
4498
|
+
bucket: parsed.bucket,
|
|
4499
|
+
key: normalizeObjectKey(parsed.key)
|
|
4500
|
+
} : null;
|
|
4501
|
+
}
|
|
4502
|
+
function normalizeStorageClass(storageClass) {
|
|
4503
|
+
if (storageClass === "Standard" || storageClass === "InfrequentAccess") return storageClass;
|
|
4504
|
+
}
|
|
4505
|
+
async function putRequestBody(r2, key, request, options) {
|
|
4506
|
+
const contentLength = request.headers.get("Content-Length");
|
|
4507
|
+
const length = contentLength ? Number.parseInt(contentLength, 10) : NaN;
|
|
4508
|
+
if (!Number.isFinite(length) || length < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
|
|
4509
|
+
if (length === 0) return r2.put(key, new Uint8Array(0), options);
|
|
4510
|
+
if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
|
|
4511
|
+
const { readable, writable } = new FixedLengthStream(length);
|
|
4512
|
+
const pipe = request.body.pipeTo(writable);
|
|
4513
|
+
const result = await r2.put(key, readable, options);
|
|
4514
|
+
await pipe;
|
|
4515
|
+
return result;
|
|
4516
|
+
}
|
|
4517
|
+
async function handleListObjects(r2, bucketName, url, mountPrefix) {
|
|
4518
|
+
const queryPrefix = normalizeObjectKey(url.searchParams.get("prefix") ?? "");
|
|
4519
|
+
const delimiter = url.searchParams.get("delimiter") ?? "";
|
|
4520
|
+
const maxKeys = Math.min(parseInt(url.searchParams.get("max-keys") ?? "1000", 10) || 1e3, 1e3);
|
|
4521
|
+
const continuationToken = url.searchParams.get("continuation-token") ?? void 0;
|
|
4522
|
+
const listOpts = {
|
|
4523
|
+
prefix: (mountPrefix ? `${mountPrefix}/${queryPrefix}` : queryPrefix) || void 0,
|
|
4524
|
+
delimiter: delimiter || void 0,
|
|
4525
|
+
limit: maxKeys,
|
|
4526
|
+
cursor: continuationToken
|
|
4527
|
+
};
|
|
4528
|
+
const result = await r2.list(listOpts);
|
|
4529
|
+
const stripKey = mountPrefix ? (k) => k.startsWith(`${mountPrefix}/`) ? k.slice(mountPrefix.length + 1) : k : (k) => k;
|
|
4530
|
+
return xmlResponse(buildListObjectsV2Xml(bucketName, queryPrefix, delimiter, maxKeys, {
|
|
4531
|
+
objects: result.objects.map((obj) => ({
|
|
4532
|
+
key: stripKey(obj.key),
|
|
4533
|
+
uploaded: obj.uploaded,
|
|
4534
|
+
httpEtag: obj.httpEtag,
|
|
4535
|
+
size: obj.size
|
|
4536
|
+
})),
|
|
4537
|
+
delimitedPrefixes: result.delimitedPrefixes.map(stripKey),
|
|
4538
|
+
truncated: result.truncated,
|
|
4539
|
+
cursor: result.truncated ? result.cursor : void 0
|
|
4540
|
+
}));
|
|
4541
|
+
}
|
|
4542
|
+
async function handleHeadObject(r2, key) {
|
|
4543
|
+
const obj = await r2.head(key);
|
|
4544
|
+
if (!obj) return new Response(null, { status: 404 });
|
|
4545
|
+
return new Response(null, {
|
|
4546
|
+
status: 200,
|
|
4547
|
+
headers: buildResponseHeaders(obj)
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
async function handleGetObject(r2, key, request) {
|
|
4551
|
+
const range = parseRange(request.headers.get("Range"));
|
|
4552
|
+
if (!range) {
|
|
4553
|
+
const obj = await r2.get(key);
|
|
4554
|
+
if (!obj) return new Response(null, { status: 404 });
|
|
4555
|
+
return new Response(obj.body, {
|
|
4556
|
+
status: 200,
|
|
4557
|
+
headers: buildResponseHeaders(obj)
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
const [headObj, rangeObj] = await Promise.all([r2.head(key), r2.get(key, { range })]);
|
|
4561
|
+
if (!headObj || !rangeObj) return new Response(null, { status: 404 });
|
|
4562
|
+
const headers = buildResponseHeaders(rangeObj);
|
|
4563
|
+
headers.set("Content-Range", buildContentRange(range, headObj.size));
|
|
4564
|
+
headers.set("Content-Length", String(getRangeContentLength(range, headObj.size)));
|
|
4565
|
+
return new Response(rangeObj.body, {
|
|
4566
|
+
status: 206,
|
|
4567
|
+
headers
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
async function handlePutObject(r2, bucketName, key, request, env$1, permitted, mountPrefix) {
|
|
4571
|
+
const copySourceHeader = request.headers.get("x-amz-copy-source");
|
|
4572
|
+
if (copySourceHeader) {
|
|
4573
|
+
const copySource = parseCopySource(copySourceHeader);
|
|
4574
|
+
if (!copySource || !copySource.key) return new Response("Bad Request: invalid x-amz-copy-source", { status: 400 });
|
|
4575
|
+
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 });
|
|
4576
|
+
const sourceBucket = copySource.bucket === bucketName ? r2 : resolveR2Bucket(env$1, copySource.bucket);
|
|
4577
|
+
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 });
|
|
4578
|
+
const sourceKey = mountPrefix && copySource.bucket === bucketName ? `${mountPrefix}/${copySource.key}` : copySource.key;
|
|
4579
|
+
const sourceObject = await sourceBucket.get(sourceKey);
|
|
4580
|
+
if (!sourceObject) return new Response(null, { status: 404 });
|
|
4581
|
+
const httpMetadata = request.headers.get("x-amz-metadata-directive")?.toUpperCase() === "REPLACE" ? extractHttpMetadata(request) : sourceObject.httpMetadata;
|
|
4582
|
+
const result$1 = await r2.put(key, sourceObject.body, {
|
|
4583
|
+
httpMetadata,
|
|
4584
|
+
customMetadata: sourceObject.customMetadata,
|
|
4585
|
+
storageClass: normalizeStorageClass(sourceObject.storageClass)
|
|
4586
|
+
});
|
|
4587
|
+
return xmlResponse(buildCopyObjectXml(result$1.httpEtag, result$1.uploaded));
|
|
4588
|
+
}
|
|
4589
|
+
const result = await putRequestBody(r2, key, request, { httpMetadata: extractHttpMetadata(request) });
|
|
4590
|
+
if (result instanceof Response) return result;
|
|
4591
|
+
const headers = new Headers();
|
|
4592
|
+
headers.set("ETag", result.httpEtag);
|
|
4593
|
+
return new Response(null, {
|
|
4594
|
+
status: 200,
|
|
4595
|
+
headers
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
async function handleDeleteObject(r2, key) {
|
|
4599
|
+
await r2.delete(key);
|
|
4600
|
+
return new Response(null, { status: 204 });
|
|
4601
|
+
}
|
|
4602
|
+
async function handleCreateMultipartUpload(r2, bucketName, key, request) {
|
|
4603
|
+
const httpMetadata = extractHttpMetadata(request);
|
|
4604
|
+
return xmlResponse(buildInitiateMultipartUploadXml(bucketName, key, (await r2.createMultipartUpload(key, { httpMetadata })).uploadId));
|
|
4605
|
+
}
|
|
4606
|
+
async function handleUploadPart(r2, key, url, request) {
|
|
4607
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4608
|
+
const partNumber = parseInt(url.searchParams.get("partNumber") ?? "0", 10);
|
|
4609
|
+
if (!uploadId || !partNumber) return new Response("Bad Request: missing uploadId or partNumber", { status: 400 });
|
|
4610
|
+
if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
|
|
4611
|
+
const contentLength = request.headers.get("Content-Length");
|
|
4612
|
+
const partLength = contentLength ? Number.parseInt(contentLength, 10) : NaN;
|
|
4613
|
+
if (!Number.isFinite(partLength) || partLength < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
|
|
4614
|
+
const upload = r2.resumeMultipartUpload(key, uploadId);
|
|
4615
|
+
let part;
|
|
4616
|
+
if (partLength === 0) part = await upload.uploadPart(partNumber, new Uint8Array(0));
|
|
4617
|
+
else {
|
|
4618
|
+
const { readable, writable } = new FixedLengthStream(partLength);
|
|
4619
|
+
const pipe = request.body.pipeTo(writable);
|
|
4620
|
+
part = await upload.uploadPart(partNumber, readable);
|
|
4621
|
+
await pipe;
|
|
4622
|
+
}
|
|
4623
|
+
const headers = new Headers();
|
|
4624
|
+
headers.set("ETag", `"${part.etag}"`);
|
|
4625
|
+
return new Response(null, {
|
|
4626
|
+
status: 200,
|
|
4627
|
+
headers
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
async function handleCompleteMultipartUpload(r2, bucketName, key, url, request) {
|
|
4631
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4632
|
+
if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
|
|
4633
|
+
const r2Parts = parseCompleteMultipartUploadBody(await request.text()).map((p) => ({
|
|
4634
|
+
partNumber: p.partNumber,
|
|
4635
|
+
etag: p.etag
|
|
4636
|
+
}));
|
|
4637
|
+
return xmlResponse(buildCompleteMultipartUploadXml(bucketName, key, (await r2.resumeMultipartUpload(key, uploadId).complete(r2Parts)).httpEtag));
|
|
4638
|
+
}
|
|
4639
|
+
async function handleAbortMultipartUpload(r2, key, url) {
|
|
4640
|
+
const uploadId = url.searchParams.get("uploadId") ?? "";
|
|
4641
|
+
if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
|
|
4642
|
+
await r2.resumeMultipartUpload(key, uploadId).abort();
|
|
4643
|
+
return new Response(null, { status: 204 });
|
|
4644
|
+
}
|
|
4645
|
+
const r2EgressHandler = async (request, env$1, ctx) => {
|
|
4646
|
+
const url = new URL(request.url);
|
|
4647
|
+
const parsed = parsePath(url.pathname);
|
|
4648
|
+
if (!parsed) return new Response("Bad Request: empty path", { status: 400 });
|
|
4649
|
+
const { bucket: bucketName, key } = parsed;
|
|
4650
|
+
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 });
|
|
4651
|
+
const bucketParams = ctx.params.buckets[bucketName];
|
|
4652
|
+
const rawPrefix = bucketParams.prefix;
|
|
4653
|
+
const mountPrefix = rawPrefix ? trimTrailingSlashes(normalizeObjectKey(rawPrefix)) : void 0;
|
|
4654
|
+
const readOnly = bucketParams.readOnly ?? false;
|
|
4655
|
+
const r2 = resolveR2Bucket(env$1, bucketName);
|
|
4656
|
+
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 });
|
|
4657
|
+
const { method } = request;
|
|
4658
|
+
if (!key) {
|
|
4659
|
+
if (method === "GET" && url.searchParams.has("location")) return xmlResponse(buildLocationXml());
|
|
4660
|
+
if (method === "GET" && url.searchParams.get("list-type") === "2") return handleListObjects(r2, bucketName, url, mountPrefix);
|
|
4661
|
+
if (method === "GET") return handleListObjects(r2, bucketName, url, mountPrefix);
|
|
4662
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
4663
|
+
}
|
|
4664
|
+
const fullKey = mountPrefix ? `${mountPrefix}/${key}` : key;
|
|
4665
|
+
const permitted = new Set(Object.keys(ctx.params.buckets));
|
|
4666
|
+
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 });
|
|
4667
|
+
if (method === "POST" && url.searchParams.has("uploads")) return handleCreateMultipartUpload(r2, bucketName, fullKey, request);
|
|
4668
|
+
if (method === "POST" && url.searchParams.has("uploadId")) return handleCompleteMultipartUpload(r2, bucketName, fullKey, url, request);
|
|
4669
|
+
if (method === "PUT" && url.searchParams.has("partNumber") && url.searchParams.has("uploadId")) return handleUploadPart(r2, fullKey, url, request);
|
|
4670
|
+
if (method === "DELETE" && url.searchParams.has("uploadId")) return handleAbortMultipartUpload(r2, fullKey, url);
|
|
4671
|
+
switch (method) {
|
|
4672
|
+
case "HEAD": return handleHeadObject(r2, fullKey);
|
|
4673
|
+
case "GET": return handleGetObject(r2, fullKey, request);
|
|
4674
|
+
case "PUT": return handlePutObject(r2, bucketName, fullKey, request, env$1, permitted, mountPrefix);
|
|
4675
|
+
case "DELETE": return handleDeleteObject(r2, fullKey);
|
|
4676
|
+
default: return new Response("Method Not Allowed", { status: 405 });
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
|
|
4680
|
+
//#endregion
|
|
4681
|
+
//#region src/tunnels/sandbox-control-callback.ts
|
|
4682
|
+
var SandboxControlCallbackImpl = class extends RpcTarget {
|
|
4683
|
+
constructor(getHandler, logger) {
|
|
4684
|
+
super();
|
|
4685
|
+
this.getHandler = getHandler;
|
|
4686
|
+
this.logger = logger;
|
|
4687
|
+
}
|
|
4688
|
+
async onTunnelExit(id, port, exitCode) {
|
|
4689
|
+
const handler = this.getHandler();
|
|
4690
|
+
if (!handler) {
|
|
4691
|
+
this.logger.debug("onTunnelExit: no handler bound; ignoring", {
|
|
4692
|
+
id,
|
|
4693
|
+
port,
|
|
4694
|
+
exitCode
|
|
4695
|
+
});
|
|
4696
|
+
return;
|
|
4697
|
+
}
|
|
4698
|
+
await handler(id, port, exitCode);
|
|
4699
|
+
}
|
|
4700
|
+
};
|
|
4701
|
+
|
|
4702
|
+
//#endregion
|
|
4703
|
+
//#region src/tunnels/tunnels-handler.ts
|
|
4704
|
+
/**
|
|
4705
|
+
* Tunnels namespace handler. Created once per Sandbox DO instance via
|
|
4706
|
+
* `createTunnelsHandler(host)` and exposed as `sandbox.tunnels`.
|
|
4707
|
+
*
|
|
4708
|
+
* Storage is the source of truth. The DO holds a `Record<portString, TunnelInfo>`
|
|
4709
|
+
* under the `tunnels` storage key. `Sandbox.onStart()` clears the key on every
|
|
4710
|
+
* container restart so any record in storage is by construction backed by a
|
|
4711
|
+
* running `cloudflared` process; the handler never needs to verify that
|
|
4712
|
+
* separately against the container.
|
|
4713
|
+
*/
|
|
4714
|
+
/** DO storage key for the `port → TunnelInfo` map. */
|
|
4715
|
+
const STORAGE_KEY = "tunnels";
|
|
4716
|
+
function validateTunnelPort(port) {
|
|
4717
|
+
if (!validatePort(port)) throw new SandboxSecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding reserved ports.`);
|
|
4718
|
+
}
|
|
4719
|
+
/** 8-char hex id derived from `crypto.getRandomValues`. Unique per sandbox. */
|
|
4720
|
+
function shortId() {
|
|
4721
|
+
const buf = new Uint8Array(4);
|
|
4722
|
+
crypto.getRandomValues(buf);
|
|
4723
|
+
return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4724
|
+
}
|
|
4725
|
+
function isTunnelNotFoundError(error) {
|
|
4726
|
+
return (error instanceof Error ? error.message : String(error)).includes("TUNNEL_NOT_FOUND");
|
|
4727
|
+
}
|
|
4728
|
+
async function readMap(storage) {
|
|
4729
|
+
return await storage.get(STORAGE_KEY) ?? {};
|
|
4730
|
+
}
|
|
4731
|
+
/**
|
|
4732
|
+
* Concrete `TunnelsHandler` implementation. Extends `RpcTarget` so it
|
|
4733
|
+
* can cross the Workers RPC boundary: the Sandbox DO is reachable from
|
|
4734
|
+
* Workers via Workers RPC (`stub.tunnels.get(port)`), and only
|
|
4735
|
+
* `RpcTarget` instances are passed by reference across that boundary.
|
|
4736
|
+
*/
|
|
4737
|
+
var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
4738
|
+
#host;
|
|
4739
|
+
#withPortLock;
|
|
4740
|
+
constructor(host, withPortLock) {
|
|
4741
|
+
super();
|
|
4742
|
+
this.#host = host;
|
|
4743
|
+
this.#withPortLock = withPortLock;
|
|
4744
|
+
}
|
|
4745
|
+
async get(port) {
|
|
4746
|
+
const startTime = Date.now();
|
|
4747
|
+
let outcome = "error";
|
|
4748
|
+
let cacheState = "miss";
|
|
4749
|
+
let caughtError;
|
|
4750
|
+
try {
|
|
4751
|
+
validateTunnelPort(port);
|
|
4752
|
+
const info = await this.#withPortLock(port, async () => {
|
|
4753
|
+
const existing = (await readMap(this.#host.storage))[port.toString()];
|
|
4754
|
+
if (existing) {
|
|
4755
|
+
cacheState = "hit";
|
|
4756
|
+
return existing;
|
|
4757
|
+
}
|
|
4758
|
+
const id = `quick-${shortId()}`;
|
|
4759
|
+
const spawned = await this.#host.client.tunnels.runQuickTunnel(id, port);
|
|
4760
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
4761
|
+
const nextMap = await readMap(txn);
|
|
4762
|
+
nextMap[port.toString()] = spawned;
|
|
4763
|
+
await txn.put(STORAGE_KEY, nextMap);
|
|
4764
|
+
});
|
|
4765
|
+
return spawned;
|
|
4766
|
+
});
|
|
4767
|
+
outcome = "success";
|
|
4768
|
+
return info;
|
|
4769
|
+
} catch (error) {
|
|
4770
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
4771
|
+
throw error;
|
|
4772
|
+
} finally {
|
|
4773
|
+
logCanonicalEvent(this.#host.logger, {
|
|
4774
|
+
event: "tunnel.get",
|
|
4775
|
+
outcome,
|
|
4776
|
+
port,
|
|
4777
|
+
cacheState,
|
|
4778
|
+
durationMs: Date.now() - startTime,
|
|
4779
|
+
error: caughtError
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
async destroy(portOrInfo) {
|
|
4784
|
+
const port = typeof portOrInfo === "number" ? portOrInfo : portOrInfo.port;
|
|
4785
|
+
const startTime = Date.now();
|
|
4786
|
+
let outcome = "error";
|
|
4787
|
+
let caughtError;
|
|
4788
|
+
let tunnelId;
|
|
4789
|
+
try {
|
|
4790
|
+
await this.#withPortLock(port, async () => {
|
|
4791
|
+
const existing = (await readMap(this.#host.storage))[port.toString()];
|
|
4792
|
+
if (!existing) return;
|
|
4793
|
+
tunnelId = existing.id;
|
|
4794
|
+
await this.#host.storage.transaction(async (txn) => {
|
|
4795
|
+
const current = await readMap(txn);
|
|
4796
|
+
delete current[port.toString()];
|
|
4797
|
+
await txn.put(STORAGE_KEY, current);
|
|
4798
|
+
});
|
|
4799
|
+
try {
|
|
4800
|
+
await this.#host.client.tunnels.destroyTunnel(existing.id);
|
|
4801
|
+
} catch (error) {
|
|
4802
|
+
if (!isTunnelNotFoundError(error)) throw error;
|
|
4803
|
+
}
|
|
4804
|
+
});
|
|
4805
|
+
outcome = "success";
|
|
4806
|
+
} catch (error) {
|
|
4807
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
4808
|
+
throw error;
|
|
4809
|
+
} finally {
|
|
4810
|
+
logCanonicalEvent(this.#host.logger, {
|
|
4811
|
+
event: "tunnel.destroy",
|
|
4812
|
+
outcome,
|
|
4813
|
+
port,
|
|
4814
|
+
tunnelId,
|
|
4815
|
+
durationMs: Date.now() - startTime,
|
|
4816
|
+
error: caughtError
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
async list() {
|
|
4821
|
+
const map = await readMap(this.#host.storage);
|
|
4822
|
+
return Object.values(map);
|
|
4823
|
+
}
|
|
4824
|
+
};
|
|
4825
|
+
function createTunnelsHandler(host) {
|
|
4826
|
+
const portLocks = /* @__PURE__ */ new Map();
|
|
4827
|
+
const withPortLock = (port, fn) => {
|
|
4828
|
+
const next = (portLocks.get(port) ?? Promise.resolve()).then(fn, fn);
|
|
4829
|
+
portLocks.set(port, next.catch(() => void 0));
|
|
4830
|
+
return next;
|
|
4831
|
+
};
|
|
4832
|
+
const tunnels = new TunnelsRpcTarget(host, withPortLock);
|
|
4833
|
+
const handleTunnelExit = async (id, port, exitCode) => {
|
|
4834
|
+
const startTime = Date.now();
|
|
4835
|
+
await withPortLock(port, async () => {
|
|
4836
|
+
await host.storage.transaction(async (txn) => {
|
|
4837
|
+
const map = await readMap(txn);
|
|
4838
|
+
if (map[port.toString()]?.id === id) {
|
|
4839
|
+
delete map[port.toString()];
|
|
4840
|
+
await txn.put(STORAGE_KEY, map);
|
|
4841
|
+
}
|
|
4842
|
+
});
|
|
4843
|
+
logCanonicalEvent(host.logger, {
|
|
4844
|
+
event: "tunnel.exit",
|
|
4845
|
+
outcome: "success",
|
|
4846
|
+
port,
|
|
4847
|
+
tunnelId: id,
|
|
4848
|
+
exitCode: exitCode ?? void 0,
|
|
4849
|
+
durationMs: Date.now() - startTime
|
|
4850
|
+
});
|
|
4851
|
+
});
|
|
4852
|
+
};
|
|
4853
|
+
return {
|
|
4854
|
+
tunnels,
|
|
4855
|
+
handleTunnelExit
|
|
4856
|
+
};
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4242
4859
|
//#endregion
|
|
4243
4860
|
//#region src/version.ts
|
|
4244
4861
|
/**
|
|
@@ -4246,11 +4863,23 @@ function isLocalhostPattern(hostname) {
|
|
|
4246
4863
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
4247
4864
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
4248
4865
|
*/
|
|
4249
|
-
const SDK_VERSION = "0.10.
|
|
4866
|
+
const SDK_VERSION = "0.10.3";
|
|
4250
4867
|
|
|
4251
4868
|
//#endregion
|
|
4252
4869
|
//#region src/sandbox.ts
|
|
4870
|
+
const R2_EGRESS_PROXY_TARGET_CLASS_NAME = "CloudflareSandboxR2EgressProxyTarget";
|
|
4871
|
+
var R2EgressProxyTarget = class extends Container {};
|
|
4872
|
+
Object.defineProperty(R2EgressProxyTarget, "name", { value: R2_EGRESS_PROXY_TARGET_CLASS_NAME });
|
|
4873
|
+
R2EgressProxyTarget.outboundHandlers = { r2EgressMount: r2EgressHandler };
|
|
4874
|
+
function isFetcher(value) {
|
|
4875
|
+
return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
|
|
4876
|
+
}
|
|
4253
4877
|
const sandboxConfigurationCache = /* @__PURE__ */ new WeakMap();
|
|
4878
|
+
const R2_DEFAULT_S3FS_OPTIONS = {
|
|
4879
|
+
stat_cache_expire: "60",
|
|
4880
|
+
enable_noobj_cache: true,
|
|
4881
|
+
multipart_size: "5"
|
|
4882
|
+
};
|
|
4254
4883
|
const BACKUP_DEFAULT_TTL_SECONDS = 259200;
|
|
4255
4884
|
const BACKUP_MAX_NAME_LENGTH = 256;
|
|
4256
4885
|
const BACKUP_CONTAINER_DIR = "/var/backups";
|
|
@@ -4383,19 +5012,72 @@ function getSandbox(ns, id, options) {
|
|
|
4383
5012
|
});
|
|
4384
5013
|
}
|
|
4385
5014
|
const defaultSessionId = `sandbox-${effectiveId}`;
|
|
5015
|
+
const useDefaultSession = options?.enableDefaultSession !== false;
|
|
4386
5016
|
const enhancedMethods = {
|
|
4387
5017
|
fetch: (request) => stub.fetch(request),
|
|
5018
|
+
exec: (command, execOptions) => useDefaultSession ? stub.exec(command, execOptions) : stub.execWithSessionToken(command, DISABLE_SESSION_TOKEN, execOptions),
|
|
5019
|
+
startProcess: (command, processOptions) => useDefaultSession || processOptions?.sessionId !== void 0 ? stub.startProcess(command, processOptions) : stub.startProcess(command, {
|
|
5020
|
+
...processOptions,
|
|
5021
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5022
|
+
}),
|
|
5023
|
+
listProcesses: (sessionId) => useDefaultSession || sessionId !== void 0 ? stub.listProcesses(sessionId) : stub.listProcesses(DISABLE_SESSION_TOKEN),
|
|
5024
|
+
getProcess: (id$1, sessionId) => useDefaultSession || sessionId !== void 0 ? stub.getProcess(id$1, sessionId) : stub.getProcess(id$1, DISABLE_SESSION_TOKEN),
|
|
5025
|
+
execStream: (command, streamOptions) => {
|
|
5026
|
+
if (useDefaultSession || streamOptions?.sessionId !== void 0) return stub.execStream(command, streamOptions);
|
|
5027
|
+
return stub.execStreamWithSessionToken(command, DISABLE_SESSION_TOKEN, streamOptions);
|
|
5028
|
+
},
|
|
5029
|
+
writeFile: (path$1, content, fileOptions = {}) => useDefaultSession || fileOptions.sessionId !== void 0 ? stub.writeFile(path$1, content, fileOptions) : stub.writeFile(path$1, content, {
|
|
5030
|
+
...fileOptions,
|
|
5031
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5032
|
+
}),
|
|
5033
|
+
readFile: (path$1, fileOptions = {}) => {
|
|
5034
|
+
const options$1 = useDefaultSession || fileOptions.sessionId !== void 0 ? fileOptions : {
|
|
5035
|
+
...fileOptions,
|
|
5036
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5037
|
+
};
|
|
5038
|
+
if (options$1.encoding === "none") return stub.readFile(path$1, options$1);
|
|
5039
|
+
return stub.readFile(path$1, options$1);
|
|
5040
|
+
},
|
|
5041
|
+
readFileStream: (path$1, fileOptions = {}) => useDefaultSession || fileOptions.sessionId !== void 0 ? stub.readFileStream(path$1, fileOptions) : stub.readFileStream(path$1, { sessionId: DISABLE_SESSION_TOKEN }),
|
|
5042
|
+
mkdir: (path$1, mkdirOptions = {}) => useDefaultSession || mkdirOptions.sessionId !== void 0 ? stub.mkdir(path$1, mkdirOptions) : stub.mkdir(path$1, {
|
|
5043
|
+
...mkdirOptions,
|
|
5044
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5045
|
+
}),
|
|
5046
|
+
deleteFile: (path$1) => useDefaultSession ? stub.deleteFile(path$1) : stub.deleteFile(path$1, DISABLE_SESSION_TOKEN),
|
|
5047
|
+
renameFile: (oldPath, newPath) => useDefaultSession ? stub.renameFile(oldPath, newPath) : stub.renameFile(oldPath, newPath, DISABLE_SESSION_TOKEN),
|
|
5048
|
+
moveFile: (sourcePath, destinationPath) => useDefaultSession ? stub.moveFile(sourcePath, destinationPath) : stub.moveFile(sourcePath, destinationPath, DISABLE_SESSION_TOKEN),
|
|
5049
|
+
listFiles: (path$1, listOptions) => useDefaultSession || listOptions?.sessionId !== void 0 ? stub.listFiles(path$1, listOptions) : stub.listFiles(path$1, {
|
|
5050
|
+
...listOptions,
|
|
5051
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5052
|
+
}),
|
|
5053
|
+
exists: (path$1, sessionId) => useDefaultSession || sessionId !== void 0 ? stub.exists(path$1, sessionId) : stub.exists(path$1, DISABLE_SESSION_TOKEN),
|
|
5054
|
+
gitCheckout: (repoUrl, gitOptions) => useDefaultSession || gitOptions?.sessionId !== void 0 ? stub.gitCheckout(repoUrl, gitOptions) : stub.gitCheckout(repoUrl, {
|
|
5055
|
+
...gitOptions,
|
|
5056
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5057
|
+
}),
|
|
4388
5058
|
createSession: async (opts) => {
|
|
4389
5059
|
return enhanceSession(stub, await stub.createSession(opts));
|
|
4390
5060
|
},
|
|
4391
5061
|
getSession: async (sessionId) => {
|
|
4392
5062
|
return enhanceSession(stub, await stub.getSession(sessionId));
|
|
4393
5063
|
},
|
|
5064
|
+
watch: (path$1, options$1 = {}) => useDefaultSession || options$1.sessionId !== void 0 ? stub.watch(path$1, options$1) : stub.watch(path$1, {
|
|
5065
|
+
...options$1,
|
|
5066
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5067
|
+
}),
|
|
5068
|
+
checkChanges: (path$1, options$1 = {}) => useDefaultSession || options$1.sessionId !== void 0 ? stub.checkChanges(path$1, options$1) : stub.checkChanges(path$1, {
|
|
5069
|
+
...options$1,
|
|
5070
|
+
sessionId: DISABLE_SESSION_TOKEN
|
|
5071
|
+
}),
|
|
4394
5072
|
terminal: (request, opts) => proxyTerminal(stub, defaultSessionId, request, opts),
|
|
4395
5073
|
wsConnect: connect(stub),
|
|
4396
5074
|
desktop: new Proxy({}, { get(_, method) {
|
|
4397
5075
|
if (typeof method !== "string" || method === "then") return void 0;
|
|
4398
5076
|
return (...args) => stub.callDesktop(method, args);
|
|
5077
|
+
} }),
|
|
5078
|
+
tunnels: new Proxy({}, { get: (_, method) => {
|
|
5079
|
+
if (typeof method !== "string" || method === "then") return void 0;
|
|
5080
|
+
return (...args) => stub.callTunnels(method, args);
|
|
4399
5081
|
} })
|
|
4400
5082
|
};
|
|
4401
5083
|
return new Proxy(stub, { get(target, prop) {
|
|
@@ -4416,19 +5098,15 @@ function connect(stub) {
|
|
|
4416
5098
|
return await stub.fetch(portSwitchedRequest);
|
|
4417
5099
|
};
|
|
4418
5100
|
}
|
|
4419
|
-
/**
|
|
4420
|
-
* Type guard for R2Bucket binding.
|
|
4421
|
-
* Checks for the minimal R2Bucket interface methods we use.
|
|
4422
|
-
*/
|
|
4423
|
-
function isR2Bucket(value) {
|
|
4424
|
-
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";
|
|
4425
|
-
}
|
|
4426
5101
|
var Sandbox = class Sandbox extends Container {
|
|
4427
5102
|
defaultPort = 3e3;
|
|
4428
5103
|
sleepAfter = "10m";
|
|
4429
5104
|
client;
|
|
4430
5105
|
codeInterpreter;
|
|
4431
5106
|
sandboxName = null;
|
|
5107
|
+
tunnelsHandler = null;
|
|
5108
|
+
tunnelExitHandler = null;
|
|
5109
|
+
controlCallback;
|
|
4432
5110
|
normalizeId = false;
|
|
4433
5111
|
defaultSession = null;
|
|
4434
5112
|
containerGeneration = 0;
|
|
@@ -4527,13 +5205,30 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4527
5205
|
* Dispatch method for desktop operations.
|
|
4528
5206
|
* Called by the client-side proxy created in getSandbox() to provide
|
|
4529
5207
|
* the `sandbox.desktop.status()` API without relying on RPC pipelining
|
|
4530
|
-
* through property getters.
|
|
5208
|
+
* through property getters which is broken when using vite-plugin.
|
|
4531
5209
|
*/
|
|
4532
5210
|
async callDesktop(method, args) {
|
|
4533
5211
|
if (!Sandbox.DESKTOP_METHODS.has(method)) throw new Error(`Unknown desktop method: ${method}`);
|
|
4534
5212
|
const client = this.client.desktop;
|
|
4535
5213
|
const fn = client[method];
|
|
4536
|
-
if (typeof fn !== "function") throw new Error(`
|
|
5214
|
+
if (typeof fn !== "function") throw new Error(`sandbox.desktop missing method: ${method}`);
|
|
5215
|
+
return fn.apply(client, args);
|
|
5216
|
+
}
|
|
5217
|
+
/**
|
|
5218
|
+
* Dispatch method for tunnel operations.
|
|
5219
|
+
* Called by the client-side proxy created in getSandbox() to provide
|
|
5220
|
+
* the `sandbox.tunnels` API without relying on RPC pipelining
|
|
5221
|
+
* through property getters which is broken when using vite-plugin.
|
|
5222
|
+
*/
|
|
5223
|
+
async callTunnels(method, args) {
|
|
5224
|
+
if (![
|
|
5225
|
+
"get",
|
|
5226
|
+
"list",
|
|
5227
|
+
"destroy"
|
|
5228
|
+
].includes(method)) throw new Error(`Unknown tunnels method: ${method}`);
|
|
5229
|
+
const client = this.tunnels;
|
|
5230
|
+
const fn = client[method];
|
|
5231
|
+
if (typeof fn !== "function") throw new Error(`sandbox.tunnels missing method: ${method}`);
|
|
4537
5232
|
return fn.apply(client, args);
|
|
4538
5233
|
}
|
|
4539
5234
|
/**
|
|
@@ -4579,6 +5274,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4579
5274
|
port: 3e3,
|
|
4580
5275
|
logger: this.logger,
|
|
4581
5276
|
retryTimeoutMs: this.computeRetryTimeoutMs(),
|
|
5277
|
+
localMain: this.controlCallback,
|
|
4582
5278
|
onActivity: () => {
|
|
4583
5279
|
this.renewActivityTimeout();
|
|
4584
5280
|
},
|
|
@@ -4593,9 +5289,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4593
5289
|
}
|
|
4594
5290
|
return this.createSandboxClient();
|
|
4595
5291
|
}
|
|
4596
|
-
constructor(ctx, env) {
|
|
4597
|
-
super(ctx, env);
|
|
4598
|
-
const envObj = env;
|
|
5292
|
+
constructor(ctx, env$1) {
|
|
5293
|
+
super(ctx, env$1);
|
|
5294
|
+
const envObj = env$1;
|
|
4599
5295
|
["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
|
|
4600
5296
|
if (envObj?.[key]) this.envVars[key] = String(envObj[key]);
|
|
4601
5297
|
});
|
|
@@ -4618,6 +5314,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4618
5314
|
accessKeyId: this.r2AccessKeyId,
|
|
4619
5315
|
secretAccessKey: this.r2SecretAccessKey
|
|
4620
5316
|
});
|
|
5317
|
+
this.controlCallback = new SandboxControlCallbackImpl(() => this.tunnelExitHandler, this.logger);
|
|
4621
5318
|
this.client = this.createClientForTransport(this.transport);
|
|
4622
5319
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
4623
5320
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
@@ -4645,6 +5342,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4645
5342
|
const previousClient = this.client;
|
|
4646
5343
|
this.client = this.createClientForTransport(storedTransport);
|
|
4647
5344
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
5345
|
+
this.tunnelsHandler = null;
|
|
5346
|
+
this.tunnelExitHandler = null;
|
|
4648
5347
|
previousClient.disconnect();
|
|
4649
5348
|
}
|
|
4650
5349
|
if (storedTransport) this.hasStoredTransport = true;
|
|
@@ -4700,13 +5399,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4700
5399
|
}
|
|
4701
5400
|
}
|
|
4702
5401
|
}
|
|
4703
|
-
/**
|
|
4704
|
-
* RPC method to configure container startup timeouts. Idempotent once
|
|
4705
|
-
* the values have been persisted: re-applying the same timeout set is a
|
|
4706
|
-
* no-op. The transport retry budget is recomputed only when at least
|
|
4707
|
-
* one timeout actually changes. Storage is written before the in-memory
|
|
4708
|
-
* mirror and derived state are updated.
|
|
4709
|
-
*/
|
|
4710
5402
|
async setContainerTimeouts(timeouts) {
|
|
4711
5403
|
const validated = { ...this.containerTimeouts };
|
|
4712
5404
|
if (timeouts.instanceGetTimeoutMS !== void 0) validated.instanceGetTimeoutMS = this.validateTimeout(timeouts.instanceGetTimeoutMS, "instanceGetTimeoutMS", 5e3, 3e5);
|
|
@@ -4719,11 +5411,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4719
5411
|
this.client.setRetryTimeoutMs(this.computeRetryTimeoutMs());
|
|
4720
5412
|
this.logger.debug("Container timeouts updated", this.containerTimeouts);
|
|
4721
5413
|
}
|
|
4722
|
-
/**
|
|
4723
|
-
* RPC method to set the transport protocol. Idempotent once the value
|
|
4724
|
-
* has been persisted: re-applying the same transport is a no-op.
|
|
4725
|
-
* Storage is written before the in-memory state and client are updated.
|
|
4726
|
-
*/
|
|
4727
5414
|
async setTransport(transport) {
|
|
4728
5415
|
if (transport !== "http" && transport !== "websocket" && transport !== "rpc") {
|
|
4729
5416
|
this.logger.warn(`Invalid transport value: "${transport}". Must be "http", "websocket", or "rpc". Ignoring.`);
|
|
@@ -4736,24 +5423,18 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4736
5423
|
this.hasStoredTransport = true;
|
|
4737
5424
|
this.client = this.createClientForTransport(transport);
|
|
4738
5425
|
this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
|
|
5426
|
+
this.tunnelsHandler = null;
|
|
5427
|
+
this.tunnelExitHandler = null;
|
|
4739
5428
|
previousClient.disconnect();
|
|
4740
5429
|
this.renewActivityTimeout();
|
|
4741
5430
|
this.logger.debug("Transport updated", { transport });
|
|
4742
5431
|
}
|
|
4743
|
-
/**
|
|
4744
|
-
* Validate a timeout value is within acceptable range
|
|
4745
|
-
* Throws error if invalid - used for user-provided values
|
|
4746
|
-
*/
|
|
4747
5432
|
validateTimeout(value, name, min, max) {
|
|
4748
5433
|
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) throw new Error(`${name} must be a valid finite number, got ${value}`);
|
|
4749
5434
|
if (value < min || value > max) throw new Error(`${name} must be between ${min}-${max}ms, got ${value}ms`);
|
|
4750
5435
|
return value;
|
|
4751
5436
|
}
|
|
4752
|
-
|
|
4753
|
-
* Get default timeouts with env var fallbacks and validation
|
|
4754
|
-
* Precedence: SDK defaults < Env vars < User config
|
|
4755
|
-
*/
|
|
4756
|
-
getDefaultTimeouts(env) {
|
|
5437
|
+
getDefaultTimeouts(env$1) {
|
|
4757
5438
|
const parseAndValidate = (envVar, name, min, max) => {
|
|
4758
5439
|
const defaultValue = this.DEFAULT_CONTAINER_TIMEOUTS[name];
|
|
4759
5440
|
if (envVar === void 0) return defaultValue;
|
|
@@ -4769,9 +5450,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4769
5450
|
return parsed;
|
|
4770
5451
|
};
|
|
4771
5452
|
return {
|
|
4772
|
-
instanceGetTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
|
|
4773
|
-
portReadyTimeoutMS: parseAndValidate(getEnvString(env, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
|
|
4774
|
-
waitIntervalMS: parseAndValidate(getEnvString(env, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
|
|
5453
|
+
instanceGetTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
|
|
5454
|
+
portReadyTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
|
|
5455
|
+
waitIntervalMS: parseAndValidate(getEnvString(env$1, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
|
|
4775
5456
|
};
|
|
4776
5457
|
}
|
|
4777
5458
|
/**
|
|
@@ -4793,7 +5474,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4793
5474
|
await this.mountBucketLocal(bucket, mountPath, options);
|
|
4794
5475
|
return;
|
|
4795
5476
|
}
|
|
4796
|
-
|
|
5477
|
+
const remoteOptions = options;
|
|
5478
|
+
if (remoteOptions.endpoint === void 0) {
|
|
5479
|
+
const binding = this.env[bucket];
|
|
5480
|
+
if (isR2Bucket(binding)) {
|
|
5481
|
+
await this.mountBucketR2Egress(bucket, mountPath, options);
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in Worker env. Ensure the binding name matches the bucket binding configured in wrangler.jsonc.`);
|
|
5485
|
+
}
|
|
5486
|
+
await this.mountBucketFuse(bucket, mountPath, remoteOptions);
|
|
4797
5487
|
}
|
|
4798
5488
|
/**
|
|
4799
5489
|
* Local dev mount: bidirectional sync via R2 binding + file/watch APIs
|
|
@@ -4850,12 +5540,109 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4850
5540
|
});
|
|
4851
5541
|
}
|
|
4852
5542
|
}
|
|
5543
|
+
getR2EgressParams() {
|
|
5544
|
+
const buckets = {};
|
|
5545
|
+
for (const [, m] of this.activeMounts) if (m.mountType === "r2-egress") buckets[m.bucket] = {
|
|
5546
|
+
prefix: m.prefix,
|
|
5547
|
+
readOnly: m.readOnly
|
|
5548
|
+
};
|
|
5549
|
+
return { buckets };
|
|
5550
|
+
}
|
|
5551
|
+
validateR2EgressS3fsOptions(options) {
|
|
5552
|
+
if (!options) return;
|
|
5553
|
+
const protectedOptions = new Set(["passwd_file", "url"]);
|
|
5554
|
+
for (const option of options) {
|
|
5555
|
+
const [key] = option.split("=");
|
|
5556
|
+
if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for R2 binding mounts`);
|
|
5557
|
+
}
|
|
5558
|
+
}
|
|
5559
|
+
/**
|
|
5560
|
+
* Credential-less R2 mount: egress interception routes s3fs requests to the
|
|
5561
|
+
* R2 binding. No S3 credentials are needed in the container or Worker env.
|
|
5562
|
+
*/
|
|
5563
|
+
async mountBucketR2Egress(bucket, mountPath, options) {
|
|
5564
|
+
const mountStartTime = Date.now();
|
|
5565
|
+
const prefix = options.prefix;
|
|
5566
|
+
let mountOutcome = "error";
|
|
5567
|
+
let mountError;
|
|
5568
|
+
try {
|
|
5569
|
+
validateBucketBindingName(bucket, mountPath);
|
|
5570
|
+
this.validateMountPath(mountPath);
|
|
5571
|
+
this.validateR2EgressS3fsOptions(options.s3fsOptions);
|
|
5572
|
+
for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
|
|
5573
|
+
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.`);
|
|
5574
|
+
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.`);
|
|
5575
|
+
}
|
|
5576
|
+
const passwordFilePath = this.generatePasswordFilePath();
|
|
5577
|
+
await this.createPasswordFile(passwordFilePath, bucket, {
|
|
5578
|
+
accessKeyId: "x",
|
|
5579
|
+
secretAccessKey: "x"
|
|
5580
|
+
});
|
|
5581
|
+
const mountInfo = {
|
|
5582
|
+
mountType: "r2-egress",
|
|
5583
|
+
bucket,
|
|
5584
|
+
mountPath,
|
|
5585
|
+
passwordFilePath,
|
|
5586
|
+
mounted: false,
|
|
5587
|
+
prefix,
|
|
5588
|
+
readOnly: options.readOnly ?? false
|
|
5589
|
+
};
|
|
5590
|
+
this.activeMounts.set(mountPath, mountInfo);
|
|
5591
|
+
await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
5592
|
+
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
5593
|
+
const s3fsSource = bucket;
|
|
5594
|
+
const optionsStr = shellEscape(serializeS3fsOptions({
|
|
5595
|
+
passwd_file: passwordFilePath,
|
|
5596
|
+
...R2_DEFAULT_S3FS_OPTIONS,
|
|
5597
|
+
...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
|
|
5598
|
+
use_path_request_style: true,
|
|
5599
|
+
url: "http://r2.internal",
|
|
5600
|
+
...options.readOnly ? { ro: true } : {}
|
|
5601
|
+
}));
|
|
5602
|
+
const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
5603
|
+
this.logger.debug("r2-egress: running s3fs", { mountCmd });
|
|
5604
|
+
const result = await this.execInternal(mountCmd);
|
|
5605
|
+
this.logger.debug("r2-egress: s3fs exited", {
|
|
5606
|
+
exitCode: result.exitCode,
|
|
5607
|
+
stdout: result.stdout,
|
|
5608
|
+
stderr: result.stderr
|
|
5609
|
+
});
|
|
5610
|
+
if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
|
|
5611
|
+
const mountpointCheck = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && echo 'FUSE_MOUNTED' || echo 'NOT_FUSE_MOUNTED'`);
|
|
5612
|
+
this.logger.debug("r2-egress: mountpoint check", {
|
|
5613
|
+
stdout: mountpointCheck.stdout.trim(),
|
|
5614
|
+
exitCode: mountpointCheck.exitCode
|
|
5615
|
+
});
|
|
5616
|
+
if (mountpointCheck.stdout.trim() !== "FUSE_MOUNTED") throw new S3FSMountError(`s3fs exited 0 but mount was not established at ${mountPath}`);
|
|
5617
|
+
mountInfo.mounted = true;
|
|
5618
|
+
mountOutcome = "success";
|
|
5619
|
+
} catch (error) {
|
|
5620
|
+
mountError = error instanceof Error ? error : new Error(String(error));
|
|
5621
|
+
const failedMount = this.activeMounts.get(mountPath);
|
|
5622
|
+
this.activeMounts.delete(mountPath);
|
|
5623
|
+
if (failedMount?.mountType === "r2-egress") await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
|
|
5624
|
+
const remainingParams = this.getR2EgressParams();
|
|
5625
|
+
await this.configureR2EgressOutbound(remainingParams).catch(() => {});
|
|
5626
|
+
throw error;
|
|
5627
|
+
} finally {
|
|
5628
|
+
logCanonicalEvent(this.logger, {
|
|
5629
|
+
event: "bucket.mount",
|
|
5630
|
+
outcome: mountOutcome,
|
|
5631
|
+
durationMs: Date.now() - mountStartTime,
|
|
5632
|
+
bucket,
|
|
5633
|
+
mountPath,
|
|
5634
|
+
provider: "r2",
|
|
5635
|
+
prefix,
|
|
5636
|
+
error: mountError
|
|
5637
|
+
});
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
4853
5640
|
/**
|
|
4854
5641
|
* Production mount: S3FS-FUSE inside the container
|
|
4855
5642
|
*/
|
|
4856
5643
|
async mountBucketFuse(bucket, mountPath, options) {
|
|
4857
5644
|
const mountStartTime = Date.now();
|
|
4858
|
-
const prefix = options.prefix
|
|
5645
|
+
const prefix = options.prefix;
|
|
4859
5646
|
let mountOutcome = "error";
|
|
4860
5647
|
let mountError;
|
|
4861
5648
|
let passwordFilePath;
|
|
@@ -4946,6 +5733,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4946
5733
|
}
|
|
4947
5734
|
mountInfo.mounted = false;
|
|
4948
5735
|
this.activeMounts.delete(mountPath);
|
|
5736
|
+
if (mountInfo.mountType === "r2-egress") await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
4949
5737
|
try {
|
|
4950
5738
|
const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
|
|
4951
5739
|
if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
|
|
@@ -4978,7 +5766,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4978
5766
|
}
|
|
4979
5767
|
}
|
|
4980
5768
|
/**
|
|
4981
|
-
*
|
|
5769
|
+
* Shared validation for mount path (absolute, not already in use).
|
|
5770
|
+
*/
|
|
5771
|
+
validateMountPath(mountPath) {
|
|
5772
|
+
if (!mountPath.startsWith("/")) throw new InvalidMountConfigError(`Mount path must be absolute (start with /): "${mountPath}"`);
|
|
5773
|
+
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.`);
|
|
5774
|
+
}
|
|
5775
|
+
/**
|
|
5776
|
+
* Validate mount options for remote (FUSE) mounts
|
|
4982
5777
|
*/
|
|
4983
5778
|
validateMountOptions(bucket, mountPath, options) {
|
|
4984
5779
|
try {
|
|
@@ -4987,8 +5782,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4987
5782
|
throw new InvalidMountConfigError(`Invalid endpoint URL: "${options.endpoint}". Must be a valid HTTP(S) URL.`);
|
|
4988
5783
|
}
|
|
4989
5784
|
validateBucketName(bucket, mountPath);
|
|
4990
|
-
|
|
4991
|
-
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.`);
|
|
5785
|
+
this.validateMountPath(mountPath);
|
|
4992
5786
|
}
|
|
4993
5787
|
/**
|
|
4994
5788
|
* Generate unique password file path for s3fs credentials
|
|
@@ -5002,7 +5796,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5002
5796
|
*/
|
|
5003
5797
|
async createPasswordFile(passwordFilePath, bucket, credentials) {
|
|
5004
5798
|
const content = `${bucket}:${credentials.accessKeyId}:${credentials.secretAccessKey}`;
|
|
5005
|
-
await this.writeFile(passwordFilePath, content);
|
|
5799
|
+
await this.client.files.writeFile(passwordFilePath, content, DISABLE_SESSION_TOKEN);
|
|
5006
5800
|
await this.execInternal(`chmod 0600 ${shellEscape(passwordFilePath)}`);
|
|
5007
5801
|
}
|
|
5008
5802
|
/**
|
|
@@ -5048,6 +5842,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5048
5842
|
if (result.exitCode === 2) throw new S3FSMountError(`S3FS mount failed: ${detail || "Unknown error"}`);
|
|
5049
5843
|
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."}`);
|
|
5050
5844
|
}
|
|
5845
|
+
async unmountTrackedFuseMount(mountPath, mountInfo) {
|
|
5846
|
+
if (!mountInfo.mounted) return;
|
|
5847
|
+
this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
|
|
5848
|
+
const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
5849
|
+
if (result.exitCode !== 0) throw new Error(`fusermount -u failed (exit ${result.exitCode}): ${result.stderr || "unknown error"}`);
|
|
5850
|
+
mountInfo.mounted = false;
|
|
5851
|
+
}
|
|
5051
5852
|
/**
|
|
5052
5853
|
* In-flight `destroy()` promise. While set, concurrent callers coalesce
|
|
5053
5854
|
* onto the same teardown instead of triggering a second one. Cleared when
|
|
@@ -5109,10 +5910,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5109
5910
|
this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
|
|
5110
5911
|
}
|
|
5111
5912
|
else {
|
|
5112
|
-
|
|
5113
|
-
this.
|
|
5114
|
-
await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
5115
|
-
mountInfo.mounted = false;
|
|
5913
|
+
try {
|
|
5914
|
+
await this.unmountTrackedFuseMount(mountPath, mountInfo);
|
|
5116
5915
|
} catch (error) {
|
|
5117
5916
|
mountFailures++;
|
|
5118
5917
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -5122,6 +5921,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5122
5921
|
}
|
|
5123
5922
|
}
|
|
5124
5923
|
await this.ctx.storage.delete("portTokens");
|
|
5924
|
+
await this.ctx.storage.delete("tunnels");
|
|
5125
5925
|
this.client.disconnect();
|
|
5126
5926
|
outcome = "success";
|
|
5127
5927
|
await super.destroy();
|
|
@@ -5149,6 +5949,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5149
5949
|
} catch (error) {
|
|
5150
5950
|
this.logger.error("Failed to restore exposed ports after container start", error instanceof Error ? error : new Error(String(error)));
|
|
5151
5951
|
}
|
|
5952
|
+
try {
|
|
5953
|
+
await this.ctx.storage.delete("tunnels");
|
|
5954
|
+
} catch (error) {
|
|
5955
|
+
this.logger.error("Failed to clear tunnel storage after container start", error instanceof Error ? error : new Error(String(error)));
|
|
5956
|
+
}
|
|
5152
5957
|
}
|
|
5153
5958
|
/**
|
|
5154
5959
|
* Re-expose ports on the container runtime using tokens persisted in DO
|
|
@@ -5169,8 +5974,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5169
5974
|
let restored = 0;
|
|
5170
5975
|
let skipped = 0;
|
|
5171
5976
|
let failed = 0;
|
|
5172
|
-
const
|
|
5173
|
-
const exposedSet = await this.client.ports.getExposedPorts(sessionId).then((response) => new Set(response.ports.map((p) => p.port))).catch((error) => {
|
|
5977
|
+
const exposedSet = await this.client.ports.getExposedPorts(DISABLE_SESSION_TOKEN).then((response) => new Set(response.ports.map((p) => p.port))).catch((error) => {
|
|
5174
5978
|
this.logger.warn("Failed to fetch exposed ports for restore; assuming none exposed", { error: error instanceof Error ? error.message : String(error) });
|
|
5175
5979
|
return /* @__PURE__ */ new Set();
|
|
5176
5980
|
});
|
|
@@ -5186,7 +5990,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5186
5990
|
continue;
|
|
5187
5991
|
}
|
|
5188
5992
|
try {
|
|
5189
|
-
await this.client.ports.exposePort(port,
|
|
5993
|
+
await this.client.ports.exposePort(port, DISABLE_SESSION_TOKEN, entry.name);
|
|
5190
5994
|
restored++;
|
|
5191
5995
|
} catch (error) {
|
|
5192
5996
|
failed++;
|
|
@@ -5251,7 +6055,10 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5251
6055
|
this.defaultSession = null;
|
|
5252
6056
|
this.defaultSessionInit = null;
|
|
5253
6057
|
this.client.disconnect();
|
|
6058
|
+
let hadR2EgressMount = false;
|
|
5254
6059
|
for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
|
|
6060
|
+
else if (m.mountType === "r2-egress") hadR2EgressMount = true;
|
|
6061
|
+
if (hadR2EgressMount) await this.configureR2EgressOutbound({ buckets: {} }).catch(() => {});
|
|
5255
6062
|
this.activeMounts.clear();
|
|
5256
6063
|
await this.ctx.storage.delete("defaultSession");
|
|
5257
6064
|
}
|
|
@@ -5566,10 +6373,78 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5566
6373
|
if (containerPlacementId === void 0) return;
|
|
5567
6374
|
await this.ctx.storage.put("containerPlacementId", containerPlacementId);
|
|
5568
6375
|
}
|
|
6376
|
+
async resolveExecution(explicitSessionId) {
|
|
6377
|
+
if (explicitSessionId !== void 0) {
|
|
6378
|
+
this.validateExplicitSessionId(explicitSessionId);
|
|
6379
|
+
if (explicitSessionId === DISABLE_SESSION_TOKEN) return { kind: "sessionless" };
|
|
6380
|
+
return {
|
|
6381
|
+
kind: "session",
|
|
6382
|
+
sessionId: explicitSessionId
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
return {
|
|
6386
|
+
kind: "session",
|
|
6387
|
+
sessionId: await this.ensureDefaultSession()
|
|
6388
|
+
};
|
|
6389
|
+
}
|
|
6390
|
+
validateExplicitSessionId(sessionId) {
|
|
6391
|
+
if (sessionId.trim().length === 0) throw new Error("sessionId must not be empty or whitespace");
|
|
6392
|
+
}
|
|
6393
|
+
serializeExecutionContext(context) {
|
|
6394
|
+
if (context.kind === "sessionless") return DISABLE_SESSION_TOKEN;
|
|
6395
|
+
return context.sessionId;
|
|
6396
|
+
}
|
|
6397
|
+
getPublicExecutionSessionId(sessionId) {
|
|
6398
|
+
return sessionId === DISABLE_SESSION_TOKEN ? void 0 : sessionId;
|
|
6399
|
+
}
|
|
6400
|
+
/**
|
|
6401
|
+
* Resolves the session ID to annotate returned Process objects.
|
|
6402
|
+
*
|
|
6403
|
+
* Unlike `resolveExecution`, this is synchronous and never creates a
|
|
6404
|
+
* session. When the default session hasn't been established yet, it returns
|
|
6405
|
+
* `undefined` rather than triggering session creation. The resolved value is
|
|
6406
|
+
* only used to populate `Process.sessionId` on the returned object — it is
|
|
6407
|
+
* never sent to the container API.
|
|
6408
|
+
*/
|
|
6409
|
+
getProcessSessionBinding(explicitSessionId) {
|
|
6410
|
+
if (explicitSessionId !== void 0) {
|
|
6411
|
+
this.validateExplicitSessionId(explicitSessionId);
|
|
6412
|
+
if (explicitSessionId === DISABLE_SESSION_TOKEN) return;
|
|
6413
|
+
return explicitSessionId;
|
|
6414
|
+
}
|
|
6415
|
+
return this.defaultSession ?? void 0;
|
|
6416
|
+
}
|
|
6417
|
+
resolveExecutionEnv(sessionId, env$1) {
|
|
6418
|
+
if (sessionId === DISABLE_SESSION_TOKEN) {
|
|
6419
|
+
const mergedEnv = filterEnvVars({
|
|
6420
|
+
...this.envVars,
|
|
6421
|
+
...env$1 ?? {}
|
|
6422
|
+
});
|
|
6423
|
+
return Object.keys(mergedEnv).length > 0 ? mergedEnv : void 0;
|
|
6424
|
+
}
|
|
6425
|
+
if (env$1 === void 0) return;
|
|
6426
|
+
const filteredEnv = filterEnvVars(env$1);
|
|
6427
|
+
return Object.keys(filteredEnv).length > 0 ? filteredEnv : void 0;
|
|
6428
|
+
}
|
|
6429
|
+
buildExecutionRequestOptions(sessionId, options) {
|
|
6430
|
+
const env$1 = this.resolveExecutionEnv(sessionId, options?.env);
|
|
6431
|
+
if (options?.timeout === void 0 && env$1 === void 0 && options?.cwd === void 0 && options?.origin === void 0) return;
|
|
6432
|
+
return {
|
|
6433
|
+
...options?.timeout !== void 0 && { timeoutMs: options.timeout },
|
|
6434
|
+
...env$1 !== void 0 && { env: env$1 },
|
|
6435
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
6436
|
+
...options?.origin !== void 0 && { origin: options.origin }
|
|
6437
|
+
};
|
|
6438
|
+
}
|
|
5569
6439
|
async exec(command, options) {
|
|
5570
|
-
const
|
|
6440
|
+
const context = await this.resolveExecution();
|
|
6441
|
+
const session = this.serializeExecutionContext(context);
|
|
5571
6442
|
return this.execWithSession(command, session, options);
|
|
5572
6443
|
}
|
|
6444
|
+
async execWithSessionToken(command, sessionId, options) {
|
|
6445
|
+
this.validateExplicitSessionId(sessionId);
|
|
6446
|
+
return this.execWithSession(command, sessionId, options);
|
|
6447
|
+
}
|
|
5573
6448
|
/**
|
|
5574
6449
|
* Execute an infrastructure command (backup, mount, env setup, etc.)
|
|
5575
6450
|
* tagged with origin: 'internal' so logging demotes it to debug level.
|
|
@@ -5592,15 +6467,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5592
6467
|
let result;
|
|
5593
6468
|
if (options?.stream && options?.onOutput) result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
|
|
5594
6469
|
else {
|
|
5595
|
-
const commandOptions =
|
|
5596
|
-
timeoutMs: options.timeout,
|
|
5597
|
-
env: options.env,
|
|
5598
|
-
cwd: options.cwd,
|
|
5599
|
-
origin: options.origin
|
|
5600
|
-
} : void 0;
|
|
6470
|
+
const commandOptions = this.buildExecutionRequestOptions(sessionId, options);
|
|
5601
6471
|
const response = await this.client.commands.execute(command, sessionId, commandOptions);
|
|
5602
6472
|
const duration = Date.now() - startTime;
|
|
5603
|
-
|
|
6473
|
+
const publicSessionId = this.getPublicExecutionSessionId(sessionId);
|
|
6474
|
+
result = this.mapExecuteResponseToExecResult(response, duration, publicSessionId);
|
|
5604
6475
|
}
|
|
5605
6476
|
execOutcome = {
|
|
5606
6477
|
exitCode: result.exitCode,
|
|
@@ -5619,7 +6490,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5619
6490
|
command,
|
|
5620
6491
|
exitCode: execOutcome?.exitCode,
|
|
5621
6492
|
durationMs: Date.now() - startTime,
|
|
5622
|
-
sessionId,
|
|
6493
|
+
sessionId: this.getPublicExecutionSessionId(sessionId),
|
|
5623
6494
|
origin: options?.origin ?? "user",
|
|
5624
6495
|
error: execError ?? void 0,
|
|
5625
6496
|
errorMessage: execError?.message
|
|
@@ -5630,12 +6501,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5630
6501
|
let stdout = "";
|
|
5631
6502
|
let stderr = "";
|
|
5632
6503
|
try {
|
|
5633
|
-
const
|
|
5634
|
-
|
|
5635
|
-
env: options.env,
|
|
5636
|
-
cwd: options.cwd,
|
|
5637
|
-
origin: options.origin
|
|
5638
|
-
});
|
|
6504
|
+
const commandOptions = this.buildExecutionRequestOptions(sessionId, options);
|
|
6505
|
+
const stream = await this.client.commands.executeStream(command, sessionId, commandOptions);
|
|
5639
6506
|
for await (const event of parseSSEStream(stream)) {
|
|
5640
6507
|
if (options.signal?.aborted) throw new Error("Operation was aborted");
|
|
5641
6508
|
switch (event.type) {
|
|
@@ -5657,7 +6524,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5657
6524
|
command,
|
|
5658
6525
|
duration,
|
|
5659
6526
|
timestamp,
|
|
5660
|
-
sessionId
|
|
6527
|
+
sessionId: this.getPublicExecutionSessionId(sessionId)
|
|
5661
6528
|
};
|
|
5662
6529
|
}
|
|
5663
6530
|
case "error": throw new Error(event.data || "Command execution failed");
|
|
@@ -5933,12 +6800,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5933
6800
|
}
|
|
5934
6801
|
async startProcess(command, options, sessionId) {
|
|
5935
6802
|
try {
|
|
5936
|
-
const
|
|
6803
|
+
const execution = await this.resolveExecution(sessionId);
|
|
6804
|
+
const session = this.serializeExecutionContext(execution);
|
|
6805
|
+
const processSession = this.getProcessSessionBinding(session);
|
|
5937
6806
|
const requestOptions = {
|
|
6807
|
+
...this.buildExecutionRequestOptions(session, {
|
|
6808
|
+
timeout: options?.timeout,
|
|
6809
|
+
env: options?.env,
|
|
6810
|
+
cwd: options?.cwd
|
|
6811
|
+
}),
|
|
5938
6812
|
...options?.processId !== void 0 && { processId: options.processId },
|
|
5939
|
-
...options?.timeout !== void 0 && { timeoutMs: options.timeout },
|
|
5940
|
-
...options?.env !== void 0 && { env: filterEnvVars(options.env) },
|
|
5941
|
-
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
5942
6813
|
...options?.encoding !== void 0 && { encoding: options.encoding },
|
|
5943
6814
|
...options?.autoCleanup !== void 0 && { autoCleanup: options.autoCleanup }
|
|
5944
6815
|
};
|
|
@@ -5951,7 +6822,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5951
6822
|
startTime: /* @__PURE__ */ new Date(),
|
|
5952
6823
|
endTime: void 0,
|
|
5953
6824
|
exitCode: void 0
|
|
5954
|
-
},
|
|
6825
|
+
}, processSession);
|
|
5955
6826
|
if (options?.onStart) options.onStart(processObj);
|
|
5956
6827
|
if (options?.onOutput || options?.onExit) this.startProcessCallbackStream(response.processId, options).catch(() => {});
|
|
5957
6828
|
return processObj;
|
|
@@ -5979,13 +6850,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5979
6850
|
if (options.onExit) options.onExit(event.exitCode ?? null);
|
|
5980
6851
|
return;
|
|
5981
6852
|
}
|
|
6853
|
+
throw new Error("Stream ended without completion event");
|
|
5982
6854
|
} catch (error) {
|
|
5983
6855
|
if (options.onError && error instanceof Error) options.onError(error);
|
|
5984
6856
|
this.logger.error("Background process streaming failed", error instanceof Error ? error : new Error(String(error)), { processId });
|
|
5985
6857
|
}
|
|
5986
6858
|
}
|
|
5987
6859
|
async listProcesses(sessionId) {
|
|
5988
|
-
const session =
|
|
6860
|
+
const session = this.getProcessSessionBinding(sessionId);
|
|
5989
6861
|
return (await this.client.processes.listProcesses()).processes.map((processData) => this.createProcessFromDTO({
|
|
5990
6862
|
id: processData.id,
|
|
5991
6863
|
pid: processData.pid,
|
|
@@ -5997,19 +6869,24 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5997
6869
|
}, session));
|
|
5998
6870
|
}
|
|
5999
6871
|
async getProcess(id, sessionId) {
|
|
6000
|
-
const session =
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6872
|
+
const session = this.getProcessSessionBinding(sessionId);
|
|
6873
|
+
try {
|
|
6874
|
+
const response = await this.client.processes.getProcess(id);
|
|
6875
|
+
if (!response.process) return null;
|
|
6876
|
+
const processData = response.process;
|
|
6877
|
+
return this.createProcessFromDTO({
|
|
6878
|
+
id: processData.id,
|
|
6879
|
+
pid: processData.pid,
|
|
6880
|
+
command: processData.command,
|
|
6881
|
+
status: processData.status,
|
|
6882
|
+
startTime: processData.startTime,
|
|
6883
|
+
endTime: processData.endTime,
|
|
6884
|
+
exitCode: processData.exitCode
|
|
6885
|
+
}, session);
|
|
6886
|
+
} catch (error) {
|
|
6887
|
+
if (error instanceof ProcessNotFoundError) return null;
|
|
6888
|
+
throw error;
|
|
6889
|
+
}
|
|
6013
6890
|
}
|
|
6014
6891
|
async killProcess(id, signal, sessionId) {
|
|
6015
6892
|
await this.client.processes.killProcess(id);
|
|
@@ -6030,23 +6907,29 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6030
6907
|
}
|
|
6031
6908
|
async execStream(command, options) {
|
|
6032
6909
|
if (options?.signal?.aborted) throw new Error("Operation was aborted");
|
|
6033
|
-
const
|
|
6034
|
-
|
|
6035
|
-
|
|
6910
|
+
const context = await this.resolveExecution(options?.sessionId);
|
|
6911
|
+
const session = this.serializeExecutionContext(context);
|
|
6912
|
+
const executionOptions = this.buildExecutionRequestOptions(session, {
|
|
6913
|
+
timeout: options?.timeout,
|
|
6036
6914
|
env: options?.env,
|
|
6037
6915
|
cwd: options?.cwd
|
|
6038
6916
|
});
|
|
6917
|
+
return this.client.commands.executeStream(command, session, executionOptions);
|
|
6918
|
+
}
|
|
6919
|
+
async execStreamWithSessionToken(command, sessionId, options) {
|
|
6920
|
+
this.validateExplicitSessionId(sessionId);
|
|
6921
|
+
return this.execStreamWithSession(command, sessionId, options);
|
|
6039
6922
|
}
|
|
6040
6923
|
/**
|
|
6041
6924
|
* Internal session-aware execStream implementation
|
|
6042
6925
|
*/
|
|
6043
6926
|
async execStreamWithSession(command, sessionId, options) {
|
|
6044
6927
|
if (options?.signal?.aborted) throw new Error("Operation was aborted");
|
|
6045
|
-
return this.client.commands.executeStream(command, sessionId, {
|
|
6046
|
-
|
|
6928
|
+
return this.client.commands.executeStream(command, sessionId, this.buildExecutionRequestOptions(sessionId, {
|
|
6929
|
+
timeout: options?.timeout,
|
|
6047
6930
|
env: options?.env,
|
|
6048
6931
|
cwd: options?.cwd
|
|
6049
|
-
});
|
|
6932
|
+
}));
|
|
6050
6933
|
}
|
|
6051
6934
|
/**
|
|
6052
6935
|
* Stream logs from a background process as a ReadableStream.
|
|
@@ -6056,7 +6939,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6056
6939
|
return this.client.processes.streamProcessLogs(processId);
|
|
6057
6940
|
}
|
|
6058
6941
|
async gitCheckout(repoUrl, options) {
|
|
6059
|
-
const
|
|
6942
|
+
const execution = await this.resolveExecution(options?.sessionId);
|
|
6943
|
+
const session = this.serializeExecutionContext(execution);
|
|
6060
6944
|
return this.client.git.checkout(repoUrl, session, {
|
|
6061
6945
|
branch: options?.branch,
|
|
6062
6946
|
targetDir: options?.targetDir,
|
|
@@ -6065,28 +6949,34 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6065
6949
|
});
|
|
6066
6950
|
}
|
|
6067
6951
|
async mkdir(path$1, options = {}) {
|
|
6068
|
-
const
|
|
6952
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
6953
|
+
const session = this.serializeExecutionContext(execution);
|
|
6069
6954
|
return this.client.files.mkdir(path$1, session, { recursive: options.recursive });
|
|
6070
6955
|
}
|
|
6071
6956
|
async writeFile(path$1, content, options = {}) {
|
|
6072
|
-
const
|
|
6957
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
6958
|
+
const session = this.serializeExecutionContext(execution);
|
|
6073
6959
|
if (content instanceof ReadableStream) return this.client.files.writeFileStream(path$1, content, session);
|
|
6074
6960
|
return this.client.files.writeFile(path$1, content, session, { encoding: options.encoding });
|
|
6075
6961
|
}
|
|
6076
6962
|
async deleteFile(path$1, sessionId) {
|
|
6077
|
-
const
|
|
6963
|
+
const execution = await this.resolveExecution(sessionId);
|
|
6964
|
+
const session = this.serializeExecutionContext(execution);
|
|
6078
6965
|
return this.client.files.deleteFile(path$1, session);
|
|
6079
6966
|
}
|
|
6080
6967
|
async renameFile(oldPath, newPath, sessionId) {
|
|
6081
|
-
const
|
|
6968
|
+
const execution = await this.resolveExecution(sessionId);
|
|
6969
|
+
const session = this.serializeExecutionContext(execution);
|
|
6082
6970
|
return this.client.files.renameFile(oldPath, newPath, session);
|
|
6083
6971
|
}
|
|
6084
6972
|
async moveFile(sourcePath, destinationPath, sessionId) {
|
|
6085
|
-
const
|
|
6973
|
+
const execution = await this.resolveExecution(sessionId);
|
|
6974
|
+
const session = this.serializeExecutionContext(execution);
|
|
6086
6975
|
return this.client.files.moveFile(sourcePath, destinationPath, session);
|
|
6087
6976
|
}
|
|
6088
6977
|
async readFile(path$1, options = {}) {
|
|
6089
|
-
const
|
|
6978
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
6979
|
+
const session = this.serializeExecutionContext(execution);
|
|
6090
6980
|
if (options.encoding === "none") return this.client.files.readFile(path$1, session, { encoding: "none" });
|
|
6091
6981
|
return this.client.files.readFile(path$1, session, { encoding: options.encoding });
|
|
6092
6982
|
}
|
|
@@ -6097,15 +6987,18 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6097
6987
|
* @param options - Optional session ID
|
|
6098
6988
|
*/
|
|
6099
6989
|
async readFileStream(path$1, options = {}) {
|
|
6100
|
-
const
|
|
6990
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
6991
|
+
const session = this.serializeExecutionContext(execution);
|
|
6101
6992
|
return this.client.files.readFileStream(path$1, session);
|
|
6102
6993
|
}
|
|
6103
6994
|
async listFiles(path$1, options) {
|
|
6104
|
-
const
|
|
6995
|
+
const context = await this.resolveExecution(options?.sessionId);
|
|
6996
|
+
const session = this.serializeExecutionContext(context);
|
|
6105
6997
|
return this.client.files.listFiles(path$1, session, options);
|
|
6106
6998
|
}
|
|
6107
6999
|
async exists(path$1, sessionId) {
|
|
6108
|
-
const
|
|
7000
|
+
const execution = await this.resolveExecution(sessionId);
|
|
7001
|
+
const session = this.serializeExecutionContext(execution);
|
|
6109
7002
|
return this.client.files.exists(path$1, session);
|
|
6110
7003
|
}
|
|
6111
7004
|
/**
|
|
@@ -6156,7 +7049,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6156
7049
|
* @param options - Watch options
|
|
6157
7050
|
*/
|
|
6158
7051
|
async watch(path$1, options = {}) {
|
|
6159
|
-
const
|
|
7052
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
7053
|
+
const sessionId = this.serializeExecutionContext(execution);
|
|
6160
7054
|
return this.client.watch.watch({
|
|
6161
7055
|
path: path$1,
|
|
6162
7056
|
recursive: options.recursive,
|
|
@@ -6176,7 +7070,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6176
7070
|
* @param options - Change-check options
|
|
6177
7071
|
*/
|
|
6178
7072
|
async checkChanges(path$1, options = {}) {
|
|
6179
|
-
const
|
|
7073
|
+
const execution = await this.resolveExecution(options.sessionId);
|
|
7074
|
+
const sessionId = this.serializeExecutionContext(execution);
|
|
6180
7075
|
return this.client.watch.checkChanges({
|
|
6181
7076
|
path: path$1,
|
|
6182
7077
|
recursive: options.recursive,
|
|
@@ -6238,7 +7133,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6238
7133
|
const tokens = await this.readPortTokens();
|
|
6239
7134
|
const existingPort = Object.entries(tokens).find(([p, entry]) => entry.token === token && p !== port.toString());
|
|
6240
7135
|
if (existingPort) throw new SandboxSecurityError(`Token '${token}' is already in use by port ${existingPort[0]}. Please use a different token.`);
|
|
6241
|
-
const sessionId = await this.
|
|
7136
|
+
const sessionId = this.serializeExecutionContext(await this.resolveExecution());
|
|
6242
7137
|
await this.client.ports.exposePort(port, sessionId, options?.name);
|
|
6243
7138
|
tokens[port.toString()] = {
|
|
6244
7139
|
token,
|
|
@@ -6278,7 +7173,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6278
7173
|
delete tokens[port.toString()];
|
|
6279
7174
|
await this.ctx.storage.put("portTokens", tokens);
|
|
6280
7175
|
}
|
|
6281
|
-
const sessionId = await this.
|
|
7176
|
+
const sessionId = this.serializeExecutionContext(await this.resolveExecution());
|
|
6282
7177
|
try {
|
|
6283
7178
|
await this.client.ports.unexposePort(port, sessionId);
|
|
6284
7179
|
} catch (error) {
|
|
@@ -6299,7 +7194,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6299
7194
|
}
|
|
6300
7195
|
}
|
|
6301
7196
|
async getExposedPorts(hostname) {
|
|
6302
|
-
const sessionId = await this.
|
|
7197
|
+
const sessionId = this.serializeExecutionContext(await this.resolveExecution());
|
|
6303
7198
|
const response = await this.client.ports.getExposedPorts(sessionId);
|
|
6304
7199
|
if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
6305
7200
|
const tokens = await this.readPortTokens();
|
|
@@ -6316,9 +7211,48 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6316
7211
|
}];
|
|
6317
7212
|
});
|
|
6318
7213
|
}
|
|
7214
|
+
/**
|
|
7215
|
+
* Namespaced tunnel API. Quick tunnels are zero-config preview URLs
|
|
7216
|
+
* backed by Cloudflare's trycloudflare service.
|
|
7217
|
+
*
|
|
7218
|
+
* - `tunnels.get(port)` — idempotent. Returns the cached tunnel for
|
|
7219
|
+
* `port` if one exists in DO storage, otherwise spawns a fresh
|
|
7220
|
+
* cloudflared process and persists the record.
|
|
7221
|
+
* - `tunnels.list()` — records currently known to this sandbox, from
|
|
7222
|
+
* DO storage.
|
|
7223
|
+
* - `tunnels.destroy(portOrInfo)` — tear down by port number or by
|
|
7224
|
+
* the record returned from `get()`.
|
|
7225
|
+
*
|
|
7226
|
+
* Storage is cleared on container restart (`onStart`), so URLs do
|
|
7227
|
+
* not survive a container restart — the next `get(port)` call will
|
|
7228
|
+
* spawn a fresh tunnel with a new URL.
|
|
7229
|
+
*
|
|
7230
|
+
* Requires the RPC transport. Calling this on a route-based transport
|
|
7231
|
+
* throws "RPC transport required".
|
|
7232
|
+
*/
|
|
7233
|
+
get tunnels() {
|
|
7234
|
+
this.ensureTunnelsBuilt();
|
|
7235
|
+
return this.tunnelsHandler;
|
|
7236
|
+
}
|
|
7237
|
+
/**
|
|
7238
|
+
* Lazily construct both the public tunnels handler and its sibling
|
|
7239
|
+
* exit-handler callback. Called from the `tunnels` getter on first
|
|
7240
|
+
* access and on every access after a transport swap clears both
|
|
7241
|
+
* fields.
|
|
7242
|
+
*/
|
|
7243
|
+
ensureTunnelsBuilt() {
|
|
7244
|
+
if (this.tunnelsHandler) return;
|
|
7245
|
+
const built = createTunnelsHandler({
|
|
7246
|
+
client: this.client,
|
|
7247
|
+
storage: this.ctx.storage,
|
|
7248
|
+
logger: this.logger
|
|
7249
|
+
});
|
|
7250
|
+
this.tunnelsHandler = built.tunnels;
|
|
7251
|
+
this.tunnelExitHandler = built.handleTunnelExit;
|
|
7252
|
+
}
|
|
6319
7253
|
async isPortExposed(port) {
|
|
6320
7254
|
try {
|
|
6321
|
-
const sessionId = await this.
|
|
7255
|
+
const sessionId = this.serializeExecutionContext(await this.resolveExecution());
|
|
6322
7256
|
return (await this.client.ports.getExposedPorts(sessionId)).ports.some((exposedPort) => exposedPort.port === port);
|
|
6323
7257
|
} catch (error) {
|
|
6324
7258
|
this.logger.error("Error checking if port is exposed", error instanceof Error ? error : new Error(String(error)), { port });
|
|
@@ -6378,6 +7312,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
6378
7312
|
*/
|
|
6379
7313
|
async createSession(options) {
|
|
6380
7314
|
const sessionId = options?.id || `session-${Date.now()}`;
|
|
7315
|
+
if (sessionId === DISABLE_SESSION_TOKEN) throw new Error(`Session ID '${DISABLE_SESSION_TOKEN}' is reserved for internal use`);
|
|
6381
7316
|
const filteredEnv = filterEnvVars({
|
|
6382
7317
|
...this.envVars,
|
|
6383
7318
|
...options?.env ?? {}
|
|
@@ -7585,8 +8520,24 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7585
8520
|
});
|
|
7586
8521
|
}
|
|
7587
8522
|
}
|
|
8523
|
+
async configureR2EgressOutbound(params) {
|
|
8524
|
+
const ctx = this.ctx;
|
|
8525
|
+
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
|
|
8526
|
+
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
8527
|
+
const fetcher = ctx.exports.ContainerProxy({ props: {
|
|
8528
|
+
enableInternet: this.enableInternet,
|
|
8529
|
+
containerId: this.ctx.id.toString(),
|
|
8530
|
+
className: R2_EGRESS_PROXY_TARGET_CLASS_NAME,
|
|
8531
|
+
outboundByHostOverrides: { "r2.internal": {
|
|
8532
|
+
method: "r2EgressMount",
|
|
8533
|
+
params
|
|
8534
|
+
} }
|
|
8535
|
+
} });
|
|
8536
|
+
if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
|
|
8537
|
+
await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
|
|
8538
|
+
}
|
|
7588
8539
|
};
|
|
7589
8540
|
|
|
7590
8541
|
//#endregion
|
|
7591
8542
|
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 };
|
|
7592
|
-
//# sourceMappingURL=sandbox-
|
|
8543
|
+
//# sourceMappingURL=sandbox-B-MUmsli.js.map
|