sproutboat 0.10.1 → 0.10.3
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 +39 -1
- package/SURFACE.md +2 -1
- package/package.json +3 -3
- package/src/surface.ts +5 -0
- package/src/toolchain.ts +69 -15
- package/types/sproutboat.d.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,42 @@ maintained going forward by the `release` skill.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.3] - 2026-09-11
|
|
14
|
+
### Added
|
|
15
|
+
- Rate-limiter result carries `resetAt`: `env.<NAME>.limit({ key })` now returns
|
|
16
|
+
`{ success, resetAt }`, where `resetAt` is epoch ms for when the fixed window
|
|
17
|
+
rolls, so a `429` can send an accurate `Retry-After`
|
|
18
|
+
(baronunread/sproutboat#69). Additive; callers reading only `success` are
|
|
19
|
+
unaffected.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- `@sproutboat/runtime` and `@sproutboat/wire` bumped to `^0.5.0`; they carry
|
|
23
|
+
the `resetAt` value the rate-limiter type now declares.
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
- `docs/standalone.md`: dropped the stale "outbound `fetch()` speaks `http://`
|
|
27
|
+
only" bullet. Standalone binaries link BearSSL and verify HTTPS against the
|
|
28
|
+
Mozilla root set (see "TLS, in both directions"). Added a
|
|
29
|
+
dev/standalone/deployed parity note: every binding op, rate limiters and
|
|
30
|
+
`request.cf.clientIp` with `SB_TRUSTED_PROXIES` included, behaves the same
|
|
31
|
+
under `sproutboat dev` as in a standalone binary and a deployed sprout.
|
|
32
|
+
- `capability-http-sync-v0.md`: documented async handlers. `fetch` may return a
|
|
33
|
+
promise it creates (not a `.then()`-chained one, which hangs); async responses
|
|
34
|
+
omit `x-sb-cpu-ms`; `crypto.subtle` digest/sign/verify are async in signature
|
|
35
|
+
only over synchronous inline C, so per-request hashing costs no I/O.
|
|
36
|
+
|
|
37
|
+
## [0.10.2] - 2026-09-11
|
|
38
|
+
### Fixed
|
|
39
|
+
- The one-time Zig download no longer depends on ziglang.org being reachable.
|
|
40
|
+
It now pulls from a random Zig community mirror, with ziglang.org kept only
|
|
41
|
+
as a last resort, and tries the mirrors in turn on failure. ziglang.org
|
|
42
|
+
rate-limits the multi-megabyte tarballs and had been timing out mid-build,
|
|
43
|
+
with no fallback (baronunread/sproutboat#166). The archive sha256 is still
|
|
44
|
+
enforced whatever the source. `SPROUTBOAT_ZIG_URL` overrides the source;
|
|
45
|
+
`SPROUTBOAT_ZIG` still points at a prebuilt binary.
|
|
46
|
+
- A leftover `~/.cache/sproutboat/zig-<version>/` directory from an earlier
|
|
47
|
+
cache layout is removed on the next build instead of sitting unused.
|
|
48
|
+
|
|
13
49
|
## [0.10.1] - 2026-09-11
|
|
14
50
|
### Fixed
|
|
15
51
|
- `npm install sproutboat` / `bunx sproutboat` work again. 0.10.0's launcher
|
|
@@ -358,7 +394,9 @@ its own package.
|
|
|
358
394
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
359
395
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
360
396
|
|
|
361
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.
|
|
397
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.3...HEAD
|
|
398
|
+
[0.10.3]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.2...v0.10.3
|
|
399
|
+
[0.10.2]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.1...v0.10.2
|
|
362
400
|
[0.10.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.0...v0.10.1
|
|
363
401
|
[0.10.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.9.0...v0.10.0
|
|
364
402
|
[0.9.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.8.0...v0.9.0
|
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.10.
|
|
6
|
+
**Package:** `sproutboat` 0.10.3 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -40,6 +40,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
40
40
|
| `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
|
|
41
41
|
| `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
|
|
42
42
|
| `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
|
|
43
|
+
| `SPROUTBOAT_ZIG_URL` | URL to download the pinned Zig tarball from, instead of a Zig community mirror. The sha256 is still enforced. |
|
|
43
44
|
| `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
45
|
| `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
46
|
| `AR` | Archiver used to assemble uSockets.a for a host build (default `ar`). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"@sproutboat/artifact": "^0.2.0",
|
|
64
64
|
"@sproutboat/assets": "^0.2.0",
|
|
65
65
|
"@sproutboat/config": "^0.3.0",
|
|
66
|
-
"@sproutboat/runtime": "^0.
|
|
66
|
+
"@sproutboat/runtime": "^0.5.0",
|
|
67
67
|
"@sproutboat/toolchain": "^0.3.0",
|
|
68
|
-
"@sproutboat/wire": "^0.
|
|
68
|
+
"@sproutboat/wire": "^0.5.0",
|
|
69
69
|
"esbuild": "^0.28.2"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
package/src/surface.ts
CHANGED
|
@@ -194,6 +194,11 @@ export const ENV_VARS: readonly EnvVar[] = [
|
|
|
194
194
|
{ name: "SPROUTBOAT_API_URL", purpose: "Control-plane URL. Overrides the saved active endpoint." },
|
|
195
195
|
{ name: "SPROUTBOAT_TOKEN", purpose: "API token. Overrides the saved credential for the endpoint." },
|
|
196
196
|
{ name: "SPROUTBOAT_ZIG", purpose: "Path to a Zig binary to use instead of downloading the pinned one." },
|
|
197
|
+
{
|
|
198
|
+
name: "SPROUTBOAT_ZIG_URL",
|
|
199
|
+
purpose:
|
|
200
|
+
"URL to download the pinned Zig tarball from, instead of a Zig community mirror. The sha256 is still enforced.",
|
|
201
|
+
},
|
|
197
202
|
{
|
|
198
203
|
name: "SPROUTBOAT_UWS_TARBALL",
|
|
199
204
|
purpose:
|
package/src/toolchain.ts
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* The build toolchain: a pinned Zig (the linux-x86_64 cross-compiler Porffor
|
|
3
3
|
* shells out to for `--musl`) plus version stamps for the artifact manifest.
|
|
4
4
|
*
|
|
5
|
-
* Zig is fetched once to ~/.cache/sproutboat/zig-<version>/ and
|
|
6
|
-
* Docker, no root.
|
|
5
|
+
* Zig is fetched once to ~/.cache/sproutboat/zig-<version>-<platform>/ and
|
|
6
|
+
* reused. No Docker, no root. It comes from a random Zig community mirror with
|
|
7
|
+
* ziglang.org as the last resort, because ziglang.org rate-limits the large
|
|
8
|
+
* tarballs and has been timing out. Override the binary with
|
|
9
|
+
* SPROUTBOAT_ZIG=/path/to/zig, or the download source with SPROUTBOAT_ZIG_URL.
|
|
7
10
|
*/
|
|
8
11
|
import { createHash } from "node:crypto";
|
|
9
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -101,24 +104,69 @@ async function zigComplete(dir: string, key: ZigPlatform, expectedArchive: strin
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
// Zig asks tooling not to hammer ziglang.org for the multi-megabyte tarballs:
|
|
108
|
+
// pick a random community mirror and keep the official host only as a last
|
|
109
|
+
// resort. https://ziglang.org/download/community-mirrors.txt
|
|
110
|
+
const ZIG_MIRRORS_URL = "https://ziglang.org/download/community-mirrors.txt";
|
|
111
|
+
|
|
112
|
+
function shuffle<T>(items: T[]): T[] {
|
|
113
|
+
for (let i = items.length - 1; i > 0; i--) {
|
|
114
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
115
|
+
[items[i], items[j]] = [items[j], items[i]];
|
|
116
|
+
}
|
|
117
|
+
return items;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ordered list of URLs to try for the pinned Zig tarball: the community mirrors
|
|
122
|
+
* (shuffled) followed by ziglang.org. Falls back to ziglang.org alone if the
|
|
123
|
+
* mirror list itself is unreachable.
|
|
124
|
+
*/
|
|
125
|
+
async function zigDownloadUrls(
|
|
126
|
+
key: ZigPlatform,
|
|
127
|
+
fetcher: NonNullable<EnsureZigOptions["fetcher"]>,
|
|
128
|
+
timeoutMs: number,
|
|
129
|
+
): Promise<string[]> {
|
|
130
|
+
const file = `zig-${key}-${ZIG_VERSION}.tar.xz`;
|
|
131
|
+
const official = `https://ziglang.org/download/${ZIG_VERSION}/${file}`;
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetcher(ZIG_MIRRORS_URL, { signal: AbortSignal.timeout(Math.min(timeoutMs, 10_000)) });
|
|
134
|
+
if (response.ok) {
|
|
135
|
+
const mirrors = (await response.text())
|
|
136
|
+
.split(/\s+/)
|
|
137
|
+
.filter((line) => line.startsWith("https://"))
|
|
138
|
+
.map((base) => `${base.replace(/\/+$/, "")}/${file}`);
|
|
139
|
+
if (mirrors.length > 0) return [...shuffle(mirrors), official];
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Mirror list unreachable: the official host is the only option left.
|
|
143
|
+
}
|
|
144
|
+
return [official];
|
|
145
|
+
}
|
|
146
|
+
|
|
104
147
|
async function downloadZig(
|
|
105
|
-
|
|
148
|
+
urls: string[],
|
|
106
149
|
path: string,
|
|
107
150
|
fetcher: NonNullable<EnsureZigOptions["fetcher"]>,
|
|
108
151
|
timeoutMs: number,
|
|
109
152
|
): Promise<void> {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
// One shot per mirror; two at the single fallback URL so a lone flaky host
|
|
154
|
+
// still gets a retry.
|
|
155
|
+
const attemptsPer = urls.length > 1 ? 1 : 2;
|
|
156
|
+
const failures: string[] = [];
|
|
157
|
+
for (const url of urls) {
|
|
158
|
+
for (let attempt = 0; attempt < attemptsPer; attempt++) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetcher(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
161
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
162
|
+
await writeFile(path, new Uint8Array(await response.arrayBuffer()));
|
|
163
|
+
return;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
failures.push(`${url}: ${String(error)}`);
|
|
166
|
+
}
|
|
119
167
|
}
|
|
120
168
|
}
|
|
121
|
-
throw new ZigToolchainError("download", `could not download pinned Zig
|
|
169
|
+
throw new ZigToolchainError("download", `could not download pinned Zig\n ${failures.join("\n ")}`);
|
|
122
170
|
}
|
|
123
171
|
|
|
124
172
|
async function waitForZigPublisher(
|
|
@@ -151,6 +199,10 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
151
199
|
const root = resolve(
|
|
152
200
|
options.cacheRoot ?? process.env.SPROUTBOAT_TOOLCHAIN_CACHE ?? resolve(homedir(), ".cache/sproutboat"),
|
|
153
201
|
);
|
|
202
|
+
// Older builds cached Zig at `zig-<version>/` with no platform suffix; nothing
|
|
203
|
+
// reads that layout now, so drop it rather than leave it as dead weight.
|
|
204
|
+
const legacy = resolve(root, `zig-${ZIG_VERSION}`);
|
|
205
|
+
if (existsSync(legacy)) await rm(legacy, { recursive: true, force: true }).catch(() => {});
|
|
154
206
|
const dir = resolve(root, `zig-${ZIG_VERSION}-${key}`);
|
|
155
207
|
const expected = options.expectedSha256 ?? ZIG_SHA256[key];
|
|
156
208
|
if (await zigComplete(dir, key, expected)) return resolve(dir, "zig");
|
|
@@ -164,14 +216,16 @@ export async function ensureZig(options: EnsureZigOptions = {}): Promise<string>
|
|
|
164
216
|
return ensureZig(options);
|
|
165
217
|
}
|
|
166
218
|
const stage = resolve(root, `.zig-${ZIG_VERSION}-${key}-${process.pid}-${crypto.randomUUID()}`);
|
|
167
|
-
const
|
|
219
|
+
const fetcher = options.fetcher ?? fetch;
|
|
220
|
+
const sourceUrl = options.url ?? process.env.SPROUTBOAT_ZIG_URL;
|
|
221
|
+
const urls = sourceUrl ? [sourceUrl] : await zigDownloadUrls(key, fetcher, options.timeoutMs ?? 30_000);
|
|
168
222
|
console.log(`Fetching Zig ${ZIG_VERSION} (${key}, one-time)...`);
|
|
169
223
|
try {
|
|
170
224
|
if (await zigComplete(dir, key, expected)) return resolve(dir, "zig");
|
|
171
225
|
await rm(dir, { recursive: true, force: true });
|
|
172
226
|
await mkdir(stage);
|
|
173
227
|
const archive = resolve(stage, "zig.tar.xz");
|
|
174
|
-
await downloadZig(
|
|
228
|
+
await downloadZig(urls, archive, fetcher, options.timeoutMs ?? 60_000);
|
|
175
229
|
const actual = await sha256File(archive);
|
|
176
230
|
if (actual !== expected)
|
|
177
231
|
throw new ZigToolchainError(
|
package/types/sproutboat.d.ts
CHANGED
|
@@ -182,9 +182,13 @@ declare global {
|
|
|
182
182
|
* A rate-limiter binding (#69). `limit` and `period` are fixed in
|
|
183
183
|
* sproutboat.jsonc; `limit({ key })` counts one call against a fixed window
|
|
184
184
|
* and reports whether the key is still under the cap.
|
|
185
|
+
*
|
|
186
|
+
* `resetAt` is epoch milliseconds for when the window rolls and the count
|
|
187
|
+
* clears — the same for a hit or a miss. Turn a rejection into a header with
|
|
188
|
+
* `Retry-After: Math.ceil((resetAt - Date.now()) / 1000)`.
|
|
185
189
|
*/
|
|
186
190
|
interface RateLimit {
|
|
187
|
-
limit(options: { key: string }): { success: boolean };
|
|
191
|
+
limit(options: { key: string }): { success: boolean; resetAt: number };
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
// ------------------------------------ Crypto
|