Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,3 +1,4 @@
- [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.
- [401/403 permission-denial surface](project_401-403-denial-surface.md) — 5 layers deny permission with inconsistent status codes (S_NO_ONE alone has 3); map all five before grading any auth-status change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
name: 401-403-denial-surface
description: The framework denies permission from 5+ layers with inconsistent 401/403 semantics — S_NO_ONE alone has 3 different status codes; map before reviewing any auth-status change
metadata:
type: project
---

`@lenne.tech/nest-server` denies a request for permission reasons from **five** layers,
and their 401/403 semantics do **not** agree. Anyone reviewing an auth-status change must
map all five, not just the ones in the diff.

| Layer | File | S_NO_ONE | authenticated + lacks right | unauthenticated |
|---|---|---|---|---|
| RolesGuard | `src/core/modules/auth/guards/roles.guard.ts` (:141, :297, :332) | 401 always | 403 | 401 |
| BetterAuthRolesGuard | `src/core/modules/better-auth/better-auth-roles.guard.ts` (:86, :146, :165) | 401 always | 403 | 401 |
| CoreTenantGuard | `src/core/modules/tenant/core-tenant.guard.ts` (:219, :261, :279, :342, :380) | **403 always** | 403 | **403** ("Authentication required" — inverse of RFC 9110) |
| service-layer rights checks | `check()` / `checkRestricted()` / `prepareInput()` | 403 (auth) / 401 (anon) — **since 11.28.0** | 403 | 401 |
| model `securityCheck()` | `src/core/modules/tenant/core-tenant-member.model.ts` (:101, :119) — the only throwing securityCheck in core | n/a | **401** (still) | 401 |

**Why:** PR #559 (11.28.0, `AccessDeniedException`) fixed only the service-layer row. Reviews of
that PR — and of any follow-up — keep having to re-derive this table, which costs a full
cross-file audit each time. Two traps it exposes:

1. **S_NO_ONE has three different status codes** across the layers. Three e2e tests lock the
guard's 401 in place and must be updated together with any change:
`tests/stories/better-auth-rest-security.e2e-spec.ts:738`,
`better-auth-autoregister-false.e2e-spec.ts:219`,
`better-auth-module-registration.e2e-spec.ts:334`.
2. **`.claude/rules/better-auth.md` only requires RolesGuard ↔ BetterAuthRolesGuard to stay in
sync with *each other*.** They do. The divergences are guard-layer ↔ service-layer ↔
securityCheck, which no rule covers — so "both guards agree" is not evidence of coherence.

Also inconsistent: email-verification denial is **403** via `BetterAuthRolesGuard` (S_VERIFIED)
and `CoreTenantGuard` ("Verification required"), but **401** via
`CoreBetterAuthController`/`Resolver` `checkEmailVerification()` (`ErrorCode.EMAIL_VERIFICATION_REQUIRED`).
The 401 is defensible at sign-in (no session yet) but is a landmine for frontends that
auto-logout on 401.

**How to apply:** Before grading any 401/403 change, re-verify this table against current source
(line numbers drift). Judge completeness against *all five* layers — a PR that "centralizes the
401/403 decision" in one layer while leaving the others is an incomplete fix, not a done one.
Related: [[core-errorcode]] — the guards throw translatable `ErrorCode.ACCESS_DENIED`, the
service layer throws raw English strings, for the same logical denial.
3 changes: 3 additions & 0 deletions .claude/agent-memory/lt-dev-docs-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
- [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
- [FRAMEWORK-API generator blind spots](framework-api-generator-allowlist.md) — FRAMEWORK-API.md only documents allowlisted config interfaces and has NO exceptions/classes concept; new public exports are silently omitted
- [Release version artifacts](release-version-artifacts.md) — a version bump must touch package.json AND spectaql.yml; the lockstep is a git-history convention, written in no rule doc
- [Migration-guide self-description traps](migration-guide-behavior-change-count-trap.md) — never trust a guide's Overview counts or its "aligns with existing pattern X" claims; derive both from the source diff
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
---
name: migration-guide-behavior-change-count-trap
description: When reviewing a nest-server migration guide, enumerate behavior changes from the better-auth.config.ts / cookies.helper.ts diff directly — the guide's Overview counts have under-reported before
description: Never trust a nest-server migration guide's own Overview counts or its "this aligns with existing pattern X" claims — derive both from the source diff and from reading every branch of X
metadata:
type: feedback
---

When assessing whether a `migration-guides/X-to-Y.md` is COMPLETE, do NOT trust the guide's own Overview "Bugfixes"/"Behavior Changes" counts. Derive the true list of consumer-visible behavior changes straight from the framework-source diff — especially `src/core/modules/better-auth/better-auth.config.ts` and `src/core/common/helpers/cookies.helper.ts`, which are large files where multiple independent behavior changes land in one release and are easy to miss.
When assessing whether a `migration-guides/X-to-Y.md` is COMPLETE and ACCURATE, do not trust the guide's self-description. Two traps, both confirmed by real reviews:

**Why:** In the 11.27.5→11.27.6 review, the guide documented 3 behavior changes and 4 bugfixes but the code had ≥5: it omitted the `advanced.useSecureCookies: false` pin entirely (a BetterAuth 2FA/passkey/`/token` 401 split-brain fix WITH an opt-out via `betterAuth.options.advanced.useSecureCookies`) and only mentioned the `deriveCookieDomainFromUrls()` `api.dev` bare-TLD guard as an export aside, not as a behavior change. Each of these had its own regression test (`tests/unit/better-auth-secure-cookies.spec.ts`) — so a fast completeness cross-check is: every new `tests/unit/*secure*`/behavior spec should map to a documented guide entry.
**Trap 1 — Overview counts under-report.** Derive the true list of consumer-visible behavior changes straight from the framework-source diff, especially `src/core/modules/better-auth/better-auth.config.ts` and `src/core/common/helpers/cookies.helper.ts`, where several independent behavior changes land in one release. In the 11.27.5→11.27.6 review the guide claimed 3 behavior changes / 4 bugfixes but the code had ≥5: it omitted the `advanced.useSecureCookies: false` pin entirely (a 2FA/passkey/`/token` 401 split-brain fix WITH an opt-out) and demoted the `deriveCookieDomainFromUrls()` bare-TLD guard to an export aside. Fast cross-check: every new `tests/unit/*` behavior spec should map to a documented guide entry.

**How to apply:** For each `+`-added behavior in the config/helper diffs, grep the guide for a matching keyword (e.g. `useSecureCookies`, `__Secure`, the changed function name). A behavior change present in code but absent from the guide's "Behavior changes" section AND its opt-out is a High-priority finding. Also re-check the guide's numeric counts ("Three consumer-visible shifts") against the actual count. See [[doc-surfaces-for-config-features]] for the full doc-surface set.
**Trap 2 — "aligns with existing pattern X" claims are only spot-checked.** When a guide justifies a breaking change as *"we are just aligning with what X already did"*, read EVERY branch of X, not the one the guide quotes. In the 11.27.6→11.28.0 review the guide claimed the new service-layer 401/403 split "aligns with the existing `RolesGuard` pattern" and listed RolesGuard under "What is NOT affected". True for the role-mismatch branch (`!user` → 401, user-lacks-role → 403) — but **false for the `S_NO_ONE` branch**: `roles.guard.ts` and `better-auth-roles.guard.ts` throw `UnauthorizedException` (401) for `S_NO_ONE` even when authenticated, while `core-tenant.guard.ts` throws `ForbiddenException` (403) and the newly changed `check()` now also throws 403. So the release *created* a three-way split while the guide asserted alignment.

Related sub-trap: check whether the aligned-with pattern also carries an **ErrorCode** that the new code drops. RolesGuard throws `ForbiddenException(ErrorCode.ACCESS_DENIED)` (`LTNS_0101`, has a German translation); the new `AccessDeniedException` sites throw raw strings with no `#LTNS_xxxx:` marker, so `useLtErrorTranslation()` cannot translate them — while the guide simultaneously says "ErrorCodes — unchanged" and recommends the error-translation layer as the remedy.

**How to apply:** For each `+`-added behavior in the diff, grep the guide for a matching keyword. A behavior change present in code but absent from the guide's "Behavior changes" section AND its opt-out is a High-priority finding. For every "already behaves like X" / "unchanged" claim, open X and enumerate its branches. See [[doc-surfaces-for-config-features]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: release-version-artifacts
description: A release bump must update BOTH package.json and spectaql.yml (info.version) — the lockstep is a git-history convention only, it is written in no rule doc
metadata:
type: project
---

Two files carry the framework version and must be bumped together in a release commit:

1. `package.json` → `"version"`
2. `spectaql.yml` → `info.version` (feeds the published GraphQL API docs site)

**Why:** `.claude/rules/versioning.md` "Release Process" lists only *"Update version in `package.json`"* — `spectaql.yml` is never mentioned, and neither is it in CLAUDE.md's Living-Documentation table. The lockstep exists purely as a git-history convention: 7 of the last 8 release commits before 11.28.0 (83b59e1, 912896a, e17fdd1, beda830, 48e58f3, c9a6477, 8d7a7be) bumped `spectaql.yml` in the same commit as `package.json`. Because no doc states it, it is reliably forgotten — the 11.28.0 PR shipped `package.json` at 11.28.0 while `spectaql.yml` still said 11.27.6, which would publish API docs advertising the wrong version.

**How to apply:** On any branch that changes `package.json`'s version, immediately run
`grep -n version spectaql.yml` and compare. If they disagree, that is a High-priority finding.
`FRAMEWORK-API.md`'s version line is NOT a third artifact to bump by hand — it is regenerated by `pnpm run build` (see [[framework-api-generator-allowlist]]). There is no CHANGELOG.md in this repo, and no `migration-guides/README.md` index, so those are never part of a version bump.
2 changes: 2 additions & 0 deletions .claude/agent-memory/lt-dev-security-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@
## 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
- [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-exception-wire-format.md](project-exception-wire-format.md) — HttpExceptionLogFilter sends `{...exception}` (class `name` is client-visible); `extends HttpException` breaks instanceof vs native Forbidden/Unauthorized
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: project-exception-wire-format
description: HttpExceptionLogFilter serializes {...exception} for REST, so a custom HttpException subclass changes the wire body (name/options) and breaks instanceof against native Nest exceptions
metadata:
type: project
---

Two non-obvious traps when reviewing any new/changed exception class in nest-server.

**1. The REST wire body is `{ ...exception }`, not `getResponse()`.**
`src/core/common/filters/http-exception-log.filter.ts` does
`res.status(status).json({ ...exception })`, and `src/main.ts` registers it via
`useGlobalFilters()` (nest-server-starter mirrors this). The spread yields
`{ response, status, options, message, name }` — so **`name` (the exception class
name) is client-visible on every REST error**. A new subclass silently changes the
wire format: `name: "AccessDeniedException"` instead of `"ForbiddenException"`, and
`options` disappears when the 3rd `super()` arg is omitted. GraphQL is unaffected —
it surfaces `getResponse()` under `extensions.originalError`.

**2. `extends HttpException` is `instanceof` NEITHER `ForbiddenException` NOR
`UnauthorizedException`.**
Verified empirically. A subclass that picks its status at runtime (e.g.
`AccessDeniedException(user)` → 403/401) therefore breaks every downstream
`instanceof UnauthorizedException` / `instanceof ForbiddenException` check and every
`@Catch(ForbiddenException)` filter — including authz-denial audit logging (OWASP
A09) in consumer projects. Framework-internal catch sites that key on this:
`core-better-auth.controller.ts:456` + `:938`, `core-system-setup.service.ts:112/217`,
`core-ai-prompt.service.ts:158`.

**Why:** found on `fix/403-for-permission-errors` (PR #559, 11.28.0). The PR's own
unit spec asserted `getResponse()` body parity with the native exceptions — which
passes — so the `instanceof` divergence went undetected, and the shipped migration
guide told consumers to "match `ForbiddenException` instead", which does not work.

**How to apply:** when a diff adds or changes an exception class, always check
(a) `instanceof` against the native Nest exception it replaces, and (b) the
`{ ...exception }` spread shape, not just `getResponse()`. Prefer a factory returning
the **native** exceptions over a new `HttpException` subclass when the only goal is
picking between 401 and 403. See also [[project-betterauth-native-cookie-forwarding]]
for another "framework serializes something verbatim" trap in this repo.
2 changes: 1 addition & 1 deletion .claude/agent-memory/lt-dev-test-reviewer/MEMORY.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- [E2E Test Isolation Model](e2e-isolation-model.md) — how the Vitest e2e suite shares one MongoDB across parallel forks and what that means for collection-wide deleteMany in tests.
- [E2E Test Isolation Model](e2e-isolation-model.md) — per-RUN unique DB but ONE DB shared across files; the `jwks` deleteMany landmine that makes "flaky 401s" a deterministic cross-file bug (pair-run to reproduce, don't blame load).
- [ConfigService Singleton in Tests](configservice-singleton-in-tests.md) — per-file fork isolation makes static ConfigService safe between files; within a file mergeConfig (lodash merge) means arrays don't clear via [] — check test ORDER, not "reset".
- [AI Module Test Coverage](ai-module-test-coverage.md) — coverage gaps observed in the AI module review (OAuth 2.1 stores, MCP HTTP session lifecycle, sessionStart/stop hooks, search_tools/ask_user_question execute bodies); brittle regex-on-message assertions caused by mixed raw-string vs ErrorCode throws.
- [Vitest Is Blind to SWC/CJS TDZ](vitest-blind-to-swc-cjs-tdz.md) — vitest (even with unplugin-swc) can NEVER catch import-cycle TDZ crashes; only an SWC→CJS build+require does. Plus: 9 pre-existing cycles, so "zero cycles" tests are a non-starter.
Loading