@lenne.tech/nest-server 12.2.0 → 12.3.0

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 (152) hide show
  1. package/.claude/rules/better-auth.md +31 -4
  2. package/.claude/rules/configurable-features.md +7 -6
  3. package/.claude/rules/package-management.md +13 -1
  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/interfaces/server-options.interface.d.ts +8 -0
  16. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  17. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  18. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  19. package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
  20. package/dist/core/modules/ai/core-ai.constants.js +2 -0
  21. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  22. package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
  23. package/dist/core/modules/ai/core-ai.controller.js +31 -1
  24. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  25. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  26. package/dist/core/modules/ai/core-ai.module.js +8 -1
  27. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  28. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  29. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  30. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  31. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  32. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  33. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  34. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  35. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  36. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  37. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  38. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  39. package/dist/core/modules/ai/index.d.ts +7 -0
  40. package/dist/core/modules/ai/index.js +6 -0
  41. package/dist/core/modules/ai/index.js.map +1 -1
  42. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  43. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  44. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  45. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  46. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  47. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  48. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  49. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  50. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  51. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  52. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  53. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  54. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  55. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  56. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  57. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  58. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  59. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  60. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  61. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  62. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  63. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  64. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  65. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  66. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  67. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  68. package/dist/core/modules/better-auth/better-auth.config.js +3 -1
  69. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  70. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  71. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  72. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  73. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  74. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  75. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  76. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  77. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  78. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  79. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  80. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  81. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  82. package/dist/core/modules/better-auth/index.d.ts +1 -0
  83. package/dist/core/modules/better-auth/index.js +1 -0
  84. package/dist/core/modules/better-auth/index.js.map +1 -1
  85. package/dist/core/modules/error-code/error-codes.d.ts +18 -0
  86. package/dist/core/modules/error-code/error-codes.js +16 -0
  87. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  88. package/dist/core/modules/sync/core-sync.service.d.ts +1 -0
  89. package/dist/core/modules/sync/core-sync.service.js +31 -2
  90. package/dist/core/modules/sync/core-sync.service.js.map +1 -1
  91. package/dist/core/modules/tenant/core-tenant.guard.js +10 -0
  92. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  93. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  94. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  95. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  96. package/dist/server/modules/error-code/error-codes.d.ts +2 -0
  97. package/dist/test/test.helper.d.ts +1 -0
  98. package/dist/test/test.helper.js +9 -2
  99. package/dist/test/test.helper.js.map +1 -1
  100. package/docs/REQUEST-LIFECYCLE.md +3 -2
  101. package/docs/error-codes.md +14 -0
  102. package/docs/offline-sync-protocol.md +17 -8
  103. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  104. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  105. package/package.json +20 -20
  106. package/src/config.env.ts +6 -2
  107. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  108. package/src/core/common/interfaces/server-options.interface.ts +80 -8
  109. package/src/core/common/persistence/sync-model.ts +1 -1
  110. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  111. package/src/core/modules/ai/README.md +163 -25
  112. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  113. package/src/core/modules/ai/core-ai.constants.ts +6 -0
  114. package/src/core/modules/ai/core-ai.controller.ts +36 -1
  115. package/src/core/modules/ai/core-ai.module.ts +12 -0
  116. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  117. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  118. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  119. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  120. package/src/core/modules/ai/index.ts +18 -0
  121. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  122. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  123. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  124. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  125. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  126. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  127. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  128. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  129. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  130. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  131. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  132. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  133. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  134. package/src/core/modules/api-token/README.md +4 -2
  135. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  136. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
  137. package/src/core/modules/better-auth/README.md +32 -1
  138. package/src/core/modules/better-auth/better-auth.config.ts +12 -1
  139. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  140. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  141. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  142. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  143. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  144. package/src/core/modules/better-auth/index.ts +1 -0
  145. package/src/core/modules/error-code/error-codes.ts +18 -0
  146. package/src/core/modules/sync/README.md +11 -1
  147. package/src/core/modules/sync/core-sync.service.ts +48 -3
  148. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  149. package/src/core/modules/tenant/README.md +49 -3
  150. package/src/core/modules/tenant/core-tenant.guard.ts +18 -2
  151. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  152. package/src/test/test.helper.ts +17 -3
