@lenne.tech/nest-server 12.1.1 → 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 (317) hide show
  1. package/.claude/rules/architecture.md +1 -0
  2. package/.claude/rules/better-auth.md +31 -4
  3. package/.claude/rules/configurable-features.md +8 -6
  4. package/.claude/rules/package-management.md +29 -2
  5. package/.claude/rules/persistence.md +27 -1
  6. package/.claude/rules/testing.md +80 -15
  7. package/.claude/rules/versioning.md +13 -2
  8. package/CLAUDE.md +7 -1
  9. package/FRAMEWORK-API.md +20 -4
  10. package/README.md +20 -2
  11. package/dist/config.env.js +2 -2
  12. package/dist/config.env.js.map +1 -1
  13. package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
  14. package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
  15. package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
  16. package/dist/core/common/interfaces/server-options.interface.d.ts +49 -0
  17. package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
  18. package/dist/core/common/persistence/core-persistence.module.js +12 -2
  19. package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
  20. package/dist/core/common/persistence/data-source.runtime.js +4 -2
  21. package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
  22. package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
  23. package/dist/core/common/persistence/id-format.d.ts +2 -0
  24. package/dist/core/common/persistence/id-format.js +14 -0
  25. package/dist/core/common/persistence/id-format.js.map +1 -1
  26. package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
  27. package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
  28. package/dist/core/common/persistence/model-metadata.registry.js +20 -2
  29. package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
  30. package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
  31. package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
  32. package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
  33. package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
  34. package/dist/core/common/persistence/mongoose-data-store.js +107 -31
  35. package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
  36. package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
  37. package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
  38. package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
  39. package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
  40. package/dist/core/common/persistence/policies/sync.policy.js +77 -0
  41. package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
  42. package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
  43. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
  44. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
  45. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
  46. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
  47. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
  48. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
  49. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
  50. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
  51. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
  52. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
  53. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
  54. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
  55. package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
  56. package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
  57. package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
  58. package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
  59. package/dist/core/common/persistence/sql/sql-errors.js +3 -1
  60. package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
  61. package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
  62. package/dist/core/common/persistence/sync-access.registry.js +8 -0
  63. package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
  64. package/dist/core/common/persistence/sync-model.d.ts +29 -0
  65. package/dist/core/common/persistence/sync-model.js +138 -0
  66. package/dist/core/common/persistence/sync-model.js.map +1 -0
  67. package/dist/core/common/services/crud.service.d.ts +13 -1
  68. package/dist/core/common/services/crud.service.js +174 -16
  69. package/dist/core/common/services/crud.service.js.map +1 -1
  70. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  71. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  72. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  73. package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
  74. package/dist/core/modules/ai/core-ai.constants.js +2 -0
  75. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  76. package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
  77. package/dist/core/modules/ai/core-ai.controller.js +31 -1
  78. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  79. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  80. package/dist/core/modules/ai/core-ai.module.js +8 -1
  81. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  82. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  83. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  84. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  85. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  86. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  87. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  88. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  89. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  90. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  91. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  92. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  93. package/dist/core/modules/ai/index.d.ts +7 -0
  94. package/dist/core/modules/ai/index.js +6 -0
  95. package/dist/core/modules/ai/index.js.map +1 -1
  96. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  97. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  98. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  99. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  100. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  101. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  102. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  103. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  104. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  105. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  106. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  107. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  108. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  109. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  110. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  111. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  112. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  113. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  114. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  115. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  116. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  117. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  118. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  119. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  120. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  121. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  122. package/dist/core/modules/better-auth/better-auth.config.js +3 -1
  123. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  124. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  125. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  126. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  127. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  128. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  129. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  130. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  131. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  132. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  133. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  134. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  135. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  136. package/dist/core/modules/better-auth/index.d.ts +1 -0
  137. package/dist/core/modules/better-auth/index.js +1 -0
  138. package/dist/core/modules/better-auth/index.js.map +1 -1
  139. package/dist/core/modules/error-code/error-codes.d.ts +72 -0
  140. package/dist/core/modules/error-code/error-codes.js +64 -0
  141. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  142. package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
  143. package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
  144. package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
  145. package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
  146. package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
  147. package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
  148. package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
  149. package/dist/core/modules/sync/core-sync-cursor.js +77 -0
  150. package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
  151. package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
  152. package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
  153. package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
  154. package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
  155. package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
  156. package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
  157. package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
  158. package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
  159. package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
  160. package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
  161. package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
  162. package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
  163. package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
  164. package/dist/core/modules/sync/core-sync-state.store.js +64 -0
  165. package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
  166. package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
  167. package/dist/core/modules/sync/core-sync.constants.js +23 -0
  168. package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
  169. package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
  170. package/dist/core/modules/sync/core-sync.controller.js +79 -0
  171. package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
  172. package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
  173. package/dist/core/modules/sync/core-sync.decorators.js +28 -0
  174. package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
  175. package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
  176. package/dist/core/modules/sync/core-sync.module.js +50 -0
  177. package/dist/core/modules/sync/core-sync.module.js.map +1 -0
  178. package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
  179. package/dist/core/modules/sync/core-sync.resolver.js +46 -0
  180. package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
  181. package/dist/core/modules/sync/core-sync.service.d.ts +143 -0
  182. package/dist/core/modules/sync/core-sync.service.js +1237 -0
  183. package/dist/core/modules/sync/core-sync.service.js.map +1 -0
  184. package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
  185. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
  186. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
  187. package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
  188. package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
  189. package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
  190. package/dist/core/modules/tenant/core-tenant.guard.js +14 -0
  191. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  192. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  193. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  194. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  195. package/dist/core.module.js +14 -0
  196. package/dist/core.module.js.map +1 -1
  197. package/dist/index.d.ts +16 -0
  198. package/dist/index.js +16 -0
  199. package/dist/index.js.map +1 -1
  200. package/dist/server/modules/error-code/error-codes.d.ts +8 -0
  201. package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
  202. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
  203. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
  204. package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
  205. package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
  206. package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
  207. package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
  208. package/dist/server/modules/sync-note/sync-note.model.js +73 -0
  209. package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
  210. package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
  211. package/dist/server/modules/sync-note/sync-note.module.js +21 -0
  212. package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
  213. package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
  214. package/dist/server/modules/sync-note/sync-note.service.js +35 -0
  215. package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
  216. package/dist/test/test.helper.d.ts +1 -0
  217. package/dist/test/test.helper.js +9 -2
  218. package/dist/test/test.helper.js.map +1 -1
  219. package/docs/REQUEST-LIFECYCLE.md +4 -2
  220. package/docs/error-codes.md +14 -0
  221. package/docs/offline-sync-protocol.md +111 -68
  222. package/docs/security-overrides.md +14 -0
  223. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  224. package/migration-guides/12.1.x-to-12.2.x.md +154 -0
  225. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  226. package/package.json +23 -21
  227. package/src/config.env.ts +6 -2
  228. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  229. package/src/core/common/interfaces/server-options.interface.ts +229 -8
  230. package/src/core/common/interfaces/service-options.interface.ts +15 -0
  231. package/src/core/common/persistence/core-persistence.module.ts +21 -2
  232. package/src/core/common/persistence/data-source.runtime.ts +5 -2
  233. package/src/core/common/persistence/data-store.interface.ts +65 -12
  234. package/src/core/common/persistence/id-format.ts +26 -0
  235. package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
  236. package/src/core/common/persistence/model-metadata.registry.ts +31 -2
  237. package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
  238. package/src/core/common/persistence/mongoose-data-store.ts +171 -35
  239. package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
  240. package/src/core/common/persistence/policies/sync.policy.ts +121 -0
  241. package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
  242. package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
  243. package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
  244. package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
  245. package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
  246. package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
  247. package/src/core/common/persistence/sql/sql-errors.ts +5 -1
  248. package/src/core/common/persistence/sync-access.registry.ts +50 -0
  249. package/src/core/common/persistence/sync-model.ts +270 -0
  250. package/src/core/common/services/crud.service.ts +257 -19
  251. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  252. package/src/core/modules/ai/README.md +163 -25
  253. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  254. package/src/core/modules/ai/core-ai.constants.ts +6 -0
  255. package/src/core/modules/ai/core-ai.controller.ts +36 -1
  256. package/src/core/modules/ai/core-ai.module.ts +12 -0
  257. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  258. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  259. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  260. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  261. package/src/core/modules/ai/index.ts +18 -0
  262. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  263. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  264. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  265. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  266. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  267. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  268. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  269. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  270. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  271. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  272. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  273. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  274. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  275. package/src/core/modules/api-token/README.md +4 -2
  276. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  277. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
  278. package/src/core/modules/better-auth/README.md +32 -1
  279. package/src/core/modules/better-auth/better-auth.config.ts +12 -1
  280. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  281. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  282. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  283. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  284. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  285. package/src/core/modules/better-auth/index.ts +1 -0
  286. package/src/core/modules/error-code/error-codes.ts +77 -0
  287. package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
  288. package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
  289. package/src/core/modules/sync/README.md +198 -0
  290. package/src/core/modules/sync/SECURITY.md +65 -0
  291. package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
  292. package/src/core/modules/sync/core-sync-cursor.ts +117 -0
  293. package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
  294. package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
  295. package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
  296. package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
  297. package/src/core/modules/sync/core-sync-state.store.ts +89 -0
  298. package/src/core/modules/sync/core-sync.constants.ts +51 -0
  299. package/src/core/modules/sync/core-sync.controller.ts +68 -0
  300. package/src/core/modules/sync/core-sync.decorators.ts +84 -0
  301. package/src/core/modules/sync/core-sync.module.ts +75 -0
  302. package/src/core/modules/sync/core-sync.resolver.ts +35 -0
  303. package/src/core/modules/sync/core-sync.service.ts +1773 -0
  304. package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
  305. package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
  306. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  307. package/src/core/modules/tenant/README.md +49 -3
  308. package/src/core/modules/tenant/core-tenant.guard.ts +26 -2
  309. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  310. package/src/core.module.ts +25 -0
  311. package/src/index.ts +21 -0
  312. package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
  313. package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
  314. package/src/server/modules/sync-note/sync-note.model.ts +77 -0
  315. package/src/server/modules/sync-note/sync-note.module.ts +16 -0
  316. package/src/server/modules/sync-note/sync-note.service.ts +25 -0
  317. package/src/test/test.helper.ts +17 -3
