@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 +32 -0
- package/README.md +46 -0
- package/dist/bridge/index.d.ts +0 -6
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/index.js +75 -23
- package/dist/bridge/index.js.map +1 -1
- package/dist/{errors-CBi-O-pF.js → errors-8Hvune8K.js} +2 -2
- package/dist/{errors-CBi-O-pF.js.map → errors-8Hvune8K.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-uC1vzWtG.js → sandbox-BcEq4aUF.js} +890 -78
- package/dist/sandbox-BcEq4aUF.js.map +1 -0
- package/dist/{sandbox-BVgScWy9.d.ts → sandbox-KdzTTnWq.d.ts} +198 -42
- package/dist/sandbox-KdzTTnWq.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/sandbox-BVgScWy9.d.ts.map +0 -1
- package/dist/sandbox-uC1vzWtG.js.map +0 -1
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
|
package/dist/bridge/index.d.ts
CHANGED
|
@@ -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":";;;;;;;
|
|
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"}
|
package/dist/bridge/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../dist-B_eXrP83.js";
|
|
2
|
-
import "../errors-
|
|
3
|
-
import { h as streamFile, n as getSandbox } from "../sandbox-
|
|
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
|
|
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: "
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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);
|