Skip to content

Release 11.27.7#563

Merged
kaihaase merged 1 commit into
mainfrom
develop
Jul 15, 2026
Merged

Release 11.27.7#563
kaihaase merged 1 commit into
mainfrom
develop

Conversation

@kaihaase

Copy link
Copy Markdown
Member

Fixes SWC temporal-dead-zone startup crashes caused by circular imports — all DI tokens now live in import-free leaf files (one cycle was already crashing nest start -b swc) — restores the Secure attribute on BetterAuth session cookies over HTTPS, and adds a check:swc-tdz CI guard that loads every module as its own entry point to catch such cycles. Also migrates the toolchain to pnpm 11 and Node 24 LTS and isolates e2e test databases per worker to remove a shared-DB 401 flake.

…hing), stop a silent Secure-cookie strip, and add the CI guard that would have caught all of it (#560)

* fix(better-auth): extract DI tokens to break module/service circular import (SWC TDZ crash)

The better-auth DI tokens were split across two files that imported each
other: BETTER_AUTH_INSTANCE was declared in core-better-auth.module.ts,
while BETTER_AUTH_CONFIG and BETTER_AUTH_COOKIE_DOMAIN were declared in
core-better-auth.service.ts. The service imported BETTER_AUTH_INSTANCE
from the module, and the module imported the other two tokens (plus the
service class) back from the service — a genuine import cycle.

Under tsc/CommonJS this happened to work by evaluation-order luck. Under
SWC (`nest start -b swc`) it crashes at startup:

  ReferenceError: Cannot access 'BETTER_AUTH_INSTANCE' before initialization

The tokens are dereferenced at module-evaluation time (`@Inject(...)` in
the CoreBetterAuthService constructor is evaluated when the class is
defined), so the cycle lands in the temporal dead zone.

Fix: move all three tokens into a dedicated leaf module,
core-better-auth.constants.ts, which imports nothing. Module and service
both import the tokens from there, making the graph acyclic and the
initialization order deterministic for every compiler.

Backward compatible: both core-better-auth.module.ts and
core-better-auth.service.ts re-export the tokens, so existing deep
imports (`from '.../core-better-auth.module'` /
`'.../core-better-auth.service'`) and the package barrel keep working
unchanged. No public API change.

Verified by compiling src/ with SWC (legacy decorators + decoratorMetadata,
matching the Nest CLI's SWC builder) and requiring the module: the
ReferenceError reproduces before this change and is gone after it. madge
reports the better-auth cycles dropping from 9 to 1; the remaining
guard -> module cycle is benign because it only dereferences
CoreBetterAuthModule lazily inside a method body, never at eval time.

Build, lint, format, 691 unit tests and 1380 e2e tests all pass.

* fix(core): merge FilterInput and CombinedFilterInput into one module

The two classes are mutually recursive and lived in two files that imported
each other. FilterInput referenced CombinedFilterInput EAGERLY — in a decorator
argument (`type: CombinedFilterInput`) and in the `design:type` metadata that
emitDecoratorMetadata emits for `combinedFilter?: CombinedFilterInput`. Both are
evaluated at class-definition time, i.e. while the module is still initializing,
so on the cycle they read a class binding still in its temporal dead zone.

This was not latent. It crashed, today, in shipped code:

  require('dist/core/common/inputs/combined-filter.input.js')
  -> ReferenceError: Cannot access 'CombinedFilterInput' before initialization

It stayed hidden only because entering through the package barrel pulls
filter.input in first via other importers (filter.args, filter.helper). A
vendor-mode deep import, a unit test importing the input directly, or a
reordering of src/index.ts would all have hit it.

A lazy thunk does NOT fix this: `type: () => CombinedFilterInput` still leaves
the eager design:type metadata behind, and SWC's `typeof` guard does not protect
the member expression it compiles to. The only fix is to remove the import edge,
so both classes now live in filter.input.ts with CombinedFilterInput declared
FIRST (that ordering is load-bearing). combined-filter.input.ts stays as a
re-export, so every existing import path still resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): take restricted.decorator off every import cycle

restricted.decorator sat on TWO runtime cycles at once:

  restricted.decorator -> db.helper -> input.helper -> restricted.decorator
  restricted.decorator -> tenant/core-tenant.helpers -> config.service
                       -> input.helper -> restricted.decorator

input.helper therefore evaluated while restricted.decorator was still
initializing. Nothing crashed, but only because every cross-cycle dereference
happened to sit inside a function body. A single top-level line in input.helper
— a module-level alias of checkRestricted, an @Restricted-decorated class,
design:type metadata — would have thrown under SWC -> CommonJS:

  ReferenceError: Cannot access 'checkRestricted' before initialization

…in the file that drives field-level access control, on a compiler the default
build never runs, with a fully green test suite.

Two leaf extractions remove both edges:

  - id.helper.ts    <- the ID cluster (equalIds, getIncludedIds, getStringIds,
                       getObjectIds) out of db.helper. Depends only on mongoose
                       Types, lodash and a type.
  - clone.helper.ts <- clone / deepFreeze out of input.helper, so config.service
                       can reach them without importing input.helper.

Both are true leaves (no framework imports), and db.helper / input.helper
re-export their former members, so every existing import path keeps working.

Removing one edge is not the same as removing the cycle: the id.helper
extraction alone left the config.service path fully intact. restricted.decorator
now appears in ZERO cycles; repo-wide cycles drop from 10 to 6, and most of the
remainder are type-only imports that madge reports but both compilers erase.

Its three exports are additionally converted from `const` arrows to hoisted
function declarations. With the cycles gone this is defense in depth: hoisted
declarations are initialized before any module body runs, so they stay TDZ-immune
whatever the import graph does next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(better-auth): remove the last module cycle and the reset() leak

The DI-token extraction broke module <-> service, but a structurally identical
cycle survived in the same module: better-auth-roles.guard imported
CoreBetterAuthModule for the static getTokenServiceInstance() accessor, while
the module imported the guard to register it as APP_GUARD. It was benign only
because both sides dereference each other lazily inside method bodies — one
static field initializer or one typed constructor parameter away from the very
crash this branch fixes.

core-better-auth.registry.ts now holds the BetterAuthTokenService reference. It
uses `import type` only, which both tsc and SWC erase, so it emits no require()
at all and is a true leaf. The guard reads the registry; the module writes it;
CoreBetterAuthModule.getTokenServiceInstance() stays as a thin delegate, so the
public API is unchanged (core-ai-mcp.controller keeps working). The better-auth
module now has zero cycles.

Also fixes a test-isolation leak found on the way: CoreBetterAuthModule.reset()
cleared serviceInstance and userMapperInstance but never the token service, so a
token service from a previous testing module survived into the next one and
BetterAuthRolesGuard verified tokens against a stale instance.

Additionally:
- The constants docblock claimed the change "makes the dependency graph acyclic".
  That was false while the guard cycle survived, and a maintainer reading it would
  reasonably have hoisted the guard's lazy lookup into a static field — recreating
  the crash in good faith. Corrected, and the real rule is spelled out: a cycle is
  only fatal when dereferenced at module-evaluation time.
- The backward-compat re-exports are narrowed to exactly the symbols each file
  exported before, instead of widening the surface to three valid import paths per
  token — which is precisely how the original cycle was born.
- Token JSDoc now states the injected type; they are untyped string tokens, so the
  docblock is the only type signal a consumer gets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(better-auth): stop a consumer key from silently stripping Secure

createBetterAuthInstance pins `advanced.useSecureCookies: false` so BetterAuth's
native handlers read the same unprefixed session-cookie name the cookie helper
writes, and restores the Secure attribute via `advanced.defaultCookieAttributes`
on an https baseURL. But a consumer's `options.advanced` was merged SHALLOWLY —
so a project setting defaultCookieAttributes for an entirely unrelated reason
wholesale-replaced the framework's `{ secure: true }`:

  options.advanced.defaultCookieAttributes = { partitioned: true }
  -> Set-Cookie: iam.session_token=…; HttpOnly; SameSite=Lax     (Secure GONE)

On an https deployment that ships session cookies in the clear on every flow
BetterAuth handles natively — 2FA verify, social callback, magic link, passkey —
all of which ESTABLISH a session. No error, no warning. `sameSite: 'none'` would
self-detect (browsers reject it without Secure), but `{ partitioned: true }` or
`{ domain: … }` strip it silently.

That one key is now deep-merged: `secure: true` is the base and the consumer's
keys spread over it, so an EXPLICIT `secure: false` still wins while an unrelated
key can no longer clobber it.

Tests: the existing spec only asserted the CONFIG shape. It would have stayed
green through this bug, and through a BetterAuth upgrade that reordered the
spread in createCookieGetter (where our Secure flag rests entirely on
defaultCookieAttributes being spread AFTER `secure: !!secureCookiePrefix`). The
new tests call BetterAuth's own getCookies() on a constructed instance and assert
the DERIVED attributes, plus sendWebResponse() — the helper a prior review accused
of dropping Secure — forwards it to Express verbatim. Derivation -> forwarding ->
wire is now pinned end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): add the SWC/TDZ guard that CI was missing entirely

