@cloudflare/sandbox 0.12.9 → 0.12.11

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
@@ -5,20 +5,20 @@ ARG NODE_VERSION=24
5
5
  # cloudflared release — shared across glibc and musl image variants.
6
6
  # Bump this together with the architecture-specific checksums below.
7
7
  ARG CLOUDFLARED_VERSION=2026.3.0
8
+ # fuse-overlayfs release — backs the copy-on-write layer of restored backups.
9
+ # Pinned upstream because Ubuntu 22.04 ships 1.7.1, which returns EEXIST when
10
+ # a restored directory is deleted, recreated and listed before files are
11
+ # created in it (fixed in 1.15). Switch back to the distro package once the
12
+ # base image ships 1.15 or newer.
13
+ # Bump this together with the architecture-specific checksums below.
14
+ ARG FUSE_OVERLAYFS_VERSION=1.18
8
15
  FROM oven/bun:${BUN_VERSION} AS bun-binary
9
16
  FROM node:${NODE_VERSION}-slim AS node-runtime
10
17
 
11
18
  # ============================================================================
12
- # cloudflared (Cloudflare Tunnel daemon) — enables sandbox.tunnels.*
13
- # Version pin lives at the top of this Dockerfile as a global ARG, shared
14
- # by every image variant so they ship the same release.
19
+ # Release downloader — shared base for fetching pinned upstream binaries
15
20
  # ============================================================================
16
- FROM alpine:3.21 AS cloudflared-binary
17
-
18
- ARG TARGETARCH
19
- ARG CLOUDFLARED_VERSION
20
- ARG CLOUDFLARED_SHA256_AMD64=4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30
21
- ARG CLOUDFLARED_SHA256_ARM64=0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee
21
+ FROM alpine:3.21 AS release-downloader
22
22
 
23
23
  # Inject the host's extra CA bundle when downloading behind a TLS-intercepting
24
24
  # proxy (e.g. Cloudflare WARP / Zero Trust). No-op when the secret isn't passed.
@@ -33,6 +33,18 @@ RUN --mount=type=secret,id=wrangler_ca \
33
33
  update-ca-certificates; \
34
34
  fi
35
35
 
36
+ # ============================================================================
37
+ # cloudflared (Cloudflare Tunnel daemon) — enables sandbox.tunnels.*
38
+ # Version pin lives at the top of this Dockerfile as a global ARG, shared
39
+ # by every image variant so they ship the same release.
40
+ # ============================================================================
41
+ FROM release-downloader AS cloudflared-binary
42
+
43
+ ARG TARGETARCH
44
+ ARG CLOUDFLARED_VERSION
45
+ ARG CLOUDFLARED_SHA256_AMD64=4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30
46
+ ARG CLOUDFLARED_SHA256_ARM64=0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee
47
+
36
48
  RUN set -eux; \
37
49
  arch="${TARGETARCH:-}"; \
38
50
  if [ -z "$arch" ]; then \
@@ -49,6 +61,32 @@ RUN set -eux; \
49
61
  chmod +x /cloudflared; \
50
62
  /cloudflared --version
51
63
 
64
+ # ============================================================================
65
+ # fuse-overlayfs — statically linked upstream release for the glibc images
66
+ # ============================================================================
67
+ FROM release-downloader AS fuse-overlayfs-binary
68
+
69
+ ARG TARGETARCH
70
+ ARG FUSE_OVERLAYFS_VERSION
71
+ ARG FUSE_OVERLAYFS_SHA256_AMD64=56b0ae0aeb8abb308b068af2f137ed8d1bd239f4f27e21672ff0def861eea1e8
72
+ ARG FUSE_OVERLAYFS_SHA256_ARM64=82fed736197b2a881a822e5357b488796f654e8371ce8573a1592331510a0133
73
+
74
+ RUN set -eux; \
75
+ arch="${TARGETARCH:-}"; \
76
+ if [ -z "$arch" ]; then \
77
+ arch="$(apk --print-arch)"; \
78
+ fi; \
79
+ case "$arch" in \
80
+ amd64|x86_64) suffix=x86_64; sha="${FUSE_OVERLAYFS_SHA256_AMD64}" ;; \
81
+ arm64|aarch64) suffix=aarch64; sha="${FUSE_OVERLAYFS_SHA256_ARM64}" ;; \
82
+ *) echo "Unsupported arch for fuse-overlayfs: $arch" >&2; exit 1 ;; \
83
+ esac; \
84
+ url="https://github.com/containers/fuse-overlayfs/releases/download/v${FUSE_OVERLAYFS_VERSION}/fuse-overlayfs-${suffix}"; \
85
+ curl -fsSL -o /fuse-overlayfs "$url"; \
86
+ echo "$sha /fuse-overlayfs" | sha256sum -c -; \
87
+ chmod +x /fuse-overlayfs; \
88
+ /fuse-overlayfs --version
89
+
52
90
  # Sandbox container images (default and python variants)
53
91
  # Multi-stage build optimized for Turborepo monorepo
54
92
 
@@ -152,7 +190,6 @@ ENV SANDBOX_VERSION=${SANDBOX_VERSION}
152
190
  # - fuse3: FUSE filesystem support (replaces legacy fuse, needed by s3fs/squashfuse/fuse-overlayfs)
153
191
  # - squashfs-tools: create squashfs archives (mksquashfs)
154
192
  # - squashfuse: mount squashfs in userspace (FUSE)
