@cloudflare/sandbox 0.12.11 → 0.12.13
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 +8 -4
- package/dist/bridge/index.js +3 -3
- package/dist/{contexts-C186NnAB.d.ts → contexts-CxAJGHFf.d.ts} +6 -1
- package/dist/contexts-CxAJGHFf.d.ts.map +1 -0
- package/dist/{dist-C3AQh4A0.js → dist-CqPJfREN.js} +25 -4
- package/dist/{dist-C3AQh4A0.js.map → dist-CqPJfREN.js.map} +1 -1
- package/dist/{errors-CpkKPbWC.js → errors-CHhwDJNh.js} +11 -1
- package/dist/errors-CHhwDJNh.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/openai/index.d.ts +1 -1
- package/dist/openai/index.js +1 -1
- package/dist/opencode/index.d.ts +7 -3
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +32 -20
- package/dist/opencode/index.js.map +1 -1
- package/dist/{sandbox-DKbJiquI.js → sandbox-BGDk-c1Y.js} +328 -124
- package/dist/sandbox-BGDk-c1Y.js.map +1 -0
- package/dist/{sandbox-D-IV6AbV.d.ts → sandbox-mYLglwK1.d.ts} +42 -4
- package/dist/sandbox-mYLglwK1.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/contexts-C186NnAB.d.ts.map +0 -1
- package/dist/errors-CpkKPbWC.js.map +0 -1
- package/dist/sandbox-D-IV6AbV.d.ts.map +0 -1
- package/dist/sandbox-DKbJiquI.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-
|
|
1
|
+
import { S as partitionEnvVars, _ as DEFAULT_GIT_CLONE_TIMEOUT_MS, a as isWSStreamChunk, b as filterEnvVars, d as createLogger, f as createNoOpLogger, g as ResultImpl, h as Execution, i as isWSResponse, l as parseSSEFrames, m as logCanonicalEvent, n as generateRequestId, p as TraceContext, r as isWSError, t as WS_KEEPALIVE_EVENT, u as shellEscape, v as GitLogger, x as getEnvString, y as extractRepoName } from "./dist-CqPJfREN.js";
|
|
2
|
+
import { n as getHttpStatus, r as ErrorCode, t as getSuggestion } from "./errors-CHhwDJNh.js";
|
|
3
3
|
import { Container, ContainerProxy, getContainer, switchPort } from "@cloudflare/containers";
|
|
4
4
|
import { AwsClient } from "aws4fetch";
|
|
5
5
|
import { RpcSession, RpcTarget } from "capnweb";
|
|
@@ -1431,6 +1431,7 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1431
1431
|
pending.onFirstChunk = void 0;
|
|
1432
1432
|
}
|
|
1433
1433
|
if (pending.isStreaming) this.resetStreamIdleTimeout(chunk.id, pending);
|
|
1434
|
+
if (chunk.event === WS_KEEPALIVE_EVENT) return;
|
|
1434
1435
|
if (!pending.streamController) {
|
|
1435
1436
|
if (!pending.bufferedChunks) pending.bufferedChunks = [];
|
|
1436
1437
|
const encoder$1 = new TextEncoder();
|
|
@@ -1853,14 +1854,15 @@ var FileClient = class extends BaseHttpClient {
|
|
|
1853
1854
|
* @param path - File path to write to
|
|
1854
1855
|
* @param content - Content to write
|
|
1855
1856
|
* @param sessionId - The session ID for this operation
|
|
1856
|
-
* @param options - Optional settings (encoding)
|
|
1857
|
+
* @param options - Optional settings (encoding, ownerOnly)
|
|
1857
1858
|
*/
|
|
1858
1859
|
async writeFile(path$1, content, sessionId, options) {
|
|
1859
1860
|
const data = {
|
|
1860
1861
|
path: path$1,
|
|
1861
1862
|
content,
|
|
1862
1863
|
sessionId,
|
|
1863
|
-
encoding: options?.encoding
|
|
1864
|
+
encoding: options?.encoding,
|
|
1865
|
+
...options?.ownerOnly === true && { ownerOnly: true }
|
|
1864
1866
|
};
|
|
1865
1867
|
return await this.post("/api/write", data);
|
|
1866
1868
|
}
|
|
@@ -2602,6 +2604,19 @@ function isDurableObjectCodeUpdateReset(error) {
|
|
|
2602
2604
|
return false;
|
|
2603
2605
|
}
|
|
2604
2606
|
/**
|
|
2607
|
+
* Whether an error was raised because the Durable Object itself was reset, by
|
|
2608
|
+
* a code update or a storage startup failure, rather than by the operation
|
|
2609
|
+
* that observed it.
|
|
2610
|
+
*/
|
|
2611
|
+
function isDurableObjectReset(error) {
|
|
2612
|
+
for (const candidate of selfAndCauses(error)) {
|
|
2613
|
+
const message = errorMessageOf(candidate);
|
|
2614
|
+
if (SUPERSEDED_ISOLATE_PATTERN.test(message)) return true;
|
|
2615
|
+
if (DO_STORAGE_STARTUP_RESET_PATTERN.test(message)) return true;
|
|
2616
|
+
}
|
|
2617
|
+
return false;
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2605
2620
|
* Whether an error represents a transient platform lifecycle/storage failure,
|
|
2606
2621
|
* not an application-level sandbox failure.
|
|
2607
2622
|
*/
|
|
@@ -5304,8 +5319,12 @@ async function forwardPreviewRequest(tcpPort, request, lifecycle) {
|
|
|
5304
5319
|
};
|
|
5305
5320
|
} catch (error) {
|
|
5306
5321
|
settleForward();
|
|
5322
|
+
if (isDurableObjectReset(error)) throw error;
|
|
5307
5323
|
if (error instanceof Error && error.message.includes("Network connection lost.")) return { status: "network-lost" };
|
|
5308
|
-
|
|
5324
|
+
return {
|
|
5325
|
+
status: "failed",
|
|
5326
|
+
error
|
|
5327
|
+
};
|
|
5309
5328
|
}
|
|
5310
5329
|
}
|
|
5311
5330
|
function bridgePreviewWebSocket(response, lifecycle, settleForward) {
|
|
@@ -6772,6 +6791,23 @@ function isTunnelAlreadyRunningError(error) {
|
|
|
6772
6791
|
return hasErrorCode(error, "TUNNEL_ALREADY_RUNNING");
|
|
6773
6792
|
}
|
|
6774
6793
|
/**
|
|
6794
|
+
* The run holding `request.port` when the container rejected `request`
|
|
6795
|
+
* with a port conflict. Containers older than this SDK omit
|
|
6796
|
+
* `activeTunnelId`, which `stopTunnelRun` needs, so their conflicts yield
|
|
6797
|
+
* `null`.
|
|
6798
|
+
*/
|
|
6799
|
+
function conflictingRunOnPort(error, request) {
|
|
6800
|
+
if (!hasErrorCode(error, "TUNNEL_RUN_CONFLICT")) return null;
|
|
6801
|
+
const context = error.context;
|
|
6802
|
+
const runId = context?.activeRunId;
|
|
6803
|
+
const tunnelId = context?.activeTunnelId;
|
|
6804
|
+
if (context?.port !== request.port || typeof runId !== "string" || typeof tunnelId !== "string" || runId === request.runId) return null;
|
|
6805
|
+
return {
|
|
6806
|
+
tunnelId,
|
|
6807
|
+
runId
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
/**
|
|
6775
6811
|
* Concrete `TunnelsHandler` implementation.
|
|
6776
6812
|
*
|
|
6777
6813
|
* Extends `RpcTarget` for forward compatibility with direct Workers RPC
|
|
@@ -6885,6 +6921,7 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
6885
6921
|
const interrupted = translateTunnelInterruption(error, "provisioning", "unknown");
|
|
6886
6922
|
if (!interrupted || options?.name || !interrupted.context.retryable) throw error;
|
|
6887
6923
|
if (recoveryAttempts >= TUNNEL_GET_MAX_RECOVERY_ATTEMPTS) {
|
|
6924
|
+
await this.#stopAbandonedQuickRun(port, recovery.quickRun);
|
|
6888
6925
|
await this.#clearQuickTunnelStorage(port);
|
|
6889
6926
|
throw createTunnelInterruptedError({
|
|
6890
6927
|
reason: "recovery_exhausted",
|
|
@@ -6896,10 +6933,50 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
6896
6933
|
});
|
|
6897
6934
|
}
|
|
6898
6935
|
recoveryAttempts++;
|
|
6899
|
-
if (interrupted.context.reason === "runtime_replaced")
|
|
6936
|
+
if (interrupted.context.reason === "runtime_replaced") {
|
|
6937
|
+
await this.#stopAbandonedQuickRun(port, recovery.quickRun);
|
|
6938
|
+
recovery.quickRun = void 0;
|
|
6939
|
+
}
|
|
6900
6940
|
await this.#clearQuickTunnelStorage(port);
|
|
6901
6941
|
}
|
|
6902
6942
|
}
|
|
6943
|
+
/**
|
|
6944
|
+
* Stop a quick tunnel run that `get()` is giving up on, so its
|
|
6945
|
+
* cloudflared does not keep holding the port. Best effort: the run may
|
|
6946
|
+
* have died with a replaced container.
|
|
6947
|
+
*/
|
|
6948
|
+
async #stopAbandonedQuickRun(port, run) {
|
|
6949
|
+
if (!run) return;
|
|
6950
|
+
try {
|
|
6951
|
+
await this.#host.client.tunnels.stopTunnelRun(run);
|
|
6952
|
+
} catch (error) {
|
|
6953
|
+
this.#host.logger.warn("tunnel.get: abandoned run cleanup failed", {
|
|
6954
|
+
port,
|
|
6955
|
+
...run,
|
|
6956
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
/**
|
|
6961
|
+
* Start or replay a quick tunnel run. The Durable Object owns every
|
|
6962
|
+
* tunnel run in its container and holds the port lock, so a different
|
|
6963
|
+
* run already on the port lost its storage record. Stop it and retry
|
|
6964
|
+
* once instead of failing every later `get()` for the port.
|
|
6965
|
+
*/
|
|
6966
|
+
async #ensureQuickTunnelRun(request) {
|
|
6967
|
+
try {
|
|
6968
|
+
return await this.#host.client.tunnels.ensureTunnelRun(request);
|
|
6969
|
+
} catch (error) {
|
|
6970
|
+
const orphan = conflictingRunOnPort(error, request);
|
|
6971
|
+
if (!orphan) throw error;
|
|
6972
|
+
this.#host.logger.warn("tunnel.get: stopping orphaned run on port", {
|
|
6973
|
+
port: request.port,
|
|
6974
|
+
...orphan
|
|
6975
|
+
});
|
|
6976
|
+
await this.#host.client.tunnels.stopTunnelRun(orphan);
|
|
6977
|
+
return await this.#host.client.tunnels.ensureTunnelRun(request);
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6903
6980
|
async #clearQuickTunnelStorage(port) {
|
|
6904
6981
|
await this.#host.storage.transaction(async (txn) => {
|
|
6905
6982
|
const map = await readMap(txn);
|
|
@@ -6980,7 +7057,7 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
6980
7057
|
let ensureResult;
|
|
6981
7058
|
let runtime = runtimeBeforeAdmission;
|
|
6982
7059
|
try {
|
|
6983
|
-
ensureResult = await this.#
|
|
7060
|
+
ensureResult = await this.#ensureQuickTunnelRun({
|
|
6984
7061
|
tunnelId,
|
|
6985
7062
|
runId,
|
|
6986
7063
|
mode: "quick",
|
|
@@ -7209,13 +7286,33 @@ var TunnelsRpcTarget = class extends RpcTarget$1 {
|
|
|
7209
7286
|
return Object.values(map);
|
|
7210
7287
|
}
|
|
7211
7288
|
};
|
|
7212
|
-
|
|
7289
|
+
/**
|
|
7290
|
+
* Per-port serialization lock. Any operation that mutates the tunnel for
|
|
7291
|
+
* a given port — get() on a cache miss, destroy(), the exit callback —
|
|
7292
|
+
* queues behind the previous operation on the same port. Two
|
|
7293
|
+
* consequences:
|
|
7294
|
+
*
|
|
7295
|
+
* - get(8080) followed by destroy(8080) is well-ordered: the destroy
|
|
7296
|
+
* observes whatever the get just wrote, even though both yield to
|
|
7297
|
+
* external RPCs in the middle.
|
|
7298
|
+
* - Two concurrent get(8080) calls share the first call's record: the
|
|
7299
|
+
* second runs after the first writes storage and takes the hit branch
|
|
7300
|
+
* (so no double cloudflared spawn).
|
|
7301
|
+
*
|
|
7302
|
+
* The lock is a plain Promise chain keyed by port. `transaction()` on the
|
|
7303
|
+
* storage write still matters for *cross-port* writes — a get(8080) and
|
|
7304
|
+
* get(8081) running in parallel are independent here.
|
|
7305
|
+
*/
|
|
7306
|
+
function createPortLock() {
|
|
7213
7307
|
const portLocks = /* @__PURE__ */ new Map();
|
|
7214
|
-
|
|
7308
|
+
return (port, fn) => {
|
|
7215
7309
|
const next = (portLocks.get(port) ?? Promise.resolve()).then(fn, fn);
|
|
7216
7310
|
portLocks.set(port, next.catch(() => void 0));
|
|
7217
7311
|
return next;
|
|
7218
7312
|
};
|
|
7313
|
+
}
|
|
7314
|
+
function createTunnelsHandler(host) {
|
|
7315
|
+
const withPortLock = host.withPortLock ?? createPortLock();
|
|
7219
7316
|
const tunnels = new TunnelsRpcTarget(host, withPortLock);
|
|
7220
7317
|
const handleTunnelExit = async (id, port, exitCode, runId) => {
|
|
7221
7318
|
const startTime = Date.now();
|
|
@@ -7302,7 +7399,7 @@ function createTunnelsHandler(host) {
|
|
|
7302
7399
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
7303
7400
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
7304
7401
|
*/
|
|
7305
|
-
const SDK_VERSION = "0.12.
|
|
7402
|
+
const SDK_VERSION = "0.12.13";
|
|
7306
7403
|
|
|
7307
7404
|
//#endregion
|
|
7308
7405
|
//#region src/sandbox.ts
|
|
@@ -7311,12 +7408,32 @@ const ACTIVE_PREVIEW_PORTS_STORAGE_KEY = "activePreviewPorts";
|
|
|
7311
7408
|
const CONTAINER_PROXY_CLASS_NAME = "ContainerProxy";
|
|
7312
7409
|
const S3_CREDENTIAL_PROXY_HOST = "s3-credential-proxy.internal";
|
|
7313
7410
|
const S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST = "s3-credential-proxy.sandbox.test";
|
|
7314
|
-
|
|
7315
|
-
Object.defineProperty(ContainerProxyOutboundTarget, "name", { value: CONTAINER_PROXY_CLASS_NAME });
|
|
7316
|
-
ContainerProxyOutboundTarget.outboundHandlers = {
|
|
7411
|
+
const SDK_OUTBOUND_HANDLERS = {
|
|
7317
7412
|
r2EgressMount: r2EgressHandler,
|
|
7318
7413
|
s3CredentialProxyMount: s3CredentialProxyHandler
|
|
7319
7414
|
};
|
|
7415
|
+
var ContainerProxyOutboundTarget = class extends Container {};
|
|
7416
|
+
Object.defineProperty(ContainerProxyOutboundTarget, "name", { value: CONTAINER_PROXY_CLASS_NAME });
|
|
7417
|
+
ContainerProxyOutboundTarget.outboundHandlers = SDK_OUTBOUND_HANDLERS;
|
|
7418
|
+
function noRestoredMountRoutes() {
|
|
7419
|
+
return {
|
|
7420
|
+
r2Buckets: {},
|
|
7421
|
+
credentialProxyMounts: {}
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7424
|
+
/**
|
|
7425
|
+
* Read the bucket mount routes that the base Container restored from storage
|
|
7426
|
+
* in its constructor. `outboundByHostOverrides` is private in its types.
|
|
7427
|
+
*/
|
|
7428
|
+
function readRestoredMountRoutes(container) {
|
|
7429
|
+
const overrides = container.outboundByHostOverrides ?? {};
|
|
7430
|
+
const r2 = overrides["r2.internal"];
|
|
7431
|
+
const credentialProxy = overrides[S3_CREDENTIAL_PROXY_HOST];
|
|
7432
|
+
return {
|
|
7433
|
+
r2Buckets: r2?.method === "r2EgressMount" ? { ...r2.params?.buckets } : {},
|
|
7434
|
+
credentialProxyMounts: credentialProxy?.method === "s3CredentialProxyMount" ? { ...credentialProxy.params?.mounts } : {}
|
|
7435
|
+
};
|
|
7436
|
+
}
|
|
7320
7437
|
/**
|
|
7321
7438
|
* SDK-level ContainerProxy that directly dispatches SDK-internal mount hosts
|
|
7322
7439
|
* (r2.internal, s3-credential-proxy.internal) without relying on
|
|
@@ -7644,6 +7761,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7644
7761
|
tunnelsHandler = null;
|
|
7645
7762
|
tunnelExitHandler = null;
|
|
7646
7763
|
destroyAllTunnels = null;
|
|
7764
|
+
tunnelPortLock = createPortLock();
|
|
7647
7765
|
controlCallback;
|
|
7648
7766
|
normalizeId = false;
|
|
7649
7767
|
defaultSession = null;
|
|
@@ -7653,6 +7771,11 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7653
7771
|
logger;
|
|
7654
7772
|
keepAliveEnabled = false;
|
|
7655
7773
|
activeMounts = /* @__PURE__ */ new Map();
|
|
7774
|
+
restoredMountRoutes = noRestoredMountRoutes();
|
|
7775
|
+
staleMountRoutes = {
|
|
7776
|
+
r2: false,
|
|
7777
|
+
credentialProxy: false
|
|
7778
|
+
};
|
|
7656
7779
|
mountOperationQueue = Promise.resolve();
|
|
7657
7780
|
currentRuntime;
|
|
7658
7781
|
currentLifetime;
|
|
@@ -7801,7 +7924,15 @@ var Sandbox = class Sandbox extends Container {
|
|
|
7801
7924
|
return this.createSandboxClient();
|
|
7802
7925
|
}
|
|
7803
7926
|
constructor(ctx, env$1) {
|
|
7927
|
+
if (ctx.exports?.ContainerProxy) {
|
|
7928
|
+
const registry = new.target;
|
|
7929
|
+
registry.outboundHandlers = {
|
|
7930
|
+
...registry.outboundHandlers,
|
|
7931
|
+
...SDK_OUTBOUND_HANDLERS
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7804
7934
|
super(ctx, env$1);
|
|
7935
|
+
this.restoredMountRoutes = readRestoredMountRoutes(this);
|
|
7805
7936
|
const envObj = env$1;
|
|
7806
7937
|
["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
|
|
7807
7938
|
if (envObj?.[key]) this.envVars[key] = String(envObj[key]);
|
|
@@ -8179,23 +8310,18 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8179
8310
|
const prefix = options.prefix;
|
|
8180
8311
|
let mountOutcome = "error";
|
|
8181
8312
|
let mountError;
|
|
8182
|
-
let passwordFilePath;
|
|
8183
|
-
let additionalHeaderFilePath;
|
|
8184
8313
|
try {
|
|
8185
8314
|
validateBucketBindingName(bucket, mountPath);
|
|
8186
8315
|
this.validateMountPath(mountPath);
|
|
8187
8316
|
this.validateProtectedS3fsOptions(options.s3fsOptions, "R2 binding");
|
|
8317
|
+
const restoredRoute = this.restoredMountRoutes.r2Buckets[bucket];
|
|
8318
|
+
if (restoredRoute && (restoredRoute.prefix !== prefix || (restoredRoute.readOnly ?? false) !== (options.readOnly ?? false))) throw new InvalidMountConfigError(`R2 binding "${bucket}" is already mounted with a different prefix or readOnly setting. Mount the same binding only once, or use the same settings for additional mounts.`);
|
|
8188
8319
|
for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
|
|
8189
8320
|
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.`);
|
|
8190
8321
|
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.`);
|
|
8191
8322
|
}
|
|
8192
|
-
passwordFilePath = this.generatePasswordFilePath();
|
|
8193
|
-
additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
|
|
8194
|
-
await this.createPasswordFile(passwordFilePath, bucket, {
|
|
8195
|
-
accessKeyId: "x",
|
|
8196
|
-
secretAccessKey: "x"
|
|
8197
|
-
});
|
|
8198
|
-
await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
|
|
8323
|
+
const passwordFilePath = this.generatePasswordFilePath();
|
|
8324
|
+
const additionalHeaderFilePath = this.generateS3FSAdditionalHeaderFilePath();
|
|
8199
8325
|
const mountInfo = {
|
|
8200
8326
|
mountId: crypto.randomUUID(),
|
|
8201
8327
|
mountType: "r2-egress",
|
|
@@ -8208,48 +8334,47 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8208
8334
|
readOnly: options.readOnly ?? false
|
|
8209
8335
|
};
|
|
8210
8336
|
this.activeMounts.set(mountPath, mountInfo);
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8337
|
+
try {
|
|
8338
|
+
await this.createPasswordFile(passwordFilePath, bucket, {
|
|
8339
|
+
accessKeyId: "x",
|
|
8340
|
+
secretAccessKey: "x"
|
|
8341
|
+
});
|
|
8342
|
+
await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
|
|
8343
|
+
await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
8344
|
+
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
8345
|
+
const s3fsSource = bucket;
|
|
8346
|
+
const optionsStr = shellEscape(serializeS3fsOptions({
|
|
8347
|
+
passwd_file: passwordFilePath,
|
|
8348
|
+
...R2_DEFAULT_S3FS_OPTIONS,
|
|
8349
|
+
...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
|
|
8350
|
+
use_path_request_style: true,
|
|
8351
|
+
url: "http://r2.internal",
|
|
8352
|
+
ahbe_conf: additionalHeaderFilePath,
|
|
8353
|
+
...options.readOnly ? { ro: true } : {}
|
|
8354
|
+
}));
|
|
8355
|
+
const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
8356
|
+
this.logger.debug("r2-egress: running s3fs", { mountCmd });
|
|
8357
|
+
const result = await this.execInternal(mountCmd);
|
|
8358
|
+
this.logger.debug("r2-egress: s3fs exited", {
|
|
8359
|
+
exitCode: result.exitCode,
|
|
8360
|
+
stdout: result.stdout,
|
|
8361
|
+
stderr: result.stderr
|
|
8362
|
+
});
|
|
8363
|
+
if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
|
|
8364
|
+
const mountpointCheck = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && echo 'FUSE_MOUNTED' || echo 'NOT_FUSE_MOUNTED'`);
|
|
8365
|
+
this.logger.debug("r2-egress: mountpoint check", {
|
|
8366
|
+
stdout: mountpointCheck.stdout.trim(),
|
|
8367
|
+
exitCode: mountpointCheck.exitCode
|
|
8368
|
+
});
|
|
8369
|
+
if (mountpointCheck.stdout.trim() !== "FUSE_MOUNTED") throw new S3FSMountError(`s3fs exited 0 but mount was not established at ${mountPath}`);
|
|
8370
|
+
} catch (error) {
|
|
8371
|
+
await this.releaseFailedR2EgressMount(mountInfo);
|
|
8372
|
+
throw error;
|
|
8373
|
+
}
|
|
8238
8374
|
mountInfo.mounted = true;
|
|
8239
8375
|
mountOutcome = "success";
|
|
8240
8376
|
} catch (error) {
|
|
8241
8377
|
mountError = error instanceof Error ? error : new Error(String(error));
|
|
8242
|
-
const failedMount = this.activeMounts.get(mountPath);
|
|
8243
|
-
this.activeMounts.delete(mountPath);
|
|
8244
|
-
if (failedMount?.mountType === "r2-egress") {
|
|
8245
|
-
await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
|
|
8246
|
-
if (failedMount.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(failedMount.additionalHeaderFilePath).catch(() => {});
|
|
8247
|
-
} else {
|
|
8248
|
-
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath).catch(() => {});
|
|
8249
|
-
if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath).catch(() => {});
|
|
8250
|
-
}
|
|
8251
|
-
const remainingParams = this.getR2EgressParams();
|
|
8252
|
-
await this.configureR2EgressOutbound(remainingParams).catch(() => {});
|
|
8253
8378
|
throw error;
|
|
8254
8379
|
} finally {
|
|
8255
8380
|
logCanonicalEvent(this.logger, {
|
|
@@ -8265,6 +8390,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8265
8390
|
}
|
|
8266
8391
|
}
|
|
8267
8392
|
/**
|
|
8393
|
+
* Undo a failed R2 binding mount: drop its reservation and support files,
|
|
8394
|
+
* then rebuild the `r2.internal` routes from the remaining mounts.
|
|
8395
|
+
*/
|
|
8396
|
+
async releaseFailedR2EgressMount(mountInfo) {
|
|
8397
|
+
this.activeMounts.delete(mountInfo.mountPath);
|
|
8398
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
8399
|
+
if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
|
|
8400
|
+
await this.configureR2EgressOutbound(this.getR2EgressParams()).catch(() => {});
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8268
8403
|
* Production mount: S3FS-FUSE inside the container
|
|
8269
8404
|
*/
|
|
8270
8405
|
async mountBucketFuse(bucket, mountPath, options) {
|
|
@@ -8272,10 +8407,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8272
8407
|
const prefix = options.prefix;
|
|
8273
8408
|
let mountOutcome = "error";
|
|
8274
8409
|
let mountError;
|
|
8275
|
-
let passwordFilePath;
|
|
8276
|
-
let additionalHeaderFilePath;
|
|
8277
8410
|
let provider = null;
|
|
8278
|
-
let dirExisted = true;
|
|
8279
8411
|
try {
|
|
8280
8412
|
this.validateMountOptions(bucket, mountPath, {
|
|
8281
8413
|
...options,
|
|
@@ -8297,8 +8429,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8297
8429
|
});
|
|
8298
8430
|
const credentialProxyEnabled = options.credentialProxy === true;
|
|
8299
8431
|
if (credentialProxyEnabled) this.validateProtectedS3fsOptions(options.s3fsOptions, "credential proxy", ["ahbe_conf", "use_path_request_style"]);
|
|
8300
|
-
passwordFilePath = this.generatePasswordFilePath();
|
|
8301
|
-
|
|
8432
|
+
const passwordFilePath = this.generatePasswordFilePath();
|
|
8433
|
+
const additionalHeaderFilePath = credentialProxyEnabled ? this.generateS3FSAdditionalHeaderFilePath() : void 0;
|
|
8302
8434
|
const mountId = crypto.randomUUID();
|
|
8303
8435
|
const mountInfo = {
|
|
8304
8436
|
mountId,
|
|
@@ -8321,55 +8453,37 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8321
8453
|
} } : {}
|
|
8322
8454
|
};
|
|
8323
8455
|
this.activeMounts.set(mountPath, mountInfo);
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8456
|
+
let dirExisted = true;
|
|
8457
|
+
try {
|
|
8458
|
+
await this.createPasswordFile(passwordFilePath, bucket, credentialProxyEnabled ? {
|
|
8459
|
+
accessKeyId: "x",
|
|
8460
|
+
secretAccessKey: "x"
|
|
8461
|
+
} : credentials);
|
|
8462
|
+
if (credentialProxyEnabled) {
|
|
8463
|
+
if (additionalHeaderFilePath) await this.createDisableExpectHeaderFile(additionalHeaderFilePath);
|
|
8464
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams());
|
|
8465
|
+
}
|
|
8466
|
+
dirExisted = (await this.execInternal(`test -d ${shellEscape(mountPath)}`)).exitCode === 0;
|
|
8467
|
+
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
8468
|
+
const effectiveOptions = credentialProxyEnabled ? {
|
|
8469
|
+
...options,
|
|
8470
|
+
endpoint: `http://${S3_CREDENTIAL_PROXY_HOST}/${mountId}`,
|
|
8471
|
+
s3fsOptions: [
|
|
8472
|
+
...provider === "r2" ? R2_DEFAULT_S3FS_OPTION_ENTRIES : [],
|
|
8473
|
+
...options.s3fsOptions ?? [],
|
|
8474
|
+
...additionalHeaderFilePath ? [`ahbe_conf=${additionalHeaderFilePath}`] : [],
|
|
8475
|
+
"use_path_request_style"
|
|
8476
|
+
]
|
|
8477
|
+
} : options;
|
|
8478
|
+
await this.executeS3FSMount(s3fsSource, mountPath, effectiveOptions, provider, passwordFilePath);
|
|
8479
|
+
} catch (error) {
|
|
8480
|
+
await this.releaseFailedFuseMount(mountInfo, !dirExisted);
|
|
8481
|
+
throw error;
|
|
8331
8482
|
}
|
|
8332
|
-
dirExisted = (await this.execInternal(`test -d ${shellEscape(mountPath)}`)).exitCode === 0;
|
|
8333
|
-
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
8334
|
-
const effectiveOptions = credentialProxyEnabled ? {
|
|
8335
|
-
...options,
|
|
8336
|
-
endpoint: `http://${S3_CREDENTIAL_PROXY_HOST}/${mountId}`,
|
|
8337
|
-
s3fsOptions: [
|
|
8338
|
-
...provider === "r2" ? R2_DEFAULT_S3FS_OPTION_ENTRIES : [],
|
|
8339
|
-
...options.s3fsOptions ?? [],
|
|
8340
|
-
...additionalHeaderFilePath ? [`ahbe_conf=${additionalHeaderFilePath}`] : [],
|
|
8341
|
-
"use_path_request_style"
|
|
8342
|
-
]
|
|
8343
|
-
} : options;
|
|
8344
|
-
await this.executeS3FSMount(s3fsSource, mountPath, effectiveOptions, provider, passwordFilePath);
|
|
8345
8483
|
mountInfo.mounted = true;
|
|
8346
8484
|
mountOutcome = "success";
|
|
8347
8485
|
} catch (error) {
|
|
8348
8486
|
mountError = error instanceof Error ? error : new Error(String(error));
|
|
8349
|
-
try {
|
|
8350
|
-
await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && fusermount -u ${shellEscape(mountPath)}`);
|
|
8351
|
-
} catch {}
|
|
8352
|
-
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
|
|
8353
|
-
if (additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(additionalHeaderFilePath);
|
|
8354
|
-
if (!dirExisted) try {
|
|
8355
|
-
await this.execInternal(`rmdir ${shellEscape(mountPath)} 2>/dev/null`);
|
|
8356
|
-
} catch {}
|
|
8357
|
-
const failedMount = this.activeMounts.get(mountPath);
|
|
8358
|
-
if (failedMount?.mountType === "fuse" && failedMount.credentialProxy) try {
|
|
8359
|
-
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: failedMount.mountId }));
|
|
8360
|
-
this.activeMounts.delete(mountPath);
|
|
8361
|
-
evictSigV4ClientCacheEntry(failedMount.mountId);
|
|
8362
|
-
evictDirectoryMarkerCacheForMount(failedMount.mountId);
|
|
8363
|
-
} catch (cleanupError) {
|
|
8364
|
-
this.logger.warn("credential proxy cleanup failed", {
|
|
8365
|
-
mountPath,
|
|
8366
|
-
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
8367
|
-
});
|
|
8368
|
-
this.activeMounts.delete(mountPath);
|
|
8369
|
-
evictSigV4ClientCacheEntry(failedMount.mountId);
|
|
8370
|
-
evictDirectoryMarkerCacheForMount(failedMount.mountId);
|
|
8371
|
-
}
|
|
8372
|
-
else this.activeMounts.delete(mountPath);
|
|
8373
8487
|
throw error;
|
|
8374
8488
|
} finally {
|
|
8375
8489
|
logCanonicalEvent(this.logger, {
|
|
@@ -8385,6 +8499,35 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8385
8499
|
}
|
|
8386
8500
|
}
|
|
8387
8501
|
/**
|
|
8502
|
+
* Undo a failed FUSE mount: unmount anything s3fs established, then drop
|
|
8503
|
+
* its support files, the directory if this mount created it, and its
|
|
8504
|
+
* reservation.
|
|
8505
|
+
*/
|
|
8506
|
+
async releaseFailedFuseMount(mountInfo, removeMountDirectory) {
|
|
8507
|
+
const { mountPath } = mountInfo;
|
|
8508
|
+
try {
|
|
8509
|
+
await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && fusermount -u ${shellEscape(mountPath)}`);
|
|
8510
|
+
} catch {}
|
|
8511
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
8512
|
+
if (mountInfo.additionalHeaderFilePath) await this.deleteAdditionalHeaderFile(mountInfo.additionalHeaderFilePath);
|
|
8513
|
+
if (removeMountDirectory) try {
|
|
8514
|
+
await this.execInternal(`rmdir ${shellEscape(mountPath)} 2>/dev/null`);
|
|
8515
|
+
} catch {}
|
|
8516
|
+
if (mountInfo.credentialProxy) {
|
|
8517
|
+
try {
|
|
8518
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams({ excludeMountId: mountInfo.mountId }));
|
|
8519
|
+
} catch (cleanupError) {
|
|
8520
|
+
this.logger.warn("credential proxy cleanup failed", {
|
|
8521
|
+
mountPath,
|
|
8522
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
8523
|
+
});
|
|
8524
|
+
}
|
|
8525
|
+
evictSigV4ClientCacheEntry(mountInfo.mountId);
|
|
8526
|
+
evictDirectoryMarkerCacheForMount(mountInfo.mountId);
|
|
8527
|
+
}
|
|
8528
|
+
this.activeMounts.delete(mountPath);
|
|
8529
|
+
}
|
|
8530
|
+
/**
|
|
8388
8531
|
* Manually unmount a bucket filesystem
|
|
8389
8532
|
*
|
|
8390
8533
|
* @param mountPath - Absolute path where the bucket is mounted
|
|
@@ -8524,11 +8667,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8524
8667
|
}
|
|
8525
8668
|
/**
|
|
8526
8669
|
* Create s3fs ahbe_conf file that suppresses the Expect: 100-continue header.
|
|
8527
|
-
* Restricted to 0600 so s3fs will accept it (same requirement as passwd files).
|
|
8528
8670
|
*/
|
|
8529
8671
|
async createDisableExpectHeaderFile(headerFilePath) {
|
|
8530
|
-
await this.
|
|
8531
|
-
await this.execInternal(`chmod 0600 ${shellEscape(headerFilePath)}`);
|
|
8672
|
+
await this.writeOwnerOnlyFile(headerFilePath, S3FS_DISABLE_EXPECT_HEADER_CONFIG);
|
|
8532
8673
|
}
|
|
8533
8674
|
/**
|
|
8534
8675
|
* Create password file with s3fs credentials
|
|
@@ -8536,8 +8677,16 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8536
8677
|
*/
|
|
8537
8678
|
async createPasswordFile(passwordFilePath, bucket, credentials) {
|
|
8538
8679
|
const content = `${bucket}:${credentials.accessKeyId}:${credentials.secretAccessKey}`;
|
|
8539
|
-
await this.
|
|
8540
|
-
|
|
8680
|
+
await this.writeOwnerOnlyFile(passwordFilePath, content);
|
|
8681
|
+
}
|
|
8682
|
+
/**
|
|
8683
|
+
* s3fs refuses support files that other users can read. Create them with
|
|
8684
|
+
* mode 0600 so the password file is never readable while it is being
|
|
8685
|
+
* written. The chmod covers container images that ignore `ownerOnly`.
|
|
8686
|
+
*/
|
|
8687
|
+
async writeOwnerOnlyFile(path$1, content) {
|
|
8688
|
+
await this.client.files.writeFile(path$1, content, DISABLE_SESSION_TOKEN, { ownerOnly: true });
|
|
8689
|
+
await this.execInternal(`chmod 0600 ${shellEscape(path$1)}`);
|
|
8541
8690
|
}
|
|
8542
8691
|
/**
|
|
8543
8692
|
* Delete password file
|
|
@@ -8705,6 +8854,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8705
8854
|
async onStart() {
|
|
8706
8855
|
this.logger.debug("Sandbox started");
|
|
8707
8856
|
await this.currentRuntime.markStarted();
|
|
8857
|
+
await this.removeStaleMountRoutes();
|
|
8708
8858
|
this.checkVersionCompatibility().catch((error) => {
|
|
8709
8859
|
this.logger.error("Version compatibility check failed", error instanceof Error ? error : new Error(String(error)));
|
|
8710
8860
|
});
|
|
@@ -8786,6 +8936,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8786
8936
|
this.containerGeneration++;
|
|
8787
8937
|
this.defaultSession = null;
|
|
8788
8938
|
this.defaultSessionInit = null;
|
|
8939
|
+
const disconnectClient = this.transport !== "rpc" || this.client.isWebSocketConnected();
|
|
8789
8940
|
await this.currentRuntime.clear();
|
|
8790
8941
|
await this.clearActivePreviewPorts();
|
|
8791
8942
|
try {
|
|
@@ -8793,7 +8944,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8793
8944
|
} catch (error) {
|
|
8794
8945
|
this.logger.error("Failed to reconcile tunnel storage after container stop", error instanceof Error ? error : new Error(String(error)));
|
|
8795
8946
|
}
|
|
8796
|
-
this.client.disconnect(this.buildDisconnectCause("runtime_replaced", "The sandbox container stopped while the operation was pending.", params));
|
|
8947
|
+
if (disconnectClient) this.client.disconnect(this.buildDisconnectCause("runtime_replaced", "The sandbox container stopped while the operation was pending.", params));
|
|
8948
|
+
await this.releaseRestoredMountRoutes();
|
|
8797
8949
|
let hadR2EgressMount = false;
|
|
8798
8950
|
let hadCredentialProxyMount = false;
|
|
8799
8951
|
for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
|
|
@@ -8941,6 +9093,13 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8941
9093
|
/**
|
|
8942
9094
|
* Explicit container-start hook for the RPC transport.
|
|
8943
9095
|
*
|
|
9096
|
+
* The control connection calls this hook every time it connects, including
|
|
9097
|
+
* when it reconnects after an idle disconnect. A running, healthy container
|
|
9098
|
+
* is left alone, as in {@link containerFetch}: the base class runs
|
|
9099
|
+
* `onStart()` on every `startAndWaitForPorts()`, and `onStart()` treats the
|
|
9100
|
+
* container as new, recording a new runtime identity and dropping quick
|
|
9101
|
+
* tunnels from storage.
|
|
9102
|
+
*
|
|
8944
9103
|
* Runs `startAndWaitForPorts` under a short instance-get budget
|
|
8945
9104
|
* ({@link RPC_START_INSTANCE_GET_TIMEOUT_MS}) so a single attempt fails fast
|
|
8946
9105
|
* under capacity pressure — the base class throws the classifiable
|
|
@@ -8960,6 +9119,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
8960
9119
|
*/
|
|
8961
9120
|
async startContainerForRPC() {
|
|
8962
9121
|
try {
|
|
9122
|
+
if ((await this.getState()).status === "healthy" && this.ctx.container?.running !== false) return;
|
|
8963
9123
|
await this.startAndWaitForPorts({
|
|
8964
9124
|
ports: 3e3,
|
|
8965
9125
|
cancellationOptions: {
|
|
@@ -9175,6 +9335,14 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9175
9335
|
if (!await this.currentRuntime.isActive(runtime)) return this.stalePreviewURLResponse();
|
|
9176
9336
|
return new Response("Container suddenly disconnected, try again", { status: 500 });
|
|
9177
9337
|
}
|
|
9338
|
+
if (result.status === "failed") {
|
|
9339
|
+
if (!await this.currentRuntime.isActive(runtime)) return this.stalePreviewURLResponse();
|
|
9340
|
+
this.logger.error("Preview request to container port failed", result.error instanceof Error ? result.error : new Error(String(result.error)), {
|
|
9341
|
+
port,
|
|
9342
|
+
method: request.method
|
|
9343
|
+
});
|
|
9344
|
+
return new Response(`Could not reach port ${port} in the container`, { status: 502 });
|
|
9345
|
+
}
|
|
9178
9346
|
return result.response;
|
|
9179
9347
|
}
|
|
9180
9348
|
buildPreviewProxyRequest(request, port, sandboxId) {
|
|
@@ -9186,10 +9354,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
9186
9354
|
headers.set("X-Forwarded-Host", url.hostname);
|
|
9187
9355
|
headers.set("X-Forwarded-Proto", url.protocol.replace(":", ""));
|
|
9188
9356
|
headers.set("X-Sandbox-Name", this.sandboxName ?? sandboxId);
|
|
9189
|
-
if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return new Request(request, {
|
|
9190
|
-
headers,
|
|
9191
|
-
redirect: "manual"
|
|
9192
|
-
});
|
|
9193
9357
|
return new Request(proxyUrl, {
|
|
9194
9358
|
method: request.method,
|
|
9195
9359
|
headers,
|
|
@@ -10174,6 +10338,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
10174
10338
|
sandboxId: this.ctx.id.toString(),
|
|
10175
10339
|
currentRuntime: this.currentRuntime,
|
|
10176
10340
|
currentLifetime: this.currentLifetime,
|
|
10341
|
+
withPortLock: this.tunnelPortLock,
|
|
10177
10342
|
getNamedTunnelConfig: async () => {
|
|
10178
10343
|
const envObj = this.env;
|
|
10179
10344
|
const token = getEnvString(envObj, "CLOUDFLARE_API_TOKEN");
|
|
@@ -11620,11 +11785,47 @@ var Sandbox = class Sandbox extends Container {
|
|
|
11620
11785
|
});
|
|
11621
11786
|
}
|
|
11622
11787
|
}
|
|
11623
|
-
|
|
11788
|
+
/**
|
|
11789
|
+
* Drop the routes restored for the mounts of a container that has stopped.
|
|
11790
|
+
*/
|
|
11791
|
+
async releaseRestoredMountRoutes() {
|
|
11792
|
+
const { r2Buckets, credentialProxyMounts } = this.restoredMountRoutes;
|
|
11793
|
+
this.restoredMountRoutes = noRestoredMountRoutes();
|
|
11794
|
+
for (const mountId of Object.keys(credentialProxyMounts)) {
|
|
11795
|
+
evictSigV4ClientCacheEntry(mountId);
|
|
11796
|
+
evictDirectoryMarkerCacheForMount(mountId);
|
|
11797
|
+
}
|
|
11798
|
+
if (Object.keys(r2Buckets).length > 0) this.staleMountRoutes.r2 = true;
|
|
11799
|
+
if (Object.keys(credentialProxyMounts).length > 0) this.staleMountRoutes.credentialProxy = true;
|
|
11800
|
+
await this.removeStaleMountRoutes();
|
|
11801
|
+
}
|
|
11802
|
+
/**
|
|
11803
|
+
* Rewrite the routes that still carry a stopped container's restored
|
|
11804
|
+
* mounts so they cover only the mounts in activeMounts. A failed update
|
|
11805
|
+
* stays marked and is retried on the next container start or stop.
|
|
11806
|
+
*/
|
|
11807
|
+
async removeStaleMountRoutes() {
|
|
11808
|
+
if (this.staleMountRoutes.r2) try {
|
|
11809
|
+
await this.configureR2EgressOutbound(this.getR2EgressParams());
|
|
11810
|
+
this.staleMountRoutes.r2 = false;
|
|
11811
|
+
} catch (error) {
|
|
11812
|
+
this.logger.warn("Failed to remove stale R2 mount routes", { error: error instanceof Error ? error.message : String(error) });
|
|
11813
|
+
}
|
|
11814
|
+
if (this.staleMountRoutes.credentialProxy) try {
|
|
11815
|
+
await this.configureS3CredentialProxyOutbound(this.getS3CredentialProxyParams());
|
|
11816
|
+
this.staleMountRoutes.credentialProxy = false;
|
|
11817
|
+
} catch (error) {
|
|
11818
|
+
this.logger.warn("Failed to remove stale credential proxy mount routes", { error: error instanceof Error ? error.message : String(error) });
|
|
11819
|
+
}
|
|
11820
|
+
}
|
|
11821
|
+
async configureR2EgressOutbound(activeParams) {
|
|
11822
|
+
const params = { buckets: {
|
|
11823
|
+
...this.restoredMountRoutes.r2Buckets,
|
|
11824
|
+
...activeParams.buckets
|
|
11825
|
+
} };
|
|
11624
11826
|
const ctx = this.ctx;
|
|
11625
11827
|
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
|
|
11626
11828
|
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
11627
|
-
this.constructor.outboundHandlers = { r2EgressMount: r2EgressHandler };
|
|
11628
11829
|
if (Object.keys(params.buckets).length > 0) await this.setOutboundByHost("r2.internal", "r2EgressMount", params);
|
|
11629
11830
|
else await this.removeOutboundByHost("r2.internal");
|
|
11630
11831
|
this.logger.debug("r2 egress: registering host interception", {
|
|
@@ -11644,12 +11845,15 @@ var Sandbox = class Sandbox extends Container {
|
|
|
11644
11845
|
if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
|
|
11645
11846
|
await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
|
|
11646
11847
|
}
|
|
11647
|
-
async configureS3CredentialProxyOutbound(
|
|
11848
|
+
async configureS3CredentialProxyOutbound(activeParams) {
|
|
11849
|
+
const params = { mounts: {
|
|
11850
|
+
...this.restoredMountRoutes.credentialProxyMounts,
|
|
11851
|
+
...activeParams.mounts
|
|
11852
|
+
} };
|
|
11648
11853
|
const ctx = this.ctx;
|
|
11649
11854
|
if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("Credential proxy bucket mounts require container outbound interception support");
|
|
11650
11855
|
if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("Credential proxy bucket mounts require exporting ContainerProxy from the Worker entrypoint");
|
|
11651
11856
|
const hosts = [S3_CREDENTIAL_PROXY_HOST, S3_CREDENTIAL_PROXY_DIAGNOSTIC_HOST];
|
|
11652
|
-
this.constructor.outboundHandlers = { s3CredentialProxyMount: s3CredentialProxyHandler };
|
|
11653
11857
|
if (Object.keys(params.mounts).length > 0) for (const host of hosts) await this.setOutboundByHost(host, "s3CredentialProxyMount", params);
|
|
11654
11858
|
else for (const host of hosts) await this.removeOutboundByHost(host);
|
|
11655
11859
|
const hostOverrides = {};
|
|
@@ -11682,4 +11886,4 @@ var Sandbox = class Sandbox extends Container {
|
|
|
11682
11886
|
|
|
11683
11887
|
//#endregion
|
|
11684
11888
|
export { PortClient as A, InvalidBackupConfigError as B, collectFile as C, SandboxClient as D, isPlatformTransientError as E, BackupCreateError as F, SessionTerminatedError as G, ProcessExitedBeforeReadyError as H, BackupExpiredError as I, BackupNotFoundError as L, FileClient as M, CommandClient as N, UtilityClient as O, BackupClient as P, BackupRestoreError as R, validateTunnelName as S, isDurableObjectCodeUpdateReset as T, ProcessReadyTimeoutError as U, OperationInterruptedError as V, RPCTransportError as W, responseToAsyncIterable as _, PREVIEW_PROXY_HEADER as a, sanitizeSandboxId as b, PREVIEW_PROXY_SANDBOX_ID_HEADER as c, BucketUnmountError as d, InvalidMountConfigError as f, parseSSEStream as g, asyncIterableToSSEStream as h, proxyTerminal as i, GitClient as j, ProcessClient as k, PREVIEW_PROXY_TOKEN_HEADER as l, S3FSMountError as m, Sandbox as n, PREVIEW_PROXY_HEADERS as o, MissingCredentialsError as p, getSandbox as r, PREVIEW_PROXY_PORT_HEADER as s, ContainerProxy$1 as t, BucketMountError as u, CodeInterpreter as v, streamFile as w, validatePort as x, SandboxSecurityError as y, ContainerUnavailableError as z };
|
|
11685
|
-
//# sourceMappingURL=sandbox-
|
|
11889
|
+
//# sourceMappingURL=sandbox-BGDk-c1Y.js.map
|