@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.
package/Dockerfile CHANGED
@@ -152,6 +152,38 @@ COPY --from=builder /app/packages/sandbox-container/dist/runtime/executors/javas
152
152
  # Users with custom startup scripts that call `bun /container-server/dist/index.js` need this
153
153
  COPY --from=builder /app/packages/sandbox-container/dist/index.js ./dist/
154
154
 
155
+ # ============================================================================
156
+ # cloudflared (Cloudflare Tunnel daemon) — enables sandbox.tunnels.*
157
+ # Pinned version with sha256 verification per architecture.
158
+ # Skipped in the musl/Alpine stage (no musl prebuilt).
159
+ # ============================================================================
160
+ ARG CLOUDFLARED_VERSION=2026.3.0
161
+ ARG CLOUDFLARED_SHA256_AMD64=4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30
162
+ ARG CLOUDFLARED_SHA256_ARM64=0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee
163
+ RUN set -eux; \
164
+ arch="$(dpkg --print-architecture)"; \
165
+ case "$arch" in \
166
+ amd64) suffix=amd64; sha="${CLOUDFLARED_SHA256_AMD64}" ;; \
167
+ arm64) suffix=arm64; sha="${CLOUDFLARED_SHA256_ARM64}" ;; \
168
+ *) echo "Unsupported arch for cloudflared: $arch" >&2; exit 1 ;; \
169
+ esac; \
170
+ url="https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-${suffix}"; \
171
+ curl -fsSL -o /usr/local/bin/cloudflared "$url"; \
172
+ echo "$sha /usr/local/bin/cloudflared" | sha256sum -c -; \
173
+ chmod +x /usr/local/bin/cloudflared; \
174
+ cloudflared --version
175
+
176
+ # Inject the host's extra CA bundle when running locally behind a TLS-
177
+ # intercepting proxy (e.g. Cloudflare WARP / Zero Trust). See
178
+ # DOCKER_README.md for the local-dev setup. No-op when the secret
179
+ # isn't passed.
180
+ RUN --mount=type=secret,id=wrangler_ca \
181
+ if [ -f /run/secrets/wrangler_ca ] && [ -s /run/secrets/wrangler_ca ]; then \
182
+ cp /run/secrets/wrangler_ca /usr/local/share/ca-certificates/wrangler-dev-ca.crt && \
183
+ cat /run/secrets/wrangler_ca >> /etc/ssl/certs/ca-certificates.crt && \
184
+ update-ca-certificates; \
185
+ fi
186
+
155
187
  RUN mkdir -p /workspace
156
188
 
157
189
  # Expose the application port (3000 for control)
package/README.md CHANGED
@@ -98,6 +98,51 @@ export default {
98
98
  };
