diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d3989c59..3d2349f8a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -180,6 +180,7 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e ### 🐛 Bug Fixes +- **fix(build):** every route no longer answers HTTP 500 on artifacts built from the release tip ([#11343](https://github.com/diegosouzapw/OmniRoute/issues/11343)) — `next.config.mjs` aliased `better-sqlite3` to its build-time stub **unconditionally**, on the premise that `serverExternalPackages` still won at runtime. It does not: a Turbopack `resolveAlias` rewrites the request *before* the externals check, so the request stopped matching the `better-sqlite3` external entry and the stub was baked into the shipped bundle. The sync driver then failed with `r(...) is not a constructor`, fell through `node:sqlite` and sql.js, and the instrumentation hook aborted at boot. Same failure shape as [#6344](https://github.com/diegosouzapw/OmniRoute/issues/6344), so it gets the same treatment: the alias is opt-in via `OMNIROUTE_BETTER_SQLITE3_STUB=1` through the shared `scripts/build/better-sqlite3-stub-flag.mjs` helper — set it only on a build host that actually hits the SIGABRT build-worker teardown ([#10060](https://github.com/diegosouzapw/OmniRoute/issues/10060)); default builds externalize the real native addon. Regression guards: `tests/unit/better-sqlite3-stub-alias-11343.test.mjs` (5) and the env matrix in `tests/unit/next-config.test.ts`. - **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963 - **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366) - **cli**: route provider test commands through configured connection test endpoints (#10570) diff --git a/next.config.mjs b/next.config.mjs index a142370a51..ef67001f34 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,6 +2,7 @@ import createNextIntlPlugin from "next-intl/plugin"; import { createMDX } from "fumadocs-mdx/next"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { betterSqlite3AliasFor } from "./scripts/build/better-sqlite3-stub-flag.mjs"; import { mitmManagerAliasFor } from "./scripts/build/mitm-stub-flag.mjs"; import { normalizeBasePath } from "./scripts/build/normalizeBasePath.mjs"; import { @@ -138,10 +139,14 @@ const nextConfig = { // the stub to every npm/Electron/VPS artifact and broke Agent Bridge // start for all non-Docker users (#6344). See scripts/build/mitm-stub-flag.mjs. ...mitmManagerAliasFor(process.env), - // Build-time stub so the bundler never traces the native better-sqlite3 - // addon into a build worker (SIGABRT at worker teardown). Runtime still - // uses the real package via serverExternalPackages. (#10060) - "better-sqlite3": "./src/lib/db/better-sqlite3.stub.js", + // better-sqlite3 → build-time stub ONLY where the build worker actually + // aborts while tracing the native addon (SIGABRT at worker teardown, + // #10060); opt in with OMNIROUTE_BETTER_SQLITE3_STUB=1. The alias used to + // be unconditional on the premise that serverExternalPackages still won + // at runtime — it does not: resolveAlias rewrites the request before the + // externals check, so the stub was bundled and EVERY route answered 500 + // (#11343). See scripts/build/better-sqlite3-stub-flag.mjs. + ...betterSqlite3AliasFor(process.env), ...minimalBuildAliases, }, // src/lib/agentSkills/generator.ts builds its fs base path from a runtime diff --git a/scripts/build/better-sqlite3-stub-flag.mjs b/scripts/build/better-sqlite3-stub-flag.mjs new file mode 100644 index 0000000000..2cdd86c8a1 --- /dev/null +++ b/scripts/build/better-sqlite3-stub-flag.mjs @@ -0,0 +1,36 @@ +/** + * Decide whether the Next.js build should alias `better-sqlite3` to the + * build-time stub (src/lib/db/better-sqlite3.stub.js). + * + * History (#11343): the alias was UNCONDITIONAL, added to keep the bundler from + * tracing the native addon into a Next.js build worker, whose thread teardown + * can abort with SIGABRT (assertion in node::RemoveEnvironmentCleanupHook) and + * leave the build without standalone output (#10060). + * + * The premise recorded next to that alias — "runtime still uses the real + * package via serverExternalPackages" — does not hold. A Turbopack + * `resolveAlias` rewrites the request BEFORE the externals check runs, so + * `better-sqlite3` becomes a relative path, no longer matches the + * `serverExternalPackages` entry, and the stub is baked into the bundle. Every + * artifact built from that config answered HTTP 500 on every route: the stub's + * default export is not a constructor, the sync driver chain fell through to + * `node:sqlite` and then sql.js, and the instrumentation hook aborted at boot. + * + * This is the same failure shape as #6344 (the @/mitm/manager stub shipping to + * every npm/Electron/VPS artifact), so it gets the same treatment: the alias is + * opt-in, and a default build gets the real, externalized native package. + * + * Set OMNIROUTE_BETTER_SQLITE3_STUB=1 ONLY on a build host that actually hits + * the SIGABRT worker teardown, and never for an artifact that will be run — + * the resulting bundle cannot open a database. + */ +export function shouldStubBetterSqlite3(env = process.env) { + return env.OMNIROUTE_BETTER_SQLITE3_STUB === "1"; +} + +/** Turbopack resolveAlias fragment for `better-sqlite3`, derived from the env. */ +export function betterSqlite3AliasFor(env = process.env) { + return shouldStubBetterSqlite3(env) + ? { "better-sqlite3": "./src/lib/db/better-sqlite3.stub.js" } + : {}; +} diff --git a/src/lib/db/better-sqlite3.stub.js b/src/lib/db/better-sqlite3.stub.js index d840d98b0c..fdaec7c958 100644 --- a/src/lib/db/better-sqlite3.stub.js +++ b/src/lib/db/better-sqlite3.stub.js @@ -1,13 +1,19 @@ // Build-time stub for better-sqlite3 (#10060). // -// Aliased in for the Next.js production build (turbopack + webpack) so the -// bundler never pulls the real native addon into a build worker. The native -// Statement destructor aborts with SIGABRT when a build worker thread exits +// OPT-IN ONLY — set OMNIROUTE_BETTER_SQLITE3_STUB=1 to alias it in, and only on +// a build host that actually hits the SIGABRT worker teardown: the native +// Statement destructor aborts when a Next.js build worker thread exits // (assertion in node::RemoveEnvironmentCleanupHook, env == nullptr), which can -// leave the build with no standalone output. At runtime the real package is -// used (it is listed in serverExternalPackages, so it is require()'d natively, -// not bundled); this stub only stands in during the build, where the DB is -// never actually queried. +// leave the build with no standalone output. +// +// It is NOT a build-only stand-in. A Turbopack resolveAlias rewrites the +// request before the externals check, so aliasing `better-sqlite3` here also +// removes it from serverExternalPackages' reach and bakes THIS FILE into the +// shipped bundle. An artifact built with the flag on cannot open a database: +// the sync driver chain fails with "r(...) is not a constructor", falls through +// node:sqlite and sql.js, and the instrumentation hook aborts at boot, so every +// route answers HTTP 500. That is exactly what an unconditional alias shipped +// in #11343. See scripts/build/better-sqlite3-stub-flag.mjs. class Database { constructor() {} prepare() { diff --git a/tests/unit/better-sqlite3-stub-alias-11343.test.mjs b/tests/unit/better-sqlite3-stub-alias-11343.test.mjs new file mode 100644 index 0000000000..206cfe2b66 --- /dev/null +++ b/tests/unit/better-sqlite3-stub-alias-11343.test.mjs @@ -0,0 +1,59 @@ +// Regression test for #11343 — an unconditional Turbopack `resolveAlias` for +// better-sqlite3 shipped the build-time stub into the runtime bundle, so every +// artifact built from the release tip answered HTTP 500 on every route (the +// stub export is not a constructor, the sync driver chain fell through to +// node:sqlite and sql.js, and the instrumentation hook aborted at boot). +// +// The alias defeats `serverExternalPackages` because resolveAlias rewrites the +// request BEFORE the externals check runs. It must therefore be opt-in, and a +// default production build must externalize the REAL native package. +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const { shouldStubBetterSqlite3, betterSqlite3AliasFor } = + await import("../../scripts/build/better-sqlite3-stub-flag.mjs"); + +describe("better-sqlite3 stub alias (#11343)", () => { + it("default env does NOT stub better-sqlite3 (shipped artifacts get the real addon)", () => { + assert.equal(shouldStubBetterSqlite3({}), false); + assert.deepEqual(betterSqlite3AliasFor({}), {}); + }); + + it("only the exact opt-in value enables the stub", () => { + for (const value of ["", "0", "true", "yes"]) { + assert.equal( + shouldStubBetterSqlite3({ OMNIROUTE_BETTER_SQLITE3_STUB: value }), + false, + `OMNIROUTE_BETTER_SQLITE3_STUB=${JSON.stringify(value)} must not enable the stub` + ); + } + }); + + it("OMNIROUTE_BETTER_SQLITE3_STUB=1 opts into the stub (SIGABRT-prone build hosts, #10060)", () => { + assert.equal(shouldStubBetterSqlite3({ OMNIROUTE_BETTER_SQLITE3_STUB: "1" }), true); + assert.deepEqual(betterSqlite3AliasFor({ OMNIROUTE_BETTER_SQLITE3_STUB: "1" }), { + "better-sqlite3": "./src/lib/db/better-sqlite3.stub.js", + }); + }); + + it("next.config.mjs derives the turbopack alias from the flag (no unconditional stub)", () => { + const config = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8"); + assert.match( + config, + /betterSqlite3AliasFor/, + "next.config.mjs must use betterSqlite3AliasFor()" + ); + assert.doesNotMatch( + config, + /^\s*"better-sqlite3":\s*"\.\/src\/lib\/db\/better-sqlite3\.stub\.js",?\s*$/m, + "next.config.mjs must not hardcode the better-sqlite3 stub alias" + ); + }); + + it("better-sqlite3 stays in serverExternalPackages so the default build externalizes it", () => { + const config = readFileSync(new URL("../../next.config.mjs", import.meta.url), "utf8"); + const externals = config.slice(config.indexOf("serverExternalPackages:")); + assert.match(externals.slice(0, externals.indexOf("]")), /"better-sqlite3"/); + }); +}); diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index 6291fb69f2..10b9f26742 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -83,6 +83,10 @@ test("next config declares Turbopack aliases, runtime assets and server external // A default production build must NOT alias it, or the stub ships to npm/Electron/VPS // artifacts and breaks Agent Bridge start. See the dedicated env-matrix test below. assert.equal(nextConfig.turbopack.resolveAlias["@/mitm/manager"], undefined); + // #11343: same story for the better-sqlite3 build stub. resolveAlias is applied + // BEFORE the serverExternalPackages check, so an unconditional alias bundles the + // stub and every route answers 500 at runtime ("r(...) is not a constructor"). + assert.equal(nextConfig.turbopack.resolveAlias["better-sqlite3"], undefined); assert.equal(nextConfig.outputFileTracingRoot, process.cwd()); assert.ok(tracingIncludes.includes("./src/lib/db/migrations/**/*")); assert.ok( @@ -118,6 +122,28 @@ test("next config declares Turbopack aliases, runtime assets and server external } }); +test("Turbopack aliases better-sqlite3 to the stub ONLY when OMNIROUTE_BETTER_SQLITE3_STUB=1 (#11343)", async () => { + const original = process.env.OMNIROUTE_BETTER_SQLITE3_STUB; + try { + delete process.env.OMNIROUTE_BETTER_SQLITE3_STUB; + const { default: def } = await loadNextConfig("bettersqlite-default"); + assert.equal(def.turbopack.resolveAlias["better-sqlite3"], undefined); + // The default build must keep the real package reachable as an external, which + // is exactly what the alias silently defeated. + assert.ok(new Set(def.serverExternalPackages).has("better-sqlite3")); + + process.env.OMNIROUTE_BETTER_SQLITE3_STUB = "1"; + const { default: stubbed } = await loadNextConfig("bettersqlite-optin"); + assert.equal( + stubbed.turbopack.resolveAlias["better-sqlite3"], + "./src/lib/db/better-sqlite3.stub.js" + ); + } finally { + if (original === undefined) delete process.env.OMNIROUTE_BETTER_SQLITE3_STUB; + else process.env.OMNIROUTE_BETTER_SQLITE3_STUB = original; + } +}); + test("Turbopack aliases @/mitm/manager to the stub ONLY when OMNIROUTE_MITM_STUB=1 (#6344)", async () => { const original = process.env.OMNIROUTE_MITM_STUB; try {