This bug class had the worst possible failure profile: it passed a fully green
CI and crashed only for consumers. Nothing in the pipeline could see it.

  - `nest build` / `pnpm run build` use tsc, whose emit tolerates these cycles.
  - vitest DOES run SWC — but through Vite's module runner, whose getter-based
    live bindings resolve a cycle lazily, so the temporal dead zone never opens.
    2000+ green tests proved nothing here.
  - oxlint has no `import/no-cycle` rule (checked; it is not implemented).

Only SWC -> CommonJS -> Node's require() fails, and no job ran that.

`pnpm run check:swc-tdz` compiles with SWC and loads the output under the CJS
loader. It is wired into check:raw (so `pnpm run check` picks it up) and into the
build workflow, which today runs neither `check` nor a server boot and is
therefore weaker than the local gate.

It loads EVERY compiled module as its own entry point, not just the barrel:
whether such a cycle throws depends on which module the graph is entered through,
and a barrel-only load reported the (real, crashing) combined-filter.input bug as
green. Sharded across 4 forked children — capped at 4 because each child pays a
full node_modules cold load, and 12 shards measured slower than running
single-threaded. It deliberately does NOT narrow the entry set to "files in a
cycle": that would make the guard's correctness depend on a static cycle analysis,
and a blind spot there would not produce a wrong answer but a GREEN one, on the
one check that exists because everything else is already blind.

