Skip to content

Releases: baronunread/sproutboat-cli

v0.11.10

Choose a tag to compare

@baronunread baronunread released this 18 Sep 10:02

[0.11.10] - 2026-09-18

Changed

  • Bumped @sproutboat/toolchain to 0.4.10, pinning Porffor to the
    alpha-7 tag. All local patches (render.js, uwebsockets.js) verified
    against the real alpha-7 source — no anchor drift, kitchen-sink
    conformance and the full test suite pass.

v0.11.9

Choose a tag to compare

@baronunread baronunread released this 17 Sep 14:59

[0.11.9] - 2026-09-17

Fixed

  • Bumped @sproutboat/toolchain to ^0.4.9 and @sproutboat/runtime to
    ^0.9.6. 0.11.6's toolchain pin (^0.4.5 since 0.4.2) predated a
    native R2 size-gating feature that shipped in 0.4.6/0.4.7 with a
    misplaced #endif in the generated C++ guard, breaking every R2-free
    native build with a compiler error. The fix landed as 0.4.8, but that
    version got stuck permanently conflicted on npm after an interrupted
    trusted-publish attempt, so 0.4.9/0.9.6 revert the feature outright
    instead: R2 transfer support is native code again, always compiled in
    with 404 stubs where unused, same as before it existed. It saved ~11KB
    of __text (~1.3% of a typical binary) and didn't even change the
    shipped file size on macOS, since __TEXT is page-aligned there — not
    worth the guard-placement bug class it introduced.

v0.11.5

Choose a tag to compare

@baronunread baronunread released this 14 Sep 12:57

[0.11.5] - 2026-09-14