99
99
  ```
100
100
 
101
+ ## Quick tunnels
102
+
103
+ `sandbox.tunnels.get(port)` exposes a service running inside the
104
+ sandbox on a `*.trycloudflare.com` URL. No Cloudflare account or DNS
105
+ setup required — cloudflared opens a persistent QUIC connection to
106
+ Cloudflare's edge and Cloudflare hands back a hostname.
107
+
108
+ ```ts
109
+ // Inside a Worker with an RPC-transport sandbox:
110
+ const tunnel = await sandbox.tunnels.get(8080);
111
+ console.log(tunnel.url);
112
+ // → https://random-words-here.trycloudflare.com
113
+
114
+ // Repeated calls for the same port return the same record:
115
+ const same = await sandbox.tunnels.get(8080);
116
+ console.log(same.url === tunnel.url); // true
117
+
118
+ // Tear down by port number or by the record:
119
+ await sandbox.tunnels.destroy(8080);
120
+ // or: await sandbox.tunnels.destroy(tunnel);
121
+ ```
122
+
123
+ `get()` is idempotent: it consults a per-sandbox cache in Durable
124
+ Object storage, returns the cached record on a hit, and only spawns a
125
+ fresh cloudflared process on a miss. `list()` returns every cached
126
+ tunnel.
127
+
128
+ Notes:
129
+
130
+ - Requires the RPC transport. The route-based transport's `tunnels`
131
+ stub throws "RPC transport required".
132
+ - URLs do **not** survive a container restart. Cloudflare assigns the
133
+ hostname during cloudflared's startup handshake, so every restart
134
+ yields a new URL. The SDK clears its cache on container start, so
135
+ the next `get(port)` after a restart returns a fresh record.
136
+ - The first fetch through a brand-new URL can take a couple of
137
+ seconds while DNS propagates, even after `get()` resolves.
138
+ - `*.trycloudflare.com` buffers `text/event-stream` responses.
139
+ WebSockets work fine.
140
+ - The musl/Alpine image variant does not ship cloudflared (no upstream
141
+ musl prebuilt); `sandbox.tunnels` is unavailable on that variant.
142
+ - Local builds behind a TLS-intercepting proxy (e.g. Cloudflare WARP)
143
+ need the host CA bundle injected at build time — see
144
+ [DOCKER_README.md](../../DOCKER_README.md).
145
+
101
146
  ## Documentation
102
147
 
103
148
  **📖 [Full Documentation](https://developers.cloudflare.com/sandbox/)**
@@ -115,6 +160,7 @@ export default {
115
160
  - **File System Access** - Read, write, and manage files
116
161
  - **Command Execution** - Run any command with streaming support
117
162
  - **Preview URLs** - Expose services with public URLs
163
+ - **Quick tunnels** - Zero-config `*.trycloudflare.com` URLs via `sandbox.tunnels.get(port)`
118
164
  - **Git Integration** - Clone repositories directly
119
165
 
120
166
  ## Contributing
@@ -2,12 +2,6 @@ import { DurableObject } from "cloudflare:workers";
2
2
 
3
3
  //#region src/bridge/types.d.ts
4
4
 
5
- /**
6
- * Wire types and configuration types for the Cloudflare Sandbox Bridge.
7
- *
8
- * These types define the JSON payloads exchanged between HTTP clients
9
- * (e.g. the Python `CloudflareSandboxClient`) and the bridge worker.
10
- */
11
5
  /**
12
6
  * Configuration options for the bridge() factory.
13
7
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AAgBA;AAgCA;;;;;AAKgB,UArCC,YAAA,CAqCD;EAEA;;;;AAehB;;;;ICEgB;IAyBA,QAAA,CAAA,EAAA,MAAA;;;;AC7EhB;AAOA;AAWC;EA6CY,cAAS,CAAA,EAAA,MAAA;EAAsB;;;;;EA6FxB,WAAA,CAAA,EAAA,MAAA;;;;;;;;UFhIH,cAAA;kBAEJ,wBAEJ,mBACJ,WAAW,QAAQ;EGiCR,SAAM,EAAA,UAAA,EH/BN,mBG+BM,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EH7Bb,gBG6Ba,CAAA,EAAA,IAAA,GH5BV,OG4BU,CAAA,IAAA,CAAA;EACZ,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;UHjBO,SAAA;;;;;;;;;;;AAAjB;;;;ACEA;AAyBgB,iBAzBA,UAAA,CAyBoB,GAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;AC7EpC;AAOA;AAmDU,iBDmBM,oBAAA,CClBL,QAAsB,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;AFThB,UElDA,cAAA,CFkDS;;;;ECEV,eAAU,CAAA,EAAA,MAAA;AAyB1B;UCtEiB,SAAA;;;EAPA;EAOA,QAAA,EAAA,MAAS;EAmDhB;EAKG,KAAA,EAAA,MAAS;EAAsB;EA2BH,MAAA,EA3E/B,QA2E+B,CA3EtB,cA2EsB,CAAA;EA6CG;EAYE,YAAA,EAAA,MAAA,GAAA,IAAA;;;;;;;UAzFpC,WAAA,CAKoB;EAAa,OAAA,EAJhC,sBAIgC;;;cAA9B,QAAA,SAAiB,cAAc;ECG5B,QAAA,MAAM;EACZ;EACC,QAAA,cAAA;EACQ;EAAhB,QAAA,WAAA;EAAe;;;;;;;;;;;mCDqBuB;;;;;;sCA6CG;;;;wCAYE;;;;cAS1B,QAAQ;;;;;oBAeF,iBAAiB;;;;;uBAUd;WAyBZ;;;;;;;;;;;;;;;;;;;;;;;;AAvMjB;AAWC;AA6CD;;;;;;;;;;AA+IiB,iBC5ID,MAAA,CD4IC,MAAA,EC3IP,cD2IO,EAAA,MAAA,CAAA,EC1IN,YD0IM,CAAA,ECzId,eDyIc,CCzIE,SDyIF,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/bridge/types.ts","../../src/bridge/helpers.ts","../../src/bridge/warm-pool.ts","../../src/bridge/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AA6DgB,UAvCC,YAAA,CAuCD;EAEP;;;AAaT;;;;ICJgB,OAAA,CAAA,EAAU,MAAA;IAyBV;;;;AC7EhB;AAOA;AAWC;AA6CD;EAA4C,cAAA,CAAA,EAAA,MAAA;EA2BH;;;;;EAiFf,WAAA,CAAA,EAAA,MAAA;;;;;;;;UFzIT,cAAA;EGgCD,KAAA,EAAA,OAAM,EH9BT,OG8BS,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EH5Bb,gBG4Ba,CAAA,EH3BjB,QG2BiB,GH3BN,OG2BM,CH3BE,QG2BF,CAAA;EACZ,SAAA,EAAA,UAAA,EH1BM,mBG0BN,EAAA,GAAA,EAAA,GAAA,EAAA,GAAA,EHxBD,gBGwBC,CAAA,EAAA,IAAA,GHvBE,OGuBF,CAAA,IAAA,CAAA;EACC,CAAA,GAAA,EAAA,MAAA,CAAA,EAAA,OAAA;;;;;;UHZM,SAAA;;;;;;;;;;;AAAjB;;;;ACJA;AAyBgB,iBAzBA,UAAA,CAyBoB,GAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;AC7EpC;AAOA;AAmDU,iBDmBM,oBAAA,CClBL,QAAsB,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;AFHhB,UExDA,cAAA,CFwDS;;;;ECJV,eAAU,CAAA,EAAA,MAAA;AAyB1B;UCtEiB,SAAA;;;EAPA;EAOA,QAAA,EAAA,MAAS;EAmDhB;EAKG,KAAA,EAAA,MAAS;EAAsB;EA2BH,MAAA,EA3E/B,QA2E+B,CA3EtB,cA2EsB,CAAA;EA6CG;EAYE,YAAA,EAAA,MAAA,GAAA,IAAA;;;;;;;UAzFpC,WAAA,CAKoB;EAAa,OAAA,EAJhC,sBAIgC;;;cAA9B,QAAA,SAAiB,cAAc;ECG5B,QAAA,MAAM;EACZ;EACC,QAAA,cAAA;EACQ;EAAhB,QAAA,WAAA;EAAe;;;;;;;;;;;mCDqBuB;;;;;;sCA6CG;;;;wCAYE;;;;cAS1B,QAAQ;;;;;oBAeF,iBAAiB;;;;;uBAUd;WAyBZ;;;;;;;;;;;;;;;;;;;;;;;;AAvMjB;AAWC;AA6CD;;;;;;;;;;AA+IiB,iBC5ID,MAAA,CD4IC,MAAA,EC3IP,cD2IO,EAAA,MAAA,CAAA,EC1IN,YD0IM,CAAA,ECzId,eDyIc,CCzIE,SDyIF,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import "../dist-B_eXrP83.js";
2
- import "../errors-CBi-O-pF.js";
3
- import { h as streamFile, n as getSandbox } from "../sandbox-uC1vzWtG.js";
2
+ import "../errors-8Hvune8K.js";
3
+ import { h as streamFile, n as getSandbox } from "../sandbox-BcEq4aUF.js";
4
4
  import { DurableObject, env } from "cloudflare:workers";
5
5
  import { Hono } from "hono";
6
6
 
@@ -208,11 +208,10 @@ const OPENAPI_SCHEMA = {
208
208
  },
209
209
  MountBucketRequestOptions: {
210
210
  type: "object",
211
- required: ["endpoint"],
212
211
  properties: {
213
212
  endpoint: {
214
213
  type: "string",
215
- description: "S3-compatible endpoint URL.",
214
+ description: "S3-compatible endpoint URL for remote mounts. Mutually exclusive with top-level `binding`.",
216
215
  example: "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com"
217
216
  },
218
217
  readOnly: {
@@ -222,28 +221,35 @@ const OPENAPI_SCHEMA = {
222
221
  },
223
222
  prefix: {
224
223
  type: "string",
225
- description: "Optional prefix/subdirectory within the bucket to mount. Must start and end with `/`.",
226
- example: "/uploads/images/"
224
+ description: "Optional prefix/subdirectory within the bucket to mount. Must start with `/`. Trailing slashes are stripped automatically.",
225
+ example: "/uploads/images"
227
226
  },
228
227
  credentials: {
229
228
  $ref: "#/components/schemas/MountBucketCredentials",
230
229
  description: "Explicit credentials. When omitted, the SDK auto-detects from Worker secrets (R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY or AWS equivalents)."
230
+ },
231
+ s3fsOptions: {
232
+ type: "array",
233
+ items: { type: "string" },
234
+ description: "Advanced: Override or extend s3fs mount options for both remote mounts and R2 binding mounts.",
235
+ example: ["nomultipart"]
231
236
  }
232
237
  }
233
238
  },
234
239
  MountBucketRequest: {
235
240
  type: "object",
236
- required: [
237
- "bucket",
238
- "mountPath",
239
- "options"
240
- ],
241
+ required: ["mountPath", "options"],
241
242
  properties: {
242
243
  bucket: {
243
244
  type: "string",
244
- description: "Bucket name.",
245
+ description: "Remote bucket name for endpoint-based S3-compatible mounts.",
245
246
  example: "my-r2-bucket"
246
247
  },
248
+ binding: {
249
+ type: "string",
250
+ description: "Worker R2 binding name for credential-less R2 binding mounts. Mutually exclusive with `options.endpoint`.",
251
+ example: "MY_BUCKET"
252
+ },
247
253
  mountPath: {
248
254
  type: "string",
249
255
  description: "Absolute path in the container to mount at.",
@@ -1614,6 +1620,55 @@ function esc(s) {
1614
1620
  function getSandbox$1(ns, containerUUID) {
1615
1621
  return getSandbox(ns, containerUUID);
1616
1622
  }
1623
+ function hasEndpoint(options) {
1624
+ return "endpoint" in options && typeof options.endpoint === "string";
1625
+ }
1626
+ function hasEndpointProperty(options) {
1627
+ return "endpoint" in options && options.endpoint !== void 0;
1628
+ }
1629
+ function hasCredentials(options) {
1630
+ return "credentials" in options && options.credentials !== void 0;
1631
+ }
1632
+ function isStringArray(value) {
1633
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
1634
+ }
1635
+ function validateMountOptions(options, binding) {
1636
+ if ("endpoint" in options && !hasEndpoint(options)) return errorJson("options.endpoint must be a string when provided", "invalid_request", 400);
1637
+ if (binding !== void 0 && typeof binding !== "string") return errorJson("binding must be a string when provided", "invalid_request", 400);
1638
+ if (binding === "") return errorJson("binding must be a non-empty string when provided", "invalid_request", 400);
1639
+ if (binding !== void 0 && hasEndpointProperty(options)) return errorJson("Provide either binding or options.endpoint, not both", "invalid_request", 400);
1640
+ if (options.s3fsOptions !== void 0 && !isStringArray(options.s3fsOptions)) return errorJson("options.s3fsOptions must be an array of strings when provided", "invalid_request", 400);
1641
+ if (options.readOnly !== void 0 && typeof options.readOnly !== "boolean") return errorJson("options.readOnly must be a boolean when provided", "invalid_request", 400);
1642
+ if (options.prefix !== void 0 && typeof options.prefix !== "string") return errorJson("options.prefix must be a string when provided", "invalid_request", 400);
1643
+ if ("credentials" in options && options.credentials !== void 0 && (typeof options.credentials !== "object" || options.credentials === null || typeof options.credentials.accessKeyId !== "string" || typeof options.credentials.secretAccessKey !== "string")) return errorJson("options.credentials must include string accessKeyId and secretAccessKey", "invalid_request", 400);
1644
+ return null;
1645
+ }
1646
+ function resolveMountBucketName(body) {
1647
+ if (hasEndpoint(body.options)) {
1648
+ if (body.bucket && typeof body.bucket === "string") return body.bucket;
1649
+ return errorJson("bucket must be a non-empty string for remote mounts", "invalid_request", 400);
1650
+ }
1651
+ if (body.binding !== void 0) return body.binding;
1652
+ return errorJson("binding must be a non-empty string for R2 binding mounts", "invalid_request", 400);
1653
+ }
1654
+ function toSDKMountOptions(options) {
1655
+ if (hasEndpoint(options)) {
1656
+ const remoteOptions = { endpoint: options.endpoint };
1657
+ if (options.readOnly !== void 0) remoteOptions.readOnly = options.readOnly;
1658
+ if (options.prefix !== void 0) remoteOptions.prefix = options.prefix;
1659
+ if (hasCredentials(options)) remoteOptions.credentials = {
1660
+ accessKeyId: options.credentials.accessKeyId,
1661
+ secretAccessKey: options.credentials.secretAccessKey
1662
+ };
1663
+ if (options.s3fsOptions !== void 0) remoteOptions.s3fsOptions = options.s3fsOptions;
1664
+ return remoteOptions;
1665
+ }
1666
+ const r2BindingOptions = {};
1667
+ if (options.readOnly !== void 0) r2BindingOptions.readOnly = options.readOnly;
1668
+ if (options.prefix !== void 0) r2BindingOptions.prefix = options.prefix;
1669
+ if (options.s3fsOptions !== void 0) r2BindingOptions.s3fsOptions = options.s3fsOptions;
1670
+ return r2BindingOptions;
1671
+ }
1617
1672
  function createBridgeApp(config) {
1618
1673
  const app = new Hono();
1619
1674
  const { sandboxBinding, warmPoolBinding, apiPrefix } = config;
@@ -1902,21 +1957,18 @@ function createBridgeApp(config) {
1902
1957
  } catch {
1903
1958
  return errorJson("Invalid JSON body", "invalid_request", 400);
1904
1959
  }
1905
- if (!body.bucket || typeof body.bucket !== "string") return errorJson("bucket must be a non-empty string", "invalid_request", 400);
1960
+ if (body.bucket !== void 0 && (typeof body.bucket !== "string" || body.bucket === "")) return errorJson("bucket must be a non-empty string", "invalid_request", 400);
1906
1961
  if (!body.mountPath || typeof body.mountPath !== "string") return errorJson("mountPath must be a non-empty string", "invalid_request", 400);
1907
1962
  if (!body.mountPath.startsWith("/")) return errorJson("mountPath must be an absolute path (start with /)", "invalid_request", 400);
1908
- if (!body.options || typeof body.options !== "object") return errorJson("options must be an object", "invalid_request", 400);
1909
- if (!body.options.endpoint || typeof body.options.endpoint !== "string") return errorJson("options.endpoint must be a non-empty string", "invalid_request", 400);
1963
+ if (!body.options || typeof body.options !== "object" || Array.isArray(body.options)) return errorJson("options must be an object", "invalid_request", 400);
1964
+ const optionsError = validateMountOptions(body.options, body.binding);
1965
+ if (optionsError) return optionsError;
1966
+ const bucketName = resolveMountBucketName(body);
1967
+ if (bucketName instanceof Response) return bucketName;
1910
1968
  const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
1911
- const sdkOptions = { endpoint: body.options.endpoint };
1912
- if (body.options.readOnly !== void 0) sdkOptions.readOnly = body.options.readOnly;
1913
- if (body.options.prefix !== void 0) sdkOptions.prefix = body.options.prefix;
1914
- if (body.options.credentials) sdkOptions.credentials = {
1915
- accessKeyId: body.options.credentials.accessKeyId,
1916
- secretAccessKey: body.options.credentials.secretAccessKey
1917
- };
1969
+ const sdkOptions = toSDKMountOptions(body.options);
1918
1970
  try {
1919
- await sandbox.mountBucket(body.bucket, body.mountPath, sdkOptions);
1971
+ await sandbox.mountBucket(bucketName, body.mountPath, sdkOptions);
1920
1972
  return c.json({ ok: true });
1921
1973
  } catch (err) {
1922
1974
  return errorJson(`mount failed: ${err instanceof Error ? err.message : String(err)}`, "mount_error", 502);