@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
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "12.1.1",
3
+ "version": "12.3.0",
4
4
  "type": "module",
5
- "description": "Server framework in TypeScript on top of NestJS: GraphQL and REST APIs on MongoDB, PostgreSQL, MySQL or MariaDB, with authentication, field-level permissions, multi-tenancy and an AI assistant.",
5
+ "description": "Server framework in TypeScript on top of NestJS: GraphQL and REST APIs on MongoDB, PostgreSQL, MySQL or MariaDB, with authentication, field-level permissions, multi-tenancy, offline sync and an AI assistant.",
6
6
  "keywords": [
7
7
  "node",
8
8
  "nest",
@@ -15,6 +15,7 @@
15
15
  "graphql",
16
16
  "rest",
17
17
  "multi-tenancy",
18
+ "offline-sync",
18
19
  "typescript"
19
20
  ],
20
21
  "author": "lenne.Tech <info@lenne.tech> (https://lenne.tech)",
@@ -103,18 +104,18 @@
103
104
  "@as-integrations/express5": "1.1.2",
104
105
  "@getbrevo/brevo": "6.2.0",
105
106
  "@modelcontextprotocol/sdk": "1.32.2",
106
- "@nestjs/apollo": "14.1.1",
107
+ "@nestjs/apollo": "14.1.2",
107
108
  "@nestjs/common": "12.3.2",
108
109
  "@nestjs/core": "12.3.2",
109
- "@nestjs/graphql": "14.1.0",
110
+ "@nestjs/graphql": "14.1.2",
110
111
  "@nestjs/jwt": "12.0.3",
111
112
  "@nestjs/passport": "12.0.0",
112
113
  "@nestjs/platform-express": "12.3.2",
113
114
  "@nestjs/schedule": "12.0.2",
114
- "@nestjs/swagger": "12.0.5",
115
+ "@nestjs/swagger": "12.1.0",
115
116
  "@nestjs/terminus": "12.1.0",
116
- "@tus/file-store": "2.1.1",
117
- "@tus/server": "2.4.6",
117
+ "@tus/file-store": "2.1.2",
118
+ "@tus/server": "2.4.7",
118
119
  "@types/supertest": "7.2.1",
119
120
  "bcrypt": "6.0.0",
120
121
  "class-transformer": "0.5.1",
@@ -130,13 +131,13 @@
130
131
  "graphql-subscriptions": "3.0.0",
131
132
  "graphql-upload": "17.1.0",
132
133
  "graphql-ws": "6.3.0",
133
- "jose": "6.2.13",
134
+ "jose": "6.2.15",
134
135
  "js-sha256": "1.0.0",
135
136
  "json-to-graphql-query": "2.3.0",
136
137
  "lodash": "4.18.1",
137
138
  "multer": "2.5.0",
138
139
  "node-mailjet": "6.0.11",
139
- "nodemailer": "10.3.0",
140
+ "nodemailer": "10.3.1",
140
141
  "passport": "0.7.0",
141
142
  "passport-jwt": "4.0.1",
142
143
  "reflect-metadata": "0.2.2",
@@ -225,9 +226,9 @@
225
226
  }
226
227
  },