Two structural specs pin what the guard cannot see. check:swc-tdz catches the
CRASH; it does not catch the DISARMING of a safety property. Each invariant below
can be reverted by a well-meaning refactor — an "organize imports", a "modernize
to arrow functions" — with nothing turning red, because the cycle would be
present-but-disarmed again:

  - the DI-token and registry files stay import-free leaves
  - the guard reaches the token service without importing the module
  - the token STRING VALUES (public API; consumers may write
    @Inject('BETTER_AUTH_INSTANCE') as a literal — a typo would be invisible
    in-repo, since provide/inject/@Inject all read the same imported symbol)
  - restricted.decorator takes its helpers from the leaves, and its exports stay
    hoisted function declarations
  - id.helper / clone.helper import no framework code
  - the AI services' load-bearing `import type` stays type-only

Every one of these was mutation-tested: the bug was reintroduced and the test
watched go red.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: refuse to drop a database that is not recognizably a test database

The global setup drops whatever MONGODB_URI points at, and that variable is not
always a test database: a running `lt dev` session exports it pointing at the
project's DEVELOPMENT database. Running the suite from that shell would silently
wipe the developer's data.

Both the setup and the stale-run cleanup now require the database name to look
like a test database before anything is dropped. CI does not set MONGODB_URI, so
the guard never triggers there, and the run databases the suite creates
(nest-server-e2e-run-*, nest-server-ci-*) satisfy it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: write down the leaf-file rule so this bug class cannot come back

