@lenne.tech/nest-server 12.1.1 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/.claude/rules/architecture.md +1 -0
  2. package/.claude/rules/better-auth.md +31 -4
  3. package/.claude/rules/configurable-features.md +8 -6
  4. package/.claude/rules/package-management.md +29 -2
  5. package/.claude/rules/persistence.md +27 -1
  6. package/.claude/rules/testing.md +80 -15
  7. package/.claude/rules/versioning.md +13 -2
  8. package/CLAUDE.md +7 -1
  9. package/FRAMEWORK-API.md +20 -4
  10. package/README.md +20 -2
  11. package/dist/config.env.js +2 -2
  12. package/dist/config.env.js.map +1 -1
  13. package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
  14. package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
  15. package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
  16. package/dist/core/common/interfaces/server-options.interface.d.ts +49 -0
  17. package/dist/core/common/interfaces/service-options.interface.d.ts +3 -0
  18. package/dist/core/common/persistence/core-persistence.module.js +12 -2
  19. package/dist/core/common/persistence/core-persistence.module.js.map +1 -1
  20. package/dist/core/common/persistence/data-source.runtime.js +4 -2
  21. package/dist/core/common/persistence/data-source.runtime.js.map +1 -1
  22. package/dist/core/common/persistence/data-store.interface.d.ts +24 -9
  23. package/dist/core/common/persistence/id-format.d.ts +2 -0
  24. package/dist/core/common/persistence/id-format.js +14 -0
  25. package/dist/core/common/persistence/id-format.js.map +1 -1
  26. package/dist/core/common/persistence/model-descriptor.interface.d.ts +6 -0
  27. package/dist/core/common/persistence/model-metadata.registry.d.ts +2 -1
  28. package/dist/core/common/persistence/model-metadata.registry.js +20 -2
  29. package/dist/core/common/persistence/model-metadata.registry.js.map +1 -1
  30. package/dist/core/common/persistence/mongo/sync-schema.plugin.d.ts +18 -0
  31. package/dist/core/common/persistence/mongo/sync-schema.plugin.js +368 -0
  32. package/dist/core/common/persistence/mongo/sync-schema.plugin.js.map +1 -0
  33. package/dist/core/common/persistence/mongoose-data-store.d.ts +19 -9
  34. package/dist/core/common/persistence/mongoose-data-store.js +107 -31
  35. package/dist/core/common/persistence/mongoose-data-store.js.map +1 -1
  36. package/dist/core/common/persistence/persisted-model.decorator.d.ts +3 -0
  37. package/dist/core/common/persistence/persisted-model.decorator.js +9 -0
  38. package/dist/core/common/persistence/persisted-model.decorator.js.map +1 -1
  39. package/dist/core/common/persistence/policies/sync.policy.d.ts +17 -0
  40. package/dist/core/common/persistence/policies/sync.policy.js +77 -0
  41. package/dist/core/common/persistence/policies/sync.policy.js.map +1 -0
  42. package/dist/core/common/persistence/sql/mysql/mysql-data-store.d.ts +12 -8
  43. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js +82 -29
  44. package/dist/core/common/persistence/sql/mysql/mysql-data-store.js.map +1 -1
  45. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.d.ts +10 -0
  46. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js +61 -4
  47. package/dist/core/common/persistence/sql/mysql/mysql-schema.compiler.js.map +1 -1
  48. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js +101 -4
  49. package/dist/core/common/persistence/sql/mysql/mysql-schema.migrator.js.map +1 -1
  50. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.d.ts +10 -0
  51. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js +41 -2
  52. package/dist/core/common/persistence/sql/postgres/postgres-schema.compiler.js.map +1 -1
  53. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js +98 -10
  54. package/dist/core/common/persistence/sql/postgres/postgres-schema.migrator.js.map +1 -1
  55. package/dist/core/common/persistence/sql/sql-data-store.d.ts +38 -14
  56. package/dist/core/common/persistence/sql/sql-data-store.js +212 -38
  57. package/dist/core/common/persistence/sql/sql-data-store.js.map +1 -1
  58. package/dist/core/common/persistence/sql/sql-errors.d.ts +2 -1
  59. package/dist/core/common/persistence/sql/sql-errors.js +3 -1
  60. package/dist/core/common/persistence/sql/sql-errors.js.map +1 -1
  61. package/dist/core/common/persistence/sync-access.registry.d.ts +17 -0
  62. package/dist/core/common/persistence/sync-access.registry.js +8 -0
  63. package/dist/core/common/persistence/sync-access.registry.js.map +1 -0
  64. package/dist/core/common/persistence/sync-model.d.ts +29 -0
  65. package/dist/core/common/persistence/sync-model.js +138 -0
  66. package/dist/core/common/persistence/sync-model.js.map +1 -0
  67. package/dist/core/common/services/crud.service.d.ts +13 -1
  68. package/dist/core/common/services/crud.service.js +174 -16
  69. package/dist/core/common/services/crud.service.js.map +1 -1
  70. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  71. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  72. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  73. package/dist/core/modules/ai/core-ai.constants.d.ts +2 -0
  74. package/dist/core/modules/ai/core-ai.constants.js +2 -0
  75. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  76. package/dist/core/modules/ai/core-ai.controller.d.ts +6 -0
  77. package/dist/core/modules/ai/core-ai.controller.js +31 -1
  78. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  79. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  80. package/dist/core/modules/ai/core-ai.module.js +8 -1
  81. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  82. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  83. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  84. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  85. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  86. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  87. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  88. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  89. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  90. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  91. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  92. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  93. package/dist/core/modules/ai/index.d.ts +7 -0
  94. package/dist/core/modules/ai/index.js +6 -0
  95. package/dist/core/modules/ai/index.js.map +1 -1
  96. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  97. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  98. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  99. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  100. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  101. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  102. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  103. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  104. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  105. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  106. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  107. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  108. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  109. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  110. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  111. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  112. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  113. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  114. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  115. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  116. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  117. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  118. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  119. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  120. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  121. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  122. package/dist/core/modules/better-auth/better-auth.config.js +3 -1
  123. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  124. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  125. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  126. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  127. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  128. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  129. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  130. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  131. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  132. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  133. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  134. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  135. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  136. package/dist/core/modules/better-auth/index.d.ts +1 -0
  137. package/dist/core/modules/better-auth/index.js +1 -0
  138. package/dist/core/modules/better-auth/index.js.map +1 -1
  139. package/dist/core/modules/error-code/error-codes.d.ts +72 -0
  140. package/dist/core/modules/error-code/error-codes.js +64 -0
  141. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  142. package/dist/core/modules/migrate/sql-migration.helper.d.ts +15 -1
  143. package/dist/core/modules/migrate/sql-migration.helper.js +90 -5
  144. package/dist/core/modules/migrate/sql-migration.helper.js.map +1 -1
  145. package/dist/core/modules/sync/core-sync-base.controller.d.ts +7 -0
  146. package/dist/core/modules/sync/core-sync-base.controller.js +44 -0
  147. package/dist/core/modules/sync/core-sync-base.controller.js.map +1 -0
  148. package/dist/core/modules/sync/core-sync-cursor.d.ts +19 -0
  149. package/dist/core/modules/sync/core-sync-cursor.js +77 -0
  150. package/dist/core/modules/sync/core-sync-cursor.js.map +1 -0
  151. package/dist/core/modules/sync/core-sync-hints.controller.d.ts +4 -0
  152. package/dist/core/modules/sync/core-sync-hints.controller.js +55 -0
  153. package/dist/core/modules/sync/core-sync-hints.controller.js.map +1 -0
  154. package/dist/core/modules/sync/core-sync-hints.service.d.ts +28 -0
  155. package/dist/core/modules/sync/core-sync-hints.service.js +155 -0
  156. package/dist/core/modules/sync/core-sync-hints.service.js.map +1 -0
  157. package/dist/core/modules/sync/core-sync-infrastructure.models.d.ts +2 -0
  158. package/dist/core/modules/sync/core-sync-infrastructure.models.js +17 -0
  159. package/dist/core/modules/sync/core-sync-infrastructure.models.js.map +1 -0
  160. package/dist/core/modules/sync/core-sync-mutation.store.d.ts +43 -0
  161. package/dist/core/modules/sync/core-sync-mutation.store.js +88 -0
  162. package/dist/core/modules/sync/core-sync-mutation.store.js.map +1 -0
  163. package/dist/core/modules/sync/core-sync-state.store.d.ts +18 -0
  164. package/dist/core/modules/sync/core-sync-state.store.js +64 -0
  165. package/dist/core/modules/sync/core-sync-state.store.js.map +1 -0
  166. package/dist/core/modules/sync/core-sync.constants.d.ts +28 -0
  167. package/dist/core/modules/sync/core-sync.constants.js +23 -0
  168. package/dist/core/modules/sync/core-sync.constants.js.map +1 -0
  169. package/dist/core/modules/sync/core-sync.controller.d.ts +8 -0
  170. package/dist/core/modules/sync/core-sync.controller.js +79 -0
  171. package/dist/core/modules/sync/core-sync.controller.js.map +1 -0
  172. package/dist/core/modules/sync/core-sync.decorators.d.ts +12 -0
  173. package/dist/core/modules/sync/core-sync.decorators.js +28 -0
  174. package/dist/core/modules/sync/core-sync.decorators.js.map +1 -0
  175. package/dist/core/modules/sync/core-sync.module.d.ts +16 -0
  176. package/dist/core/modules/sync/core-sync.module.js +50 -0
  177. package/dist/core/modules/sync/core-sync.module.js.map +1 -0
  178. package/dist/core/modules/sync/core-sync.resolver.d.ts +9 -0
  179. package/dist/core/modules/sync/core-sync.resolver.js +46 -0
  180. package/dist/core/modules/sync/core-sync.resolver.js.map +1 -0
  181. package/dist/core/modules/sync/core-sync.service.d.ts +143 -0
  182. package/dist/core/modules/sync/core-sync.service.js +1237 -0
  183. package/dist/core/modules/sync/core-sync.service.js.map +1 -0
  184. package/dist/core/modules/sync/interfaces/core-sync.interfaces.d.ts +56 -0
  185. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js +2 -0
  186. package/dist/core/modules/sync/interfaces/core-sync.interfaces.js.map +1 -0
  187. package/dist/core/modules/sync/models/core-sync-hint.model.d.ts +4 -0
  188. package/dist/core/modules/sync/models/core-sync-hint.model.js +30 -0
  189. package/dist/core/modules/sync/models/core-sync-hint.model.js.map +1 -0
  190. package/dist/core/modules/tenant/core-tenant.guard.js +14 -0
  191. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  192. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  193. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  194. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  195. package/dist/core.module.js +14 -0
  196. package/dist/core.module.js.map +1 -1
  197. package/dist/index.d.ts +16 -0
  198. package/dist/index.js +16 -0
  199. package/dist/index.js.map +1 -1
  200. package/dist/server/modules/error-code/error-codes.d.ts +8 -0
  201. package/dist/server/modules/sync-note/inputs/sync-note-create.input.d.ts +4 -0
  202. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js +27 -0
  203. package/dist/server/modules/sync-note/inputs/sync-note-create.input.js.map +1 -0
  204. package/dist/server/modules/sync-note/inputs/sync-note.input.d.ts +6 -0
  205. package/dist/server/modules/sync-note/inputs/sync-note.input.js +37 -0
  206. package/dist/server/modules/sync-note/inputs/sync-note.input.js.map +1 -0
  207. package/dist/server/modules/sync-note/sync-note.model.d.ts +10 -0
  208. package/dist/server/modules/sync-note/sync-note.model.js +73 -0
  209. package/dist/server/modules/sync-note/sync-note.model.js.map +1 -0
  210. package/dist/server/modules/sync-note/sync-note.module.d.ts +2 -0
  211. package/dist/server/modules/sync-note/sync-note.module.js +21 -0
  212. package/dist/server/modules/sync-note/sync-note.module.js.map +1 -0
  213. package/dist/server/modules/sync-note/sync-note.service.d.ts +10 -0
  214. package/dist/server/modules/sync-note/sync-note.service.js +35 -0
  215. package/dist/server/modules/sync-note/sync-note.service.js.map +1 -0
  216. package/dist/test/test.helper.d.ts +1 -0
  217. package/dist/test/test.helper.js +9 -2
  218. package/dist/test/test.helper.js.map +1 -1
  219. package/docs/REQUEST-LIFECYCLE.md +4 -2
  220. package/docs/error-codes.md +14 -0
  221. package/docs/offline-sync-protocol.md +111 -68
  222. package/docs/security-overrides.md +14 -0
  223. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  224. package/migration-guides/12.1.x-to-12.2.x.md +154 -0
  225. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  226. package/package.json +23 -21
  227. package/src/config.env.ts +6 -2
  228. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  229. package/src/core/common/interfaces/server-options.interface.ts +229 -8
  230. package/src/core/common/interfaces/service-options.interface.ts +15 -0
  231. package/src/core/common/persistence/core-persistence.module.ts +21 -2
  232. package/src/core/common/persistence/data-source.runtime.ts +5 -2
  233. package/src/core/common/persistence/data-store.interface.ts +65 -12
  234. package/src/core/common/persistence/id-format.ts +26 -0
  235. package/src/core/common/persistence/model-descriptor.interface.ts +12 -0
  236. package/src/core/common/persistence/model-metadata.registry.ts +31 -2
  237. package/src/core/common/persistence/mongo/sync-schema.plugin.ts +508 -0
  238. package/src/core/common/persistence/mongoose-data-store.ts +171 -35
  239. package/src/core/common/persistence/persisted-model.decorator.ts +26 -0
  240. package/src/core/common/persistence/policies/sync.policy.ts +121 -0
  241. package/src/core/common/persistence/sql/mysql/mysql-data-store.ts +108 -30
  242. package/src/core/common/persistence/sql/mysql/mysql-schema.compiler.ts +107 -4
  243. package/src/core/common/persistence/sql/mysql/mysql-schema.migrator.ts +129 -3
  244. package/src/core/common/persistence/sql/postgres/postgres-schema.compiler.ts +74 -2
  245. package/src/core/common/persistence/sql/postgres/postgres-schema.migrator.ts +139 -7
  246. package/src/core/common/persistence/sql/sql-data-store.ts +325 -44
  247. package/src/core/common/persistence/sql/sql-errors.ts +5 -1
  248. package/src/core/common/persistence/sync-access.registry.ts +50 -0
  249. package/src/core/common/persistence/sync-model.ts +270 -0
  250. package/src/core/common/services/crud.service.ts +257 -19
  251. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  252. package/src/core/modules/ai/README.md +163 -25
  253. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  254. package/src/core/modules/ai/core-ai.constants.ts +6 -0
  255. package/src/core/modules/ai/core-ai.controller.ts +36 -1
  256. package/src/core/modules/ai/core-ai.module.ts +12 -0
  257. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  258. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  259. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  260. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  261. package/src/core/modules/ai/index.ts +18 -0
  262. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  263. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  264. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  265. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  266. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  267. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  268. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  269. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  270. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  271. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  272. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  273. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  274. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  275. package/src/core/modules/api-token/README.md +4 -2
  276. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  277. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +7 -0
  278. package/src/core/modules/better-auth/README.md +32 -1
  279. package/src/core/modules/better-auth/better-auth.config.ts +12 -1
  280. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  281. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  282. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  283. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  284. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  285. package/src/core/modules/better-auth/index.ts +1 -0
  286. package/src/core/modules/error-code/error-codes.ts +77 -0
  287. package/src/core/modules/migrate/sql-migration.helper.ts +141 -7
  288. package/src/core/modules/sync/INTEGRATION-CHECKLIST.md +95 -0
  289. package/src/core/modules/sync/README.md +198 -0
  290. package/src/core/modules/sync/SECURITY.md +65 -0
  291. package/src/core/modules/sync/core-sync-base.controller.ts +43 -0
  292. package/src/core/modules/sync/core-sync-cursor.ts +117 -0
  293. package/src/core/modules/sync/core-sync-hints.controller.ts +52 -0
  294. package/src/core/modules/sync/core-sync-hints.service.ts +178 -0
  295. package/src/core/modules/sync/core-sync-infrastructure.models.ts +40 -0
  296. package/src/core/modules/sync/core-sync-mutation.store.ts +146 -0
  297. package/src/core/modules/sync/core-sync-state.store.ts +89 -0
  298. package/src/core/modules/sync/core-sync.constants.ts +51 -0
  299. package/src/core/modules/sync/core-sync.controller.ts +68 -0
  300. package/src/core/modules/sync/core-sync.decorators.ts +84 -0
  301. package/src/core/modules/sync/core-sync.module.ts +75 -0
  302. package/src/core/modules/sync/core-sync.resolver.ts +35 -0
  303. package/src/core/modules/sync/core-sync.service.ts +1773 -0
  304. package/src/core/modules/sync/interfaces/core-sync.interfaces.ts +85 -0
  305. package/src/core/modules/sync/models/core-sync-hint.model.ts +18 -0
  306. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  307. package/src/core/modules/tenant/README.md +49 -3
  308. package/src/core/modules/tenant/core-tenant.guard.ts +26 -2
  309. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  310. package/src/core.module.ts +25 -0
  311. package/src/index.ts +21 -0
  312. package/src/server/modules/sync-note/inputs/sync-note-create.input.ts +14 -0
  313. package/src/server/modules/sync-note/inputs/sync-note.input.ts +23 -0
  314. package/src/server/modules/sync-note/sync-note.model.ts +77 -0
  315. package/src/server/modules/sync-note/sync-note.module.ts +16 -0
  316. package/src/server/modules/sync-note/sync-note.service.ts +25 -0
  317. package/src/test/test.helper.ts +17 -3
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable } from '@nestjs/common';
1
+ import { Inject, Injectable, Logger } from '@nestjs/common';
2
2
  import { InjectConnection, InjectModel } from '../../../common/persistence/mongo/mongoose-compat.js';
