@lenne.tech/nest-server 12.0.0 → 12.1.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 (693) hide show
  1. package/.claude/rules/architecture.md +6 -1
  2. package/.claude/rules/better-auth.md +46 -0
  3. package/.claude/rules/configurable-features.md +3 -2
  4. package/.claude/rules/package-management.md +24 -2
  5. package/.claude/rules/persistence.md +100 -0
  6. package/.claude/rules/role-system.md +9 -0
  7. package/.claude/rules/testing.md +116 -18
  8. package/CLAUDE.md +21 -2
  9. package/FRAMEWORK-API.md +6 -4
  10. package/README.md +106 -34
  11. package/dist/core/common/decorators/unified-field.decorator.d.ts +2 -1
  12. package/dist/core/common/decorators/unified-field.decorator.js +4 -1
  13. package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
  14. package/dist/core/common/filters/mongoose-cast-error.filter.d.ts +7 -5
  15. package/dist/core/common/filters/mongoose-cast-error.filter.js +3 -2
  16. package/dist/core/common/filters/mongoose-cast-error.filter.js.map +1 -1
  17. package/dist/core/common/helpers/db.helper.d.ts +2 -1
  18. package/dist/core/common/helpers/db.helper.js +32 -26
  19. package/dist/core/common/helpers/db.helper.js.map +1 -1
  20. package/dist/core/common/helpers/decorator.helper.d.ts +1 -1
  21. package/dist/core/common/helpers/filter.helper.d.ts +2 -1
  22. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  23. package/dist/core/common/helpers/gridfs.helper.d.ts +4 -4
  24. package/dist/core/common/helpers/gridfs.helper.js +14 -6
  25. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  26. package/dist/core/common/helpers/id.helper.d.ts +4 -1
  27. package/dist/core/common/helpers/id.helper.js +10 -4
  28. package/dist/core/common/helpers/id.helper.js.map +1 -1
  29. package/dist/core/common/helpers/model.helper.js +5 -5
  30. package/dist/core/common/helpers/model.helper.js.map +1 -1
  31. package/dist/core/common/helpers/service.helper.js +3 -3
  32. package/dist/core/common/helpers/service.helper.js.map +1 -1
  33. package/dist/core/common/interfaces/server-options.interface.d.ts +18 -2
  34. package/dist/core/common/interfaces/service-options.interface.d.ts +2 -2
  35. package/dist/core/common/models/core-persistence.model.d.ts +1 -2
  36. package/dist/core/common/models/core-persistence.model.js +3 -3
  37. package/dist/core/common/models/core-persistence.model.js.map +1 -1
  38. package/dist/core/common/persistence/core-persistence.lifecycle.d.ts +9 -0
  39. package/dist/core/common/persistence/core-persistence.lifecycle.js +38 -0
  40. package/dist/core/common/persistence/core-persistence.lifecycle.js.map +1 -0
  41. package/dist/core/common/persistence/core-persistence.module.d.ts +9 -0
  42. package/dist/core/common/persistence/core-persistence.module.js +76 -0
  43. package/dist/core/common/persistence/core-persistence.module.js.map +1 -0
  44. package/dist/core/common/persistence/data-source.config.d.ts +33 -0
  45. package/dist/core/common/persistence/data-source.config.js +151 -0
  46. package/dist/core/common/persistence/data-source.config.js.map +1 -0
  47. package/dist/core/common/persistence/data-source.runtime.d.ts +45 -0
  48. package/dist/core/common/persistence/data-source.runtime.js +278 -0
  49. package/dist/core/common/persistence/data-source.runtime.js.map +1 -0
  50. package/dist/core/common/persistence/data-store.interface.d.ts +73 -0
  51. package/dist/core/common/persistence/data-store.interface.js +2 -0
  52. package/dist/core/common/persistence/data-store.interface.js.map +1 -0
  53. package/dist/core/common/persistence/expiry-sweep.d.ts +12 -0
  54. package/dist/core/common/persistence/expiry-sweep.js +55 -0
  55. package/dist/core/common/persistence/expiry-sweep.js.map +1 -0
  56. package/dist/core/common/persistence/id-format.d.ts +3 -0
  57. package/dist/core/common/persistence/id-format.js +21 -0
  58. package/dist/core/common/persistence/id-format.js.map +1 -0
  59. package/dist/core/common/persistence/infrastructure-locks.d.ts +13 -0
  60. package/dist/core/common/persistence/infrastructure-locks.js +35 -0
  61. package/dist/core/common/persistence/infrastructure-locks.js.map +1 -0
  62. package/dist/core/common/persistence/inject-store.decorator.d.ts +1 -0
  63. package/dist/core/common/persistence/inject-store.decorator.js +6 -0
  64. package/dist/core/common/persistence/inject-store.decorator.js.map +1 -0
  65. package/dist/core/common/persistence/lock-store.interface.d.ts +5 -0
  66. package/dist/core/common/persistence/lock-store.interface.js +2 -0
  67. package/dist/core/common/persistence/lock-store.interface.js.map +1 -0
  68. package/dist/core/common/persistence/model-descriptor.interface.d.ts +36 -0
  69. package/dist/core/common/persistence/model-descriptor.interface.js +2 -0
  70. package/dist/core/common/persistence/model-descriptor.interface.js.map +1 -0
  71. package/dist/core/common/persistence/model-metadata.registry.d.ts +14 -0
  72. package/dist/core/common/persistence/model-metadata.registry.js +194 -0
  73. package/dist/core/common/persistence/model-metadata.registry.js.map +1 -0
  74. package/dist/core/common/persistence/mongo/collection-name.d.ts +2 -0
  75. package/dist/core/common/persistence/mongo/collection-name.js +73 -0
  76. package/dist/core/common/persistence/mongo/collection-name.js.map +1 -0
  77. package/dist/core/common/persistence/mongo/declared-schema.view.d.ts +11 -0
  78. package/dist/core/common/persistence/mongo/declared-schema.view.js +292 -0
  79. package/dist/core/common/persistence/mongo/declared-schema.view.js.map +1 -0
  80. package/dist/core/common/persistence/mongo/hex-object-id.d.ts +13 -0
  81. package/dist/core/common/persistence/mongo/hex-object-id.js +54 -0
  82. package/dist/core/common/persistence/mongo/hex-object-id.js.map +1 -0
  83. package/dist/core/common/persistence/mongo/model-declarations.d.ts +35 -0
  84. package/dist/core/common/persistence/mongo/model-declarations.js +80 -0
  85. package/dist/core/common/persistence/mongo/model-declarations.js.map +1 -0
  86. package/dist/core/common/persistence/mongo/mongo-packages.d.ts +15 -0
  87. package/dist/core/common/persistence/mongo/mongo-packages.js +66 -0
  88. package/dist/core/common/persistence/mongo/mongo-packages.js.map +1 -0
  89. package/dist/core/common/persistence/mongo/mongoose-compat.d.ts +22 -0
  90. package/dist/core/common/persistence/mongo/mongoose-compat.js +65 -0
  91. package/dist/core/common/persistence/mongo/mongoose-compat.js.map +1 -0
  92. package/dist/core/common/persistence/mongo/no-mongo-connection.module.d.ts +2 -0
  93. package/dist/core/common/persistence/mongo/no-mongo-connection.module.js +19 -0
  94. package/dist/core/common/persistence/mongo/no-mongo-connection.module.js.map +1 -0
  95. package/dist/core/common/persistence/mongo-lock-store.d.ts +21 -0
  96. package/dist/core/common/persistence/mongo-lock-store.js +72 -0
  97. package/dist/core/common/persistence/mongo-lock-store.js.map +1 -0
  98. package/dist/core/common/persistence/mongoose-data-store.d.ts +33 -0
  99. package/dist/core/common/persistence/mongoose-data-store.js +187 -0
  100. package/dist/core/common/persistence/mongoose-data-store.js.map +1 -0
  101. package/dist/core/common/persistence/neutral-populate.d.ts +6 -0
  102. package/dist/core/common/persistence/neutral-populate.js +72 -0
  103. package/dist/core/common/persistence/neutral-populate.js.map +1 -0
  104. package/dist/core/common/persistence/persisted-model.decorator.d.ts +6 -0
  105. package/dist/core/common/persistence/persisted-model.decorator.js +24 -0
  106. package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -0
  107. package/dist/core/common/persistence/persistence-runtime.registry.d.ts +61 -0
  108. package/dist/core/common/persistence/persistence-runtime.registry.js +47 -0
  109. package/dist/core/common/persistence/persistence-runtime.registry.js.map +1 -0
  110. package/dist/core/common/persistence/persistence.tokens.d.ts +1 -0
  111. package/dist/core/common/persistence/persistence.tokens.js +4 -0
  112. package/dist/core/common/persistence/persistence.tokens.js.map +1 -0
  113. package/dist/core/common/persistence/policies/audit.policy.d.ts +8 -0
  114. package/dist/core/common/persistence/policies/audit.policy.js +37 -0
  115. package/dist/core/common/persistence/policies/audit.policy.js.map +1 -0
  116. package/dist/core/common/persistence/policies/password.policy.d.ts +3 -0
  117. package/dist/core/common/persistence/policies/password.policy.js +46 -0
  118. package/dist/core/common/persistence/policies/password.policy.js.map +1 -0
  119. package/dist/core/common/persistence/policies/role-guard.policy.d.ts +3 -0
  120. package/dist/core/common/persistence/policies/role-guard.policy.js +46 -0
  121. package/dist/core/common/persistence/policies/role-guard.policy.js.map +1 -0
  122. package/dist/core/common/persistence/policies/system-role.policy.d.ts +5 -0
  123. package/dist/core/common/persistence/policies/system-role.policy.js +46 -0
  124. package/dist/core/common/persistence/policies/system-role.policy.js.map +1 -0
  125. package/dist/core/common/persistence/policies/tenant.policy.d.ts +19 -0
  126. package/dist/core/common/persistence/policies/tenant.policy.js +120 -0
  127. package/dist/core/common/persistence/policies/tenant.policy.js.map +1 -0
  128. package/dist/core/common/persistence/routed-model.guard.d.ts +4 -0
  129. package/dist/core/common/persistence/routed-model.guard.js +73 -0
  130. package/dist/core/common/persistence/routed-model.guard.js.map +1 -0
  131. package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +46 -0
  132. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +350 -0
  133. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -0
  134. package/dist/core/common/persistence/sql/mysql/mysql-executor.d.ts +13 -0
  135. package/dist/core/common/persistence/sql/mysql/mysql-executor.js +194 -0
  136. package/dist/core/common/persistence/sql/mysql/mysql-executor.js.map +1 -0
  137. package/dist/core/common/persistence/sql/mysql/mysql-lock-store.d.ts +11 -0
  138. package/dist/core/common/persistence/sql/mysql/mysql-lock-store.js +55 -0
  139. package/dist/core/common/persistence/sql/mysql/mysql-lock-store.js.map +1 -0
  140. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +36 -0
  141. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +288 -0
  142. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -0
  143. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.d.ts +19 -0
  144. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +328 -0
  145. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -0
  146. package/dist/core/common/persistence/sql/mysql/mysql-session.d.ts +13 -0
  147. package/dist/core/common/persistence/sql/mysql/mysql-session.js +85 -0
  148. package/dist/core/common/persistence/sql/mysql/mysql-session.js.map +1 -0
  149. package/dist/core/common/persistence/sql/mysql/mysql-update.compiler.d.ts +9 -0
  150. package/dist/core/common/persistence/sql/mysql/mysql-update.compiler.js +264 -0
  151. package/dist/core/common/persistence/sql/mysql/mysql-update.compiler.js.map +1 -0
  152. package/dist/core/common/persistence/sql/mysql/mysql-where.compiler.d.ts +20 -0
  153. package/dist/core/common/persistence/sql/mysql/mysql-where.compiler.js +519 -0
  154. package/dist/core/common/persistence/sql/mysql/mysql-where.compiler.js.map +1 -0
  155. package/dist/core/common/persistence/sql/postgres/postgres-lock-store.d.ts +11 -0
  156. package/dist/core/common/persistence/sql/postgres/postgres-lock-store.js +44 -0
  157. package/dist/core/common/persistence/sql/postgres/postgres-lock-store.js.map +1 -0
  158. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +42 -0
  159. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +220 -0
  160. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -0
  161. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.d.ts +12 -0
  162. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +315 -0
  163. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -0
  164. package/dist/core/common/persistence/sql/postgres/postgres-update.compiler.d.ts +5 -0
  165. package/dist/core/common/persistence/sql/postgres/postgres-update.compiler.js +225 -0
  166. package/dist/core/common/persistence/sql/postgres/postgres-update.compiler.js.map +1 -0
  167. package/dist/core/common/persistence/sql/postgres/postgres-where.compiler.d.ts +13 -0
  168. package/dist/core/common/persistence/sql/postgres/postgres-where.compiler.js +484 -0
  169. package/dist/core/common/persistence/sql/postgres/postgres-where.compiler.js.map +1 -0
  170. package/dist/core/common/persistence/sql/sql-cast.d.ts +2 -0
  171. package/dist/core/common/persistence/sql/sql-cast.js +194 -0
  172. package/dist/core/common/persistence/sql/sql-cast.js.map +1 -0
  173. package/dist/core/common/persistence/sql/sql-data-store.d.ts +79 -0
  174. package/dist/core/common/persistence/sql/sql-data-store.js +592 -0
  175. package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -0
  176. package/dist/core/common/persistence/sql/sql-dialect.d.ts +20 -0
  177. package/dist/core/common/persistence/sql/sql-dialect.js +38 -0
  178. package/dist/core/common/persistence/sql/sql-dialect.js.map +1 -0
  179. package/dist/core/common/persistence/sql/sql-errors.d.ts +17 -0
  180. package/dist/core/common/persistence/sql/sql-errors.js +25 -0
  181. package/dist/core/common/persistence/sql/sql-errors.js.map +1 -0
  182. package/dist/core/common/persistence/sql/sql-executor.factory.d.ts +11 -0
  183. package/dist/core/common/persistence/sql/sql-executor.factory.js +199 -0
  184. package/dist/core/common/persistence/sql/sql-executor.factory.js.map +1 -0
  185. package/dist/core/common/persistence/sql/sql-executor.interface.d.ts +32 -0
  186. package/dist/core/common/persistence/sql/sql-executor.interface.js +2 -0
  187. package/dist/core/common/persistence/sql/sql-executor.interface.js.map +1 -0
  188. package/dist/core/common/persistence/sql/sql-naming.d.ts +15 -0
  189. package/dist/core/common/persistence/sql/sql-naming.js +90 -0
  190. package/dist/core/common/persistence/sql/sql-naming.js.map +1 -0
  191. package/dist/core/common/persistence/sql/sql-schema.shared.d.ts +25 -0
  192. package/dist/core/common/persistence/sql/sql-schema.shared.js +69 -0
  193. package/dist/core/common/persistence/sql/sql-schema.shared.js.map +1 -0
  194. package/dist/core/common/persistence/sql/sql-session.d.ts +6 -0
  195. package/dist/core/common/persistence/sql/sql-session.js +78 -0
  196. package/dist/core/common/persistence/sql/sql-session.js.map +1 -0
  197. package/dist/core/common/persistence/sql/where-shared.d.ts +18 -0
  198. package/dist/core/common/persistence/sql/where-shared.js +93 -0
  199. package/dist/core/common/persistence/sql/where-shared.js.map +1 -0
  200. package/dist/core/common/persistence/store-guards.d.ts +10 -0
  201. package/dist/core/common/persistence/store-guards.js +49 -0
  202. package/dist/core/common/persistence/store-guards.js.map +1 -0
  203. package/dist/core/common/persistence/store-upsert.d.ts +2 -0
  204. package/dist/core/common/persistence/store-upsert.js +20 -0
  205. package/dist/core/common/persistence/store-upsert.js.map +1 -0
  206. package/dist/core/common/persistence/unavailable-store.d.ts +2 -0
  207. package/dist/core/common/persistence/unavailable-store.js +53 -0
  208. package/dist/core/common/persistence/unavailable-store.js.map +1 -0
  209. package/dist/core/common/persistence/unsupported-capability.error.d.ts +3 -0
  210. package/dist/core/common/persistence/unsupported-capability.error.js +4 -0
  211. package/dist/core/common/persistence/unsupported-capability.error.js.map +1 -0
  212. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js +24 -68
  213. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js.map +1 -1
  214. package/dist/core/common/plugins/mongoose-id.plugin.js +3 -0
  215. package/dist/core/common/plugins/mongoose-id.plugin.js.map +1 -1
  216. package/dist/core/common/plugins/mongoose-password.plugin.d.ts +1 -3
  217. package/dist/core/common/plugins/mongoose-password.plugin.js +2 -45
  218. package/dist/core/common/plugins/mongoose-password.plugin.js.map +1 -1
  219. package/dist/core/common/plugins/mongoose-role-guard.plugin.js +5 -46
  220. package/dist/core/common/plugins/mongoose-role-guard.plugin.js.map +1 -1
  221. package/dist/core/common/plugins/mongoose-system-role.plugin.js +2 -43
  222. package/dist/core/common/plugins/mongoose-system-role.plugin.js.map +1 -1
  223. package/dist/core/common/plugins/mongoose-tenant.plugin.js +10 -70
  224. package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
  225. package/dist/core/common/scalars/upload.scalar.js +1 -1
  226. package/dist/core/common/scalars/upload.scalar.js.map +1 -1
  227. package/dist/core/common/services/core-cron-jobs.initializer.js +1 -1
  228. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -1
  229. package/dist/core/common/services/core-cron-jobs.service.d.ts +1 -0
  230. package/dist/core/common/services/core-cron-jobs.service.js +23 -14
  231. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  232. package/dist/core/common/services/crud.service.d.ts +4 -1
  233. package/dist/core/common/services/crud.service.js +132 -23
  234. package/dist/core/common/services/crud.service.js.map +1 -1
  235. package/dist/core/common/services/model-doc.service.js +1 -1
  236. package/dist/core/common/services/model-doc.service.js.map +1 -1
  237. package/dist/core/common/services/module.service.d.ts +9 -2
  238. package/dist/core/common/services/module.service.js +51 -6
  239. package/dist/core/common/services/module.service.js.map +1 -1
  240. package/dist/core/common/types/field-selection.type.d.ts +1 -1
  241. package/dist/core/common/types/id.type.d.ts +4 -4
  242. package/dist/core/common/types/object-id-like.type.d.ts +3 -0
  243. package/dist/core/common/types/object-id-like.type.js +2 -0
  244. package/dist/core/common/types/object-id-like.type.js.map +1 -0
  245. package/dist/core/common/types/populate-config.type.d.ts +1 -1
  246. package/dist/core/common/types/string-or-object-id.type.d.ts +2 -2
  247. package/dist/core/modules/ai/core-ai.constants.d.ts +4 -0
  248. package/dist/core/modules/ai/core-ai.constants.js +4 -0
  249. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  250. package/dist/core/modules/ai/core-ai.module.js +21 -17
  251. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  252. package/dist/core/modules/ai/models/core-ai-budget-limit.model.d.ts +2 -92
  253. package/dist/core/modules/ai/models/core-ai-budget-limit.model.js +1 -1
  254. package/dist/core/modules/ai/models/core-ai-budget-limit.model.js.map +1 -1
  255. package/dist/core/modules/ai/models/core-ai-connection-preference.model.d.ts +2 -87
  256. package/dist/core/modules/ai/models/core-ai-connection-preference.model.js +1 -1
  257. package/dist/core/modules/ai/models/core-ai-connection-preference.model.js.map +1 -1
  258. package/dist/core/modules/ai/models/core-ai-connection.model.d.ts +2 -177
  259. package/dist/core/modules/ai/models/core-ai-connection.model.js +1 -1
  260. package/dist/core/modules/ai/models/core-ai-connection.model.js.map +1 -1
  261. package/dist/core/modules/ai/models/core-ai-conversation.model.d.ts +2 -92
  262. package/dist/core/modules/ai/models/core-ai-conversation.model.js +3 -4
  263. package/dist/core/modules/ai/models/core-ai-conversation.model.js.map +1 -1
  264. package/dist/core/modules/ai/models/core-ai-interaction.model.d.ts +2 -120
  265. package/dist/core/modules/ai/models/core-ai-interaction.model.js +2 -3
  266. package/dist/core/modules/ai/models/core-ai-interaction.model.js.map +1 -1
  267. package/dist/core/modules/ai/models/core-ai-mcp-oauth.models.d.ts +3 -0
  268. package/dist/core/modules/ai/models/core-ai-mcp-oauth.models.js +21 -0
  269. package/dist/core/modules/ai/models/core-ai-mcp-oauth.models.js.map +1 -0
  270. package/dist/core/modules/ai/models/core-ai-mode.model.d.ts +2 -102
  271. package/dist/core/modules/ai/models/core-ai-mode.model.js +1 -1
  272. package/dist/core/modules/ai/models/core-ai-mode.model.js.map +1 -1
  273. package/dist/core/modules/ai/models/core-ai-prompt-hint.model.d.ts +2 -97
  274. package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js +1 -1
  275. package/dist/core/modules/ai/models/core-ai-prompt-hint.model.js.map +1 -1
  276. package/dist/core/modules/ai/models/core-ai-prompt.model.d.ts +2 -112
  277. package/dist/core/modules/ai/models/core-ai-prompt.model.js +1 -1
  278. package/dist/core/modules/ai/models/core-ai-prompt.model.js.map +1 -1
  279. package/dist/core/modules/ai/models/core-ai-slot.model.d.ts +2 -112
  280. package/dist/core/modules/ai/models/core-ai-slot.model.js +1 -1
  281. package/dist/core/modules/ai/models/core-ai-slot.model.js.map +1 -1
  282. package/dist/core/modules/ai/models/core-ai-tool-grant.model.d.ts +2 -97
  283. package/dist/core/modules/ai/models/core-ai-tool-grant.model.js +1 -1
  284. package/dist/core/modules/ai/models/core-ai-tool-grant.model.js.map +1 -1
  285. package/dist/core/modules/ai/models/core-ai-tool-policy.model.d.ts +2 -103
  286. package/dist/core/modules/ai/models/core-ai-tool-policy.model.js +1 -1
  287. package/dist/core/modules/ai/models/core-ai-tool-policy.model.js.map +1 -1
  288. package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +3 -1
  289. package/dist/core/modules/ai/services/core-ai-budget.service.js +15 -5
  290. package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
  291. package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -1
  292. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +6 -10
  293. package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
  294. package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -1
  295. package/dist/core/modules/ai/services/core-ai-connection.service.js +20 -42
  296. package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
  297. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -1
  298. package/dist/core/modules/ai/services/core-ai-conversation.service.js +28 -29
  299. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  300. package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +1 -1
  301. package/dist/core/modules/ai/services/core-ai-interaction.service.js +3 -4
  302. package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
  303. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.d.ts +8 -0
  304. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +70 -18
  305. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  306. package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -1
  307. package/dist/core/modules/ai/services/core-ai-mode.service.js +3 -7
  308. package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
  309. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +8 -1
  310. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +32 -26
  311. package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
  312. package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -1
  313. package/dist/core/modules/ai/services/core-ai-prompt.service.js +10 -13
  314. package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
  315. package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -1
  316. package/dist/core/modules/ai/services/core-ai-slot.service.js +14 -25
  317. package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
  318. package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -1
  319. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +6 -18
  320. package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
  321. package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -1
  322. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +4 -8
  323. package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
  324. package/dist/core/modules/api-token/core-api-token.model.js +1 -1
  325. package/dist/core/modules/api-token/core-api-token.model.js.map +1 -1
  326. package/dist/core/modules/api-token/core-api-token.module.js +3 -2
  327. package/dist/core/modules/api-token/core-api-token.module.js.map +1 -1
  328. package/dist/core/modules/api-token/core-api-token.service.d.ts +8 -2
  329. package/dist/core/modules/api-token/core-api-token.service.js +106 -39
  330. package/dist/core/modules/api-token/core-api-token.service.js.map +1 -1
  331. package/dist/core/modules/auth/core-auth.module.js +2 -0
  332. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  333. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +1 -2
  334. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  335. package/dist/core/modules/auth/guards/role-guard.token.d.ts +1 -0
  336. package/dist/core/modules/auth/guards/role-guard.token.js +2 -0
  337. package/dist/core/modules/auth/guards/role-guard.token.js.map +1 -0
  338. package/dist/core/modules/auth/services/core-auth.service.js +4 -2
  339. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  340. package/dist/core/modules/better-auth/better-auth-sql.helper.d.ts +6 -0
  341. package/dist/core/modules/better-auth/better-auth-sql.helper.js +113 -0
  342. package/dist/core/modules/better-auth/better-auth-sql.helper.js.map +1 -0
  343. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +3 -1
  344. package/dist/core/modules/better-auth/better-auth-token.service.js +15 -24
  345. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -1
  346. package/dist/core/modules/better-auth/better-auth.config.d.ts +7 -0
  347. package/dist/core/modules/better-auth/better-auth.config.js +16 -5
  348. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  349. package/dist/core/modules/better-auth/better-auth.types.d.ts +1 -1
  350. package/dist/core/modules/better-auth/core-better-auth-api.middleware.d.ts +1 -0
  351. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js +6 -1
  352. package/dist/core/modules/better-auth/core-better-auth-api.middleware.js.map +1 -1
  353. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js +30 -15
  354. package/dist/core/modules/better-auth/core-better-auth-challenge.service.js.map +1 -1
  355. package/dist/core/modules/better-auth/core-better-auth-mongo.users.d.ts +10 -0
  356. package/dist/core/modules/better-auth/core-better-auth-mongo.users.js +24 -0
  357. package/dist/core/modules/better-auth/core-better-auth-mongo.users.js.map +1 -0
  358. package/dist/core/modules/better-auth/core-better-auth-sql.registry.d.ts +3 -0
  359. package/dist/core/modules/better-auth/core-better-auth-sql.registry.js +8 -0
  360. package/dist/core/modules/better-auth/core-better-auth-sql.registry.js.map +1 -0
  361. package/dist/core/modules/better-auth/core-better-auth-sql.store.d.ts +71 -0
  362. package/dist/core/modules/better-auth/core-better-auth-sql.store.js +190 -0
  363. package/dist/core/modules/better-auth/core-better-auth-sql.store.js.map +1 -0
  364. package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +24 -1
  365. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +308 -32
  366. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  367. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -0
  368. package/dist/core/modules/better-auth/core-better-auth.module.js +54 -32
  369. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  370. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +7 -2
  371. package/dist/core/modules/better-auth/core-better-auth.service.js +61 -9
  372. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  373. package/dist/core/modules/file/core-file-info.model.d.ts +1 -2
  374. package/dist/core/modules/file/core-file-info.model.js +2 -2
  375. package/dist/core/modules/file/core-file-info.model.js.map +1 -1
  376. package/dist/core/modules/file/core-file.service.d.ts +5 -3
  377. package/dist/core/modules/file/core-file.service.js +16 -9
  378. package/dist/core/modules/file/core-file.service.js.map +1 -1
  379. package/dist/core/modules/file/file-metadata.helper.d.ts +1 -1
  380. package/dist/core/modules/file/file-metadata.helper.js +2 -2
  381. package/dist/core/modules/file/file-metadata.helper.js.map +1 -1
  382. package/dist/core/modules/file/file-metadata.models.d.ts +35 -0
  383. package/dist/core/modules/file/file-metadata.models.js +20 -0
  384. package/dist/core/modules/file/file-metadata.models.js.map +1 -0
  385. package/dist/core/modules/file/file-metadata.store.d.ts +6 -0
  386. package/dist/core/modules/file/file-metadata.store.js +77 -0
  387. package/dist/core/modules/file/file-metadata.store.js.map +1 -0
  388. package/dist/core/modules/file/file-storage.helper.d.ts +1 -0
  389. package/dist/core/modules/file/file-storage.helper.js +34 -0
  390. package/dist/core/modules/file/file-storage.helper.js.map +1 -1
  391. package/dist/core/modules/file/filesystem-file.helper.d.ts +1 -1
  392. package/dist/core/modules/file/filesystem-file.helper.js +4 -4
  393. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -1
  394. package/dist/core/modules/file/s3-file.helper.d.ts +1 -1
  395. package/dist/core/modules/file/s3-file.helper.js +5 -5
  396. package/dist/core/modules/file/s3-file.helper.js.map +1 -1
  397. package/dist/core/modules/health-check/core-health-check.module.js +12 -2
  398. package/dist/core/modules/health-check/core-health-check.module.js.map +1 -1
  399. package/dist/core/modules/health-check/core-health-check.service.d.ts +4 -3
  400. package/dist/core/modules/health-check/core-health-check.service.js +26 -5
  401. package/dist/core/modules/health-check/core-health-check.service.js.map +1 -1
  402. package/dist/core/modules/hub/core-hub.controller.d.ts +1 -0
  403. package/dist/core/modules/hub/core-hub.controller.js +10 -0
  404. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  405. package/dist/core/modules/hub/core-hub.service.js +1 -1
  406. package/dist/core/modules/hub/core-hub.service.js.map +1 -1
  407. package/dist/core/modules/hub/helpers/hub-client-js.helper.js +23 -0
  408. package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -1
  409. package/dist/core/modules/hub/hub-action-messages.d.ts +2 -0
  410. package/dist/core/modules/hub/hub-action-messages.js +2 -0
  411. package/dist/core/modules/hub/hub-action-messages.js.map +1 -1
  412. package/dist/core/modules/hub/hub-nav.js +1 -0
  413. package/dist/core/modules/hub/hub-nav.js.map +1 -1
  414. package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +17 -1
  415. package/dist/core/modules/hub/services/core-hub-db.service.d.ts +6 -1
  416. package/dist/core/modules/hub/services/core-hub-db.service.js +16 -14
  417. package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -1
  418. package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +5 -0
  419. package/dist/core/modules/hub/services/core-hub-migrations.service.js +74 -1
  420. package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -1
  421. package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +2 -1
  422. package/dist/core/modules/hub/services/core-hub-sources.service.js +32 -0
  423. package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -1
  424. package/dist/core/modules/hub/services/hub-query-profiler.service.js +1 -1
  425. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  426. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +2 -0
  427. package/dist/core/modules/migrate/cli/migrate-cli.js +35 -1
  428. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  429. package/dist/core/modules/migrate/index.d.ts +3 -0
  430. package/dist/core/modules/migrate/index.js +3 -0
  431. package/dist/core/modules/migrate/index.js.map +1 -1
  432. package/dist/core/modules/migrate/migration-runner.d.ts +2 -2
  433. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  434. package/dist/core/modules/migrate/migration-state-store.interface.d.ts +16 -0
  435. package/dist/core/modules/migrate/migration-state-store.interface.js +2 -0
  436. package/dist/core/modules/migrate/migration-state-store.interface.js.map +1 -0
  437. package/dist/core/modules/migrate/migration.helper.d.ts +3 -2
  438. package/dist/core/modules/migrate/migration.helper.js +21 -6
  439. package/dist/core/modules/migrate/migration.helper.js.map +1 -1
  440. package/dist/core/modules/migrate/mongo-state-store.d.ts +5 -12
  441. package/dist/core/modules/migrate/mongo-state-store.js +30 -24
  442. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  443. package/dist/core/modules/migrate/sql-migration.helper.d.ts +4 -0
  444. package/dist/core/modules/migrate/sql-migration.helper.js +34 -0
  445. package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -0
  446. package/dist/core/modules/migrate/sql-state-store.d.ts +24 -0
  447. package/dist/core/modules/migrate/sql-state-store.js +129 -0
  448. package/dist/core/modules/migrate/sql-state-store.js.map +1 -0
  449. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +8 -0
  450. package/dist/core/modules/system-setup/core-system-setup.service.js +41 -25
  451. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  452. package/dist/core/modules/tenant/core-tenant-member.model.js +1 -1
  453. package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -1
  454. package/dist/core/modules/tenant/core-tenant-member.store.d.ts +2 -0
  455. package/dist/core/modules/tenant/core-tenant-member.store.js +6 -0
  456. package/dist/core/modules/tenant/core-tenant-member.store.js.map +1 -0
  457. package/dist/core/modules/tenant/core-tenant.guard.d.ts +8 -3
  458. package/dist/core/modules/tenant/core-tenant.guard.js +57 -28
  459. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  460. package/dist/core/modules/tenant/core-tenant.module.js +3 -2
  461. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  462. package/dist/core/modules/tenant/core-tenant.service.d.ts +3 -1
  463. package/dist/core/modules/tenant/core-tenant.service.js +48 -28
  464. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  465. package/dist/core/modules/tus/core-tus.service.d.ts +1 -1
  466. package/dist/core/modules/tus/core-tus.service.js +8 -7
  467. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  468. package/dist/core/modules/tus/tus.module.js +1 -1
  469. package/dist/core/modules/tus/tus.module.js.map +1 -1
  470. package/dist/core/modules/user/core-user.model.d.ts +1 -1
  471. package/dist/core/modules/user/core-user.model.js +1 -1
  472. package/dist/core/modules/user/core-user.model.js.map +1 -1
  473. package/dist/core/modules/user/core-user.service.d.ts +3 -1
  474. package/dist/core/modules/user/core-user.service.js +71 -16
  475. package/dist/core/modules/user/core-user.service.js.map +1 -1
  476. package/dist/core.module.js +39 -5
  477. package/dist/core.module.js.map +1 -1
  478. package/dist/index.d.ts +26 -0
  479. package/dist/index.js +26 -0
  480. package/dist/index.js.map +1 -1
  481. package/dist/server/common/models/persistence.model.d.ts +1 -1
  482. package/dist/server/common/models/persistence.model.js +3 -3
  483. package/dist/server/common/models/persistence.model.js.map +1 -1
  484. package/dist/server/modules/file/file-info.model.d.ts +1 -77
  485. package/dist/server/modules/file/file-info.model.js +1 -1
  486. package/dist/server/modules/file/file-info.model.js.map +1 -1
  487. package/dist/server/modules/file/file.service.js +1 -1
  488. package/dist/server/modules/file/file.service.js.map +1 -1
  489. package/dist/server/modules/user/user.model.d.ts +2 -186
  490. package/dist/server/modules/user/user.model.js +4 -5
  491. package/dist/server/modules/user/user.model.js.map +1 -1
  492. package/dist/server/modules/user/user.module.js +4 -3
  493. package/dist/server/modules/user/user.module.js.map +1 -1
  494. package/dist/server/modules/user/user.service.d.ts +1 -1
  495. package/dist/server/modules/user/user.service.js +5 -8
  496. package/dist/server/modules/user/user.service.js.map +1 -1
  497. package/dist/test/test.helper.js +2 -2
  498. package/dist/test/test.helper.js.map +1 -1
  499. package/dist/types/graphql-upload.d.ts +10 -7
  500. package/docs/REQUEST-LIFECYCLE.md +6 -3
  501. package/docs/choosing-a-database.md +107 -0
  502. package/docs/native-driver-security.md +29 -0
  503. package/docs/security-overrides.md +15 -6
  504. package/migration-guides/12.0.x-to-12.1.x.md +255 -0
  505. package/package.json +75 -29
  506. package/src/core/common/decorators/unified-field.decorator.ts +11 -1
  507. package/src/core/common/filters/mongoose-cast-error.filter.ts +12 -6
  508. package/src/core/common/helpers/db.helper.ts +40 -25
  509. package/src/core/common/helpers/decorator.helper.ts +1 -1
  510. package/src/core/common/helpers/filter.helper.ts +2 -1
  511. package/src/core/common/helpers/gridfs.helper.ts +28 -9
  512. package/src/core/common/helpers/id.helper.ts +21 -4
  513. package/src/core/common/helpers/model.helper.ts +5 -6
  514. package/src/core/common/helpers/service.helper.ts +3 -3
  515. package/src/core/common/interfaces/server-options.interface.ts +118 -10
  516. package/src/core/common/interfaces/service-options.interface.ts +2 -2
  517. package/src/core/common/models/core-persistence.model.ts +3 -3
  518. package/src/core/common/persistence/core-persistence.lifecycle.ts +47 -0
  519. package/src/core/common/persistence/core-persistence.module.ts +109 -0
  520. package/src/core/common/persistence/data-source.config.ts +271 -0
  521. package/src/core/common/persistence/data-source.runtime.ts +351 -0
  522. package/src/core/common/persistence/data-store.interface.ts +155 -0
  523. package/src/core/common/persistence/expiry-sweep.ts +105 -0
  524. package/src/core/common/persistence/id-format.ts +43 -0
  525. package/src/core/common/persistence/infrastructure-locks.ts +66 -0
  526. package/src/core/common/persistence/inject-store.decorator.ts +16 -0
  527. package/src/core/common/persistence/lock-store.interface.ts +17 -0
  528. package/src/core/common/persistence/model-descriptor.interface.ts +90 -0
  529. package/src/core/common/persistence/model-metadata.registry.ts +285 -0
  530. package/src/core/common/persistence/mongo/collection-name.ts +87 -0
  531. package/src/core/common/persistence/mongo/declared-schema.view.ts +378 -0
  532. package/src/core/common/persistence/mongo/hex-object-id.ts +75 -0
  533. package/src/core/common/persistence/mongo/model-declarations.ts +135 -0
  534. package/src/core/common/persistence/mongo/mongo-packages.ts +130 -0
  535. package/src/core/common/persistence/mongo/mongoose-compat.ts +129 -0
  536. package/src/core/common/persistence/mongo/no-mongo-connection.module.ts +18 -0
  537. package/src/core/common/persistence/mongo-lock-store.ts +121 -0
  538. package/src/core/common/persistence/mongoose-data-store.ts +232 -0
  539. package/src/core/common/persistence/neutral-populate.ts +98 -0
  540. package/src/core/common/persistence/persisted-model.decorator.ts +61 -0
  541. package/src/core/common/persistence/persistence-runtime.registry.ts +152 -0
  542. package/src/core/common/persistence/persistence.tokens.ts +9 -0
  543. package/src/core/common/persistence/policies/audit.policy.ts +80 -0
  544. package/src/core/common/persistence/policies/password.policy.ts +69 -0
  545. package/src/core/common/persistence/policies/role-guard.policy.ts +74 -0
  546. package/src/core/common/persistence/policies/system-role.policy.ts +90 -0
  547. package/src/core/common/persistence/policies/tenant.policy.ts +245 -0
  548. package/src/core/common/persistence/routed-model.guard.ts +107 -0
  549. package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +503 -0
  550. package/src/core/common/persistence/sql/mysql/mysql-executor.ts +281 -0
  551. package/src/core/common/persistence/sql/mysql/mysql-lock-store.ts +87 -0
  552. package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +424 -0
  553. package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +453 -0
  554. package/src/core/common/persistence/sql/mysql/mysql-session.ts +131 -0
  555. package/src/core/common/persistence/sql/mysql/mysql-update.compiler.ts +379 -0
  556. package/src/core/common/persistence/sql/mysql/mysql-where.compiler.ts +686 -0
  557. package/src/core/common/persistence/sql/postgres/postgres-lock-store.ts +76 -0
  558. package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +328 -0
  559. package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +448 -0
  560. package/src/core/common/persistence/sql/postgres/postgres-update.compiler.ts +310 -0
  561. package/src/core/common/persistence/sql/postgres/postgres-where.compiler.ts +638 -0
  562. package/src/core/common/persistence/sql/sql-cast.ts +227 -0
  563. package/src/core/common/persistence/sql/sql-data-store.ts +762 -0
  564. package/src/core/common/persistence/sql/sql-dialect.ts +85 -0
  565. package/src/core/common/persistence/sql/sql-errors.ts +49 -0
  566. package/src/core/common/persistence/sql/sql-executor.factory.ts +282 -0
  567. package/src/core/common/persistence/sql/sql-executor.interface.ts +57 -0
  568. package/src/core/common/persistence/sql/sql-naming.ts +133 -0
  569. package/src/core/common/persistence/sql/sql-schema.shared.ts +105 -0
  570. package/src/core/common/persistence/sql/sql-session.ts +158 -0
  571. package/src/core/common/persistence/sql/where-shared.ts +118 -0
  572. package/src/core/common/persistence/store-guards.ts +73 -0
  573. package/src/core/common/persistence/store-upsert.ts +36 -0
  574. package/src/core/common/persistence/unavailable-store.ts +63 -0
  575. package/src/core/common/persistence/unsupported-capability.error.ts +7 -0
  576. package/src/core/common/plugins/mongoose-audit-fields.plugin.ts +33 -74
  577. package/src/core/common/plugins/mongoose-id.plugin.ts +9 -0
  578. package/src/core/common/plugins/mongoose-password.plugin.ts +6 -62
  579. package/src/core/common/plugins/mongoose-role-guard.plugin.ts +11 -63
  580. package/src/core/common/plugins/mongoose-system-role.plugin.ts +7 -78
  581. package/src/core/common/plugins/mongoose-tenant.plugin.ts +23 -144
  582. package/src/core/common/scalars/upload.scalar.ts +8 -3
  583. package/src/core/common/services/core-cron-jobs.initializer.ts +1 -1
  584. package/src/core/common/services/core-cron-jobs.service.ts +33 -22
  585. package/src/core/common/services/crud.service.ts +166 -35
  586. package/src/core/common/services/model-doc.service.ts +1 -1
  587. package/src/core/common/services/module.service.ts +78 -6
  588. package/src/core/common/types/field-selection.type.ts +1 -1
  589. package/src/core/common/types/id.type.ts +2 -2
  590. package/src/core/common/types/object-id-like.type.ts +11 -0
  591. package/src/core/common/types/populate-config.type.ts +1 -1
  592. package/src/core/common/types/string-or-object-id.type.ts +2 -2
  593. package/src/core/modules/ai/README.md +15 -2
  594. package/src/core/modules/ai/core-ai.constants.ts +10 -0
  595. package/src/core/modules/ai/core-ai.module.ts +32 -17
  596. package/src/core/modules/ai/models/core-ai-budget-limit.model.ts +2 -2
  597. package/src/core/modules/ai/models/core-ai-connection-preference.model.ts +2 -2
  598. package/src/core/modules/ai/models/core-ai-connection.model.ts +2 -2
  599. package/src/core/modules/ai/models/core-ai-conversation.model.ts +8 -4
  600. package/src/core/modules/ai/models/core-ai-interaction.model.ts +3 -3
  601. package/src/core/modules/ai/models/core-ai-mcp-oauth.models.ts +46 -0
  602. package/src/core/modules/ai/models/core-ai-mode.model.ts +2 -2
  603. package/src/core/modules/ai/models/core-ai-prompt-hint.model.ts +2 -2
  604. package/src/core/modules/ai/models/core-ai-prompt.model.ts +2 -2
  605. package/src/core/modules/ai/models/core-ai-slot.model.ts +2 -2
  606. package/src/core/modules/ai/models/core-ai-tool-grant.model.ts +2 -2
  607. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +2 -2
  608. package/src/core/modules/ai/services/core-ai-budget.service.ts +22 -4
  609. package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +6 -13
  610. package/src/core/modules/ai/services/core-ai-connection.service.ts +34 -44
  611. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -29
  612. package/src/core/modules/ai/services/core-ai-interaction.service.ts +3 -3
  613. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +93 -20
  614. package/src/core/modules/ai/services/core-ai-mode.service.ts +3 -6
  615. package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +43 -26
  616. package/src/core/modules/ai/services/core-ai-prompt.service.ts +15 -12
  617. package/src/core/modules/ai/services/core-ai-slot.service.ts +20 -24
  618. package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +17 -27
  619. package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +7 -10
  620. package/src/core/modules/api-token/core-api-token.model.ts +1 -1
  621. package/src/core/modules/api-token/core-api-token.module.ts +4 -2
  622. package/src/core/modules/api-token/core-api-token.service.ts +118 -43
  623. package/src/core/modules/auth/core-auth.module.ts +2 -0
  624. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +4 -3
  625. package/src/core/modules/auth/guards/role-guard.token.ts +7 -0
  626. package/src/core/modules/auth/services/core-auth.service.ts +16 -2
  627. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +1 -1
  628. package/src/core/modules/better-auth/README.md +10 -1
  629. package/src/core/modules/better-auth/better-auth-sql.helper.ts +191 -0
  630. package/src/core/modules/better-auth/better-auth-token.service.ts +26 -35
  631. package/src/core/modules/better-auth/better-auth.config.ts +43 -7
  632. package/src/core/modules/better-auth/better-auth.types.ts +1 -1
  633. package/src/core/modules/better-auth/core-better-auth-api.middleware.ts +13 -1
  634. package/src/core/modules/better-auth/core-better-auth-challenge.service.ts +36 -18
  635. package/src/core/modules/better-auth/core-better-auth-mongo.users.ts +35 -0
  636. package/src/core/modules/better-auth/core-better-auth-sql.registry.ts +20 -0
  637. package/src/core/modules/better-auth/core-better-auth-sql.store.ts +309 -0
  638. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +418 -40
  639. package/src/core/modules/better-auth/core-better-auth.module.ts +71 -35
  640. package/src/core/modules/better-auth/core-better-auth.service.ts +81 -12
  641. package/src/core/modules/file/README.md +16 -3
  642. package/src/core/modules/file/core-file-info.model.ts +2 -2
  643. package/src/core/modules/file/core-file.service.ts +27 -18
  644. package/src/core/modules/file/file-metadata.helper.ts +7 -3
  645. package/src/core/modules/file/file-metadata.models.ts +44 -0
  646. package/src/core/modules/file/file-metadata.store.ts +109 -0
  647. package/src/core/modules/file/file-storage.helper.ts +47 -2
  648. package/src/core/modules/file/filesystem-file.helper.ts +11 -8
  649. package/src/core/modules/file/s3-file.helper.ts +11 -7
  650. package/src/core/modules/health-check/core-health-check.module.ts +16 -2
  651. package/src/core/modules/health-check/core-health-check.service.ts +25 -3
  652. package/src/core/modules/hub/README.md +20 -11
  653. package/src/core/modules/hub/core-hub.controller.ts +5 -0
  654. package/src/core/modules/hub/core-hub.service.ts +1 -1
  655. package/src/core/modules/hub/helpers/hub-client-js.helper.ts +23 -0
  656. package/src/core/modules/hub/hub-action-messages.ts +6 -0
  657. package/src/core/modules/hub/hub-nav.ts +1 -0
  658. package/src/core/modules/hub/interfaces/hub-panels.interface.ts +8 -1
  659. package/src/core/modules/hub/services/core-hub-db.service.ts +35 -17
  660. package/src/core/modules/hub/services/core-hub-migrations.service.ts +96 -2
  661. package/src/core/modules/hub/services/core-hub-sources.service.ts +47 -1
  662. package/src/core/modules/hub/services/hub-query-profiler.service.ts +1 -1
  663. package/src/core/modules/migrate/README.md +54 -0
  664. package/src/core/modules/migrate/cli/migrate-cli.ts +44 -3
  665. package/src/core/modules/migrate/index.ts +3 -0
  666. package/src/core/modules/migrate/migration-runner.ts +3 -2
  667. package/src/core/modules/migrate/migration-state-store.interface.ts +24 -0
  668. package/src/core/modules/migrate/migration.helper.ts +35 -10
  669. package/src/core/modules/migrate/mongo-state-store.ts +64 -65
  670. package/src/core/modules/migrate/sql-migration.helper.ts +48 -0
  671. package/src/core/modules/migrate/sql-state-store.ts +198 -0
  672. package/src/core/modules/system-setup/README.md +14 -2
  673. package/src/core/modules/system-setup/core-system-setup.service.ts +77 -45
  674. package/src/core/modules/tenant/README.md +9 -2
  675. package/src/core/modules/tenant/core-tenant-member.model.ts +1 -1
  676. package/src/core/modules/tenant/core-tenant-member.store.ts +13 -0
  677. package/src/core/modules/tenant/core-tenant.guard.ts +77 -24
  678. package/src/core/modules/tenant/core-tenant.module.ts +6 -3
  679. package/src/core/modules/tenant/core-tenant.service.ts +59 -42
  680. package/src/core/modules/tus/core-tus.service.ts +11 -12
  681. package/src/core/modules/tus/tus.module.ts +1 -1
  682. package/src/core/modules/user/core-user.model.ts +2 -2
  683. package/src/core/modules/user/core-user.service.ts +98 -20
  684. package/src/core.module.ts +60 -5
  685. package/src/index.ts +26 -0
  686. package/src/server/common/models/persistence.model.ts +4 -4
  687. package/src/server/modules/file/file-info.model.ts +1 -1
  688. package/src/server/modules/file/file.service.ts +1 -1
  689. package/src/server/modules/user/user.model.ts +10 -5
  690. package/src/server/modules/user/user.module.ts +9 -3
  691. package/src/server/modules/user/user.service.ts +9 -11
  692. package/src/test/test.helper.ts +2 -2
  693. package/src/types/graphql-upload.d.ts +10 -7