The constraint this branch establishes existed nowhere except the source files it
fixed — and the footgun path stayed open: importing a token from
core-better-auth.module inside the module still compiles, still passes every test,
and only crashes under SWC. The same shape is still latent in tus.module.ts and in
nine AI service files.

  - .claude/rules/better-auth.md gains a §6 on DI-token placement: the rule, the
    exact error, the mistakes table, and — most importantly — a table of which
    tools can and cannot see a regression here. A green `pnpm test` is NO evidence.
  - .claude/rules/architecture.md generalises it to all core modules, lists which
    modules are already safe and which are one back-import away, and records the
    lesson that cost the most time: removing one edge is not removing the cycle.
    Re-run madge and check the file appears in ZERO cycles.
  - migration-guides/11.27.6-to-11.27.7.md covers the SWC startup crash (with the
    error string, so an affected consumer searching for it lands here), the
    CombinedFilterInput deep-import crash, the cookie Secure fix, and the
    vendor-mode note that the BetterAuth change is an atomic file set — a partial
    core sync that omits the new leaf leaves an unresolvable import.
  - CLAUDE.md: document check:swc-tdz, and correct the stale claim that this repo
    lints with ESLint and formats with Prettier (it uses oxlint / oxfmt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(check): make the SWC guard side-effect free, fail-fast and unfoolable

Three defects in the guard as first committed — one of which made it capable of
reporting GREEN while checking nothing at all.

1. It built into the REAL dist/.
   `nest-cli.json` sets `deleteOutDir: true`, so `pnpm run check:swc-tdz` wiped the
   tsc-built artifact and left a partial SWC build in its place — the directory
   `pnpm start:prod` runs. Inside the check chain the next step rebuilt it, which is
   why it went unnoticed; standalone it silently destroyed the user's build. It now
   builds into `dist-swc-tdz/` via its own tsconfig: removed on success, KEPT on
   failure, because the emitted JS is the evidence (it shows whether the cyclic
   binding is dereferenced at top level or inside a function body).

2. It could pass having loaded zero modules.
   An empty or mis-emitted output directory meant `files = []`, no shard had
   anything to do, no failure was reported, and it printed "0 modules load clean"
   and exited 0. The one check that exists because tsc, vitest and oxlint are all
   blind to this bug class could itself go quietly blind. There is now a floor: if
   fewer than 200 loadable modules are found it fails instead of reporting success.

3. Nothing bounded a forked shard.
   A module that blocks inside require() — a socket, stdin, or a server boot if an
   exclusion ever stops matching — would have left the parent in Promise.all forever:
   a hang with no output and no error. Each shard now has a hard timeout, and
   `main.js` (whose last line calls `bootstrap()`) is excluded by BASENAME rather
   than by an absolute path, which would silently stop matching if the emit layout
   ever shifted. `process.send()` also waits for the flush before exiting, so a
   healthy shard cannot be misread as a dead one.

Being side-effect free also removes the ordering constraint, so the guard moves
AHEAD of the test step: a cycle now fails in ~10s instead of after the full suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(better-auth): keep the token-service registry out of the public API

The registry was re-exported through the module barrel, which put
`setBetterAuthTokenService()` on the package's public surface — letting a consumer
swap or null the very token service `BetterAuthRolesGuard` makes its authorization
decisions with. In a PATCH release, no less.

It is module-internal plumbing: it exists only so the guard can reach
BetterAuthTokenService without importing CoreBetterAuthModule (the cycle). Nothing
outside the module needs it, and consumers that want the instance already have the
public, read-only `CoreBetterAuthModule.getTokenServiceInstance()`.

With it dropped, the package root exports exactly what it did on develop — 472
symbols, zero added, zero removed. Verified by building both refs and diffing.

Also pins the backward-compat re-exports. Every symbol moved to a leaf in this
branch stays reachable only because its old home re-exports it, and this package has
no `exports` map — consumers deep-import these files directly. Those re-export lines
carry a public contract while looking exactly like dead code to anyone tidying up
imports, and deleting one would break every deep importer with nothing turning red:
the symbol is still exported from the barrel, so the package-root tests stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix the rule that still showed the forbidden pattern, complete the guide

- .claude/rules/better-auth.md §5 still showed `CoreBetterAuthModule.getTokenServiceInstance()`
  inside the BetterAuthRolesGuard sample, while §6 — added in this same branch —
  lists importing the module into the guard as the mistake that re-creates the
  cycle. An agent copying §5 would have re-armed the bug the branch just fixed.
  The sample now reads the registry, with a note on when the static accessor is
  still fine (everywhere except the guard).

- The migration guide documented ONE atomic file set for vendor-mode consumers.
  There are three: the BetterAuth tokens, the core helpers (id.helper / clone.helper
  — a sync that takes db.helper without id.helper gives an unresolvable import,
  exactly the hazard the guide already warned about elsewhere), and the filter
  inputs. All three are now listed, along with an explicit warning not to "clean up"
  the re-export lines that hold backward compatibility together.

- The guide called the new leaves "internal". Now that the registry is genuinely
  internal (not barrel-exported) and the others are not, it says which is which —
  and records that the public API is unchanged, verified by diffing the export
  surface of both versions.

FRAMEWORK-API.md regenerated for the version bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): move every remaining DI token into an import-free leaf