227
228
  "devDependencies": {
228
- "@aws-sdk/client-s3": "3.1159.0",
229
- "@aws-sdk/lib-storage": "3.1159.0",
230
- "@aws-sdk/s3-request-presigner": "3.1159.0",
229
+ "@aws-sdk/client-s3": "3.1161.0",
230
+ "@aws-sdk/lib-storage": "3.1161.0",
231
+ "@aws-sdk/s3-request-presigner": "3.1161.0",
231
232
  "@better-auth/core": "1.7.14",
232
233
  "@better-auth/passkey": "1.7.14",
233
234
  "@compodoc/compodoc": "2.0.0",
@@ -236,8 +237,8 @@
236
237
  "@nestjs/schematics": "12.0.12",
237
238
  "@nestjs/testing": "12.3.2",
238
239
  "@swc/cli": "0.8.2",
239
- "@swc/core": "1.16.20",
240
- "@tus/s3-store": "2.0.9",
240
+ "@swc/core": "1.16.21",
241
+ "@tus/s3-store": "2.1.0",
241
242
  "@types/compression": "1.8.1",
242
243
  "@types/cookie-parser": "1.4.10",
243
244
  "@types/ejs": "3.1.5",
@@ -247,31 +248,32 @@
247
248
  "@types/node": "26.6.6",
248
249
  "@types/passport": "1.0.17",
249
250
  "@types/pg": "8.23.1",
250
- "@vitest/coverage-v8": "5.0.5",
251
+ "@vitest/coverage-v8": "5.1.0",
251
252
  "better-auth": "1.7.14",
252
- "bullmq": "6.5.1",
253
+ "bullmq": "6.5.4",
253
254
  "cross-env": "10.1.0",
254
255
  "drizzle-orm": "1.0.0-rc.5",
255
256
  "ioredis": "6.0.0",
256
- "kysely": "0.29.7",
257
+ "kysely": "0.29.8",
257
258
  "mongodb": "7.7.0",
258
259
  "mongoose": "9.11.4",
259
260
  "mysql2": "3.24.6",
260
261
  "nodemon": "3.1.14",
261
- "otpauth": "9.5.2",
262
+ "otpauth": "9.6.0",
262
263
  "oxfmt": "0.75.0",
263
264
  "oxlint": "1.90.0",
264
265
  "oxlint-tsgolint": "7.0.2005",
265
266
  "pg": "8.23.1",
267
+ "playwright-core": "1.64.1",
266
268
  "rimraf": "6.1.3",
267
- "smtp-server": "3.21.1",
269
+ "smtp-server": "3.21.3",
268
270
  "tsx": "4.23.15",
269
271
  "tus-js-client": "4.3.1",
270
272
  "typeorm": "1.1.2",
271
273
  "typescript": "6.0.3",
272
274
  "unplugin-swc": "2.0.0",
273
- "vite": "8.3.4",
274
- "vitest": "5.0.5"
275
+ "vite": "8.3.5",
276
+ "vitest": "5.1.0"
275
277
  },
276
278
  "main": "dist/index.js",
277
279
  "types": "dist/index.d.ts",
package/src/config.env.ts CHANGED
@@ -59,7 +59,9 @@ const config: { [env: string]: IServerOptions } = {
59
59
  rpId: 'localhost',
60
60
  rpName: 'Nest Server Local',
61
61
  },
62
- rateLimit: { enabled: true, max: 100, windowSeconds: 60 },
62
+ // 200: sign-in and sign-up get the strict half (100), the headroom the suites had while the limiter
63
+ // counted every IAM request of a client on one shared counter of 100
64
+ rateLimit: { enabled: true, max: 200, windowSeconds: 60 },
63
65
  secret: process.env.BETTER_AUTH_SECRET || 'BETTER_AUTH_SECRET_LOCAL_32_CHARS_M',
64
66
  // Social providers disabled in local environment (no credentials)
65
67
  socialProviders: {
@@ -367,7 +369,9 @@ const config: { [env: string]: IServerOptions } = {
367
369
  rpId: 'localhost',
368
370
  rpName: 'Nest Server Local',
369
371
  },
370
- rateLimit: { enabled: true, max: 100, windowSeconds: 60 },
372
+ // 200: sign-in and sign-up get the strict half (100), the headroom the suites had while the limiter
373
+ // counted every IAM request of a client on one shared counter of 100
374
+ rateLimit: { enabled: true, max: 200, windowSeconds: 60 },
371
375
  secret: process.env.BETTER_AUTH_SECRET || 'BETTER_AUTH_SECRET_LOCAL_32_CHARS_M',
372
376
  // Social providers disabled in local environment (no credentials)
373
377
  socialProviders: {
@@ -82,6 +82,17 @@ export interface IWsConnectionRegistration {
82
82
  */
83
83
  carrier: object;
84
84
 
85
+ /** How the log names the connection when it is closed. Default `GraphQL WebSocket`. */
86
+ channel?: string;
87
+
88
+ /**
89
+ * The connection cannot be re-checked in place — an HTTP stream has no handshake to repeat — so every re-check ends
90
+ * it, and the client's reconnect passes the guards again. The end the re-check interval brings is planned and logged
91
+ * at debug; one an invalidation brings (sign-out, role or tenant change) is logged like every withdrawn
92
+ * authorization. `reauthenticate` is not called.
93
+ */
94
+ endsOnRecheck?: boolean;
95
+
85
96
  /**
86
97
  * Re-run the handshake's authentication. Resolves to the user the credential authenticates as NOW,
87
98
  * or to a falsy value when it no longer does. A rejection counts as falsy. Without it only the
@@ -105,7 +116,9 @@ export interface IWsConnectionIdentity {
105
116
 
106
117
  /** @internal The registered state of one connection. */
107
118
  export interface WsConnection {
119
+ readonly channel: string;
108
120
  readonly email?: string;
121
+ readonly endsOnRecheck: boolean;
109
122
  readonly iamId?: string;
110
123
  readonly reauthenticate?: () => any;
111
124
  readonly socket?: IWsConnectionSocket;
@@ -206,13 +219,15 @@ export function readWsTenantHeader(contextValue: any): string | undefined {
206
219
  * the handshake recorded on the carrier.
207
220
  */
208
221
  export function registerWsConnection(registration: IWsConnectionRegistration): void {
209
- const { carrier, reauthenticate, socket, user } = registration;
222
+ const { carrier, channel, endsOnRecheck, reauthenticate, socket, user } = registration;
210
223
  if (!carrier || typeof carrier !== 'object') {
211
224
  return;
212
225
  }
213
226
  const userId = user?.id ?? user?._id;
214
227
  const connection: WsConnection = {
228
+ channel: channel || 'GraphQL WebSocket',
215
229
  email: typeof user?.email === 'string' ? user.email.toLowerCase() : undefined,
230
+ endsOnRecheck: endsOnRecheck === true,
216
231
  iamId: typeof user?.iamId === 'string' ? user.iamId : undefined,
217
232
  reauthenticate,
218
233
  socket,
@@ -246,15 +261,20 @@ export function getWsConnection(contextValue: any): undefined | WsConnection {
246
261
 
247
262
  /**
248
263
  * Close a connection whose authorization no longer holds. Idempotent; always answers `false` so a
249
- * caller can `return terminate(...)`.
264
+ * caller can `return terminate(...)`. `debug` is for a planned end, which says nothing about the user.
250
265
  */
251
- function terminate(connection: WsConnection, reason: string): false {
266
+ function terminate(connection: WsConnection, reason: string, level: 'debug' | 'log' = 'log'): false {
252
267
  if (connection.terminated) {
253
268
  return false;
254
269
  }
255
270
  connection.terminated = true;
256
271
  liveConnections.delete(connection);
257
- logger.log(`Closing GraphQL WebSocket of user ${connection.userId ?? '(unknown)'}: ${reason}`);
272
+ const message = `Closing ${connection.channel} of user ${connection.userId ?? '(unknown)'}: ${reason}`;
273
+ if (level === 'debug') {
274
+ logger.debug(message);
275
+ } else {
276
+ logger.log(message);
277
+ }
258
278
  try {
259
279
  // A close reason is limited to 123 bytes on the wire; these are short and ASCII.
260
280
  connection.socket?.close(WS_AUTHORIZATION_CHANGED_CLOSE_CODE, reason);
@@ -265,7 +285,15 @@ function terminate(connection: WsConnection, reason: string): false {
265
285
  }
266
286
 
267
287
  async function revalidate(connection: WsConnection): Promise<boolean> {
288
+ const invalidated = connection.stale;
268
289
  connection.stale = false;
290
+ if (connection.endsOnRecheck) {
291
+ // Nothing to check in place: the end IS the re-check, and the reconnect is authorized afresh. Only an invalidation
292
+ // says that the rights may have changed; the interval is the planned beat and must not read like a lost session.
293
+ return invalidated
294
+ ? terminate(connection, 'authorization invalidated, client reconnects')
295
+ : terminate(connection, 're-check interval reached, client reconnects', 'debug');
296
+ }
269
297
  try {
270
298
  let user = connection.user;
271
299
  if (connection.reauthenticate) {
@@ -510,8 +510,14 @@ export type IBetterAuth = IBetterAuthWithoutPasskey | IBetterAuthWithPasskey;
510
510
  */
511
511
  export interface IBetterAuthEmailVerificationConfig {
512
512
  /**
513
- * Whether to automatically sign in the user after email verification.
514
- * @default true
513
+ * Whether `GET /iam/verify-email` signs in the browser that opens the verification link.
514
+ *
515
+ * Off by default, as in Better-Auth (12.3.0+; before, the framework defaulted to `true`). Switching it on signs in
516
+ * WHOEVER opens the link, to the account the link belongs to: someone who signs up with their own address and
517
+ * forwards the unopened link has the recipient's browser signed in to their account, and what the recipient then
518
+ * enters lands there (login CSRF). Only switch it on where verification links reach nobody but their owner.
519
+ *
520
+ * @default false
515
521
  */
516
522
  autoSignInAfterVerification?: boolean;
517
523
 
@@ -975,6 +981,17 @@ export interface IBetterAuthSocialProvider {
975
981
  */
976
982
  clientSecret: string;
977
983
 
984
+ /**
985
+ * Whether a first sign-in through this provider may create an account.
986
+ *
987
+ * Unset, the provider follows `betterAuth.emailAndPassword.disableSignUp`: with that set to `true` (since 12.3.0)
988
+ * nobody gets a new account through any provider, while existing users keep signing in. `false` opens sign-up
989
+ * through this provider although `disableSignUp` closes it otherwise; `true` closes it for this provider alone.
990
+ * Only a boolean counts: any other value follows `emailAndPassword.disableSignUp`.
991
+ * @default emailAndPassword.disableSignUp
992
+ */
993
+ disableSignUp?: boolean;
994
+
978
995
  /**
979
996
  * Whether this provider is enabled.
980
997
  * Defaults to true when clientId and clientSecret are provided.
@@ -2004,7 +2021,13 @@ export interface IAi {
2004
2021
  * Fallback total context window (input + output tokens) used to budget the
2005
2022
  * assembled session messages when a connection has no `contextWindow` set. When the
2006
2023
  * conversation (per user/session) would overflow, the oldest history turns are
2007
- * dropped and oversized payloads truncated before the LLM call.
2024
+ * dropped and oversized payloads truncated before the LLM call. Native tool
2025
+ * definitions count against that budget too (12.3.0+), and when the system prompt and
2026
+ * tools take more than half of it, a warning names the connection once per process.
2027
+ *
2028
+ * A test that guards the prompt size against this value guards the connections WITHOUT
2029
+ * a window of their own — the worst case — not the window a pinned or detected
2030
+ * connection actually has. Name it that way; raising its limit removes that guard.
2008
2031
  * @default 8192
2009
2032
  */
2010
2033
  contextWindow?: number;
@@ -2061,14 +2084,14 @@ export interface IAi {
2061
2084
  * a caller sees and may run is decided server-side by the registry's role filter
2062
2085
  * (`AiToolRegistry.forUser()`), re-checked at execution time, and the
2063
2086
  * confirmation gate reads the `mutating`/`destructive` FLAGS, never the
2064
- * description text. (`AiTool.authorize()` runs in plan mode only — in auto mode
2065
- * and over MCP, data-level checks belong inside `execute()`.)
2087
+ * description text. (`AiTool.authorize()` runs before every execution — auto
2088
+ * mode, plan mode and MCP — on the call's arguments, never its description.)
2066
2089
  *
2067
2090
  * Applies to the EMULATED tool protocol, not to a provider as such. In auto mode
2068
2091
  * a connection with native tool calling receives every full description and schema
2069
- * through `buildToolSchemas()` regardless, so no truncation is applied there and no
2070
- * `search_tools` banner is emitted — capping would otherwise assert a truncation
2071
- * that the tool payload right next to it contradicts.
2092
+ * through `buildToolSchemas()` regardless, and its catalog lists names only since
2093
+ * 12.3.0 (`nativeToolCatalog`), so this cap has no effect there and no `search_tools`
2094
+ * banner is emitted.
2072
2095
  *
2073
2096
  * **Plan mode always uses the emulated protocol** (it sends no native schemas), so a
2074
2097
  * native connection IS truncated there. Because plan mode returns a complete plan in
@@ -2091,6 +2114,17 @@ export interface IAi {
2091
2114
  */
2092
2115
  deferToolSummaryChars?: number;
2093
2116
 
2117
+ /**
2118
+ * What the system prompt's tool catalog holds for a connection with NATIVE tool calling in auto mode (12.3.0+).
2119
+ * Such a connection receives every description and parameter schema in the tool payload, so by default the catalog
2120
+ * lists names only — repeating the descriptions (and, without `deferToolSchemas`, the schemas) paid for every tool
2121
+ * twice. `'full'` restores the full catalog, for a model that picks tools better with the descriptions in the prompt.
2122
+ *
2123
+ * Emulated connections and plan mode always get the full catalog: there it is the model's only source.
2124
+ * @default 'names'
2125
+ */
2126
+ nativeToolCatalog?: 'full' | 'names';
2127
+
2094
2128
  /** Maximum number of agent-loop iterations (tool round-trips). @default 5 */
2095
2129
  maxIterations?: number;
2096
2130
 
@@ -2142,11 +2176,32 @@ export interface IAi {
2142
2176
  * answers with a built-in consent page: sign-in through Better-Auth (password + 2FA),
2143
2177
  * then Allow / Deny. Replace it via `CoreModule.forRoot(env, { ai: { mcpOAuthService } })`.
2144
2178
  * Without `oauth`, MCP authenticates via the existing Bearer/session token.
2179
+ *
2180
+ * `tools/call` applies the admin's tool policies and a tool's `authorize()`, and asks the user before a
2181
+ * tool that needs confirmation (`destructive`, or `mutating` under `ai.confirmation.mutating`) — see
2182
+ * `confirmation`.
2145
2183
  * @default false
2146
2184
  */
2147
2185
  mcp?:
2148
2186
  | boolean
2149
2187
  | {
2188
+ /**
2189
+ * Whether MCP asks the user — through MCP elicitation, a yes/no form the client shows — before a
2190
+ * `destructive` tool, or a `mutating` one under `ai.confirmation.mutating` (12.3.0+). The tool runs only
2191
+ * on an explicit yes; a client without the `elicitation` capability gets a refusal. `authorize()` runs
2192
+ * either way, and an admin's `ask` tool policy always asks.
2193
+ *
2194
+ * - `true` / `{}` / unset: MCP asks, unless a user switched it off for themselves
2195
+ * (`PUT /ai/settings`, `updateAiSettings` — like a "bypass permissions" mode).
2196
+ * - `false`: MCP asks nobody, unless a user switched it on for themselves.
2197
+ * - `{ default, enforced }`: `default` as above; `enforced: true` ignores every user's choice and
2198
+ * refuses a change with 403.
2199
+ *
2200
+ * Only `false` / `default: false` switch the default off and only `enforced: true` enforces, so a
2201
+ * misspelt value never relaxes the gate.
2202
+ * @default { default: true, enforced: false }
2203
+ */
2204
+ confirmation?: boolean | { default?: boolean; enforced?: boolean };
2150
2205
  enabled?: boolean;
2151
2206
  oauth?: boolean;
2152
2207
  /**
@@ -2164,6 +2219,17 @@ export interface IAi {
2164
2219
  */
2165
2220
  oauthLoginUrl?: string;
2166
2221
  oauthSecret?: string;
2222
+ /**
2223
+ * Serve `/ai/mcp` without sessions: every POST builds its own server and transport for the
2224
+ * authenticated user and ends with the response, no `mcp-session-id` is issued, and GET /
2225
+ * DELETE answer 405. Any replica can answer any request, so no sticky sessions are needed
2226
+ * behind a load balancer. The price is what a session carries: server-initiated messages
2227
+ * (notifications, SSE streams) and server-initiated requests — MCP elicitation among them, so a
2228
+ * tool that needs confirmation (`confirmation`) is refused unless the user or `confirmation`
2229
+ * switched the question off. A server that only offers read tools uses none of it (12.3.0+).
2230
+ * @default false
2231
+ */
2232
+ stateless?: boolean;
2167
2233
  };
2168
2234
 
2169
2235
  /** Rate limiting for prompts. */
@@ -3509,6 +3575,16 @@ export interface IServerOptions {
3509
3575
  path?: string;
3510
3576
  };
3511
3577
 
3578
+ /**
3579
+ * Offline sync (`docs/offline-sync-protocol.md`): `GET/POST /sync/{model}` for the models whose service carries
3580
+ * `@SyncService()` and whose model is declared with `@PersistedModel({ sync })`. Presence implies enabled — `true`
3581
+ * or `{}` enables it with the defaults, `{ enabled: false }` pre-configures without enabling. Absent: no route, no
3582
+ * store, no code path.
3583
+ *
3584
+ * @since 12.2.0
3585
+ */
3586
+ sync?: boolean | ISync;
3587
+
3512
3588
  /**
3513
3589
  * System setup configuration for initial admin creation.
3514
3590
  *
@@ -3831,6 +3907,125 @@ export interface IS3Config {
3831
3907
  stagingBucket?: string;
3832
3908
  }
3833
3909
 
3910
+ /**
3911
+ * Offline sync (`sync`, presence implies enabled). See `src/core/modules/sync/README.md`.
3912
+ *
3913
+ * @since 12.2.0
3914
+ */
3915
+ export interface ISync {
3916
+ /**
3917
+ * Secret the pull cursors are signed with (HMAC; the key is derived from it). Falls back to the
3918
+ * `SECRETS_ENCRYPTION_KEY` environment variable. REQUIRED in `production` / `staging` — the boot fails without it;
3919
+ * elsewhere an insecure development default is used with a warning. Changing it answers every client's next pull
3920
+ * with a full resync (`410 invalid`).
3921
+ */
3922
+ cursorSecret?: string;
3923
+
3924
+ /**
3925
+ * Pre-configure without enabling.
3926
+ * @default true (when the object is present)
3927
+ */
3928
+ enabled?: boolean;
3929
+
3930
+ /**
3931
+ * Hints that a synchronized model changed (protocol §9): SSE `GET /sync/hints` and the GraphQL subscription
3932
+ * `syncHint`, carrying `{ model, at }` only, to callers with the model's pull roles and — on a tenant-scoped model —
3933
+ * for changes in their own tenants. A hint only means "pull now": a client works without them. `false` switches both
3934
+ * channels off; with `redis` a hint reaches the subscribers of every replica.
3935
+ * @default on (both channels)
3936
+ */
3937
+ hints?:
3938
+ | boolean
3939
+ | {
3940
+ /**
3941
+ * At most one hint per model and tenant within this many milliseconds — a burst of writes is one hint.
3942
+ * @default 250
3943
+ */
3944
+ debounceMs?: number;
3945
+ /** @default true (when the object is present) */
3946
+ enabled?: boolean;
3947
+ /**
3948
+ * The GraphQL subscription `syncHint` (only where GraphQL is enabled).
3949
+ * @default true
3950
+ */
3951
+ graphql?: boolean;
3952
+ /**
3953
+ * The SSE stream `GET /sync/hints`.
3954
+ * @default true
3955
+ */
3956
+ sse?: boolean;
3957
+ };
3958
+
3959
+ /**
3960
+ * Route prefix of the sync endpoints.
3961
+ * @default 'sync'
3962
+ */
3963
+ path?: string;
3964
+
3965
+ pull?: {
3966
+ /**
3967
+ * Records per page when the client sends no `limit`.
3968
+ * @default 200
3969
+ */
3970
+ defaultLimit?: number;
3971
+ /**
3972
+ * Upper bound of the serialized size of one page; a page ends before it would exceed it (it always holds at least
3973
+ * one item when one is pending).
3974
+ * @default 1048576 (1 MiB)
3975
+ */
3976
+ maxBytes?: number;
3977
+ /**
3978
+ * Highest `limit` a client may ask for (at most 1000).
3979
+ * @default 1000
3980
+ */
3981
+ maxLimit?: number;
3982
+ /**
3983
+ * MongoDB only: the window W a pull run re-reads below its previous start (plan §3.4). Changes stamped within W
3984
+ * before a run started may be repeated; a write that takes longer than W from its stamp until it is visible would
3985
+ * be missed, so W must cover the longest such write.
3986
+ * @default replica set / sharded: the server's `transactionLifetimeLimitSeconds` (60 when unreadable) + 30;
3987
+ * standalone: 30
3988
+ */
3989
+ windowSeconds?: number;
3990
+ };
3991
+
3992
+ push?: {
3993
+ /**
3994
+ * Items per push request; more answer 400.
3995
+ * @default 100
3996
+ */
3997
+ maxItems?: number;
3998
+ };
3999
+
4000
+ /**
4001
+ * Per-user rate limits of pull and push (fixed window; exact across replicas when `redis` is configured). `false`
4002
+ * switches both off, `{ pull: false }` one of them.
4003
+ * @default pull 120 / 60 s, push 60 / 60 s
4004
+ */
4005
+ rateLimit?:
4006
+ | false
4007
+ | {
4008
+ pull?: false | { max?: number; windowSeconds?: number };
4009
+ push?: false | { max?: number; windowSeconds?: number };
4010
+ };
4011
+
4012
+ retention?: {
4013
+ /**
4014
+ * How long the outcome of a pushed mutation is kept, so a repeat is answered with it (protocol §6.5).
4015
+ * @default 30
4016
+ */
4017
+ mutationsDays?: number;
4018
+ /**
4019
+ * How long a tombstone is kept. Every hour (on one replica, under a lease) the purge first moves the model's
4020
+ * horizon to this age, then deletes the older tombstones for good; a client whose cursor is older than the horizon
4021
+ * answers its next pull with a full resync (`410 horizon`). `false` keeps tombstones for ever; an invalid value
4022
+ * (zero, negative, not a number) falls back to the default — never to "purge everything".
4023
+ * @default 90
4024
+ */
4025
+ tombstonesDays?: false | number;
4026
+ };
4027
+ }
4028
+
3834
4029
  export interface ISystemSetup {
3835
4030
  /**
3836
4031
  * Whether system setup is enabled.
@@ -4258,6 +4453,10 @@ interface IBetterAuthBase {
4258
4453
  * Disable user registration (sign-up) via BetterAuth.
4259
4454
  * Passed through to better-auth's native emailAndPassword.disableSignUp.
4260
4455
  * Custom endpoints (GraphQL + REST) also check this flag early.
4456
+ * Since 12.3.0 it closes social sign-up too: every configured social provider receives it as its own
4457
+ * `disableSignUp`, so a first sign-in through Google, GitHub etc. creates no account, while existing users keep
4458
+ * signing in. `socialProviders.<name>.disableSignUp: false` opens one provider deliberately. Plugins a project
4459
+ * registers itself through `options.plugins` are not covered.
4261
4460
  * The Swagger document built by `setupSwagger()` then leaves out `POST /iam/sign-up/email`,
4262
4461
  * which would answer `SIGNUP_DISABLED` to every caller.
4263
4462
  * @default false
@@ -4831,6 +5030,26 @@ export interface ICoreModuleOverrides {
4831
5030
  service?: Type<any>;
4832
5031
  };
4833
5032
 
5033
+ /**
5034
+ * Override the offline sync endpoints, the hint subscription and their service (`sync` config).
5035
+ *
5036
+ * - `controller` must extend `CoreSyncController` (e.g. to release a sync scope to API tokens)
5037
+ * - `hintsController` must extend `CoreSyncHintsController` (`GET /sync/hints`, e.g. for API tokens there too)
5038
+ * - `resolver` must extend `CoreSyncResolver` (the `syncHint` subscription)
5039
+ * - `service` must extend `CoreSyncService`
5040
+ *
5041
+ * @example
5042
+ * ```typescript
5043
+ * { sync: { controller: SyncController } }
5044
+ * ```
5045
+ */
5046
+ sync?: {
5047
+ controller?: Type<any>;
5048
+ hintsController?: Type<any>;
5049
+ resolver?: Type<any>;
5050
+ service?: Type<any>;
5051
+ };
5052
+
4834
5053
  /**
4835
5054
  * Override AI module collaborators with project-specific subclasses.
4836
5055
  *
@@ -4853,6 +5072,7 @@ export interface ICoreModuleOverrides {
4853
5072
  * - `slotService` must extend `CoreAiSlotService`
4854
5073
  * - `toolGrantService` must extend `CoreAiToolGrantService`
4855
5074
  * - `toolPolicyService` must extend `CoreAiToolPolicyService`
5075
+ * - `userSettingService` must extend `CoreAiUserSettingService`
4856
5076
  *
4857
5077
  * @example
4858
5078
  * ```typescript
@@ -4886,6 +5106,7 @@ export interface ICoreModuleOverrides {
4886
5106
  slotService?: Type<any>;
4887
5107
  toolGrantService?: Type<any>;
4888
5108
  toolPolicyService?: Type<any>;
5109
+ userSettingService?: Type<any>;
4889
5110
  };
4890
5111
 
4891
5112
  /**
@@ -32,6 +32,11 @@ export interface ServiceOptions {
32
32
  // If truly: create mode is activated
33
33
  create?: boolean;
34
34
 
35
+ // The id `create()` gives the new record: on a synchronized model the id a client chose offline, in the model's
36
+ // format (refused otherwise), on any other model a 24-character hex id. Passed beside the input because an input
37
+ // class keeps only the fields it declares: an `id` inside the input is lost the moment `inputType` maps it
38
+ createId?: string;
39
+
35
40
  // Current user to set ownership, check rights and other things
36
41
  currentUser?: {
37
42
  [key: string]: any;
@@ -42,9 +47,19 @@ export interface ServiceOptions {
42
47
  // Field selection for results (will be overwritten by populate if populate was set)
43
48
  fieldSelection?: FieldSelection;
44
49
 
50
+ // Conditional change of a synchronized model (`@PersistedModel({ sync })`): `update()` and `delete()` apply only
51
+ // when the stored record still has this version, in one atomic statement, and throw a 409 (`VersionConflictError`)
52
+ // otherwise. Refused on a model that is not synchronized
53
+ expectedVersion?: number;
54
+
45
55
  // Determines whether all restrictions are ignored
46
56
  force?: boolean;
47
57
 
58
+ // Read a synchronized model's tombstones too — its deleted records, which every other read leaves out (get, find,
59
+ // findOne, findAndCount, distinct). Reads only: update() and delete() never touch a tombstone and answer 404 for one.
60
+ // Applies to the main model only: a populated reference to a tombstone stays empty
61
+ includeTombstones?: boolean;
62
+
48
63
  // Overwrites type of input (array items)
49
64
  inputType?: new (...params: any[]) => any;
50
65
 
@@ -4,8 +4,15 @@ import { getConnectionToken, getModelToken } from './mongo/mongoose-compat.js';
4
4
  import type { Model } from 'mongoose';
5
5
 
6
6
  import { registerModelSchema } from './model-metadata.registry.js';
7
+ import { isDeclaredSchema } from './mongo/model-declarations.js';
8
+ import { applySyncSchema, assertSyncCollectionReady } from './mongo/sync-schema.plugin.js';
7
9
  import { MongooseDataStore } from './mongoose-data-store.js';
8
- import { getPersistedModelOptions, getPersistedModelOptionsByName } from './persisted-model.decorator.js';
10
+ import {
11
+ getPersistedModelOptions,
12
+ getPersistedModelOptionsByName,
13
+ getPersistedSyncByName,
14
+ } from './persisted-model.decorator.js';
15
+ import { resolveSyncOptions } from './sync-model.js';
9
16
  import {
10
17
  bindStoreToModel,
11
18
  getPersistenceRuntime,
@@ -46,7 +53,15 @@ export interface PersistenceModelDefinition extends ModelDefinition {
46
53
  export class CorePersistenceModule {
47
54
  static forFeature(models: PersistenceModelDefinition[]): DynamicModule {
48
55
  for (const model of models) {
49
- registerModelSchema(model.name, model.schema);
56
+ const sync = resolveSyncOptions(
57
+ model.model ? getPersistedModelOptions(model.model)?.sync : getPersistedSyncByName(model.name),
58
+ model.name,
59
+ );
60
+ // Described as declared first, then prepared: the descriptor must not see the Mongoose sync paths
61
+ registerModelSchema(model.name, model.schema, sync);
62
+ if (sync && !isDeclaredSchema(model.schema)) {
63
+ applySyncSchema(model.schema, sync);
64
+ }
50
65
  registerPersistedModel({
51
66
  declaredSource:
52
67
  model.dataSource ??
@@ -88,6 +103,10 @@ export class CorePersistenceModule {
88
103
  ? // No SQL source and no MongoDB connection: the project runs without a database (`database: false`)
89
104
  unavailableStore(model.name, 'the project runs without a database (`database: false`)')
90
105
  : new MongooseDataStore(mongooseModel);
106
+ // A synchronized model on MongoDB: the collection must hold nothing from before it was synchronized
107
+ if (store.capabilities.mongoosePopulate && store.capabilities.changeOrder) {
108
+ await assertSyncCollectionReady(mongooseModel);
109
+ }
91
110
  registerDataStore(model.name, store);
92
111
  bindStoreToModel(mongooseModel, store);
93
112
  // A stand-in already refuses, naming the cause; a real Mongoose model of a model routed elsewhere is made to
@@ -13,6 +13,7 @@ import {
13
13
  } from './data-source.config.js';
14
14
  import type { ModelDescriptor } from './model-descriptor.interface.js';
15
15
  import { getModelDescriptor } from './model-metadata.registry.js';
16
+ import { withSequenceField } from './sync-model.js';
16
17
  import { MongooseDataStore } from './mongoose-data-store.js';
17
18
  import {
18
19
  getPersistedModels,
@@ -190,10 +191,12 @@ export class DataSourceRuntime implements PersistenceRuntime {
190
191
  }
191
192
 
192
193
  private async openStore(source: ResolvedDataSource, name: string): Promise<DataStore<any>> {
193
- const model = getModelDescriptor(name);
194
- if (!model) {
194
+ const declared = getModelDescriptor(name);
195
+ if (!declared) {
195
196
  throw new Error(`No descriptor for model "${name}" — register it with CorePersistenceModule.forFeature()`);
196
197
  }
198
+ // A synchronized model's table also carries its commit-ordered change position (offline-sync plan §3.4)
199
+ const model = withSequenceField(declared);
197
200
  const app = await this.executor(source);
198
201
  const dialect = this.dialect(source);
199
202
  let changes: SchemaChange[];