Skip to content

fix(config): keep the SQLite driver out of the client bundle (#10692) - #10695

Merged
diegosouzapw merged 2 commits into
release/v3.8.50from
fix/10692-client-bundle-server-leak
Aug 18, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.50from
fix/10692-client-bundle-server-leak

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Closes #10692. Stacked on #10691 — that fix is required for the build to get far enough to hit
this one.

Root cause

IMAGE_PROVIDERS is reachable from "use client" dashboard pages: they read its keys to
derive which providers support which media kind (mediaServiceKinds.ts → serviceKindIndex.ts).
The aihorde entry imported its live-catalog service directly, so the browser graph inherited:

aihordeImageCatalog → safeOutboundFetch → proxyFetch → featureFlags
  → db/featureFlags → db/core → driverFactory → sqljsAdapter   ← fs / net / tls

serviceKindIndex.ts already stated the invariant in a comment — "Client-safe: mediaServiceKinds
only pulls in the pure-data media registries (no server-only deps)"
— but a comment cannot fail a
build, so #10542 broke it unnoticed.

Why a dynamic import() is not the fix

Tried first, and it does not work: deferring the module still requires the bundler to make it
browser-loadable, so the same 28 errors came back. The dependency has to be inverted.

The fix

New pure module open-sse/config/dynamicImageModelSources.ts (register / get / reset, no imports).

  • The registry entry asks it for models — it no longer knows the service exists.
  • aihordeImageCatalog registers itself at import time. Every server path that needs live models
    already imports it (api/v1/models/catalog.ts, which is also the only caller of
    getAllImageModels(); the image-generation handler; provider validation), so server behaviour
    is unchanged.
  • With nothing registered the getter yields [] — exactly what the live catalog returned before
    its first poll, so the pre-poll client view is unchanged too.

Validation

Full npm run build:release on this branch: 0 Module not found, artifact produced
(dist/BUILD_SHA written). That is the first successful build of this branch in 60+ recorded
Build App runs.

  • 29 existing AI Horde tests green (catalog, generation, key validation, optional key, fallback).
  • typecheck:core clean, eslint clean.

Guard

tests/unit/client-bundle-no-server-only-10692.test.ts walks the real static-import graph from
the client-safe entry points and fails if any of them reaches db/core, driverFactory,
sqljsAdapter or proxyFetch — printing the offending chain. It deliberately does not follow
import(), and it runs in ~30ms, so this class of regression is caught without waiting for an
hour-long build. Proven to discriminate: it reproduced the exact 6-hop chain before the fix.

`open-sse/services/combo.ts` imported "../../src/lib/localDb.js" — a .js suffix
on a module that only exists as .ts. Turbopack resolved it by accident until the
dependency-tree change in #10647; after that the instrumentation hook died at boot
with MODULE_NOT_FOUND, breaking `npm run dev` and the production build (60
consecutive red `Build App` runs on release/v3.8.50).

Fixes the same latent pattern in src/lib/usage/usageLedger.ts, which survived only
because it is an `import type` and is erased before resolution.

Adds a guard rejecting relative .js specifiers across open-sse/ and src/. Package
specifiers are untouched: publishing ESM as .js is legitimate there (e.g.
@modelcontextprotocol/sdk), and only first-party relative imports are first-party
TypeScript.

Closes #10674
The `aihorde` entry in IMAGE_PROVIDERS imported its live-catalog service directly.
IMAGE_PROVIDERS is reachable from "use client" dashboard pages — they read its KEYS
to derive which providers support which media kind — so that import dragged
aihordeImageCatalog → safeOutboundFetch → proxyFetch → featureFlags → db/core →
sqljsAdapter into the browser graph. The build then tried to bundle fs/net/tls for
the browser and failed with 28 Module not found errors, leaving `Build App` red for
60 consecutive runs and no artifact buildable from the branch.

A dynamic import() does not fix this: the bundler still has to make the module
browser-loadable. The dependency is inverted instead — the registry entry knows only
a pure registration module, and the server-only service registers itself on import,
which every server path needing live models already does. With nothing registered the
getter yields [], exactly what the live catalog returned before its first poll.

Validated by a full `npm run build:release`: 0 Module not found, artifact produced.

Closes #10692
@diegosouzapw

Copy link
Copy Markdown
Collaborator Author

Merging on top of #10691 (now merged). The remaining red checks are the same inherited set documented there (#9985) — stale doc counts, executors/index.ts TS2345, Antigravity cloudcode tests, and stryker tap.testFiles entries for other people's tests.

The check this PR is about is proven green locally in a way CI could not show before: a full npm run build:release on this branch produced 0 Module not found and wrote dist/BUILD_SHA — the first artifact this branch has been able to build in 60+ recorded Build App runs.

@diegosouzapw
diegosouzapw merged commit 22b89a2 into release/v3.8.50 Aug 18, 2026
11 checks passed
@diegosouzapw
diegosouzapw deleted the fix/10692-client-bundle-server-leak branch August 18, 2026 22:23
diegosouzapw added a commit that referenced this pull request Aug 19, 2026
…y point (#10692) (#10700)

The guard shipped with #10695 watched two hand-picked modules. It now walks the static
import graph from all 753 "use client" files in src/ (plus the two originally pinned
entries), so the invariant is verified across the repo instead of where someone
remembered to look. Full sweep runs in ~750ms.

Two exclusions make that practical:

- `import type` is not an edge — TypeScript erases it before the bundler sees it.
  Counting type imports turns 3 real findings into 29; a guard that cries wolf gets
  switched off.
- Dynamic `import()` is still not followed. It does not break a bundle edge (that was
  tried for #10692 and failed) but it does move the module into a chunk the browser
  fetches on demand, which is a legitimate boundary.

The widened sweep immediately found what the narrow one could not: five value-form
imports of `db/batches` / `db/files` across three files under dashboard/batch, each
reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord,
FileRecord) used in type position, so the compiler was eliding them and the build stayed
green — the same latent shape as #10692 before #10647 removed the toolchain's tolerance.
Marking them `import type` makes the elision explicit instead of incidental.

Refs #10692

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…-inference#10692) (cheaper-inference#10695)

* fix(sse): import localDb through its real .ts extension (cheaper-inference#10674)

`open-sse/services/combo.ts` imported "../../src/lib/localDb.js" — a .js suffix
on a module that only exists as .ts. Turbopack resolved it by accident until the
dependency-tree change in cheaper-inference#10647; after that the instrumentation hook died at boot
with MODULE_NOT_FOUND, breaking `npm run dev` and the production build (60
consecutive red `Build App` runs on release/v3.8.50).

Fixes the same latent pattern in src/lib/usage/usageLedger.ts, which survived only
because it is an `import type` and is erased before resolution.

Adds a guard rejecting relative .js specifiers across open-sse/ and src/. Package
specifiers are untouched: publishing ESM as .js is legitimate there (e.g.
@modelcontextprotocol/sdk), and only first-party relative imports are first-party
TypeScript.

Closes cheaper-inference#10674

* fix(config): keep the SQLite driver out of the client bundle (cheaper-inference#10692)

The `aihorde` entry in IMAGE_PROVIDERS imported its live-catalog service directly.
IMAGE_PROVIDERS is reachable from "use client" dashboard pages — they read its KEYS
to derive which providers support which media kind — so that import dragged
aihordeImageCatalog → safeOutboundFetch → proxyFetch → featureFlags → db/core →
sqljsAdapter into the browser graph. The build then tried to bundle fs/net/tls for
the browser and failed with 28 Module not found errors, leaving `Build App` red for
60 consecutive runs and no artifact buildable from the branch.

A dynamic import() does not fix this: the bundler still has to make the module
browser-loadable. The dependency is inverted instead — the registry entry knows only
a pure registration module, and the server-only service registers itself on import,
which every server path needing live models already does. With nothing registered the
getter yields [], exactly what the live catalog returned before its first poll.

Validated by a full `npm run build:release`: 0 Module not found, artifact produced.

Closes cheaper-inference#10692

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…y point (cheaper-inference#10692) (cheaper-inference#10700)

The guard shipped with cheaper-inference#10695 watched two hand-picked modules. It now walks the static
import graph from all 753 "use client" files in src/ (plus the two originally pinned
entries), so the invariant is verified across the repo instead of where someone
remembered to look. Full sweep runs in ~750ms.

Two exclusions make that practical:

- `import type` is not an edge — TypeScript erases it before the bundler sees it.
  Counting type imports turns 3 real findings into 29; a guard that cries wolf gets
  switched off.
- Dynamic `import()` is still not followed. It does not break a bundle edge (that was
  tried for cheaper-inference#10692 and failed) but it does move the module into a chunk the browser
  fetches on demand, which is a legitimate boundary.

The widened sweep immediately found what the narrow one could not: five value-form
imports of `db/batches` / `db/files` across three files under dashboard/batch, each
reaching db/core → the SQLite driver. All five bind only interfaces (BatchRecord,
FileRecord) used in type position, so the compiler was eliding them and the build stayed
green — the same latent shape as cheaper-inference#10692 before cheaper-inference#10647 removed the toolchain's tolerance.
Marking them `import type` makes the elision explicit instead of incidental.

Refs cheaper-inference#10692

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants