sproutboat 0.12.7 → 0.14.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 CHANGED
@@ -10,6 +10,37 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.14.0] - 2026-10-04
14
+
15
+ ### Changed
16
+
17
+ - `fetch()` reaches any public address with no configuration, and never a
18
+ private or reserved one: loopback, private networks, link-local and cloud
19
+ metadata, and their IPv6 forms. The check runs on the resolved address at
20
+ connect time, in deployed sprouts and standalone binaries alike. Operators can
21
+ allow specific addresses with `SB_EGRESS_ALLOW`
22
+ (baronunread/sproutboat#174).
23
+ - Every build writes `bindings.json`, so every deployment gets a broker.
24
+
25
+ ### Deprecated
26
+
27
+ - `outbound` in `sproutboat.jsonc` is ignored with a warning, and will be
28
+ refused in a later release.
29
+
30
+ ### Fixed
31
+
32
+ - `fetch()` to an IPv6 literal such as `http://[::1]:9/` works in standalone
33
+ binaries; `URL.hostname` used to come back as `[`.
34
+
35
+ ## [0.13.0] - 2026-10-04
36
+
37
+ ### Added
38
+
39
+ - `sproutboat build` reads `_headers` and `_redirects` (Cloudflare Pages
40
+ format) from the assets directory and writes the rules into `assets.json`.
41
+ The deployed edge applies them: redirects before any file lookup, header
42
+ rules on edge-served files (baronunread/sproutboat#61).
43
+
13
44
  ## [0.12.7] - 2026-10-03
14
45
 
15
46
  ### Fixed
@@ -773,7 +804,9 @@ its own package.
773
804
  - Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
774
805
  `sprout` bin alias in favour of a user-defined shell alias.
775
806
 
776
- [Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.7...HEAD
807
+ [Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.14.0...HEAD
808
+ [0.14.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.13.0...v0.14.0
809
+ [0.13.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.7...v0.13.0
777
810
  [0.12.7]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.6...v0.12.7
778
811
  [0.12.6]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.5...v0.12.6
779
812
  [0.12.5]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.4...v0.12.5
package/README.md CHANGED
@@ -133,7 +133,6 @@ complete export. Existing output files require `--force`.
133
133
  "analytics_engine_datasets": ["METRICS"], // bare name only, no id
134
134
  "durable_objects": { "COUNTER": "Counter" },
135
135
  "ratelimiters": [{ "binding": "API", "limit": 100, "period": 60 }], // env.API.limit({ key })
136
- "outbound": ["api.example.com"],
137
136
  "triggers": { "crons": ["*/5 * * * *"] },
138
137
  "assets": { "directory": "public", "binding": "ASSETS", "run_sprout_first": ["/api/*"] }
139
138
  }
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.12.7
6
+ **Package:** `sproutboat` 0.14.0
7
7
 
8
8
  ## Commands
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.12.7",
3
+ "version": "0.14.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",
@@ -63,10 +63,10 @@
63
63
  "dependencies": {
64
64
  "@sproutboat/artifact": "^0.3.0",
65
65
  "@sproutboat/assets": "^0.3.0",
66
- "@sproutboat/config": "^0.4.0",
67
- "@sproutboat/runtime": "^0.13.5",
66
+ "@sproutboat/config": "^0.5.0",
67
+ "@sproutboat/runtime": "^0.14.0",
68
68
  "@sproutboat/toolchain": "^0.4.24",
69
- "@sproutboat/wire": "^0.9.3"
69
+ "@sproutboat/wire": "^0.11.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@oxlint/plugins": "1.81.0",
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.12.7",
82
- "@sproutboat/cli-darwin-x64": "0.12.7",
83
- "@sproutboat/cli-linux-arm64": "0.12.7",
84
- "@sproutboat/cli-linux-x64": "0.12.7"
81
+ "@sproutboat/cli-darwin-arm64": "0.14.0",
82
+ "@sproutboat/cli-darwin-x64": "0.14.0",
83
+ "@sproutboat/cli-linux-arm64": "0.14.0",
84
+ "@sproutboat/cli-linux-x64": "0.14.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
package/src/build.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, resolve } from "node:path";
4
- import { walkAssets, type AssetManifest } from "./assets";
4
+ import { readAssetRules, walkAssets, type AssetManifest } from "./assets";
5
5
  import { resourceRefs, type SproutboatConfig } from "./config";
6
6
  import { ensureSqliteObject } from "./sqlite";
7
7
  import { sqliteStamp } from "./sqlite";
@@ -138,7 +138,6 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
138
138
  const bindings = {
139
139
  kv: refsByKind.kv.map((ref) => ref.binding),
140
140
  secrets: input.config.secrets ?? [],
141
- outbound: input.config.outbound ?? [],
142
141
  d1: refsByKind.d1.map((ref) => ref.binding),
143
142
  r2: refsByKind.r2.map((ref) => ref.binding),
144
143
  queues: refsByKind.queue.map((ref) => ref.binding),
@@ -166,14 +165,18 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
166
165
  ) {
167
166
  throw new Error(`assets.directory "${input.config.assets?.directory}" not found — run your site build first`);
168
167
  }
169
- const assetManifest: AssetManifest | undefined =
170
- assetDir && input.config.assets
171
- ? {
172
- notFound: input.config.assets.not_found_handling ?? "none",
173
- runSproutFirst: input.config.assets.run_sprout_first ?? false,
174
- files: walkAssets(assetDir),
175
- }
176
- : undefined;
168
+ let assetManifest: AssetManifest | undefined;
169
+ if (assetDir && input.config.assets) {
170
+ assetManifest = {
171
+ notFound: input.config.assets.not_found_handling ?? "none",
172
+ runSproutFirst: input.config.assets.run_sprout_first ?? false,
173
+ files: walkAssets(assetDir),
174
+ };
175
+ // #61 — `_headers`/`_redirects` ride along for the edge to apply.
176
+ const rules = readAssetRules(assetDir);
177
+ if (rules.headers.length) assetManifest.headers = rules.headers;
178
+ if (rules.redirects.length) assetManifest.redirects = rules.redirects;
179
+ }
177
180
  // #15 — an embedded binary has no files beside it, so assets are baked into
178
181
  // the module. Read them from the source directory: the artifact copy happens
179
182
  // after the compile, and the compile is what needs them. Bytes travel as a
@@ -316,15 +319,10 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
316
319
  await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
317
320
  // Bindings live beside the manifest, not in it: the artifact manifest schema is
318
321
  // frozen at v2. The control plane reads this to configure the per-deployment
319
- // broker (KV / D1 / R2 / queue names, secret names, outbound allowlist, cron
320
- // schedules, Durable Object classes).
321
- const hasBindings =
322
- Object.values(bindings).some((value) => Array.isArray(value) && value.length > 0) ||
323
- Object.keys(bindings.resources).length > 0 ||
324
- Object.keys(bindings.vars).length > 0;
325
- if (hasBindings) {
326
- await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
327
- } else await rm(resolve(artifactDir, "bindings.json"), { force: true });
322
+ // broker (KV / D1 / R2 / queue names, secret names, cron schedules, Durable
323
+ // Object classes). Written even when empty: its presence is what gives a
324
+ // deployment a broker, and fetch() needs one with no binding (#174).
325
+ await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
328
326
 
329
327
  // Static assets: copy the directory next to the artifact and record a manifest
330
328
  // the edge serves from directly (assets-first) and the broker reads for
package/src/main.ts CHANGED
@@ -212,6 +212,7 @@ async function readProjectConfig(directory = process.cwd()) {
212
212
  }
213
213
  const parsed = parseConfig(configSource);
214
214
  if (!parsed.ok) fail(parsed.errors.join("\n"));
215
+ for (const warning of parsed.warnings) console.warn(amber(`! ${warning}`));
215
216
  return { directory: projectDirectory, config: parsed.value };
216
217
  }
217
218
 
@@ -234,7 +235,7 @@ async function readProject(directory = process.cwd()) {
234
235
  } catch (cause) {
235
236
  fail(cause instanceof BundleError ? cause.message : String(cause));
236
237
  }
237
- const supported = validateHttpSyncSource(bundle.code, (project.config.outbound ?? []).length > 0);
238
+ const supported = validateHttpSyncSource(bundle.code);
238
239
  if (!supported.ok) fail(supported.errors.join("\n"));
239
240
  return { ...project, sourcePath, source, bundle };
240
241
  }
package/src/report.ts CHANGED
@@ -66,7 +66,6 @@ function bindingRows(config: SproutboatConfig): string[][] {
66
66
  rows.push([`env.${name}`, "durable object", className]);
67
67
  for (const rl of config.ratelimiters ?? [])
68
68
  rows.push([`env.${rl.binding}`, "rate limiter", `${rl.limit} / ${rl.period}s`]);
69
- for (const host of config.outbound ?? []) rows.push([`fetch()`, "outbound", host]);
70
69
  for (const cron of config.triggers?.crons ?? []) rows.push([`scheduled()`, "cron", cron]);
71
70
  if (config.assets?.binding) rows.push([`env.${config.assets.binding}`, "assets", config.assets.directory ?? ""]);
72
71
  return rows;
@@ -29,7 +29,7 @@ export type StandaloneBuildResult = { outPath: string; bytes: number };
29
29
  * Outbound `fetch` is absent from this list: http and https both work, with
30
30
  * BearSSL and the Mozilla root set compiled in.
31
31
  */
32
- export function unsupportedBindings(bindings: Pick<Partial<Bindings>, "services" | "outbound">): string[] {
32
+ export function unsupportedBindings(bindings: Pick<Partial<Bindings>, "services">): string[] {
33
33
  const reasons: string[] = [];
34
34
  if ((bindings.services ?? []).length > 0) {
35
35
  reasons.push("service bindings call another deployment through an edge, which a standalone binary has none of");