sproutboat 0.7.0 → 0.8.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/SURFACE.md +10 -3
- package/package.json +4 -2
- package/src/bearssl.ts +180 -0
- package/src/broker.ts +372 -14
- package/src/build.ts +53 -1
- package/src/compile.ts +52 -3
- package/src/config.ts +25 -0
- package/src/main.ts +30 -4
- package/src/native-fetch-prelude.js +127 -168
- package/src/patch-porffor.ts +104 -6
- package/src/sqlite.ts +99 -0
- package/src/standalone-build.ts +64 -0
- package/src/surface.ts +37 -2
- package/src/transport-broker.js +290 -0
- package/src/transport-embedded.js +1567 -0
- package/src/wrap.ts +46 -2
- package/vendor/sqlite-amalgamation-3500400/shell.c +34019 -0
- package/vendor/sqlite-amalgamation-3500400/sqlite3.c +262899 -0
- package/vendor/sqlite-amalgamation-3500400/sqlite3.h +13775 -0
- package/vendor/sqlite-amalgamation-3500400/sqlite3ext.h +723 -0
- package/vendor/sqlite-amalgamation.zip +0 -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.
|
|
6
|
+
**Package:** `sproutboat` 0.8.0 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@
|
|
|
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
14
|
| `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. |
|
|
15
|
+
| `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
16
|
| `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
17
|
| `versions` | `<list | view <version-id>> [project-dir]` | List the project's deployed versions, or show one version's artifact and bindings. |
|
|
18
18
|
| `rollback` | `<version-id> [project-dir]` | Re-activate a previous version. |
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
| `whoami` | — | Show the active endpoint and the account the stored token belongs to. |
|
|
30
30
|
|
|
31
31
|
```
|
|
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>
|
|
32
|
+
usage: sproutboat <init [name] | check [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>> | 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
33
|
```
|
|
34
34
|
|
|
35
35
|
## Environment variables
|
|
@@ -51,6 +51,13 @@ usage: sproutboat <init [name] | check [project-dir] | dev [project-dir] [--port
|
|
|
51
51
|
| `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
52
|
| `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
53
|
| `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. |
|
|
54
|
+
| `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. |
|
|
55
|
+
| `SPROUTBOAT_DATA` | Data directory for a standalone binary, after --data and SB_DATA_DIR, before the ./<name>.data default (#15). |
|
|
56
|
+
| `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. |
|
|
57
|
+
| `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. |
|
|
58
|
+
| `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. |
|
|
59
|
+
| `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). |
|
|
60
|
+
| `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
61
|
|
|
55
62
|
## Build toolchain (pinned)
|
|
56
63
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -49,7 +49,9 @@
|
|
|
49
49
|
"surface": "UPDATE_SURFACE=1 bun test src/surface.test.ts",
|
|
50
50
|
"version": "bun run surface && git add SURFACE.md",
|
|
51
51
|
"prepublishOnly": "bun run typecheck && bun test",
|
|
52
|
-
"style": "bunx style-doctor@latest --quiet"
|
|
52
|
+
"style": "bunx style-doctor@latest --quiet",
|
|
53
|
+
"kitchen-sink": "bun examples/kitchen-sink/harness.ts",
|
|
54
|
+
"kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
|
|
53
55
|
},
|
|
54
56
|
"dependencies": {
|
|
55
57
|
"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}`;
|