Fixed

  • Bumped @sproutboat/runtime to ^0.6.6. __sbEntry called handlers.fetch()
    (and the scheduled/queue/alarm trigger paths) with no try/catch at all:
    any synchronous throw, or a rejected async handler promise, propagated all
    the way up and crashed the whole process: not just the request that hit
    it, but every other in-flight and future request on that sprout, until
    whatever supervises it restarted the binary (baronunread/sproutboat#179). A
    throwing/rejecting fetch() now returns a 500 instead. scheduled()/
    alarm() still reply 204 either way (no failure signal existed before
    this either); a throwing queue() falls through to the existing
    default-ack pass, same as a handler that never calls ack()/retry() on
    every message.
  • Upgrading the CLI is the only way to pick this up. The runtime prelude is
    embedded in the CLI binary when that binary is built, so a project's own
    @sproutboat/runtime dependency has no effect on --standalone or
    --target host builds.

v0.11.4

Choose a tag to compare

@baronunread baronunread released this 14 Sep 12:15

[0.11.4] - 2026-09-14

Fixed

  • Bumped @sproutboat/runtime to ^0.6.5, undoing a severe performance
    regression that v0.11.3 shipped (baronunread/sproutboat#181). 0.6.4's
    encode fix was correct for the large inputs it targeted and badly wrong for
    small ones: an app hashing ~150-byte values through crypto.subtle.digest
    on every request lost 64% of its throughput (4,700 to 1,680 req/s),
    tripled its p50 (4.6ms to 13.4ms) and grew 41% in RSS. The encoder now only
    reaches for an array once 512 bytes have accumulated, so a small input runs
    exactly like v0.11.2 while a large one keeps the #180 fix.
  • Upgrading the CLI is the only way to pick this up. The runtime prelude is
    embedded in the CLI binary when that binary is built, so a project's own
    @sproutboat/runtime dependency has no effect on --standalone or
    --target host builds. Anyone on v0.11.3 should move to this release;
    v0.11.2 is the last unaffected version before it.

Changed

  • The darwin-x64 platform package is now cross-compiled on the arm64 macOS
    runner instead of built natively on macos-13. GitHub's only x64 macOS pool
    routinely left that leg queued for 30+ minutes and blocked two releases,
    including v0.11.3, where the root package published while its own
    optionalDependency on @sproutboat/cli-darwin-x64 was still waiting on it.
    Nothing in the package needs x64 hardware to produce. scripts/build-cli.ts
    gains --target <os>-<arch> for this.

v0.11.3

Choose a tag to compare

@baronunread baronunread released this 14 Sep 12:15

[0.11.3] - 2026-09-14

Fixed

  • Bumped @sproutboat/runtime to ^0.6.4, picking up the encode-side half of
    the same O(n^2) string-building bug v0.11.2 fixed on the decode side.
    __sbToBytes sits behind every crypto.subtle call a handler makes:
    digest, importKey, sign, verify and the PBKDF2/scrypt path all funnel
    their data, keys, salts and signatures through it, so hashing a request body
    was quadratic in the body's size. __sbHexOrBytes, which decodes the
    expected side of an HMAC verify, was worse still; it rewrote its
    accumulator's last character on every odd nibble, copying the whole string
    twice per byte (baronunread/sproutboat#180).

Superseded by v0.11.4. Do not use this release. The runtime bump it carries regressed small-input encoding badly: an app hashing ~150-byte values through crypto.subtle on every request lost 64% of its throughput, tripled its p50 and grew 41% in RSS (baronunread/sproutboat#181). Upgrade to v0.11.4, or stay on v0.11.2.

v0.10.2

Choose a tag to compare

@baronunread baronunread released this 10 Sep 23:55

[0.10.2] - 2026-09-11

Fixed

  • The one-time Zig download no longer depends on ziglang.org being reachable.
    It now pulls from a random Zig community mirror, with ziglang.org kept only
    as a last resort, and tries the mirrors in turn on failure. ziglang.org
    rate-limits the multi-megabyte tarballs and had been timing out mid-build,
    with no fallback (baronunread/sproutboat#166). The archive sha256 is still
    enforced whatever the source. SPROUTBOAT_ZIG_URL overrides the source;
    SPROUTBOAT_ZIG still points at a prebuilt binary.
  • A leftover ~/.cache/sproutboat/zig-<version>/ directory from an earlier
    cache layout is removed on the next build instead of sitting unused.

v0.10.1

Choose a tag to compare

@baronunread baronunread released this 10 Sep 22:57

[0.10.1] - 2026-09-11

Fixed

  • npm install sproutboat / bunx sproutboat work again. 0.10.0's launcher
    hard-required per-platform @sproutboat/cli-* packages that are not published
    yet, so it always failed with "optional package … is missing". The launcher
    now falls back to running src/main.ts with Bun when no platform binary is
    installed (the v0.9.0 model), and optionalDependencies are dropped until the
    binaries actually ship. 0.10.0 is deprecated on npm.
  • sproutboat --version and the deploy banner report the real version instead
    of a hard-coded 0.9.0 when run from the npm package rather than a
    bun build --compile binary.
  • sproutboat build / deploy locate esbuild via the installed dependency,
    not just process.execPath's directory, so bundling works on the Bun
    fallback path.
  • The launcher no longer hangs when it receives SIGINT / SIGTERM while
    forwarding a signal to a running platform binary.

v0.10.0

Choose a tag to compare

@baronunread baronunread released this 10 Sep 22:19

[0.10.0] - 2026-09-11

Added

  • Per-platform native CLI: npm install sproutboat pulls a prebuilt binary for
    the host (@sproutboat/cli-{darwin,linux}-{arm64,x64}) behind a thin launcher
    that preserves signals and embeds its own version, so the CLI runs without Bun
    on PATH.
  • Porffor is fetched on the first build, not installed as a dependency: the
    pinned commit is downloaded into ~/.cache/sproutboat, SHA-256 verified, and
    patched there. Installation clones nothing and runs no Git, Make or compiler.
  • request.cf.clientIp in standalone builds, from a server-set
    x-sb-remote-addr, with SB_TRUSTED_PROXIES for X-Forwarded-For resolution
    behind a reverse proxy (baronunread/sproutboat#163).
  • env.<D1>.backup(name?) — an online, integrity-checked single-file snapshot
    of a D1 database via VACUUM INTO, on both the embedded and broker transports
    (baronunread/sproutboat#164).
  • Rate Limiting binding: ratelimiters: [{ binding, limit, period }] in
    sproutboat.jsonc gives env.<NAME>.limit({ key }) -> { success }, a
    fixed-window counter on both transports (baronunread/sproutboat#69).
  • crypto.subtle subset: digest (SHA-256/384/512) and HMAC
    importKey / sign / verify, backed by reference SHA-2 as inline C so it
    works on both transports (baronunread/sproutboat#133). No ECDSA or AES yet.
  • crypto.scryptVerify(password, salt, expected, { N, r, p }) — a verify-only
    scrypt (RFC 7914) for migrating password hashes made by Node/Bun scrypt
    (baronunread/sproutboat#153). Not a blessed KDF for new credentials.
  • x-sb-cpu-ms: per-invocation CPU time, carried on the handler response.

Fixed

  • 303 (and every other status not in Porffor's table) no longer resets the
    connection on standalone builds (baronunread/sproutboat#156).
  • Handler console.log / console.error reach stderr, unbuffered, in
    standalone builds instead of vanishing (baronunread/sproutboat#165).
  • sproutboat dev no longer leaves the previous sprout running across a
    rebuild, delivers no triggers to a candidate that failed to start, and cleans
    up every failed setup path on rebuild and on shutdown.
  • The Zig toolchain cache is hardened against a partial or concurrent download.

Changed

  • Porffor pin bumped alpha-4 → alpha-5 (1f4ae4ae); the same UWS_COMMIT,
    so no uWebSockets re-vendor.
  • Config parsing, the artifact manifest, the binding broker, the wire assets and
    the whole native-fetch runtime (prelude + transports) now come from published
    @sproutboat/* packages; the CLI keeps thin re-export shims. The Porffor pin
    and its source patches moved to @sproutboat/toolchain.
  • Broker cron / queue / alarm delivery is gated by a local signal, so only a
    promoted candidate runs timers.

Performance

  • Embedded transport caches prepared statements per database (FIFO, 32/db)
    instead of recompiling the SQL on every binding op — the op boundary drops
    from ~0.6 ms to tens of µs (baronunread/sproutboat#155).
  • The broker's due-queue and due-alarm polls are indexed.

Docs

  • docs/standalone.md documents the single-threaded execution model and the
    SO_REUSEPORT multi-process recipe for scaling past one core
    (baronunread/sproutboat#154); the embedded transport also sets
    PRAGMA busy_timeout so shared-data-dir writers wait instead of failing.
    Also: the client-IP behaviour and backing up D1.

v0.8.0

Choose a tag to compare

@baronunread baronunread released this 06 Sep 23:40

[0.8.0] — 2026-09-07

Added

  • sproutboat build --standalone: one executable that carries its own bindings.
    SQLite is compiled into the sprout, so a ~2 MB file serves KV, D1, R2, queues,
    Durable Objects, alarms, analytics and assets with nothing beside it on disk —
    no Bun, no broker, no control plane. State lives in <name>.data/store.sqlite
    plus d1/<binding>.sqlite, the same layout sproutboat dev writes, which is
    what lets one conformance suite hold both backends to the same behaviour.
    Secrets come from the environment (then <data>/secrets.json) and a missing
    one refuses the boot, listing every name at once, rather than throwing on the
    first request that needs it. Cron ticks, queue batches and DO alarms run on
    in-process timers; assets are baked in, capped at 8 MB.
  • Outbound TLS from a standalone binary. fetch("https://…") verifies against
    curl's Mozilla-derived root set via BearSSL, linked in beside SQLite
    (1.86 → 2.02 MB). SB_CA_BUNDLE adds a private or corporate CA to that set;
    it only ever adds trust, and nothing disables verification.
  • Durable Object alarms: setAlarm / getAlarm / deleteAlarm and the
    alarm() handler. At most one alarm is pending per object and a later
    setAlarm replaces it, matching Workers. Delivery claims before running, so
    an alarm() that schedules its own next run is not erased by the delivery
    that invoked it.
  • Service bindings: env.<BINDING>.fetch() reaches another deployment on the
    same node through the edge on loopback. This is the CLI half; a binding that
    resolves to nothing reports the target as undeployed rather than failing as a
    502.
  • Binary values in the binding frame. An R2 object body now travels beside the
    JSON rather than encoded inside it.
  • SB_FETCH_MAX_BYTES (32 MiB default) caps an outbound response body. An
    unbounded upstream could previously drive a sprout's memory to whatever it
    chose to send.

Fixed

  • d1.exec ran only the first statement of a multi-statement script, so a
    schema built in one exec() call silently created only its first table.
  • r2.get / head / put / list returned flat fields where the shim reads
    r.object, and ae.query omitted the count its caller reads.
  • Trigger authentication accepted any caller when no token was configured.
    The hole predates this release; a standalone binary listening on a public
    interface is what made it reachable.
  • Binary R2 values were corrupted in transit — 0x08 and 0x0c arrived as
    b and f.
  • A retried binding call could apply a write twice. Every request now carries an
    id and the broker replays the cached reply for a repeat of a mutating op
    instead of performing it again.

Changed

  • A dropped broker connection is retried four times with 0/5/25/100 ms backoff
    instead of failing after one attempt, which covers a broker restart mid-call.
  • The bundled (Bun) standalone backend is gone. The embedded one passes the same
    suite at 1.9 MB against 63 MB, and TLS removed its last real advantage.
  • A native-fetch binary cannot see argv — Porffor's runtime init calls
    porf_init(0, NULL) — so a standalone binary is configured through PORT,
    SB_DATA_DIR / SPROUTBOAT_DATA and the environment only, never flags.

Performance

  • An 8 MB R2 put through the broker went from 255 MB to 149 MB peak RSS.
  • The broker's frame reader no longer re-concatenates its buffer per chunk.

v0.6.0

Choose a tag to compare

@baronunread baronunread released this 04 Sep 23:54

Added

  • bindings.json now carries vars — the baked plain values a sprout was
    built with — so the control plane can show what a version was compiled
    against. They ride along for display only; the broker never serves them,
    they are compiled into the sprout itself. A project whose only binding
    config is vars now gets a sidecar written at all, where before it got
    none.

Changed

  • Tooling only, no change to how the CLI behaves: the tree is now formatted
    with oxfmt 0.66.0 (the config landed in 0.5.0 but was never run over the
    tree), markdown is excluded from formatting, a lefthook pre-commit hook
    formats staged files and lints, the last 10 oxlint warnings are cleared,
    and CI gates lint alongside typecheck and test.
  • README rewritten shorter: a logo lockup that survives both GitHub themes
    (docs/logo-light.svg / docs/logo-dark.svg behind a <picture>), the
    everyday commands as a five-row table, and the full command inventory left
    to the generated SURFACE.md instead of duplicated by hand.