@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
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../../common/services/rate-limit-store.js';
11
11
  import { RequestContext } from '../../../common/services/request-context.service.js';
12
12
  import { ErrorCode } from '../../error-code/index.js';
13
+ import { authorizeToolCall, toolRequiresConfirmation } from '../helpers/ai-tool-authorization.helper.js';
13
14
  import { AiToolAuthorization, AiToolContext, AiToolResult, IAiTool } from '../interfaces/ai-tool.interface.js';
14
15
  import { LlmMessage, LlmResponse, LlmToolCall } from '../interfaces/llm-provider.interface.js';
15
16
  import { ResolvedAiConnection } from '../interfaces/resolved-ai-connection.interface.js';
@@ -107,6 +108,9 @@ export interface AiRunContext {
107
108
  export class CoreAiService {
108
109
  protected readonly logger = new Logger(CoreAiService.name);
109
110
 
111
+ /** Connections already warned about a crowded context window; created on first use, at most 1000 entries. */
112
+ protected crowdedContextWarned?: Set<string>;
113
+
110
114
  /** Rate-limit counters keyed by user id (Redis-backed when configured). */
111
115
  protected rateLimitStore?: RateLimitStore;
112
116
 
@@ -142,7 +146,10 @@ export class CoreAiService {
142
146
  hooks?: { onAction?: (action: CoreAiAction) => void },
143
147
  ): Promise<CoreAiResponse> {
144
148
  const mode = input.mode || ConfigService.get<string>('ai.defaultMode') || 'auto';
145
- const resolvedInput = await this.resolvePromptPlaceholders(input, serviceOptions);
149
+ const resolvedInput = await this.scopeConversation(
150
+ await this.resolvePromptPlaceholders(input, serviceOptions),
151
+ serviceOptions,
152
+ );
146
153
  const run = await this.prepareRun(resolvedInput, serviceOptions);
147
154
  // No usable connection → AI handling is effectively disabled.
148
155
  if (!run) {
@@ -157,6 +164,28 @@ export class CoreAiService {
157
164
  return response;
158
165
  }
159
166
 
167
+ /**
168
+ * Keep `conversationId` only when the caller owns that conversation; otherwise the run has no conversation at all:
169
+ * no history, no persisted turns, no conversation-scoped tool grants, and no `conversationId` in the response.
170
+ *
171
+ * Every conversation use of the run reads the id from the input, so it is decided once, before the run starts. A
172
+ * foreign id must not reach any of them: its turns would land in another user's history and feed that user's next
173
+ * runs, where tools act with the owner's rights, and a "remember my decision" grant stored on it would let the
174
+ * owner's assistant run a mutating tool unconfirmed. Admins included — they may read other users' conversations,
175
+ * never continue them. An unknown id is answered exactly like a foreign one, so the response tells nobody which
176
+ * conversation ids exist.
177
+ */
178
+ protected async scopeConversation(
179
+ input: CoreAiPromptInput,
180
+ serviceOptions: ServiceOptions,
181
+ ): Promise<CoreAiPromptInput> {
182
+ if (!input?.conversationId) {
183
+ return input;
184
+ }
185
+ const owned = await this.conversationService?.isOwnedBy(input.conversationId, serviceOptions?.currentUser?.id);
186
+ return owned ? input : { ...input, conversationId: undefined };
187
+ }
188
+
160
189
  /**
161
190
  * Resolve `{{placeholder}}` tokens in the user's prompt text via the
162
191
  * runtime placeholder registry. Returns the input untouched when no
@@ -403,6 +432,10 @@ export class CoreAiService {
403
432
  },
404
433
  );
405
434
  const toolSchemas = this.promptBuilder.buildToolSchemas(tools);
435
+ // A native connection carries the tool definitions in the payload, outside the messages: they occupy the context
436
+ // window all the same, so they come off the input budget (12.3.0).
437
+ const toolTokens = provider.capabilities.nativeTools ? this.estimateTokens(JSON.stringify(toolSchemas)) : 0;
438
+ this.warnOnCrowdedContext(connection, this.estimateTokens(systemPrompt) + toolTokens);
406
439
  // The JSON output contract belongs to the EMULATED protocol only: `output_contract`
407
440
  // and `tool_protocol_emulated` both carry `capability: 'emulated'`, so a NATIVE-tools
408
441
  // run is asked for PROSE and is handed no JSON shape to fill. Forcing
@@ -475,8 +508,8 @@ export class CoreAiService {
475
508
  iterations++;
476
509
  // Keep the session within the model's context window before every call:
477
510
  // LLM-driven compaction first (summarize), then hard trim as a fallback.
478
- await this.compactMessages(messages, connection);
479
- this.fitMessagesToContext(messages, connection);
511
+ await this.compactMessages(messages, connection, toolTokens);
512
+ this.fitMessagesToContext(messages, connection, toolTokens);
480
513
  const completion = await provider.chat(messages, toolSchemas, {
481
514
  ...jsonMode,
482
515
  maxTokens: connection.defaultMaxTokens,
@@ -642,8 +675,17 @@ export class CoreAiService {
642
675
  // Persist the turn pair for multi-turn conversations (not while awaiting confirmation
643
676
  // or a pending clarifying question — the user hasn't given a real answer yet).
644
677
  if (input.conversationId && this.conversationService && !requiresConfirmation && !pendingQuestion) {
645
- await this.conversationService.appendMessage(input.conversationId, { content: input.prompt, role: 'user' });
646
- await this.conversationService.appendMessage(input.conversationId, { content: finalText, role: 'assistant' });
678
+ const owner = currentUser?.id;
679
+ await this.conversationService.appendMessage(
680
+ input.conversationId,
681
+ { content: input.prompt, role: 'user' },
682
+ owner,
683
+ );
684
+ await this.conversationService.appendMessage(
685
+ input.conversationId,
686
+ { content: finalText, role: 'assistant' },
687
+ owner,
688
+ );
647
689
  }
648
690
 
649
691
  await this.audit({
@@ -676,6 +718,7 @@ export class CoreAiService {
676
718
 
677
719
  // 1. Planning call — the model returns the full ordered plan, executes nothing.
678
720
  const planSystemPrompt = await this.promptBuilder.buildPlanSystemPrompt(tools, currentUser, { language });
721
+ this.warnOnCrowdedContext(connection, this.estimateTokens(planSystemPrompt));
679
722
  const messages: LlmMessage[] = [{ content: planSystemPrompt, role: 'system' }];
680
723
  for (const turn of history) {
681
724
  messages.push({ content: turn.content, role: turn.role === 'assistant' ? 'assistant' : 'user' });
@@ -782,7 +825,7 @@ export class CoreAiService {
782
825
  response.text = finalText;
783
826
  response.usage = Object.assign(new CoreAiUsage(), usage);
784
827
 
785
- await this.persistTurn(input, response);
828
+ await this.persistTurn(input, response, run);
786
829
  await this.audit(this.auditRecord(input, run, response, usage));
787
830
  return response;
788
831
  }
@@ -895,9 +938,10 @@ export class CoreAiService {
895
938
  }
896
939
 
897
940
  /**
898
- * Pre-flight authorization for a planned/requested tool call. Combines the
899
- * registry role filter (tool absent from the user's set → denied) with the
900
- * tool's optional `authorize()` data-level check. Never mutates anything.
941
+ * Authorization for one tool call: the registry role filter (tool absent from the user's set → denied) and the
942
+ * tool's optional `authorize()` data-level check, fail-closed ({@link authorizeToolCall}). Runs before every
943
+ * execution (`executeToolCall()`) and, in plan mode, once more for the whole plan before any step runs. Never
944
+ * mutates anything.
901
945
  */
902
946
  protected async authorizeCall(
903
947
  call: LlmToolCall,
@@ -908,11 +952,7 @@ export class CoreAiService {
908
952
  if (!tool) {
909
953
  return { allowed: false, reason: `Unknown or not permitted tool: ${call.name}` };
910
954
  }
911
- if (!tool.authorize) {
912
- return { allowed: true };
913
- }
914
- const result = await tool.authorize(call.arguments ?? {}, context);
915
- return typeof result === 'boolean' ? { allowed: result } : result;
955
+ return authorizeToolCall(tool, call.arguments ?? {}, context);
916
956
  }
917
957
 
918
958
  /**
@@ -921,18 +961,7 @@ export class CoreAiService {
921
961
  * client override unless enforced).
922
962
  */
923
963
  protected confirmationRequiredFor(tool: IAiTool, input: CoreAiPromptInput): boolean {
924
- if (tool.destructive) {
925
- return true;
926
- }
927
- if (!tool.mutating) {
928
- return false;
929
- }
930
- const cfg = ConfigService.get<{ enabled?: boolean; default?: boolean; enforced?: boolean }>(
931
- 'ai.confirmation.mutating',
932
- );
933
- const enforced = cfg?.enforced === true;
934
- const adminDefault = cfg?.default === true;
935
- return enforced ? true : (input.requireConfirmation ?? adminDefault);
964
+ return toolRequiresConfirmation(tool, input.requireConfirmation);
936
965
  }
937
966
 
938
967
  /**
@@ -1051,13 +1080,22 @@ export class CoreAiService {
1051
1080
  }
1052
1081
 
1053
1082
  /**
1054
- * Append the user + assistant turns to the conversation (skipped while awaiting
1083
+ * Append the user + assistant turns to the conversation of the run's user (skipped while awaiting
1055
1084
  * confirmation or when denied).
1056
1085
  */
1057
- protected async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse): Promise<void> {
1086
+ protected async persistTurn(input: CoreAiPromptInput, response: CoreAiResponse, run: AiRunContext): Promise<void> {
1058
1087
  if (input.conversationId && this.conversationService && !response.requiresConfirmation && !response.denied) {
1059
- await this.conversationService.appendMessage(input.conversationId, { content: input.prompt, role: 'user' });
1060
- await this.conversationService.appendMessage(input.conversationId, { content: response.text, role: 'assistant' });
1088
+ const owner = run?.currentUser?.id;
1089
+ await this.conversationService.appendMessage(
1090
+ input.conversationId,
1091
+ { content: input.prompt, role: 'user' },
1092
+ owner,
1093
+ );
1094
+ await this.conversationService.appendMessage(
1095
+ input.conversationId,
1096
+ { content: response.text, role: 'assistant' },
1097
+ owner,
1098
+ );
1061
1099
  }
1062
1100
  }
1063
1101
 
@@ -1195,8 +1233,24 @@ export class CoreAiService {
1195
1233
  }
1196
1234
  }
1197
1235
 
1236
+ // Authorize the arguments that will actually run — after the hooks rewrote them. No learning signal: a refusal
1237
+ // is a decision about this user and these arguments, not a mistake the model should be taught to avoid.
1238
+ const authorization = await this.authorizeCall(call, availableTools, context);
1239
+ if (!authorization.allowed) {
1240
+ action.success = false;
1241
+ action.result = {
1242
+ error: {
1243
+ code: 'TOOL_NOT_AVAILABLE',
1244
+ hint: 'The user may not run this tool with these arguments. Do not retry it; explain this to the user.',
1245
+ message: authorization.reason || `Not permitted: ${call.name}`,
1246
+ },
1247
+ };
1248
+ return action;
1249
+ }
1250
+ const toolContext: AiToolContext = tool.authorize ? { ...context, authorization } : context;
1251
+
1198
1252
  try {
1199
- const raw = await tool.execute(call.arguments ?? {}, context);
1253
+ const raw = await tool.execute(call.arguments ?? {}, toolContext);
1200
1254
  let success = true;
1201
1255
  let payload: unknown = raw;
1202
1256
  let message: string | undefined;
@@ -1468,6 +1522,47 @@ export class CoreAiService {
1468
1522
  return Math.max(512, window - reserve - margin);
1469
1523
  }
1470
1524
 
1525
+ /**
1526
+ * The share of {@link contextBudget} the messages may take: what remains after `reservedTokens` — the native tool
1527
+ * definitions, which travel beside the messages but occupy the same window.
1528
+ */
1529
+ protected messageBudget(connection: ResolvedAiConnection, reservedTokens = 0): number {
1530
+ return Math.max(512, this.contextBudget(connection) - Math.max(0, reservedTokens));
1531
+ }
1532
+
1533
+ /** Share of the input budget the system prompt and tool definitions may take before {@link warnOnCrowdedContext} warns. */
1534
+ protected crowdedContextShare(): number {
1535
+ return 0.5;
1536
+ }
1537
+
1538
+ /**
1539
+ * Warns once per connection and process when the system prompt and the native tool definitions take more than
1540
+ * {@link crowdedContextShare} of the input budget (12.3.0). What remains is all the conversation has: older turns
1541
+ * are dropped early, and a long user message is cut to fit — which, logged on debug only, read as an assistant that
1542
+ * answers generically. Names the remedy: a context window for the connection, or a smaller catalog.
1543
+ */
1544
+ protected warnOnCrowdedContext(connection: ResolvedAiConnection, fixedTokens: number): void {
1545
+ const budget = this.contextBudget(connection);
1546
+ const key = connection.id ?? connection.name ?? '';
1547
+ const warned = (this.crowdedContextWarned ??= new Set<string>());
1548
+ if (fixedTokens <= budget * this.crowdedContextShare() || warned.has(key)) {
1549
+ return;
1550
+ }
1551
+ if (warned.size >= 1000) {
1552
+ warned.clear();
1553
+ }
1554
+ warned.add(key);
1555
+ const window = connection.contextWindow
1556
+ ? `its context window of ${connection.contextWindow}`
1557
+ : `the ai.contextWindow fallback of ${ConfigService.get<number>('ai.contextWindow') ?? 8192} (the connection has no context window of its own)`;
1558
+ this.logger.warn(
1559
+ `AI connection "${connection.name ?? key}": the system prompt and tool definitions take ~${fixedTokens} of ` +
1560
+ `${budget} input tokens (${window}). Little room is left for the conversation: older turns are dropped ` +
1561
+ `early and a long message is cut. Set the connection's contextWindow, or shrink the tool catalog ` +
1562
+ `(ai.deferToolSchemas, fewer tools per role).`,
1563
+ );
1564
+ }
1565
+
1471
1566
  /**
1472
1567
  * Keep the assembled session messages within the model's context window. Operates
1473
1568
  * per user/session: the system prompt (index 0) and the most recent message (the
@@ -1483,14 +1578,18 @@ export class CoreAiService {
1483
1578
  *
1484
1579
  * Enabled by `ai.compaction !== false`. When OFF, only the hard trim is used.
1485
1580
  */
1486
- protected async compactMessages(messages: LlmMessage[], connection: ResolvedAiConnection): Promise<number> {
1581
+ protected async compactMessages(
1582
+ messages: LlmMessage[],
1583
+ connection: ResolvedAiConnection,
1584
+ reservedTokens = 0,
1585
+ ): Promise<number> {
1487
1586
  if (ConfigService.get<boolean>('ai.compaction') === false) {
1488
1587
  return 0;
1489
1588
  }
1490
1589
  if (messages.length <= 3) {
1491
1590
  return 0;
1492
1591
  }
1493
- const budget = this.contextBudget(connection);
1592
+ const budget = this.messageBudget(connection, reservedTokens);
1494
1593
  const total = (): number => messages.reduce((sum, m) => sum + this.estimateTokens(m.content) + 4, 0);
1495
1594
  if (total() <= budget) {
1496
1595
  return 0;
@@ -1539,11 +1638,11 @@ export class CoreAiService {
1539
1638
  }
1540
1639
  }
1541
1640
 
1542
- protected fitMessagesToContext(messages: LlmMessage[], connection: ResolvedAiConnection): void {
1641
+ protected fitMessagesToContext(messages: LlmMessage[], connection: ResolvedAiConnection, reservedTokens = 0): void {
1543
1642
  if (messages.length === 0) {
1544
1643
  return;
1545
1644
  }
1546
- const budget = this.contextBudget(connection);
1645
+ const budget = this.messageBudget(connection, reservedTokens);
1547
1646
  const total = (): number => messages.reduce((sum, m) => sum + this.estimateTokens(m.content) + 4, 0);
1548
1647
  let dropped = 0;
1549
1648
  // Drop oldest non-system, non-last messages until within budget.
@@ -199,8 +199,10 @@ it has no signed assertions.
199
199
  `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` is on. With it off — and on a password
200
200
  CHANGE — user tokens survive, like personal access tokens elsewhere; after a suspected compromise
201
201
  call `revokeAllForUser(userId)`. Tenant tokens are never touched by a user's reset.
202
- - **Deleting a tenant.** Call `deleteAllForTenant(tenantId)` — nothing does it for you, and until you
203
- do, the tenant's tokens keep authenticating (the core has no tenant model to notice the deletion).
202
+ - **Deleting a tenant.** Call `CoreTenantService.purgeTenantData(tenantId)`, which deletes the tokens
203
+ with the rest of the tenant's data, or `deleteAllForTenant(tenantId)` for the tokens alone — nothing
204
+ does it for you, and until you do, the tenant's tokens keep authenticating (the core has no tenant
205
+ model to notice the deletion).
204
206
  - **Banned users.** If you add Better-Auth's admin plugin, override `loadTokenUser()` so a banned
205
207
  user's tokens are refused as well — the framework does not know that flag.
206
208
  - **Brute force.** Failed attempts are not limited per IP (guessing 256 bits is not the risk); add an
@@ -472,7 +472,7 @@ const config = {
472
472
  template: 'email-verification', // Default: 'email-verification'
473
473
  expiresIn: 86400, // Default: 86400 (24 hours)
474
474
  callbackURL: '/auth/verify-email', // Frontend verification page
475
- autoSignInAfterVerification: true, // Default: true
475
+ autoSignInAfterVerification: false, // Default: false — true signs in whoever opens the link (see README)
476
476
  resendCooldownSeconds: 60, // Default: 60
477
477
  },
478
478
  },
@@ -273,6 +273,10 @@ Email verification is **enabled by default** via Better-Auth's `emailVerificatio
273
273
  - Users receive a verification email after sign-up
274
274
  - Email contains a verification link with token
275
275
  - After verification, `verifiedAt` is automatically set on the user
276
+ - The user is **not** signed in by the verification link and signs in once afterwards (12.3.0+, as in Better-Auth).
277
+ `emailVerification: { autoSignInAfterVerification: true }` signs in whoever opens the link, to the link owner's
278
+ account — a forwarded link signs its recipient in to the sender's account. Your frontend's verify page should lead
279
+ to the sign-in rather than expect a session
276
280
  - Templates are provided in German and English
277
281
 
278
282
  ### Configuration
@@ -448,6 +452,9 @@ After integration, verify:
448
452
  - [ ] GraphQL `betterAuthSignUp` returns error `LTNS_0026`
449
453
  - [ ] `GET /iam/features` reports `signUpEnabled: false`
450
454
  - [ ] Sign-in still works for existing users
455
+ - [ ] With a social provider: a first social sign-in of an unknown user creates no account (`error=signup_disabled`),
456
+ an existing user still signs in through it. A provider meant for self-registration carries
457
+ `disableSignUp: false` (12.3.0+)
451
458
 
452
459
  ### Additional checks for Migration scenario:
453
460
 
@@ -550,6 +550,13 @@ const config = {
550
550
 
551
551
  **Brevo Integration:** When `brevoTemplateId` is set and Brevo is configured (`config.brevo`), verification emails are sent via Brevo's transactional API instead of SMTP/EJS templates. The same template variables (`name`, `link`, `appName`, `expiresIn`) are passed to the Brevo template.
552
552
 
553
+ **Auto sign-in after verification (`autoSignInAfterVerification`, default `false` since 12.3.0):** with it on,
554
+ `GET /iam/verify-email` signs in the browser that opens the link — to the account the link belongs to, whoever opens
555
+ it. Someone who signs up with their own address and forwards the unopened link then has the recipient's browser signed
556
+ in to their account, and what the recipient enters there lands with them (login CSRF). Off, the user verifies and then
557
+ signs in once, as in Better-Auth's own default. Switch it on (`emailVerification: { autoSignInAfterVerification: true }`)
558
+ only where a verification link reaches nobody but its owner.
559
+
553
560
  ### Sign-Up Checks (v11.13.0+)
554
561
 
555
562
  Sign-up validation is **enabled by default** requiring `termsAndPrivacyAccepted: true`.
@@ -622,13 +629,31 @@ When disabled:
622
629
  - REST `POST /iam/sign-up/email` returns `400 Bad Request` with error `LTNS_0026`
623
630
  - GraphQL `betterAuthSignUp` mutation returns error `LTNS_0026`
624
631
  - `betterAuthFeatures` reports `signUpEnabled: false`
625
- - Sign-in continues to work for existing users
632
+ - A first sign-in through a social provider creates no account (since 12.3.0): the OAuth callback redirects with
633
+ `error=signup_disabled`, `/sign-in/social` with an `idToken` answers 401 `signup disabled`
634
+ - Sign-in continues to work for existing users, through e-mail and every social provider
626
635
 
627
636
  **Defense in Depth:** The flag is enforced at two layers:
628
637
 
629
638
  1. **Custom check** (`CoreBetterAuthService.ensureSignUpEnabled()`) runs in Controller/Resolver _before_ any BetterAuth API call and returns a structured `LTNS_0026` error.
630
639
  2. **Native BetterAuth** `emailAndPassword.disableSignUp` acts as a safety net for any direct API access that bypasses the custom check.
631
640
 
641
+ **Social providers:** Better-Auth reads `emailAndPassword.disableSignUp` for `/sign-up/email` only and decides a social
642
+ sign-up per provider, so `buildSocialProviders()` hands every provider the flag as its own `disableSignUp`. One provider
643
+ can deviate on purpose:
644
+
645
+ ```typescript
646
+ socialProviders: {
647
+ google: { clientId: '...', clientSecret: '...', disableSignUp: false }, // new users through Google despite the global flag
648
+ github: { clientId: '...', clientSecret: '...', disableSignUp: true }, // closed for GitHub even while sign-up is open
649
+ },
650
+ ```
651
+
652
+ Only a boolean counts on the provider; any other value follows `emailAndPassword.disableSignUp`, so a string from
653
+ `NEST_SERVER_CONFIG` cannot open a closed registration. Plugins a project adds itself through `betterAuth.options.plugins`
654
+ (Generic OAuth, Magic Link, …) are not covered and need their own sign-up options. Before 12.3.0 every provider created
655
+ accounts whatever `disableSignUp` said.
656
+
632
657
  **Default:** `false` (sign-up enabled) - fully backward compatible.
633
658
 
634
659
  ### Additional User Fields
@@ -2205,6 +2230,12 @@ These endpoints bypass rate limiting entirely:
2205
2230
  - `/session` - Session checks (frequent client-side calls)
2206
2231
  - `/callback` - OAuth callbacks
2207
2232
 
2233
+ Entries match whole path segments of the path the request is served under (dot segments resolved, case-insensitive, as Express routes): `/sign-in` covers
2234
+ `/sign-in/email`, `/session` does not cover `/sessions-export`. A request whose path under the base path carries a
2235
+ `.` or `..` segment (also `%2e`-encoded), a backslash, an encoded `/` or `\`, or a `#` is refused with 400
2236
+ (`#LTNS_0305: Invalid request path`) before the limiter runs — no route needs such a path, and the limiter and
2237
+ Better-Auth would otherwise read it as two different routes.
2238
+
2208
2239
  ### Response Headers
2209
2240
 
2210
2241
  When rate limiting is enabled, the following headers are added to responses:
@@ -444,6 +444,7 @@ interface PasskeyNormalizationResult {
444
444
  interface SocialProviderConfig {
445
445
  clientId: string;
446
446
  clientSecret: string;
447
+ disableSignUp: boolean;
447
448
  }
448
449
 
449
450
  /**
@@ -955,7 +956,9 @@ function buildEmailVerificationConfig(
955
956
  // Get configuration values (with defaults)
956
957
  const configObj = typeof emailVerificationConfig === 'object' ? emailVerificationConfig : {};
957
958
  const expiresIn = configObj.expiresIn ?? 86400; // 24 hours
958
- const autoSignInAfterVerification = configObj.autoSignInAfterVerification ?? true;
959
+ // Off unless switched on, as Better-Auth has it: on verification Better-Auth signs in the browser that opens the
960
+ // link, to the account the link belongs to — a forwarded link signs its recipient in to the sender's account
961
+ const autoSignInAfterVerification = configObj.autoSignInAfterVerification === true;
959
962
 
960
963
  const result: Record<string, unknown> = {
961
964
  autoSignInAfterVerification,
@@ -1118,9 +1121,16 @@ function serverOnlyPlugin<T extends BetterAuthPlugin>(plugin: T): T {
1118
1121
  * Builds the social providers configuration object dynamically.
1119
1122
  * Iterates over all configured providers and includes those that are enabled
1120
1123
  * with valid clientId and clientSecret.
1124
+ *
1125
+ * `emailAndPassword.disableSignUp: true` closes every way to a new account (12.3.0). Better-Auth reads that flag for
1126
+ * `/sign-up/email` only and decides a social sign-up per provider (`provider.options.disableSignUp`, in the OAuth
1127
+ * callback and in `/sign-in/social` with an `idToken`), so each provider receives it explicitly. A boolean
1128
+ * `disableSignUp` on the provider is the deliberate exception for that provider; any other value follows the global
1129
+ * flag, so a string arriving through `NEST_SERVER_CONFIG` cannot open a closed registration.
1121
1130
  */
1122
1131
  function buildSocialProviders(config: IBetterAuth): Record<string, SocialProviderConfig> {
1123
1132
  const socialProvidersConfig: Record<string, SocialProviderConfig> = {};
1133
+ const signUpClosed = config.emailAndPassword?.disableSignUp === true;
1124
1134
 
1125
1135
  // Iterate over all configured social providers dynamically
1126
1136
  // A provider is enabled by default if it has credentials configured
@@ -1131,6 +1141,7 @@ function buildSocialProviders(config: IBetterAuth): Record<string, SocialProvide
1131
1141
  socialProvidersConfig[name] = {
1132
1142
  clientId: provider.clientId,
1133
1143
  clientSecret: provider.clientSecret,
1144
+ disableSignUp: typeof provider.disableSignUp === 'boolean' ? provider.disableSignUp : signUpClosed,
1134
1145
  };
1135
1146
  }
1136
1147
  }
@@ -38,7 +38,7 @@ type ResolvedEmailVerificationConfig = Pick<
38
38
  * Default configuration for email verification
39
39
  */
40
40
  const DEFAULT_CONFIG: ResolvedEmailVerificationConfig = {
41
- autoSignInAfterVerification: true,
41
+ autoSignInAfterVerification: false,
42
42
  callbackURL: '/auth/verify-email',
43
43
  enabled: true,
44
44
  expiresIn: 86400, // 24 hours in seconds
@@ -192,9 +192,12 @@ export class CoreBetterAuthEmailVerificationService {
192
192
 
193
193
  /**
194
194
  * Check if auto sign-in after verification is enabled
195
+ *
196
+ * `=== true` as in `better-auth.config.ts`, which decides what Better-Auth does: a value arriving as
197
+ * a string in `NEST_SERVER_CONFIG` must not report a sign-in that Better-Auth never performs.
195
198
  */
196
199
  shouldAutoSignIn(): boolean {
197
- return this.config.autoSignInAfterVerification;
200
+ return this.config.autoSignInAfterVerification === true;
198
201
  }
199
202
 
200
203
  /**
@@ -0,0 +1,85 @@
1
+ import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common';
2
+ import type { NextFunction, Request, Response } from 'express';
3
+
4
+ import { ErrorCode } from '../error-code/error-codes.js';
5
+
6
+ /**
7
+ * Whether a raw request path contains something URL resolution rewrites: a `.` or `..` segment (also
8
+ * percent-encoded), a backslash (a segment separator for WHATWG URLs), an encoded slash or backslash, or a `#`
9
+ * (Express routes the part before it, the forwarding to Better-Auth reads the whole string).
10
+ *
11
+ * Every check in front of Better-Auth (rate limiter, controller routes, password routes) reads the RAW path, while
12
+ * Better-Auth routes the path `new URL()` resolved. A path the two read differently passes those checks as one route
13
+ * and runs as another. No route under the Better-Auth base path needs any of these, so such a request is refused.
14
+ *
15
+ * A loop over segments, no regex over the whole path: the path comes from an unauthenticated request and may be as
16
+ * long as Node's header limit allows.
17
+ */
18
+ export function hasNonCanonicalPathSegments(path: string): boolean {
19
+ if (path.includes('\\') || path.includes('#')) {
20
+ return true;
21
+ }
22
+ for (const segment of path.split('/')) {
23
+ if (segment.includes('%')) {
24
+ const lower = segment.toLowerCase();
25
+ if (lower.includes('%2f') || lower.includes('%5c')) {
26
+ return true;
27
+ }
28
+ }
29
+ // The longest dot segment is `%2e%2e`
30
+ if (segment.length === 0 || segment.length > 6) {
31
+ continue;
32
+ }
33
+ const dots = segment.replace(/%2e/gi, '.');
34
+ if (dots === '.' || dots === '..') {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * The path as Better-Auth routes it: dot segments resolved (also percent-encoded ones), backslashes read as slashes —
43
+ * what `new URL()` makes of it. Query string and fragment are dropped. A path `new URL()` refuses comes back as given.
44
+ */
45
+ export function canonicalRequestPath(path: string): string {
46
+ try {
47
+ return new URL(path, 'http://canonical.invalid').pathname;
48
+ } catch {
49
+ return path;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Whether `path` contains the segments of `endpoint` as a contiguous run of WHOLE segments: `/sign-in` matches
55
+ * `/sign-in/email` and `/phone-number/sign-in`, never `/sign-in-legacy`. Segments are compared as they are; pass a
56
+ * {@link canonicalRequestPath} result, so `..` is already resolved.
57
+ */
58
+ export function pathContainsSegments(path: string, endpoint: string): boolean {
59
+ const wanted = endpoint.split('/').filter(Boolean);
60
+ if (wanted.length === 0) {
61
+ return false;
62
+ }
63
+ const segments = path.split('/').filter(Boolean);
64
+ for (let start = 0; start + wanted.length <= segments.length; start++) {
65
+ if (wanted.every((segment, offset) => segments[start + offset] === segment)) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * First middleware under the Better-Auth base path: refuses a request whose path is not in canonical form (see
74
+ * {@link hasNonCanonicalPathSegments}) with 400, before any check reads that path.
75
+ */
76
+ @Injectable()
77
+ export class CoreBetterAuthPathGuardMiddleware implements NestMiddleware {
78
+ use(req: Request, _res: Response, next: NextFunction) {
79
+ const path = (req.originalUrl || req.url || '').split('?')[0];
80
+ if (hasNonCanonicalPathSegments(path)) {
81
+ throw new BadRequestException(ErrorCode.INVALID_REQUEST_PATH);
82
+ }
83
+ next();
84
+ }
85
+ }
@@ -50,9 +50,12 @@ export class CoreBetterAuthRateLimitMiddleware implements NestMiddleware {
50
50
  // Get client IP
51
51
  const ip = this.getClientIp(req);
52
52
 
53
- // Get the path relative to basePath
53
+ // The path relative to basePath, read from `originalUrl`: Nest mounts this middleware on `${basePath}/*path`, and
54
+ // Express moves the whole match into `req.baseUrl`, so `req.path` is "/" for every request here. Reading it made
55
+ // every IAM request of one client share a single counter with the full limit — no skip list, no strict limit
54
56
  const basePath = this.betterAuthService.getBasePath();
55
- const path = req.path.startsWith(basePath) ? req.path.substring(basePath.length) : req.path;
57
+ const requestPath = (req.originalUrl || req.url || '').split('?')[0];
58
+ const path = requestPath.startsWith(basePath) ? requestPath.substring(basePath.length) : requestPath;
56
59
 
57
60
  // Check rate limit
58
61
  const result = await this.rateLimiter.check(ip, path);
@@ -2,6 +2,7 @@ import { Injectable, Logger, Optional } from '@nestjs/common';
2
2
 
3
3
  import { IBetterAuthRateLimit } from '../../common/interfaces/server-options.interface.js';
4
4
  import { CoreRedisService } from '../../common/services/core-redis.service.js';
5
+ import { canonicalRequestPath, pathContainsSegments } from './core-better-auth-path-guard.middleware.js';
5
6
  import {
6
7
  InMemoryRateLimitStore,
7
8
  rateLimitKey,
@@ -134,10 +135,10 @@ export class CoreBetterAuthRateLimiter {
134
135
  * Check if a request is allowed under the rate limit
135
136
  *
136
137
  * @param ip - Client IP address
137
- * @param path - Request path (relative to basePath)
138
+ * @param requestPath - Request path (relative to basePath), as sent; dot segments are resolved here
138
139
  * @returns Rate limit check result
139
140
  */
140
- async check(ip: string, path: string): Promise<RateLimitResult> {
141
+ async check(ip: string, requestPath: string): Promise<RateLimitResult> {
141
142
  // If rate limiting is disabled, always allow
142
143
  if (!this.config.enabled) {
143
144
  return {
@@ -149,6 +150,13 @@ export class CoreBetterAuthRateLimiter {
149
150
  };
150
151
  }
151
152
 
153
+ // Decide on the path the request is served under, not the one as sent: `/callback/../sign-in/email` reaches the
154
+ // sign-in handler, so it must not be skipped as a callback (CoreBetterAuthPathGuardMiddleware refuses such paths
155
+ // before they get here; this keeps the limiter correct where that guard is not registered). Lower-cased because
156
+ // Express routes case-insensitively: `/sign-in/EMAIL` is the same sign-in and must share its counter and its
157
+ // strict limit
158
+ const path = canonicalRequestPath(requestPath).toLowerCase();
159
+
152
160
  // Check if this endpoint should skip rate limiting
153
161
  if (this.shouldSkip(path)) {
154
162
  return {
@@ -249,10 +257,11 @@ export class CoreBetterAuthRateLimiter {
249
257
  }
250
258
 
251
259
  /**
252
- * Determine if an endpoint should skip rate limiting
260
+ * Determine if an endpoint should skip rate limiting: an entry matches whole path segments, so `/session` skips
261
+ * `/session` but not `/sessions-export`
253
262
  */
254
263
  private shouldSkip(path: string): boolean {
255
- return this.config.skipEndpoints.some((skip) => path === skip || path.endsWith(skip) || path.includes(skip));
264
+ return this.config.skipEndpoints.some((skip) => pathContainsSegments(path, skip.toLowerCase()));
256
265
  }
257
266
 
258
267
  /**
@@ -260,9 +269,7 @@ export class CoreBetterAuthRateLimiter {
260
269
  * Strict endpoints get half the normal limit
261
270
  */
262
271
  private getLimit(path: string): number {
263
- const isStrict = this.config.strictEndpoints.some(
264
- (strict) => path === strict || path.endsWith(strict) || path.includes(strict),
265
- );
272
+ const isStrict = this.config.strictEndpoints.some((strict) => pathContainsSegments(path, strict.toLowerCase()));
266
273
 
267
274
  return isStrict ? Math.ceil(this.config.max / 2) : this.config.max;
268
275
  }