The leaf rule this branch introduced shipped with ten known violations of itself:
`TUS_CONFIG` in `tus.module.ts`, and 22 `AI_*` tokens spread across eleven
`ai/services/*.service.ts` files. None crashed — their graphs happened to be
acyclic — but "happens to be acyclic" is not a safety property, it is luck that
nothing is watching:

  - `@Inject(TOKEN)` is a constructor-parameter decorator, evaluated at
    class-definition time. A token declared in the file that also gets imported
    back is one back-import away from being read inside its own temporal dead zone.
  - `tsc`, `pnpm test` and `oxlint` are ALL blind to that. Only
    `pnpm run check:swc-tdz` sees it, and only after the fact.

TUS_CONFIG now lives in `tus.constants.ts`, the AI tokens in `core-ai.constants.ts`.
Both files import nothing, so they can never be mid-evaluation when someone imports
them, under any compiler.

Also removes the last real cycle in the AI module. `core-ai.service` imports
`core-ai-interaction.service` (it needs the class), and the interaction service
needed the `AiInteractionRecord` type back — a cycle held apart by ONE keyword: the
`import type`, which both compilers erase. `core-ai.service` has a constructor
`@Inject`, so `decoratorMetadata` emits `design:paramtypes` at module-evaluation
time: an IDE "organize imports" widening that import would have armed the crash
silently. The type moved to its own interface leaf, so the edge is gone rather than
merely erased.

Every old location re-exports what it lost. Public API is byte-identical: 472
exports on develop, 472 here, zero added, zero removed — verified by building both
refs and diffing.

Repo-wide cycles: 6 -> 5. The five that remain are type-only imports madge reports
but both compilers erase (empty emits) — not runtime cycles at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): auto-enable low-resource mode when the machine is already loaded

The e2e suites intermittently failed with `socket hang up`, 401s on auth queries,
and tests hanging past the 30s timeout — all of which read like genuine product
bugs while being pure resource starvation. The trigger is two full e2e runs
overlapping: several `lt dev` environments, a second project's check, or simply a
forgotten background run.

The escape hatch already existed — `CHECK_LOW_RESOURCE=1` caps parallel forks and
raises timeouts — but it was OPT-IN, which is the wrong default. It only helps the
developer who already knows the env var exists, and that is precisely the developer
who does not need it. Whoever gets bitten is the one who has never heard of it,
staring at a "flaky" auth test and re-running it until it passes.

It now auto-enables from the 1-minute load average normalised per core. Explicit
settings still win in both directions:

  CHECK_LOW_RESOURCE=1     -> force on   (CI, or a machine you know is busy)
  CHECK_LOW_RESOURCE=0     -> force off  (benchmarking; never auto-throttle)
  unset                    -> auto       (on iff normalised load >= 0.7)

Verified all three: under 12 CPU burners the unset case throttles (maxForks=4,
timeouts raised) and prints why; `=0` overrides it back off. Load average is
unavailable on Windows, where auto-detection stays off and the flag remains.

Throttling costs some wall-clock. A green suite that has to be re-run costs more,
and a red one that gets dismissed as "flaky" costs the most.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: enforce the leaf rule instead of documenting it

.claude/rules/architecture.md introduced "DI tokens belong in an import-free leaf"
and then listed ten live violations of itself. A rule with known violations is a
suggestion, and a suggestion does not survive a refactor.

`import-cycle-invariants.spec.ts` now fails if:
  - any DI token is declared in a *.module.ts or a *.service.ts (re-exports are
    fine — those are the backward-compat shims; only declarations re-create the edge)
  - any constants leaf grows an import, an export-from, or a require
  - `core-ai-interaction.service` imports from `core-ai.service` at all

This matters because `check:swc-tdz` catches the CRASH, not the DISARMING of a
safety property — and only the second one is silent. A "modernize to arrow
functions", an "organize imports", a "split this file up" can each re-arm the bug
with everything staying green.

Every assertion was mutation-tested: the violation reintroduced, the test watched
go red. That includes a false positive in the first version of the spec, which
flagged the leaves' own docblocks — they legitimately contain the words `import`
and `require()` while explaining the bug they prevent. Only the code is evidence.

Rules updated to describe what is actually true: zero known violations, cycles
10 -> 5, and the lesson that cost the most time — removing one edge is not removing
the cycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sync spectaql version stamp to 11.27.7

Auto-updated by extras/update-spectaql-version.mjs to follow the package.json bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: complete the vendor-mode file sets (AI + TUS leaves)

The guide listed three atomic file sets; the AI and TUS token extractions landed
afterwards and were missing. There are five, and six new files — a partial core
sync that omits any one of them leaves an unresolvable import.

Also adds what the guide did not say and should have: vendor-mode projects with
local edits in db.helper, input.helper, filter.input, restricted.decorator or any
ai/services/*.service.ts WILL get merge conflicts, because this release moves code
out of exactly those files. The resolution is always the same — keep the local
logic, keep the new import/export-from lines pointing at the leaf, do not restore
the declarations that moved out.

And the warning that matters most: the `export { … } from './…'` lines now scattered
across seventeen files look exactly like dead code and are the ENTIRE backward-
compatibility layer. This package has no `exports` map, so consumers deep-import
those files directly; deleting one line breaks all of them with nothing turning red,
because the symbol is still exported from the package root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: correct the new-file count in the migration guide (six -> seven)

The prose said six; the list under it had seven, and the diff has seven. A
vendor-mode consumer verifying "all six exist" after an interrupted sync would have
stopped one file short — of a file whose absence is an unresolvable import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(check): don't hard-fail when npm retires the audit endpoint

npm retired the legacy `/-/npm/v1/security/audits/quick` endpoint (HTTP 410), which
every pnpm 10.x calls — so `pnpm audit`, and therefore `pnpm run check`, failed for a
reason unrelated to the code and unfixable by any pnpm 10 version. Blocking check on
that is wrong twice: it is not a finding, and a permanently-red audit trains everyone
to ignore a red audit (the real hazard).

check.mjs now degrades ONLY the retired-endpoint signature (ERR_PNPM_AUDIT_BAD_RESPONSE
/ "audit ... retired") to a loud non-blocking warning — never a green ✓, since that
would claim "no vulnerabilities", which was not verified. A real vulnerability (parseable
counts) and any other non-zero exit stay fatal. On pnpm 11 (next commit) audit works
again via the bulk endpoint and this degrade never triggers; it stays as a safety net.

npm audit is NOT used as a fallback: it resolves the tree from package.json alone,
ignoring pnpm-lock.yaml and the security overrides, so it reports phantom findings for
CVEs the overrides already fix. A misleading red is worse than an honest "could not run".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(pnpm): upgrade to pnpm 11 and migrate config to pnpm-workspace.yaml

Motivation: `pnpm audit` is broken on every pnpm 10.x — npm retired the legacy audit
endpoint (HTTP 410) and only pnpm 11 uses the working bulk-advisory endpoint. Verified
empirically across 10.33.2, 10.34.5 and 11.13.0 (the repo's packageManager pin forces
pnpm to re-exec the pinned version, so the fix only lands with the packageManager bump).

pnpm 11 is a breaking config change, migrated canonically per https://pnpm.io/blog/releases/11.0:

- The `pnpm` field in package.json is no longer read → moved to pnpm-workspace.yaml:
  overrides, allowBuilds, nodeLinker, autoInstallPeers, strictPeerDependencies,
  peerDependencyRules. .npmrc is now auth/registry only.
- `allowBuilds` replaces `onlyBuiltDependencies` — a map that must classify EVERY
  install-script package as true/false. bcrypt/@swc/core/esbuild/… = true;
  @scarf/scarf (telemetry) = false. An unclassified one makes `pnpm install` exit
  non-zero and appends a broken stub (that stub was the pre-existing mess in the repo).

Security overrides pruned 36 → 9. Each survivor is load-bearing: removing it lets the
package resolve back INTO its vulnerable range (verified by a with/without lockfile diff,
and confirmed by `pnpm audit` — clean before and after the prune). The 27 removed were
no-ops (parents have since shipped fixed versions). Kept: ajv, @babel/core, picomatch,
ws, uuid, vite, nodemailer, multer, js-yaml.

Verified working on pnpm 11:
- `pnpm install` (frozen + non-frozen), native builds (bcrypt), 0 ignored-builds errors
- `pnpm audit` runs for real via the bulk endpoint → 0 vulnerabilities
- every pnpm invocation in scripts/Dockerfile/CI: pnpm pack, store prune,
  install --prod --ignore-scripts, rebuild bcrypt — all exit 0; none use commands
  removed in v11 (install -g, link, server)
- `pnpm run check` fully green (2107 tests)
- CI (pnpm/action-setup@v6) and Docker (corepack) follow the packageManager field, so
  they use pnpm 11 with no workflow/Dockerfile edits

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): isolate the run database per worker to kill the 401 flake

The e2e config runs spec files in parallel forks (fileParallelism: true), and
global-setup gives the whole RUN one database. So every parallel file shared one
DB — and a file that mutates a GLOBAL collection corrupted the others mid-run.

Concrete failure: better-auth-integration.story.test.ts clears the `jwks`
collection (BetterAuth's JWT signing keys, used implicitly by every BetterAuth
instance) in a beforeAll. Run in parallel with better-auth-autoregister-false,
it wiped the keys that file's token was signed with → its authenticated request
got a spurious 401 (expected 200). Isolated, both pass; together under parallel
load, the 401 appeared intermittently. Root cause, not flakiness.

tests/setup.ts runs in every fork BEFORE the test file (and config.env.ts) is
imported, so it appends the vitest pool id to MONGODB_URI there: each concurrent
fork gets its own database (…-run-<ts>-p<pid>-w<N>). Files sharing a fork run
sequentially (safe); files in different forks can no longer collide. This is the
right layer — better-auth-integration uses ServerModule, which reads the DB from
the already-imported config, so per-file overrides in the spec don't work.

No reporter change needed: db-lifecycle.reporter drops every DB whose name starts
with the run DB name, which already matches the -w<N> forks — verified no orphaned
databases remain after a run.

Verified: the two colliding files pass together 3x; full `pnpm run check` green 3x
in a row (2107 tests); zero orphaned -w databases afterwards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): point the failure-debug message at the per-worker fork DBs

Per-worker DB isolation put the actual test data in `${runDb}-w<N>` fork
databases, but the "kept for debugging" message still named the base `${runDb}`
— which the main process holds but no fork writes to. A developer debugging a
failed run would connect to an empty database.

The failure branch now lists the real `${runDb}-w*` fork databases (best-effort;
falls back to the base name if the listing fails). Verified end to end: a failing
run keeps the fork DB with its data and names it correctly; the next passing run
drops it (no orphaned databases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kaihaase
kaihaase merged commit 7abad12 into main Jul 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant