@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.
- package/.claude/rules/better-auth.md +31 -4
- package/.claude/rules/configurable-features.md +7 -6
- package/.claude/rules/package-management.md +13 -1
- package/.claude/rules/persistence.md +2 -2
- package/.claude/rules/testing.md +5 -5
- package/.claude/rules/versioning.md +13 -2
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +4 -3
- package/README.md +2 -2
- package/dist/config.env.js +2 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
- package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
- package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +8 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.constants.js +2 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
- package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.controller.js +31 -1
- package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
- package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
- package/dist/core/modules/ai/core-ai.module.js +8 -1
- package/dist/core/modules/ai/core-ai.module.js.map +1 -1
- package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
- package/dist/core/modules/ai/core-ai.resolver.js +32 -0
- package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
- package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
- package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +7 -0
- package/dist/core/modules/ai/index.js +6 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
- package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
- package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
- package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
- package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
- package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
- package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
- package/dist/core/modules/ai/services/core-ai.service.js +68 -29
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +3 -1
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
- package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/error-code/error-codes.d.ts +18 -0
- package/dist/core/modules/error-code/error-codes.js +16 -0
- package/dist/core/modules/error-code/error-codes.js.map +1 -1
- package/dist/core/modules/sync/core-sync.service.d.ts +1 -0
- package/dist/core/modules/sync/core-sync.service.js +31 -2
- package/dist/core/modules/sync/core-sync.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.js +10 -0
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
- package/dist/core/modules/tenant/core-tenant.service.js +153 -1
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/server/modules/error-code/error-codes.d.ts +2 -0
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +9 -2
- package/dist/test/test.helper.js.map +1 -1
- package/docs/REQUEST-LIFECYCLE.md +3 -2
- package/docs/error-codes.md +14 -0
- package/docs/offline-sync-protocol.md +17 -8
- package/migration-guides/12.0.x-to-12.1.x.md +0 -1
- package/migration-guides/12.2.x-to-12.3.x.md +440 -0
- package/package.json +20 -20
- package/src/config.env.ts +6 -2
- package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
- package/src/core/common/interfaces/server-options.interface.ts +80 -8
- package/src/core/common/persistence/sync-model.ts +1 -1
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
- package/src/core/modules/ai/README.md +163 -25
- package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
- package/src/core/modules/ai/core-ai.constants.ts +6 -0
- package/src/core/modules/ai/core-ai.controller.ts +36 -1
- package/src/core/modules/ai/core-ai.module.ts +12 -0
- package/src/core/modules/ai/core-ai.resolver.ts +36 -0
- package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
- package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
- package/src/core/modules/ai/index.ts +18 -0
- package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
- package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
- package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
- package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
- package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
- package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
- package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
- package/src/core/modules/ai/services/core-ai.service.ts +134 -35
- package/src/core/modules/api-token/README.md +4 -2
- package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
- package/src/core/modules/better-auth/README.md +32 -1
- package/src/core/modules/better-auth/better-auth.config.ts +12 -1
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
- package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
- package/src/core/modules/better-auth/index.ts +1 -0
- package/src/core/modules/error-code/error-codes.ts +18 -0
- package/src/core/modules/sync/README.md +11 -1
- package/src/core/modules/sync/core-sync.service.ts +48 -3
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
- package/src/core/modules/tenant/README.md +49 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +18 -2
- package/src/core/modules/tenant/core-tenant.service.ts +287 -2
- 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
|
-
|
|
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
|
|
514
|
-
*
|
|
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
|
|
2065
|
-
* and
|
|
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,
|
|
2070
|
-
* `
|
|
2071
|
-
*
|
|
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`,
|
|
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
|
-
- **
|
|
150
|
-
|
|
151
|
-
|
|
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,
|
|
178
|
-
|
|
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
|
-
- **
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
> **
|
|
204
|
-
> `
|
|
205
|
-
>
|
|
206
|
-
>
|
|
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
|
|
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
|
|