sproutboat 0.7.0 → 0.9.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/CHANGELOG.md ADDED
@@ -0,0 +1,304 @@
1
+ # Changelog
2
+
3
+ All notable changes to `sproutboat` (the CLI) are documented here. Format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
5
+ follows [SemVer](https://semver.org/), pre-1.0 so a minor bump can still carry
6
+ a breaking change.
7
+
8
+ Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
9
+ maintained going forward by the `release` skill.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.9.0] - 2026-09-07
14
+ ### Added
15
+ - KV content management: `kv key get`, `put`, `delete`, and `list`; bounded
16
+ `kv bulk get`, `put`, and `delete`; and cursor-paginated `kv export` files
17
+ that can be restored with `kv bulk put`.
18
+ - CLI integration coverage for pagination, bounded batches, text and file
19
+ output, overwrite refusal, and cleanup after failed exports.
20
+ - `sproutboat-env.d.ts` generation from project bindings.
21
+ - Focused runnable examples for every supported binding.
22
+
23
+ ### Fixed
24
+ - Static assets keep arbitrary binary bytes when they travel through the broker.
25
+ - KV exports use a permission-restricted temporary file and move into place only
26
+ after a complete export. Failed exports leave neither partial output nor a
27
+ temporary file behind.
28
+ - First-host builds no longer require Git or Make.
29
+
30
+ ### Changed
31
+ - Development binaries compile with Porffor `-O0` for faster iteration.
32
+ - The installation docs state that the CLI requires Bun and should be invoked
33
+ with `bunx` rather than `npx`.
34
+
35
+ ## [0.8.0] — 2026-09-07
36
+ ### Added
37
+ - `sproutboat build --standalone`: one executable that carries its own bindings.
38
+ SQLite is compiled into the sprout, so a ~2 MB file serves KV, D1, R2, queues,
39
+ Durable Objects, alarms, analytics and assets with nothing beside it on disk —
40
+ no Bun, no broker, no control plane. State lives in `<name>.data/store.sqlite`
41
+ plus `d1/<binding>.sqlite`, the same layout `sproutboat dev` writes, which is
42
+ what lets one conformance suite hold both backends to the same behaviour.
43
+ Secrets come from the environment (then `<data>/secrets.json`) and a missing
44
+ one refuses the boot, listing every name at once, rather than throwing on the
45
+ first request that needs it. Cron ticks, queue batches and DO alarms run on
46
+ in-process timers; assets are baked in, capped at 8 MB.
47
+ - Outbound TLS from a standalone binary. `fetch("https://…")` verifies against
48
+ curl's Mozilla-derived root set via BearSSL, linked in beside SQLite
49
+ (1.86 → 2.02 MB). `SB_CA_BUNDLE` adds a private or corporate CA to that set;
50
+ it only ever adds trust, and nothing disables verification.
51
+ - Durable Object alarms: `setAlarm` / `getAlarm` / `deleteAlarm` and the
52
+ `alarm()` handler. At most one alarm is pending per object and a later
53
+ `setAlarm` replaces it, matching Workers. Delivery claims before running, so
54
+ an `alarm()` that schedules its own next run is not erased by the delivery
55
+ that invoked it.
56
+ - Service bindings: `env.<BINDING>.fetch()` reaches another deployment on the
57
+ same node through the edge on loopback. This is the CLI half; a binding that
58
+ resolves to nothing reports the target as undeployed rather than failing as a
59
+ 502.
60
+ - Binary values in the binding frame. An R2 object body now travels beside the
61
+ JSON rather than encoded inside it.
62
+ - `SB_FETCH_MAX_BYTES` (32 MiB default) caps an outbound response body. An
63
+ unbounded upstream could previously drive a sprout's memory to whatever it
64
+ chose to send.
65
+
66
+ ### Fixed
67
+ - `d1.exec` ran only the first statement of a multi-statement script, so a
68
+ schema built in one `exec()` call silently created only its first table.
69
+ - `r2.get` / `head` / `put` / `list` returned flat fields where the shim reads
70
+ `r.object`, and `ae.query` omitted the `count` its caller reads.
71
+ - Trigger authentication accepted *any* caller when no token was configured.
72
+ The hole predates this release; a standalone binary listening on a public
73
+ interface is what made it reachable.
74
+ - Binary R2 values were corrupted in transit — `0x08` and `0x0c` arrived as
75
+ `b` and `f`.
76
+ - A retried binding call could apply a write twice. Every request now carries an
77
+ id and the broker replays the cached reply for a repeat of a mutating op
78
+ instead of performing it again.
79
+
80
+ ### Changed
81
+ - A dropped broker connection is retried four times with 0/5/25/100 ms backoff
82
+ instead of failing after one attempt, which covers a broker restart mid-call.
83
+ - The bundled (Bun) standalone backend is gone. The embedded one passes the same
84
+ suite at 1.9 MB against 63 MB, and TLS removed its last real advantage.
85
+ - A native-fetch binary cannot see `argv` — Porffor's runtime init calls
86
+ `porf_init(0, NULL)` — so a standalone binary is configured through `PORT`,
87
+ `SB_DATA_DIR` / `SPROUTBOAT_DATA` and the environment only, never flags.
88
+
89
+ ### Performance
90
+ - An 8 MB R2 put through the broker went from 255 MB to 149 MB peak RSS.
91
+ - The broker's frame reader no longer re-concatenates its buffer per chunk.
92
+
93
+ ## [0.7.0] — 2026-09-06
94
+ ### Added
95
+ - `compatibility_date` now reaches the artifact instead of being validated and
96
+ dropped. It is recorded in `manifest.json` as `compatibilityDate` and baked
97
+ into the binary as `__sbCompat`, so a future runtime change can be gated on
98
+ `__sbCompat >= "YYYY-MM-DD"` and old binaries keep the semantics they were
99
+ compiled with. The manifest field is optional and `schemaVersion` stays at 2,
100
+ so artifacts built before this release remain deployable and rollback keeps
101
+ working.
102
+ - A version-skew warning. Control planes advertise `x-sproutboat-control` and
103
+ `x-sproutboat-min-cli` on every `/api/` response; when this CLI is below the
104
+ advertised minimum it says so once per run, instead of leaving the user with
105
+ an unexplained 400. A control plane that predates the handshake sends no
106
+ headers and nothing changes.
107
+ - `CONTRACTS.md`, generated from source in the same style as `SURFACE.md`: the
108
+ broker ops, storage tables, manifest fields and config keys that a release may
109
+ not break, plus golden fixtures for the manifest and the storage format so a
110
+ regression fails a test rather than a user's deployment.
111
+
112
+ ## [0.6.1] — 2026-09-06
113
+ ### Fixed
114
+ - `sproutboat queues` help (and the generated `SURFACE.md`) claimed queue
115
+ consumers "are not implemented yet". They are implemented, and they run on
116
+ the deployed path as well as under `dev`: the supervisor passes
117
+ `--sprout-url` to the broker, which delivers messages in batches, retries a
118
+ failed or explicitly-retried message after 5s, and stops delivering it after
119
+ 5 attempts. The summary now describes what actually happens.
120
+
121
+ ### Changed
122
+ - Tooling only, no change to how the CLI behaves: oxfmt is scoped to the JS
123
+ family with the pre-commit hook's glob matched to it, TypeScript moves
124
+ 5.9 → 7.0, and the GitHub Actions group is bumped.
125
+
126
+ ## [0.6.0] — 2026-09-05
127
+ ### Added
128
+ - `bindings.json` now carries `vars` — the baked plain values a sprout was
129
+ built with — so the control plane can show what a version was compiled
130
+ against. They ride along for display only; the broker never serves them,
131
+ they are compiled into the sprout itself. A project whose only binding
132
+ config is `vars` now gets a sidecar written at all, where before it got
133
+ none.
134
+
135
+ ### Changed
136
+ - Tooling only, no change to how the CLI behaves: the tree is now formatted
137
+ with oxfmt 0.66.0 (the config landed in 0.5.0 but was never run over the
138
+ tree), markdown is excluded from formatting, a lefthook pre-commit hook
139
+ formats staged files and lints, the last 10 oxlint warnings are cleared,
140
+ and CI gates lint alongside typecheck and test.
141
+ - README rewritten shorter: a logo lockup that survives both GitHub themes
142
+ (`docs/logo-light.svg` / `docs/logo-dark.svg` behind a `<picture>`), the
143
+ everyday commands as a five-row table, and the full command inventory left
144
+ to the generated `SURFACE.md` instead of duplicated by hand.
145
+
146
+ ## [0.5.0] — 2026-09-03
147
+ ### Added
148
+ - `dev [--port <n>] [--no-watch]` — run the project on this machine against a
149
+ real broker (KV/D1/secrets/etc. all work), rebuilding on save.
150
+ - `build --target host` — compile for the machine doing the build instead of
151
+ cross-compiling for a box; what `dev` uses, and runnable standalone.
152
+ - Handlers may now `import` — relative modules across the project, and npm
153
+ packages from the project's own `node_modules`. The entry point is bundled
154
+ before it reaches Porffor; the capability checks run against that bundled
155
+ output, so a dependency can't reach `process`/`Bun`/`node:*` any more than
156
+ hand-written code can.
157
+ - `sproutboat init` scaffolds a `.gitignore` (`.sproutboat/`, `.dev.vars`,
158
+ `node_modules/`) alongside the project files, unless one already exists.
159
+
160
+ ### Fixed
161
+ - Async `fetch` handlers hung indefinitely — `__sbEntry` chained the #28
162
+ CPU-time tag onto the handler's own promise, and Porffor's native-fetch
163
+ server only resolves a promise a handler returns directly, never one
164
+ derived from `.then()`.
165
+ - `new Proxy(...)` compiles under Porffor alpha-4 and then silently ignores
166
+ every trap — a trapped property just reads back `undefined`. `check` now
167
+ rejects it before that reaches a deploy as an unexplained 502.
168
+ - `sproutboat init` crashed with a raw `EEXIST` stack trace, and could leave
169
+ a half-scaffolded project, if `src/index.js` already existed but
170
+ `sproutboat.jsonc` didn't. Both targets are checked before either is
171
+ written.
172
+ - Re-running `sproutboat build` (or `dev`'s rebuild-on-save) could fail to
173
+ link: the artifact directory is content-addressed, so an unchanged rebuild
174
+ targeted the previous binary, which was `chmod 0555` and possibly still
175
+ running.
176
+ - The broker's local dev state directory was never created before opening
177
+ its SQLite file, so a first `sproutboat dev` run failed outright.
178
+
179
+ ### Changed
180
+ - Lint: adopted the anti-slop Oxlint plugin and migrated the tree onto it —
181
+ no more bare `unknown`/`Record<string, unknown>` at binding boundaries,
182
+ every non-const type assertion carries a `SAFETY:` comment.
183
+
184
+ ## [0.4.11] — 2026-09-02
185
+ ### Changed
186
+ - README rewritten for current commands and config; points at the docs site.
187
+
188
+ ## [0.4.10] — 2026-09-02
189
+ ### Changed
190
+ - `domains`: prints the A record to add, and any DNS reachability warning.
191
+
192
+ ## [0.4.9] — 2026-09-02
193
+ ### Changed
194
+ - `deploy` (#80): dropped the client-side dedup check in favour of trusting
195
+ the server's own no-op response.
196
+
197
+ ## [0.4.8] — 2026-09-02
198
+ ### Fixed
199
+ - `deploy` (#80): no longer skipped the upload when only assets or bindings
200
+ had changed but the sprout binary hadn't.
201
+
202
+ ## [0.4.7] — 2026-09-02
203
+ ### Added
204
+ - `deploy` auto-provisions an id-less storage binding (wrangler-style):
205
+ creates the account-level resource, writes its id back into
206
+ `sproutboat.jsonc`.
207
+
208
+ ## [0.4.6] — 2026-09-02
209
+ ### Added
210
+ - `sproutboat resource` — manage account-level storage resources directly.
211
+ - `sproutboat.jsonc` storage bindings accept `{ binding, id }`, not just a
212
+ bare name.
213
+ ### Changed
214
+ - Broker keys KV/R2/queue/D1 stores by resource id when one is bound, so the
215
+ data survives a redeploy and can be shared across projects.
216
+
217
+ ## [0.4.5] — 2026-09-02
218
+ ### Changed
219
+ - `deploy` dropped the "✓ serving" line — silence now means the health check
220
+ passed.
221
+
222
+ ## [0.4.4] — 2026-09-02
223
+ ### Added
224
+ - `--version`, a once-a-day update-available notice, a richer deploy echo.
225
+ ### Changed
226
+ - Misuse now exits `2` (getopt convention) instead of `1`.
227
+
228
+ ## [0.4.3] — 2026-09-02
229
+ ### Changed
230
+ - `--help` output: grouped, emoji-labelled, aligned — was one wall-of-text
231
+ usage line.
232
+
233
+ ## [0.4.2] — 2026-09-02
234
+ ### Added
235
+ - `tail --sprout` streams the running sprout's and broker's stdout/stderr.
236
+ ### Changed
237
+ - `deploy` waits for the health check and prints every binding in the
238
+ report; `delete` takes flexible args plus `?confirm`; the banner reads the
239
+ real installed version.
240
+
241
+ ## [0.4.0] — 2026-09-01
242
+ ### Added
243
+ - `sproutboat domains` and `sproutboat secrets` commands.
244
+ - `deploy` uploads the `bindings.json` / `assets.json` sidecars alongside
245
+ the sprout binary.
246
+ - The worker self-reports per-invocation CPU time (`x-sb-cpu-ms`).
247
+ - Deploy surfaces a Porffor pin drift warning when the live version was
248
+ built against a different compiler pin than the one about to deploy.
249
+ ### Changed
250
+ - Renamed "worker" to "sprout" throughout the CLI, broker, and examples.
251
+
252
+ ## [0.3.0] — 2026-09-01
253
+ ### Added
254
+ - Published to npm via Trusted Publishing (OIDC) — no token secret in CI.
255
+ ### Changed
256
+ - `src/wrap.ts` extracted with `runtime/*` subpath exports, so the monorepo
257
+ can consume the binding/manifest contracts as a dependency instead of a
258
+ hand-vendored copy.
259
+ - CSPRNG-backed `crypto.getRandomValues`; the deploy binary is stripped.
260
+ - One long-lived broker connection per worker instead of reconnecting on
261
+ every binding call; `env.<SECRET>` memoized; `assets.get` made synchronous
262
+ with the broker service; WAL + `synchronous=NORMAL` and parameterised
263
+ `LIMIT` back-ported from the monorepo's broker.
264
+
265
+ ## [0.2.1] — 2026-08-31
266
+ ### Changed
267
+ - Ships a prebuilt uWebSockets archive, so the first build needs neither
268
+ `git` nor `make` on `PATH`.
269
+
270
+ ## [0.2.0] — 2026-08-31
271
+ Initial release, extracted from the `sproutboat` monorepo (`apps/cli`) as
272
+ its own package.
273
+ ### Added
274
+ - Cross-compiles a handler to a static `linux-x86_64` binary with Porffor +
275
+ Zig — no Docker.
276
+ - Static assets binding, with the `examples/kitchen-sink` Astro app as a
277
+ worked example.
278
+ - `SURFACE.md`, generated and drift-checked against the actual command/env
279
+ surface.
280
+ ### Changed
281
+ - Patches Porffor at build time rather than via a `postinstall` hook.
282
+ - Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
283
+ `sprout` bin alias in favour of a user-defined shell alias.
284
+
285
+ [Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.8.0...HEAD
286
+ [0.8.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.7.0...v0.8.0
287
+ [0.7.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.1...v0.7.0
288
+ [0.6.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.0...v0.6.1
289
+ [0.6.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.5.0...v0.6.0
290
+ [0.5.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.11...v0.5.0
291
+ [0.4.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.10...v0.4.11
292
+ [0.4.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.9...v0.4.10
293
+ [0.4.9]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.8...v0.4.9
294
+ [0.4.8]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.7...v0.4.8
295
+ [0.4.7]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.6...v0.4.7
296
+ [0.4.6]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.5...v0.4.6
297
+ [0.4.5]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.4...v0.4.5
298
+ [0.4.4]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.3...v0.4.4
299
+ [0.4.3]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.2...v0.4.3
300
+ [0.4.2]: https://github.com/baronunread/sproutboat-cli/compare/v0.4.0...v0.4.2
301
+ [0.4.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.3.0...v0.4.0
302
+ [0.3.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.2.1...v0.3.0
303
+ [0.2.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.2.0...v0.2.1
304
+ [0.2.0]: https://github.com/baronunread/sproutboat-cli/releases/tag/v0.2.0
package/README.md CHANGED
@@ -19,6 +19,12 @@ for agents: [sproutboat.com/llms.txt](https://sproutboat.com/llms.txt)).
19
19
 
20
20
  ## Using
21
21
 
22
+ Sproutboat is a Bun tool. It needs **Bun 1.4+** on the machine you build from,
23
+ and `bunx` rather than `npx`: the CLI runs on Bun's APIs, and `npm install`
24
+ cannot resolve it today ([#134](https://github.com/baronunread/sproutboat/issues/134)).
25
+ Nothing is needed on the machine that *runs* a sprout: that gets a static
26
+ binary.
27
+
22
28
  ```sh
23
29
  bunx sproutboat init hello
24
30
  cd hello
@@ -32,7 +38,7 @@ bunx sproutboat login --api-url https://control.example.com # one browser appr
32
38
  bunx sproutboat deploy
33
39
  ```
34
40
 
35
- Or install it once and drop the `bunx`:
41
+ Or install it once with Bun and drop the `bunx`:
36
42
 
37
43
  ```sh
38
44
  bun add -g sproutboat # then: sproutboat deploy, sproutboat tail, ...
@@ -63,6 +69,32 @@ Run `sproutboat` with no arguments for the grouped list.
63
69
  [`SURFACE.md`](SURFACE.md) is the generated inventory: every command, every
64
70
  argument, every env var, kept honest by a drift test.
65
71
 
72
+ ### KV data and waitlist export
73
+
74
+ Manage one namespace by its account-level name:
75
+
76
+ ```sh
77
+ sproutboat kv key list registrations --prefix email:
78
+ sproutboat kv key get registrations email:person@example.com --text
79
+ sproutboat kv key put registrations email:person@example.com joined
80
+ sproutboat kv key delete registrations email:person@example.com --yes
81
+ sproutboat kv export registrations --prefix email: --output registrations.json
82
+ ```
83
+
84
+ Bulk put, and therefore exported dumps, use the version 1 text-value format:
85
+
86
+ ```json
87
+ [
88
+ { "key": "email:person@example.com", "value": "joined" }
89
+ ]
90
+ ```
91
+
92
+ `kv bulk get` and `kv bulk delete` accept a JSON array of key strings. `kv bulk
93
+ put` accepts the entry array above. The CLI sends large inputs in bounded
94
+ batches. Export pages through the namespace and writes incrementally to a
95
+ permission-restricted temporary file, then renames it into place only after a
96
+ complete export. Existing output files require `--force`.
97
+
66
98
  ## Config
67
99
 
68
100
  `sproutboat.jsonc`: the entry point plus Cloudflare-shaped `env.*` bindings.
package/SURFACE.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
4
4
  > toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
5
5
 
6
- **Package:** `sproutboat` 0.7.0 · runs on Bun (use `bunx`, not `npx`)
6
+ **Package:** `sproutboat` 0.9.0 · runs on Bun (use `bunx`, not `npx`)
7
7
 
8
8
  ## Commands
9
9
 
@@ -11,13 +11,14 @@
11
11
  | --- | --- | --- |
12
12
  | `init` | `[name]` | Scaffold sproutboat.jsonc + src/index.js in ./<name>. |
13
13
  | `check` | `[project-dir]` | Validate the config and entry point without building. |
14
+ | `types` | `[project-dir]` | Write sproutboat-env.d.ts from the config, so an editor knows what `env` holds. `dev` and `deploy` refresh it when the config is newer. |
14
15
  | `dev` | `[project-dir] [--port <n>] [--no-watch]` | Run the project on this machine against a real broker, rebuilding on save. |
15
- | `build` | `[project-dir] [--target host]` | Cross-compile the native-fetch sprout (Porffor + Zig). `--target host` builds for this machine instead, to run locally — not deployable. |
16
+ | `build` | `[project-dir] [--target host] [--standalone]` | Cross-compile the native-fetch sprout (Porffor + Zig). `--target host` builds for this machine instead, to run locally — not deployable. `--standalone` emits one executable carrying its own bindings, with SQLite compiled in (~2 MB) and no broker process. |
16
17
  | `deploy` | `[project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision]` | Build (unless --artifact), auto-provision id-less storage bindings and pin their ids into sproutboat.jsonc, print the report, upload, wait until the URL serves. The control plane skips an upload that matches the live artifact byte-for-byte. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores. |
17
18
  | `versions` | `<list | view <version-id>> [project-dir]` | List the project's deployed versions, or show one version's artifact and bindings. |
18
19
  | `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
19
20
  | `tail` | `[project-dir] [--sprout]` | Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead. |
20
- | `kv` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | KV namespaces. `create` prints the id to bind from sproutboat.jsonc. |
21
+ | `kv` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>> | <key | bulk | export> ...` | KV namespaces. `create` prints the id to bind from sproutboat.jsonc; key and bulk operations manage contents, and export writes a restorable JSON dump. |
21
22
  | `d1` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | D1 databases. `create` prints the id to bind from sproutboat.jsonc. |
22
23
  | `r2` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | R2 buckets. `create` prints the id to bind from sproutboat.jsonc. |
23
24
  | `queues` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | Queues. `create` prints the id to bind from sproutboat.jsonc; consumers deliver in batches with retries, and stop after 5 attempts. |
@@ -29,7 +30,7 @@
29
30
  | `whoami` | — | Show the active endpoint and the account the stored token belongs to. |
30
31
 
31
32
  ```
32
- usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions <list | view <version-id>> [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--sprout] | kv <list | create <name> | info <name> | rename <name> <new> | delete <name>> | d1 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | r2 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | queues <list | create <name> | info <name> | rename <name> <new> | delete <name>> | domains <list | add <host> | verify <host> | delete <host>> [project-dir] | secrets <list | put <NAME> [--value <value>] | delete <NAME>> [project-dir] | delete [project-dir] [--name <project>] --yes | login [--api-url <url>] [--token <token>] | logout [--api-url <url>] | whoami>
33
+ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev [project-dir] [--port <n>] [--no-watch] | build [project-dir] [--target host] [--standalone] | deploy [project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision] | versions <list | view <version-id>> [project-dir] | rollback <version-id> [project-dir] | tail [project-dir] [--sprout] | kv <list | create <name> | info <name> | rename <name> <new> | delete <name>> | <key | bulk | export> ... | d1 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | r2 <list | create <name> | info <name> | rename <name> <new> | delete <name>> | queues <list | create <name> | info <name> | rename <name> <new> | delete <name>> | domains <list | add <host> | verify <host> | delete <host>> [project-dir] | secrets <list | put <NAME> [--value <value>] | delete <NAME>> [project-dir] | delete [project-dir] [--name <project>] --yes | login [--api-url <url>] [--token <token>] | logout [--api-url <url>] | whoami>
33
34
  ```
34
35
 
35
36
  ## Environment variables
@@ -39,7 +40,9 @@ usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port
39
40
  | `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
40
41
  | `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
41
42
  | `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
42
- | `SPROUTBOAT_UWS_TARBALL` | Path to a prebuilt uWebSockets (x86_64-linux-musl) tarball to seed the Porffor cache with, instead of downloading it (removes the first-build git + make need). |
43
+ | `SPROUTBOAT_UWS_TARBALL` | Path to a uWebSockets source tarball to seed the Porffor cache with, instead of the one vendored in the package. Both targets are seeded from it. |
44
+ | `CC` | C compiler used to build uSockets for a host build (default `cc`). A host build needs one regardless: it is what Porffor compiles its own generated C with. |
45
+ | `AR` | Archiver used to assemble uSockets.a for a host build (default `ar`). |
43
46
  | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
44
47
  | `SPROUTBOAT_VARS_JSON` | JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module. |
45
48
  | `SPROUTBOAT_BINDINGS_JSON` | The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line. |
@@ -51,6 +54,13 @@ usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port
51
54
  | `SB_BROKER_PORT` | Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs). |
52
55
  | `SB_BROKER_TOKEN` | Per-deployment auth token the sprout sends on every broker frame, and the broker sends back on scheduled/queue triggers (paired with SB_BROKER_PORT). |
53
56
  | `SB_SPROUT_URL` | http://127.0.0.1:<PORT> of the sprout; when set, `src/broker.ts` runs the cron scheduler and queue consumer and delivers triggers to it. |
57
+ | `SB_DATA_DIR` | Where a standalone binary keeps store.sqlite and d1/ (#15). Read by the sprout itself in an embedded build; defaults to ./<name>.data. |
58
+ | `SPROUTBOAT_DATA` | Data directory for a standalone binary, after --data and SB_DATA_DIR, before the ./<name>.data default (#15). |
59
+ | `SB_CA_BUNDLE` | PEM bundle of extra certificate authorities a standalone binary should trust, on top of the compiled-in Mozilla root set (#15). Adds trust; nothing disables verification. |
60
+ | `SB_FETCH_MAX_BYTES` | Largest outbound `fetch()` response body a sprout will read, in bytes (default 33554432). The size is the remote host's choice and the body is held whole, so this is the ceiling that stops one upstream exhausting memory. |
61
+ | `SB_REQUEST_BODY_MAX` | Largest inbound request body the runtime accepts, in bytes (default 1048576). Anything larger is refused with 413 before the handler runs. |
62
+ | `SB_EXTRA_CFLAGS` | Flags added to Porffor's native-fetch compile step, set by the build so inline C can include BearSSL's header (#15). |
63
+ | `SB_EXTRA_LINK` | Objects to add to Porffor's native-fetch link line, set by the build so a standalone binary links SQLite and BearSSL (#15). |
54
64
 
55
65
  ## Build toolchain (pinned)
56
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "keywords": [
6
6
  "cli",
@@ -22,12 +22,14 @@
22
22
  "sproutboat": "src/main.ts"
23
23
  },
24
24
  "files": [
25
+ "CHANGELOG.md",
26
+ "SURFACE.md",
25
27
  "src",
26
- "!src/*.test.ts",
27
- "vendor",
28
- "SURFACE.md"
28
+ "types",
29
+ "vendor"
29
30
  ],
30
31
  "type": "module",
32
+ "types": "./types/sproutboat.d.ts",
31
33
  "exports": {
32
34
  "./runtime/config": "./src/config.ts",
33
35
  "./runtime/source": "./src/source.ts",
@@ -36,12 +38,14 @@
36
38
  "./runtime/broker": "./src/broker.ts",
37
39
  "./runtime/wrap": "./src/wrap.ts",
38
40
  "./runtime/prelude": "./src/native-fetch-prelude.js",
39
- "./package.json": "./package.json"
41
+ "./package.json": "./package.json",
42
+ "./types": "./types/sproutboat.d.ts"
40
43
  },
41
44
  "scripts": {
42
45
  "prepare": "lefthook install || true",
43
46
  "typecheck": "tsc --noEmit",
44
47
  "lint": "oxlint .",
48
+ "examples": "bun examples/smoke.ts",
45
49
  "fmt": "oxfmt .",
46
50
  "fmt:check": "oxfmt --check .",
47
51
  "test": "bun test",
@@ -49,7 +53,9 @@
49
53
  "surface": "UPDATE_SURFACE=1 bun test src/surface.test.ts",
50
54
  "version": "bun run surface && git add SURFACE.md",
51
55
  "prepublishOnly": "bun run typecheck && bun test",
52
- "style": "bunx style-doctor@latest --quiet"
56
+ "style": "bunx style-doctor@latest --quiet",
57
+ "kitchen-sink": "bun examples/kitchen-sink/harness.ts",
58
+ "kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
53
59
  },
54
60
  "dependencies": {
55
61
  "esbuild": "^0.28.2",
package/src/bearssl.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * #15 — the TLS client an embedded sprout links against.
3
+ *
4
+ * BearSSL (MIT), plus the Mozilla root set compiled in as trust anchors. Both
5
+ * are downloaded, checksummed and built once into ~/.cache/sproutboat, keyed by
6
+ * version and target, in the same shape as the SQLite object.
7
+ *
8
+ * Why compiled-in anchors: a static musl binary has no portable system trust
9
+ * store — locations differ per distro and macOS wants keychain calls — so the
10
+ * root set travels with the binary. celld does the same. `SB_CA_BUNDLE` can
11
+ * point at a different bundle at run time.
12
+ *
13
+ * Why BearSSL over mbedTLS: measured, not assumed. A complete static client
14
+ * with all 121 anchors came to 192 KB against a 1.9 MB sprout.
15
+ */
16
+ import { spawn } from "node:child_process";
17
+ import { createHash } from "node:crypto";
18
+ import { existsSync } from "node:fs";
19
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
20
+ import { homedir } from "node:os";
21
+ import { resolve } from "node:path";
22
+
23
+ export const BEARSSL_VERSION = "0.6";
24
+ const BEARSSL_URL = `https://bearssl.org/bearssl-${BEARSSL_VERSION}.tar.gz`;
25
+ const BEARSSL_SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139efc83ff14";
26
+
27
+ /**
28
+ * The Mozilla root set, as curl publishes it. Pinned by hash like everything
29
+ * else: a root store that changed under us without a version bump would be a
30
+ * silent change to what the binary trusts.
31
+ *
32
+ * ponytail: pinned per CLI release, so a certificate authority added after this
33
+ * release is unknown until the next one. Fine while releases are frequent;
34
+ * revisit if a user is ever stuck on an old CLI for months.
35
+ */
36
+ const CACERT_URL = "https://curl.se/ca/cacert.pem";
37
+ const CACERT_SHA256 = "f66dff1bdf8f96060b8177976f8b7d9254bc89bc4db933d769f7384d28480bc9";
38
+
39
+ const cacheDir = (): string => resolve(homedir(), ".cache/sproutboat", `bearssl-${BEARSSL_VERSION}`);
40
+
41
+ export type BearsslInput = {
42
+ target: "linux-x86_64" | "host";
43
+ /** Zig binary, required for the linux target. */
44
+ zigBin?: string;
45
+ };
46
+
47
+ export type BearsslBuild = {
48
+ /** Add to the compile step so the prelude can `#include <bearssl.h>`. */
49
+ includeDir: string;
50
+ /** Objects to add to the link line: the library plus the trust anchors. */
51
+ objects: string[];
52
+ };
53
+
54
+ const run = (cmd: string, args: string[], cwd?: string): Promise<{ code: number; stderr: string }> =>
55
+ new Promise((done) => {
56
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
57
+ let stderr = "";
58
+ child.stderr.on("data", (chunk) => (stderr += String(chunk)));
59
+ child.on("close", (code) => done({ code: code ?? 1, stderr }));
60
+ });
61
+
62
+ async function download(url: string, expected: string, to: string): Promise<Buffer> {
63
+ const response = await fetch(url);
64
+ if (!response.ok) throw new Error(`could not download ${url}: HTTP ${response.status}`);
65
+ const bytes = Buffer.from(await response.arrayBuffer());
66
+ const digest = createHash("sha256").update(bytes).digest("hex");
67
+ if (digest !== expected) throw new Error(`checksum mismatch for ${url}: expected ${expected}, got ${digest}`);
68
+ await writeFile(to, bytes);
69
+ return bytes;
70
+ }
71
+
72
+ /** Unpack the source tree once, and build the host tools we need from it. */
73
+ async function sourceTree(): Promise<string> {
74
+ const dir = cacheDir();
75
+ const tree = resolve(dir, `bearssl-${BEARSSL_VERSION}`);
76
+ if (existsSync(resolve(tree, "inc/bearssl.h"))) return tree;
77
+ await mkdir(dir, { recursive: true });
78
+ const tarball = resolve(dir, "bearssl.tar.gz");
79
+ if (!existsSync(tarball)) await download(BEARSSL_URL, BEARSSL_SHA256, tarball);
80
+ const untar = await run("tar", ["xzf", tarball], dir);
81
+ if (untar.code !== 0) throw new Error(`could not unpack BearSSL: ${untar.stderr}`);
82
+ return tree;
83
+ }
84
+
85
+ /** Every .c under src/, which is what the library is. */
86
+ async function sources(tree: string): Promise<string[]> {
87
+ const found: string[] = [];
88
+ const walk = async (dir: string): Promise<void> => {
89
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
90
+ const path = resolve(dir, entry.name);
91
+ if (entry.isDirectory()) await walk(path);
92
+ else if (entry.name.endsWith(".c")) found.push(path);
93
+ }
94
+ };
95
+ await walk(resolve(tree, "src"));
96
+ return found.sort();
97
+ }
98
+
99
+ /**
100
+ * `brssl ta` turns a PEM bundle into C trust-anchor structs. It is a host tool,
101
+ * so it is built with the host compiler whatever the sprout's target is.
102
+ */
103
+ async function trustAnchorTool(tree: string): Promise<string> {
104
+ const tool = resolve(tree, "build/brssl");
105
+ if (existsSync(tool)) return tool;
106
+ const make = await run("make", ["-j8", "build/brssl"], tree);
107
+ if (!existsSync(tool)) throw new Error(`could not build brssl: ${make.stderr}`);
108
+ return tool;
109
+ }
110
+
111
+ /**
112
+ * Generate the trust anchors as linkable C.
113
+ *
114
+ * `brssl` emits them `static`, which is right for its own samples and useless
115
+ * to us: the prelude has to reach them from another translation unit. Rename
116
+ * the array and publish a count beside it.
117
+ */
118
+ async function trustAnchorSource(tree: string): Promise<string> {
119
+ const out = resolve(cacheDir(), "trust-anchors.c");
120
+ if (existsSync(out)) return out;
121
+ const pem = resolve(cacheDir(), "cacert.pem");
122
+ if (!existsSync(pem)) await download(CACERT_URL, CACERT_SHA256, pem);
123
+ const tool = await trustAnchorTool(tree);
124
+
125
+ const generated = await new Promise<string>((done, fail) => {
126
+ const child = spawn(tool, ["ta", pem], { stdio: ["ignore", "pipe", "pipe"] });
127
+ let text = "";
128
+ let err = "";
129
+ child.stdout.on("data", (chunk) => (text += String(chunk)));
130
+ child.stderr.on("data", (chunk) => (err += String(chunk)));
131
+ child.on("close", (code) => (code === 0 ? done(text) : fail(new Error(`brssl ta failed: ${err}`))));
132
+ });
133
+
134
+ const count = /#define TAs_NUM\s+(\d+)/.exec(generated)?.[1];
135
+ if (!count) throw new Error("brssl ta produced no TAs_NUM — its output format changed");
136
+ const linkable = generated
137
+ .replace(/static const br_x509_trust_anchor TAs\[/, "const br_x509_trust_anchor sb_trust_anchors[")
138
+ .replace(/#define TAs_NUM\s+\d+/, `const size_t sb_trust_anchor_count = ${count};`);
139
+ await writeFile(out, `#include <stddef.h>\n#include "bearssl.h"\n${linkable}\n`);
140
+ return out;
141
+ }
142
+
143
+ /** Compile one C file, caching on the output path. */
144
+ async function compile(cc: string, prefix: string[], source: string, out: string, includes: string[]): Promise<void> {
145
+ if (existsSync(out)) return;
146
+ const result = await run(cc, [...prefix, "-c", source, "-o", out, "-Os", ...includes.flatMap((i) => ["-I", i])]);
147
+ if (result.code !== 0) throw new Error(`could not compile ${source}:\n${result.stderr}`);
148
+ }
149
+
150
+ /** BearSSL objects plus trust anchors for this target, building them on first use. */
151
+ export async function ensureBearssl(input: BearsslInput): Promise<BearsslBuild> {
152
+ const tree = await sourceTree();
153
+ const includeDir = resolve(tree, "inc");
154
+ const objDir = resolve(cacheDir(), `obj-${input.target}`);
155
+ await mkdir(objDir, { recursive: true });
156
+
157
+ const [cc, prefix] =
158
+ input.target === "host"
159
+ ? (["cc", []] as const)
160
+ : ([input.zigBin ?? "zig", ["cc", "-target", "x86_64-linux-musl"]] as const);
161
+ const includes = [includeDir, resolve(tree, "src")];
162
+
163
+ const objects: string[] = [];
164
+ // Sequential rather than parallel: this runs once per target and a burst of
165
+ // ~180 compiler processes is a worse neighbour than a slow first build.
166
+ for (const source of await sources(tree)) {
167
+ const out = resolve(objDir, `${source.split("/").pop()!.replace(/\.c$/, "")}.o`);
168
+ await compile(cc, [...prefix], source, out, includes);
169
+ objects.push(out);
170
+ }
171
+
172
+ const anchors = await trustAnchorSource(tree);
173
+ const anchorObject = resolve(objDir, "trust-anchors.o");
174
+ await compile(cc, [...prefix], anchors, anchorObject, includes);
175
+ objects.push(anchorObject);
176
+
177
+ return { includeDir, objects };
178
+ }
179
+
180
+ export const bearsslStamp = (): string => `bearssl/${BEARSSL_VERSION}`;