@cloudflare/sandbox 0.10.3 → 0.12.0
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 +0 -55
- package/dist/bridge/index.js +194 -3
- package/dist/bridge/index.js.map +1 -1
- package/dist/{contexts-D_shbnJs.d.ts → contexts-B0_bcx9f.d.ts} +2 -33
- package/dist/contexts-B0_bcx9f.d.ts.map +1 -0
- package/dist/{errors-8Hvune8K.js → errors-aRUdk9K8.js} +3 -21
- package/dist/errors-aRUdk9K8.js.map +1 -0
- package/dist/index.d.ts +3 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -5
- package/dist/index.js.map +1 -0
- package/dist/openai/index.d.ts +1 -1
- package/dist/opencode/index.d.ts +2 -2
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-CwcSm_60.d.ts → sandbox-D3N9M5EI.d.ts} +179 -465
- package/dist/sandbox-D3N9M5EI.d.ts.map +1 -0
- package/dist/{sandbox-B-MUmsli.js → sandbox-Duj2gvUC.js} +2207 -763
- package/dist/sandbox-Duj2gvUC.js.map +1 -0
- package/package.json +2 -2
- package/dist/contexts-D_shbnJs.d.ts.map +0 -1
- package/dist/errors-8Hvune8K.js.map +0 -1
- package/dist/sandbox-B-MUmsli.js.map +0 -1
- package/dist/sandbox-CwcSm_60.d.ts.map +0 -1
package/Dockerfile
CHANGED
|
@@ -284,61 +284,6 @@ EXPOSE 4096
|
|
|
284
284
|
|
|
285
285
|
ENTRYPOINT ["/container-server/sandbox"]
|
|
286
286
|
|
|
287
|
-
# ============================================================================
|
|
288
|
-
# Desktop variant — full Linux desktop with robotgo native control
|
|
289
|
-
# ============================================================================
|
|
290
|
-
FROM golang:1.25-bookworm AS go-builder
|
|
291
|
-
|
|
292
|
-
RUN mkdir -p /usr/local/share/ca-certificates
|
|
293
|
-
RUN --mount=type=secret,id=wrangler_ca \
|
|
294
|
-
apt-get update && apt-get install -y --no-install-recommends ca-certificates && \
|
|
295
|
-
if [ -f /run/secrets/wrangler_ca ] && [ -s /run/secrets/wrangler_ca ]; then \
|
|
296
|
-
cp /run/secrets/wrangler_ca /usr/local/share/ca-certificates/wrangler-dev-ca.crt; \
|
|
297
|
-
fi && \
|
|
298
|
-
update-ca-certificates && \
|
|
299
|
-
rm -rf /var/lib/apt/lists/*
|
|
300
|
-
|
|
301
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
302
|
-
gcc libx11-dev libxtst-dev libxinerama-dev libpng-dev \
|
|
303
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
304
|
-
|
|
305
|
-
COPY packages/sandbox-container/native/desktop-wrapper/ /build/
|
|
306
|
-
WORKDIR /build
|
|
307
|
-
RUN go mod tidy && go build -buildmode=c-shared -o /usr/lib/desktop.so .
|
|
308
|
-
|
|
309
|
-
FROM runtime-base AS desktop
|
|
310
|
-
|
|
311
|
-
# Install display stack
|
|
312
|
-
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
313
|
-
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
|
314
|
-
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
|
315
|
-
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \
|
|
316
|
-
apt-get update && apt-get install -y --no-install-recommends \
|
|
317
|
-
xvfb x11vnc novnc websockify \
|
|
318
|
-
xfce4 xfce4-terminal dbus-x11 \
|
|
319
|
-
libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 \
|
|
320
|
-
libxss1 libxtst6 libxi6 libxinerama1 \
|
|
321
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
322
|
-
|
|
323
|
-
COPY --from=go-builder /usr/lib/desktop.so /usr/lib/desktop.so
|
|
324
|
-
COPY --from=go-builder /usr/lib/desktop.h /usr/lib/desktop.h
|
|
325
|
-
|
|
326
|
-
COPY --from=builder /app/packages/sandbox-container/dist/sandbox /container-server/sandbox
|
|
327
|
-
COPY --from=builder /app/packages/sandbox-container/dist/workers/ /container-server/workers/
|
|
328
|
-
|
|
329
|
-
# Install koffi for FFI worker thread
|
|
330
|
-
WORKDIR /container-server
|
|
331
|
-
RUN npm init -y && npm install koffi
|
|
332
|
-
|
|
333
|
-
EXPOSE 6080
|
|
334
|
-
|
|
335
|
-
ENV DISPLAY=:99
|
|
336
|
-
ENV PYTHON_POOL_MIN_SIZE=0
|
|
337
|
-
ENV JAVASCRIPT_POOL_MIN_SIZE=0
|
|
338
|
-
ENV TYPESCRIPT_POOL_MIN_SIZE=0
|
|
339
|
-
|
|
340
|
-
ENTRYPOINT ["/container-server/sandbox"]
|
|
341
|
-
|
|
342
287
|
# ============================================================================
|
|
343
288
|
# Stage 5d: Musl image - Alpine-based with musl-linked binary
|
|
344
289
|
# ============================================================================
|
package/dist/bridge/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../dist-B_eXrP83.js";
|
|
2
|
-
import "../errors-
|
|
3
|
-
import {
|
|
2
|
+
import "../errors-aRUdk9K8.js";
|
|
3
|
+
import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-Duj2gvUC.js";
|
|
4
4
|
import { DurableObject, env } from "cloudflare:workers";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
|
|
@@ -228,6 +228,11 @@ const OPENAPI_SCHEMA = {
|
|
|
228
228
|
$ref: "#/components/schemas/MountBucketCredentials",
|
|
229
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
230
|
},
|
|
231
|
+
credentialProxy: {
|
|
232
|
+
type: "boolean",
|
|
233
|
+
description: "Keep credentials in the Durable Object and sign intercepted s3fs requests from the Worker. Credentials may be explicit or auto-detected from Worker secrets.",
|
|
234
|
+
default: false
|
|
235
|
+
},
|
|
231
236
|
s3fsOptions: {
|
|
232
237
|
type: "array",
|
|
233
238
|
items: { type: "string" },
|
|
@@ -267,6 +272,50 @@ const OPENAPI_SCHEMA = {
|
|
|
267
272
|
example: "/mnt/data"
|
|
268
273
|
} }
|
|
269
274
|
},
|
|
275
|
+
TunnelRequest: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: { name: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description: "Subdomain prefix for a named tunnel, such as `app`. Do not pass a full hostname. Omit to create or reuse an ephemeral tunnel.",
|
|
280
|
+
example: "app"
|
|
281
|
+
} }
|
|
282
|
+
},
|
|
283
|
+
Tunnel: {
|
|
284
|
+
type: "object",
|
|
285
|
+
required: [
|
|
286
|
+
"id",
|
|
287
|
+
"port",
|
|
288
|
+
"url",
|
|
289
|
+
"hostname",
|
|
290
|
+
"createdAt"
|
|
291
|
+
],
|
|
292
|
+
properties: {
|
|
293
|
+
id: { type: "string" },
|
|
294
|
+
port: {
|
|
295
|
+
type: "integer",
|
|
296
|
+
description: "Container port served by the tunnel.",
|
|
297
|
+
example: 8080
|
|
298
|
+
},
|
|
299
|
+
url: {
|
|
300
|
+
type: "string",
|
|
301
|
+
format: "uri",
|
|
302
|
+
example: "https://app.example.com"
|
|
303
|
+
},
|
|
304
|
+
hostname: {
|
|
305
|
+
type: "string",
|
|
306
|
+
example: "app.example.com"
|
|
307
|
+
},
|
|
308
|
+
createdAt: {
|
|
309
|
+
type: "string",
|
|
310
|
+
format: "date-time"
|
|
311
|
+
},
|
|
312
|
+
name: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "Present for named tunnels only.",
|
|
315
|
+
example: "app"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
270
319
|
ErrorResponse: {
|
|
271
320
|
type: "object",
|
|
272
321
|
required: ["error", "code"],
|
|
@@ -290,7 +339,8 @@ const OPENAPI_SCHEMA = {
|
|
|
290
339
|
"pool_error",
|
|
291
340
|
"mount_error",
|
|
292
341
|
"unmount_error",
|
|
293
|
-
"session_error"
|
|
342
|
+
"session_error",
|
|
343
|
+
"tunnel_error"
|
|
294
344
|
]
|
|
295
345
|
}
|
|
296
346
|
}
|
|
@@ -439,6 +489,103 @@ const OPENAPI_SCHEMA = {
|
|
|
439
489
|
}
|
|
440
490
|
}
|
|
441
491
|
} },
|
|
492
|
+
"/v1/sandbox/{id}/tunnel/{port}": {
|
|
493
|
+
post: {
|
|
494
|
+
operationId: "createTunnel",
|
|
495
|
+
summary: "Create or reuse a tunnel for a sandbox port",
|
|
496
|
+
description: "Returns an existing tunnel for the port when one is already recorded, or provisions one when needed. The service must already be listening inside the sandbox. Omit `name` for an ephemeral `*.trycloudflare.com` tunnel, or pass `name` to choose the subdomain prefix for a named tunnel. Use a value such as `app`, not a full hostname.",
|
|
497
|
+
"x-codeSamples": [{
|
|
498
|
+
lang: "curl",
|
|
499
|
+
label: "Ephemeral tunnel",
|
|
500
|
+
source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
|
|
501
|
+
}, {
|
|
502
|
+
lang: "curl",
|
|
503
|
+
label: "Named tunnel",
|
|
504
|
+
source: "curl -X POST https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"app\"}'"
|
|
505
|
+
}],
|
|
506
|
+
parameters: [{
|
|
507
|
+
name: "id",
|
|
508
|
+
in: "path",
|
|
509
|
+
required: true,
|
|
510
|
+
schema: { type: "string" },
|
|
511
|
+
description: "Sandbox instance name."
|
|
512
|
+
}, {
|
|
513
|
+
name: "port",
|
|
514
|
+
in: "path",
|
|
515
|
+
required: true,
|
|
516
|
+
schema: {
|
|
517
|
+
type: "integer",
|
|
518
|
+
minimum: 1024,
|
|
519
|
+
maximum: 65535
|
|
520
|
+
},
|
|
521
|
+
description: "Container port to tunnel. Port 3000 is reserved."
|
|
522
|
+
}],
|
|
523
|
+
requestBody: {
|
|
524
|
+
required: false,
|
|
525
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/TunnelRequest" } } }
|
|
526
|
+
},
|
|
527
|
+
responses: {
|
|
528
|
+
"200": {
|
|
529
|
+
description: "Tunnel created or reused.",
|
|
530
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/Tunnel" } } }
|
|
531
|
+
},
|
|
532
|
+
"400": { $ref: "#/components/responses/InvalidRequest" },
|
|
533
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
534
|
+
"502": {
|
|
535
|
+
description: "Tunnel creation failed.",
|
|
536
|
+
content: { "application/json": {
|
|
537
|
+
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
|
538
|
+
example: {
|
|
539
|
+
error: "tunnel failed: cloudflared could not be found",
|
|
540
|
+
code: "tunnel_error"
|
|
541
|
+
}
|
|
542
|
+
} }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
delete: {
|
|
547
|
+
operationId: "deleteTunnel",
|
|
548
|
+
summary: "Delete the tunnel for a sandbox port",
|
|
549
|
+
description: "Stops the tunnel process for the port and removes any named-tunnel Cloudflare resources tracked by the sandbox.",
|
|
550
|
+
"x-codeSamples": [{
|
|
551
|
+
lang: "curl",
|
|
552
|
+
label: "Delete tunnel",
|
|
553
|
+
source: "curl -X DELETE https://$HOST/v1/sandbox/my-sandbox/tunnel/8080 \\\n -H \"Authorization: Bearer $SANDBOX_API_KEY\""
|
|
554
|
+
}],
|
|
555
|
+
parameters: [{
|
|
556
|
+
name: "id",
|
|
557
|
+
in: "path",
|
|
558
|
+
required: true,
|
|
559
|
+
schema: { type: "string" },
|
|
560
|
+
description: "Sandbox instance name."
|
|
561
|
+
}, {
|
|
562
|
+
name: "port",
|
|
563
|
+
in: "path",
|
|
564
|
+
required: true,
|
|
565
|
+
schema: {
|
|
566
|
+
type: "integer",
|
|
567
|
+
minimum: 1024,
|
|
568
|
+
maximum: 65535
|
|
569
|
+
},
|
|
570
|
+
description: "Container port whose tunnel should be deleted. Port 3000 is reserved."
|
|
571
|
+
}],
|
|
572
|
+
responses: {
|
|
573
|
+
"204": { description: "Tunnel deleted or already absent." },
|
|
574
|
+
"400": { $ref: "#/components/responses/InvalidRequest" },
|
|
575
|
+
"401": { $ref: "#/components/responses/Unauthorized" },
|
|
576
|
+
"502": {
|
|
577
|
+
description: "Tunnel deletion failed.",
|
|
578
|
+
content: { "application/json": {
|
|
579
|
+
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
|
580
|
+
example: {
|
|
581
|
+
error: "tunnel failed: cleanup failed",
|
|
582
|
+
code: "tunnel_error"
|
|
583
|
+
}
|
|
584
|
+
} }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
},
|
|
442
589
|
"/v1/sandbox/{id}/file/{path}": {
|
|
443
590
|
get: {
|
|
444
591
|
operationId: "readFile",
|
|
@@ -1632,6 +1779,24 @@ function hasCredentials(options) {
|
|
|
1632
1779
|
function isStringArray(value) {
|
|
1633
1780
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1634
1781
|
}
|
|
1782
|
+
function parseTunnelOptions(rawBody) {
|
|
1783
|
+
if (!rawBody.trim()) return void 0;
|
|
1784
|
+
let body;
|
|
1785
|
+
try {
|
|
1786
|
+
body = JSON.parse(rawBody);
|
|
1787
|
+
} catch {
|
|
1788
|
+
return errorJson("Invalid JSON body", "invalid_request", 400);
|
|
1789
|
+
}
|
|
1790
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || body.name === void 0) return;
|
|
1791
|
+
if (typeof body.name !== "string") return errorJson("name must be a string when provided", "invalid_request", 400);
|
|
1792
|
+
try {
|
|
1793
|
+
validateTunnelName(body.name);
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
if (err instanceof SandboxSecurityError) return errorJson(err.message, "invalid_request", 400);
|
|
1796
|
+
throw err;
|
|
1797
|
+
}
|
|
1798
|
+
return { name: body.name };
|
|
1799
|
+
}
|
|
1635
1800
|
function validateMountOptions(options, binding) {
|
|
1636
1801
|
if ("endpoint" in options && !hasEndpoint(options)) return errorJson("options.endpoint must be a string when provided", "invalid_request", 400);
|
|
1637
1802
|
if (binding !== void 0 && typeof binding !== "string") return errorJson("binding must be a string when provided", "invalid_request", 400);
|
|
@@ -1640,6 +1805,7 @@ function validateMountOptions(options, binding) {
|
|
|
1640
1805
|
if (options.s3fsOptions !== void 0 && !isStringArray(options.s3fsOptions)) return errorJson("options.s3fsOptions must be an array of strings when provided", "invalid_request", 400);
|
|
1641
1806
|
if (options.readOnly !== void 0 && typeof options.readOnly !== "boolean") return errorJson("options.readOnly must be a boolean when provided", "invalid_request", 400);
|
|
1642
1807
|
if (options.prefix !== void 0 && typeof options.prefix !== "string") return errorJson("options.prefix must be a string when provided", "invalid_request", 400);
|
|
1808
|
+
if ("credentialProxy" in options && options.credentialProxy !== void 0 && typeof options.credentialProxy !== "boolean") return errorJson("options.credentialProxy must be a boolean when provided", "invalid_request", 400);
|
|
1643
1809
|
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
1810
|
return null;
|
|
1645
1811
|
}
|
|
@@ -1661,6 +1827,7 @@ function toSDKMountOptions(options) {
|
|
|
1661
1827
|
secretAccessKey: options.credentials.secretAccessKey
|
|
1662
1828
|
};
|
|
1663
1829
|
if (options.s3fsOptions !== void 0) remoteOptions.s3fsOptions = options.s3fsOptions;
|
|
1830
|
+
if ("credentialProxy" in options && typeof options.credentialProxy === "boolean") remoteOptions.credentialProxy = options.credentialProxy;
|
|
1664
1831
|
return remoteOptions;
|
|
1665
1832
|
}
|
|
1666
1833
|
const r2BindingOptions = {};
|
|
@@ -1865,6 +2032,30 @@ function createBridgeApp(config) {
|
|
|
1865
2032
|
return errorJson(`write failed: ${err instanceof Error ? err.message : String(err)}`, "workspace_archive_write_error", 502);
|
|
1866
2033
|
}
|
|
1867
2034
|
});
|
|
2035
|
+
app.post(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
|
|
2036
|
+
const port = Number(c.req.param("port"));
|
|
2037
|
+
if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
|
|
2038
|
+
const options = parseTunnelOptions(await c.req.text());
|
|
2039
|
+
if (options instanceof Response) return options;
|
|
2040
|
+
const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2041
|
+
try {
|
|
2042
|
+
const tunnel = await sandbox.tunnels.get(port, options);
|
|
2043
|
+
return c.json(tunnel);
|
|
2044
|
+
} catch (err) {
|
|
2045
|
+
return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
app.delete(`${apiPrefix}/sandbox/:id/tunnel/:port`, async (c) => {
|
|
2049
|
+
const port = Number(c.req.param("port"));
|
|
2050
|
+
if (!validatePort(port)) return errorJson("Invalid port", "invalid_request", 400);
|
|
2051
|
+
const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
|
|
2052
|
+
try {
|
|
2053
|
+
await sandbox.tunnels.destroy(port);
|
|
2054
|
+
return c.body(null, 204);
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
return errorJson(`tunnel failed: ${err instanceof Error ? err.message : String(err)}`, "tunnel_error", 502);
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
1868
2059
|
app.get(`${apiPrefix}/sandbox/:id/running`, async (c) => {
|
|
1869
2060
|
const sandbox = getSandbox$1(getSandboxNs(c.env), c.get("containerUUID"));
|
|
1870
2061
|
try {
|