@lenne.tech/nest-server 12.2.0 → 12.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.claude/rules/better-auth.md +31 -4
  2. package/.claude/rules/configurable-features.md +8 -7
  3. package/.claude/rules/package-management.md +18 -2
  4. package/.claude/rules/persistence.md +2 -2
  5. package/.claude/rules/testing.md +5 -5
  6. package/.claude/rules/versioning.md +13 -2
  7. package/CLAUDE.md +5 -0
  8. package/FRAMEWORK-API.md +4 -3
  9. package/README.md +2 -2
  10. package/dist/config.env.js +2 -2
  11. package/dist/config.env.js.map +1 -1
  12. package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
  13. package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
  14. package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
  15. package/dist/core/common/interceptors/check-security.interceptor.js +19 -2
  16. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  17. package/dist/core/common/interfaces/server-options.interface.d.ts +8 -0
  18. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  19. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  20. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  21. package/dist/core/modules/ai/core-ai.constants.d.ts +3 -0
  22. package/dist/core/modules/ai/core-ai.constants.js +3 -0
  23. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  24. package/dist/core/modules/ai/core-ai.controller.d.ts +9 -0
  25. package/dist/core/modules/ai/core-ai.controller.js +48 -1
  26. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  27. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  28. package/dist/core/modules/ai/core-ai.module.js +9 -1
  29. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  30. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  31. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  32. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  33. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  34. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  35. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  36. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  37. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  38. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  39. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  40. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  41. package/dist/core/modules/ai/index.d.ts +8 -0
  42. package/dist/core/modules/ai/index.js +7 -0
  43. package/dist/core/modules/ai/index.js.map +1 -1
  44. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  45. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  46. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  47. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  48. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  49. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  50. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  51. package/dist/core/modules/ai/models/core-ai-status.model.d.ts +3 -0
  52. package/dist/core/modules/ai/models/core-ai-status.model.js +25 -0
  53. package/dist/core/modules/ai/models/core-ai-status.model.js.map +1 -0
  54. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  55. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  56. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  57. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  58. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  59. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  60. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  61. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  62. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  63. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  64. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  65. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  66. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  67. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  68. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  69. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  70. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  71. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  72. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  73. package/dist/core/modules/better-auth/better-auth.config.js +10 -4
  74. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  75. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  76. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  77. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  78. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  79. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  80. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  81. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  82. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  83. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  84. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  85. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  86. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  87. package/dist/core/modules/better-auth/index.d.ts +1 -0
  88. package/dist/core/modules/better-auth/index.js +1 -0
  89. package/dist/core/modules/better-auth/index.js.map +1 -1
  90. package/dist/core/modules/error-code/error-codes.d.ts +18 -0
  91. package/dist/core/modules/error-code/error-codes.js +16 -0
  92. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  93. package/dist/core/modules/sync/core-sync.service.d.ts +1 -0
  94. package/dist/core/modules/sync/core-sync.service.js +31 -2
  95. package/dist/core/modules/sync/core-sync.service.js.map +1 -1
  96. package/dist/core/modules/tenant/core-tenant.guard.js +10 -0
  97. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  98. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  99. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  100. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  101. package/dist/server/modules/error-code/error-codes.d.ts +2 -0
  102. package/dist/test/test.helper.d.ts +1 -0
  103. package/dist/test/test.helper.js +9 -2
  104. package/dist/test/test.helper.js.map +1 -1
  105. package/docs/REQUEST-LIFECYCLE.md +5 -4
  106. package/docs/error-codes.md +14 -0
  107. package/docs/offline-sync-protocol.md +17 -8
  108. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  109. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  110. package/migration-guides/12.3.0-to-12.3.1.md +136 -0
  111. package/package.json +26 -26
  112. package/src/config.env.ts +6 -2
  113. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  114. package/src/core/common/interceptors/check-security.interceptor.ts +26 -2
  115. package/src/core/common/interfaces/server-options.interface.ts +84 -8
  116. package/src/core/common/persistence/sync-model.ts +1 -1
  117. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  118. package/src/core/modules/ai/README.md +178 -25
  119. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  120. package/src/core/modules/ai/core-ai.constants.ts +10 -0
  121. package/src/core/modules/ai/core-ai.controller.ts +55 -1
  122. package/src/core/modules/ai/core-ai.module.ts +14 -0
  123. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  124. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  125. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  126. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  127. package/src/core/modules/ai/index.ts +19 -0
  128. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  129. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  130. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  131. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  132. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  133. package/src/core/modules/ai/models/core-ai-status.model.ts +16 -0
  134. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  135. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  136. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  137. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  138. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  139. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  140. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  141. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  142. package/src/core/modules/api-token/README.md +4 -2
  143. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  144. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -2
  145. package/src/core/modules/better-auth/README.md +32 -1
  146. package/src/core/modules/better-auth/better-auth.config.ts +21 -5
  147. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  148. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  149. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  150. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  151. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  152. package/src/core/modules/better-auth/index.ts +1 -0
  153. package/src/core/modules/error-code/error-codes.ts +18 -0
  154. package/src/core/modules/sync/README.md +11 -1
  155. package/src/core/modules/sync/core-sync.service.ts +48 -3
  156. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  157. package/src/core/modules/tenant/README.md +49 -3
  158. package/src/core/modules/tenant/core-tenant.guard.ts +18 -2
  159. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  160. package/src/test/test.helper.ts +17 -3