155
- # - fuse-overlayfs: userspace overlayfs for copy-on-write snapshots
156
193
  # Cache-bust: 2 - Clear apt cache to force fresh package list
157
194
  RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
158
195
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
@@ -160,7 +197,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
160
197
  echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \
161
198
  rm -rf /var/cache/apt/* /var/lib/apt/lists/* && \
162
199
  apt-get update && apt-get install -y --no-install-recommends \
163
- s3fs fuse3 squashfs-tools squashfuse fuse-overlayfs \
200
+ s3fs fuse3 squashfs-tools squashfuse \
164
201
  ca-certificates curl wget procps git unzip zip jq file \
165
202
  inotify-tools \
166
203
  libssl3 zlib1g libbz2-1.0 libreadline8 libsqlite3-0 \
@@ -173,6 +210,10 @@ RUN sed -i 's/#user_allow_other/user_allow_other/' /etc/fuse.conf
173
210
  # fusermount requires /etc/mtab to locate active mounts
174
211
  RUN ln -sf /proc/mounts /etc/mtab
175
212
 
213
+ # Userspace overlayfs for copy-on-write backup restores
214
+ COPY --from=fuse-overlayfs-binary /fuse-overlayfs /usr/bin/fuse-overlayfs
215
+ RUN fuse-overlayfs --version
216
+
176
217
  # Install Node.js from official Node image (defaults to 24 LTS,
177
218
  # override via --build-arg NODE_VERSION=<version>)
178
219
  COPY --from=node-runtime /usr/local/bin/node /usr/local/bin/node
@@ -1,6 +1,6 @@
1
- import "../dist-B_eXrP83.js";
1
+ import "../dist-C3AQh4A0.js";
2
2
  import "../errors-CpkKPbWC.js";
3
- import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-D0rNqxlr.js";
3
+ import { S as validateTunnelName, r as getSandbox, w as streamFile, x as validatePort, y as SandboxSecurityError } from "../sandbox-DKbJiquI.js";
4
4
  import { DurableObject, env, tracing } from "cloudflare:workers";
5
5
  import { Hono } from "hono";
6
6
 
@@ -115,12 +115,23 @@ function validateSessionId(id) {
115
115
  /**
116
116
  * Convert an SSE stream from readFileStream() into a raw byte stream.
117
117
  * Decodes base64 chunks for binary files and UTF-8-encodes text chunks.
118
+ * Reads up to the first chunk before returning. A missing file arrives as an
119
+ * error event inside a successful stream, so it rejects here, while the caller
120
+ * can still choose the HTTP status.
118
121
  */
119
- function sseToByteStream(sse) {
122
+ async function sseToByteStream(sse) {
120
123
  const encoder = new TextEncoder();
124
+ const enqueue = (controller, chunk) => {
125
+ controller.enqueue(chunk instanceof Uint8Array ? chunk : encoder.encode(chunk));
126
+ };
127
+ const chunks = streamFile(sse);
128
+ const first = await chunks.next();
121
129
  return new ReadableStream({ async start(controller) {
122
130
  try {
123
- for await (const chunk of streamFile(sse)) controller.enqueue(chunk instanceof Uint8Array ? chunk : encoder.encode(chunk));
131
+ if (!first.done) {
132
+ enqueue(controller, first.value);
133
+ for await (const chunk of chunks) enqueue(controller, chunk);
134
+ }
124
135
  controller.close();
125
136
  } catch (err) {
126
137
  controller.error(err);
@@ -2056,14 +2067,16 @@ function createBridgeApp(config) {
2056
2067
  executor = await sandbox.getSession(sessionId);
2057
2068
  }
2058
2069
  try {
2059
- const stream = await executor.readFileStream(resolvedPath);
2060
- return new Response(sseToByteStream(stream), {
2070
+ const byteStream = await sseToByteStream(await executor.readFileStream(resolvedPath));
2071
+ return new Response(byteStream, {
2061
2072
  status: 200,
2062
2073
  headers: { "Content-Type": "application/octet-stream" }
2063
2074
  });
2064
2075
  } catch (err) {
2065
- if (err.code === "FILE_NOT_FOUND") return errorJson(`File not found: ${resolvedPath}`, "workspace_read_not_found", 404);
2066
- return errorJson(`read failed: ${err instanceof Error ? err.message : String(err)}`, "exec_transport_error", 502);
2076
+ const code = err.code;
2077
+ const msg = err instanceof Error ? err.message : String(err);
2078
+ if (code === "FILE_NOT_FOUND" || msg.startsWith("File streaming error: File not found: ")) return errorJson(`File not found: ${resolvedPath}`, "workspace_read_not_found", 404);
2079
+ return errorJson(`read failed: ${msg}`, "exec_transport_error", 502);
2067
2080
  }
2068
2081
  }));
2069
2082
  app.put(`${apiPrefix}/sandbox/:id/file/*`, traced("write_file", async (c) => {
@@ -2185,7 +2198,7 @@ function createBridgeApp(config) {
2185
2198
  if (result.exitCode !== 0) return errorJson(`tar failed (exit ${result.exitCode}): ${result.stderr}`, "workspace_archive_read_error", 502);
2186
2199
  const stream = await sandbox.readFileStream(tmpPath);
2187
2200
  sandbox.exec(`rm -f ${shellQuote(tmpPath)}`).catch(() => {});
2188
- return new Response(sseToByteStream(stream), {
2201
+ return new Response(await sseToByteStream(stream), {
2189
2202
  status: 200,
2190
2203
  headers: { "Content-Type": "application/octet-stream" }
2191
2204
  });