@@ -52,6 +52,11 @@ config is present, and keep their previous single-process behaviour when it is n
52
52
  `@Optional()` and always keep the non-configured fallback path — that is the pattern every core
53
53
  consumer follows. See `.claude/rules/configurable-features.md` for the full option reference.
54
54
 
55
+ **MongoDB itself is optional since 12.1.0** — the same pattern one level down: `mongoose`, `@nestjs/mongoose` and
56
+ `mongodb` are optional peers, reached only through `common/persistence/mongo/` (never a static value import in
57
+ `src/core/`), and `CoreModule` connects MongoDB only when a data source uses it. `pnpm run check:without-mongodb`
58
+ proves the build loads and runs with them unresolvable. Rules: `.claude/rules/persistence.md`.
59
+
55
60
  ## Core Modules (`src/core/modules/`)
56
61
 
57
62
  | Module | Purpose |
@@ -62,7 +67,7 @@ consumer follows. See `.claude/rules/configurable-features.md` for the full opti
62
67
  | **ErrorCode** | Centralized error codes with unique identifiers |
63
68
  | **File** | File upload/download with GridFS storage |
64
69
  | **HealthCheck** | Application health monitoring |
65
- | **Hub** | Build-free ADMIN-gated operator cockpit at `/hub` (config-gated per environment; 16 panels, runtime collectors, mailbox, admin actions) |
70
+ | **Hub** | Build-free ADMIN-gated operator cockpit at `/hub` (config-gated per environment; 17 panels, runtime collectors, mailbox, admin actions) |
66
71
  | **Migrate** | Database migration utilities |
67
72
  | **Permissions** | ADMIN-gated security-map report (routes + roles + `@Restricted` fields); also surfaced in the Hub's "Routes / Permissions" panel |
68
73
  | **SystemSetup** | Initial admin creation for fresh deployments |
@@ -504,6 +504,51 @@ this protection must set `betterAuth.options.advanced.disableOriginCheck: false`
504
504
  removed. `tests/better-auth-signin-csrf.e2e-spec.ts` does; mutations `iam-signin-csrf-check-skipped` /
505
505
  `iam-signup-csrf-check-skipped`.
506
506
 
507
+ ## 11. Better-Auth on a SQL source — it follows the users table (db-agnostic phase 3)
508
+
509
+ Better-Auth keeps its data where the user model lives. `DataSourceRuntime.iamStorage()` finds the persisted model whose
510
+ table is `users`; when that model is routed to a SQL source, `CoreBetterAuthModule.createInstance()` hands Better-Auth
511
+ the application role's `pg` pool of that source, and Better-Auth runs its own Kysely adapter on it (it recognises a pool
512
+ by its `connect` method — the core imports no Kysely). On MongoDB nothing changes.
513
+
514
+ | Piece | Where | Rule |
515
+ |---|---|---|
516
+ | Naming | `withSqlStorage()` in `better-auth-sql.helper.ts` | `user` → `users`, every table and field snake_case — the plugins' too, on copies, never the plugin objects; ids are ObjectId hex (`generateId`), so a Better-Auth user id IS the framework's id and `iamId` equals it |
517
+ | One row per user | the same helper + `registerForeignColumns('users', …)` | Better-Auth's own columns (`name`, `email_verified`, `image`) live in the framework's `users` table; its required ones are added nullable first, so a row the user model writes alone stays valid, and the framework's schema planner does not report them as undeclared |
518
+ | Schema | `applyBetterAuthSchema()` | Better-Auth validates its schema when the instance is created, so its migrations run BEFORE, as the owner, under the framework's schema lock, followed by the grants and the safe-role check. Without an owner connection the boot stops while anything is pending — the application role never runs DDL |
519
+ | Framework access | `BetterAuthSqlStore` (`core-better-auth-sql.store.ts`), reached through the import-free leaf `core-better-auth-sql.registry.ts` | framework fields of the user row go through the user model's `DataStore` (its policies apply); Better-Auth's own fields and tables go through Better-Auth's `internalAdapter` / `adapter` (its hooks run — a deleted session reaches `wsSessionRevocationPlugin()`). Every MongoDB path keeps its code; the SQL branch sits beside it (`*OnSql` in the user mapper) |
520
+ | Passkey challenges | `storeChallenge()` | Better-Auth verification values with the identifier prefix `lt-passkey-challenge:` — no table of the framework's own, expiry checked on read, expired ones removed as new ones arrive |
521
+ | Expired rows | `BetterAuthSqlStore.deleteExpired()`, scheduled by `CoreBetterAuthService.startExpirySweep()` | Better-Auth never deletes an expired session and clears expired verification values only when one is looked up; on MongoDB the verification TTL index does it. On SQL the expiry sweep (`scheduleExpirySweep()`, every 15 min, one replica per interval) removes both — through Better-Auth's adapter, so its naming applies. Stopped in `onModuleDestroy()`, before the data sources close (mutation `iam-expired-sessions-kept`) |
522
+ | Per-request path | token service, middlewares, guard, web and cookie helpers | ids are strings; only `BetterAuthMongoUsers` (`core-better-auth-mongo.users.ts`) knows what an ObjectId is. Pinned by `tests/unit/iam-request-path-ids.spec.ts` |
523
+ | Boot steps | `CoreBetterAuthService.onModuleInit()` | `ensureIndices()` and the account-issuer backfill concern MongoDB's collections and are skipped on SQL |
524
+ | Counting | `BetterAuthSqlStore.migrationCounts()` | counts in SQL (`count(*)`, `EXISTS` over `account`), names from `$context.tables`. **Never read a whole table through Better-Auth's `adapter.findMany()`**: without a `limit` it stops at 100 rows (`defaultFindManyLimit`), which capped the migration status at 100 migrated users (mutation `iam-migration-status-capped-at-a-page`) |
525
+ | Readers outside the module | `CoreAiMcpOAuthService.loadUser()` | every native read of `users` needs the SQL branch too — an OAuth subject's roles came from MongoDB, i.e. nobody or a stale copy (mutation `mcp-oauth-user-on-mongo`). Find candidates with `grep -rn "collection('users')" src/core` |
526
+
527
+ **Sign-up holds a pool connection while it hashes (accepted).** Better-Auth runs its Kysely adapter with transactions
528
+ on a `pg` pool, and `/sign-up/email` hashes the password inside its transaction — on the framework's application pool
529
+ (10 connections). A burst of concurrent sign-ups therefore makes other queries wait (measured: ~0.16 s at 20, ~1.3 s
530
+ at 100). Kept on purpose: it is Better-Auth's standard behaviour on PostgreSQL, it makes user and account creation
531
+ atomic, and switching it off would need a Kysely instance the core cannot rely on (`kysely` is an optional peer).
532
+ `betterAuth.rateLimit` bounds it.
533
+
534
+ **Not on SQL, and why that is no gap:** the account-issuer backfill (§8) repairs account documents better-auth 1.6
535
+ wrote to MongoDB before the field existed. On a SQL source every account row is written with what the installed line
536
+ expects — by Better-Auth itself, and by the framework's own writes through the issuer helper (`migrateAccountToIamOnSql()`,
537
+ the system setup) — so there is nothing to backfill. Nothing is copied from MongoDB when a project routes its users to
538
+ SQL.
539
+
540
+ **The challenge cookie is named by Better-Auth.** The API middleware reads the passkey challenge token from the cookie
541
+ Better-Auth sets and asks the instance for its name (`createAuthCookie(<key>).name`, `iam.better-auth-passkey` by
542
+ default) — a hard-coded `better-auth.` prefix missed it after the cookie prefix began to follow the base path, so no
543
+ mapping was stored and no `challengeId` returned, on every database (mutation `passkey-challenge-cookie-unprefixed`).
544
+ `passkey.webAuthnChallengeCookie` is the KEY; it reaches the plugin as `advanced.webAuthnChallengeCookie`, the only
545
+ place the plugin reads it.
546
+
547
+ Tests: every IAM suite runs on MongoDB and on PostgreSQL with each engine (`.claude/rules/testing.md` → the IAM
548
+ projects); `tests/better-auth-storage-paths.e2e-spec.ts` covers the four paths the stories pass either way (session
549
+ token as bearer, token service, passkey challenge, e-mail verified), `tests/persistence-sql-wiring.e2e-spec.ts` the
550
+ boot (one row, ownership, a second boot, the system setup).
551
+
507
552
  ## Summary
508
553
 
509
554
  | Principle | Requirement |
@@ -518,3 +563,4 @@ removed. `tests/better-auth-signin-csrf.e2e-spec.ts` does; mutations `iam-signin
518
563
  | `account` reads / writes | Reads filter on `providerId` alone, never `issuer`. Writes set `issuer` only on better-auth 1.7.0–1.7.2, via the helper (§8) |
519
564
  | `openAPI()` plugin | Registered internally for `setupSwagger()`, kept off the router (`SERVER_ONLY`) unless the project registered it — never by a path check (§9) |
520
565
  | Direct `api.*` calls | Hand Better-Auth the request (CSRF headers only) + `asResponse: false`, or its login-CSRF check is skipped (§10) |
566
+ | SQL source | Better-Auth follows the users table; the framework reaches its data through `BetterAuthSqlStore`, ids stay strings (§11) |
@@ -321,7 +321,8 @@ This pattern is currently applied to:
321
321
  | Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
322
322
  | Mongoose CastError Filter | `security.mongooseCastErrorFilter` | Explicit Boolean — **safe default** | **`true` since 11.42.16.** `MongooseCastErrorFilter`, registered by `CoreModule` as `APP_FILTER` (so `main.ts` and `Test.createTestingModule()` setups need nothing). A malformed id reached Mongoose and its `CastError` — not an `HttpException` — escaped every `@Catch(HttpException)` filter, so `GET /users/<malformed-id>` and every other `:id` route answered **500**. The filter decides by WHERE the failing value came from: a route parameter or a top-level GraphQL argument → **404** `ErrorCode.RESOURCE_NOT_FOUND` (answered like an unknown id); the query string, an update or filter body, or a nested GraphQL input → **400** `ErrorCode.INVALID_FIELD_FORMAT [<path>]`; a value the request does not contain (a broken stored id, a server-computed value) → **stays a 500**, with Nest's own body and log line (context `ExceptionsHandler`), because that is a data or code defect monitoring must keep seeing — relabelling it as a client error is the mistake the refusal tests pin (mutations `cast-error-server-value-relabelled-*`). Only primitives and arrays of primitives (`$in`) are matched: an object cast as a whole yields no candidates, so its inner strings cannot match the body by coincidence. Never sends the Mongoose message (it names the model). REST bodies use the `HttpExceptionLogFilter` shape (`{ ...exception }`). A project's own catch-all filter via `app.useGlobalFilters()` is consulted first and wins. Covers casts Mongoose raises while running a QUERY or an UPDATE (`findById`, filters, `findByIdAndUpdate` — `CrudService.update()`). NOT covered, both stay a 500: a malformed id in a document being CREATED (`save()` / `create()` / `insertMany()` — `CrudService.create()`), which Mongoose reports as a `ValidationError` wrapping the CastError (validate in the input: `validator: () => [IsMongoId()]`); and a `BSONError` from `new Types.ObjectId(bad)`, which carries no value (check the id in the service, `CoreFileService.isMalformedFileId()`). `false` restores the 500. Implementation: `common/filters/mongoose-cast-error.filter.ts` |
323
323
  | Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
324
- | Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | **`excludeSchemas` is an OFF SWITCH for isolation, per model** — a listed schema gets no tenant filter at all; since 11.35.0 the plugin WARNS once per model when the excluded schema declares `tenantId` (i.e. was built for isolation). The framework's own docs recommended `['User', 'Session']` until then; that is withdrawn — it is only right for the global-user model. Since 11.35.0 GraphQL operations over the WEBSOCKET also run inside a `RequestContext` (`CoreModule` installs a context-aware `execute`/`subscribe` pair inside `subscriptions`, where `ApolloDriver` forwards it): the tenant comes from the handshake header validated against an active membership, else from the subscriber's memberships, and an unresolvable tenant leaves the safety net to refuse the read rather than returning everything. `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing. **`isTenantActive` (11.42.0)**: optional hook `(tenantId, { connection }) => boolean \| Promise<boolean>`, absent = every tenant active (no change). Asked AFTER the membership check on every path that establishes a tenant: 403 on header paths and for tenant API tokens, no enrichment on `S_EVERYONE`, dropped from the header-less tenant list, no tenant for a NEW WebSocket subscription, and since 11.42.3 an OPEN one is closed with 4403 on `invalidateTenant()` (or at its next event once `graphQl.subscriptionRevalidationMs` has passed — see the GraphQL Subscription Re-check row); an admin under `adminBypass` is exempt (someone must be able to switch it back on). Only `false` deactivates. Cached for `cacheTtlMs`; `invalidateTenant(tenantId)` clears and broadcasts (`{ scope: 'tenant' }`). A function, so `config.env.ts` only — not reachable through `NSC__*`. **`adminBypass: false` (11.42.0)** additionally withholds two things that went around it: `CoreUserService.update()` refuses changing another account's e-mail or password (`assertCredentialChangeAllowed()`, administrator targets included — promote-then-change — system work without a current user exempt), and the AI tool registry grants no ADMIN bypass |
324
+ | Data Sources | `database` | Explicit Value — absent = today's `mongoose` config | **Absent → the `mongoose` configuration is the only data source, exactly as before; nothing SQL is loaded or installed.** A URL string (`'postgres://…'`) sets the default source (driver from the scheme, `sqlEngine` defaults to `typeorm`; `kysely` and `drizzle` are the alternatives — optional peers, each with `pg`); an object adds `ownerUrl` (default source), `sources` (name → URL or `{ url, ownerUrl, sqlEngine }`) and per-model exceptions (`models`); `false` means no database. Precedence for a model: `database.models` > the `dataSource` of a `CorePersistenceModule.forFeature()` definition > `@PersistedModel({ dataSource })` (read from the definition's `model:` class, otherwise from the decorated class named like the model; two such classes naming different sources refuse to guess) > default. When `database` sets a default and the project ALSO configures `mongoose` explicitly, that MongoDB becomes the source `mongoose` — only the project's own `mongoose` counts, because `CoreModule.forRoot()` always merges a fallback URI. **Since phase 1 (DEV-3829) PostgreSQL sources are wired**: only models registered with `CorePersistenceModule.forFeature()` are routed — **since phase 2 (DEV-3830) that includes the core's own models**: the user model (the reference `UserModule`), tenant memberships and API tokens, whose services switch to the routed store (a service that only received `@InjectModel()` follows its model's store; the Mongoose model of a model routed to SQL refuses every data access with a named error, so a direct `mainDbModel` call cannot split a model's data). **Since phase 3 (DEV-3831) Better-Auth follows the user model**: with the user model (table `users`) on a SQL source, Better-Auth runs its built-in Kysely adapter on that source's application pool — ONE `users` row per user shared with the user model (Better-Auth's own columns `name`, `email_verified`, `image` beside the framework's; its required ones are added nullable, so a row the user model writes alone stays valid), snake_case tables (`session`, `account`, `verification`, `jwks`, `two_factor`, `passkey`), ObjectId-hex ids. Its schema is applied as the OWNER at boot, under the same schema lock and grants as the framework's tables; without `ownerUrl` the application role only verifies and the boot stops while Better-Auth's migrations are pending. The framework's own reads and writes of Better-Auth's data follow it (session lookup by token, token service, JWKS, passkey challenge mappings — as Better-Auth verification values —, the e-mail-verified hook, user mapper, system setup, migration status — counted in SQL —, the MCP OAuth subject's roles); the MongoDB boot steps (indices, account-issuer backfill) are skipped there. Nothing is copied from MongoDB: a project that moves its users brings them over itself. **Since phase 4 (DEV-3832) the remaining core persistence follows too**: the AI module's eleven models and the three MCP OAuth stores (clients, codes, refresh tokens) are registered through `CorePersistenceModule.forFeature()` and run on their routed store (one code path; an upsert is update-then-insert with a retry on a duplicate, `sum()` replaces the budget aggregation, `find(…, { exclude })` the `-field` projection); the S3 and filesystem file metadata are two framework models (`CoreS3File`, `CoreFilesystemFile`, tables `s3_files` / `filesystem_files`) read and written through `fileMetadataCollection()` by the file service, the TUS hand-off and the Hub — GridFS stays MongoDB-only. A collection name with a hyphen becomes a table with an underscore. A TTL (`expires`, `index: { expireAfterSeconds }`) is a plain index on SQL; an **expiry sweep** (`scheduleExpirySweep()`, every 15 min, one replica per interval through a lease in the default SQL source's `lt_locks`, fail-open) deletes the expired rows of every routed TTL model, and — with Better-Auth on SQL — expired sessions and verification values. Nothing relies on it for correctness: every reader checks the expiry (an MCP authorization code older than ten minutes is refused on read on both databases). MongoDB keeps its TTL indexes and gets no sweep. The framework's leases (system-setup claim, cron, migrations) live in the default source: on MongoDB in the 12.0.0 documents (a rolling deploy with older replicas shares them), on SQL in `lt_locks`, namespaced, swept an hour after expiry. With `healthCheck.configs.database` every SQL source answers as `<key>:<source>`; `createMigrationStore(<owner URL>)` keeps the migration state in `lt_migrations` with an advisory run lock, on the first SQL engine installed (the CLI cannot read `database.sqlEngine`), and the Hub refuses to run migrations while that table is empty and the MongoDB collection is not (a `migrate.js` still writing MongoDB); the Hub's Persistence panel shows the routing. a SQL model's table is migrated as the OWNER (`ownerUrl`) when its store is created — without `ownerUrl` the application role only verifies and the boot stops while changes are pending; ONE owner connection per source, released once the application is bootstrapped — and the boot refuses an application role that is superuser, has `BYPASSRLS`, owns (or inherits the owner of) a framework table or may `TRUNCATE` it. Two roles in every environment (plan §3.9). Changes the framework never applies on its own (type change, new unique index, rename collision) stop the boot with the suggested statements; `migrate create <name> --sql` + `runSqlMigration()` apply them after review. **Since phase 5 (DEV-3833, 12.1.0) MongoDB is optional**: `mongoose`, `@nestjs/mongoose` and `mongodb` are optional peers, loaded through `common/persistence/mongo/mongo-packages.ts`; `CoreModule` connects MongoDB only when a source uses it (`usesMongoDb()` — `mongoose` configured explicitly, `database` naming MongoDB, or `database` absent), otherwise `@InjectConnection()` receives `null` and a model token without a connection is a stand-in that refuses data access. A configured MongoDB feature without the packages fails the boot with `MongoPackagesMissingError`. `database: false` runs a service without any database: a registered model's store refuses with the reason, Better-Auth is refused at boot. Proven by `pnpm run check:without-mongodb` (dist, packages hidden) and `check:without-mongodb:package` (the tarball installed without them, publish path). **Since phase 6 (DEV-3848) MySQL 8.4 and MariaDB 11.8 sources are wired** (`mysql://`, `mariadb://` — the scheme must name the server the URL reaches): `mysql2` is an optional peer beside the engine, all three engines run on one pool the framework creates (strict session with backslash escapes, UTC), `MysqlDataStore` writes the statements, the owner grants the application account `SELECT, INSERT, UPDATE, DELETE` on the database once and the boot refuses an application account that may alter, drop, index, trigger or grant; leases, migration state (`createMigrationStore()` / `runSqlMigration()` under `GET_LOCK`), the expiry sweep, health check and Hub work there. **Neither database has row-level security**: tenant isolation is the store's, the boot warns once per source, and native / raw access on a tenant table must filter by tenant itself. Better-Auth is refused at boot while the user model lives there. An indexed text field is `varchar(255)` (longer: 400); a DDL run is not transactional. Still refused with a named `DataSourceConfigError`: a second MongoDB source, and an `ownerUrl` without `url` — what `NSC__DATABASE__OWNER_URL` leaves of a `database` given as a URL string, because the environment merge replaces the string with an object (write `database: { url }`, or set `NSC__DATABASE__URL` too). An update path the model does not declare is dropped on SQL as by MongoDB's strict mode (`CrudService.update()` adds `updatedBy` to every update with a current user). URLs appear in errors and logs as scheme + host only. Implementation: `common/persistence/data-source.config.ts`, `data-source.runtime.ts`, `sql/` (plan: `.claude/plans/db-agnostic/PLAN.md` §3.9) |
325
+ | Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | **`excludeSchemas` is an OFF SWITCH for isolation, per model** — a listed schema gets no tenant filter at all; since 11.35.0 the plugin WARNS once per model when the excluded schema declares `tenantId` (i.e. was built for isolation). The framework's own docs recommended `['User', 'Session']` until then; that is withdrawn — it is only right for the global-user model. Since 11.35.0 GraphQL operations over the WEBSOCKET also run inside a `RequestContext` (`CoreModule` installs a context-aware `execute`/`subscribe` pair inside `subscriptions`, where `ApolloDriver` forwards it): the tenant comes from the handshake header validated against an active membership, else from the subscriber's memberships, and an unresolvable tenant leaves the safety net to refuse the read rather than returning everything. `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing. **`isTenantActive` (11.42.0)**: optional hook `(tenantId, { connection, store }) => boolean \| Promise<boolean>`, absent = every tenant active (no change). `store(modelName)` (12.x, db-agnostic) is the data store of a model registered with `CorePersistenceModule.forFeature()`, wherever `database` routes it, and THROWS for any other model — an `undefined` would read as "active" in a hook written with `?.` and open every deactivated tenant. Asked AFTER the membership check on every path that establishes a tenant: 403 on header paths and for tenant API tokens, no enrichment on `S_EVERYONE`, dropped from the header-less tenant list, no tenant for a NEW WebSocket subscription, and since 11.42.3 an OPEN one is closed with 4403 on `invalidateTenant()` (or at its next event once `graphQl.subscriptionRevalidationMs` has passed — see the GraphQL Subscription Re-check row); an admin under `adminBypass` is exempt (someone must be able to switch it back on). Only `false` deactivates. Cached for `cacheTtlMs`; `invalidateTenant(tenantId)` clears and broadcasts (`{ scope: 'tenant' }`). A function, so `config.env.ts` only — not reachable through `NSC__*`. **`adminBypass: false` (11.42.0)** additionally withholds two things that went around it: `CoreUserService.update()` refuses changing another account's e-mail or password (`assertCredentialChangeAllowed()`, administrator targets included — promote-then-change — system work without a current user exempt), and the AI tool registry grants no ADMIN bypass |
325
326
  | GraphQL Subscription Re-check | `graphQl.subscriptionRevalidationMs` | Numeric — `0` = STRICTEST, `false` = off, invalid → default (a fifth shape: Family C's "invalid never means unbounded", but `0` asks for MORE checking, not less) | **30000 since 11.42.3.** Before, a WebSocket was authorized once, at the handshake, and every event and operation on it ran on that snapshot until the client reconnected. Every connection a `CoreModule` `onConnect` accepts is now registered (`registerWsConnection()`) and re-checked — at once when an invalidation names it (`CoreTenantGuard.invalidateUser()` / `invalidateTenant()` / `invalidateAll()`, `CoreUserService.setRoles()` and role/verification `update()`s, the legacy password reset, and Better-Auth deleting a session through `wsSessionRevocationPlugin()`), and otherwise before the next event or operation once the last check is older than this value. The check re-runs the handshake's own authentication (session store, Better-Auth JWT signature/expiry, or for legacy tokens `CoreAuthService.verifyJwt()` — signature/expiry, verified on the WebSocket since 11.42.3 — plus the device binding) and the tenant resolution; any difference in user id, roles, verification state or tenant scope closes the socket with **4403**, which the graphql-ws client retries — the user continues with current rights or is refused at the handshake. `0` re-checks on every event and operation (one session lookup each); `false` switches only the TIME-based check off, invalidations still apply. Process-local registry: an invalidation reaches other replicas only where the tenant guard's Redis broadcast carries it; everything else is bounded by the interval. A project that replaces `onConnect` through `graphQl.driver.subscriptions` must call `registerWsConnection()` itself to keep this. Implementation: `common/helpers/graphql-ws-connection.helper.ts` |
326
327
  | API Tokens | `apiTokens` | Boolean Shorthand + Presence Implies Enabled — absent = off | `prefix: 'ltt'`, `scopes: []` (vocabulary; empty = no token creatable, boot warning), `manageRole`: highest hierarchy role, `maxAssertionLifetimeSeconds: 900` (invalid → default, never unbounded — Family C), `rateLimit: { max: 600, windowSeconds: 60 }` per token (**on by default**, `false` disables; Redis-shared when configured), `userTokens: true`, `tenantTokens: true` (effective only with `multiTenancy`), `encryptionKey` → `SECRETS_ENCRYPTION_KEY` → dev default (boot FAILS in production/staging). Top-level, NOT under `multiTenancy`: user tokens must work without it. **Deny by default**: every guard refuses a token on a route without `@ApiTokenScopes()`, public routes included, via ONE shared function (`enforceApiTokenRoute`) called BEFORE each guard's public-route shortcut. USER tokens act as their user with global roles REMOVED (no admin bypass); TENANT tokens act with the lowest hierarchy role in their own tenant, and a route guarded ONLY by `S_SELF` / `S_CREATOR` refuses them (a tenant token is nobody's self — skipping those roles would grant for any target). A tenant token depends on no member, so it outlives an off-boarded or deleted tenant until the project calls `deleteAllForTenant()`. The boot refuses a hierarchy in which that lowest role reaches `manageRole`. A prefixed credential that fails answers 401 on every route (not anonymous); `CoreBetterAuthMiddleware` skips prefixed credentials entirely, cookie fallback included. A password reset that ends the user's sessions ends their USER tokens too (legacy reset always; IAM reset while `betterAuth.emailAndPassword.revokeSessionsOnPasswordReset` is on), wired through `core-api-token.registry.ts` so neither reset path imports the optional module. Implementation: `src/core/modules/api-token/` |
327
328
  | BetterAuth Tenant Skip | `betterAuth.skipTenantCheck` | Explicit Boolean | `true` (default). When `true` and no `X-Tenant-Id` header is sent, IAM endpoints (controller + resolver) skip `CoreTenantGuard` tenant validation. When header IS present, normal membership validation runs regardless. Set `false` for tenant-aware auth scenarios (subdomain-based, invite links, SSO per tenant) |
@@ -332,7 +333,7 @@ This pattern is currently applied to:
332
333
  | Central Redis | `redis` | Boolean Shorthand + Presence Implies Enabled | `host: 'localhost'`, `port: 6379`, `db: 0`, `keyPrefix: <package.json name, slugified>` (per APPLICATION, not per framework — a constant default silently collides when two apps share one Redis; set it explicitly when sharing IS intended); `url` (takes precedence over host/port/db/credentials), `username`/`password`, `options` (passed to the ioredis constructor). `true` / `{}` enables with defaults; `{ enabled: false }` pre-configures without enabling; **absent = every consumer keeps its process-local fallback** (that fallback is the whole backward-compatibility story of 11.33.0). Requires the OPTIONAL peer `ioredis` — configured-but-missing **fails the boot** with a named error rather than crashing on first use. `keyPrefix` is applied by the framework per key, NOT as ioredis `keyPrefix` (that would collide with BullMQ's own prefix). One `CoreRedisService` serves all features: shared client (`getClient()`), one cached subscriber (`getSubscriber()` — a subscribing client cannot run commands), dedicated connections (`createClient(label)`); all are tracked and quit on shutdown. Switches on automatically: exact cross-replica rate limits via `RedisRateLimitStore` (see the Legacy Auth / BetterAuth / AI rate-limit rows), cron dedup (see Cron Job Deduplication row), `CoreRedisPubSub` as `PUB_SUB` for cluster-wide GraphQL subscriptions (**payloads must be JSON-serializable — `Date`, class instances, `Map`/`Set`, `undefined` do not survive**), tenant-cache invalidation broadcast, Hub collector mirroring, MCP session registry (turns a wrong-replica request from a misleading `404` into a `409` — `/ai/mcp` still REQUIRES sticky sessions, sessions are not portable) |
333
334
  | File Access Roles | `file.downloadRoles`, `file.uploadRoles`, `file.deleteRoles` | Config-Driven Role List | `[RoleEnum.ADMIN]` each. **These are the only breaking change a single-replica project gets in 11.33.0** — six members moved from `@Roles(S_EVERYONE)` to these knobs: `GET /files/id/:id`, `GET /files/:filename` and `getFileInfo` (`downloadRoles`), `uploadFile` / `uploadFiles` (`uploadRoles`), `deleteFile` (`deleteRoles`). Plain role STRINGS, not `RoleEnum` members, so project roles work (`['company-admin', 'editor']`). Applied at boot by `applyFileRoles()` via `Reflect.defineMetadata('roles', …)` on the base-class methods — the same runtime mechanism `CorePermissionsModule` uses, because the value is only known from config. **`[]`, a non-array, or an array holding a non-string is REJECTED with a warning and the default applies** — an all-empty role set reads to the guards as "no roles required" and would OPEN the route, the exact opposite of the intent. **ADMIN is always unioned in**: both `CoreFileController` and `CoreFileResolver` carry a class-level `@Roles(RoleEnum.ADMIN)` and the guards UNION class + handler metadata, so these knobs can grant but never exclude admins. Both classes also carry `@SkipTenantCheck()` — GridFS and the S3 metadata collection are reached outside Mongoose, so `mongooseTenantPlugin` never scopes them and a role name alone cannot express a per-tenant rule; roles resolve against `user.roles`, never `membership.role`. **A subclass that OVERRIDES a member opts out permanently** (decorator metadata lives on the function object, and an override is a different function) — inherit the member instead. **Since 11.35.1 that opt-out is REPORTED**: `CoreFileAccessAuditInitializer` reads the roles off the class the project actually registered at `onApplicationBootstrap` (the earliest point at which the route table exists, which is why it is a provider and not part of `applyFileRoles()`) and warns when a member is open beyond ADMIN for a reason the configuration cannot explain — the case the construction-time warning below is structurally blind to. It only REPORTS: overwriting an override's metadata, the way `TusModule.applyRoles()` does, would silently relax a route a project pinned on purpose. Roles are the coarse filter only; per-file rules belong in `CoreFileService.checkRights()`, which now receives `currentUser` and can read raw metadata via `getRawFileInfo()`. **Since 11.35.0 a reused FILENAME resolves to the most recent file** in all three stores (`uploadDate` desc, `_id` tie-break) and every by-name read path resolves a document and then reads by ID — under GridFS the by-name metadata lookup answered the oldest document while `openDownloadStreamByName()` served the newest, so a per-file rule approved one file and the bytes of another came back. Consequence: `duplicateById()` keeps the source's filename and the copy carries no metadata, so the copy now wins the name and an `ownerId` rule refuses it — give the copy its own metadata or its own name. **Since 11.35.0 `CoreFileService` WARNS at construction** when `multiTenancy` is active AND a knob names a role other than ADMIN AND `checkRights()` is not overridden — the file stores are reached outside Mongoose, so these role names resolve against `user.roles` (global) and every holder reaches every tenant's files; only `checkRights()` can narrow that. Silent on the admin-only default and silent when a per-file rule exists. Implementation: `src/core/modules/file/file-roles.config.ts` (`FILE_ROLE_DEFAULTS`, `FILE_ROLE_MEMBERS`, the three warnings, the shared `hasDeclaredFilePolicy()` silencer) + `file-roles.helper.ts` (`applyFileRoles`) + `core-file-access-audit.initializer.ts` (the registered-class audit) |
334
335
  | TUS Roles | `tus.roles` | Config-Driven Role List | `[RoleEnum.S_USER]` (was `S_EVERYONE` — breaking). A TUS upload writes into the SAME store the download routes guard, and the termination extension (on by default) can delete from it, so anonymous writes into a store only privileged callers may read is the wrong way round. Applied by `TusModule.applyRoles()` onto the registered controller class **and** onto `handleTus` / `handleTusWithId`. Same rejection rule as the file roles: `[]` / non-array / non-string → warning + `DEFAULT_TUS_CONFIG.roles`. **`OPTIONS` is deliberately exempt**: `handleTusOptions` / `handleTusOptionsWithId` keep their own handler-level `@Roles(RoleEnum.S_EVERYONE)`, because that is the CORS preflight — browsers send it WITHOUT credentials, and it returns server capabilities only. Gating it would make every browser upload fail before the first byte. `CoreTusController` also carries `@SkipTenantCheck()`. A custom controller is covered as long as it INHERITS the handlers; one that re-declares `@All()` / `@Roles()` carries its own metadata and thereby opts out — the documented way to hard-code a policy config must not be able to change. **Set `roles: [RoleEnum.S_EVERYONE]` explicitly if you accept attachments on a public form.** **Since 11.35.0 `tus.roles` is no longer the only gate**: `onUploadCreate` records the authenticated creator under `TUS_OWNER_METADATA_KEY` (`ltOwnerId`, always OVERWRITING any client-supplied value) and `onIncomingRequest` refuses HEAD/PATCH/DELETE naming an upload the caller does not own — with **404**, the file module's refusal policy. Before that, any authenticated caller who learned an upload id could APPEND BYTES to somebody else's upload, which are then migrated into the file store under the victim's filename. The finished file also gains `metadata.ownerId`, without which the documented per-file ownership rule could never authorize a tus-uploaded file (admin-only in practice). An OWNER-LESS upload stays reachable — uploads predating 11.35.0 have none, and neither does a deliberately public form. `readRequestUserId()` / `assertUploadOwnership()` are `protected`. **Since 11.41.9 the tenant is recorded too, but ONLY with `multiTenancy` configured** (not `enabled: false`) and a tenant guard registered: `onUploadCreate` validates the tenant header against the caller's active memberships through `core-tenant-context.registry.ts` (the routes carry `@SkipTenantCheck()`, so the guard did not), refuses a non-member with **403**, writes it under `TUS_TENANT_METADATA_KEY` (`ltTenantId`, always overwritten like the owner key) and the finished file gains `metadata.tenantId`. No header → tenant-less, as before; without `multiTenancy` nothing is read or written. A project hooks in through `TusModule.forRoot({ service })` (a `CoreTusService` subclass, constructed by the module as `(connection, options)` — project providers come from `options.moduleRef` at call time) and the `protected onUploadCreate(req, upload)`, the one point where `Upload-Length` is known and nothing is stored yet; an override must return `super.onUploadCreate()`. Note that `@tus/server` v2 hands its hooks a WHATWG `ServerRequest`, so a guard's `req.user` is reachable only via `runtime.node.req`. **CORS (11.42.1)**: the tus handler's `Access-Control-Allow-Origin` comes from `buildCorsConfig()` (or `tus.allowedOrigins`), plus `allowedCredentials` — without it `@tus/server` answered `*`, which browsers refuse on a credentialed request, so cookie-authenticated uploads across split app/API origins failed in every browser while the server answered 201; no credentialed CORS configured → the `*` default stays. **compression (11.42.2)**: `@tus/server` finishes responses with `res.end(callback)`, which `compression`'s two-argument `res.end(chunk, encoding)` patch reads as a chunk (`ERR_INVALID_ARG_TYPE … Received function`); with the starter's `filter: () => true, threshold: 0` every finished upload HUNG without an answer AFTER the file was stored (headers already sent, so no error response could follow). `CoreTusService` wraps `server.handle()` and normalises `res.end` per response (`normalizeEndCallback()`, callback on `finish`/`close`), so no `main.ts` change is needed. Implementation: `src/core/modules/tus/tus.module.ts`, `src/core/modules/tus/core-tus.service.ts`, `src/core/modules/tus/tus.constants.ts` |
335
- | File Storage | `file.storage` + `file.storageDir` + `s3` | Explicit Enum with DERIVED default | `'filesystem' \| 'gridfs' \| 's3'`. **Unset → derived**, most capable first: `'s3'` when `s3.bucket` is set, else `'gridfs'` when `mongoose.uri` is set, else `'filesystem'`. **Set → enforced**: an unavailable store FAILS THE BOOT (`assertFileStorageAvailable()`), it never falls back — a silent fallback puts files in a store the operator does not believe they are in, unrecoverably. A DERIVED driver is enforced too (`s3.bucket` set but `s3Service` not forwarded to `super()` → boot error). Metadata always lives in MongoDB whichever driver holds the bytes (`fs.files` / `s3-files` / `filesystem-files`) — it has to stay queryable for `findFileInfo()` and `checkRights()`. Reads consult ALL stores so switching drivers is forward-only with no migration; writes go to the active driver only. `'filesystem'` is pod-local: not shared between replicas, lost on restart unless `storageDir` is a mounted volume. `s3`: `bucket` (required — the one thing S3 cannot default, hence the eligibility test), `region: 'us-east-1'`, `forcePathStyle: false`, `autoCreateBucket: false` (off by design: production buckets come from infrastructure code, whose credentials usually cannot CreateBucket), `stagingBucket: bucket`, `presignedDownloads: false` (`true`/`{}` → `expiresInSeconds: 300`), `endpoint` (MinIO/RustFS), `accessKeyId`/`secretAccessKey` (omit → AWS default credential chain), `enabled: false` to pre-configure. Requires the OPTIONAL peer `@aws-sdk/client-s3`, plus `@aws-sdk/s3-request-presigner` for presigned downloads. **Uploads are never buffered on the S3 path** (11.41.7): a known length (`FileUploadSource.size`, TUS, a buffer) goes up as one streamed PutObject up to `CoreS3Service.MAX_SINGLE_PUT_BYTES` (5 GiB — AWS S3 refuses more in one request, `EntityTooLarge`) and as a multipart upload above it, an unknown one — every GraphQL and streamed REST upload — as a multipart upload via the OPTIONAL peer `@aws-sdk/lib-storage`. Missing peer → one boot warning and the old in-memory buffering, NOT a boot failure (multipart optimises a path that works without it). `size` must be the file's EXACT length — never a multipart request's `Content-Length`; anything but a positive integer counts as unknown (`isKnownUploadSize()`), and the recorded length of an unknown-length upload comes from a HEAD of the stored object. `presignedDownloads` makes `GET /files/id/:id` answer `302` to a time-limited S3 URL instead of streaming — the URL is a bearer capability, authorized once at issue time. The resolved driver is logged at boot (`[CoreFileStorage] File storage: …`) |
336
+ | File Storage | `file.storage` + `file.storageDir` + `s3` | Explicit Enum with DERIVED default | `'filesystem' \| 'gridfs' \| 's3'`. **Unset → derived**, most capable first: `'s3'` when `s3.bucket` is set, else `'filesystem'` with `database: false` or when `database` routes the file metadata to a SQL source (`fileMetadataSqlSource()`; GridFS would put bytes and metadata into MongoDB — an explicit `'gridfs'` keeps working), else `'gridfs'` when `mongoose.uri` is set, else `'filesystem'`. **Set → enforced**: an unavailable store FAILS THE BOOT (`assertFileStorageAvailable()`), it never falls back — a silent fallback puts files in a store the operator does not believe they are in, unrecoverably. A DERIVED driver is enforced too (`s3.bucket` set but `s3Service` not forwarded to `super()` → boot error). Metadata stays queryable whichever driver holds the bytes (`fs.files` / `s3-files` / `filesystem-files`, the latter two as the tables `s3_files` / `filesystem_files` when `database` routes them to a SQL source) — `findFileInfo()` and `checkRights()` need it. Reads consult ALL stores so switching drivers is forward-only with no migration; writes go to the active driver only. `'filesystem'` is pod-local: not shared between replicas, lost on restart unless `storageDir` is a mounted volume. `s3`: `bucket` (required — the one thing S3 cannot default, hence the eligibility test), `region: 'us-east-1'`, `forcePathStyle: false`, `autoCreateBucket: false` (off by design: production buckets come from infrastructure code, whose credentials usually cannot CreateBucket), `stagingBucket: bucket`, `presignedDownloads: false` (`true`/`{}` → `expiresInSeconds: 300`), `endpoint` (MinIO/RustFS), `accessKeyId`/`secretAccessKey` (omit → AWS default credential chain), `enabled: false` to pre-configure. Requires the OPTIONAL peer `@aws-sdk/client-s3`, plus `@aws-sdk/s3-request-presigner` for presigned downloads. **Uploads are never buffered on the S3 path** (11.41.7): a known length (`FileUploadSource.size`, TUS, a buffer) goes up as one streamed PutObject up to `CoreS3Service.MAX_SINGLE_PUT_BYTES` (5 GiB — AWS S3 refuses more in one request, `EntityTooLarge`) and as a multipart upload above it, an unknown one — every GraphQL and streamed REST upload — as a multipart upload via the OPTIONAL peer `@aws-sdk/lib-storage`. Missing peer → one boot warning and the old in-memory buffering, NOT a boot failure (multipart optimises a path that works without it). `size` must be the file's EXACT length — never a multipart request's `Content-Length`; anything but a positive integer counts as unknown (`isKnownUploadSize()`), and the recorded length of an unknown-length upload comes from a HEAD of the stored object. `presignedDownloads` makes `GET /files/id/:id` answer `302` to a time-limited S3 URL instead of streaming — the URL is a bearer capability, authorized once at issue time. The resolved driver is logged at boot (`[CoreFileStorage] File storage: …`) |
336
337
  | TUS S3 Staging | `tus.s3Staging` | Explicit Boolean (default ON when S3 configured) | `true` when `s3` is configured, otherwise inert. Stages in-progress uploads in `s3.stagingBucket` via `@tus/s3-store` instead of `tus.uploadDir` on local disk, so resumable uploads survive replica restarts and need no sticky sessions. Set `false` to force local disk. Missing OPTIONAL peer `@tus/s3-store` → warning + fall back to local disk (NOT a boot failure). **Give the staging bucket a lifecycle rule expiring incomplete multipart uploads** — the framework's own expiration cleanup is skipped in S3 mode (S3 is the right place for that policy), so aborted uploads otherwise accumulate parts nothing removes |
337
338
  | TUS Configuration Source, Path & Extensions | `tus` (server config) / `TusModule.forRoot({ config })` | Enabled by Default + explicit-config-wins | **Since 11.42.9 `TusModule.forRoot()` without `config` reads the server config's `tus` key** (`ConfigService.get('tus')`, the zero-config rule `CoreBetterAuthModule.forRoot()` follows); an explicit `config` wins. Before, the key was never read: `tus: false`, `allowedTypes`, `maxSize`, `roles` in `config.env.ts` did nothing — real projects had upload restrictions they believed were enforced. **`path`** has three readings (`applyPath()`): unset → follows the controller's route; a new route (`/uploads`) → mounts the registered controller there (writes `PATH_METADATA`, config wins like the roles; the declared route is remembered per class and restored by a later `forRoot()` without `path`); a path ENDING with the controller's route (`/api/tus`, `/files/tus`) → the PUBLIC URL: the controller stays and upload URLs use it. The third reading is load-bearing: it was the only working setting under a global prefix or a prefix-stripping proxy before 11.42.9, and treating it as a route doubled the prefix (`/api/api/tus`, 404 on every upload — caught in the second pre-release review). The upload URL (`Location`) is built from the path the creation request arrived at (`uploadCollectionPath(req)`), or from `path` when that is longer and ends with it (proxy strip); `@tus/server` built it from `path` alone, which broke every resume below a global prefix, a URI version or a project route. **`relativeLocation`** (new): the framework owns `generateUrl`, which takes precedence over `@tus/server`'s option of that name, so it is honoured from `tus.relativeLocation` AND from the tus server's own options (a vendored core may pass it there). **Extensions**: `@tus/server` consults no flag, so `creation`, `creationWithUpload` and `termination` are enforced by `CoreTusService` — the store's advertised `extensions` are narrowed, and `assertExtensionEnabled()` answers 501 inside the server's `onIncomingRequest` / `onUploadCreate` options (not in the overridable hook, so an override without `super` cannot reopen it). `checksum` / `concatenation` have no effect: neither built-in store supports them. `configure(false)` now yields a DISABLED config (before it kept the defaults and started a tus server behind a module with no endpoint). Implementation: `tus.module.ts`, `core-tus.service.ts` |
338
339
  | Health Check | `healthCheck` | Presence Implies Enabled | Absent → no endpoints. Any object registers `GET /health-check` + the `healthCheck` query; **`enabled: false` leaves them out (honoured since 11.42.9 — before, it was declared and ignored)**. Individual checks under `configs.*` each have their own `enabled`. Implementation: `core.module.ts` |
@@ -171,8 +171,10 @@ Package overrides live in the `overrides:` section of **`pnpm-workspace.yaml`**
171
171
  **Keep the set minimal.** On the pnpm 11 upgrade the list was pruned from 36 to the 9 then still
172
172
  load-bearing (it has since grown back to as many as 20, mostly as new advisories landed). On
173
173
  2026-10-24 (nest-server 12.0.0) two fresh resolves showed that 16 of the remaining 18 changed nothing,
174
- and they were removed: **two entries remain** (`@graphql-tools/utils`, `multer`), both on an upstream
175
- EXACT pin with an open advisory. An override is only necessary if removing it lets the package resolve
174
+ and they were removed, leaving `@graphql-tools/utils` and `multer`, both on an upstream EXACT pin with
175
+ an open advisory. Those two left on 2026-10-29 (nest-server 12.1.0), once `@nestjs/graphql` 14.1.0 and
176
+ `@nestjs/platform-express` 12.3.2 moved their pins past the fixes: **the set is empty** (`overrides: {}`,
177
+ written explicitly so `check:overrides` reads it as a decision). An override is only necessary if removing it lets the package resolve
176
178
  back INTO its vulnerable range (verify with a with/without lockfile diff; `pnpm audit` is the arbiter).
177
179
  "Cheap insurance against a parent re-pinning downwards" is not a reason to keep one: `pnpm audit` and
178
180
  `check:overrides` run on every `check` and report such a regression the day it happens. Each surviving entry carries its CVE rationale as a comment. Remove an entry once its parent dependency ships a fixed version.
@@ -337,6 +339,26 @@ on `FIX AVAILABLE` (a `first_patched_version` now exists) or on a withdrawn advi
337
339
  API is reported as `UNVERIFIED` and tolerated locally — but **fails under `CI`**, because a skip
338
340
  nobody reads is indistinguishable from a check that ran.
339
341
 
342
+ **A fix nobody named is still a fix.** GitHub leaves `first_patched_version` null on some advisories
343
+ after a release outside the vulnerable range shipped: GHSA-86w9-cpqp-85rv (node-forge) read
344
+ `<= 1.4.0` / null a day after 1.4.1 was published. Until 12.0.x the guard skipped such an advisory,
345
+ so its suppression passed every run. It now reads the package's packument from the registry pnpm
346
+ installs from and, per vulnerable copy in the lockfile, looks for a stable release above it that
347
+ lies outside **every** range the advisory lists. A deprecated release is held back, not ruled out: it
348
+ counts only when every such release is deprecated, because some packages deprecate all of them
349
+ (node-sass: 132 of 132 stable releases), and ruling them out reads a published fix as "no fix" for good.
350
+
351
+ | Finding | Verdict |
352
+ |---------|---------|
353
+ | every copy has such a release, past `minimumReleaseAge` | `FIX AVAILABLE`, naming it — `unusableFixConsumers` applies as for a named fix |
354
+ | one is published but younger than `minimumReleaseAge` | passes, printed as `pending` with the date the gate clears (pnpm would refuse it, so failing would order a `minimumReleaseAgeExclude` nobody decided on) |
355
+ | some copy has none | passes — the suppression is still justified |
356
+ | no version the lockfile resolves is in the range any more | `NOT IN TREE` — the entry hides nothing |
357
+ | registry or lockfile unreadable | `UNVERIFIED`, with the reason (fails under `CI`) |
358
+
359
+ `--advisory-file` runs stay offline: they take packuments from `--packument-file` and never ask the
360
+ registry.
361
+
340
362
  One consequence worth knowing before you reach for this mechanism: the guard fires when the advisory
341
363
  has a patched version for **any** affected range. For most findings that is the honest outcome — a
342
364
  fix exists, take it, and the case is an override problem rather than a suppression problem.
@@ -0,0 +1,100 @@
1
+ # Persistence Rules (MongoDB, PostgreSQL, MySQL and MariaDB)
2
+
3
+ These rules apply to every model, service and query — in `src/core/` and in projects. The design and its
4
+ reasoning are in `.claude/plans/db-agnostic/PLAN.md` (framework repository); which database a project uses is decided by
5
+ `docs/choosing-a-database.md`; the configuration reference is the "Data Sources" row of
6
+ `.claude/rules/configurable-features.md`.
7
+
8
+ ## One pipeline, one store per model
9
+
10
+ `CrudService` runs every call through the same security pipeline (`process()`) and hands the database work to the
11
+ model's **data store** (`DataStore<T>`, `common/persistence/data-store.interface.ts`): `MongooseDataStore` on MongoDB,
12
+ `SqlDataStore` on PostgreSQL, `MysqlDataStore` (the same policies, MySQL's statements) on MySQL and MariaDB. What else
13
+ differs per SQL database — schema, role check, leases — is the source's `SqlDialect` (`sql/sql-dialect.ts`). A model
14
+ reaches its store by being registered with
15
+ `CorePersistenceModule.forFeature([{ name, schema, model }])`; `database` routes it to a source.
16
+
17
+ | Do | Never |
18
+ |---|---|
19
+ | Register a model with `CorePersistenceModule.forFeature()` | `MongooseModule.forFeature()` for a model that may live in SQL — it stays on MongoDB and nothing routes it |
20
+ | Declare models with the framework's `Schema`, `Prop`, `SchemaFactory`, `InjectModel` (from `@lenne.tech/nest-server`, or `common/persistence/mongo/mongoose-compat.js` inside the core) | Import them from `@nestjs/mongoose` in the core — the core must load where MongoDB's libraries are not installed (`pnpm run check:without-mongodb`) |
21
+ | Reach a model through `this.dataStore` in a `CrudService`, or `@InjectStore(Model)` elsewhere | Call `this.mainDbModel.find(…)` — on a model routed to SQL the Mongoose model refuses every data method (`routed-model.guard.ts`) |
22
+ | Write ids as 24-hex strings (`generateId()`, `newObjectId()`, `isObjectIdLike()`) | `new Types.ObjectId()` / `instanceof Types.ObjectId` in the core — Mongoose may not be installed |
23
+ | Query with the Mongo-operator subset the SQL compiler supports (`$eq $ne $gt $gte $lt $lte $in $nin $exists $type $regex $not $and $or $nor $elemMatch $all $size`, dotted paths) | Build an aggregation pipeline for something the port offers (`count`, `sum`, `distinct`, `find` with `sort` / `skip` / `limit` / `sample`) |
24
+
25
+ ## MongoDB's libraries are optional peers (12.1.0+)
26
+
27
+ `mongoose`, `@nestjs/mongoose` and `mongodb` are loaded through `common/persistence/mongo/mongo-packages.ts` — never by
28
+ a static value import in `src/core/`. `import type` is fine (erased), and so is the compat module, which records each
29
+ declaration and applies the real decorator where Mongoose is installed.
30
+
31
+ | Need | Use |
32
+ |---|---|
33
+ | The packages, where a feature cannot work without them (GridFS, a MongoDB migration) | `requireMongoPackages('<feature, in configuration words>')` — throws `MongoPackagesMissingError` naming what to install |
34
+ | Whether they are there | `optionalMongoPackages()` / `hasMongoPackages()` |
35
+ | A GridFS bucket | `createGridFsBucket(db, { bucketName })` |
36
+ | A Mongoose query check | `isMongooseQuery(value)` |
37
+ | An inline schema (no class) | `createSchema(definition, options)`; free-form fields `{ type: Object }` |
38
+ | An ObjectId field type in a decorator | `objectIdType()` |
39
+
40
+ Where Mongoose is not installed, a model's descriptor comes from what its decorators recorded
41
+ (`mongo/model-declarations.ts`), read through `mongo/declared-schema.view.ts`, which builds paths and indexes exactly
42
+ as Mongoose would. `tests/unit/persistence-model-descriptor.spec.ts` proves both sources agree for every shipped
43
+ model — **a new model the framework ships goes into `SHIPPED_MODELS` there.**
44
+
45
+ `CoreModule` connects MongoDB only when a source uses it (`usesMongoDb()`); otherwise `NoMongoConnectionModule`
46
+ provides `null` for the connection token, and `@InjectConnection()` receives `null`. A service that uses the connection
47
+ on its MongoDB path checks it.
48
+
49
+ ## Native access in three tiers
50
+
51
+ | Tier | API | What still protects the data |
52
+ |---|---|---|
53
+ | 1 | `CrudService` / `DataStore` | everything: before-layer policies, database layer (RLS on PostgreSQL), tenant row check, output pipeline |
54
+ | 2 | `store.native(reason)` — the engine's own API, bound to the store's transaction | MongoDB: the Mongoose plugins. PostgreSQL: row-level security (same connection). MySQL / MariaDB: **nothing in the database**. HTTP interceptors |
55
+ | 3 | `store.raw(reason)` / `getNativeConnection(reason)` | PostgreSQL: row-level security. HTTP interceptors. Nothing else |
56
+
57
+ `reason` is mandatory and logged. Parameters only — an identifier (table, column) never comes from user input.
58
+
59
+ **MySQL and MariaDB: tenant isolation is the application's.** Neither database has row-level security, and a trigger
60
+ guard was measured and rejected (plan, phase 6). On these sources a tier-2/3 query on a tenant-scoped table MUST filter
61
+ by `tenantId` (and a write MUST set it) itself — a review treats a missing filter there as a cross-tenant leak, not a
62
+ style issue. The boot warns once per source.
63
+
64
+ ## High-frequency paths
65
+
66
+ The "use direct Mongoose in system-internal hot paths" advice in `CLAUDE.md` applies to MongoDB, where the Mongoose
67
+ plugins still fire on `Model.create()` / `findByIdAndUpdate()`. The database-neutral form of the same advice is the
68
+ data store: `this.dataStore.insert()`, `updateById()`, `findById()`, `pushToArray()` skip `process()` (no cloning, no
69
+ output pipeline) and keep every policy — on MongoDB through the plugins, on PostgreSQL in the store. Prefer the store in
70
+ new code; it is the fast path on both databases.
71
+
72
+ ## Schema on SQL
73
+
74
+ The framework creates and evolves tables from the model descriptors when a store is created, as the **owner** role:
75
+ new tables, columns, indexes and grants are applied; anything that could lose or reject data (type change, new unique
76
+ index, a rename collision, a table losing tenant isolation) stops the boot with the suggested statements — write a
77
+ migration (`migrate create <name> --sql`, `runSqlMigration()`). Declare a renamed field with
78
+ `@UnifiedField({ renamedFrom: 'oldName' })`. Never enable an engine's own `synchronize` or migration generator.
79
+
80
+ On MySQL / MariaDB a text field that is part of an index is `varchar(255)` (a longer value: 400), every other text
81
+ `longtext` — so adding an index to an existing text field is a type change. An index over more than 3072 bytes indexes a
82
+ prefix when it is a lookup index and is refused when unique. DDL commits on its own there: a run is serialized by
83
+ `GET_LOCK`, not a transaction, and a failed change leaves the statements before it applied.
84
+
85
+ ## Updates on MySQL / MariaDB
86
+
87
+ Two operators on one path, or on a path and a path inside it, are refused (`Updating the path … would create a
88
+ conflict`) — MongoDB's own rule; PostgreSQL chains them. Write such an update as one operator per path.
89
+
90
+ On every SQL database a path segment `__proto__`, `constructor` or `prototype` is refused: a compiler that builds an
91
+ object from path segments would otherwise reach `Object.prototype` — on MySQL that switched tenant isolation off for the
92
+ whole process. Never build an update path from request data without validating it against the model.
93
+
94
+ ## Tests
95
+
96
+ A suite proves a behaviour on both databases: the `e2e` vitest project runs it on MongoDB, the `postgres` project on
97
+ PostgreSQL (`.claude/rules/testing.md`). MySQL and MariaDB are proven through the matrices (persistence, core modules,
98
+ where parity) and their own suites, not by a third run of the whole e2e suite. Read and prepare data through `tests/helpers/iam-test-db.ts` or the
99
+ application's stores — never through a MongoDB client of the suite's own, which reads the wrong database on
100
+ PostgreSQL. A contract with several implementations is a matrix (`tests/helpers/persistence-matrix.ts`).
@@ -417,6 +417,15 @@ otherwise lets the role guard check `user.roles` — fail-closed, warned once. P
417
417
  **Configured is not registered.** When you add a guard that delegates to another one, ask whether
418
418
  the other one RUNS, not whether it is configured.
419
419
 
420
+ **Registered is not ordered (DEV-3830).** Global guards run in registration order, and `CoreModule` registers
421
+ `CoreTenantModule` ahead of a project's `AuthModule` — so with legacy JWTs (no middleware identifies them) the tenant
422
+ guard ran before the role guard, found nobody on the request and answered every tenant route with 401. Never noticed
423
+ because every multi-tenant suite signed in through IAM, where the middleware sets the user first. The tenant guard now
424
+ lets the application's role guard identify the caller when nobody is on the request yet (`ROLE_GUARD` token, provided by
425
+ the module that registers the role guard, resolved through `ModuleRef` so it is always THIS application's guard). The
426
+ role guard then runs twice on such a request; it is stateless, and its 401/403 is the answer either way. Pinned by
427
+ `tests/core-module-parity.e2e-spec.ts` (mutation `tenant-guard-before-identification`).
428
+
420
429
  ## Every entry point, not just the route (11.42.0)
421
430
 
422
431
  A `@Roles()` decides who may call a ROUTE. An operation is also reached from MCP tools, AI tools,
@@ -32,11 +32,11 @@ that every `*.spec.ts` / `*.test.ts` in the repo is claimed by **exactly one** r
32
32
  mis-named suite fails the build instead of silently passing. Type-only tests
33
33
  (`tests/types/*.type-test.ts`) are compiled by `pnpm run test:types`, never executed.
34
34
 
35
- ## Infrastructure containers (Redis + S3)
35
+ ## Infrastructure containers (Redis + S3 + PostgreSQL + MySQL + MariaDB)
36
36
 
37
- **Eight** e2e suites talk to a **real** Redis and/or a **real** S3-compatible store rather than a
38
- mock, and they **fail loudly** when it is missing — a silently skipped infrastructure test is how an
39
- untested driver ships:
37
+ **Twenty** e2e suites talk to a **real** Redis, a **real** S3-compatible store and/or a **real**
38
+ PostgreSQL, MySQL or MariaDB rather than a mock, and they **fail loudly** when it is missing — a silently skipped
39
+ infrastructure test is how an untested driver ships:
40
40
 
41
41
  | Suite | Needs |
42
42
  |-------|-------|
@@ -44,14 +44,29 @@ untested driver ships:
44
44
  | `tests/redis-consumers.e2e-spec.ts` | Redis |
45
45
  | `tests/multi-replica.e2e-spec.ts` | Redis |
46
46
  | `tests/s3-infra.e2e-spec.ts` | S3 |
47
- | `tests/file-storage-parity.e2e-spec.ts` | S3 |
48
- | `tests/file-storage-http-parity.e2e-spec.ts` | S3 |
47
+ | `tests/file-storage-parity.e2e-spec.ts` | S3 + PostgreSQL |
48
+ | `tests/file-storage-http-parity.e2e-spec.ts` | S3 + PostgreSQL |
49
49
  | `tests/redis-s3-bootstrap.e2e-spec.ts` | Redis + S3 |
50
50
  | `tests/graceful-shutdown.e2e-spec.ts` | Redis + S3 |
51
-
52
- `tests/global-setup.ts` starts both containers automatically, so `pnpm test` works from a clean
51
+ | `tests/persistence-sql-executor.e2e-spec.ts` | PostgreSQL |
52
+ | `tests/persistence-sql-where.e2e-spec.ts` | PostgreSQL + MySQL + MariaDB (and MongoDB) |
53
+ | `tests/persistence-sql-schema.e2e-spec.ts` | PostgreSQL |
54
+ | `tests/persistence-sql-store.e2e-spec.ts` | PostgreSQL |
55
+ | `tests/persistence-parity.e2e-spec.ts` | PostgreSQL + MySQL + MariaDB (and MongoDB) |
56
+ | `tests/persistence-lock-store.e2e-spec.ts` | PostgreSQL + MySQL + MariaDB (and MongoDB) |
57
+ | `tests/persistence-sql-wiring.e2e-spec.ts` | PostgreSQL (and MongoDB) |
58
+ | `tests/core-module-parity.e2e-spec.ts` | PostgreSQL + MySQL + MariaDB (and MongoDB) |
59
+ | `tests/migrate/sql-state-store.e2e-spec.ts` | PostgreSQL + MySQL + MariaDB |
60
+ | `tests/persistence-mysql-executor.e2e-spec.ts` | MySQL + MariaDB |
61
+ | `tests/persistence-mysql-schema.e2e-spec.ts` | MySQL + MariaDB |
62
+ | `tests/persistence-mysql-wiring.e2e-spec.ts` | MySQL + MariaDB |
63
+
64
+ Beyond these twenty, the `postgres` vitest project runs the whole e2e suite once more on PostgreSQL (see "Behaviour
65
+ matrices" below), so without the PostgreSQL container that project fails as a whole while `e2e` runs.
66
+
67
+ `tests/global-setup.ts` starts all five containers automatically, so `pnpm test` works from a clean
53
68
  machine with no manual docker command. It is idempotent (a running container is reused) and never
54
- fatal: without Docker every other suite still runs, and the eight above report their own actionable
69
+ fatal: without Docker every other suite still runs, and the twenty above report their own actionable
55
70
  error.
56
71
 
57
72
  ```bash
@@ -60,10 +75,28 @@ pnpm run test:infra:status # what is running
60
75
  pnpm run test:infra:down # stop and remove
61
76
  ```
62
77
 
63
- | | Redis | S3 (RustFS) |
64
- |---|---|---|
65
- | Port | 6380 (6379 is an auth-protected TurboOps Redis on lt dev machines) | 9102 |
66
- | Container | `nest-server-2985-redis` | `nest-server-2985-rustfs` |
78
+ | | Redis | S3 (RustFS) | PostgreSQL | MySQL 8.4 | MariaDB 11.8 |
79
+ |---|---|---|---|---|---|
80
+ | Port | 6380 (6379 is an auth-protected TurboOps Redis on lt dev machines) | 9102 | 5440 | 3316 | 3317 |
81
+ | Container | `nest-server-2985-redis` | `nest-server-2985-rustfs` | `nest-server-2985-postgres` | `nest-server-2985-mysql` | `nest-server-2985-mariadb` |
82
+
83
+ **PostgreSQL runs with two roles, as every environment does** (`.claude/plans/db-agnostic/PLAN.md`
84
+ §3.9): `nest_owner` owns the databases and their schema, `nest_app` owns nothing and has neither
85
+ superuser nor `BYPASSRLS` — so a suite running as the application role meets row-level security the
86
+ way production does. The readiness step creates `nest_app`, which is why the container is started
87
+ through `pnpm run test:infra` (or the CI action) and not by a bare `docker run`. Each suite creates
88
+ its own database through `tests/helpers/postgres-test-db.ts`, named after the run
89
+ (`deriveTestDbUri()`), refused unless it passes the drop guard, and dropped in `afterAll`. Overridable
90
+ via `PG_HOST`, `PG_PORT`, `PG_OWNER_USER`, `PG_OWNER_PASSWORD`, `PG_APP_USER`, `PG_APP_PASSWORD`.
91
+
92
+ **MySQL and MariaDB run with two accounts the same way** (phase 6): `nest_owner` may do anything,
93
+ `nest_app` holds nothing until `tests/helpers/mysql-test-db.ts` creates a suite's database and grants it
94
+ `SELECT, INSERT, UPDATE, DELETE` there — and revokes that grant before the drop, because a database-level
95
+ grant outlives its database. Database names follow the PostgreSQL scheme and pass the same drop guard.
96
+ Overridable via `MYSQL_HOST`, `MYSQL_PORT`, `MARIADB_HOST`, `MARIADB_PORT`, `MYSQL_OWNER_USER`,
97
+ `MYSQL_OWNER_PASSWORD`, `MYSQL_APP_USER`, `MYSQL_APP_PASSWORD`. MySQL and MariaDB are not a third run of the
98
+ whole e2e suite: the matrices carry their columns (persistence: one per engine; core modules and where
99
+ parity: one per server).
67
100
 
68
101
  **CI provisions its own** containers in `.github/workflows/*.yml` and sets `LT_TEST_INFRA=0` so the
69
102
  automatic start stays out of the way. Same images, same ports — only who starts them differs. Set
@@ -212,6 +245,70 @@ exactly that (`getRawFileInfoByName()` consulted two of the three stores), and e
212
245
  | Route-contract executor (boots the real `ServerModule` per driver) | `tests/file-storage-http-parity.e2e-spec.ts` |
213
246
  | Structural guard | `tests/unit/file-storage-parity-matrix.spec.ts` |
214
247
 
248
+ **It has a database dimension (DEV-3832).** S3 and filesystem metadata follow `database` to a SQL table, so a column
249
+ is a driver WITH its metadata database: `s3` / `filesystem` / `gridfs` (MongoDB, the names they always had) plus
250
+ `s3@postgres` and `filesystem@postgres`, which run every case. `gridfs@postgres` is declared `IMPOSSIBLE` case by case
251
+ — GridFS keeps bytes and metadata together in MongoDB — and the guard refuses a case that runs for a driver on MongoDB
252
+ but not on PostgreSQL. The PostgreSQL environment routes the two metadata models through a real `DataSourceRuntime`
253
+ (`routeFileMetadataToPostgres()`); the HTTP executor installs that runtime before it compiles `ServerModule`, as
254
+ `CoreModule.forRoot()` would. **The harness reads and plants through the stores, never through
255
+ `fileMetadataCollection()`**: a harness using the routing it checks follows a broken routing to MongoDB and stays green
256
+ (mutation `file-metadata-routing-ignored` turns 22 cells red only because of that).
257
+
258
+ The **core modules** follow it one level up: `tests/helpers/core-module-matrix.ts` declares what the framework's own
259
+ modules answer through REST and GraphQL — users and legacy sign-in, tenant memberships and the tenant guard, API
260
+ tokens, health check, Hub panels — and `tests/core-module-parity.e2e-spec.ts` assembles them like the reference server
261
+ once with MongoDB alone and once with PostgreSQL as the default source (default engine; the persistence matrix covers
262
+ the engines). `tests/unit/core-module-parity-matrix.spec.ts` is its structural guard. The PostgreSQL column signs in
263
+ through the legacy endpoints with `betterAuth: false`, so it proves the legacy-auth path on SQL; IAM on SQL is the job of
264
+ the projects below.
265
+
266
+ The **whole e2e suite runs on both databases** (12.1.0+): the `e2e` project on MongoDB, the `postgres` project again
267
+ with PostgreSQL as the default data source (`POSTGRES_E2E_ENGINE`, the default engine TypeORM). Each test file gets a
268
+ PostgreSQL database of its own (`tests/postgres-file.setup.ts`, created before the file's first import and dropped in
269
+ its `afterAll`): two suites with different `multiTenancy` sharing one database stop each other's boot, because a table
270
+ that loses tenant isolation refuses to start. A `describe` whose configuration differs from the rest of its file — or
271
+ that needs an empty user base, as the system setup does — takes one more of its own through
272
+ `ownPostgresDatabase()` / `onOwnPostgresDatabase()` (`tests/helpers/postgres-test-db.ts`). The project runs every e2e
273
+ file except those in `POSTGRES_E2E_EXCLUDED` (`vitest.include-globs.mts`), each with one of two reasons: `MONGODB-ONLY:`
274
+ (the suite checks a MongoDB mechanism against MongoDB itself, and names where its PostgreSQL counterpart is proven) or
275
+ `OWN-DATABASES:` (a parity matrix or a suite that creates its PostgreSQL databases itself). An entry is a claim about the
276
+ suite, never a way to get a run green: a suite that fails on PostgreSQL because it reads MongoDB directly belongs on
277
+ the neutral helpers. `tests/unit/postgres-e2e-coverage.spec.ts` keeps the list honest (existing suites, one of the two
278
+ reasons, an upper bound on its size).
279
+
280
+ A suite stays database-neutral through three rules. It reads and prepares users, sessions, accounts and reset tokens
281
+ through `tests/helpers/iam-test-db.ts` — `openIamTestDb(uri, { users: 'mongodb' })` for an application built on
282
+ `CoreModule.forRoot()` alone, which registers no user model and therefore keeps its users on MongoDB. It reads a model
283
+ the application registered with `CorePersistenceModule.forFeature()` through that model's store
284
+ (`app.get(getStoreToken(name), { strict: false })`, inside `RequestContext.runWithBypassTenantGuard()` for a
285
+ tenant-scoped one) — never through a MongoDB client, which would read the wrong database on PostgreSQL. And it keeps
286
+ what is genuinely MongoDB's on MongoDB (`iamDb.mongo`: GridFS, a model registered with `MongooseModule.forFeature()`
287
+ alone). Where a behaviour differs by design — PostgreSQL's CHECK constraint refuses a stored system role that MongoDB
288
+ lets a raw insert plant — the case asserts what happens instead, per `iamDb.driver`.
289
+
290
+ The **IAM suites** run on the two other engines as well, through vitest projects in `vitest-e2e.config.mts`:
291
+ `iam-sql-kysely` and `iam-sql-drizzle` (`IAM_SQL_ENGINES`), each over the files in `IAM_SQL_TEST_INCLUDE`
292
+ (`vitest.include-globs.mts`: the Better-Auth stories, privilege escalation, sign-in CSRF, migration status, the storage
293
+ paths, the bidirectional sync, and since phase 4 the AI suite and both MCP OAuth suites). A project's global setup (`tests/iam-sql.global-setup.ts`) creates its PostgreSQL
294
+ database per run and drops it afterwards; its setup file (`tests/iam-sql.setup.ts`) points `NSC__DATABASE__URL`,
295
+ `NSC__DATABASE__OWNER_URL` and `NSC__DATABASE__SQL_ENGINE` at it, so the unchanged `ServerModule` boots with its users —
296
+ and Better-Auth with them — on PostgreSQL. The suites read and prepare state through `tests/helpers/iam-test-db.ts`,
297
+ which answers from whichever database the run routes the users to (camelCase fields, string ids, a column never written
298
+ left out rather than `null`), never through a MongoDB client of their own; the AI suite does the same through
299
+ `tests/helpers/ai-test-stores.ts` (the stores the application routes the AI models to, `system()` for fixtures that span
300
+ tenants). `tests/ai-mcp-oauth-tenant.e2e-spec.ts` boots with `multiTenancy` and therefore creates a PostgreSQL database
301
+ of its own in those projects: a table that loses tenant isolation stops the boot of a suite configured without it, so
302
+ two suites with different `multiTenancy` must never share one. Keep the root config free of `include`: with
303
+ `extends: true` it is merged into every project, and the IAM projects would run the whole suite. vitest runs the global
304
+ setup of EVERY project before any test, and the project setups before the root one — so a throw in
305
+ `tests/iam-sql.global-setup.ts` used to stop the whole run, MongoDB project included, before the root setup could start
306
+ the containers. The IAM setup therefore starts the test infrastructure itself (once per run) and never throws: a
307
+ database it still cannot create (no Docker) makes every suite of that project fail in `tests/iam-sql.setup.ts` with the
308
+ reason, never fall back to MongoDB, while the `e2e` project runs. `--project e2e` runs MongoDB alone.
309
+
310
+ The **data store** contract follows the same discipline: `tests/helpers/persistence-matrix.ts` declares the cases (CRUD contract plus the security policies), `tests/persistence-parity.e2e-spec.ts` executes them against a store obtained via `CorePersistenceModule.forFeature()` + `@InjectStore()` with every security plugin registered, and `tests/unit/persistence-parity-matrix.spec.ts` guards that every (case, database) cell is declared. It has ten columns: `mongodb`, PostgreSQL once per engine (`postgres-typeorm`, `postgres-kysely`, `postgres-drizzle`) — a `SqlDataStore` over the schema the migrator created, connected as the application role, so row-level security binds every case — and MySQL and MariaDB once per engine (`mysql-*`, `mariadb-*`, phase 6), a `MysqlDataStore` connected as the DML-only application account. Without row-level security there, `native-api-tenant-refused` is DIFFERENT-BY-DESIGN for those six columns and its complement asserts that a native query reaches every tenant while the store still isolates. Each tenant layer of the SQL store is additionally proven ALONE in `tests/persistence-sql-store.e2e-spec.ts` — the matrix runs them together, where one layer hides the loss of another.
311
+
215
312
  ### A cell is EXECUTED, IMPOSSIBLE, or DIFFERENT-BY-DESIGN — never merely absent
216
313
 
217
314
  Omitting a case for one implementation and declaring it impossible there produce the same artefact
@@ -273,7 +370,7 @@ pnpm run check:mutations -- --id=<id> # one mutation
273
370
  pnpm run check:mutations -- --list # the registry, without running anything
274
371
  pnpm run check:mutations -- --allow-dirty # when the fix and its evidence share a working tree
275
372
  pnpm run check:mutations -- --jobs=4 # N mutations at a time (default: 2, or 4 on >=12 cores)
276
- pnpm run check:mutations -- --no-infra # only the 185 that need no MongoDB
373
+ pnpm run check:mutations -- --no-infra # only the 222 that need no MongoDB
277
374
  pnpm run check:mutations -- --since=<ref> # only mutations touching files changed since <ref>
278
375
  ```
279
376
 
@@ -292,7 +389,7 @@ when the registry, a vitest config or a setup file changed, since those can move
292
389
  commit, and in CI alongside the publish without blocking it. The price of caching is a cache that
293
390
  has to model each spec's full dependency closure correctly, and getting that wrong produces a stale
294
391
  PASS for a test that has since gone vacuous — exactly what the gate is there to prevent. Bad trade
295
- at 274 mutations.
392
+ at 407 mutations.
296
393
 
297
394
  **Decided 2026-10-03 (repo owner): keep the full run.** Measured at 128 mutations, the CI job for
298
395
  11.41.6 took **14 minutes**. The trigger to revisit is that wall time, not the mutation count —
@@ -337,9 +434,9 @@ exactly the environment where the answer matters.
337
434
 
338
435
  ### The cost is vitest's cold start, not the tests
339
436
 
340
- Worth knowing before optimising the wrong thing: the specs behind all 89 e2e mutations add up to
437
+ Worth knowing before optimising the wrong thing: the specs behind all 185 e2e mutations add up to
341
438
  **~40 seconds**. The step takes ~740s. The remaining ~700s is paying vitest's startup — process
342
- spawn, transform, module graph, mongod connect, DB create and drop — once per mutation, 274 times.
439
+ spawn, transform, module graph, mongod connect, DB create and drop — once per mutation, 407 times.
343
440
  That work is largely single-threaded I/O and barely scales with cores: the full registry measures
344
441
  **744s on a 12-core laptop and 777s on a 4-vCPU CI runner**.
345
442
 
@@ -735,11 +832,12 @@ catches the *failure*, never the *disarming* of the safety property.
735
832
  |-----------|-------|
736
833
  | DI tokens live in import-free leaves (SWC/TDZ) | `tests/unit/import-cycle-invariants.spec.ts` |
737
834
  | Internal calls forward the caller's `ServiceOptions` | `tests/unit/service-options-forwarding.spec.ts` |
738
- | The parity matrix is complete and honest | `tests/unit/file-storage-parity-matrix.spec.ts` |
835
+ | The parity matrix is complete and honest | `tests/unit/file-storage-parity-matrix.spec.ts`, `tests/unit/persistence-parity-matrix.spec.ts`, `tests/unit/core-module-parity-matrix.spec.ts` |
739
836
  | Regression tests carry re-runnable evidence | `tests/unit/regression-evidence.spec.ts` |
740
837
  | The core is valid as CommonJS AND ESM (no `require()`, `__dirname`, `import x = require()`, `import.meta`, relative dynamic `import()`) | `tests/unit/module-format-invariants.spec.ts`; `pnpm run check:esm` proves the loading half |
741
838
  | The ESM package stays loadable by `require()` (no import from a CommonJS directory into the graph, no top-level `await`) and its CommonJS islands (`bin/`, `migrate/helpers/`) load in place | `tests/unit/module-format-invariants.spec.ts`, `tests/unit/package-format-contracts.spec.ts`; `pnpm run check:manifest` repeats it against the packed build |
742
839
  | Type-aware lint is armed (flag, `oxlint-tsgolint`, `no-floating-promises: error`) | `tests/unit/lint-type-aware.spec.ts` |
840
+ | The core needs no MongoDB library (12.1.0+): no static value import of `mongoose` / `@nestjs/mongoose` / `mongodb`, both model-description sources agree | `pnpm run check:without-mongodb` (dist, packages unresolvable; `:package` installs the tarball without them); `tests/unit/persistence-model-descriptor.spec.ts`; `tests/unit/import-cycle-invariants.spec.ts` (the compat leaf) |
743
841
 
744
842
  `service-options-forwarding` is 11.33.1 generalised: inside `src/core/`, a method that HAS the
745
843
  caller's context must pass it to every internal call that accepts one. Passing a local derived from