Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8786d83
fix(better-auth): extract DI tokens to break module/service circular …
kaihaase Jul 13, 2026
0ec3ea9
fix(core): merge FilterInput and CombinedFilterInput into one module
kaihaase Jul 14, 2026
588523b
fix(core): take restricted.decorator off every import cycle
kaihaase Jul 14, 2026
15b9ac1
fix(better-auth): remove the last module cycle and the reset() leak
kaihaase Jul 14, 2026
87333cb
fix(better-auth): stop a consumer key from silently stripping Secure
kaihaase Jul 14, 2026
2da23b2
test(core): add the SWC/TDZ guard that CI was missing entirely
kaihaase Jul 14, 2026
b7c680f
test: refuse to drop a database that is not recognizably a test database
kaihaase Jul 14, 2026
f0c43bd
docs: write down the leaf-file rule so this bug class cannot come back
kaihaase Jul 14, 2026
13e8860
fix(check): make the SWC guard side-effect free, fail-fast and unfool…
kaihaase Jul 14, 2026
eb9f539
fix(better-auth): keep the token-service registry out of the public API
kaihaase Jul 14, 2026
aba4bd5
docs: fix the rule that still showed the forbidden pattern, complete …
kaihaase Jul 14, 2026
313c6e8
fix(core): move every remaining DI token into an import-free leaf
kaihaase Jul 14, 2026
740ed4d
test(e2e): auto-enable low-resource mode when the machine is already …
kaihaase Jul 14, 2026
6500e99
test: enforce the leaf rule instead of documenting it
kaihaase Jul 14, 2026
5d8651a
chore: sync spectaql version stamp to 11.27.7
kaihaase Jul 14, 2026
453a39a
docs: complete the vendor-mode file sets (AI + TUS leaves)
kaihaase Jul 14, 2026
14a7e5f
docs: correct the new-file count in the migration guide (six -> seven)
kaihaase Jul 14, 2026
97f95f7
fix(check): don't hard-fail when npm retires the audit endpoint
kaihaase Jul 15, 2026
a11b1c2
chore(pnpm): upgrade to pnpm 11 and migrate config to pnpm-workspace.…
kaihaase Jul 15, 2026
e232291
test(e2e): isolate the run database per worker to kill the 401 flake
kaihaase Jul 15, 2026
2c1eb57
test(e2e): point the failure-debug message at the per-worker fork DBs
kaihaase Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/lt-dev-backend-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- [Core ErrorCode in framework repo](project_core-errorcode.md) — src/core has its own ErrorCode registry; modern core modules use it, but the baseline is mixed (core-user still raw strings).
- [AI module prompt/slot service raw exceptions](project_ai-module-prompt-service-errors.md) — most of the AI module routes through ErrorCode, but CoreAiPromptService + CoreAiSlotService still throw raw-string ForbiddenExceptions; consistency cleanup, not a security gap.
- [SWC TDZ import cycles](project_swc-tdz-import-cycles.md) — cycles crash under `-b swc` only when deref'd at module-eval time; full 9-cycle audit: only 3 are real, `filter.input ↔ combined-filter.input` already throws on deep import.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: swc-tdz-import-cycles
description: Import cycles in src/core only TDZ-crash under SWC when a binding is dereferenced at module-EVAL time (param decorators, static fields); check/CI has no SWC or madge step, so these are invisible to green CI.
metadata:
type: project
---

Circular imports inside `src/core/**` are a live crash class under SWC (`nest start -b swc`),
but **only** when one side dereferences the other's binding during **module evaluation**.

**Why:** SWC's ESM→CJS emit exposes each export as a getter over a TDZ'd `const`/`class` binding.
On a cycle, the second `require()` returns a partially-populated exports object; touching a
property then fires the getter into the TDZ → `ReferenceError: Cannot access 'X' before initialization`.
tsc/CommonJS survives the same graph by evaluation-order luck. Fixed 2026-07 in
`better-auth` (commit 8786d83): `@Inject(BETTER_AUTH_INSTANCE)` sat in a **constructor-parameter
decorator**, which SWC evaluates at class-definition time (top-level statement).

**How to apply — the triage question is always "eval-time or lazy?":**
- CRASHES: constructor param decorators (`@Inject(X)`), class-level decorator *arguments*,
`static` field initializers, top-level `const`, and `design:paramtypes` metadata
(`typeof _mod.X` still fires the CJS getter).
- SAFE (latent): references only inside **method bodies** — resolved at request time, long after
both modules finish evaluating.

To settle a verdict, compile the two files with `@swc/core` (commonjs + legacyDecorator +
decoratorMetadata) and grep the emit: a deref at column 0 / inside `_ts_decorate([...])` is a
crash; one inside a method body is not.

**Known latent (not crashing) as of 2026-07:** `better-auth-roles.guard.ts ↔ core-better-auth.module.ts`
(guard touches `CoreBetterAuthModule.getTokenServiceInstance()` only inside a method body; the module
touches the guard only inside `createDeferredModule()`). Repo-wide `madge --circular src/` reports
~10 cycles — treat that count as the baseline.

**Full 9-cycle audit (2026-07-13, SWC-emit verified).** Only 3 of madge's 9 are RUNTIME cycles;
#3/#6/#7/#8 emit an empty CJS file (type-only) and #5/#9 have a type-only return edge — all madge
false positives. Configure madge `skipTypeImports` to cut the noise.
- **`filter.input.ts ↔ combined-filter.input.ts` is ALREADY BROKEN** — `filter.input.ts:25`
(`type: CombinedFilterInput`, eager) + its `design:type` metadata deref inside a top-level
`_ts_decorate`. Deep-importing `combined-filter.input` FIRST throws today, zero source changes.
The barrel survives only because 3 modules (`filter.args`, `filter.helper`, `single-filter.input`)
pull `FilterInput` in first. **A `type: () => X` thunk does NOT fix it** — the crash just moves to
the `_ts_metadata("design:type", …)` line, which `decoratorMetadata` emits eagerly and userland
cannot make lazy. Only structural de-cycling (merge the mutually-recursive pair into one module) works.
- `restricted.decorator → db.helper → input.helper`: benign today; the exposed edge is the RETURN one
(`input.helper` dereffing restricted's TDZ `const` arrows), not the one reviewers keep checking.
- `core-ai.service.ts:106` has a constructor-param `design:paramtypes` deref (the literal better-auth
shape); the ONLY thing defusing it is `import type` on `core-ai-interaction.service.ts:8` —
one IDE "organize imports" autofix away from a crash.

**Load-bearing `import type`:** in this repo `import type` is not cosmetic — it is what keeps several
cycles off the runtime graph. Never let a lint autofix widen one to a value import.

**Blind spot:** `pnpm run check` → `scripts/check.mjs` runs `nest build` (tsc) only. There is **no
SWC build and no madge/circular step** in check.mjs, check-server-start.sh, or .github/workflows/.
So this entire bug class keeps CI green and only detonates for consumers running `-b swc` —
especially **vendor-mode** consumers who copy `src/core/` into their own tree.
See [[core-errorcode]] for the other core-module consistency baseline.
5 changes: 4 additions & 1 deletion .claude/agent-memory/lt-dev-docs-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
- [FRAMEWORK-API generator interface allowlist](framework-api-generator-allowlist.md) — FRAMEWORK-API.md only documents config interfaces hardcoded in generate-framework-api.ts; new interfaces silently omitted
- [FRAMEWORK-API generator interface allowlist](framework-api-generator-allowlist.md) — only allowlisted config interfaces are expanded; consts/DI tokens/helpers are out of scope, and a date-only diff is build churn
- [Patch-release migration-guide convention](patch-release-migration-guide-convention.md) — every 11.27.x patch has a guide, incl. zero-effort internal bugfixes; don't auto-grade "bugfix → no guide" as N/A
- [Doc places to check for config features](doc-surfaces-for-config-features.md) — the full set of doc surfaces a new configurable feature must update in this repo
- [AI module doc coverage gaps](ai-module-doc-coverage-gaps.md) — AI features exported in index.ts (hooks, tool-grants, modes, attachments, claudeCli, compaction) but missing from user-facing docs
- [Migration-guide behavior-change count trap](migration-guide-behavior-change-count-trap.md) — derive behavior changes from better-auth.config.ts/cookies.helper.ts diffs; guide Overview counts have under-reported before
- [Review committed state vs working tree](review-committed-vs-working-tree.md) — `git status` FIRST; the author's uncommitted delta can be a better revision than the commits, and untracked keystone files ship breaks
- [Vendor-mode atomic file-set check](vendor-mode-atomic-file-set-check.md) — every new file under src/core/ that an existing core file imports must be an enumerated atomic set in the guide
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: framework-api-generator-allowlist
description: FRAMEWORK-API.md only expands config interfaces named in a hardcoded list in scripts/generate-framework-api.ts; new interfaces are silently dropped
description: FRAMEWORK-API.md only expands config interfaces named in a hardcoded list in scripts/generate-framework-api.ts; new interfaces are silently dropped, and non-interface changes are structurally out of scope
metadata:
type: project
---
Expand All @@ -10,3 +10,13 @@ metadata:
**Why:** The CLAUDE.md framework-compatibility rule claims FRAMEWORK-API is "auto-generated... includes the new interface and all fields" — but that is only true if the interface name was added to the allowlist. A new top-level config interface (e.g. `IAi`, `IAiRateLimit`, `IAiDefaultConnection`) appears only as a `field?: boolean | IName` reference under its parent, never expanded, unless someone edits the generator.

**How to apply:** When reviewing a branch that adds a new config interface to `server-options.interface.ts`, do NOT treat "FRAMEWORK-API.md was regenerated" as sufficient. Grep FRAMEWORK-API.md for a dedicated `### <NewInterface>` heading. If absent, flag it AND flag the missing edit to the generator's `targetInterfaces` array — a one-time regeneration won't fix it.

## The generator's TOTAL scope (everything else is legitimately N/A)

It emits exactly five things: (1) `CoreModule.forRoot()` overload signatures, (2) the allowlisted config interfaces, (3) `ServiceOptions`, (4) `CrudService` public method signatures, (5) a core-modules table built by scanning `src/core/modules/*` dirs for the mere *presence* of README.md / INTEGRATION-CHECKLIST.md.

Consequence: **exported `const`s, DI tokens, helpers, guards, middleware and new non-interface files can never appear in FRAMEWORK-API.md.** For such a change, FRAMEWORK-API.md being absent from the diff is CORRECT — do not flag it as a missed Living-Documentation update. Adding a new `.ts` file inside an existing module dir also changes nothing (the table only checks whether the two doc files exist).

## Date-stamp churn is not a doc gap

Line 3 is `> Auto-generated from source code on YYYY-MM-DD (vX.Y.Z)`. Because the date is stamped fresh on every run, re-running the generator on an unchanged API produces a **1-line, date-only diff** — which shows up as a dirty `FRAMEWORK-API.md` in `git status`. That is build churn, not a pending documentation update. Verify with `git diff -- FRAMEWORK-API.md`: if the only hunk is the date line, the API surface is genuinely unchanged. Run it standalone via `npx tsx scripts/generate-framework-api.ts` (it is the last step of `pnpm run build`, so a full build is not needed to check).
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: patch-release-migration-guide-convention
description: nest-server ships a migration guide for EVERY patch release incl. zero-effort internal bugfixes — the observed convention is stricter than the written rule in .claude/rules/migration-guides.md
metadata:
type: project
---

`.claude/rules/migration-guides.md` says a guide is "Not required if purely internal bugfixes with no user action needed". **The repo's actual practice contradicts this**: `migration-guides/` has an unbroken chain for every patch in the 11.27.x line (11.27.0→.1→.2→.3→.4→.5→.6), including guides whose own Overview says *"Migration Effort: 0 minutes (automatic) — `pnpm update` is enough"* and *"No source-code or config changes are required in consuming projects"* (see `11.27.2-to-11.27.3.md`, a pure internal unhandled-rejection fix, and `11.24.0-to-11.24.1.md`).

**Why:** Consumers use the guides as the release-notes surface, not just as migration instructions. A guide for a "silent" bugfix still earns its keep when the bug had a *user-visible symptom* — the reader who googles the crash/error string needs to land on "fixed in vX". Grading "no guide needed, it's just a bugfix" as 100% N/A therefore under-reports.

**How to apply:** When a branch is a pure internal bugfix, do NOT auto-grade the migration-guide dimension as N/A. Ask: (1) did the bug have a symptom a consumer could observe (startup crash, 401, error string)? (2) does the fix require anything of **vendor-mode** consumers, who never run `pnpm update` and instead sync `src/core/` via the `lt-dev:nest-server-core-updater` agent — e.g. a multi-file atomic change where a partial sync breaks the build? If either is yes, a guide is warranted.

**Timing nuance (do not mis-attribute):** release commits are titled `11.27.X: <summary>` and bundle the version bump + migration guide + FRAMEWORK-API regen. Feature/fix branches use conventional commits (`fix(better-auth): …`) and leave `package.json` version alone. So "no version bump / no guide on a fix branch" is *normal*; flag the guide as a **release-time deliverable**, not a branch blocker. See [[framework-api-generator-allowlist]] and [[doc-surfaces-for-config-features]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: review-committed-vs-working-tree
description: Always run `git status` when auditing a PR here — the author's working tree can hold a newer, better revision than the commits, and a doc↔code contradiction can live entirely in that uncommitted delta
metadata:
type: feedback
---

When auditing a branch/PR in nest-server, do NOT audit `git diff <base>...HEAD` alone. **Run `git status --porcelain` first** and audit the committed state and the working tree as two separate things.

**Why:** In the 11.27.7 / `fix/better-auth-di-token-circular-import` audit, the initial `gitStatus` context said "clean" (a stale snapshot), but the tree actually had 6 modified files plus an **untracked** `tsconfig.swc-tdz.json`. The uncommitted delta was a *later, better* revision of the same work: committed HEAD had `check:swc-tdz` = `nest build -b swc` (no `-p`), which builds SWC output into the REAL `dist/` (nest-cli has `deleteOutDir: true`) — the exact footgun the working-tree version's own docblock says it fixed by adding `tsconfig.swc-tdz.json` + a throwaway `dist-swc-tdz/`. Reading only `git diff develop...HEAD` would have reported the broken design; reading only the working-tree files would have missed that the fix was never committed. The keystone file being **untracked** meant one `git commit -a` away from shipping a hard-failing `check` script for everyone.

**How to apply:** At the start of every review: (1) `git status --porcelain`; (2) if dirty, diff working tree vs HEAD per file (`git diff -- <file>`) and read the committed version with `git show HEAD:<file>`; (3) call out untracked files that other committed/uncommitted files reference — a `package.json` script pointing at an untracked config is a guaranteed break. Quote file:line from the version you are actually grading, and say explicitly which one that is. See [[migration-guide-behavior-change-count-trap]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: vendor-mode-atomic-file-set-check
description: Any NEW file under src/core/ that an existing core file imports is a partial-vendor-sync hazard — the migration guide must enumerate EVERY such atomic file set, not just the headline one
metadata:
type: project
---

Vendor-mode consumers copy `src/core/` into their tree and sync via the `lt-dev:nest-server-core-updater` agent, file by file — not via `pnpm update`. So **every new file under `src/core/` that an existing core file imports creates an atomic file set**: if the sync takes the edited importer but not the new leaf, the consumer gets an unresolvable import and a broken build.

**How to apply:** For each branch, list new files under `src/core/` (`git diff <base>...HEAD --diff-filter=A --name-only -- src/core/`) and, for each, grep which existing core files now import it. Every such cluster needs a row in the guide's vendor-mode section. Then re-count the guide's own claim — it has been wrong.

**Why (concrete):** The 11.27.7 guide documented exactly one atomic set (BetterAuth), titled it *"the change is an atomic 4-file set"* while its own table listed **6** files, and omitted two further sets entirely:
- **core helpers:** `id.helper.ts` (new) + `clone.helper.ts` (new) + `db.helper.ts` + `input.helper.ts` + `restricted.decorator.ts` + `config.service.ts` — `db.helper.ts` now does `export { equalIds, … } from './id.helper'`, so taking db.helper without id.helper breaks the build. Identical hazard to the one the guide *did* warn about.
- **filter inputs:** `filter.input.ts` + `combined-filter.input.ts` (the latter is now a re-export shim).

Two independent count/coverage errors in one guide's vendor section — treat this section as high-suspicion by default. Related: [[migration-guide-behavior-change-count-trap]], [[patch-release-migration-guide-convention]].
3 changes: 3 additions & 0 deletions .claude/agent-memory/lt-dev-performance-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

## Project
- [AI Module Performance Profile](ai-module-perf.md) — per-prompt DB query budget + memory characteristics of src/core/modules/ai; what to re-check vs what's already correct.

## Build & Startup
- [SWC/CJS TDZ + CI Gap](swc-cjs-tdz-and-ci-gap.md) — circular-import crashes hit `nest start -b swc` but NOT CI (vitest's unplugin-swc misses it); includes the cycle-triage rule.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: swc-cjs-tdz-and-ci-gap
description: Circular-import TDZ crashes only reproduce under SWC->CJS + Node require (nest start -b swc); vitest's unplugin-swc does NOT cover it, so CI is green while dev crashes. Includes the cycle-triage rule.
metadata:
type: project
---

Circular imports in this repo can crash `nest start -b swc` with
`ReferenceError: Cannot access 'X' before initialization` while **CI stays green**.

**Why:** `nest build` uses **tsc** (`nest-cli.json` has no `"builder": "swc"`), so the
published `dist/` never sees SWC semantics. SWC *is* used in two places — the dev
scripts (`start:dev:swc`, `start:local:swc`) and **vitest** (both `vitest.config.ts`
and `vitest-e2e.config.ts` use `unplugin-swc`). But vitest runs SWC output through
**Vite's module runner** (ESM live bindings via getters, cycle-tolerant), *not*
through Node's CJS `require()` on SWC's emitted CJS. The TDZ only manifests in the
**SWC-CJS + Node-CJS-loader** combination that `nest start -b swc` produces, and
**no CI job runs that**. This is how the `BETTER_AUTH_INSTANCE` TDZ bug shipped to
develop with a green pipeline.

**Cycle-triage rule** — a cycle is only fatal when it carries a **TDZ-subject binding**
(`const` / `class` / `let`) that is **dereferenced during module evaluation**:

| Binding in the cycle | Deref timing | Fatal? |
|---|---|---|
| `const` (e.g. a DI token string) | module-eval, e.g. inside an `@Inject()` **parameter decorator** | **YES** — this was the bug |
| `class` | deferred into a method / factory body | No, but **latent** — moving the deref to a decorator arg or static initializer reintroduces the crash |
| `export function` | any | No — function declarations are **hoisted**, TDZ-immune |
| type-only (`Type<Foo>`, interfaces) | erased at compile | No — not a runtime cycle at all (madge reports these as false positives) |

**How to reproduce/verify a suspected cycle** (cheap, no MongoDB needed):
SWC-compile with `module: { type: "commonjs" }` + legacy decorators + decoratorMetadata,
then plain `node -e "require('<out>/core/modules/<mod>/<file>.js')"`. Throws on a fatal
cycle, silent otherwise. Diff the same require against a `git worktree` of the base branch
to get a clean A/B.

**How to apply:** When reviewing anything that touches the import graph in `src/core/`,
do NOT trust green CI as evidence that the SWC path is safe. Run `madge --circular
--extensions ts src/core/...`, then triage each cycle with the table above — most are
benign (`function` / type-only), so report only cycles carrying `const`/`class` bindings.
Known latent one (pre-existing, NOT yet fixed): `better-auth-roles.guard.ts` ↔
`core-better-auth.module.ts` — a `class` cycle, currently benign only because both
dereference sites sit in deferred function bodies.

Related: [[ai-module-perf]]
7 changes: 6 additions & 1 deletion .claude/agent-memory/lt-dev-security-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
- [feedback-typeof-detection.md](feedback-typeof-detection.md) — typeof detection for CoreModule.forRoot() signature; security classification of Type<any>
- [project-ai-module-secret-stripping.md](project-ai-module-secret-stripping.md) — AI module 3-layer apiKeyEncrypted protection + secretFields override fragility; MCP PKCE enforced by SDK
- [project-ai-mcp-oauth-refresh-token-binding.md](project-ai-mcp-oauth-refresh-token-binding.md) — CoreAiMcpOAuthService.exchangeRefreshToken ignores rotating client_id; cross-client token theft risk when ai.mcp.oauth=true
- [project-betterauth-native-cookie-forwarding.md](project-betterauth-native-cookie-forwarding.md) — BetterAuth native-handler paths forward Set-Cookie verbatim, bypass the cookie helper's Secure flag; useSecureCookies:false (11.27.6) drops Secure on 2FA/social/magic-link session cookies
- [project-betterauth-native-cookie-forwarding.md](project-betterauth-native-cookie-forwarding.md) — BetterAuth's helper vs native-forward cookie paths; SEC-001 ("useSecureCookies:false strips Secure") is FIXED — do not re-report
- [project-betterauth-di-failclosed-and-cycle-triage.md](project-betterauth-di-failclosed-and-cycle-triage.md) — @Optional() auth-instance injection degrades fail-CLOSED (401, not bypass); how to triage madge cycles as TDZ-risky (decorator-arg) vs benign (method-body)

## Review Methodology

- [project-e2e-node-env-trap.md](project-e2e-node-env-trap.md) — e2e without NODE_ENV=e2e fabricates 5 bogus BetterAuth "Invalid credentials" failures; reproduces on base branch too, so a control-diff won't catch it
Loading