@@ -82,6 +82,17 @@ export interface IWsConnectionRegistration {
82
82
  */
83
83
  carrier: object;
84
84
 
85
+ /** How the log names the connection when it is closed. Default `GraphQL WebSocket`. */
86
+ channel?: string;
87
+
88
+ /**
89
+ * The connection cannot be re-checked in place — an HTTP stream has no handshake to repeat — so every re-check ends
90
+ * it, and the client's reconnect passes the guards again. The end the re-check interval brings is planned and logged
91
+ * at debug; one an invalidation brings (sign-out, role or tenant change) is logged like every withdrawn
92
+ * authorization. `reauthenticate` is not called.
93
+ */
94
+ endsOnRecheck?: boolean;
95
+
85
96
  /**
86
97
  * Re-run the handshake's authentication. Resolves to the user the credential authenticates as NOW,
87
98
  * or to a falsy value when it no longer does. A rejection counts as falsy. Without it only the
@@ -105,7 +116,9 @@ export interface IWsConnectionIdentity {
105
116
 
106
117
  /** @internal The registered state of one connection. */
107
118
  export interface WsConnection {
119
+ readonly channel: string;
108
120
  readonly email?: string;
121
+ readonly endsOnRecheck: boolean;
109
122
  readonly iamId?: string;
110
123
  readonly reauthenticate?: () => any;
111
124
  readonly socket?: IWsConnectionSocket;
@@ -206,13 +219,15 @@ export function readWsTenantHeader(contextValue: any): string | undefined {
206
219
  * the handshake recorded on the carrier.
207
220
  */
208
221
  export function registerWsConnection(registration: IWsConnectionRegistration): void {
209
- const { carrier, reauthenticate, socket, user } = registration;
222
+ const { carrier, channel, endsOnRecheck, reauthenticate, socket, user } = registration;
210
223
  if (!carrier || typeof carrier !== 'object') {
211
224
  return;
212
225
  }
213
226
  const userId = user?.id ?? user?._id;
214
227
  const connection: WsConnection = {
228
+ channel: channel || 'GraphQL WebSocket',
215
229
  email: typeof user?.email === 'string' ? user.email.toLowerCase() : undefined,
230
+ endsOnRecheck: endsOnRecheck === true,
216
231
  iamId: typeof user?.iamId === 'string' ? user.iamId : undefined,
217
232
  reauthenticate,
218
233
  socket,
@@ -246,15 +261,20 @@ export function getWsConnection(contextValue: any): undefined | WsConnection {
246
261
 
247
262
  /**
248
263
  * Close a connection whose authorization no longer holds. Idempotent; always answers `false` so a
249
- * caller can `return terminate(...)`.
264
+ * caller can `return terminate(...)`. `debug` is for a planned end, which says nothing about the user.
250
265
  */
251
- function terminate(connection: WsConnection, reason: string): false {
266
+ function terminate(connection: WsConnection, reason: string, level: 'debug' | 'log' = 'log'): false {
252
267
  if (connection.terminated) {
253
268
  return false;
254
269
  }
255
270
  connection.terminated = true;
256
271
  liveConnections.delete(connection);
257
- logger.log(`Closing GraphQL WebSocket of user ${connection.userId ?? '(unknown)'}: ${reason}`);
272
+ const message = `Closing ${connection.channel} of user ${connection.userId ?? '(unknown)'}: ${reason}`;
273
+ if (level === 'debug') {
274
+ logger.debug(message);
275
+ } else {
276
+ logger.log(message);
277
+ }
258
278
  try {
259
279
  // A close reason is limited to 123 bytes on the wire; these are short and ASCII.
260
280
  connection.socket?.close(WS_AUTHORIZATION_CHANGED_CLOSE_CODE, reason);
@@ -265,7 +285,15 @@ function terminate(connection: WsConnection, reason: string): false {
265
285
  }
266
286
 
267
287
  async function revalidate(connection: WsConnection): Promise<boolean> {
288
+ const invalidated = connection.stale;
268
289
  connection.stale = false;
290
+ if (connection.endsOnRecheck) {
291
+ // Nothing to check in place: the end IS the re-check, and the reconnect is authorized afresh. Only an invalidation
292
+ // says that the rights may have changed; the interval is the planned beat and must not read like a lost session.
293
+ return invalidated
294
+ ? terminate(connection, 'authorization invalidated, client reconnects')
295
+ : terminate(connection, 're-check interval reached, client reconnects', 'debug');
296
+ }
269
297
  try {
270
298
  let user = connection.user;
271
299
  if (connection.reauthenticate) {
@@ -510,8 +510,14 @@ export type IBetterAuth = IBetterAuthWithoutPasskey | IBetterAuthWithPasskey;
510
510
  */
511
511
  export interface IBetterAuthEmailVerificationConfig {
512
512
  /**
513
- * Whether to automatically sign in the user after email verification.
514
- * @default true
513
+ * Whether `GET /iam/verify-email` signs in the browser that opens the verification link.
514
+ *
515
+ * Off by default, as in Better-Auth (12.3.0+; before, the framework defaulted to `true`). Switching it on signs in
516
+ * WHOEVER opens the link, to the account the link belongs to: someone who signs up with their own address and
517
+ * forwards the unopened link has the recipient's browser signed in to their account, and what the recipient then
518
+ * enters lands there (login CSRF). Only switch it on where verification links reach nobody but their owner.
519
+ *
520
+ * @default false
515
521
  */
516
522
  autoSignInAfterVerification?: boolean;
517
523
 
@@ -975,6 +981,17 @@ export interface IBetterAuthSocialProvider {
975
981
  */
976
982
  clientSecret: string;
977
983
 
984
+ /**
985
+ * Whether a first sign-in through this provider may create an account.
986
+ *
987
+ * Unset, the provider follows `betterAuth.emailAndPassword.disableSignUp`: with that set to `true` (since 12.3.0)
988
+ * nobody gets a new account through any provider, while existing users keep signing in. `false` opens sign-up
989
+ * through this provider although `disableSignUp` closes it otherwise; `true` closes it for this provider alone.
990
+ * Only a boolean counts: any other value follows `emailAndPassword.disableSignUp`.
991
+ * @default emailAndPassword.disableSignUp
992
+ */
993
+ disableSignUp?: boolean;
994
+
978
995
  /**
979
996
  * Whether this provider is enabled.
980
997
  * Defaults to true when clientId and clientSecret are provided.
@@ -2004,7 +2021,13 @@ export interface IAi {
2004
2021
  * Fallback total context window (input + output tokens) used to budget the
2005
2022
  * assembled session messages when a connection has no `contextWindow` set. When the
2006
2023
  * conversation (per user/session) would overflow, the oldest history turns are
2007
- * dropped and oversized payloads truncated before the LLM call.
2024
+ * dropped and oversized payloads truncated before the LLM call. Native tool
2025
+ * definitions count against that budget too (12.3.0+), and when the system prompt and
2026
+ * tools take more than half of it, a warning names the connection once per process.
2027
+ *
2028
+ * A test that guards the prompt size against this value guards the connections WITHOUT
2029
+ * a window of their own — the worst case — not the window a pinned or detected
2030
+ * connection actually has. Name it that way; raising its limit removes that guard.
2008
2031
  * @default 8192
2009
2032
  */
2010
2033
  contextWindow?: number;
@@ -2061,14 +2084,14 @@ export interface IAi {
2061
2084
  * a caller sees and may run is decided server-side by the registry's role filter
2062
2085
  * (`AiToolRegistry.forUser()`), re-checked at execution time, and the
2063
2086
  * confirmation gate reads the `mutating`/`destructive` FLAGS, never the
2064
- * description text. (`AiTool.authorize()` runs in plan mode only — in auto mode
2065
- * and over MCP, data-level checks belong inside `execute()`.)
2087
+ * description text. (`AiTool.authorize()` runs before every execution — auto
2088
+ * mode, plan mode and MCP — on the call's arguments, never its description.)
2066
2089
  *
2067
2090
  * Applies to the EMULATED tool protocol, not to a provider as such. In auto mode
2068
2091
  * a connection with native tool calling receives every full description and schema
2069
- * through `buildToolSchemas()` regardless, so no truncation is applied there and no
2070
- * `search_tools` banner is emitted — capping would otherwise assert a truncation
2071
- * that the tool payload right next to it contradicts.
2092
+ * through `buildToolSchemas()` regardless, and its catalog lists names only since
2093
+ * 12.3.0 (`nativeToolCatalog`), so this cap has no effect there and no `search_tools`
2094
+ * banner is emitted.
2072
2095
  *
2073
2096
  * **Plan mode always uses the emulated protocol** (it sends no native schemas), so a
2074
2097
  * native connection IS truncated there. Because plan mode returns a complete plan in
@@ -2091,6 +2114,17 @@ export interface IAi {
2091
2114
  */
2092
2115
  deferToolSummaryChars?: number;
2093
2116
 
2117
+ /**
2118
+ * What the system prompt's tool catalog holds for a connection with NATIVE tool calling in auto mode (12.3.0+).
2119
+ * Such a connection receives every description and parameter schema in the tool payload, so by default the catalog
2120
+ * lists names only — repeating the descriptions (and, without `deferToolSchemas`, the schemas) paid for every tool
2121
+ * twice. `'full'` restores the full catalog, for a model that picks tools better with the descriptions in the prompt.
2122
+ *
2123
+ * Emulated connections and plan mode always get the full catalog: there it is the model's only source.
2124
+ * @default 'names'
2125
+ */
2126
+ nativeToolCatalog?: 'full' | 'names';
2127
+
2094
2128
  /** Maximum number of agent-loop iterations (tool round-trips). @default 5 */
2095
2129
  maxIterations?: number;
2096
2130
 
@@ -2142,11 +2176,32 @@ export interface IAi {
2142
2176
  * answers with a built-in consent page: sign-in through Better-Auth (password + 2FA),
2143
2177
  * then Allow / Deny. Replace it via `CoreModule.forRoot(env, { ai: { mcpOAuthService } })`.
2144
2178
  * Without `oauth`, MCP authenticates via the existing Bearer/session token.
2179
+ *
2180
+ * `tools/call` applies the admin's tool policies and a tool's `authorize()`, and asks the user before a
2181
+ * tool that needs confirmation (`destructive`, or `mutating` under `ai.confirmation.mutating`) — see
2182
+ * `confirmation`.
2145
2183
  * @default false
2146
2184
  */
2147
2185
  mcp?:
2148
2186
  | boolean
2149
2187
  | {
2188
+ /**
2189
+ * Whether MCP asks the user — through MCP elicitation, a yes/no form the client shows — before a
2190
+ * `destructive` tool, or a `mutating` one under `ai.confirmation.mutating` (12.3.0+). The tool runs only
2191
+ * on an explicit yes; a client without the `elicitation` capability gets a refusal. `authorize()` runs
2192
+ * either way, and an admin's `ask` tool policy always asks.
2193
+ *
2194
+ * - `true` / `{}` / unset: MCP asks, unless a user switched it off for themselves
2195
+ * (`PUT /ai/settings`, `updateAiSettings` — like a "bypass permissions" mode).
2196
+ * - `false`: MCP asks nobody, unless a user switched it on for themselves.
2197
+ * - `{ default, enforced }`: `default` as above; `enforced: true` ignores every user's choice and
2198
+ * refuses a change with 403.
2199
+ *
2200
+ * Only `false` / `default: false` switch the default off and only `enforced: true` enforces, so a
2201
+ * misspelt value never relaxes the gate.
2202
+ * @default { default: true, enforced: false }
2203
+ */
2204
+ confirmation?: boolean | { default?: boolean; enforced?: boolean };
2150
2205
  enabled?: boolean;
2151
2206
  oauth?: boolean;
2152
2207
  /**
@@ -2164,6 +2219,17 @@ export interface IAi {
2164
2219
  */
2165
2220
  oauthLoginUrl?: string;
2166
2221
  oauthSecret?: string;
2222
+ /**
2223
+ * Serve `/ai/mcp` without sessions: every POST builds its own server and transport for the
2224
+ * authenticated user and ends with the response, no `mcp-session-id` is issued, and GET /
2225
+ * DELETE answer 405. Any replica can answer any request, so no sticky sessions are needed
2226
+ * behind a load balancer. The price is what a session carries: server-initiated messages
2227
+ * (notifications, SSE streams) and server-initiated requests — MCP elicitation among them, so a
2228
+ * tool that needs confirmation (`confirmation`) is refused unless the user or `confirmation`
2229
+ * switched the question off. A server that only offers read tools uses none of it (12.3.0+).
2230
+ * @default false
2231
+ */
2232
+ stateless?: boolean;
2167
2233
  };
2168
2234
 
2169
2235
  /** Rate limiting for prompts. */
@@ -4387,6 +4453,10 @@ interface IBetterAuthBase {
4387
4453
  * Disable user registration (sign-up) via BetterAuth.
4388
4454
  * Passed through to better-auth's native emailAndPassword.disableSignUp.
4389
4455
  * Custom endpoints (GraphQL + REST) also check this flag early.
4456
+ * Since 12.3.0 it closes social sign-up too: every configured social provider receives it as its own
4457
+ * `disableSignUp`, so a first sign-in through Google, GitHub etc. creates no account, while existing users keep
4458
+ * signing in. `socialProviders.<name>.disableSignUp: false` opens one provider deliberately. Plugins a project
4459
+ * registers itself through `options.plugins` are not covered.
4390
4460
  * The Swagger document built by `setupSwagger()` then leaves out `POST /iam/sign-up/email`,
4391
4461
  * which would answer `SIGNUP_DISABLED` to every caller.
4392
4462
  * @default false
@@ -5002,6 +5072,7 @@ export interface ICoreModuleOverrides {
5002
5072
  * - `slotService` must extend `CoreAiSlotService`
5003
5073
  * - `toolGrantService` must extend `CoreAiToolGrantService`
5004
5074
  * - `toolPolicyService` must extend `CoreAiToolPolicyService`
5075
+ * - `userSettingService` must extend `CoreAiUserSettingService`
5005
5076
  *
5006
5077
  * @example
5007
5078
  * ```typescript
@@ -5035,6 +5106,7 @@ export interface ICoreModuleOverrides {
5035
5106
  slotService?: Type<any>;
5036
5107
  toolGrantService?: Type<any>;
5037
5108
  toolPolicyService?: Type<any>;
5109
+ userSettingService?: Type<any>;
5038
5110
  };
5039
5111
 
5040
5112
  /**
@@ -9,7 +9,7 @@ import type {
9
9
  export type { SyncDescriptor, SyncIdFormat } from './model-descriptor.interface.js';
10
10
 
11
11
  /**
12
- * Synchronized models (offline sync, `docs/offline-sync-protocol.md`, plan `.claude/plans/offline-sync/PLAN.md` §3.2).
12
+ * Synchronized models (offline sync, `docs/offline-sync-protocol.md`, offline-sync plan §3.2).
13
13
  *
14
14
  * A model declared with `@PersistedModel({ sync })` carries three system fields every store maintains and no caller
15
15
  * writes — `version`, `deletedAt` and its position in the change order — and its delete leaves a tombstone instead of
@@ -146,9 +146,10 @@ ai: {
146
146
  }
147
147
  ```
148
148
 
149
- - **Plan mode / pre-flight permissions:** give data-mutating tools an `authorize()` that
150
- checks permission WITHOUT mutating (e.g. load the record + verify ownership). Plan mode
151
- runs it for every step before executing anything.
149
+ - **Data-level permissions:** give data-mutating tools an `authorize()` that checks
150
+ permission WITHOUT mutating (e.g. load the record + verify ownership). It runs before every
151
+ execution — auto mode, plan mode (for every step before anything runs) and MCP (12.3.0+);
152
+ return `data` to hand the loaded record to `execute()` (`context.authorization.data`).
152
153
  - **Confirmation:** mark create/update/delete tools `mutating: true` and irreversible ones
153
154
  `destructive: true`.
154
155
  - **Self-optimizing prompts:** the system prompt is built from editable, keyed slots —
@@ -174,8 +175,9 @@ ai: {
174
175
  `deferToolSummaryChars` (~200–400) ALONGSIDE it — otherwise the descriptions alone
175
176
  re-inflate the prompt you just shrank. Both are scoped to the **emulated tool protocol**:
176
177
  in auto mode a connection with `supportsNativeTools: true` gets every full description and
177
- schema in the native `tools` payload regardless, so truncation and the banner are skipped
178
- there. **Plan mode always uses the emulated protocol**, so a native connection IS truncated
178
+ schema in the native `tools` payload regardless, and its prompt catalog lists names only
179
+ (12.3.0+, `ai.nativeToolCatalog: 'full'` restores it), so truncation and the banner are
180
+ skipped there. **Plan mode always uses the emulated protocol**, so a native connection IS truncated
179
181
  there — and since plan mode plans in one call without executing, it cannot follow the
180
182
  `search_tools` hint. Keep the cap generous if you use plan mode.
181
183
  The truncated tail is typically where a tool's preconditions and role limits are written;
@@ -183,10 +185,16 @@ ai: {
183
185
  stays with the registry's role filter, the execution-time re-check, and the
184
186
  `mutating`/`destructive` flags behind the confirmation gate, none of which read the
185
187
  description.
186
- - **Where data-level checks belong:** `authorize()` runs in **plan mode only**. In auto mode
187
- (the default) and over MCP, the orchestrator calls `execute()` directly. Put ownership and
188
- tenant checks INSIDE `execute()` (via `CrudService` + `context.serviceOptions`) — a check
189
- that lives only in `authorize()` will not run for most callers.
188
+ - **Guarding the prompt size in a test?** Measure against `ai.contextWindow`, the fallback for
189
+ connections without a window of their own, and name the test that way: it guards the worst
190
+ case, not the window a pinned or detected connection has. Raising its limit removes the guard;
191
+ shrink the catalog instead. At runtime the framework warns once per connection when the system
192
+ prompt and tool definitions take more than half of the real input budget.
193
+ - **Where data-level checks belong:** `authorize()` decides whether a call may be tried;
194
+ `execute()` still routes every read and write through `CrudService` +
195
+ `context.serviceOptions`, so `@Restricted`, `securityCheck()` and tenant scoping apply.
196
+ Until 12.2 `authorize()` ran in plan mode only — a project that moved its checks into
197
+ `execute()` for that reason keeps working unchanged.
190
198
 
191
199
  ### MCP server (only when `ai.mcp.enabled` or `ai.mcp` is truthy)
192
200
 
@@ -200,10 +208,14 @@ A **503 Service Unavailable** from `/ai/mcp` therefore means the module could no
200
208
  _resolved_, not that it is missing — check the server log for the underlying error.
201
209
  The response body carries the stable `#LTNS_0901` code, never the raw error.
202
210
 
203
- > **No confirmation gate over MCP.** `mcpCallTool` does not consult the
204
- > `mutating`/`destructive` flags, and `authorize()` does not run there either. A
205
- > destructive tool invoked through `/ai/mcp` executes immediately. Only expose MCP to
206
- > clients you trust, and put every permission check inside `execute()`.
211
+ > **Confirmation over MCP (12.3.0+).** `tools/call` applies the admin's tool policies and
212
+ > `authorize()`, and asks the user through MCP elicitation before a `destructive` tool (or a
213
+ > `mutating` one under `ai.confirmation.mutating`) runs. A client without elicitation gets an
214
+ > error result. Each user may switch the question off (and on) for themselves via
215
+ > `PUT /ai/settings` / `updateAiSettings`; `ai.mcp.confirmation` sets the default
216
+ > (`false` = off) and `{ enforced: true }` takes the switch away. **Check your MCP clients**
217
+ > and offer users the switch in your UI (`GET /ai/settings` says whether it is enforced).
218
+ > See the module README, "Authorization and confirmation over MCP".
207
219
 
208
220
  ### MCP OAuth 2.1 (only when `ai.mcp.oauth` is enabled)
209
221
 
@@ -229,9 +241,15 @@ Put `{returnTo}` into it only if that login page can send the user back to the A
229
241
  (the nuxt-base-starter login accepts same-origin targets only, so leave it out there).
230
242
 
231
243
  **Optional — your own consent UI:** a subclass of `CoreAiMcpOAuthService` overriding
232
- `renderConsentPage(view)` (restyle) or `authorizeConsent(client, params, res)` (replace), registered via
244
+ `renderConsentPage(view)` (restyle — `renderMcpOAuthConsentPage(view, { texts, styles })` changes wording and
245
+ CSS without copying markup, scripts or headers; fill both from code or configuration only) or
246
+ `authorizeConsent(client, params, res)` (replace), registered via
233
247
  `CoreModule.forRoot(envConfig, { ai: { mcpOAuthService: McpOAuthService } })`.
234
248
 
249
+ **Optional — no sticky sessions:** `ai: { mcp: { stateless: true } }` serves `/ai/mcp` without sessions, so any
250
+ replica answers any request. Without a session the server cannot ask the user (no MCP elicitation): tools that
251
+ need confirmation are refused there unless the user or `ai.mcp.confirmation` switched the question off.
252
+
235
253
  **WHY a POST, never "signed in, so issue":** anybody can register a client with their own redirect
236
254
  URI and start the flow — PKCE is then theirs too. A GET to `/authorize` is what any link produces,
237
255
  and it carries the session cookie, so issuing on GET hands a code for whoever clicks to the
@@ -242,7 +260,10 @@ with a CSRF token.
242
260
  **WHY refuse with `this.loadOAuthErrors()`:** the SDK maps an error to its OAuth code only when it
243
261
  is one of its own `OAuthError` classes, and turns anything else into `server_error`. A user who
244
262
  declines consent should send the client back with `access_denied` (`AccessDeniedError`), not with
245
- a server error.
263
+ a server error. Check the CSRF token on Deny as on Allow (12.3.0): the refusal redirects the browser to
264
+ the client's registered URI, so an unprotected Deny lets a cross-site POST send a signed-in user there.
265
+ An error before your method runs (a malformed request) is answered with 400 by the framework and never
266
+ redirected.
246
267
 
247
268
  ## Verification Checklist
248
269