@cloudflare/sandbox 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-CBi-O-pF.js";
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
- const binaryString = atob(response.data);
1833
- const bytes = new Uint8Array(binaryString.length);
1834
- for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
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
- const binaryString = atob(response.data);
1854
- const bytes = new Uint8Array(binaryString.length);
1855
- for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
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 await this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
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
@@ -2769,13 +2789,15 @@ var ContainerControlConnection = class {
2769
2789
  port;
2770
2790
  logger;
2771
2791
  retryTimeoutMs;
2792
+ onClose;
2772
2793
  constructor(options) {
2773
2794
  this.containerStub = options.stub;
2774
2795
  this.port = options.port ?? 3e3;
2775
2796
  this.logger = options.logger ?? createNoOpLogger();
2776
2797
  this.retryTimeoutMs = options.retryTimeoutMs ?? DEFAULT_RETRY_TIMEOUT_MS;
2798
+ this.onClose = options.onClose;
2777
2799
  this.transport = new DeferredTransport();
2778
- this.session = new RpcSession(this.transport);
2800
+ this.session = new RpcSession(this.transport, options.localMain);
2779
2801
  this.stub = this.session.getRemoteMain();
2780
2802
  }
2781
2803
  /**
@@ -2815,6 +2837,8 @@ var ContainerControlConnection = class {
2815
2837
  this.stub[Symbol.dispose]?.();
2816
2838
  } catch {}
2817
2839
  if (this.ws) {
2840
+ this.ws.removeEventListener("close", this.onWebSocketClose);
2841
+ this.ws.removeEventListener("error", this.onWebSocketError);
2818
2842
  try {
2819
2843
  this.ws.close();
2820
2844
  } catch {}
@@ -2831,6 +2855,42 @@ var ContainerControlConnection = class {
2831
2855
  setRetryTimeoutMs(ms) {
2832
2856
  this.retryTimeoutMs = ms;
2833
2857
  }
2858
+ /**
2859
+ * Run the owner-provided `onClose` callback exactly once per call,
2860
+ * swallowing any errors so a buggy listener can't keep the connection
2861
+ * object in a half-torn-down state.
2862
+ */
2863
+ fireOnClose() {
2864
+ if (!this.onClose) return;
2865
+ try {
2866
+ this.onClose();
2867
+ } catch (err) {
2868
+ this.logger.warn("ContainerControlConnection onClose handler threw", { error: err instanceof Error ? err.message : String(err) });
2869
+ }
2870
+ }
2871
+ /**
2872
+ * WebSocket `close` listener. Defined as a bound arrow field so the
2873
+ * same reference can be passed to both `addEventListener` and
2874
+ * `removeEventListener` — a fresh anonymous lambda would silently
2875
+ * fail to unbind.
2876
+ */
2877
+ onWebSocketClose = () => {
2878
+ const wasConnected = this.connected;
2879
+ this.connected = false;
2880
+ this.ws = null;
2881
+ this.logger.debug("ContainerControlConnection WebSocket closed");
2882
+ if (wasConnected) this.fireOnClose();
2883
+ };
2884
+ /**
2885
+ * WebSocket `error` listener. Same field-form rationale as
2886
+ * {@link onWebSocketClose}.
2887
+ */
2888
+ onWebSocketError = () => {
2889
+ const wasConnected = this.connected;
2890
+ this.connected = false;
2891
+ this.ws = null;
2892
+ if (wasConnected) this.fireOnClose();
2893
+ };
2834
2894
  async doConnect() {
2835
2895
  try {
2836
2896
  const response = await this.fetchUpgradeWithRetry();
@@ -2838,15 +2898,8 @@ var ContainerControlConnection = class {
2838
2898
  const ws = response.webSocket;
2839
2899
  if (!ws) throw new Error("No WebSocket in upgrade response");
2840
2900
  ws.accept();
2841
- ws.addEventListener("close", () => {
2842
- this.connected = false;
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
- });
2901
+ ws.addEventListener("close", this.onWebSocketClose);
2902
+ ws.addEventListener("error", this.onWebSocketError);
2850
2903
  this.ws = ws;
2851
2904
  this.transport.activate(ws);
2852
2905
  this.connected = true;
@@ -3126,19 +3179,16 @@ var ContainerControlClient = class {
3126
3179
  busyPollTimer = null;
3127
3180
  /** Tracks whether we currently believe the session is busy. */
3128
3181
  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
3182
  constructor(options) {
3137
3183
  this.connOptions = {
3138
3184
  stub: options.stub,
3139
3185
  port: options.port,
3186
+ localMain: options.localMain,
3140
3187
  logger: options.logger,
3141
- retryTimeoutMs: options.retryTimeoutMs
3188
+ retryTimeoutMs: options.retryTimeoutMs,
3189
+ onClose: () => {
3190
+ if (this.conn) this.destroyConnection();
3191
+ }
3142
3192
  };
3143
3193
  this.idleDisconnectMs = options.idleDisconnectMs ?? DEFAULT_IDLE_DISCONNECT_MS;
3144
3194
  this.busyPollIntervalMs = options.busyPollIntervalMs ?? BUSY_POLL_INTERVAL_MS;
@@ -3180,11 +3230,7 @@ var ContainerControlClient = class {
3180
3230
  pollBusyState = () => {
3181
3231
  const conn = this.conn;
3182
3232
  if (!conn) return;
3183
- if (!conn.isConnected()) {
3184
- if (this.wasEverConnected) this.destroyConnection();
3185
- return;
3186
- }
3187
- this.wasEverConnected = true;
3233
+ if (!conn.isConnected()) return;
3188
3234
  const { imports, exports } = conn.getStats();
3189
3235
  if (imports > IDLE_IMPORT_THRESHOLD || exports > IDLE_EXPORT_THRESHOLD) {
3190
3236
  if (!this.busy) {
@@ -3217,7 +3263,7 @@ var ContainerControlClient = class {
3217
3263
  if (!conn || !conn.isConnected()) return;
3218
3264
  const { imports, exports } = conn.getStats();
3219
3265
  if (imports <= IDLE_IMPORT_THRESHOLD && exports <= IDLE_EXPORT_THRESHOLD) {
3220
- this.logger.debug("Disconnecting idle capnweb connection");
3266
+ this.logger.debug("Disconnecting idle RPC connection");
3221
3267
  this.destroyConnection();
3222
3268
  }
3223
3269
  }, this.idleDisconnectMs);
@@ -3239,7 +3285,6 @@ var ContainerControlClient = class {
3239
3285
  this.conn.disconnect();
3240
3286
  this.conn = null;
3241
3287
  }
3242
- this.wasEverConnected = false;
3243
3288
  }
3244
3289
  get commands() {
3245
3290
  return wrapStub(this.getConnection().rpc().commands, this.renewActivity);
@@ -3263,11 +3308,37 @@ var ContainerControlClient = class {
3263
3308
  return wrapStub(this.getConnection().rpc().backup, this.renewActivity);
3264
3309
  }
3265
3310
  get desktop() {
3266
- return wrapStub(this.getConnection().rpc().desktop, this.renewActivity);
3311
+ const stub = wrapStub(this.getConnection().rpc().desktop, this.renewActivity);
3312
+ const wire = stub;
3313
+ return new Proxy(stub, { get(target, prop, receiver) {
3314
+ if (prop === "screenshot") return async (options) => {
3315
+ const { format, ...rest } = options ?? {};
3316
+ const result = await wire.screenshot(rest);
3317
+ return format === "bytes" ? {
3318
+ ...result,
3319
+ data: base64ToBytes(result.data)
3320
+ } : result;
3321
+ };
3322
+ if (prop === "screenshotRegion") return async (region, options) => {
3323
+ const { format, ...rest } = options ?? {};
3324
+ const result = await wire.screenshotRegion({
3325
+ region,
3326
+ ...rest
3327
+ });
3328
+ return format === "bytes" ? {
3329
+ ...result,
3330
+ data: base64ToBytes(result.data)
3331
+ } : result;
3332
+ };
3333
+ return Reflect.get(target, prop, receiver);
3334
+ } });
3267
3335
  }
3268
3336
  get watch() {
3269
3337
  return wrapStub(this.getConnection().rpc().watch, this.renewActivity);
3270
3338
  }
3339
+ get tunnels() {
3340
+ return wrapStub(this.getConnection().rpc().tunnels, this.renewActivity);
3341
+ }
3271
3342
  get interpreter() {
3272
3343
  return wrapStub(this.getConnection().rpc().interpreter, this.renewActivity);
3273
3344
  }
@@ -3808,6 +3879,13 @@ function resolveS3fsOptions(provider, userOptions) {
3808
3879
 
3809
3880
  //#endregion
3810
3881
  //#region src/storage-mount/validation.ts
3882
+ /**
3883
+ * Type guard for R2Bucket binding.
3884
+ * Checks for the minimal R2Bucket interface methods we use.
3885
+ */
3886
+ function isR2Bucket(value) {
3887
+ return typeof value === "object" && value !== null && "put" in value && typeof value.put === "function" && "get" in value && typeof value.get === "function" && "head" in value && typeof value.head === "function" && "delete" in value && typeof value.delete === "function" && "list" in value && typeof value.list === "function";
3888
+ }
3811
3889
  function validatePrefix(prefix) {
3812
3890
  if (!prefix.startsWith("/")) throw new InvalidMountConfigError(`Prefix must start with '/': "${prefix}"`);
3813
3891
  }
@@ -3818,6 +3896,13 @@ function validateBucketName(bucket, mountPath) {
3818
3896
  }
3819
3897
  if (!/^[a-z0-9]([a-z0-9.-]{0,61}[a-z0-9])?$/.test(bucket)) throw new InvalidMountConfigError(`Invalid bucket name: "${bucket}". Bucket names must be 3-63 characters, lowercase alphanumeric, dots, or hyphens, and cannot start/end with dots or hyphens.`);
3820
3898
  }
3899
+ function validateBucketBindingName(bucketBinding, mountPath) {
3900
+ if (bucketBinding.includes(":")) {
3901
+ const [bucketName, prefixPart] = bucketBinding.split(":");
3902
+ throw new InvalidMountConfigError(`Bucket name cannot contain ':'. To mount a prefix, use the 'prefix' option:\n mountBucket('${bucketName}', '${mountPath}', { ...options, prefix: '${prefixPart}' })`);
3903
+ }
3904
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(bucketBinding)) throw new InvalidMountConfigError(`Invalid R2 binding name: "${bucketBinding}". Binding names must start with a letter or underscore and contain only letters, numbers, or underscores.`);
3905
+ }
3821
3906
  /**
3822
3907
  * Builds the s3fs source string from bucket name and optional prefix.
3823
3908
  * Format: "bucket" or "bucket:/prefix/" for subdirectory mounts.
@@ -4142,7 +4227,7 @@ async function proxyTerminal(stub, sessionId, request, options) {
4142
4227
 
4143
4228
  //#endregion
4144
4229
  //#region src/request-handler.ts
4145
- async function proxyToSandbox(request, env) {
4230
+ async function proxyToSandbox(request, env$1) {
4146
4231
  const logger = createLogger({
4147
4232
  component: "sandbox-do",
4148
4233
  traceId: TraceContext.fromHeaders(request.headers) || TraceContext.generate(),
@@ -4153,7 +4238,7 @@ async function proxyToSandbox(request, env) {
4153
4238
  const routeInfo = extractSandboxRoute(url);
4154
4239
  if (!routeInfo) return null;
4155
4240
  const { sandboxId, port, path: path$1, token } = routeInfo;
4156
- const sandbox = getSandbox(env.Sandbox, sandboxId, { normalizeId: true });
4241
+ const sandbox = getSandbox(env$1.Sandbox, sandboxId, { normalizeId: true });
4157
4242
  if (port !== 3e3) {
4158
4243
  if (!await sandbox.validatePortToken(port, token)) {
4159
4244
  logger.warn("Invalid token access blocked", {
@@ -4239,6 +4324,513 @@ function isLocalhostPattern(hostname) {
4239
4324
  return hostPart === "localhost" || hostPart === "127.0.0.1" || hostPart === "0.0.0.0";
4240
4325
  }
4241
4326
 
4327
+ //#endregion
4328
+ //#region src/storage-mount/r2-egress-handler.ts
4329
+ const XML_NS = "xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"";
4330
+ const XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
4331
+ function escapeXML(s) {
4332
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4333
+ }
4334
+ function xmlResponse(body, status = 200) {
4335
+ return new Response(XML_DECL + body, {
4336
+ status,
4337
+ headers: { "Content-Type": "application/xml" }
4338
+ });
4339
+ }
4340
+ function normalizeObjectKey(value) {
4341
+ return value.replace(/^\/+/, "");
4342
+ }
4343
+ function trimTrailingSlashes(s) {
4344
+ let end = s.length;
4345
+ while (end > 0 && s[end - 1] === "/") end--;
4346
+ return s.slice(0, end);
4347
+ }
4348
+ function parsePath(pathname) {
4349
+ const stripped = pathname.startsWith("/") ? pathname.slice(1) : pathname;
4350
+ if (!stripped) return null;
4351
+ const slash = stripped.indexOf("/");
4352
+ if (slash === -1) return {
4353
+ bucket: stripped,
4354
+ key: ""
4355
+ };
4356
+ return {
4357
+ bucket: stripped.slice(0, slash),
4358
+ key: normalizeObjectKey(stripped.slice(slash + 1))
4359
+ };
4360
+ }
4361
+ function resolveR2Bucket(env$1, name) {
4362
+ if (typeof env$1 !== "object" || env$1 === null) return null;
4363
+ const val = env$1[name];
4364
+ return isR2Bucket(val) ? val : null;
4365
+ }
4366
+ function parseRange(header) {
4367
+ if (!header) return void 0;
4368
+ const m = header.match(/^bytes=(\d*)-(\d*)$/);
4369
+ if (!m) return void 0;
4370
+ const start = m[1] ? parseInt(m[1], 10) : void 0;
4371
+ const end = m[2] ? parseInt(m[2], 10) : void 0;
4372
+ if (start === void 0 && end !== void 0) return { suffix: end };
4373
+ if (start !== void 0 && end !== void 0) return {
4374
+ offset: start,
4375
+ length: end - start + 1
4376
+ };
4377
+ if (start !== void 0) return { offset: start };
4378
+ }
4379
+ function buildListObjectsV2Xml(bucketName, prefix, delimiter, maxKeys, result) {
4380
+ const contents = result.objects.map((obj) => `<Contents><Key>${escapeXML(obj.key)}</Key><LastModified>${obj.uploaded.toISOString()}</LastModified><ETag>${escapeXML(obj.httpEtag)}</ETag><Size>${obj.size}</Size><StorageClass>STANDARD</StorageClass></Contents>`).join("");
4381
+ const commonPrefixes = result.delimitedPrefixes.map((p) => `<CommonPrefixes><Prefix>${escapeXML(p)}</Prefix></CommonPrefixes>`).join("");
4382
+ const nextToken = result.truncated && result.cursor ? `<NextContinuationToken>${escapeXML(result.cursor)}</NextContinuationToken>` : "";
4383
+ const keyCount = result.objects.length + result.delimitedPrefixes.length;
4384
+ return `<ListBucketResult ${XML_NS}><Name>${escapeXML(bucketName)}</Name><Prefix>${escapeXML(prefix)}</Prefix><KeyCount>${keyCount}</KeyCount><MaxKeys>${maxKeys}</MaxKeys>` + (delimiter ? `<Delimiter>${escapeXML(delimiter)}</Delimiter>` : "") + `<IsTruncated>${result.truncated}</IsTruncated>` + nextToken + contents + commonPrefixes + `</ListBucketResult>`;
4385
+ }
4386
+ function buildLocationXml() {
4387
+ return `<LocationConstraint ${XML_NS}/>`;
4388
+ }
4389
+ function buildInitiateMultipartUploadXml(bucketName, key, uploadId) {
4390
+ return `<InitiateMultipartUploadResult ${XML_NS}><Bucket>${escapeXML(bucketName)}</Bucket><Key>${escapeXML(key)}</Key><UploadId>${escapeXML(uploadId)}</UploadId></InitiateMultipartUploadResult>`;
4391
+ }
4392
+ function buildCompleteMultipartUploadXml(bucketName, key, etag) {
4393
+ return `<CompleteMultipartUploadResult ${XML_NS}><Location>http://r2.internal/${escapeXML(bucketName)}/${escapeXML(key)}</Location><Bucket>${escapeXML(bucketName)}</Bucket><Key>${escapeXML(key)}</Key><ETag>${escapeXML(etag)}</ETag></CompleteMultipartUploadResult>`;
4394
+ }
4395
+ function buildCopyObjectXml(etag, uploaded) {
4396
+ return `<CopyObjectResult ${XML_NS}><LastModified>${uploaded.toISOString()}</LastModified><ETag>${escapeXML(etag)}</ETag></CopyObjectResult>`;
4397
+ }
4398
+ function extractXmlTagContent(segment, tagName) {
4399
+ const openTag = `<${tagName}>`;
4400
+ const closeTag = `</${tagName}>`;
4401
+ const start = segment.indexOf(openTag);
4402
+ if (start === -1) return null;
4403
+ const contentStart = start + openTag.length;
4404
+ const end = segment.indexOf(closeTag, contentStart);
4405
+ if (end === -1) return null;
4406
+ return segment.slice(contentStart, end);
4407
+ }
4408
+ function parseCompleteMultipartUploadBody(body) {
4409
+ const parts = [];
4410
+ let pos = 0;
4411
+ while (pos < body.length) {
4412
+ const start = body.indexOf("<Part>", pos);
4413
+ if (start === -1) break;
4414
+ const end = body.indexOf("</Part>", start + 6);
4415
+ if (end === -1) break;
4416
+ const segment = body.slice(start, end + 7);
4417
+ pos = end + 7;
4418
+ const partNumberText = extractXmlTagContent(segment, "PartNumber");
4419
+ const etagText = extractXmlTagContent(segment, "ETag");
4420
+ const partNumber = partNumberText ? parseInt(partNumberText, 10) : NaN;
4421
+ if (Number.isFinite(partNumber) && etagText) parts.push({
4422
+ partNumber,
4423
+ etag: etagText.replace(/^"|"$/g, "")
4424
+ });
4425
+ }
4426
+ return parts;
4427
+ }
4428
+ function buildResponseHeaders(obj) {
4429
+ const headers = new Headers();
4430
+ headers.set("ETag", obj.httpEtag);
4431
+ headers.set("Content-Length", String(obj.size));
4432
+ headers.set("Last-Modified", obj.uploaded.toUTCString());
4433
+ headers.set("Accept-Ranges", "bytes");
4434
+ if (obj.httpMetadata?.contentType) headers.set("Content-Type", obj.httpMetadata.contentType);
4435
+ if (obj.httpMetadata?.contentDisposition) headers.set("Content-Disposition", obj.httpMetadata.contentDisposition);
4436
+ if (obj.httpMetadata?.contentEncoding) headers.set("Content-Encoding", obj.httpMetadata.contentEncoding);
4437
+ if (obj.httpMetadata?.contentLanguage) headers.set("Content-Language", obj.httpMetadata.contentLanguage);
4438
+ if (obj.httpMetadata?.cacheControl) headers.set("Cache-Control", obj.httpMetadata.cacheControl);
4439
+ return headers;
4440
+ }
4441
+ function buildContentRange(range, totalSize) {
4442
+ if ("suffix" in range) return `bytes ${Math.max(0, totalSize - range.suffix)}-${totalSize - 1}/${totalSize}`;
4443
+ const start = range.offset ?? 0;
4444
+ return `bytes ${start}-${range.length !== void 0 ? start + range.length - 1 : totalSize - 1}/${totalSize}`;
4445
+ }
4446
+ function getRangeContentLength(range, totalSize) {
4447
+ if ("suffix" in range) return Math.min(range.suffix, totalSize);
4448
+ const start = range.offset ?? 0;
4449
+ if (start >= totalSize) return 0;
4450
+ const requestedLength = range.length !== void 0 ? range.length : totalSize - start;
4451
+ return Math.min(requestedLength, totalSize - start);
4452
+ }
4453
+ function extractHttpMetadata(request) {
4454
+ const meta = {};
4455
+ const ct = request.headers.get("Content-Type");
4456
+ if (ct) meta.contentType = ct;
4457
+ const cd = request.headers.get("Content-Disposition");
4458
+ if (cd) meta.contentDisposition = cd;
4459
+ const ce = request.headers.get("Content-Encoding");
4460
+ if (ce) meta.contentEncoding = ce;
4461
+ const cl = request.headers.get("Content-Language");
4462
+ if (cl) meta.contentLanguage = cl;
4463
+ const cc = request.headers.get("Cache-Control");
4464
+ if (cc) meta.cacheControl = cc;
4465
+ return meta;
4466
+ }
4467
+ function parseCopySource(header) {
4468
+ const sourcePath = header.split("?")[0] ?? "";
4469
+ if (!sourcePath) return null;
4470
+ const decoded = decodeURIComponent(sourcePath);
4471
+ const parsed = parsePath(decoded.startsWith("/") ? decoded : `/${decoded}`);
4472
+ return parsed ? {
4473
+ bucket: parsed.bucket,
4474
+ key: normalizeObjectKey(parsed.key)
4475
+ } : null;
4476
+ }
4477
+ function normalizeStorageClass(storageClass) {
4478
+ if (storageClass === "Standard" || storageClass === "InfrequentAccess") return storageClass;
4479
+ }
4480
+ async function putRequestBody(r2, key, request, options) {
4481
+ const contentLength = request.headers.get("Content-Length");
4482
+ const length = contentLength ? Number.parseInt(contentLength, 10) : NaN;
4483
+ if (!Number.isFinite(length) || length < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
4484
+ if (length === 0) return r2.put(key, new Uint8Array(0), options);
4485
+ if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
4486
+ const { readable, writable } = new FixedLengthStream(length);
4487
+ const pipe = request.body.pipeTo(writable);
4488
+ const result = await r2.put(key, readable, options);
4489
+ await pipe;
4490
+ return result;
4491
+ }
4492
+ async function handleListObjects(r2, bucketName, url, mountPrefix) {
4493
+ const queryPrefix = normalizeObjectKey(url.searchParams.get("prefix") ?? "");
4494
+ const delimiter = url.searchParams.get("delimiter") ?? "";
4495
+ const maxKeys = Math.min(parseInt(url.searchParams.get("max-keys") ?? "1000", 10) || 1e3, 1e3);
4496
+ const continuationToken = url.searchParams.get("continuation-token") ?? void 0;
4497
+ const listOpts = {
4498
+ prefix: (mountPrefix ? `${mountPrefix}/${queryPrefix}` : queryPrefix) || void 0,
4499
+ delimiter: delimiter || void 0,
4500
+ limit: maxKeys,
4501
+ cursor: continuationToken
4502
+ };
4503
+ const result = await r2.list(listOpts);
4504
+ const stripKey = mountPrefix ? (k) => k.startsWith(`${mountPrefix}/`) ? k.slice(mountPrefix.length + 1) : k : (k) => k;
4505
+ return xmlResponse(buildListObjectsV2Xml(bucketName, queryPrefix, delimiter, maxKeys, {
4506
+ objects: result.objects.map((obj) => ({
4507
+ key: stripKey(obj.key),
4508
+ uploaded: obj.uploaded,
4509
+ httpEtag: obj.httpEtag,
4510
+ size: obj.size
4511
+ })),
4512
+ delimitedPrefixes: result.delimitedPrefixes.map(stripKey),
4513
+ truncated: result.truncated,
4514
+ cursor: result.truncated ? result.cursor : void 0
4515
+ }));
4516
+ }
4517
+ async function handleHeadObject(r2, key) {
4518
+ const obj = await r2.head(key);
4519
+ if (!obj) return new Response(null, { status: 404 });
4520
+ return new Response(null, {
4521
+ status: 200,
4522
+ headers: buildResponseHeaders(obj)
4523
+ });
4524
+ }
4525
+ async function handleGetObject(r2, key, request) {
4526
+ const range = parseRange(request.headers.get("Range"));
4527
+ if (!range) {
4528
+ const obj = await r2.get(key);
4529
+ if (!obj) return new Response(null, { status: 404 });
4530
+ return new Response(obj.body, {
4531
+ status: 200,
4532
+ headers: buildResponseHeaders(obj)
4533
+ });
4534
+ }
4535
+ const [headObj, rangeObj] = await Promise.all([r2.head(key), r2.get(key, { range })]);
4536
+ if (!headObj || !rangeObj) return new Response(null, { status: 404 });
4537
+ const headers = buildResponseHeaders(rangeObj);
4538
+ headers.set("Content-Range", buildContentRange(range, headObj.size));
4539
+ headers.set("Content-Length", String(getRangeContentLength(range, headObj.size)));
4540
+ return new Response(rangeObj.body, {
4541
+ status: 206,
4542
+ headers
4543
+ });
4544
+ }
4545
+ async function handlePutObject(r2, bucketName, key, request, env$1, permitted, mountPrefix) {
4546
+ const copySourceHeader = request.headers.get("x-amz-copy-source");
4547
+ if (copySourceHeader) {
4548
+ const copySource = parseCopySource(copySourceHeader);
4549
+ if (!copySource || !copySource.key) return new Response("Bad Request: invalid x-amz-copy-source", { status: 400 });
4550
+ if (!permitted.has(copySource.bucket)) return new Response(`Access to R2 bucket "${copySource.bucket}" is not permitted. Call mountBucket() with this bucket before accessing it.`, { status: 403 });
4551
+ const sourceBucket = copySource.bucket === bucketName ? r2 : resolveR2Bucket(env$1, copySource.bucket);
4552
+ if (!sourceBucket) return new Response(`R2 binding "${copySource.bucket}" not found in Worker env. Ensure the binding name matches the bucket name passed to mountBucket().`, { status: 500 });
4553
+ const sourceKey = mountPrefix && copySource.bucket === bucketName ? `${mountPrefix}/${copySource.key}` : copySource.key;
4554
+ const sourceObject = await sourceBucket.get(sourceKey);
4555
+ if (!sourceObject) return new Response(null, { status: 404 });
4556
+ const httpMetadata = request.headers.get("x-amz-metadata-directive")?.toUpperCase() === "REPLACE" ? extractHttpMetadata(request) : sourceObject.httpMetadata;
4557
+ const result$1 = await r2.put(key, sourceObject.body, {
4558
+ httpMetadata,
4559
+ customMetadata: sourceObject.customMetadata,
4560
+ storageClass: normalizeStorageClass(sourceObject.storageClass)
4561
+ });
4562
+ return xmlResponse(buildCopyObjectXml(result$1.httpEtag, result$1.uploaded));
4563
+ }
4564
+ const result = await putRequestBody(r2, key, request, { httpMetadata: extractHttpMetadata(request) });
4565
+ if (result instanceof Response) return result;
4566
+ const headers = new Headers();
4567
+ headers.set("ETag", result.httpEtag);
4568
+ return new Response(null, {
4569
+ status: 200,
4570
+ headers
4571
+ });
4572
+ }
4573
+ async function handleDeleteObject(r2, key) {
4574
+ await r2.delete(key);
4575
+ return new Response(null, { status: 204 });
4576
+ }
4577
+ async function handleCreateMultipartUpload(r2, bucketName, key, request) {
4578
+ const httpMetadata = extractHttpMetadata(request);
4579
+ return xmlResponse(buildInitiateMultipartUploadXml(bucketName, key, (await r2.createMultipartUpload(key, { httpMetadata })).uploadId));
4580
+ }
4581
+ async function handleUploadPart(r2, key, url, request) {
4582
+ const uploadId = url.searchParams.get("uploadId") ?? "";
4583
+ const partNumber = parseInt(url.searchParams.get("partNumber") ?? "0", 10);
4584
+ if (!uploadId || !partNumber) return new Response("Bad Request: missing uploadId or partNumber", { status: 400 });
4585
+ if (!request.body) return new Response("Bad Request: missing request body", { status: 400 });
4586
+ const contentLength = request.headers.get("Content-Length");
4587
+ const partLength = contentLength ? Number.parseInt(contentLength, 10) : NaN;
4588
+ if (!Number.isFinite(partLength) || partLength < 0) return new Response("Bad Request: missing or invalid Content-Length", { status: 400 });
4589
+ const upload = r2.resumeMultipartUpload(key, uploadId);
4590
+ let part;
4591
+ if (partLength === 0) part = await upload.uploadPart(partNumber, new Uint8Array(0));
4592
+ else {
4593
+ const { readable, writable } = new FixedLengthStream(partLength);
4594
+ const pipe = request.body.pipeTo(writable);
4595
+ part = await upload.uploadPart(partNumber, readable);
4596
+ await pipe;
4597
+ }
4598
+ const headers = new Headers();
4599
+ headers.set("ETag", `"${part.etag}"`);
4600
+ return new Response(null, {
4601
+ status: 200,
4602
+ headers
4603
+ });
4604
+ }
4605
+ async function handleCompleteMultipartUpload(r2, bucketName, key, url, request) {
4606
+ const uploadId = url.searchParams.get("uploadId") ?? "";
4607
+ if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
4608
+ const r2Parts = parseCompleteMultipartUploadBody(await request.text()).map((p) => ({
4609
+ partNumber: p.partNumber,
4610
+ etag: p.etag
4611
+ }));
4612
+ return xmlResponse(buildCompleteMultipartUploadXml(bucketName, key, (await r2.resumeMultipartUpload(key, uploadId).complete(r2Parts)).httpEtag));
4613
+ }
4614
+ async function handleAbortMultipartUpload(r2, key, url) {
4615
+ const uploadId = url.searchParams.get("uploadId") ?? "";
4616
+ if (!uploadId) return new Response("Bad Request: missing uploadId", { status: 400 });
4617
+ await r2.resumeMultipartUpload(key, uploadId).abort();
4618
+ return new Response(null, { status: 204 });
4619
+ }
4620
+ const r2EgressHandler = async (request, env$1, ctx) => {
4621
+ const url = new URL(request.url);
4622
+ const parsed = parsePath(url.pathname);
4623
+ if (!parsed) return new Response("Bad Request: empty path", { status: 400 });
4624
+ const { bucket: bucketName, key } = parsed;
4625
+ if (!ctx.params?.buckets || !(bucketName in ctx.params.buckets)) return new Response(`Access to R2 bucket "${bucketName}" is not permitted. Call mountBucket() with this bucket before accessing it.`, { status: 403 });
4626
+ const bucketParams = ctx.params.buckets[bucketName];
4627
+ const rawPrefix = bucketParams.prefix;
4628
+ const mountPrefix = rawPrefix ? trimTrailingSlashes(normalizeObjectKey(rawPrefix)) : void 0;
4629
+ const readOnly = bucketParams.readOnly ?? false;
4630
+ const r2 = resolveR2Bucket(env$1, bucketName);
4631
+ if (!r2) return new Response(`R2 binding "${bucketName}" not found in Worker env. Ensure the binding name matches the bucket name passed to mountBucket().`, { status: 500 });
4632
+ const { method } = request;
4633
+ if (!key) {
4634
+ if (method === "GET" && url.searchParams.has("location")) return xmlResponse(buildLocationXml());
4635
+ if (method === "GET" && url.searchParams.get("list-type") === "2") return handleListObjects(r2, bucketName, url, mountPrefix);
4636
+ if (method === "GET") return handleListObjects(r2, bucketName, url, mountPrefix);
4637
+ return new Response("Method Not Allowed", { status: 405 });
4638
+ }
4639
+ const fullKey = mountPrefix ? `${mountPrefix}/${key}` : key;
4640
+ const permitted = new Set(Object.keys(ctx.params.buckets));
4641
+ if (readOnly && (method === "PUT" || method === "DELETE" || method === "POST" && (url.searchParams.has("uploads") || url.searchParams.has("uploadId")))) return new Response("Forbidden: bucket mount is read-only", { status: 403 });
4642
+ if (method === "POST" && url.searchParams.has("uploads")) return handleCreateMultipartUpload(r2, bucketName, fullKey, request);
4643
+ if (method === "POST" && url.searchParams.has("uploadId")) return handleCompleteMultipartUpload(r2, bucketName, fullKey, url, request);
4644
+ if (method === "PUT" && url.searchParams.has("partNumber") && url.searchParams.has("uploadId")) return handleUploadPart(r2, fullKey, url, request);
4645
+ if (method === "DELETE" && url.searchParams.has("uploadId")) return handleAbortMultipartUpload(r2, fullKey, url);
4646
+ switch (method) {
4647
+ case "HEAD": return handleHeadObject(r2, fullKey);
4648
+ case "GET": return handleGetObject(r2, fullKey, request);
4649
+ case "PUT": return handlePutObject(r2, bucketName, fullKey, request, env$1, permitted, mountPrefix);
4650
+ case "DELETE": return handleDeleteObject(r2, fullKey);
4651
+ default: return new Response("Method Not Allowed", { status: 405 });
4652
+ }
4653
+ };
4654
+
4655
+ //#endregion
4656
+ //#region src/tunnels/sandbox-control-callback.ts
4657
+ var SandboxControlCallbackImpl = class extends RpcTarget {
4658
+ constructor(getHandler, logger) {
4659
+ super();
4660
+ this.getHandler = getHandler;
4661
+ this.logger = logger;
4662
+ }
4663
+ async onTunnelExit(id, port, exitCode) {
4664
+ const handler = this.getHandler();
4665
+ if (!handler) {
4666
+ this.logger.debug("onTunnelExit: no handler bound; ignoring", {
4667
+ id,
4668
+ port,
4669
+ exitCode
4670
+ });
4671
+ return;
4672
+ }
4673
+ await handler(id, port, exitCode);
4674
+ }
4675
+ };
4676
+
4677
+ //#endregion
4678
+ //#region src/tunnels/tunnels-handler.ts
4679
+ /**
4680
+ * Tunnels namespace handler. Created once per Sandbox DO instance via
4681
+ * `createTunnelsHandler(host)` and exposed as `sandbox.tunnels`.
4682
+ *
4683
+ * Storage is the source of truth. The DO holds a `Record<portString, TunnelInfo>`
4684
+ * under the `tunnels` storage key. `Sandbox.onStart()` clears the key on every
4685
+ * container restart so any record in storage is by construction backed by a
4686
+ * running `cloudflared` process; the handler never needs to verify that
4687
+ * separately against the container.
4688
+ */
4689
+ /** DO storage key for the `port → TunnelInfo` map. */
4690
+ const STORAGE_KEY = "tunnels";
4691
+ function validateTunnelPort(port) {
4692
+ if (!validatePort(port)) throw new SandboxSecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding reserved ports.`);
4693
+ }
4694
+ /** 8-char hex id derived from `crypto.getRandomValues`. Unique per sandbox. */
4695
+ function shortId() {
4696
+ const buf = new Uint8Array(4);
4697
+ crypto.getRandomValues(buf);
4698
+ return Array.from(buf).map((b) => b.toString(16).padStart(2, "0")).join("");
4699
+ }
4700
+ function isTunnelNotFoundError(error) {
4701
+ return (error instanceof Error ? error.message : String(error)).includes("TUNNEL_NOT_FOUND");
4702
+ }
4703
+ async function readMap(storage) {
4704
+ return await storage.get(STORAGE_KEY) ?? {};
4705
+ }
4706
+ /**
4707
+ * Concrete `TunnelsHandler` implementation. Extends `RpcTarget` so it
4708
+ * can cross the Workers RPC boundary: the Sandbox DO is reachable from
4709
+ * Workers via Workers RPC (`stub.tunnels.get(port)`), and only
4710
+ * `RpcTarget` instances are passed by reference across that boundary.
4711
+ */
4712
+ var TunnelsRpcTarget = class extends RpcTarget$1 {
4713
+ #host;
4714
+ #withPortLock;
4715
+ constructor(host, withPortLock) {
4716
+ super();
4717
+ this.#host = host;
4718
+ this.#withPortLock = withPortLock;
4719
+ }
4720
+ async get(port) {
4721
+ const startTime = Date.now();
4722
+ let outcome = "error";
4723
+ let cacheState = "miss";
4724
+ let caughtError;
4725
+ try {
4726
+ validateTunnelPort(port);
4727
+ const info = await this.#withPortLock(port, async () => {
4728
+ const existing = (await readMap(this.#host.storage))[port.toString()];
4729
+ if (existing) {
4730
+ cacheState = "hit";
4731
+ return existing;
4732
+ }
4733
+ const id = `quick-${shortId()}`;
4734
+ const spawned = await this.#host.client.tunnels.runQuickTunnel(id, port);
4735
+ await this.#host.storage.transaction(async (txn) => {
4736
+ const nextMap = await readMap(txn);
4737
+ nextMap[port.toString()] = spawned;
4738
+ await txn.put(STORAGE_KEY, nextMap);
4739
+ });
4740
+ return spawned;
4741
+ });
4742
+ outcome = "success";
4743
+ return info;
4744
+ } catch (error) {
4745
+ caughtError = error instanceof Error ? error : new Error(String(error));
4746
+ throw error;
4747
+ } finally {
4748
+ logCanonicalEvent(this.#host.logger, {
4749
+ event: "tunnel.get",
4750
+ outcome,
4751
+ port,
4752
+ cacheState,
4753
+ durationMs: Date.now() - startTime,
4754
+ error: caughtError
4755
+ });
4756
+ }
4757
+ }
4758
+ async destroy(portOrInfo) {
4759
+ const port = typeof portOrInfo === "number" ? portOrInfo : portOrInfo.port;
4760
+ const startTime = Date.now();
4761
+ let outcome = "error";
4762
+ let caughtError;
4763
+ let tunnelId;
4764
+ try {
4765
+ await this.#withPortLock(port, async () => {
4766
+ const existing = (await readMap(this.#host.storage))[port.toString()];
4767
+ if (!existing) return;
4768
+ tunnelId = existing.id;
4769
+ await this.#host.storage.transaction(async (txn) => {
4770
+ const current = await readMap(txn);
4771
+ delete current[port.toString()];
4772
+ await txn.put(STORAGE_KEY, current);
4773
+ });
4774
+ try {
4775
+ await this.#host.client.tunnels.destroyTunnel(existing.id);
4776
+ } catch (error) {
4777
+ if (!isTunnelNotFoundError(error)) throw error;
4778
+ }
4779
+ });
4780
+ outcome = "success";
4781
+ } catch (error) {
4782
+ caughtError = error instanceof Error ? error : new Error(String(error));
4783
+ throw error;
4784
+ } finally {
4785
+ logCanonicalEvent(this.#host.logger, {
4786
+ event: "tunnel.destroy",
4787
+ outcome,
4788
+ port,
4789
+ tunnelId,
4790
+ durationMs: Date.now() - startTime,
4791
+ error: caughtError
4792
+ });
4793
+ }
4794
+ }
4795
+ async list() {
4796
+ const map = await readMap(this.#host.storage);
4797
+ return Object.values(map);
4798
+ }
4799
+ };
4800
+ function createTunnelsHandler(host) {
4801
+ const portLocks = /* @__PURE__ */ new Map();
4802
+ const withPortLock = (port, fn) => {
4803
+ const next = (portLocks.get(port) ?? Promise.resolve()).then(fn, fn);
4804
+ portLocks.set(port, next.catch(() => void 0));
4805
+ return next;
4806
+ };
4807
+ const tunnels = new TunnelsRpcTarget(host, withPortLock);
4808
+ const handleTunnelExit = async (id, port, exitCode) => {
4809
+ const startTime = Date.now();
4810
+ await withPortLock(port, async () => {
4811
+ await host.storage.transaction(async (txn) => {
4812
+ const map = await readMap(txn);
4813
+ if (map[port.toString()]?.id === id) {
4814
+ delete map[port.toString()];
4815
+ await txn.put(STORAGE_KEY, map);
4816
+ }
4817
+ });
4818
+ logCanonicalEvent(host.logger, {
4819
+ event: "tunnel.exit",
4820
+ outcome: "success",
4821
+ port,
4822
+ tunnelId: id,
4823
+ exitCode: exitCode ?? void 0,
4824
+ durationMs: Date.now() - startTime
4825
+ });
4826
+ });
4827
+ };
4828
+ return {
4829
+ tunnels,
4830
+ handleTunnelExit
4831
+ };
4832
+ }
4833
+
4242
4834
  //#endregion
4243
4835
  //#region src/version.ts
4244
4836
  /**
@@ -4246,11 +4838,23 @@ function isLocalhostPattern(hostname) {
4246
4838
  * This file is auto-updated by .github/changeset-version.ts during releases
4247
4839
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
4248
4840
  */
4249
- const SDK_VERSION = "0.10.1";
4841
+ const SDK_VERSION = "0.10.2";
4250
4842
 
4251
4843
  //#endregion
4252
4844
  //#region src/sandbox.ts
4845
+ const R2_EGRESS_PROXY_TARGET_CLASS_NAME = "CloudflareSandboxR2EgressProxyTarget";
4846
+ var R2EgressProxyTarget = class extends Container {};
4847
+ Object.defineProperty(R2EgressProxyTarget, "name", { value: R2_EGRESS_PROXY_TARGET_CLASS_NAME });
4848
+ R2EgressProxyTarget.outboundHandlers = { r2EgressMount: r2EgressHandler };
4849
+ function isFetcher(value) {
4850
+ return typeof value === "object" && value !== null && "fetch" in value && typeof value.fetch === "function";
4851
+ }
4253
4852
  const sandboxConfigurationCache = /* @__PURE__ */ new WeakMap();
4853
+ const R2_DEFAULT_S3FS_OPTIONS = {
4854
+ stat_cache_expire: "60",
4855
+ enable_noobj_cache: true,
4856
+ multipart_size: "5"
4857
+ };
4254
4858
  const BACKUP_DEFAULT_TTL_SECONDS = 259200;
4255
4859
  const BACKUP_MAX_NAME_LENGTH = 256;
4256
4860
  const BACKUP_CONTAINER_DIR = "/var/backups";
@@ -4396,6 +5000,10 @@ function getSandbox(ns, id, options) {
4396
5000
  desktop: new Proxy({}, { get(_, method) {
4397
5001
  if (typeof method !== "string" || method === "then") return void 0;
4398
5002
  return (...args) => stub.callDesktop(method, args);
5003
+ } }),
5004
+ tunnels: new Proxy({}, { get: (_, method) => {
5005
+ if (typeof method !== "string" || method === "then") return void 0;
5006
+ return (...args) => stub.callTunnels(method, args);
4399
5007
  } })
4400
5008
  };
4401
5009
  return new Proxy(stub, { get(target, prop) {
@@ -4416,19 +5024,15 @@ function connect(stub) {
4416
5024
  return await stub.fetch(portSwitchedRequest);
4417
5025
  };
4418
5026
  }
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
5027
  var Sandbox = class Sandbox extends Container {
4427
5028
  defaultPort = 3e3;
4428
5029
  sleepAfter = "10m";
4429
5030
  client;
4430
5031
  codeInterpreter;
4431
5032
  sandboxName = null;
5033
+ tunnelsHandler = null;
5034
+ tunnelExitHandler = null;
5035
+ controlCallback;
4432
5036
  normalizeId = false;
4433
5037
  defaultSession = null;
4434
5038
  containerGeneration = 0;
@@ -4527,13 +5131,30 @@ var Sandbox = class Sandbox extends Container {
4527
5131
  * Dispatch method for desktop operations.
4528
5132
  * Called by the client-side proxy created in getSandbox() to provide
4529
5133
  * the `sandbox.desktop.status()` API without relying on RPC pipelining
4530
- * through property getters.
5134
+ * through property getters which is broken when using vite-plugin.
4531
5135
  */
4532
5136
  async callDesktop(method, args) {
4533
5137
  if (!Sandbox.DESKTOP_METHODS.has(method)) throw new Error(`Unknown desktop method: ${method}`);
4534
5138
  const client = this.client.desktop;
4535
5139
  const fn = client[method];
4536
- if (typeof fn !== "function") throw new Error(`Unknown desktop method: ${method}`);
5140
+ if (typeof fn !== "function") throw new Error(`sandbox.desktop missing method: ${method}`);
5141
+ return fn.apply(client, args);
5142
+ }
5143
+ /**
5144
+ * Dispatch method for tunnel operations.
5145
+ * Called by the client-side proxy created in getSandbox() to provide
5146
+ * the `sandbox.tunnels` API without relying on RPC pipelining
5147
+ * through property getters which is broken when using vite-plugin.
5148
+ */
5149
+ async callTunnels(method, args) {
5150
+ if (![
5151
+ "get",
5152
+ "list",
5153
+ "destroy"
5154
+ ].includes(method)) throw new Error(`Unknown tunnels method: ${method}`);
5155
+ const client = this.tunnels;
5156
+ const fn = client[method];
5157
+ if (typeof fn !== "function") throw new Error(`sandbox.tunnels missing method: ${method}`);
4537
5158
  return fn.apply(client, args);
4538
5159
  }
4539
5160
  /**
@@ -4579,6 +5200,7 @@ var Sandbox = class Sandbox extends Container {
4579
5200
  port: 3e3,
4580
5201
  logger: this.logger,
4581
5202
  retryTimeoutMs: this.computeRetryTimeoutMs(),
5203
+ localMain: this.controlCallback,
4582
5204
  onActivity: () => {
4583
5205
  this.renewActivityTimeout();
4584
5206
  },
@@ -4593,9 +5215,9 @@ var Sandbox = class Sandbox extends Container {
4593
5215
  }
4594
5216
  return this.createSandboxClient();
4595
5217
  }
4596
- constructor(ctx, env) {
4597
- super(ctx, env);
4598
- const envObj = env;
5218
+ constructor(ctx, env$1) {
5219
+ super(ctx, env$1);
5220
+ const envObj = env$1;
4599
5221
  ["SANDBOX_LOG_LEVEL", "SANDBOX_LOG_FORMAT"].forEach((key) => {
4600
5222
  if (envObj?.[key]) this.envVars[key] = String(envObj[key]);
4601
5223
  });
@@ -4618,6 +5240,7 @@ var Sandbox = class Sandbox extends Container {
4618
5240
  accessKeyId: this.r2AccessKeyId,
4619
5241
  secretAccessKey: this.r2SecretAccessKey
4620
5242
  });
5243
+ this.controlCallback = new SandboxControlCallbackImpl(() => this.tunnelExitHandler, this.logger);
4621
5244
  this.client = this.createClientForTransport(this.transport);
4622
5245
  this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
4623
5246
  this.ctx.blockConcurrencyWhile(async () => {
@@ -4645,6 +5268,8 @@ var Sandbox = class Sandbox extends Container {
4645
5268
  const previousClient = this.client;
4646
5269
  this.client = this.createClientForTransport(storedTransport);
4647
5270
  this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
5271
+ this.tunnelsHandler = null;
5272
+ this.tunnelExitHandler = null;
4648
5273
  previousClient.disconnect();
4649
5274
  }
4650
5275
  if (storedTransport) this.hasStoredTransport = true;
@@ -4736,6 +5361,8 @@ var Sandbox = class Sandbox extends Container {
4736
5361
  this.hasStoredTransport = true;
4737
5362
  this.client = this.createClientForTransport(transport);
4738
5363
  this.codeInterpreter = new CodeInterpreter(() => this.client.interpreter);
5364
+ this.tunnelsHandler = null;
5365
+ this.tunnelExitHandler = null;
4739
5366
  previousClient.disconnect();
4740
5367
  this.renewActivityTimeout();
4741
5368
  this.logger.debug("Transport updated", { transport });
@@ -4753,7 +5380,7 @@ var Sandbox = class Sandbox extends Container {
4753
5380
  * Get default timeouts with env var fallbacks and validation
4754
5381
  * Precedence: SDK defaults < Env vars < User config
4755
5382
  */
4756
- getDefaultTimeouts(env) {
5383
+ getDefaultTimeouts(env$1) {
4757
5384
  const parseAndValidate = (envVar, name, min, max) => {
4758
5385
  const defaultValue = this.DEFAULT_CONTAINER_TIMEOUTS[name];
4759
5386
  if (envVar === void 0) return defaultValue;
@@ -4769,9 +5396,9 @@ var Sandbox = class Sandbox extends Container {
4769
5396
  return parsed;
4770
5397
  };
4771
5398
  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)
5399
+ instanceGetTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_INSTANCE_TIMEOUT_MS"), "instanceGetTimeoutMS", 5e3, 3e5),
5400
+ portReadyTimeoutMS: parseAndValidate(getEnvString(env$1, "SANDBOX_PORT_TIMEOUT_MS"), "portReadyTimeoutMS", 1e4, 6e5),
5401
+ waitIntervalMS: parseAndValidate(getEnvString(env$1, "SANDBOX_POLL_INTERVAL_MS"), "waitIntervalMS", 100, 5e3)
4775
5402
  };
4776
5403
  }
4777
5404
  /**
@@ -4793,7 +5420,16 @@ var Sandbox = class Sandbox extends Container {
4793
5420
  await this.mountBucketLocal(bucket, mountPath, options);
4794
5421
  return;
4795
5422
  }
4796
- await this.mountBucketFuse(bucket, mountPath, options);
5423
+ const remoteOptions = options;
5424
+ if (remoteOptions.endpoint === void 0) {
5425
+ const binding = this.env[bucket];
5426
+ if (isR2Bucket(binding)) {
5427
+ await this.mountBucketR2Egress(bucket, mountPath, options);
5428
+ return;
5429
+ }
5430
+ throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in Worker env. Ensure the binding name matches the bucket binding configured in wrangler.jsonc.`);
5431
+ }
5432
+ await this.mountBucketFuse(bucket, mountPath, remoteOptions);
4797
5433
  }
4798
5434
  /**
4799
5435
  * Local dev mount: bidirectional sync via R2 binding + file/watch APIs
@@ -4850,12 +5486,109 @@ var Sandbox = class Sandbox extends Container {
4850
5486
  });
4851
5487
  }
4852
5488
  }
5489
+ getR2EgressParams() {
5490
+ const buckets = {};
5491
+ for (const [, m] of this.activeMounts) if (m.mountType === "r2-egress") buckets[m.bucket] = {
5492
+ prefix: m.prefix,
5493
+ readOnly: m.readOnly
5494
+ };
5495
+ return { buckets };
5496
+ }
5497
+ validateR2EgressS3fsOptions(options) {
5498
+ if (!options) return;
5499
+ const protectedOptions = new Set(["passwd_file", "url"]);
5500
+ for (const option of options) {
5501
+ const [key] = option.split("=");
5502
+ if (protectedOptions.has(key)) throw new InvalidMountConfigError(`s3fs option "${key}" cannot be overridden for R2 binding mounts`);
5503
+ }
5504
+ }
5505
+ /**
5506
+ * Credential-less R2 mount: egress interception routes s3fs requests to the
5507
+ * R2 binding. No S3 credentials are needed in the container or Worker env.
5508
+ */
5509
+ async mountBucketR2Egress(bucket, mountPath, options) {
5510
+ const mountStartTime = Date.now();
5511
+ const prefix = options.prefix;
5512
+ let mountOutcome = "error";
5513
+ let mountError;
5514
+ try {
5515
+ validateBucketBindingName(bucket, mountPath);
5516
+ this.validateMountPath(mountPath);
5517
+ this.validateR2EgressS3fsOptions(options.s3fsOptions);
5518
+ for (const [existingMountPath, mountInfo$1] of this.activeMounts) {
5519
+ if (mountInfo$1.mountType === "r2-egress" && mountInfo$1.bucket === bucket && mountInfo$1.prefix !== prefix) throw new InvalidMountConfigError(`R2 binding "${bucket}" is already mounted at ${existingMountPath} with a different prefix. Mount the same binding only once, or use the same prefix for additional mounts.`);
5520
+ if (mountInfo$1.mountType === "r2-egress" && mountInfo$1.bucket === bucket && mountInfo$1.readOnly !== (options.readOnly ?? false)) throw new InvalidMountConfigError(`R2 binding "${bucket}" is already mounted at ${existingMountPath} with a different readOnly setting. Mount the same binding only once, or use the same readOnly value for additional mounts.`);
5521
+ }
5522
+ const passwordFilePath = this.generatePasswordFilePath();
5523
+ await this.createPasswordFile(passwordFilePath, bucket, {
5524
+ accessKeyId: "x",
5525
+ secretAccessKey: "x"
5526
+ });
5527
+ const mountInfo = {
5528
+ mountType: "r2-egress",
5529
+ bucket,
5530
+ mountPath,
5531
+ passwordFilePath,
5532
+ mounted: false,
5533
+ prefix,
5534
+ readOnly: options.readOnly ?? false
5535
+ };
5536
+ this.activeMounts.set(mountPath, mountInfo);
5537
+ await this.configureR2EgressOutbound(this.getR2EgressParams());
5538
+ await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
5539
+ const s3fsSource = bucket;
5540
+ const optionsStr = shellEscape(serializeS3fsOptions({
5541
+ passwd_file: passwordFilePath,
5542
+ ...R2_DEFAULT_S3FS_OPTIONS,
5543
+ ...parseS3fsOptions(resolveS3fsOptions("r2", options.s3fsOptions)),
5544
+ use_path_request_style: true,
5545
+ url: "http://r2.internal",
5546
+ ...options.readOnly ? { ro: true } : {}
5547
+ }));
5548
+ const mountCmd = `s3fs ${shellEscape(s3fsSource)} ${shellEscape(mountPath)} -o ${optionsStr}`;
5549
+ this.logger.debug("r2-egress: running s3fs", { mountCmd });
5550
+ const result = await this.execInternal(mountCmd);
5551
+ this.logger.debug("r2-egress: s3fs exited", {
5552
+ exitCode: result.exitCode,
5553
+ stdout: result.stdout,
5554
+ stderr: result.stderr
5555
+ });
5556
+ if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
5557
+ const mountpointCheck = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} && echo 'FUSE_MOUNTED' || echo 'NOT_FUSE_MOUNTED'`);
5558
+ this.logger.debug("r2-egress: mountpoint check", {
5559
+ stdout: mountpointCheck.stdout.trim(),
5560
+ exitCode: mountpointCheck.exitCode
5561
+ });
5562
+ if (mountpointCheck.stdout.trim() !== "FUSE_MOUNTED") throw new S3FSMountError(`s3fs exited 0 but mount was not established at ${mountPath}`);
5563
+ mountInfo.mounted = true;
5564
+ mountOutcome = "success";
5565
+ } catch (error) {
5566
+ mountError = error instanceof Error ? error : new Error(String(error));
5567
+ const failedMount = this.activeMounts.get(mountPath);
5568
+ this.activeMounts.delete(mountPath);
5569
+ if (failedMount?.mountType === "r2-egress") await this.deletePasswordFile(failedMount.passwordFilePath).catch(() => {});
5570
+ const remainingParams = this.getR2EgressParams();
5571
+ await this.configureR2EgressOutbound(remainingParams).catch(() => {});
5572
+ throw error;
5573
+ } finally {
5574
+ logCanonicalEvent(this.logger, {
5575
+ event: "bucket.mount",
5576
+ outcome: mountOutcome,
5577
+ durationMs: Date.now() - mountStartTime,
5578
+ bucket,
5579
+ mountPath,
5580
+ provider: "r2",
5581
+ prefix,
5582
+ error: mountError
5583
+ });
5584
+ }
5585
+ }
4853
5586
  /**
4854
5587
  * Production mount: S3FS-FUSE inside the container
4855
5588
  */
4856
5589
  async mountBucketFuse(bucket, mountPath, options) {
4857
5590
  const mountStartTime = Date.now();
4858
- const prefix = options.prefix || void 0;
5591
+ const prefix = options.prefix;
4859
5592
  let mountOutcome = "error";
4860
5593
  let mountError;
4861
5594
  let passwordFilePath;
@@ -4946,6 +5679,7 @@ var Sandbox = class Sandbox extends Container {
4946
5679
  }
4947
5680
  mountInfo.mounted = false;
4948
5681
  this.activeMounts.delete(mountPath);
5682
+ if (mountInfo.mountType === "r2-egress") await this.configureR2EgressOutbound(this.getR2EgressParams());
4949
5683
  try {
4950
5684
  const cleanup = await this.execInternal(`mountpoint -q ${shellEscape(mountPath)} || rmdir ${shellEscape(mountPath)}`);
4951
5685
  if (cleanup.exitCode !== 0) this.logger.warn("mount directory removal failed", {
@@ -4978,7 +5712,14 @@ var Sandbox = class Sandbox extends Container {
4978
5712
  }
4979
5713
  }
4980
5714
  /**
4981
- * Validate mount options
5715
+ * Shared validation for mount path (absolute, not already in use).
5716
+ */
5717
+ validateMountPath(mountPath) {
5718
+ if (!mountPath.startsWith("/")) throw new InvalidMountConfigError(`Mount path must be absolute (start with /): "${mountPath}"`);
5719
+ if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path "${mountPath}" is already in use by bucket "${this.activeMounts.get(mountPath)?.bucket}". Unmount the existing bucket first or use a different mount path.`);
5720
+ }
5721
+ /**
5722
+ * Validate mount options for remote (FUSE) mounts
4982
5723
  */
4983
5724
  validateMountOptions(bucket, mountPath, options) {
4984
5725
  try {
@@ -4987,8 +5728,7 @@ var Sandbox = class Sandbox extends Container {
4987
5728
  throw new InvalidMountConfigError(`Invalid endpoint URL: "${options.endpoint}". Must be a valid HTTP(S) URL.`);
4988
5729
  }
4989
5730
  validateBucketName(bucket, mountPath);
4990
- if (!mountPath.startsWith("/")) throw new InvalidMountConfigError(`Mount path must be absolute (start with /): "${mountPath}"`);
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.`);
5731
+ this.validateMountPath(mountPath);
4992
5732
  }
4993
5733
  /**
4994
5734
  * Generate unique password file path for s3fs credentials
@@ -5048,6 +5788,13 @@ var Sandbox = class Sandbox extends Container {
5048
5788
  if (result.exitCode === 2) throw new S3FSMountError(`S3FS mount failed: ${detail || "Unknown error"}`);
5049
5789
  throw new S3FSMountError(`S3FS mount failed: FUSE filesystem never appeared at ${mountPath}. ${detail ? `s3fs log: ${detail}` : "No s3fs log output captured. The s3fs daemon may have exited before writing logs."}`);
5050
5790
  }
5791
+ async unmountTrackedFuseMount(mountPath, mountInfo) {
5792
+ if (!mountInfo.mounted) return;
5793
+ this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
5794
+ const result = await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
5795
+ if (result.exitCode !== 0) throw new Error(`fusermount -u failed (exit ${result.exitCode}): ${result.stderr || "unknown error"}`);
5796
+ mountInfo.mounted = false;
5797
+ }
5051
5798
  /**
5052
5799
  * In-flight `destroy()` promise. While set, concurrent callers coalesce
5053
5800
  * onto the same teardown instead of triggering a second one. Cleared when
@@ -5109,10 +5856,8 @@ var Sandbox = class Sandbox extends Container {
5109
5856
  this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
5110
5857
  }
5111
5858
  else {
5112
- if (mountInfo.mounted) try {
5113
- this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
5114
- await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
5115
- mountInfo.mounted = false;
5859
+ try {
5860
+ await this.unmountTrackedFuseMount(mountPath, mountInfo);
5116
5861
  } catch (error) {
5117
5862
  mountFailures++;
5118
5863
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -5122,6 +5867,7 @@ var Sandbox = class Sandbox extends Container {
5122
5867
  }
5123
5868
  }
5124
5869
  await this.ctx.storage.delete("portTokens");
5870
+ await this.ctx.storage.delete("tunnels");
5125
5871
  this.client.disconnect();
5126
5872
  outcome = "success";
5127
5873
  await super.destroy();
@@ -5149,6 +5895,11 @@ var Sandbox = class Sandbox extends Container {
5149
5895
  } catch (error) {
5150
5896
  this.logger.error("Failed to restore exposed ports after container start", error instanceof Error ? error : new Error(String(error)));
5151
5897
  }
5898
+ try {
5899
+ await this.ctx.storage.delete("tunnels");
5900
+ } catch (error) {
5901
+ this.logger.error("Failed to clear tunnel storage after container start", error instanceof Error ? error : new Error(String(error)));
5902
+ }
5152
5903
  }
5153
5904
  /**
5154
5905
  * Re-expose ports on the container runtime using tokens persisted in DO
@@ -5251,7 +6002,10 @@ var Sandbox = class Sandbox extends Container {
5251
6002
  this.defaultSession = null;
5252
6003
  this.defaultSessionInit = null;
5253
6004
  this.client.disconnect();
6005
+ let hadR2EgressMount = false;
5254
6006
  for (const [, m] of this.activeMounts) if (m.mountType === "local-sync") await m.syncManager.stop().catch(() => {});
6007
+ else if (m.mountType === "r2-egress") hadR2EgressMount = true;
6008
+ if (hadR2EgressMount) await this.configureR2EgressOutbound({ buckets: {} }).catch(() => {});
5255
6009
  this.activeMounts.clear();
5256
6010
  await this.ctx.storage.delete("defaultSession");
5257
6011
  }
@@ -5998,8 +6752,11 @@ var Sandbox = class Sandbox extends Container {
5998
6752
  }
5999
6753
  async getProcess(id, sessionId) {
6000
6754
  const session = sessionId ?? await this.ensureDefaultSession();
6001
- const response = await this.client.processes.getProcess(id);
6002
- if (!response.process) return null;
6755
+ const response = await this.client.processes.getProcess(id).catch((e) => {
6756
+ if (e instanceof ProcessNotFoundError) return null;
6757
+ throw e;
6758
+ });
6759
+ if (!response?.process) return null;
6003
6760
  const processData = response.process;
6004
6761
  return this.createProcessFromDTO({
6005
6762
  id: processData.id,
@@ -6316,6 +7073,45 @@ var Sandbox = class Sandbox extends Container {
6316
7073
  }];
6317
7074
  });
6318
7075
  }
7076
+ /**
7077
+ * Namespaced tunnel API. Quick tunnels are zero-config preview URLs
7078
+ * backed by Cloudflare's trycloudflare service.
7079
+ *
7080
+ * - `tunnels.get(port)` — idempotent. Returns the cached tunnel for
7081
+ * `port` if one exists in DO storage, otherwise spawns a fresh
7082
+ * cloudflared process and persists the record.
7083
+ * - `tunnels.list()` — records currently known to this sandbox, from
7084
+ * DO storage.
7085
+ * - `tunnels.destroy(portOrInfo)` — tear down by port number or by
7086
+ * the record returned from `get()`.
7087
+ *
7088
+ * Storage is cleared on container restart (`onStart`), so URLs do
7089
+ * not survive a container restart — the next `get(port)` call will
7090
+ * spawn a fresh tunnel with a new URL.
7091
+ *
7092
+ * Requires the RPC transport. Calling this on a route-based transport
7093
+ * throws "RPC transport required".
7094
+ */
7095
+ get tunnels() {
7096
+ this.ensureTunnelsBuilt();
7097
+ return this.tunnelsHandler;
7098
+ }
7099
+ /**
7100
+ * Lazily construct both the public tunnels handler and its sibling
7101
+ * exit-handler callback. Called from the `tunnels` getter on first
7102
+ * access and on every access after a transport swap clears both
7103
+ * fields.
7104
+ */
7105
+ ensureTunnelsBuilt() {
7106
+ if (this.tunnelsHandler) return;
7107
+ const built = createTunnelsHandler({
7108
+ client: this.client,
7109
+ storage: this.ctx.storage,
7110
+ logger: this.logger
7111
+ });
7112
+ this.tunnelsHandler = built.tunnels;
7113
+ this.tunnelExitHandler = built.handleTunnelExit;
7114
+ }
6319
7115
  async isPortExposed(port) {
6320
7116
  try {
6321
7117
  const sessionId = await this.ensureDefaultSession();
@@ -7585,8 +8381,24 @@ var Sandbox = class Sandbox extends Container {
7585
8381
  });
7586
8382
  }
7587
8383
  }
8384
+ async configureR2EgressOutbound(params) {
8385
+ const ctx = this.ctx;
8386
+ if (!ctx.container?.interceptOutboundHttp) throw new InvalidMountConfigError("R2 binding mounts require container outbound interception support");
8387
+ if (!ctx.exports?.ContainerProxy) throw new InvalidMountConfigError("R2 binding mounts require exporting ContainerProxy from the Worker entrypoint");
8388
+ const fetcher = ctx.exports.ContainerProxy({ props: {
8389
+ enableInternet: this.enableInternet,
8390
+ containerId: this.ctx.id.toString(),
8391
+ className: R2_EGRESS_PROXY_TARGET_CLASS_NAME,
8392
+ outboundByHostOverrides: { "r2.internal": {
8393
+ method: "r2EgressMount",
8394
+ params
8395
+ } }
8396
+ } });
8397
+ if (!isFetcher(fetcher)) throw new InvalidMountConfigError("R2 binding mounts require ContainerProxy to return a valid Fetcher");
8398
+ await ctx.container.interceptOutboundHttp("r2.internal", fetcher);
8399
+ }
7588
8400
  };
7589
8401
 
7590
8402
  //#endregion
7591
8403
  export { DesktopInvalidOptionsError as A, CommandClient as C, BackupNotFoundError as D, BackupExpiredError as E, InvalidBackupConfigError as F, ProcessExitedBeforeReadyError as I, ProcessReadyTimeoutError as L, DesktopProcessCrashedError as M, DesktopStartFailedError as N, BackupRestoreError as O, DesktopUnavailableError as P, RPCTransportError as R, DesktopClient as S, BackupCreateError as T, UtilityClient as _, BucketMountError as a, GitClient as b, MissingCredentialsError as c, parseSSEStream as d, responseToAsyncIterable as f, SandboxClient as g, streamFile as h, proxyTerminal as i, DesktopNotStartedError as j, DesktopInvalidCoordinatesError as k, S3FSMountError as l, collectFile as m, getSandbox as n, BucketUnmountError as o, CodeInterpreter as p, proxyToSandbox as r, InvalidMountConfigError as s, Sandbox as t, asyncIterableToSSEStream as u, ProcessClient as v, BackupClient as w, FileClient as x, PortClient as y, SessionTerminatedError as z };
7592
- //# sourceMappingURL=sandbox-uC1vzWtG.js.map
8404
+ //# sourceMappingURL=sandbox-BcEq4aUF.js.map