3
3
  import type { Connection, Model } from 'mongoose';
4
4
 
@@ -41,6 +41,8 @@ export class CoreAiConversationService extends CrudService<
41
41
  CoreAiConversationCreateInput,
42
42
  CoreAiConversationInput
43
43
  > {
44
+ protected readonly logger = new Logger(CoreAiConversationService.name);
45
+
44
46
  /** Maximum number of messages retained per conversation (capped on `$push`). */
45
47
  protected readonly maxRetainedMessages = 500;
46
48
 
@@ -155,12 +157,35 @@ export class CoreAiConversationService extends CrudService<
155
157
  }
156
158
 
157
159
  /**
158
- * Append a message to a conversation via `$push` (system-internal; the user
159
- * already authorized the prompt that produced it). The array is capped at
160
- * {@link maxRetainedMessages} via `$slice` so a long-lived conversation cannot
161
- * grow unbounded.
160
+ * Whether the conversation exists and was created by `userId`. Admins are not owners: they may READ any
161
+ * conversation (`securityCheck`), but a prompt only ever continues the caller's own one, because its turns
162
+ * become part of that conversation's next runs.
162
163
  */
163
- async appendMessage(id: string, message: { content: string; createdAt?: Date; role: string }): Promise<void> {
164
+ async isOwnedBy(id: string, userId: string | undefined): Promise<boolean> {
165
+ if (!isWellFormedId(id) || !isWellFormedId(userId)) {
166
+ return false;
167
+ }
168
+ return !!(await this.dataStore.findOne({ createdBy: userId, id }, { select: ['id'] }));
169
+ }
170
+
171
+ /**
172
+ * Append a message to the conversation of `ownerId` via `$push` (system-internal; the user already authorized
173
+ * the prompt that produced it). Writes nothing unless `ownerId` created the conversation: a conversation's turns
174
+ * feed its owner's next runs, in which tools act with the owner's rights, so a write on anybody else's behalf
175
+ * would plant prompt input into another user's assistant. The check sits here rather than in each caller so that
176
+ * no caller can leave it out. `createdBy` is never changed after creation, so nothing can happen between the
177
+ * check and the push that would make the write foreign. The array is capped at {@link maxRetainedMessages} via
178
+ * `$slice` so a long-lived conversation cannot grow unbounded.
179
+ */
180
+ async appendMessage(
181
+ id: string,
182
+ message: { content: string; createdAt?: Date; role: string },
183
+ ownerId: string | undefined,
184
+ ): Promise<void> {
185
+ if (!(await this.isOwnedBy(id, ownerId))) {
186
+ this.logger.warn(`Message not appended: conversation ${id} is not owned by user ${ownerId ?? '(none)'}`);
187
+ return;
188
+ }
164
189
  await this.dataStore.pushToArray(id, 'messages', [{ ...message, createdAt: message.createdAt ?? new Date() }], {
165
190
  slice: -this.maxRetainedMessages,
166
191
  });
@@ -597,8 +597,10 @@ export class CoreAiMcpOAuthService implements OnModuleInit {
597
597
  * - signed in, GET → the consent view: which client, where the code goes, Allow / Deny, with a
598
598
  * CSRF token bound to user, client, redirect URI and PKCE challenge.
599
599
  * - signed in, POST with Allow and a valid token → the code is issued, the browser goes to
600
- * `redirect_uri?code=…&state=…`. Deny → `access_denied`. A stale token shows the form again; a
601
- * foreign or tampered one too, and issues nothing.
600
+ * `redirect_uri?code=…&state=…`. Deny with a valid token → `access_denied`. A stale token shows the
601
+ * form again; a foreign or tampered one too, and decides nothing.
602
+ * - an error before this method runs (a malformed request) is answered with 400, never sent to the
603
+ * client's redirect URI — `mountAiMcpOAuthRouter()` guards `/authorize` (RFC 9700 §4.11.2).
602
604
  *
603
605
  * The signed-in user is whoever the Better-Auth middleware put on the request (session cookie or
604
606
  * bearer). An API token never gets here — `buildOAuthProvider().authorize` refuses it first — and a
@@ -615,20 +617,21 @@ export class CoreAiMcpOAuthService implements OnModuleInit {
615
617
  const user = await this.consentUser(req);
616
618
  const decision = req?.method === 'POST' ? req.body?.[MCP_OAUTH_CONSENT_DECISION_FIELD] : undefined;
617
619
 
618
- if (user && decision === 'deny') {
619
- const { AccessDeniedError } = await this.loadOAuthErrors();
620
- throw new AccessDeniedError('The user denied access');
621
- }
622
-
623
620
  let notice: IMcpOAuthConsentView['notice'];
624
- if (user && decision === 'approve') {
621
+ if (user && (decision === 'approve' || decision === 'deny')) {
625
622
  const binding: IAiMcpOAuthConsentBinding = {
626
623
  clientId: String(client.client_id),
627
624
  codeChallenge: String(params.codeChallenge),
628
625
  redirectUri: String(params.redirectUri),
629
626
  userId: user.id,
630
627
  };
628
+ // Deny needs the token as Allow does: it sends the browser to the client's redirect URI, and without the token a
629
+ // cross-site POST could do that for a signed-in user whose session cookie is `SameSite=None`
631
630
  const verdict = this.verifyConsentToken(req.body?.[MCP_OAUTH_CONSENT_TOKEN_FIELD], binding);
631
+ if (verdict === 'valid' && decision === 'deny') {
632
+ const { AccessDeniedError } = await this.loadOAuthErrors();
633
+ throw new AccessDeniedError('The user denied access');
634
+ }
632
635
  if (verdict === 'valid') {
633
636
  const code = this.generateId();
634
637
  await this.saveAuthorizationCode(code, {
@@ -649,7 +652,7 @@ export class CoreAiMcpOAuthService implements OnModuleInit {
649
652
  } else {
650
653
  this.logger.warn(
651
654
  `MCP OAuth consent for client ${binding.clientId} carried an invalid token (forged, tampered, or from ` +
652
- 'another session); no code issued, the form is shown again.',
655
+ 'another session); neither approved nor denied, the form is shown again.',
653
656
  );
654
657
  }
655
658
  }
@@ -1,12 +1,30 @@
1
- import { Injectable, Logger } from '@nestjs/common';
1
+ import { Injectable, Logger, Optional } from '@nestjs/common';
2
2
 
3
- import { AiToolContext } from '../interfaces/ai-tool.interface.js';
3
+ import { RequestContext } from '../../../common/services/request-context.service.js';
4
+ import {
5
+ authorizeToolCall,
6
+ effectiveMcpConfirmation,
7
+ mcpConfirmationPolicy,
8
+ toolRequiresConfirmation,
9
+ } from '../helpers/ai-tool-authorization.helper.js';
10
+ import { AiToolContext, IAiTool } from '../interfaces/ai-tool.interface.js';
4
11
  import { AiToolRegistry, AiToolUser } from '../tools/ai-tool.registry.js';
12
+ import type { AiToolPolicyDecision } from './core-ai-tool-policy.service.js';
13
+ import { CoreAiToolPolicyService } from './core-ai-tool-policy.service.js';
14
+ import { CoreAiUserSettingService } from './core-ai-user-setting.service.js';
5
15
 
6
16
  /**
7
- * MCP tool descriptor (name + description + JSON-schema input).
17
+ * MCP tool descriptor (name + description + JSON-schema input + behaviour hints).
8
18
  */
9
19
  export interface McpToolDescriptor {
20
+ /**
21
+ * MCP tool annotations for a tool that declares what it does: `destructive` → `destructiveHint: true`,
22
+ * `mutating` → `destructiveHint: false`, both with `readOnlyHint: false`. Absent for a tool with neither flag:
23
+ * such a tool may still write (the flags used to matter for the chat only), so it is never advertised as
24
+ * read-only and clients keep the spec's cautious defaults. Hints for the client's own confirmation UI, never a
25
+ * security boundary — the server enforces `authorize()` and its confirmation gate itself.
26
+ */
27
+ annotations?: { destructiveHint: boolean; readOnlyHint: boolean };
10
28
  description: string;
11
29
  inputSchema: Record<string, any>;
12
30
  name: string;
@@ -20,6 +38,30 @@ export interface McpCallResult {
20
38
  isError?: boolean;
21
39
  }
22
40
 
41
+ /**
42
+ * The question put to the user before a tool that needs confirmation runs over MCP.
43
+ */
44
+ export interface McpConfirmationPrompt {
45
+ /** The arguments the tool will run with. */
46
+ arguments: Record<string, any>;
47
+ /** The text the MCP client shows the user. */
48
+ message: string;
49
+ /** The tool's name. */
50
+ tool: string;
51
+ }
52
+
53
+ /**
54
+ * Options of {@link CoreAiMcpService.mcpCallTool}.
55
+ */
56
+ export interface McpCallToolOptions {
57
+ /**
58
+ * Asks the user to confirm the call; resolves `true` only when they did. `createServer()` passes one that uses MCP
59
+ * elicitation when the client supports it. Absent: the client cannot ask, so a call that needs confirmation is
60
+ * refused.
61
+ */
62
+ confirm?: (prompt: McpConfirmationPrompt) => Promise<boolean>;
63
+ }
64
+
23
65
  /**
24
66
  * Exposes the {@link AiToolRegistry} as a Model Context Protocol (MCP) server so
25
67
  * external MCP clients (e.g. desktop MCP clients) can use the same backend tools with
@@ -29,6 +71,11 @@ export interface McpCallResult {
29
71
  * `tools/list` and `tools/call` are filtered to that user's permitted tools and
30
72
  * executed with their permissions (via each tool's `execute()` → CrudService).
31
73
  *
74
+ * Since 12.3.0 `tools/call` also applies the gates of the chat orchestrator: the admin's tool policies (`deny`
75
+ * refuses, `ask` asks), the tool's `authorize()`, and the confirmation of a `destructive` tool (or a `mutating` one
76
+ * under `ai.confirmation.mutating`) through MCP elicitation. Whether MCP asks is `ai.mcp.confirmation` (on by default),
77
+ * which each user may switch for themselves (`PUT /ai/settings`) unless the server enforces it.
78
+ *
32
79
  * The `@modelcontextprotocol/sdk` is imported lazily in {@link createServer} so it
33
80
  * is only loaded when MCP is actually enabled — keeping the core lean.
34
81
  */
@@ -36,40 +83,75 @@ export interface McpCallResult {
36
83
  export class CoreAiMcpService {
37
84
  protected readonly logger = new Logger(CoreAiMcpService.name);
38
85
 
39
- constructor(protected readonly toolRegistry: AiToolRegistry) {}
86
+ /** How long the server waits for the user's answer to a confirmation request. */
87
+ protected readonly confirmationTimeoutMs = 5 * 60_000;
88
+
89
+ constructor(
90
+ protected readonly toolRegistry: AiToolRegistry,
91
+ @Optional() protected readonly userSettingService?: CoreAiUserSettingService,
92
+ @Optional() protected readonly toolPolicyService?: CoreAiToolPolicyService,
93
+ ) {}
40
94
 
41
95
  /**
42
96
  * List the MCP tool descriptors the user is allowed to use.
43
97
  */
44
98
  mcpListTools(user: AiToolUser | null | undefined): McpToolDescriptor[] {
45
- return this.toolRegistry
46
- .forUser(user)
47
- .map((tool) => ({ description: tool.description, inputSchema: tool.parameters, name: tool.name }));
99
+ return this.toolRegistry.forUser(user).map((tool) => ({
100
+ ...(tool.destructive || tool.mutating
101
+ ? { annotations: { destructiveHint: tool.destructive === true, readOnlyHint: false } }
102
+ : {}),
103
+ description: tool.description,
104
+ inputSchema: tool.parameters,
105
+ name: tool.name,
106
+ }));
48
107
  }
49
108
 
50
109
  /**
51
110
  * Execute an MCP tool call with the user's permissions. Returns an MCP content
52
- * result; unknown/forbidden tools and execution errors yield `isError: true`.
111
+ * result; unknown/forbidden tools, a refused `authorize()`, a missing confirmation and
112
+ * execution errors yield `isError: true`.
53
113
  */
54
114
  async mcpCallTool(
55
115
  user: AiToolUser | null | undefined,
56
116
  name: string,
57
117
  args: Record<string, any>,
118
+ options: McpCallToolOptions = {},
58
119
  ): Promise<McpCallResult> {
59
120
  const tool = this.toolRegistry.forUser(user).find((t) => t.name === name);
60
121
  if (!tool) {
61
- return { content: [{ text: `Unknown or not permitted tool: ${name}`, type: 'text' }], isError: true };
122
+ return this.errorResult(`Unknown or not permitted tool: ${name}`);
62
123
  }
124
+ const callArgs = args ?? {};
63
125
  const context: AiToolContext = {
64
126
  currentUser: (user ?? undefined) as AiToolContext['currentUser'],
65
127
  serviceOptions: { currentUser: (user ?? undefined) as AiToolContext['currentUser'] },
66
128
  };
129
+
130
+ const policy = await this.evaluateToolPolicy(tool, callArgs, user);
131
+ if (policy?.decision === 'deny') {
132
+ return this.errorResult(policy.reason || `"${name}" is not permitted by policy.`);
133
+ }
134
+ const authorization = await authorizeToolCall(tool, callArgs, context);
135
+ if (!authorization.allowed) {
136
+ return this.errorResult(authorization.reason || `Not permitted: ${name}`);
137
+ }
138
+ if (policy?.decision === 'ask' || (await this.confirmationRequired(tool, user))) {
139
+ if (!options.confirm) {
140
+ return this.errorResult(
141
+ `"${name}" needs the user's confirmation, and this MCP client cannot ask for it (elicitation). Nothing was run.`,
142
+ );
143
+ }
144
+ if (!(await this.askForConfirmation(tool, callArgs, options.confirm))) {
145
+ return this.errorResult(`"${name}" was not run: the user did not confirm it.`);
146
+ }
147
+ }
148
+
67
149
  try {
68
- const result = await tool.execute(args ?? {}, context);
150
+ const result = await tool.execute(callArgs, tool.authorize ? { ...context, authorization } : context);
69
151
  return { content: [{ text: JSON.stringify(result), type: 'text' }] };
70
152
  } catch (err) {
71
153
  this.logger.warn(`MCP tool "${name}" failed: ${(err as Error).message}`);
72
- return { content: [{ text: (err as Error).message, type: 'text' }], isError: true };
154
+ return this.errorResult((err as Error).message);
73
155
  }
74
156
  }
75
157
 
@@ -91,10 +173,121 @@ export class CoreAiMcpService {
91
173
  const server: any = new Server({ name: 'lt-nest-server-ai', version: '1.0.0' }, { capabilities: { tools: {} } });
92
174
 
93
175
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.mcpListTools(currentUser()) }));
94
- server.setRequestHandler(CallToolRequestSchema, async (request: any) =>
95
- this.mcpCallTool(currentUser(), request.params.name, request.params.arguments ?? {}),
176
+ server.setRequestHandler(CallToolRequestSchema, async (request: any, extra: any) =>
177
+ this.mcpCallTool(currentUser(), request.params.name, request.params.arguments ?? {}, {
178
+ // The SDK reads an empty `elicitation: {}` as form elicitation, the kind a yes/no question needs.
179
+ confirm: server.getClientCapabilities()?.elicitation?.form
180
+ ? (prompt: McpConfirmationPrompt) => this.elicitConfirmation(server, prompt, extra?.requestId)
181
+ : undefined,
182
+ }),
96
183
  );
97
184
 
98
185
  return server;
99
186
  }
187
+
188
+ /**
189
+ * Whether this call must be confirmed by the user: a tool that needs confirmation ({@link toolRequiresConfirmation},
190
+ * with `ai.confirmation.mutating`'s default for `mutating`), while MCP asks this user — `ai.mcp.confirmation`, or the
191
+ * user's own choice where the server allows one. An admin's `ask` policy is decided before and asks regardless.
192
+ */
193
+ protected async confirmationRequired(tool: IAiTool, user: AiToolUser | null | undefined): Promise<boolean> {
194
+ if (!toolRequiresConfirmation(tool)) {
195
+ return false;
196
+ }
197
+ if (this.userSettingService) {
198
+ return this.userSettingService.mcpConfirmationFor(user?.id);
199
+ }
200
+ return effectiveMcpConfirmation(mcpConfirmationPolicy());
201
+ }
202
+
203
+ /**
204
+ * The admin's tool policy for this call (`aiToolPolicies`), as the chat orchestrator evaluates it. `undefined`
205
+ * without the policy service, without a matching rule, or when the evaluation fails (logged).
206
+ */
207
+ protected async evaluateToolPolicy(
208
+ tool: IAiTool,
209
+ args: Record<string, any>,
210
+ user: AiToolUser | null | undefined,
211
+ ): Promise<AiToolPolicyDecision | undefined> {
212
+ if (!this.toolPolicyService) {
213
+ return undefined;
214
+ }
215
+ try {
216
+ return await this.toolPolicyService.evaluate(tool.name, args, {
217
+ roles: (user?.roles as string[]) ?? [],
218
+ tenantId: RequestContext.getTenantId(),
219
+ userId: user?.id,
220
+ });
221
+ } catch (err) {
222
+ this.logger.warn(`AI tool-policy evaluation for "${tool.name}" failed: ${(err as Error).message}`);
223
+ return undefined;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * The question shown to the user. English, because an MCP request carries no language; override to localize.
229
+ *
230
+ * Every argument gets a line of its own and only its value is shortened: the arguments come from the model, and
231
+ * a cut over the whole text would let a long first value push the one that decides the outcome (the target id)
232
+ * out of the question the user approves.
233
+ */
234
+ protected confirmationPrompt(tool: IAiTool, args: Record<string, any>): McpConfirmationPrompt {
235
+ const maxValueLength = 200;
236
+ const lines = Object.entries(args).map(([key, value]) => {
237
+ const text = typeof value === 'string' ? value : (JSON.stringify(value) ?? String(value));
238
+ const shown =
239
+ text.length > maxValueLength ? `${text.slice(0, maxValueLength)}… (${text.length} characters)` : text;
240
+ return `- ${key}: ${shown}`;
241
+ });
242
+ const consequence = tool.destructive ? 'This cannot be undone.' : 'This changes data.';
243
+ const argumentsText = lines.length ? `\nArguments:\n${lines.join('\n')}` : '';
244
+ return {
245
+ arguments: args,
246
+ message: `Allow the AI assistant to run "${tool.name}"? ${consequence}${argumentsText}`,
247
+ tool: tool.name,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Puts the question to the user. Anything but an explicit yes — a no, a cancelled dialog, an error, a timeout —
253
+ * counts as "not confirmed".
254
+ */
255
+ protected async askForConfirmation(
256
+ tool: IAiTool,
257
+ args: Record<string, any>,
258
+ confirm: NonNullable<McpCallToolOptions['confirm']>,
259
+ ): Promise<boolean> {
260
+ try {
261
+ return (await confirm(this.confirmationPrompt(tool, args))) === true;
262
+ } catch (err) {
263
+ this.logger.warn(`Confirmation of MCP tool "${tool.name}" failed, not run: ${(err as Error).message}`);
264
+ return false;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Asks through MCP form elicitation, as part of the running `tools/call` request (`relatedRequestId`), so a
270
+ * Streamable HTTP client receives the question on that request's stream.
271
+ */
272
+ protected async elicitConfirmation(server: any, prompt: McpConfirmationPrompt, requestId: unknown): Promise<boolean> {
273
+ const result = await server.elicitInput(
274
+ {
275
+ message: prompt.message,
276
+ requestedSchema: {
277
+ properties: {
278
+ confirm: { description: `Run "${prompt.tool}" now`, title: 'Confirm', type: 'boolean' },
279
+ },
280
+ required: ['confirm'],
281
+ type: 'object',
282
+ },
283
+ },
284
+ { relatedRequestId: requestId, timeout: this.confirmationTimeoutMs },
285
+ );
286
+ return result?.action === 'accept' && result.content?.confirm === true;
287
+ }
288
+
289
+ /** An MCP error result with one text block. */
290
+ protected errorResult(text: string): McpCallResult {
291
+ return { content: [{ text, type: 'text' }], isError: true };
292
+ }
100
293
  }
@@ -221,12 +221,19 @@ export class CoreAiPromptBuilderService {
221
221
  : '\n\n[Schemas deferred. Call `search_tools` with the tool name to fetch its parameter schema BEFORE you call it.]';
222
222
  }
223
223
  }
224
- const toolCatalog = defer
225
- ? (tools.map((t) => `- ${t.name}: ${this.summarizeToolDescription(t.description, summaryChars)}`).join('\n') ||
226
- '(none)') + deferNote
227
- : tools
228
- .map((t) => `- ${t.name}: ${t.description}\n parameters (JSON schema): ${JSON.stringify(t.parameters)}`)
229
- .join('\n') || '(none)';
224
+ // A native connection receives every description and schema in the tool payload (`buildToolSchemas()`), so its
225
+ // catalog lists names only (12.3.0) — repeating them here paid for each tool twice. Emulated connections and plan
226
+ // mode have no tool payload; their catalog is the model's only source. `ai.nativeToolCatalog: 'full'` restores
227
+ // the full catalog for a model that picks tools better with it.
228
+ const namesOnly = supportsNativeTools && !planMode && ConfigService.get<string>('ai.nativeToolCatalog') !== 'full';
229
+ const toolCatalog = namesOnly
230
+ ? tools.map((t) => `- ${t.name}`).join('\n') || '(none)'
231
+ : defer
232
+ ? (tools.map((t) => `- ${t.name}: ${this.summarizeToolDescription(t.description, summaryChars)}`).join('\n') ||
233
+ '(none)') + deferNote
234
+ : tools
235
+ .map((t) => `- ${t.name}: ${t.description}\n parameters (JSON schema): ${JSON.stringify(t.parameters)}`)
236
+ .join('\n') || '(none)';
230
237
 
231
238
  if (this.placeholderRegistry) {
232
239
  return this.placeholderRegistry.resolveAll({ toolCatalog, tools, user });
@@ -0,0 +1,85 @@
1
+ import { ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
2
+ import { InjectModel } from '../../../common/persistence/mongo/mongoose-compat.js';
3
+ import type { Model } from 'mongoose';
4
+
5
+ import { upsertRecord } from '../../../common/persistence/store-upsert.js';
6
+ import { CrudService } from '../../../common/services/crud.service.js';
7
+ import type { CoreModelConstructor } from '../../../common/types/core-model-constructor.type.js';
8
+ import { ErrorCode } from '../../error-code/index.js';
9
+ import { effectiveMcpConfirmation, mcpConfirmationPolicy } from '../helpers/ai-tool-authorization.helper.js';
10
+ import type { CoreAiSettingsInput } from '../inputs/core-ai-settings.input.js';
11
+ import { CoreAiSettings } from '../models/core-ai-settings.model.js';
12
+ import { AiUserSettingDocument, CoreAiUserSetting } from '../models/core-ai-user-setting.model.js';
13
+
14
+ import { AI_USER_SETTING_CLASS, AI_USER_SETTING_MODEL } from '../core-ai.constants.js';
15
+
16
+ /**
17
+ * Personal AI settings (12.3.0+): what a user chose for themselves, combined with the server configuration into the
18
+ * settings that take effect. Today that is whether MCP asks the user before a tool that needs confirmation
19
+ * (`ai.mcp.confirmation`) — the user's own "bypass permissions" switch, unless the server enforces it.
20
+ *
21
+ * Reads and writes go through the routed store with the user id the caller passes; the endpoints pass the current
22
+ * user's, so nobody reaches another user's settings.
23
+ *
24
+ * Override via `CoreModule.forRoot(env, { ai: { userSettingService } })`.
25
+ */
26
+ @Injectable()
27
+ export class CoreAiUserSettingService extends CrudService<CoreAiUserSetting, CoreAiUserSetting, CoreAiUserSetting> {
28
+ protected readonly logger = new Logger(CoreAiUserSettingService.name);
29
+
30
+ constructor(
31
+ @InjectModel(AI_USER_SETTING_MODEL) protected override readonly mainDbModel: Model<AiUserSettingDocument>,
32
+ @Inject(AI_USER_SETTING_CLASS)
33
+ protected override readonly mainModelConstructor: CoreModelConstructor<CoreAiUserSetting>,
34
+ ) {
35
+ super();
36
+ }
37
+
38
+ /**
39
+ * The user's settings as they take effect.
40
+ */
41
+ async getSettings(userId: string | undefined): Promise<CoreAiSettings> {
42
+ const policy = mcpConfirmationPolicy();
43
+ const stored = userId ? await this.findForUser(userId) : undefined;
44
+ const settings = new CoreAiSettings();
45
+ settings.mcpConfirmation = effectiveMcpConfirmation(policy, stored?.mcpConfirmation);
46
+ settings.mcpConfirmationEnforced = policy.enforced;
47
+ return settings;
48
+ }
49
+
50
+ /**
51
+ * Stores the user's choices and answers with the settings that take effect. A choice the server enforces is
52
+ * refused with 403 (`AI_SETTING_ENFORCED`) rather than stored and ignored.
53
+ */
54
+ async updateSettings(userId: string, input: CoreAiSettingsInput): Promise<CoreAiSettings> {
55
+ if (typeof input?.mcpConfirmation === 'boolean') {
56
+ if (mcpConfirmationPolicy().enforced) {
57
+ throw new ForbiddenException(ErrorCode.AI_SETTING_ENFORCED);
58
+ }
59
+ await upsertRecord(this.dataStore, { userId }, { mcpConfirmation: input.mcpConfirmation });
60
+ }
61
+ return this.getSettings(userId);
62
+ }
63
+
64
+ /**
65
+ * Whether MCP asks this user before a tool that needs confirmation. When the stored choice cannot be read, it asks
66
+ * — unless the policy is enforced, which needs no stored choice.
67
+ */
68
+ async mcpConfirmationFor(userId: string | undefined): Promise<boolean> {
69
+ const policy = mcpConfirmationPolicy();
70
+ if (policy.enforced || !userId) {
71
+ return policy.default;
72
+ }
73
+ try {
74
+ return effectiveMcpConfirmation(policy, (await this.findForUser(userId))?.mcpConfirmation);
75
+ } catch (err) {
76
+ this.logger.warn(`AI settings of user ${userId} could not be read, MCP asks: ${(err as Error).message}`);
77
+ return true;
78
+ }
79
+ }
80
+
81
+ /** The stored record of one user, if any. */
82
+ protected async findForUser(userId: string): Promise<CoreAiUserSetting | undefined> {
83
+ return ((await this.dataStore.findOne({ userId })) as CoreAiUserSetting | null) ?? undefined;
84
+ }
85
+ }