@@ -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
 
@@ -396,6 +396,16 @@ factory.registerBuilder('my-provider', (conn) => new MyProvider(conn));
396
396
  `conversationId` to `aiPrompt` to load prior turns and append the new ones.
397
397
  Manage via `createAiConversation` / `findAiConversations` / `getAiConversation`
398
398
  / `deleteAiConversation`.
399
+ A prompt only continues the caller's OWN conversation, on REST, the stream and
400
+ GraphQL alike. An id the caller did not create — another user's, or one that does
401
+ not exist — counts as no conversation: the run reads no history, writes nothing,
402
+ uses no conversation-scoped tool grants, and its response carries no
403
+ `conversationId` (12.3.0; before, the turns were written into the other user's
404
+ conversation). Admins are no exception here: they may READ every conversation
405
+ (`getAiConversation`, `findAiConversations` with `all`), never continue one.
406
+ `CoreAiConversationService.appendMessage(id, message, ownerId)` checks the owner
407
+ itself and writes nothing for anyone else; a project that calls it directly passes
408
+ the id of the user the turn belongs to.
399
409
  - **Audit** (`ai.audit: true`): every run is persisted to `aiInteractions`
400
410
  (admin-readable via `findAiInteractions` / `getAiInteraction`).
401
411
  - **Streaming**: `POST /ai/stream` returns Server-Sent Events (`action`, `token`,
@@ -418,26 +428,32 @@ export class TransferFundsTool extends AiTool {
418
428
  readonly mutating = true; // governed by the confirmation policy
419
429
  readonly destructive = true; // always requires confirmation
420
430
 
421
- // PLAN-MODE pre-flight, without mutating: lets the whole plan be rejected before
422
- // any step runs. It does NOT run in auto mode or over MCP — so it must never be
423
- // the only place a permission is checked.
431
+ // Runs before EVERY execution (12.3.0+): auto mode, plan mode (for the whole plan
432
+ // before any step runs, and again per step) and MCP — on the arguments that will
433
+ // run, after the PreToolUse hooks. Never mutates. A throw counts as a refusal.
424
434
  async authorize(args, context) {
425
435
  const account = await this.accountService.get(args.fromId, context.serviceOptions).catch(() => null);
426
- return { allowed: !!account, reason: account ? undefined : 'No access to source account' };
436
+ return account
437
+ ? { allowed: true, data: account } // reaches execute() as context.authorization.data
438
+ : { allowed: false, reason: 'No access to source account' };
427
439
  }
428
440
 
429
441
  async execute(args, context) {
430
- // The REAL gate, on every path: route through the service with the caller's
431
- // serviceOptions so `@Restricted`, `securityCheck()` and tenant scoping apply.
432
- // Do not rely on authorize() having run.
433
- const account = await this.accountService.getForUser(args.fromId, context.serviceOptions);
442
+ // Still route the write through the service with the caller's serviceOptions, so
443
+ // `@Restricted`, `securityCheck()` and tenant scoping apply: authorize() decides
444
+ // whether to try, the service decides what the user may change.
445
+ const account =
446
+ context.authorization?.data ?? (await this.accountService.getForUser(args.fromId, context.serviceOptions));
434
447
  return this.accountService.transfer(account, args.toId, args.amount, context.serviceOptions);
435
448
  }
436
449
  }
437
450
  ```
438
451
 
439
- > **Do not put a permission check only in `authorize()`.** It runs in plan mode
440
- > only — auto mode (the default) and MCP go straight to `execute()`.
452
+ > **`authorize()` runs on every path since 12.3.0.** Until 12.2 it ran in plan mode
453
+ > only — auto mode (the default) and MCP went straight to `execute()`. A refusal skips
454
+ > the call: the model receives `TOOL_NOT_AVAILABLE` with the `reason` (keep it free of
455
+ > anything the user may not know), an MCP client an error result. The refusal is not
456
+ > recorded as a learning signal.
441
457
 
442
458
  ## Confirmation for changes
443
459
 
@@ -447,6 +463,8 @@ export class TransferFundsTool extends AiTool {
447
463
  admin set `enforced: true`.
448
464
  - When confirmation is needed the response has `requiresConfirmation: true` +
449
465
  `pendingActions`; re-send the prompt with `confirm: true` to proceed.
466
+ - Over MCP the same flags apply through MCP elicitation (12.3.0+) — see
467
+ [Authorization and confirmation over MCP](#authorization-and-confirmation-over-mcp-1230).
450
468
 
451
469
  ## Client metadata & prompt enrichment
452
470
 
@@ -455,6 +473,16 @@ export class TransferFundsTool extends AiTool {
455
473
  - The system prompt is enriched with the user's roles + available tools and optional
456
474
  `ai.documentation` (override `CoreAiPromptBuilderService.getDocumentation()` for RAG).
457
475
 
476
+ **What bounds the size.** `input.context` and `input.metadata` are capped at 4000 characters
477
+ each for the prompt — that cap protects the context window, not the CPU. The work is bounded
478
+ earlier, by the body parser: a request over `bodyParser.json.limit` (100 kB unless configured)
479
+ is refused with 413 before it is parsed. A project that raises the limit for `dataUrl`
480
+ attachments should raise it only as far as its images need; serializing a client block for the
481
+ cap then costs about half of what parsing the same body already did (measured 2026-11-03:
482
+ 2 MB → ~10 ms parse, ~5 ms serialize). The same holds for tool results: `ai.maxToolResultChars`
483
+ keeps them out of the context window, while a tool's own query decides how much data it loads —
484
+ page large result sets in the tool rather than relying on the cap.
485
+
458
486
  ## Self-optimizing prompts (editable templates + governed learning)
459
487
 
460
488
  The system prompt is assembled from **keyed fragments** by `CoreAiPromptBuilderService`,
@@ -506,8 +534,8 @@ so it can recover within the run.
506
534
  > **Security:** learned hints and template overrides only ever **add textual guidance** —
507
535
  > they can never relax the permission model. Tool role-filtering,
508
536
  > `CrudService`/`@Restricted` and `secretFields` are enforced backend-side regardless of
509
- > the prompt. (`authorize()` is _not_ part of that unconditional set — it runs in plan
510
- > mode only; see [Deferred tool schemas](#deferred-tool-schemas-large-registries) below.)
537
+ > the prompt. Since 12.3.0 a tool's `authorize()` belongs to that set too: it runs before
538
+ > every execution, in every mode and over MCP.
511
539
 
512
540
  ```typescript
513
541
  ai: {
@@ -533,6 +561,37 @@ When a user's session history would overflow, the **oldest non-system turns are
533
561
  `ai.maxToolResultChars` (default `12000`) — so long-running conversations never exceed the
534
562
  model's limit.
535
563
 
564
+ **Native tool definitions count against the budget** (12.3.0+): a connection with
565
+ `supportsNativeTools: true` carries every description and schema in the `tools` payload, beside
566
+ the messages, and that payload occupies the same window. **A crowded window is reported**: when
567
+ the system prompt and the tool definitions take more than half of the input budget, the
568
+ framework warns once per connection and process, naming the connection, the numbers and whether
569
+ the window is the connection's own or the `ai.contextWindow` fallback. Before, the only trace was
570
+ a debug line, and the visible symptom was an assistant that answered generically because the
571
+ user's message had been cut.
572
+
573
+ **Guarding the prompt size in a project test:** measure against `ai.contextWindow` (the
574
+ fallback) and say so in the test — it guards the connections without a window of their own,
575
+ the worst case, not the window a pinned or detected connection has. Raising its limit removes
576
+ that guard; shrink the catalog instead (`deferToolSchemas`, `deferToolSummaryChars`, fewer
577
+ tools per role).
578
+
579
+ ## Tool catalog for native connections (12.3.0+)
580
+
581
+ A connection with native tool calling receives every tool's description and parameter schema
582
+ in the `tools` payload. Its system-prompt catalog therefore lists **names only** — repeating
583
+ the descriptions (and, without `deferToolSchemas`, the schemas) paid for every tool twice.
584
+ Emulated connections and plan mode keep the full catalog: there it is the model's only source.
585
+
586
+ ```typescript
587
+ ai: {
588
+ nativeToolCatalog: 'full', // restores descriptions (and schemas) in the native catalog; default 'names'
589
+ }
590
+ ```
591
+
592
+ Use `'full'` only for a model that demonstrably picks tools better with the descriptions in the
593
+ prompt; the cost is the duplicated text on every call.
594
+
536
595
  ## Deferred tool schemas (large registries)
537
596
 
538
597
  The system prompt normally carries every tool's full JSON parameter schema. With a large
@@ -558,9 +617,10 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
558
617
 
559
618
  > **Scoped to the emulated tool protocol, not to the provider.** In auto mode a connection
560
619
  > with `supportsNativeTools: true` receives every full description and JSON schema through
561
- > the native `tools` payload anyway, so truncation is skipped there and no `search_tools`
562
- > banner is emitted — telling the model a description was cut while handing it the full text
563
- > in the same request would only buy a wasted round-trip against `maxIterations`.
620
+ > the native `tools` payload anyway, and its catalog lists names only (see above), so
621
+ > `deferToolSummaryChars` has no effect there and no `search_tools` banner is emitted —
622
+ > telling the model a description was cut while handing it the full text in the same request
623
+ > would only buy a wasted round-trip against `maxIterations`.
564
624
  >
565
625
  > **Plan mode always uses the emulated protocol**, so a native connection IS truncated there.
566
626
  > Because plan mode returns a complete plan in one call without executing anything, the model
@@ -577,10 +637,8 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
577
637
  > confirmation gate reads a tool's `mutating`/`destructive` **flags**, never its description
578
638
  > — so a truncated "requires confirmation" sentence cannot disable it.
579
639
  >
580
- > Note `AiTool.authorize()` is **not** part of that chain in every mode: it runs in **plan
581
- > mode only**. In auto mode (the default) and over MCP, tools go straight to `execute()` — so
582
- > data-level checks (ownership, tenant scope) must live **inside `execute()`**, routed through
583
- > `CrudService` with `context.serviceOptions`, not in `authorize()` alone.
640
+ > `AiTool.authorize()` is part of that chain on every path since 12.3.0 (auto mode, plan
641
+ > mode, MCP), and it reads the call's arguments, never the description.
584
642
 
585
643
  ## Token budgets & usage
586
644
 
@@ -671,6 +729,53 @@ the server log; the response carries only a stable `#LTNS_0901` code.
671
729
  - Answering a foreign id `404` rather than `403` is deliberate — a 403 would confirm
672
730
  that the id exists, and the id travels in a response header and through client logs.
673
731
 
732
+ ### Authorization and confirmation over MCP (12.3.0+)
733
+
734
+ `tools/call` applies the same gates as the chat orchestrator, in this order:
735
+
736
+ 1. **Role filter** — the tool must be in the user's set (`AiToolRegistry.forUser()`).
737
+ 2. **Tool policies** — the admin's `aiToolPolicies` rules for this call: `deny` answers an
738
+ error result, `ask` asks in step 4 whatever else is configured.
739
+ 3. **`authorize()`** — the tool's data-level check on the call's arguments. A refusal, a
740
+ thrown error or an `allowed` that is not `true` answers an error result; nothing runs.
741
+ 4. **Confirmation** — a `destructive` tool always, a `mutating` tool when
742
+ `ai.confirmation.mutating` says so (`enforced`, else `default`; MCP has no per-request
743
+ `requireConfirmation`), as long as MCP asks this user (below). The server asks through
744
+ **MCP elicitation** (a yes/no form the client shows) and runs the tool only on an
745
+ explicit yes. A no, a cancelled dialog, an error or a timeout (5 minutes) means "not
746
+ run". A client without the `elicitation` capability gets an error result instead of an
747
+ unconfirmed run.
748
+ 5. **`execute()`** — with `context.authorization` from step 3.
749
+
750
+ **Whether MCP asks** is a server default each user may switch for themselves, like a
751
+ "bypass permissions" mode:
752
+
753
+ | `ai.mcp.confirmation` | Asks a user who has not chosen | A user's own switch |
754
+ | ------------------------------------ | ------------------------------ | -------------------------------------------- |
755
+ | unset, `true`, `{}` | yes | may switch it off |
756
+ | `false` | no | may switch it on |
757
+ | `{ default: true, enforced: true }` | yes | ignored; a change answers 403 (`#LTNS_0609`) |
758
+ | `{ default: false, enforced: true }` | no | ignored; a change answers 403 |
759
+
760
+ The user's switch is `PUT /ai/settings` with `{ mcpConfirmation: false }` (GraphQL
761
+ `updateAiSettings`), read back with `GET /ai/settings` (`aiSettings`), which answers the
762
+ effective value and `mcpConfirmationEnforced` so a UI can show the switch disabled. It is
763
+ stored per user (`aiUserSettings`, one record per user) and touches nobody else's calls.
764
+ Only `false` switches the default off and only `enforced: true` enforces — a misspelt value
765
+ never relaxes the gate. `authorize()` and the admin's `deny` / `ask` rules apply whatever
766
+ the switch says.
767
+
768
+ `tools/list` gives a `destructive` tool `annotations: { destructiveHint: true, readOnlyHint: false }`
769
+ and a `mutating` one `{ destructiveHint: false, readOnlyHint: false }` — hints for the client's own
770
+ UI, not a security boundary. A tool with neither flag carries no annotations: it may still write,
771
+ so it is never advertised as read-only, and clients keep the spec's cautious defaults.
772
+
773
+ The confirmation keeps the model from acting without the user; it cannot stop a hostile
774
+ client, which holds the user's credential and could call the API directly. The boundary
775
+ against that stays the role filter, `authorize()` and the `CrudService` inside `execute()`.
776
+ The question is English (an MCP request carries no language); override
777
+ `CoreAiMcpService.confirmationPrompt()` to localize it.
778
+
674
779
  ### Multi-replica / sticky sessions
675
780
 
676
781
  An MCP session owns a **live Streamable-HTTP transport** — an open response stream
@@ -696,6 +801,16 @@ and sends operators looking in the wrong place.
696
801
 
697
802
  Without Redis nothing changes: unknown ids answer `404` exactly as before.
698
803
 
804
+ **No sticky sessions available?** Set `ai.mcp.stateless: true` (12.3.0+). Every POST then builds its own
805
+ server and transport for the authenticated user (the SDK's stateless mode, `sessionIdGenerator: undefined`),
806
+ no `mcp-session-id` is issued, a session id a client still sends is ignored, and GET / DELETE answer `405`
807
+ with `Allow: POST`. Any replica can answer any request, and the user's rights and tenant are read per request
808
+ by construction. What is lost is what a session carries: server-initiated messages, the SSE stream, and
809
+ server-initiated requests — **MCP elicitation among them**. A tool that needs confirmation (above) is
810
+ therefore refused in this mode unless the user switched the question off (`PUT /ai/settings`) or
811
+ `ai.mcp.confirmation` is `false`. A server that only offers read tools is not affected.
812
+ `testHelper.mcpSession()` follows the setting (no session id expected or sent; `sessionId` is `''`).
813
+
699
814
  ### OAuth 2.1 (`ai.mcp.oauth: true`)
700
815
 
701
816
  For generic MCP clients that auto-discover + register:
@@ -767,7 +882,13 @@ user decides whether an MCP client may act for them:
767
882
  - **Allow** issues the code only with the page's CSRF token, signed with the OAuth secret and bound to
768
883
  the user, the client, the redirect URI and the PKCE challenge (10 minutes); a stale token shows the
769
884
  form again, a forged or tampered one too, and nothing is issued. **Deny** sends the client back with
770
- `access_denied`.
885
+ `access_denied` — with the same token since 12.3.0; without it the form is shown again.
886
+ - **A malformed request** (a missing `code_challenge`, a wrong `response_type`, …) is answered with `400`
887
+ and `{ error, error_description }`, never redirected to the client (12.3.0). The redirect URI is whatever
888
+ the client's registrant wrote, so the SDK's error redirect made `/authorize` an open redirect on this
889
+ domain (RFC 9700 §4.11.2). Registration itself stays open — claude.ai, ChatGPT, Cursor and Claude Code
890
+ register their own URIs. Only what happens once the request reached the provider (the user's Allow or
891
+ Deny, and refusals raised in `authorizeConsent()`) still redirects.
771
892
 
772
893
  The page carries its own `Content-Security-Policy` (script and style by nonce, `frame-ancestors 'none'`,
773
894
  no `form-action`, which browsers would apply to the redirect to the client), `X-Frame-Options: DENY`,
@@ -792,8 +913,25 @@ the page offers no sign-in. Its sign-in goes through the IAM sign-in controller,
792
913
  hands Better-Auth the request so Better-Auth's own login-CSRF protection runs — otherwise a hostile page
793
914
  could sign the visitor in to the attacker's account before they approve a client.
794
915
 
795
- **Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it (keep every value
796
- escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
916
+ **Your own wording and look (12.3.0+).** Override `renderConsentPage(view)` and call
917
+ `renderMcpOAuthConsentPage(view, { texts, styles })`: `texts` replaces single texts per locale (merged over
918
+ the built-in ones, still escaped), `styles` is CSS appended inside the page's nonce'd `<style>` element. The
919
+ markup, the scripts and the security headers stay the framework's, so the page keeps receiving their fixes.
920
+ Fill both from code or configuration only, never from user or tenant input: a `<` in `styles` is neutralized
921
+ so it cannot end the `<style>` element, and the page's CSP blocks what CSS could load from elsewhere, but CSS
922
+ still decides what the page shows.
923
+
924
+ ```typescript
925
+ protected override renderConsentPage(view: IMcpOAuthConsentView): string {
926
+ return renderMcpOAuthConsentPage(view, {
927
+ styles: ':root{--accent:#0b5}',
928
+ texts: { de: { intro: '{client} möchte in Ihrem Namen auf {app} zugreifen.' } },
929
+ });
930
+ }
931
+ ```
932
+
933
+ **Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it beyond that (keep every
934
+ value escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
797
935
  → sign in, GET → show which client, POST with a CSRF token → `saveAuthorizationCode()` and redirect;
798
936
  refuse with `AccessDeniedError` from `this.loadOAuthErrors()`. Register the subclass via
799
937
  `CoreModule.forRoot(env, { ai: { mcpOAuthService } })`, so the MCP controller and the endpoints share it.
@@ -802,9 +940,9 @@ production secret check runs again before the endpoints are mounted (without a s
802
940
  environment outside `local` / `ci` / `e2e` refuses to boot and to sign tokens at all — deployed `develop` and `test`
803
941
  stages included).
804
942
 
805
- Known property of the SDK: `/authorize` redirects its error answers to the client's registered
806
- redirect URI, and any client may register any https URI — an open redirect in the sense of RFC 9700
807
- §4.11.2, carrying only an OAuth error code.
943
+ Until 12.3.0 the SDK's error redirect made `/authorize` an open redirect in the sense of RFC 9700
944
+ §4.11.2, since any client may register any https URI; the framework now answers those errors itself
945
+ (see "A malformed request" under the consent page above).
808
946
 
809
947
  **Refusals are OAuth errors (11.43.0).** The SDK's handlers answer an error with its OAuth code only
810
948
  when it is one of the SDK's own `OAuthError` classes; anything else becomes `500 server_error`. The
@@ -48,7 +48,8 @@ import { AiToolUser } from './tools/ai-tool.registry.js';
48
48
  * sessions behind a load balancer. When Redis is configured, session ids are additionally
49
49
  * registered in a shared registry (replica + owning user), which turns a mis-routed request
50
50
  * into an explicit 409 naming the owning replica instead of a misleading "unknown session" 404.
51
- * Without Redis the behavior is unchanged.
51
+ * Without Redis the behavior is unchanged. `ai.mcp.stateless` removes the requirement altogether:
52
+ * every request then builds its own server and transport, and any replica can answer it.
52
53
  *
53
54
  * The registry entry records the OWNING USER, and only that user is ever told about the 409.
54
55
  * Otherwise the cross-replica path would undo the local one: an authenticated caller probing
@@ -130,6 +131,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
130
131
  return;
131
132
  }
132
133
 
134
+ if (this.statelessEnabled()) {
135
+ await this.handleStatelessPost(req, res, user);
136
+ return;
137
+ }
138
+
133
139
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
134
140
  let entry = sessionId ? this.transports.get(sessionId) : undefined;
135
141
 
@@ -342,6 +348,42 @@ export class CoreAiMcpController implements OnModuleDestroy {
342
348
  return body?.method === 'initialize';
343
349
  }
344
350
 
351
+ /**
352
+ * Whether `/ai/mcp` runs without sessions (`ai.mcp.stateless`).
353
+ */
354
+ protected statelessEnabled(): boolean {
355
+ const mcp = ConfigService.get<{ stateless?: boolean }>('ai.mcp');
356
+ return typeof mcp === 'object' && mcp?.stateless === true;
357
+ }
358
+
359
+ /**
360
+ * One request, one server: the stateless mode of the SDK transport (`sessionIdGenerator: undefined`).
361
+ *
362
+ * Nothing outlives the response, so nothing has to reach the same replica again — the reason this
363
+ * mode exists. Rights are the user's as of THIS request by construction. A `mcp-session-id` a client
364
+ * still sends (from before a switch to this mode) is ignored rather than answered 404: there is no
365
+ * session it could refer to, and a 404 would only send the client into a reconnect.
366
+ */
367
+ protected async handleStatelessPost(req: Request, res: Response, user: AiToolUser): Promise<void> {
368
+ let StreamableHTTPServerTransport: any;
369
+ try {
370
+ ({ StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js'));
371
+ } catch (err) {
372
+ // Same lazy import and 503 reasoning as the session path in handlePost().
373
+ return this.mcpUnavailable(res, err as Error);
374
+ }
375
+ const transport: any = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
376
+ const server = await this.mcpService.createServer(user);
377
+ // Released with the response — an aborted request included, so a client that hangs up cannot
378
+ // leave a server behind.
379
+ res.on('close', () => {
380
+ Promise.resolve(transport.close?.()).catch(() => undefined);
381
+ Promise.resolve(server.close?.()).catch(() => undefined);
382
+ });
383
+ await server.connect(transport);
384
+ await transport.handleRequest(req, res, req.body);
385
+ }
386
+
345
387
  /**
346
388
  * Whether the OAuth 2.1 layer is enabled via `ai.mcp.oauth`.
347
389
  */
@@ -359,6 +401,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
359
401
  this.unauthorized(res);
360
402
  return;
361
403
  }
404
+ // Without sessions there is no stream to open and nothing to end (MCP spec: 405 for GET then).
405
+ if (this.statelessEnabled()) {
406
+ res.status(405).set('Allow', 'POST').json({ error: 'Method not allowed: this MCP server is stateless' });
407
+ return;
408
+ }
362
409
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
363
410
  const held = sessionId ? this.transports.get(sessionId) : undefined;
364
411
  // Same ownership rule as the POST path: another user's session is not ours to serve.
@@ -91,6 +91,12 @@ export const AI_TOOL_POLICY_CLASS = 'AI_TOOL_POLICY_CLASS';
91
91
  /** Mongoose injection token. */
92
92
  export const AI_TOOL_POLICY_MODEL = 'AiToolPolicy';
93
93
 
94
+ // ---- user-setting ----
95
+ /** DI token for the model constructor (used by CrudService mapping). */
96
+ export const AI_USER_SETTING_CLASS = 'AI_USER_SETTING_CLASS';
97
+ /** Mongoose injection token. */
98
+ export const AI_USER_SETTING_MODEL = 'AiUserSetting';
99
+
94
100
  // ---- MCP OAuth (stored natively on MongoDB; these models give them their tables on a SQL source) ----
95
101
  /** Mongoose model name of the registered OAuth clients (`aiMcpOAuthClients`). */
96
102
  export const AI_MCP_OAUTH_CLIENT_MODEL = 'AiMcpOAuthClient';
@@ -1,4 +1,4 @@
1
- import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res } from '@nestjs/common';
1
+ import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Query, Res } from '@nestjs/common';
2
2
  import type { Response } from 'express';
3
3
 
4
4
  import { RESTServiceOptions } from '../../common/decorators/rest-service-options.decorator.js';
@@ -19,6 +19,7 @@ import { CoreAiPromptUpdateInput } from './inputs/core-ai-prompt-update.input.js
19
19
  import { CoreAiSlotCreateInput } from './inputs/core-ai-slot-create.input.js';
20
20
  import { CoreAiSlotUpdateInput } from './inputs/core-ai-slot-update.input.js';
21
21
  import { CoreAiPromptInput } from './inputs/core-ai-prompt.input.js';
22
+ import { CoreAiSettingsInput } from './inputs/core-ai-settings.input.js';
22
23
  import { CoreAiAvailableConnection } from './models/core-ai-available-connection.model.js';
23
24
  import { CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
24
25
  import { CoreAiConnectionPreference } from './models/core-ai-connection-preference.model.js';
@@ -29,6 +30,7 @@ import { CoreAiPromptHint } from './models/core-ai-prompt-hint.model.js';
29
30
  import { CoreAiPrompt } from './models/core-ai-prompt.model.js';
30
31
  import { CoreAiSlot } from './models/core-ai-slot.model.js';
31
32
  import { CoreAiResponse } from './models/core-ai-response.model.js';
33
+ import { CoreAiSettings } from './models/core-ai-settings.model.js';
32
34
  import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
33
35
  import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
34
36
  import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
@@ -37,6 +39,7 @@ import { CoreAiConnectionService } from './services/core-ai-connection.service.j
37
39
  import { CoreAiConversationService } from './services/core-ai-conversation.service.js';
38
40
  import { CoreAiInteractionService } from './services/core-ai-interaction.service.js';
39
41
  import { CoreAiPlaceholderRegistry } from './services/core-ai-placeholder.registry.js';
42
+ import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
40
43
  import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.js';
41
44
  import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
42
45
  import { CoreAiSlotService } from './services/core-ai-slot.service.js';
@@ -67,6 +70,13 @@ export class CoreAiController {
67
70
  protected readonly placeholderRegistry: CoreAiPlaceholderRegistry,
68
71
  ) {}
69
72
 
73
+ /**
74
+ * Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
75
+ * written before it keeps compiling and still gets the settings endpoints.
76
+ */
77
+ @Inject(CoreAiUserSettingService)
78
+ protected readonly userSettingService: CoreAiUserSettingService;
79
+
70
80
  /**
71
81
  * Send a prompt to the AI assistant.
72
82
  */
@@ -362,6 +372,31 @@ export class CoreAiController {
362
372
  return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
363
373
  }
364
374
 
375
+ // ===================================================================================================================
376
+ // Personal settings
377
+ // ===================================================================================================================
378
+
379
+ /**
380
+ * The current user's AI settings as they take effect.
381
+ */
382
+ @Get('settings')
383
+ @Roles(RoleEnum.S_USER)
384
+ async settings(@RESTServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
385
+ return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
386
+ }
387
+
388
+ /**
389
+ * Change the current user's own AI settings (403 for a setting the server enforces).
390
+ */
391
+ @Put('settings')
392
+ @Roles(RoleEnum.S_USER)
393
+ async updateSettings(
394
+ @RESTServiceOptions() serviceOptions: ServiceOptions,
395
+ @Body() input: CoreAiSettingsInput,
396
+ ): Promise<CoreAiSettings> {
397
+ return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
398
+ }
399
+
365
400
  /**
366
401
  * Find AI budget limits (admin).
367
402
  */
@@ -11,6 +11,8 @@ import {
11
11
  AI_MCP_OAUTH_CLIENT_MODEL,
12
12
  AI_MCP_OAUTH_CODE_MODEL,
13
13
  AI_MCP_OAUTH_REFRESH_TOKEN_MODEL,
14
+ AI_USER_SETTING_CLASS,
15
+ AI_USER_SETTING_MODEL,
14
16
  } from './core-ai.constants.js';
15
17
  import { AiBudgetLimitSchema, CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
16
18
  import {
@@ -31,6 +33,7 @@ import { AiSlotSchema, CoreAiSlot } from './models/core-ai-slot.model.js';
31
33
  import { AiModeSchema, CoreAiMode } from './models/core-ai-mode.model.js';
32
34
  import { AiToolGrantSchema, CoreAiToolGrant } from './models/core-ai-tool-grant.model.js';
33
35
  import { AiToolPolicySchema, CoreAiToolPolicy } from './models/core-ai-tool-policy.model.js';
36
+ import { AiUserSettingSchema, CoreAiUserSetting } from './models/core-ai-user-setting.model.js';
34
37
  import { LlmProviderFactory } from './providers/llm-provider.factory.js';
35
38
  import { AiCryptoService } from './services/ai-crypto.service.js';
36
39
  import {
@@ -79,6 +82,7 @@ import {
79
82
  CoreAiToolPolicyService,
80
83
  } from './services/core-ai-tool-policy.service.js';
81
84
  import { AI_MODE_CLASS, AI_MODE_MODEL, CoreAiModeService } from './services/core-ai-mode.service.js';
85
+ import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
82
86
  import { CoreAiMcpClientService } from './services/core-ai-mcp-client.service.js';
83
87
  import { CoreAiService } from './services/core-ai.service.js';
84
88
  import { AiHookRegistry } from './hooks/ai-hook.registry.js';
@@ -149,6 +153,9 @@ export interface CoreAiModuleOptions {
149
153
  /** Custom scoped tool-policy store (extends CoreAiToolPolicyService). */
150
154
  toolPolicyService?: Type<CoreAiToolPolicyService>;
151
155
 
156
+ /** Custom personal-settings store (extends CoreAiUserSettingService). */
157
+ userSettingService?: Type<CoreAiUserSettingService>;
158
+
152
159
  /** Custom named-mode store (extends CoreAiModeService). */
153
160
  modeService?: Type<CoreAiModeService>;
154
161
 
@@ -208,6 +215,7 @@ export class CoreAiModule {
208
215
  { model: CoreAiToolGrant, name: AI_TOOL_GRANT_MODEL, schema: AiToolGrantSchema },
209
216
  { model: CoreAiToolPolicy, name: AI_TOOL_POLICY_MODEL, schema: AiToolPolicySchema },
210
217
  { model: CoreAiMode, name: AI_MODE_MODEL, schema: AiModeSchema },
218
+ { model: CoreAiUserSetting, name: AI_USER_SETTING_MODEL, schema: AiUserSettingSchema },
211
219
  // Stored natively on MongoDB; the models give them their tables on a SQL source
212
220
  { name: AI_MCP_OAUTH_CLIENT_MODEL, schema: AiMcpOAuthClientSchema },
213
221
  { name: AI_MCP_OAUTH_CODE_MODEL, schema: AiMcpOAuthCodeSchema },
@@ -215,6 +223,7 @@ export class CoreAiModule {
215
223
  ]);
216
224
  const ToolGrantServiceClass = options.toolGrantService || CoreAiToolGrantService;
217
225
  const ToolPolicyServiceClass = options.toolPolicyService || CoreAiToolPolicyService;
226
+ const UserSettingServiceClass = options.userSettingService || CoreAiUserSettingService;
218
227
  const ModeServiceClass = options.modeService || CoreAiModeService;
219
228
  const McpClientServiceClass = options.mcpClientService || CoreAiMcpClientService;
220
229
  const McpOAuthServiceClass = options.mcpOAuthService || CoreAiMcpOAuthService;
@@ -250,6 +259,7 @@ export class CoreAiModule {
250
259
  CoreAiService,
251
260
  CoreAiToolGrantService,
252
261
  CoreAiToolPolicyService,
262
+ CoreAiUserSettingService,
253
263
  CoreAiModeService,
254
264
  CoreAiMcpClientService,
255
265
  LlmProviderFactory,
@@ -275,6 +285,7 @@ export class CoreAiModule {
275
285
  { provide: AI_TOOL_GRANT_CLASS, useValue: CoreAiToolGrant },
276
286
  { provide: AI_TOOL_POLICY_CLASS, useValue: CoreAiToolPolicy },
277
287
  { provide: AI_MODE_CLASS, useValue: CoreAiMode },
288
+ { provide: AI_USER_SETTING_CLASS, useValue: CoreAiUserSetting },
278
289
  { provide: CoreAiBudgetService, useClass: BudgetServiceClass },
279
290
  { provide: CoreAiConversationService, useClass: ConversationServiceClass },
280
291
  { provide: CoreAiInteractionService, useClass: InteractionServiceClass },
@@ -293,6 +304,7 @@ export class CoreAiModule {
293
304
  { provide: CoreAiService, useClass: ServiceClass },
294
305
  { provide: CoreAiToolGrantService, useClass: ToolGrantServiceClass },
295
306
  { provide: CoreAiToolPolicyService, useClass: ToolPolicyServiceClass },
307
+ { provide: CoreAiUserSettingService, useClass: UserSettingServiceClass },
296
308
  { provide: CoreAiModeService, useClass: ModeServiceClass },
297
309
  { provide: CoreAiMcpClientService, useClass: McpClientServiceClass },
298
310
  { provide: CoreAiMcpOAuthService, useClass: McpOAuthServiceClass },