@@ -0,0 +1,440 @@
1
+ # Migration Guide: 12.2.x → 12.3.x
2
+
3
+ ## 12.3.0 contains breaking changes
4
+
5
+ The version number moves only the MINOR digit (the MAJOR follows NestJS). Read §1–§3, §7 and §8 before you update: none
6
+ of them needs code in a typical project, but each one changes what a running application does.
7
+
8
+ ## Overview
9
+
10
+ | Category | Details |
11
+ |---|---|
12
+ | **Breaking Changes** | E-mail verification no longer signs the user in (§1). A prompt continues only the caller's own AI conversation, `CoreAiConversationService.appendMessage()` takes the owner and the protected `CoreAiService.persistTurn()` the run as a third parameter (§2). Non-canonical paths under the Better-Auth base path answer 400, and the IAM rate limit tells routes apart, with half the limit on sign-in and sign-up (§3). `emailAndPassword.disableSignUp: true` closes social sign-up too (§7). `AiTool.authorize()` runs on every path, and MCP asks before a destructive tool or refuses a client that cannot ask (§8) |
13
+ | **New Features** | `socialProviders.<name>.disableSignUp` opens or closes sign-up for one provider (§7). `ai.mcp.confirmation` and a per-user switch (`GET/PUT /ai/settings`), `context.authorization` for tools, `destructiveHint` annotations in `tools/list` (§8). `ai.nativeToolCatalog`, a warning for a crowded context window (§9). `ai.mcp.stateless`, consent page `texts` / `styles` (§10). `CoreTenantService.purgeTenantData()` and `deleteAllMembers()` remove a tenant's data (§11) |
14
+ | **Bugfixes** | Login CSRF through a forwarded verification link (§1), turns written into another user's AI conversation (§2), rate limiter bypass through dot segments and an IAM rate limit that never told routes apart over HTTP (§3), a planned end of the SSE hint stream logged as a lost session (§4), a sync push that applied a change chained to a refused one in the same request on another writer's version (§5), an open redirect through `/authorize` of the MCP OAuth endpoints (§6), new accounts through social login while sign-up was switched off (§7), tool checks and admin tool policies that MCP and auto mode skipped (§8), tool descriptions sent twice to native connections and native tool definitions missing from the context budget (§9), cached memberships that kept a deleted tenant reachable for up to `cacheTtlMs` after `invalidateTenant()` (§11) |
15
+ | **Dependencies** | `@nestjs/graphql` 14.1.0 → 14.1.2, `@nestjs/apollo` 14.1.1 → 14.1.2, `@nestjs/swagger` 12.0.5 → 12.1.0, `@tus/server` 2.4.6 → 2.4.7, `@tus/file-store` 2.1.1 → 2.1.2, `jose` 6.2.13 → 6.2.15, `nodemailer` 10.3.0 → 10.3.1. Tested optional peers raised: `bullmq` 6.5.1 → 6.5.4, `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` 3.1159.0 → 3.1161.0, `@tus/s3-store` 2.0.9 → 2.1.0, `kysely` 0.29.7 → 0.29.8 — `pnpm run update` raises them where a project pins them. Development: `vitest` 5.0.5 → 5.1.0 together with `vite` 8.3.4 → 8.3.5 (Dependencies) |
16
+ | **Migration Effort** | Usually none. A frontend whose verify page expects a session: one redirect. A project calling `appendMessage()` or overriding `persistTurn()`: one argument. A deployment with a tight `betterAuth.rateLimit.max`: check the value (§3). A project that closed sign-up but wants new users through a social provider: one flag on that provider (§7). A project using destructive AI tools over MCP: check the clients, offer users the switch (§8) |
17
+
18
+ ## Quick Migration
19
+
20
+ ```bash
21
+ pnpm add @lenne.tech/nest-server@12.3.0
22
+ pnpm run update # nest-server-starter based projects: aligns the pins the framework raised
23
+ pnpm run build
24
+ pnpm test
25
+ ```
26
+
27
+ **Vendor mode**: run `/lt-dev:backend:update-nest-server-core` as usual.
28
+
29
+ ### Dependencies
30
+
31
+ `pnpm run update` raises the pins a project shares with the framework: `@nestjs/graphql`, `@nestjs/apollo` and
32
+ `@nestjs/swagger` (patch and minor releases of NestJS, no API change), the optional peers it tests against, and the
33
+ development tooling. Nothing else in a project changes.
34
+
35
+ **`vite` may leave 8.3.4 now.** The 12.0.x guide (§12) asked to keep `vite` at 8.3.4 while `vitest` was 5.0.5: that
36
+ combination crashed a run with a `globalSetup` or an own reporter after every test was green (`code.charCodeAt is not a
37
+ function`, vitest-dev/vitest#11682). `vitest` 5.1.0 carries the fix. Raise both together — `vite` 8.3.5 with `vitest`
38
+ below 5.1.0 still crashes.
39
+
40
+ ## 1. Auto sign-in after e-mail verification is off by default
41
+
42
+ `betterAuth.emailVerification.autoSignInAfterVerification` defaulted to `true`; it is now `false`, which is
43
+ Better-Auth's own default.
44
+
45
+ With the option on, `GET /iam/verify-email` creates a session for the account the link belongs to in whatever browser
46
+ opens the link. A verification link travels by mail, so it can be forwarded: someone who signs up with their own
47
+ address and sends the unopened link to someone else has that person's browser signed in to the sender's account, and
48
+ what the recipient enters there (payment data, documents, messages) lands with the sender (login CSRF).
49
+
50
+ **What changes:** after clicking the link the user is verified but not signed in, and signs in once.
51
+ `CoreBetterAuthEmailVerificationService.shouldAutoSignIn()` answers `false` accordingly.
52
+
53
+ **What to check:** a frontend whose verify page assumes a session afterwards should send the user to the sign-in, for
54
+ example with a "verified, please sign in" message. The nuxt-base-starter's verify page already does ("Jetzt
55
+ anmelden").
56
+
57
+ **To keep the previous behaviour** — only where a verification link reaches nobody but its owner:
58
+
59
+ ```typescript
60
+ // config.env.ts
61
+ betterAuth: {
62
+ emailVerification: {
63
+ autoSignInAfterVerification: true,
64
+ },
65
+ },
66
+ ```
67
+
68
+ Only the boolean `true` switches it on (`NSC__*` variables turn `true` into a boolean). Any other value — the string
69
+ `"true"` in `NEST_SERVER_CONFIG`, `1`, `yes` — is read as off, in what Better-Auth receives and in what
70
+ `shouldAutoSignIn()` answers alike.
71
+
72
+ ## 2. A prompt continues only the caller's own AI conversation
73
+
74
+ `POST /ai/prompt`, `POST /ai/stream` and the GraphQL `aiPrompt` accepted any `conversationId` and wrote the new turns
75
+ into that conversation, also when it belonged to another user. Now a `conversationId` the caller did not create —
76
+ another user's, or one that does not exist — counts as no conversation: the run reads no history, writes nothing, uses
77
+ no conversation-scoped tool grants, and its response carries no `conversationId`. **Administrators are no exception**:
78
+ they still read every conversation (`getAiConversation`, `findAiConversations` with `all`), but continue only their
79
+ own.
80
+
81
+ `CoreAiConversationService.appendMessage()` checks the owner itself and has a third parameter:
82
+
83
+ **Before:**
84
+ ```typescript
85
+ await this.conversationService.appendMessage(conversationId, { content, role: 'user' });
86
+ ```
87
+
88
+ **After:**
89
+ ```typescript
90
+ // The id of the user the turn belongs to; anyone else's turn is not written.
91
+ await this.conversationService.appendMessage(conversationId, { content, role: 'user' }, currentUser.id);
92
+ ```
93
+
94
+ A call without it fails to compile (TS2554). A subclass that overrides `appendMessage()` and does not call `super`
95
+ must check the owner itself — `isOwnedBy(id, userId)` is the new public helper for it.
96
+
97
+ The protected `CoreAiService.persistTurn()`, which calls `appendMessage()`, takes the run as a third parameter for the
98
+ same reason — the owner comes from `run.currentUser`:
99
+
100
+ ```typescript
101
+ // Before: protected async persistTurn(input, response)
102
+ protected override async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse, run: AiRunContext) {
103
+ // … your own steps …
104
+ await super.persistTurn(input, response, run);
105
+ }
106
+ ```
107
+
108
+ An override that still calls `super.persistTurn(input, response)` fails to compile (TS2554).
109
+
110
+ ## 3. Requests under the Better-Auth base path
111
+
112
+ ### Non-canonical paths answer 400
113
+
114
+ Every check under the base path (`/iam` by default) — rate limiter, controller routes, the forwarding to Better-Auth —
115
+ read the path as sent, while Better-Auth routes the path after `new URL()` resolved it. `/iam/callback/../sign-in/email`
116
+ therefore passed the rate limiter as a skipped OAuth callback and ran as a sign-in.
117
+
118
+ `CoreBetterAuthPathGuardMiddleware` now runs first under the base path and answers **400** (`#LTNS_0305: Invalid
119
+ request path`) for a path with a `.` or `..` segment (also `%2e`-encoded), a backslash, an encoded `/` or `\`, or a
120
+ `#`. No Better-Auth route needs any of them; a client that sends one has a bug. It is registered by
121
+ `CoreBetterAuthModule` itself — nothing to add.
122
+
123
+ ### The IAM rate limit tells routes apart
124
+
125
+ The rate-limit middleware read `req.path`, which is `/` for a middleware Nest mounts under the base path. So every IAM
126
+ request of a client shared one counter with the full limit, the skip list (`/session`, `/callback`) and the halved
127
+ limit for sign-in, sign-up and the password routes never applied over HTTP.
128
+
129
+ Now a client IP has one counter per route NAME — the last path segment — so `/sign-in/email` and `/sign-up/email` (both
130
+ `email`) share one counter. `/session` and `/callback` are not counted, and the strict routes (sign-in, sign-up, the
131
+ password reset and change routes) get half of `betterAuth.rateLimit.max`, rounded up. The skip and strict lists match
132
+ whole path segments, case-insensitively, as Express routes.
133
+
134
+ **What to check:** a deployment that set `betterAuth.rateLimit.max` (the nest-server-starter reads it from
135
+ `IAM_RATE_LIMIT_MAX`) gets half of it for the e-mail sign-in and sign-up now, and the two count together — with
136
+ `max: 10`, five attempts per window for both combined. Behind a shared NAT or proxy, sign-ups use up the same budget as
137
+ sign-ins. Raise the value if that is too tight. A single-page app that polls `/iam/session` no longer uses up the
138
+ limit.
139
+
140
+ ## 4. SSE hint stream: a planned end logs on debug
141
+
142
+ `GET /sync/hints` ends its stream on every re-check (`graphQl.subscriptionRevalidationMs`) and the client reconnects.
143
+ Every such planned end logged `Closing GraphQL WebSocket of user …: Session no longer valid` at log level. It now logs
144
+ `Closing SSE hint stream of user …` on debug; an end an invalidation causes (role change, sign-out) stays at log level.
145
+
146
+ `registerWsConnection()` takes two optional fields for a long-lived channel of a project's own: `channel` (the name in
147
+ the log line, default `GraphQL WebSocket`) and `endsOnRecheck` (every re-check ends the connection). GraphQL WebSockets
148
+ log as before.
149
+
150
+ ## 5. Sync push: a change chained to a refused one is held back
151
+
152
+ A client sends the changes of one record with chained base versions, also in one push (protocol §6.2). The server
153
+ applied every item on its own: when the first item conflicted with another writer's version 2, the second — base
154
+ `2` — matched that version and was stored on data the client never saw, while the client dropped it locally.
155
+
156
+ Once an item of a record is not stored (`conflict`, `rejected`, `not_found`, `retry`), the later items of that record
157
+ in the same push are now held back: answered `retry` behind a `retry`, otherwise `conflict` with the current record
158
+ (`not_found` when the caller may not see one). Items of other records are unaffected.
159
+
160
+ The server holds back within one request only. Protocol §11 now says what a client does across requests: it sends a
161
+ later change of a record only in the same request as the change before it, or after that one came back `applied` or
162
+ `merged`, and drops or rebases the later changes of a record whose change was `rejected` or `not_found`. **Check your
163
+ client** if it splits its queue into several requests (the protocol caps a push at
164
+ `sync.push.maxItems`, 100 by default): after a `retry` it must keep the record's later changes back until the next
165
+ sync. A client that sent a single change per record and request to avoid the defect may send chains in one request
166
+ again; across requests it still waits for each answer.
167
+
168
+ ## 6. MCP OAuth: no error redirect to a self-registered URI
169
+
170
+ Only with `ai.mcp.oauth: true`. Dynamic client registration accepts any redirect URI, and the MCP SDK sent an error
171
+ it found after the client check (a missing `code_challenge`, a wrong `response_type`) to that URI with a 302. A client
172
+ registered with a foreign URI turned one crafted `/authorize` link into a redirect from the API's own domain to any
173
+ site. Such an error is now answered with `400` and `{ error, error_description }`. Registration stays open, so
174
+ claude.ai, ChatGPT, Cursor and Claude Code connect as before; the user's Allow and Deny on the consent page still
175
+ redirect.
176
+
177
+ The built-in Deny now needs the consent form's CSRF token, as Allow always did; a Deny without it shows the form again.
178
+ A consent UI of your own (`authorizeConsent()` override) should check its token on Deny too — see the AI module's
179
+ INTEGRATION-CHECKLIST.
180
+
181
+ ## 7. Social login creates no account while sign-up is switched off
182
+
183
+ Only with `betterAuth.emailAndPassword.disableSignUp: true` and at least one social provider. The flag closed
184
+ `/sign-up/email` only. Better-Auth decides a social sign-up per provider, and each provider got only its `clientId` and
185
+ `clientSecret`, so whoever signed in with Google, GitHub etc. for the first time got an account in a project that had
186
+ closed self-registration (accounts only through an admin or an invitation).
187
+
188
+ **What changes:** every configured provider now receives the flag. A first sign-in through a provider by somebody who
189
+ has no account is refused (the OAuth callback redirects with `error=signup_disabled`, `/sign-in/social` with an
190
+ `idToken` answers 401 `signup disabled`). Users who already have an account keep signing in, also through a provider
191
+ they have not used before: Better-Auth checks the flag only when no user exists for the address.
192
+
193
+ **What to check:** a project that closed sign-up on purpose needs nothing. A project that closed e-mail sign-up but
194
+ wants new users through a provider opens that one deliberately:
195
+
196
+ ```typescript
197
+ // config.env.ts
198
+ betterAuth: {
199
+ emailAndPassword: { disableSignUp: true },
200
+ socialProviders: {
201
+ google: { clientId: '...', clientSecret: '...', disableSignUp: false }, // new users through Google
202
+ github: { clientId: '...', clientSecret: '...' }, // follows disableSignUp: closed
203
+ },
204
+ },
205
+ ```
206
+
207
+ `disableSignUp: true` on a provider closes sign-up for that provider alone, while sign-up stays open elsewhere. Only a
208
+ boolean counts there: any other value, such as the string `"false"` in `NEST_SERVER_CONFIG`, follows
209
+ `emailAndPassword.disableSignUp`. `GET /iam/features` keeps reporting `signUpEnabled` from the global flag. Plugins a
210
+ project registers itself through `betterAuth.options.plugins` (Generic OAuth, Magic Link, …) are not covered; set their
211
+ own sign-up options.
212
+
213
+ ## 8. AI tools: `authorize()` on every path, confirmation over MCP
214
+
215
+ Only with the `ai` module. Two gates held in the chat's plan mode alone:
216
+
217
+ - **`AiTool.authorize()`** ran in plan mode only. Auto mode (the default) and `/ai/mcp` called `execute()` directly,
218
+ so a data-level check a tool placed there ("may this user change record 42?") did not run for most callers.
219
+ - **`destructive` / `mutating`** were read by the chat only. Over MCP a destructive tool ran on the first call, and an
220
+ admin's `aiToolPolicies` rules (`deny`, `ask`) did not apply at all.
221
+
222
+ **What changes:**
223
+
224
+ - `authorize()` runs before every execution — auto mode, plan mode (for the whole plan, and again per step), MCP — on
225
+ the arguments that will run, after the PreToolUse hooks. A thrown `authorize()`, a missing answer or an `allowed`
226
+ that is not exactly `true` refuses the call. The model receives `TOOL_NOT_AVAILABLE` with the `reason` (keep it
227
+ free of anything the user may not know); a refusal is no longer a learning signal. Return `data` to hand what the
228
+ check loaded to `execute()` as `context.authorization.data`.
229
+ - `tools/call` applies the admin's tool policies (`deny` refuses, `ask` asks), then `authorize()`, then asks the user
230
+ through **MCP elicitation** before a `destructive` tool, and before a `mutating` one when `ai.confirmation.mutating`
231
+ says so (`enforced`, else `default`). Only an explicit yes runs the tool. **A client without the `elicitation`
232
+ capability gets an error result for such a tool.**
233
+ - Each user may switch the question off for themselves, like a "bypass permissions" mode: `PUT /ai/settings` with
234
+ `{ "mcpConfirmation": false }` (GraphQL `updateAiSettings`), read with `GET /ai/settings` (`aiSettings`). The choice
235
+ is stored per user in the new `aiUserSettings` collection / table, created at boot like every AI model.
236
+ `authorize()` and the admin's `deny` / `ask` rules apply whatever the user chose.
237
+ - `tools/list` carries `annotations` (`destructiveHint`, `readOnlyHint: false`) for `destructive` and `mutating`
238
+ tools. A tool with neither flag carries none, so it is never advertised as read-only.
239
+
240
+ **What to check:**
241
+
242
+ 1. **Tools with `authorize()`:** it now runs in auto mode and over MCP too. A check written for plan mode only (for
243
+ example one that refuses whatever it does not expect) refuses those callers from now on. A tool without
244
+ `authorize()` is unchanged.
245
+ 2. **MCP clients:** whether yours support elicitation. Where a client you need cannot ask, choose the default for the
246
+ deployment:
247
+
248
+ ```typescript
249
+ // config.env.ts
250
+ ai: {
251
+ mcp: {
252
+ confirmation: true, // default: asks, each user may switch it off
253
+ // confirmation: false, // asks nobody, each user may switch it on
254
+ // confirmation: { default: true, enforced: true } // asks everybody, nobody may switch it off (403 #LTNS_0609)
255
+ },
256
+ },
257
+ ```
258
+
259
+ Only `false` / `default: false` switch the default off and only `enforced: true` enforces; any other value keeps
260
+ asking.
261
+ 3. **Your frontend:** offer the switch where users manage their AI settings — `mcpConfirmationEnforced: true` in the
262
+ answer means it is fixed and should be shown disabled.
263
+
264
+ **API changes:** `CoreAiMcpService.mcpCallTool()` takes an optional fourth parameter (`{ confirm }`), its constructor
265
+ two optional services (`CoreAiUserSettingService`, `CoreAiToolPolicyService`). `CoreAiController` and
266
+ `CoreAiResolver` receive `CoreAiUserSettingService` as an injected property, so a project subclass keeps its
267
+ constructor. `CoreModule.forRoot(env, { ai: { userSettingService } })` replaces the new store.
268
+
269
+ ## 9. AI: names-only tool catalog for native connections, a context window that counts the tools
270
+
271
+ Only with the `ai` module and a connection that uses native tool calling (`supportsNativeTools: true`, detected
272
+ automatically).
273
+
274
+ **What changes:**
275
+
276
+ - The system prompt's tool catalog lists **tool names only** for such a connection in auto mode. Descriptions and
277
+ parameter schemas reach the model through the native `tools` payload as before. Until now the catalog repeated them,
278
+ so every tool was sent twice. Emulated connections and plan mode keep the full catalog.
279
+ - The native tool definitions count against the context budget, so older turns are summarized or dropped before the
280
+ window overflows rather than after.
281
+ - When the system prompt and the tool definitions take more than half of a connection's input budget, the framework
282
+ logs one warning per connection and process. It names the numbers and says whether the window is the connection's
283
+ own or the `ai.contextWindow` fallback. Until now the only trace was a debug line, and the user's message was cut
284
+ without notice.
285
+
286
+ **What to check:**
287
+
288
+ 1. Nothing for most projects. A model that picks tools noticeably worse without the descriptions in the prompt gets
289
+ the old catalog back:
290
+
291
+ ```typescript
292
+ // config.env.ts
293
+ ai: {
294
+ nativeToolCatalog: 'full', // default 'names'
295
+ },
296
+ ```
297
+
298
+ 2. A project test that guards the prompt size: it measures against the `ai.contextWindow` fallback, so it guards
299
+ connections without a window of their own. Keep it, and name it that way. Raising its limit removes the guard.
300
+ 3. `deferToolSummaryChars` never applied to native connections in auto mode and still does not. It applies to emulated
301
+ connections and plan mode.
302
+
303
+ ## 10. MCP without sessions, and the consent page's wording and look
304
+
305
+ Both are opt-in; nothing changes without configuration.
306
+
307
+ **`ai.mcp.stateless: true`** serves `/ai/mcp` without sessions.
308
+ - Every POST builds its own server and transport for the authenticated user, and no `mcp-session-id` is issued.
309
+ - A session id a client still sends is ignored. GET and DELETE answer `405` with `Allow: POST`.
310
+ - Any replica answers any request, so `/ai/mcp` no longer needs sticky sessions behind a load balancer.
311
+ - There are no server-initiated requests in this mode, so the server cannot ask the user (§8). A tool that needs
312
+ confirmation is refused unless the user switched the question off (`PUT /ai/settings`) or
313
+ `ai.mcp.confirmation` is `false`.
314
+ - `testHelper.mcpSession()` follows the setting: no session id is expected or sent, and `sessionId` is `''`.
315
+
316
+ ```typescript
317
+ // config.env.ts
318
+ ai: { mcp: { oauth: true, stateless: true } },
319
+ ```
320
+
321
+ **Consent page wording and CSS.** A subclass of `CoreAiMcpOAuthService` overriding `renderConsentPage(view)` calls
322
+ `renderMcpOAuthConsentPage(view, { texts, styles })`.
323
+ - `texts` replaces single texts per locale. They stay escaped.
324
+ - `styles` is CSS appended inside the page's `<style>` element. A `<` in it is neutralized, so the CSS cannot end that
325
+ element, and the page's CSP blocks external loads.
326
+ - Fill both from code or configuration only.
327
+ - The page's scripts and security headers stay internal and are not exported.
328
+
329
+ ## 11. Removing a tenant
330
+
331
+ The core had no way to delete a tenant's data: a project had to find every place itself, and the ones that are not a
332
+ `tenantId` field (API tokens, the AI settings with `scope: 'tenant'`, the AI connections' `tenantIds`) were the easy ones
333
+ to miss. When a project deletes its tenant record, it now calls:
334
+
335
+ ```typescript
336
+ const report = await this.tenantService.purgeTenantData(tenantId); // { TenantMember: 4, Invoice: 120, ApiToken: 2 }
337
+ await this.tenantService.purgeTenantData(tenantId, { dryRun: true }); // counts only
338
+ await this.tenantService.purgeTenantData(tenantId, { exclude: ['AuditEntry'] });
339
+ ```
340
+
341
+ - Every way in goes first: the memberships (`deleteAllMembers()`, new as well), with every member's cached access
342
+ ending at once, and the tenant's API tokens, which need no member.
343
+ - Then every registered model with a `tenantId` path, on MongoDB and SQL, the project's own models included; the core
344
+ models that name the tenant in another field; and the AI connections — one only this tenant could use is deleted,
345
+ from a shared one the tenant is removed.
346
+ - Not deleted: models in `multiTenancy.excludeSchemas` or `exclude`, users, and files. Delete a tenant's files through
347
+ `CoreFileService` first; only the project knows which ones are the tenant's. Users stay also where `User` carries a
348
+ `tenantId`: delete per-tenant users through the user service first, which also ends their Better-Auth sessions.
349
+ - A project reference the core does not know of: override the protected `purgeTenantRecords()` and call `super`.
350
+ - The purge runs as a system operation. Call it only from a route that checked who may delete the tenant.
351
+
352
+ **Also changed:** `CoreTenantGuard.invalidateTenant()` clears the tenant's cached memberships and every cached tenant
353
+ list that contains it, on every replica with Redis. Before, it forgot only the `isTenantActive` answer: where a project
354
+ removed a tenant's memberships without calling `invalidateUser()` (a deleted tenant) and no `isTenantActive` answered
355
+ `false`, the cached memberships kept the members in for up to `cacheTtlMs`. A tenant switched off through
356
+ `isTenantActive` was already refused at once. Nothing to change in a project.
357
+
358
+ ## Compatibility Notes
359
+
360
+ | Pattern | Status |
361
+ |---|---|
362
+ | Frontend verify page that leads to the sign-in | Unchanged |
363
+ | Frontend verify page that expects a session after the link | Redirect to the sign-in, or set `autoSignInAfterVerification: true` (§1) |
364
+ | Project calling `appendMessage()` | Pass the owner id (§2) |
365
+ | Project subclass of `CoreAiService` overriding `prompt()` | Call `super.prompt()`, or the conversation is not scoped (§2) |
366
+ | Project subclass of `CoreAiService` overriding `persistTurn()` | Take `run` as third parameter and pass it to `super` (§2) |
367
+ | Project controller extending `CoreBetterAuthController` | Unchanged; the path guard runs before it |
368
+ | Custom `betterAuth.rateLimit.skipEndpoints` / `strictEndpoints` | Matched by whole segments now: `/session` no longer covers `/sessions-export` |
369
+ | Sync client pushing chained changes of one record in one request | Unchanged; behind a refused item the rest of the chain is answered `conflict` (or `retry`) instead of being applied (§5) |
370
+ | MCP client sending a malformed `/authorize` request | Gets a `400` page in the browser instead of an error redirect (§6) |
371
+ | Own MCP OAuth consent UI (`authorizeConsent()` override) | Unchanged; check the CSRF token on Deny too (§6) |
372
+ | Sync client splitting a record's changes across requests | Keep a later change back until the one before it was stored, drop it after `rejected` / `not_found` (§5, protocol §11) |
373
+ | `disableSignUp: true` without social providers | Unchanged |
374
+ | `disableSignUp: true` with social providers, new users only through admins or invitations | Unchanged in intent; social login no longer creates accounts (§7) |
375
+ | `disableSignUp: true` with a provider meant for self-registration | Set `disableSignUp: false` on that provider (§7) |
376
+ | AI tool without `authorize()` | Unchanged |
377
+ | AI tool whose `authorize()` was written for plan mode | Runs in auto mode and over MCP too (§8) |
378
+ | Destructive AI tool used through an MCP client with elicitation | The user is asked first (§8) |
379
+ | Destructive AI tool used through an MCP client without elicitation | Refused until the user switches confirmation off, or `ai.mcp.confirmation` is `false` (§8) |
380
+ | Project subclass of `CoreAiController` / `CoreAiResolver` | Unchanged; the settings service is property-injected (§8) |
381
+ | Native AI connection | Catalog lists names only; `ai.nativeToolCatalog: 'full'` restores it (§9) |
382
+ | `/ai/mcp` behind a load balancer without sticky sessions | Set `ai.mcp.stateless: true` (§10) |
383
+ | Destructive AI tool over MCP with `ai.mcp.stateless` | Refused unless the user or `ai.mcp.confirmation` switched the question off (§10) |
384
+ | Override of `fitMessagesToContext()` / `compactMessages()` | Unchanged; both take an optional third parameter, the tokens reserved for native tool definitions (§9) |
385
+ | Project deleting tenants with its own clean-up | Replace it with `purgeTenantData()`, or keep it and add what it misses (§11) |
386
+ | Project subclass of `CoreTenantService` | Unchanged; the MongoDB connection the purge reads is property-injected (§11) |
387
+
388
+ ## Troubleshooting
389
+
390
+ | Symptom | Cause | Fix |
391
+ |---|---|---|
392
+ | User not signed in after clicking the verification link | §1 | Expected; lead to the sign-in, or opt in |
393
+ | TS2554 on `appendMessage()` or `super.persistTurn()` | §2 | Pass the owner id, or the run, as third argument |
394
+ | A continued AI conversation answers without history, no `conversationId` in the response | The conversation belongs to another user (also for admins) | Continue only conversations the caller created |
395
+ | 400 `#LTNS_0305` on an IAM request | The path carries a dot segment, a backslash, an encoded separator or a `#` | Fix the client's URL |
396
+ | 429 on sign-in earlier than before | The strict limit (half of `max`) applies now, shared by e-mail sign-in and sign-up | Raise `betterAuth.rateLimit.max` |
397
+ | New users land on the error page with `error=signup_disabled` after a social login | §7: `disableSignUp: true` covers social providers | Expected; or set `disableSignUp: false` on the provider |
398
+ | MCP answers "needs the user's confirmation, and this MCP client cannot ask for it" | §8: destructive tool, client without elicitation | Use a client with elicitation, or let the user switch it off (`PUT /ai/settings`) |
399
+ | `TOOL_NOT_AVAILABLE` in auto mode for a tool that worked before | §8: its `authorize()` refuses | Fix the check, or return `{ allowed: true }` where it should pass |
400
+ | 403 `#LTNS_0609` on `PUT /ai/settings` | §8: `ai.mcp.confirmation.enforced` | Expected |
401
+ | Warning "the system prompt and tool definitions take ~N of M input tokens" | §9: the window is crowded | Set the connection's `contextWindow`, or shrink the catalog |
402
+
403
+ ## Module Documentation
404
+
405
+ ### Better-Auth
406
+
407
+ - **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md) — "Email Verification", "Rate Limiting", "Disable Sign-Up"
408
+ - **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
409
+ - **Key Files:** `better-auth.config.ts` (the verification options Better-Auth receives, `buildSocialProviders()`),
410
+ `core-better-auth-path-guard.middleware.ts`, `core-better-auth-rate-limiter.service.ts`
411
+
412
+ ### AI
413
+
414
+ - **README:** [src/core/modules/ai/README.md](../src/core/modules/ai/README.md)
415
+ - **Key Files:** `services/core-ai.service.ts` (`scopeConversation()`, `executeToolCall()`), `services/core-ai-conversation.service.ts`
416
+ (`isOwnedBy()`, `appendMessage()`), `helpers/ai-tool-authorization.helper.ts`, `services/core-ai-mcp.service.ts`
417
+ (`mcpCallTool()`), `services/core-ai-user-setting.service.ts`
418
+ - **README sections:** "Plan mode", "Authorization and confirmation over MCP"
419
+
420
+ ### AI (MCP OAuth)
421
+
422
+ - **README:** [src/core/modules/ai/README.md](../src/core/modules/ai/README.md) — "OAuth 2.1", consent page
423
+ - **Integration Checklist:** [src/core/modules/ai/INTEGRATION-CHECKLIST.md](../src/core/modules/ai/INTEGRATION-CHECKLIST.md) — own consent UI
424
+ - **Key Files:** `helpers/ai-mcp-oauth.helper.ts` (`mountAiMcpOAuthRouter()`), `services/core-ai-mcp-oauth.service.ts` (`authorizeConsent()`)
425
+
426
+ ### Multi-Tenancy
427
+
428
+ - **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md) — "Removing a Tenant"
429
+ - **Key Files:** `core-tenant.service.ts` (`purgeTenantData()`, `purgeTenantRecords()`, `deleteAllMembers()`),
430
+ `core-tenant.guard.ts` (`clearTenant()`)
431
+
432
+ ### Sync
433
+
434
+ - **README:** [src/core/modules/sync/README.md](../src/core/modules/sync/README.md) — hint stream log lines, held-back push items
435
+ - **Protocol:** [docs/offline-sync-protocol.md](../docs/offline-sync-protocol.md) §6.2, §6.3
436
+
437
+ ## References
438
+
439
+ - nest-server-starter: no code change; it keeps Better-Auth's defaults, configures neither `disableSignUp` nor a social
440
+ provider, does not call `appendMessage()`, and configures no `ai` module
@@ -0,0 +1,136 @@
1
+ # Migration Guide: 12.3.0 → 12.3.1
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | **Security** | better-auth 1.7.16: closes the better-auth advisories published on 2026-11-03 — social sign-in, passkeys, e-mail change, revoked sessions with a session cache — and brings better-call 1.4.5 (§1) |
8
+ | **New Features** | `GET /ai/status`: a public answer whether the AI module is on and whether MCP runs (§2) |
9
+ | **Bugfixes** | `betterAuth.passkey.userVerification`, `residentKey` and `authenticatorAttachment` take effect — they never reached the passkey plugin before (§3). The HTTP safety net strips secret fields under their SQL column names too, so a native SQL row returned by a route no longer hands out `password_reset_token` and the other secrets (§4) |
10
+ | **Breaking Changes** | The better-auth peer range starts at 1.7.16 (was 1.7.12). A project pinning an older version gets a peer warning, and keeps the vulnerability, until it raises the pin (§1). A project that set one of the three passkey options now gets what it configured (§3) |
11
+ | **Migration Effort** | Raise the better-auth trio to 1.7.16 in the API **and** in the app, in the same change (§1). Check §3 if your `betterAuth.passkey` block sets `userVerification`, `residentKey` or `authenticatorAttachment` |
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ # API (projects/api)
17
+ pnpm add better-auth@1.7.16 @better-auth/passkey@1.7.16 @better-auth/core@1.7.16
18
+ pnpm add @lenne.tech/nest-server@12.3.1
19
+ pnpm run update # nest-server-starter based projects
20
+
21
+ # App (projects/app) — together with @lenne.tech/nuxt-extensions, see its release notes
22
+ pnpm add better-auth@1.7.16 @better-auth/passkey@1.7.16
23
+
24
+ pnpm run build && pnpm test
25
+ ```
26
+
27
+ Vendor-mode projects pick it up with the core update (`/lt-dev:backend:update-nest-server-core`) and raise the trio in
28
+ their own `package.json`.
29
+
30
+ ## 1. better-auth 1.7.16 (security)
31
+
32
+ better-auth 1.7.16 closes the better-auth advisories published on 2026-11-03. Whether one reaches your project depends
33
+ on the features it uses; the ones that can matter for a nest-server project:
34
+
35
+ | Advisory | Affects you when |
36
+ | --------------------- | ----------------------------------------------------------------- |
37
+ | GHSA-fvxx-76fc-893x | revoked sessions stayed valid with secondary storage or the cookie cache |
38
+ | GHSA-9hm7-xhfp-cx58 | social sign-in (`socialProviders`): an unverified provider e-mail counted as verified |
39
+ | GHSA-gv75-j3v8-3crm | passkeys: origin and user verification were not enforced |
40
+ | GHSA-hwq2-v29r-p69r | e-mail change: links stayed valid after the address changed hands |
41
+ | GHSA-48c2-w96j-mv3m | the Kysely / Drizzle adapter on MySQL with serial ids (nest-server uses ObjectId hex ids) |
42
+
43
+ It also pins better-call 1.4.5, which fixes GHSA-23w8-wx9r-vm57 (values a router middleware returns were shared between
44
+ requests). That one is not reachable through nest-server, on 1.7.12 as on 1.7.16: better-auth's handler, which
45
+ nest-server calls for every `/iam/*` request it forwards, builds a new router around a per-request copy of its context,
46
+ so no two requests share one, and nest-server's direct `api.*` calls do not go through a router at all. 1.4.5 is a
47
+ second layer there, not the reason for this release.
48
+
49
+ **Raise all three packages in the API and in the app together.** better-auth is a wire protocol between nest-server
50
+ and nuxt-extensions; the two ends must not run different minors, and the peer ranges of both libraries now start at
51
+ 1.7.16. `pnpm run update` does not raise these pins — the project owns them.
52
+
53
+ **Installing before 2026-11-04 21:24 UTC.** pnpm's `minimumReleaseAge` (24 h) refuses the new versions until then.
54
+ To take the fix the same day, exempt exactly these versions in `pnpm-workspace.yaml` (each carries SLSA provenance
55
+ from the projects' own GitHub Actions) and drop the entries with your next maintenance — after that time they change
56
+ nothing:
57
+
58
+ ```yaml
59
+ minimumReleaseAgeExclude:
60
+ - '@better-auth/core@1.7.16'
61
+ - '@better-auth/drizzle-adapter@1.7.16'
62
+ - '@better-auth/kysely-adapter@1.7.16'
63
+ - '@better-auth/memory-adapter@1.7.16'
64
+ - '@better-auth/mongo-adapter@1.7.16'
65
+ - '@better-auth/passkey@1.7.16'
66
+ - '@better-auth/prisma-adapter@1.7.16'
67
+ - '@better-auth/telemetry@1.7.16'
68
+ - better-auth@1.7.16
69
+ - better-call@1.4.5
70
+ ```
71
+
72
+ Never override `better-call` alone instead: better-auth pins it exactly, and an override puts a combination into the
73
+ tree that nobody tested.
74
+
75
+ ## 2. `GET /ai/status`
76
+
77
+ A frontend asks before anybody signs in whether it shows the assistant at all:
78
+
79
+ | Server | Answer |
80
+ | ------------------------------ | ------------------------------ |
81
+ | `ai` configured, `ai.mcp` off | `200 { "mcp": false }` |
82
+ | `ai` configured, `ai.mcp` on | `200 { "mcp": true }` |
83
+ | no `ai` (or `enabled: false`) | `404` — the route does not exist |
84
+
85
+ The route is public and names no user data; every other `/ai` route keeps its roles. A project controller extending
86
+ `CoreAiController` inherits it. nuxt-extensions reads it through its AI status composable. Nothing to configure.
87
+
88
+ ## 3. Passkey options take effect
89
+
90
+ `betterAuth.passkey.userVerification`, `residentKey` and `authenticatorAttachment` were handed to the passkey plugin
91
+ at the top level of its options, where it never reads them: it takes them from `authenticatorSelection`. Every project
92
+ therefore ran on the plugin's defaults (`residentKey` and `userVerification` `"preferred"`, no attachment), whatever it
93
+ configured. Since 12.3.1 the framework passes them where the plugin reads them.
94
+
95
+ Nothing changes for a project that sets none of the three. A project that sets one gets what it configured, and
96
+ better-auth 1.7.16 now enforces `userVerification: 'required'`: a passkey that answers without user verification (no
97
+ biometric or PIN check) is refused at registration and at sign-in. That is the point of the setting — check it is what
98
+ you want before you update:
99
+
100
+ ```typescript
101
+ // config.env.ts
102
+ betterAuth: {
103
+ passkey: {
104
+ userVerification: 'required', // refuses passkeys without biometric/PIN verification
105
+ residentKey: 'required', // registration creates a discoverable credential
106
+ authenticatorAttachment: 'platform', // only built-in authenticators (Touch ID, Windows Hello)
107
+ },
108
+ },
109
+ ```
110
+
111
+ ## 4. Secret fields are stripped under their SQL column names too
112
+
113
+ `CheckSecurityInterceptor` removes the fields of `security.secretFields` (`password`, `verificationToken`,
114
+ `passwordResetToken`, `refreshTokens`, `tempTokens`, `apiKeyEncrypted`, … plus your own) from every response as
115
+ a safety net. It compared the field names only. A row read through `store.native(reason)` or `store.raw(reason)`
116
+ on a SQL source carries the snake_case columns (`password_reset_token`), so a route that returned such a row
117
+ unchanged — a `SELECT *` or `RETURNING *` — dropped `password` (spelled alike) and handed every other secret to
118
+ the client.
119
+
120
+ Since 12.3.1 each secret field is also stripped under its column name, your own entries in `security.secretFields`
121
+ included. Nothing to change. Still: a route should map a native row to the fields the client needs rather than rely
122
+ on the safety net — see `.claude/rules/persistence.md` → "Native access in three tiers".
123
+
124
+ ## Troubleshooting
125
+
126
+ | Symptom | Cause | Fix |
127
+ | ------- | ----- | --- |
128
+ | `ERR_PNPM_NO_MATURE_MATCHING_VERSION` for better-auth 1.7.16 | installed before 2026-11-04 21:24 UTC | add the `minimumReleaseAgeExclude` entries from §1, or wait |
129
+ | `unmet peer better-auth@">=1.7.16 <1.8.0"` | the project still pins an older better-auth | raise the trio (§1) |
130
+ | The app shows no assistant although `ai` is configured | the API answers `/ai/status` with 404 — it runs a version before 12.3.1 | update the API |
131
+ | Passkey registration or sign-in fails after the update for some users | `betterAuth.passkey.userVerification: 'required'` now takes effect, and their authenticator answered without user verification | keep `'required'` and let those users use a verifying authenticator, or set `'preferred'` |
132
+
133
+ ## Module Documentation
134
+
135
+ - AI module: [`src/core/modules/ai/README.md`](../src/core/modules/ai/README.md) → "Is the assistant available?"
136
+ - Better-Auth: [`src/core/modules/better-auth/INTEGRATION-CHECKLIST.md`](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)