@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
@@ -89,6 +89,20 @@ overrides:
89
89
  '@graphql-tools/utils@>=12.0.0 <12.1.0': '12.1.0'
90
90
  ```
91
91
 
92
+ ### Advisories no override can fix yet: `minimatch` and `glob` (2026-10-31)
93
+
94
+ [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (minimatch `< 11.0.1`, moderate, DoS
95
+ through globparts amplification) and [GHSA-3g3m-2fjr-6pw4](https://github.com/advisories/GHSA-3g3m-2fjr-6pw4)
96
+ (glob `<= 14.0.1`, moderate) are fixed only in new majors that no consumer can take yet: `@nestjs/cli` 12.0.11
97
+ pins minimatch 10.2.6 exactly, ts-morph 28 (`@ts-morph/common` 0.29), nodemon, `@swc/cli` and glob 13 need the 9.x /
98
+ 10.x lines, rimraf 6.1.3 needs glob 13 — all on their newest release. An override across the major is no way out
99
+ (minimatch 11 removed `makeRe()` and `Minimatch#regexp`). nest-server suppresses both in its own repository with
100
+ `auditConfig.unusableFixConsumers`, which `check:overrides` re-tests on every run. What it found for its own tree:
101
+ the only production path is `@lenne.tech/nest-server > ts-morph > @ts-morph/common > minimatch`, used by the
102
+ Permissions module's scanner with globs the developer writes; every other path is dev tooling. Your project's tree
103
+ may have other paths, so check `pnpm why minimatch --prod` and `pnpm why glob --prod` before you suppress the same
104
+ way, and remove the entries once your consumers move to minimatch 11 / glob 14.
105
+
92
106
  ### Only on nest-server 11.x: `ws` and `js-yaml`
93
107
 
94
108
  `@nestjs/graphql` 13 and `@nestjs/swagger` 11 exact-pin vulnerable versions of these two; their 12.x
@@ -256,5 +256,4 @@ graphql-upload 17 accepts `@types/express` 4 and 5.
256
256
 
257
257
  ## References
258
258
 
259
- - Plan and decisions: `.claude/plans/db-agnostic/PLAN.md` (framework repository)
260
259
  - Reference project: [nest-server-starter](https://github.com/lenneTech/nest-server-starter)
@@ -0,0 +1,154 @@
1
+ # Migration Guide: 12.1.x → 12.2.x
2
+
3
+ ## No breaking change
4
+
5
+ 12.2.0 adds **offline sync**: mobile apps (React Native, Capacitor) with SQLite, web apps and PWAs with IndexedDB, and
6
+ desktop or Node clients read and write while disconnected and reconcile with the server afterwards. It is opt-in in
7
+ two places — the `sync` configuration and a declaration per model — and **without both nothing changes**: no field,
8
+ index, route or code path is added, and a project's test suite stays as it is (protocol §13). It is a MINOR because of
9
+ its size, not because anything existing moves.
10
+
11
+ ## Overview
12
+
13
+ | Category | Details |
14
+ |---|---|
15
+ | **Breaking Changes** | None |
16
+ | **New Features** | Synchronized models: `version`, tombstones, live-only unique indexes, a change order on every database (§2). The sync module: `GET/POST /sync/{model}` with cursor pull, idempotent push, conflict strategies, client ids, resync (§3). Change hints over SSE and GraphQL, tombstone purge, the `syncUpdate` migration helper (§4). `ServiceOptions.expectedVersion` for conditional changes of a synchronized model (§2) |
17
+ | **Bugfixes** | None that change existing behaviour |
18
+ | **Migration Effort** | None for an update. Offering a model to offline clients: about an hour per model, plus the client (§5) |
19
+
20
+ ## Quick Migration
21
+
22
+ ```bash
23
+ pnpm add @lenne.tech/nest-server@12.2.0
24
+ pnpm run build
25
+ pnpm test
26
+ ```
27
+
28
+ No dependency of a project changes. **Vendor mode**: run `/lt-dev:backend:update-nest-server-core` as usual; the new
29
+ module is `src/core/modules/sync/`.
30
+
31
+ ## 1. What changes for an existing project
32
+
33
+ Nothing, unless it switches offline sync on. The framework's own models are not synchronized, the `CrudService` keeps
34
+ its behaviour for every model that does not declare `sync`, and no route is registered without the `sync`
35
+ configuration.
36
+
37
+ ## 2. Synchronized models
38
+
39
+ A model takes part in offline sync when it is declared with `sync` and registered with
40
+ `CorePersistenceModule.forFeature()`:
41
+
42
+ ```typescript
43
+ @PersistedModel({
44
+ sync: {
45
+ roles: { create: [RoleEnum.S_USER], delete: [RoleEnum.S_CREATOR], pull: [RoleEnum.S_USER], update: [RoleEnum.S_CREATOR] },
46
+ strategy: 'merge',
47
+ },
48
+ })
49
+ @Schema({ timestamps: true })
50
+ @ObjectType()
51
+ export class Note extends CorePersistenceModel { … }
52
+ ```
53
+
54
+ The store then keeps three system fields on every write path — `version`, `deletedAt` and the change position
55
+ (`syncSeq` on SQL, `syncAt` on MongoDB) — never settable through an input:
56
+
57
+ - **Deletes leave tombstones**: `CrudService.delete()` and every port delete keep the record as a tombstone without
58
+ content, so clients learn of the deletion. Reads leave tombstones out.
59
+ - **Unique indexes apply to live records only**, so delete-then-recreate with the same value works.
60
+ - **Conditional changes**: `ServiceOptions.expectedVersion` on `update()` / `delete()` is one statement with the
61
+ version as its condition; another version answers `409` (`VersionConflictError`). Only a synchronized model has a
62
+ `version`: on any other model `expectedVersion` throws `UnsupportedCapabilityError`.
63
+
64
+ **Making an EXISTING model synchronized is a migration** on every database — the boot refuses with the statements to
65
+ run (SQL: the new column and live-only indexes; MongoDB: `SyncMigrationRequiredError`, dropping the old unique indexes
66
+ and stamping every document). Details: `.claude/rules/persistence.md` → "Synchronized models".
67
+
68
+ ## 3. The sync module
69
+
70
+ ```typescript
71
+ // config.env.ts
72
+ sync: {
73
+ cursorSecret: process.env.SYNC_CURSOR_SECRET, // else SECRETS_ENCRYPTION_KEY; required in production/staging
74
+ },
75
+ ```
76
+
77
+ ```typescript
78
+ @SyncService({ createInput: NoteCreateInput, model: Note, updateInput: NoteInput })
79
+ @Injectable()
80
+ export class NoteService extends CrudService<Note, NoteCreateInput, NoteInput> { … }
81
+ ```
82
+
83
+ - `GET /sync/{model}`: one page of changes after an opaque, signed cursor; `410` with a reason when the client must
84
+ resync (`invalid`, `horizon`, `access`).
85
+ - `POST /sync/{model}`: items processed in order, each atomic; idempotent through mutation ids; client-generated ids
86
+ (ObjectId-hex with a random tail, or uuidv7 for new models); strategies `reject`, `lww`, `merge` (`@SyncField()` for
87
+ fields), `custom` (`resolveSyncConflict()` on the service).
88
+ - Every pull and push runs through the model's `CrudService` with the roles from `sync.roles` — the roles of the
89
+ project's own routes do not apply here, and an operation without roles is ADMIN only.
90
+ - Without `X-Tenant-Id` a request spans every tenant the caller belongs to.
91
+
92
+ The protocol for client authors: `docs/offline-sync-protocol.md`. The module: `src/core/modules/sync/README.md`.
93
+
94
+ ## 4. Hints, purge, data migrations
95
+
96
+ - **Hints**: `GET /sync/hints` (Server-Sent Events, event `change`) and the GraphQL subscription `syncHint(models)` tell
97
+ a client "pull now" — `{ model, at }` only, to callers with the pull roles and access to the tenant. With `redis`
98
+ every replica's subscribers receive them. `sync.hints: false` switches them off.
99
+ - **Purge**: tombstones older than `sync.retention.tombstonesDays` (90) are deleted every hour on one replica, after
100
+ the retention horizon moved past them; a client behind it resyncs.
101
+ - **Data migrations** of a synchronized SQL table: `runSqlMigration(ownerUrl, [{ syncUpdate: { table, set, where } }])`
102
+ raises `version` and numbers the rows like a store write. A bare `UPDATE` would let a stale client overwrite the
103
+ migrated value without a conflict.
104
+ - **Writes past the CrudService** (data store, native API) call `CoreSyncService.notifyChange()` for hints and
105
+ `bumpAccessEpoch()` when a record changes tenant or owner.
106
+
107
+ ## 5. Offering a model to offline clients — checklist
108
+
109
+ Follow `src/core/modules/sync/INTEGRATION-CHECKLIST.md`. In short:
110
+
111
+ 1. Declare `@PersistedModel({ sync: { roles, strategy } })` and register the model with `CorePersistenceModule.forFeature()`.
112
+ 2. Put `@SyncService({ model, createInput, updateInput })` on its `CrudService`.
113
+ 3. Add `sync: {}` (with a `cursorSecret` for deployed stages) to the configuration.
114
+ 4. Give the client its duties: delete every local record, cursor and unsent change on logout and on an account switch
115
+ (`src/core/modules/sync/SECURITY.md`).
116
+
117
+ The reference model is `src/server/modules/sync-note/`; the framework's own test clients (a PWA on IndexedDB and a Node
118
+ client on SQLite, `tests/clients/` in the framework repository) show a complete client in a few hundred lines.
119
+
120
+ ## Compatibility Notes
121
+
122
+ | Pattern | Status |
123
+ |---|---|
124
+ | Project without `sync` configuration | Unchanged |
125
+ | Model without `sync` in a project with `sync` | Unchanged; not reachable under `/sync` |
126
+ | `CrudService.delete()` on a synchronized model | Leaves a tombstone; a hard delete is the purge's alone |
127
+ | Project controller extending `CoreSyncController` | Fine — the hint route is `CoreSyncHintsController`'s, registered first, so re-declaring `pull()` cannot shadow it |
128
+ | TTL index on a synchronized model | Refused at registration — an expired record would vanish without a tombstone |
129
+
130
+ ## Troubleshooting
131
+
132
+ | Symptom | Cause | Fix |
133
+ |---|---|---|
134
+ | Boot fails: `cursorSecret` missing | Production / staging without `sync.cursorSecret` or `SECRETS_ENCRYPTION_KEY` | Set one of them |
135
+ | Boot fails with SQL statements or `SyncMigrationRequiredError` | An existing model was declared `sync` | Run the printed migration (§2) |
136
+ | Warning "declares no sync roles" | `sync.roles` missing an operation | Declare roles per operation; otherwise ADMIN only |
137
+ | `400` on a push | `Content-Type` is not `application/json` | Send JSON |
138
+ | `403` on a cookie push | `Sec-Fetch-Site: cross-site` or an untrusted `Origin` | Push from a trusted origin (`appUrl`, `baseUrl`, `cors.allowedOrigins`) |
139
+ | Every cursor answers `410 invalid` | `cursorSecret` changed | Expected: clients resync once |
140
+ | Boot fails on MySQL / MariaDB with error 1419 (`You do not have the SUPER privilege and binary logging is enabled`) once a model is declared `sync` | The change-order triggers are created by the owner account (`database.ownerUrl`); with binary logging on (the MySQL 8.4 default) that needs `SUPER` or `log_bin_trust_function_creators=1` | Set `log_bin_trust_function_creators=1` on the server (the parameter group on managed MySQL), or grant the owner `SUPER`. The application account needs neither |
141
+ | A client keeps records after its user left a tenant | The client treats only `410 access` as access loss | A pull answering `403` (pull roles withdrawn, or `X-Tenant-Id` naming a left tenant) is access loss too: drop that model's local records and cursor (`SECURITY.md`) |
142
+ | Hints only on one replica | No `redis` configured | Configure `redis`; hints are never a correctness signal |
143
+
144
+ ## Module Documentation
145
+
146
+ - `src/core/modules/sync/README.md` — module overview, configuration, security model, hints, purge
147
+ - `src/core/modules/sync/INTEGRATION-CHECKLIST.md` — integration steps
148
+ - `src/core/modules/sync/SECURITY.md` — what the server enforces, what a client must do
149
+ - `docs/offline-sync-protocol.md` — the client contract
150
+ - `src/server/modules/sync-note/` — reference model
151
+
152
+ ## References
153
+
154
+ - nest-server-starter: unaffected (it declares no synchronized model)
@@ -0,0 +1,440 @@
1
+ # Migration Guide: 12.2.x → 12.3.x
2
+
3
+ ## 12.3.0 contains breaking changes
4
+
5
+ The version number moves only the MINOR digit (the MAJOR follows NestJS). Read §1–§3, §7 and §8 before you update: none
6
+ of them needs code in a typical project, but each one changes what a running application does.
7
+
8
+ ## Overview
9
+
10
+ | Category | Details |
11
+ |---|---|
12
+ | **Breaking Changes** | E-mail verification no longer signs the user in (§1). A prompt continues only the caller's own AI conversation, `CoreAiConversationService.appendMessage()` takes the owner and the protected `CoreAiService.persistTurn()` the run as a third parameter (§2). Non-canonical paths under the Better-Auth base path answer 400, and the IAM rate limit tells routes apart, with half the limit on sign-in and sign-up (§3). `emailAndPassword.disableSignUp: true` closes social sign-up too (§7). `AiTool.authorize()` runs on every path, and MCP asks before a destructive tool or refuses a client that cannot ask (§8) |
13
+ | **New Features** | `socialProviders.<name>.disableSignUp` opens or closes sign-up for one provider (§7). `ai.mcp.confirmation` and a per-user switch (`GET/PUT /ai/settings`), `context.authorization` for tools, `destructiveHint` annotations in `tools/list` (§8). `ai.nativeToolCatalog`, a warning for a crowded context window (§9). `ai.mcp.stateless`, consent page `texts` / `styles` (§10). `CoreTenantService.purgeTenantData()` and `deleteAllMembers()` remove a tenant's data (§11) |
14
+ | **Bugfixes** | Login CSRF through a forwarded verification link (§1), turns written into another user's AI conversation (§2), rate limiter bypass through dot segments and an IAM rate limit that never told routes apart over HTTP (§3), a planned end of the SSE hint stream logged as a lost session (§4), a sync push that applied a change chained to a refused one in the same request on another writer's version (§5), an open redirect through `/authorize` of the MCP OAuth endpoints (§6), new accounts through social login while sign-up was switched off (§7), tool checks and admin tool policies that MCP and auto mode skipped (§8), tool descriptions sent twice to native connections and native tool definitions missing from the context budget (§9), cached memberships that kept a deleted tenant reachable for up to `cacheTtlMs` after `invalidateTenant()` (§11) |
15
+ | **Dependencies** | `@nestjs/graphql` 14.1.0 → 14.1.2, `@nestjs/apollo` 14.1.1 → 14.1.2, `@nestjs/swagger` 12.0.5 → 12.1.0, `@tus/server` 2.4.6 → 2.4.7, `@tus/file-store` 2.1.1 → 2.1.2, `jose` 6.2.13 → 6.2.15, `nodemailer` 10.3.0 → 10.3.1. Tested optional peers raised: `bullmq` 6.5.1 → 6.5.4, `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` 3.1159.0 → 3.1161.0, `@tus/s3-store` 2.0.9 → 2.1.0, `kysely` 0.29.7 → 0.29.8 — `pnpm run update` raises them where a project pins them. Development: `vitest` 5.0.5 → 5.1.0 together with `vite` 8.3.4 → 8.3.5 (Dependencies) |
16
+ | **Migration Effort** | Usually none. A frontend whose verify page expects a session: one redirect. A project calling `appendMessage()` or overriding `persistTurn()`: one argument. A deployment with a tight `betterAuth.rateLimit.max`: check the value (§3). A project that closed sign-up but wants new users through a social provider: one flag on that provider (§7). A project using destructive AI tools over MCP: check the clients, offer users the switch (§8) |
17
+
18
+ ## Quick Migration
19
+
20
+ ```bash
21
+ pnpm add @lenne.tech/nest-server@12.3.0
22
+ pnpm run update # nest-server-starter based projects: aligns the pins the framework raised
23
+ pnpm run build
24
+ pnpm test
25
+ ```
26
+
27
+ **Vendor mode**: run `/lt-dev:backend:update-nest-server-core` as usual.
28
+
29
+ ### Dependencies
30
+
31
+ `pnpm run update` raises the pins a project shares with the framework: `@nestjs/graphql`, `@nestjs/apollo` and
32
+ `@nestjs/swagger` (patch and minor releases of NestJS, no API change), the optional peers it tests against, and the
33
+ development tooling. Nothing else in a project changes.
34
+
35
+ **`vite` may leave 8.3.4 now.** The 12.0.x guide (§12) asked to keep `vite` at 8.3.4 while `vitest` was 5.0.5: that
36
+ combination crashed a run with a `globalSetup` or an own reporter after every test was green (`code.charCodeAt is not a
37
+ function`, vitest-dev/vitest#11682). `vitest` 5.1.0 carries the fix. Raise both together — `vite` 8.3.5 with `vitest`
38
+ below 5.1.0 still crashes.
39
+
40
+ ## 1. Auto sign-in after e-mail verification is off by default
41
+
42
+ `betterAuth.emailVerification.autoSignInAfterVerification` defaulted to `true`; it is now `false`, which is
43
+ Better-Auth's own default.
44
+
45
+ With the option on, `GET /iam/verify-email` creates a session for the account the link belongs to in whatever browser
46
+ opens the link. A verification link travels by mail, so it can be forwarded: someone who signs up with their own
47
+ address and sends the unopened link to someone else has that person's browser signed in to the sender's account, and
48
+ what the recipient enters there (payment data, documents, messages) lands with the sender (login CSRF).
49
+
50
+ **What changes:** after clicking the link the user is verified but not signed in, and signs in once.
51
+ `CoreBetterAuthEmailVerificationService.shouldAutoSignIn()` answers `false` accordingly.
52
+
53
+ **What to check:** a frontend whose verify page assumes a session afterwards should send the user to the sign-in, for
54
+ example with a "verified, please sign in" message. The nuxt-base-starter's verify page already does ("Jetzt
55
+ anmelden").
56
+
57
+ **To keep the previous behaviour** — only where a verification link reaches nobody but its owner:
58
+
59
+ ```typescript
60
+ // config.env.ts
61
+ betterAuth: {
62
+ emailVerification: {
63
+ autoSignInAfterVerification: true,
64
+ },
65
+ },
66
+ ```
67
+
68
+ Only the boolean `true` switches it on (`NSC__*` variables turn `true` into a boolean). Any other value — the string
69
+ `"true"` in `NEST_SERVER_CONFIG`, `1`, `yes` — is read as off, in what Better-Auth receives and in what
70
+ `shouldAutoSignIn()` answers alike.
71
+
72
+ ## 2. A prompt continues only the caller's own AI conversation
73
+
74
+ `POST /ai/prompt`, `POST /ai/stream` and the GraphQL `aiPrompt` accepted any `conversationId` and wrote the new turns
75
+ into that conversation, also when it belonged to another user. Now a `conversationId` the caller did not create —
76
+ another user's, or one that does not exist — counts as no conversation: the run reads no history, writes nothing, uses
77
+ no conversation-scoped tool grants, and its response carries no `conversationId`. **Administrators are no exception**:
78
+ they still read every conversation (`getAiConversation`, `findAiConversations` with `all`), but continue only their
79
+ own.
80
+
81
+ `CoreAiConversationService.appendMessage()` checks the owner itself and has a third parameter:
82
+
83
+ **Before:**
84
+ ```typescript
85
+ await this.conversationService.appendMessage(conversationId, { content, role: 'user' });
86
+ ```
87
+
88
+ **After:**
89
+ ```typescript
90
+ // The id of the user the turn belongs to; anyone else's turn is not written.
91
+ await this.conversationService.appendMessage(conversationId, { content, role: 'user' }, currentUser.id);
92
+ ```
93
+
94
+ A call without it fails to compile (TS2554). A subclass that overrides `appendMessage()` and does not call `super`
95
+ must check the owner itself — `isOwnedBy(id, userId)` is the new public helper for it.
96
+
97
+ The protected `CoreAiService.persistTurn()`, which calls `appendMessage()`, takes the run as a third parameter for the
98
+ same reason — the owner comes from `run.currentUser`:
99
+
100
+ ```typescript
101
+ // Before: protected async persistTurn(input, response)
102
+ protected override async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse, run: AiRunContext) {
103
+ // … your own steps …
104
+ await super.persistTurn(input, response, run);
105
+ }
106
+ ```
107
+
108
+ An override that still calls `super.persistTurn(input, response)` fails to compile (TS2554).
109
+
110
+ ## 3. Requests under the Better-Auth base path
111
+
112
+ ### Non-canonical paths answer 400
113
+
114
+ Every check under the base path (`/iam` by default) — rate limiter, controller routes, the forwarding to Better-Auth —
115
+ read the path as sent, while Better-Auth routes the path after `new URL()` resolved it. `/iam/callback/../sign-in/email`
116
+ therefore passed the rate limiter as a skipped OAuth callback and ran as a sign-in.
117
+
118
+ `CoreBetterAuthPathGuardMiddleware` now runs first under the base path and answers **400** (`#LTNS_0305: Invalid
119
+ request path`) for a path with a `.` or `..` segment (also `%2e`-encoded), a backslash, an encoded `/` or `\`, or a
120
+ `#`. No Better-Auth route needs any of them; a client that sends one has a bug. It is registered by
121
+ `CoreBetterAuthModule` itself — nothing to add.
122
+
123
+ ### The IAM rate limit tells routes apart
124
+
125
+ The rate-limit middleware read `req.path`, which is `/` for a middleware Nest mounts under the base path. So every IAM
126
+ request of a client shared one counter with the full limit, the skip list (`/session`, `/callback`) and the halved
127
+ limit for sign-in, sign-up and the password routes never applied over HTTP.
128
+
129
+ Now a client IP has one counter per route NAME — the last path segment — so `/sign-in/email` and `/sign-up/email` (both
130
+ `email`) share one counter. `/session` and `/callback` are not counted, and the strict routes (sign-in, sign-up, the
131
+ password reset and change routes) get half of `betterAuth.rateLimit.max`, rounded up. The skip and strict lists match
132
+ whole path segments, case-insensitively, as Express routes.
133
+
134
+ **What to check:** a deployment that set `betterAuth.rateLimit.max` (the nest-server-starter reads it from
135
+ `IAM_RATE_LIMIT_MAX`) gets half of it for the e-mail sign-in and sign-up now, and the two count together — with
136
+ `max: 10`, five attempts per window for both combined. Behind a shared NAT or proxy, sign-ups use up the same budget as
137
+ sign-ins. Raise the value if that is too tight. A single-page app that polls `/iam/session` no longer uses up the
138
+ limit.
139
+
140
+ ## 4. SSE hint stream: a planned end logs on debug
141
+
142
+ `GET /sync/hints` ends its stream on every re-check (`graphQl.subscriptionRevalidationMs`) and the client reconnects.
143
+ Every such planned end logged `Closing GraphQL WebSocket of user …: Session no longer valid` at log level. It now logs
144
+ `Closing SSE hint stream of user …` on debug; an end an invalidation causes (role change, sign-out) stays at log level.
145
+
146
+ `registerWsConnection()` takes two optional fields for a long-lived channel of a project's own: `channel` (the name in
147
+ the log line, default `GraphQL WebSocket`) and `endsOnRecheck` (every re-check ends the connection). GraphQL WebSockets
148
+ log as before.
149
+
150
+ ## 5. Sync push: a change chained to a refused one is held back
151
+
152
+ A client sends the changes of one record with chained base versions, also in one push (protocol §6.2). The server
153
+ applied every item on its own: when the first item conflicted with another writer's version 2, the second — base
154
+ `2` — matched that version and was stored on data the client never saw, while the client dropped it locally.
155
+
156
+ Once an item of a record is not stored (`conflict`, `rejected`, `not_found`, `retry`), the later items of that record
157
+ in the same push are now held back: answered `retry` behind a `retry`, otherwise `conflict` with the current record
158
+ (`not_found` when the caller may not see one). Items of other records are unaffected.
159
+
160
+ The server holds back within one request only. Protocol §11 now says what a client does across requests: it sends a
161
+ later change of a record only in the same request as the change before it, or after that one came back `applied` or
162
+ `merged`, and drops or rebases the later changes of a record whose change was `rejected` or `not_found`. **Check your
163
+ client** if it splits its queue into several requests (the protocol caps a push at
164
+ `sync.push.maxItems`, 100 by default): after a `retry` it must keep the record's later changes back until the next
165
+ sync. A client that sent a single change per record and request to avoid the defect may send chains in one request
166
+ again; across requests it still waits for each answer.
167
+
168
+ ## 6. MCP OAuth: no error redirect to a self-registered URI
169
+
170
+ Only with `ai.mcp.oauth: true`. Dynamic client registration accepts any redirect URI, and the MCP SDK sent an error
171
+ it found after the client check (a missing `code_challenge`, a wrong `response_type`) to that URI with a 302. A client
172
+ registered with a foreign URI turned one crafted `/authorize` link into a redirect from the API's own domain to any
173
+ site. Such an error is now answered with `400` and `{ error, error_description }`. Registration stays open, so
174
+ claude.ai, ChatGPT, Cursor and Claude Code connect as before; the user's Allow and Deny on the consent page still
175
+ redirect.
176
+
177
+ The built-in Deny now needs the consent form's CSRF token, as Allow always did; a Deny without it shows the form again.
178
+ A consent UI of your own (`authorizeConsent()` override) should check its token on Deny too — see the AI module's
179
+ INTEGRATION-CHECKLIST.
180
+
181
+ ## 7. Social login creates no account while sign-up is switched off
182
+
183
+ Only with `betterAuth.emailAndPassword.disableSignUp: true` and at least one social provider. The flag closed
184
+ `/sign-up/email` only. Better-Auth decides a social sign-up per provider, and each provider got only its `clientId` and
185
+ `clientSecret`, so whoever signed in with Google, GitHub etc. for the first time got an account in a project that had
186
+ closed self-registration (accounts only through an admin or an invitation).
187
+
188
+ **What changes:** every configured provider now receives the flag. A first sign-in through a provider by somebody who
189
+ has no account is refused (the OAuth callback redirects with `error=signup_disabled`, `/sign-in/social` with an
190
+ `idToken` answers 401 `signup disabled`). Users who already have an account keep signing in, also through a provider
191
+ they have not used before: Better-Auth checks the flag only when no user exists for the address.
192
+
193
+ **What to check:** a project that closed sign-up on purpose needs nothing. A project that closed e-mail sign-up but
194
+ wants new users through a provider opens that one deliberately:
195
+
196
+ ```typescript
197
+ // config.env.ts
198
+ betterAuth: {
199
+ emailAndPassword: { disableSignUp: true },
200
+ socialProviders: {
201
+ google: { clientId: '...', clientSecret: '...', disableSignUp: false }, // new users through Google
202
+ github: { clientId: '...', clientSecret: '...' }, // follows disableSignUp: closed
203
+ },
204
+ },
205
+ ```
206
+
207
+ `disableSignUp: true` on a provider closes sign-up for that provider alone, while sign-up stays open elsewhere. Only a
208
+ boolean counts there: any other value, such as the string `"false"` in `NEST_SERVER_CONFIG`, follows
209
+ `emailAndPassword.disableSignUp`. `GET /iam/features` keeps reporting `signUpEnabled` from the global flag. Plugins a
210
+ project registers itself through `betterAuth.options.plugins` (Generic OAuth, Magic Link, …) are not covered; set their
211
+ own sign-up options.
212
+
213
+ ## 8. AI tools: `authorize()` on every path, confirmation over MCP
214
+
215
+ Only with the `ai` module. Two gates held in the chat's plan mode alone:
216
+
217
+ - **`AiTool.authorize()`** ran in plan mode only. Auto mode (the default) and `/ai/mcp` called `execute()` directly,
218
+ so a data-level check a tool placed there ("may this user change record 42?") did not run for most callers.
219
+ - **`destructive` / `mutating`** were read by the chat only. Over MCP a destructive tool ran on the first call, and an
220
+ admin's `aiToolPolicies` rules (`deny`, `ask`) did not apply at all.
221
+
222
+ **What changes:**
223
+
224
+ - `authorize()` runs before every execution — auto mode, plan mode (for the whole plan, and again per step), MCP — on
225
+ the arguments that will run, after the PreToolUse hooks. A thrown `authorize()`, a missing answer or an `allowed`
226
+ that is not exactly `true` refuses the call. The model receives `TOOL_NOT_AVAILABLE` with the `reason` (keep it
227
+ free of anything the user may not know); a refusal is no longer a learning signal. Return `data` to hand what the
228
+ check loaded to `execute()` as `context.authorization.data`.
229
+ - `tools/call` applies the admin's tool policies (`deny` refuses, `ask` asks), then `authorize()`, then asks the user
230
+ through **MCP elicitation** before a `destructive` tool, and before a `mutating` one when `ai.confirmation.mutating`
231
+ says so (`enforced`, else `default`). Only an explicit yes runs the tool. **A client without the `elicitation`
232
+ capability gets an error result for such a tool.**
233
+ - Each user may switch the question off for themselves, like a "bypass permissions" mode: `PUT /ai/settings` with
234
+ `{ "mcpConfirmation": false }` (GraphQL `updateAiSettings`), read with `GET /ai/settings` (`aiSettings`). The choice
235
+ is stored per user in the new `aiUserSettings` collection / table, created at boot like every AI model.
236
+ `authorize()` and the admin's `deny` / `ask` rules apply whatever the user chose.
237
+ - `tools/list` carries `annotations` (`destructiveHint`, `readOnlyHint: false`) for `destructive` and `mutating`
238
+ tools. A tool with neither flag carries none, so it is never advertised as read-only.
239
+
240
+ **What to check:**
241
+
242
+ 1. **Tools with `authorize()`:** it now runs in auto mode and over MCP too. A check written for plan mode only (for
243
+ example one that refuses whatever it does not expect) refuses those callers from now on. A tool without
244
+ `authorize()` is unchanged.
245
+ 2. **MCP clients:** whether yours support elicitation. Where a client you need cannot ask, choose the default for the
246
+ deployment:
247
+
248
+ ```typescript
249
+ // config.env.ts
250
+ ai: {
251
+ mcp: {
252
+ confirmation: true, // default: asks, each user may switch it off
253
+ // confirmation: false, // asks nobody, each user may switch it on
254
+ // confirmation: { default: true, enforced: true } // asks everybody, nobody may switch it off (403 #LTNS_0609)
255
+ },
256
+ },
257
+ ```
258
+
259
+ Only `false` / `default: false` switch the default off and only `enforced: true` enforces; any other value keeps
260
+ asking.
261
+ 3. **Your frontend:** offer the switch where users manage their AI settings — `mcpConfirmationEnforced: true` in the
262
+ answer means it is fixed and should be shown disabled.
263
+
264
+ **API changes:** `CoreAiMcpService.mcpCallTool()` takes an optional fourth parameter (`{ confirm }`), its constructor
265
+ two optional services (`CoreAiUserSettingService`, `CoreAiToolPolicyService`). `CoreAiController` and
266
+ `CoreAiResolver` receive `CoreAiUserSettingService` as an injected property, so a project subclass keeps its
267
+ constructor. `CoreModule.forRoot(env, { ai: { userSettingService } })` replaces the new store.
268
+
269
+ ## 9. AI: names-only tool catalog for native connections, a context window that counts the tools
270
+
271
+ Only with the `ai` module and a connection that uses native tool calling (`supportsNativeTools: true`, detected
272
+ automatically).
273
+
274
+ **What changes:**
275
+
276
+ - The system prompt's tool catalog lists **tool names only** for such a connection in auto mode. Descriptions and
277
+ parameter schemas reach the model through the native `tools` payload as before. Until now the catalog repeated them,
278
+ so every tool was sent twice. Emulated connections and plan mode keep the full catalog.
279
+ - The native tool definitions count against the context budget, so older turns are summarized or dropped before the
280
+ window overflows rather than after.
281
+ - When the system prompt and the tool definitions take more than half of a connection's input budget, the framework
282
+ logs one warning per connection and process. It names the numbers and says whether the window is the connection's
283
+ own or the `ai.contextWindow` fallback. Until now the only trace was a debug line, and the user's message was cut
284
+ without notice.
285
+
286
+ **What to check:**
287
+
288
+ 1. Nothing for most projects. A model that picks tools noticeably worse without the descriptions in the prompt gets
289
+ the old catalog back:
290
+
291
+ ```typescript
292
+ // config.env.ts
293
+ ai: {
294
+ nativeToolCatalog: 'full', // default 'names'
295
+ },
296
+ ```
297
+
298
+ 2. A project test that guards the prompt size: it measures against the `ai.contextWindow` fallback, so it guards
299
+ connections without a window of their own. Keep it, and name it that way. Raising its limit removes the guard.
300
+ 3. `deferToolSummaryChars` never applied to native connections in auto mode and still does not. It applies to emulated
301
+ connections and plan mode.
302
+
303
+ ## 10. MCP without sessions, and the consent page's wording and look
304
+
305
+ Both are opt-in; nothing changes without configuration.
306
+
307
+ **`ai.mcp.stateless: true`** serves `/ai/mcp` without sessions.
308
+ - Every POST builds its own server and transport for the authenticated user, and no `mcp-session-id` is issued.
309
+ - A session id a client still sends is ignored. GET and DELETE answer `405` with `Allow: POST`.
310
+ - Any replica answers any request, so `/ai/mcp` no longer needs sticky sessions behind a load balancer.
311
+ - There are no server-initiated requests in this mode, so the server cannot ask the user (§8). A tool that needs
312
+ confirmation is refused unless the user switched the question off (`PUT /ai/settings`) or
313
+ `ai.mcp.confirmation` is `false`.
314
+ - `testHelper.mcpSession()` follows the setting: no session id is expected or sent, and `sessionId` is `''`.
315
+
316
+ ```typescript
317
+ // config.env.ts
318
+ ai: { mcp: { oauth: true, stateless: true } },
319
+ ```
320
+
321
+ **Consent page wording and CSS.** A subclass of `CoreAiMcpOAuthService` overriding `renderConsentPage(view)` calls
322
+ `renderMcpOAuthConsentPage(view, { texts, styles })`.
323
+ - `texts` replaces single texts per locale. They stay escaped.
324
+ - `styles` is CSS appended inside the page's `<style>` element. A `<` in it is neutralized, so the CSS cannot end that
325
+ element, and the page's CSP blocks external loads.
326
+ - Fill both from code or configuration only.
327
+ - The page's scripts and security headers stay internal and are not exported.
328
+
329
+ ## 11. Removing a tenant
330
+
331
+ The core had no way to delete a tenant's data: a project had to find every place itself, and the ones that are not a
332
+ `tenantId` field (API tokens, the AI settings with `scope: 'tenant'`, the AI connections' `tenantIds`) were the easy ones
333
+ to miss. When a project deletes its tenant record, it now calls:
334
+
335
+ ```typescript
336
+ const report = await this.tenantService.purgeTenantData(tenantId); // { TenantMember: 4, Invoice: 120, ApiToken: 2 }
337
+ await this.tenantService.purgeTenantData(tenantId, { dryRun: true }); // counts only
338
+ await this.tenantService.purgeTenantData(tenantId, { exclude: ['AuditEntry'] });
339
+ ```
340
+
341
+ - Every way in goes first: the memberships (`deleteAllMembers()`, new as well), with every member's cached access
342
+ ending at once, and the tenant's API tokens, which need no member.
343
+ - Then every registered model with a `tenantId` path, on MongoDB and SQL, the project's own models included; the core
344
+ models that name the tenant in another field; and the AI connections — one only this tenant could use is deleted,
345
+ from a shared one the tenant is removed.
346
+ - Not deleted: models in `multiTenancy.excludeSchemas` or `exclude`, users, and files. Delete a tenant's files through
347
+ `CoreFileService` first; only the project knows which ones are the tenant's. Users stay also where `User` carries a
348
+ `tenantId`: delete per-tenant users through the user service first, which also ends their Better-Auth sessions.
349
+ - A project reference the core does not know of: override the protected `purgeTenantRecords()` and call `super`.
350
+ - The purge runs as a system operation. Call it only from a route that checked who may delete the tenant.
351
+
352
+ **Also changed:** `CoreTenantGuard.invalidateTenant()` clears the tenant's cached memberships and every cached tenant
353
+ list that contains it, on every replica with Redis. Before, it forgot only the `isTenantActive` answer: where a project
354
+ removed a tenant's memberships without calling `invalidateUser()` (a deleted tenant) and no `isTenantActive` answered
355
+ `false`, the cached memberships kept the members in for up to `cacheTtlMs`. A tenant switched off through
356
+ `isTenantActive` was already refused at once. Nothing to change in a project.
357
+
358
+ ## Compatibility Notes
359
+
360
+ | Pattern | Status |
361
+ |---|---|
362
+ | Frontend verify page that leads to the sign-in | Unchanged |
363
+ | Frontend verify page that expects a session after the link | Redirect to the sign-in, or set `autoSignInAfterVerification: true` (§1) |
364
+ | Project calling `appendMessage()` | Pass the owner id (§2) |
365
+ | Project subclass of `CoreAiService` overriding `prompt()` | Call `super.prompt()`, or the conversation is not scoped (§2) |
366
+ | Project subclass of `CoreAiService` overriding `persistTurn()` | Take `run` as third parameter and pass it to `super` (§2) |
367
+ | Project controller extending `CoreBetterAuthController` | Unchanged; the path guard runs before it |
368
+ | Custom `betterAuth.rateLimit.skipEndpoints` / `strictEndpoints` | Matched by whole segments now: `/session` no longer covers `/sessions-export` |
369
+ | Sync client pushing chained changes of one record in one request | Unchanged; behind a refused item the rest of the chain is answered `conflict` (or `retry`) instead of being applied (§5) |
370
+ | MCP client sending a malformed `/authorize` request | Gets a `400` page in the browser instead of an error redirect (§6) |
371
+ | Own MCP OAuth consent UI (`authorizeConsent()` override) | Unchanged; check the CSRF token on Deny too (§6) |
372
+ | Sync client splitting a record's changes across requests | Keep a later change back until the one before it was stored, drop it after `rejected` / `not_found` (§5, protocol §11) |
373
+ | `disableSignUp: true` without social providers | Unchanged |
374
+ | `disableSignUp: true` with social providers, new users only through admins or invitations | Unchanged in intent; social login no longer creates accounts (§7) |
375
+ | `disableSignUp: true` with a provider meant for self-registration | Set `disableSignUp: false` on that provider (§7) |
376
+ | AI tool without `authorize()` | Unchanged |
377
+ | AI tool whose `authorize()` was written for plan mode | Runs in auto mode and over MCP too (§8) |
378
+ | Destructive AI tool used through an MCP client with elicitation | The user is asked first (§8) |
379
+ | Destructive AI tool used through an MCP client without elicitation | Refused until the user switches confirmation off, or `ai.mcp.confirmation` is `false` (§8) |
380
+ | Project subclass of `CoreAiController` / `CoreAiResolver` | Unchanged; the settings service is property-injected (§8) |
381
+ | Native AI connection | Catalog lists names only; `ai.nativeToolCatalog: 'full'` restores it (§9) |
382
+ | `/ai/mcp` behind a load balancer without sticky sessions | Set `ai.mcp.stateless: true` (§10) |
383
+ | Destructive AI tool over MCP with `ai.mcp.stateless` | Refused unless the user or `ai.mcp.confirmation` switched the question off (§10) |
384
+ | Override of `fitMessagesToContext()` / `compactMessages()` | Unchanged; both take an optional third parameter, the tokens reserved for native tool definitions (§9) |
385
+ | Project deleting tenants with its own clean-up | Replace it with `purgeTenantData()`, or keep it and add what it misses (§11) |
386
+ | Project subclass of `CoreTenantService` | Unchanged; the MongoDB connection the purge reads is property-injected (§11) |
387
+
388
+ ## Troubleshooting
389
+
390
+ | Symptom | Cause | Fix |
391
+ |---|---|---|
392
+ | User not signed in after clicking the verification link | §1 | Expected; lead to the sign-in, or opt in |
393
+ | TS2554 on `appendMessage()` or `super.persistTurn()` | §2 | Pass the owner id, or the run, as third argument |
394
+ | A continued AI conversation answers without history, no `conversationId` in the response | The conversation belongs to another user (also for admins) | Continue only conversations the caller created |
395
+ | 400 `#LTNS_0305` on an IAM request | The path carries a dot segment, a backslash, an encoded separator or a `#` | Fix the client's URL |
396
+ | 429 on sign-in earlier than before | The strict limit (half of `max`) applies now, shared by e-mail sign-in and sign-up | Raise `betterAuth.rateLimit.max` |
397
+ | New users land on the error page with `error=signup_disabled` after a social login | §7: `disableSignUp: true` covers social providers | Expected; or set `disableSignUp: false` on the provider |
398
+ | MCP answers "needs the user's confirmation, and this MCP client cannot ask for it" | §8: destructive tool, client without elicitation | Use a client with elicitation, or let the user switch it off (`PUT /ai/settings`) |
399
+ | `TOOL_NOT_AVAILABLE` in auto mode for a tool that worked before | §8: its `authorize()` refuses | Fix the check, or return `{ allowed: true }` where it should pass |
400
+ | 403 `#LTNS_0609` on `PUT /ai/settings` | §8: `ai.mcp.confirmation.enforced` | Expected |
401
+ | Warning "the system prompt and tool definitions take ~N of M input tokens" | §9: the window is crowded | Set the connection's `contextWindow`, or shrink the catalog |
402
+
403
+ ## Module Documentation
404
+
405
+ ### Better-Auth
406
+
407
+ - **README:** [src/core/modules/better-auth/README.md](../src/core/modules/better-auth/README.md) — "Email Verification", "Rate Limiting", "Disable Sign-Up"
408
+ - **Integration Checklist:** [src/core/modules/better-auth/INTEGRATION-CHECKLIST.md](../src/core/modules/better-auth/INTEGRATION-CHECKLIST.md)
409
+ - **Key Files:** `better-auth.config.ts` (the verification options Better-Auth receives, `buildSocialProviders()`),
410
+ `core-better-auth-path-guard.middleware.ts`, `core-better-auth-rate-limiter.service.ts`
411
+
412
+ ### AI
413
+
414
+ - **README:** [src/core/modules/ai/README.md](../src/core/modules/ai/README.md)
415
+ - **Key Files:** `services/core-ai.service.ts` (`scopeConversation()`, `executeToolCall()`), `services/core-ai-conversation.service.ts`
416
+ (`isOwnedBy()`, `appendMessage()`), `helpers/ai-tool-authorization.helper.ts`, `services/core-ai-mcp.service.ts`
417
+ (`mcpCallTool()`), `services/core-ai-user-setting.service.ts`
418
+ - **README sections:** "Plan mode", "Authorization and confirmation over MCP"
419
+
420
+ ### AI (MCP OAuth)
421
+
422
+ - **README:** [src/core/modules/ai/README.md](../src/core/modules/ai/README.md) — "OAuth 2.1", consent page
423
+ - **Integration Checklist:** [src/core/modules/ai/INTEGRATION-CHECKLIST.md](../src/core/modules/ai/INTEGRATION-CHECKLIST.md) — own consent UI
424
+ - **Key Files:** `helpers/ai-mcp-oauth.helper.ts` (`mountAiMcpOAuthRouter()`), `services/core-ai-mcp-oauth.service.ts` (`authorizeConsent()`)
425
+
426
+ ### Multi-Tenancy
427
+
428
+ - **README:** [src/core/modules/tenant/README.md](../src/core/modules/tenant/README.md) — "Removing a Tenant"
429
+ - **Key Files:** `core-tenant.service.ts` (`purgeTenantData()`, `purgeTenantRecords()`, `deleteAllMembers()`),
430
+ `core-tenant.guard.ts` (`clearTenant()`)
431
+
432
+ ### Sync
433
+
434
+ - **README:** [src/core/modules/sync/README.md](../src/core/modules/sync/README.md) — hint stream log lines, held-back push items
435
+ - **Protocol:** [docs/offline-sync-protocol.md](../docs/offline-sync-protocol.md) §6.2, §6.3
436
+
437
+ ## References
438
+
439
+ - nest-server-starter: no code change; it keeps Better-Auth's defaults, configures neither `disableSignUp` nor a social
440
+ provider, does not call `appendMessage()`, and configures no `ai` module