@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,3 +1,4 @@
1
+ import { Inject } from '@nestjs/common';
1
2
  import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
2
3
 
3
4
  import { FilterArgs } from '../../common/args/filter.args.js';
@@ -19,6 +20,7 @@ import { CoreAiPromptUpdateInput } from './inputs/core-ai-prompt-update.input.js
19
20
  import { CoreAiSlotCreateInput } from './inputs/core-ai-slot-create.input.js';
20
21
  import { CoreAiSlotUpdateInput } from './inputs/core-ai-slot-update.input.js';
21
22
  import { CoreAiPromptInput } from './inputs/core-ai-prompt.input.js';
23
+ import { CoreAiSettingsInput } from './inputs/core-ai-settings.input.js';
22
24
  import { CoreAiAvailableConnection } from './models/core-ai-available-connection.model.js';
23
25
  import { CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
24
26
  import { CoreAiConnectionPreference } from './models/core-ai-connection-preference.model.js';
@@ -29,6 +31,7 @@ import { CoreAiPromptHint } from './models/core-ai-prompt-hint.model.js';
29
31
  import { CoreAiPrompt } from './models/core-ai-prompt.model.js';
30
32
  import { CoreAiSlot } from './models/core-ai-slot.model.js';
31
33
  import { CoreAiResponse } from './models/core-ai-response.model.js';
34
+ import { CoreAiSettings } from './models/core-ai-settings.model.js';
32
35
  import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
33
36
  import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
34
37
  import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
@@ -40,6 +43,7 @@ import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.
40
43
  import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
41
44
  import { CoreAiSlotService } from './services/core-ai-slot.service.js';
42
45
  import { CoreAiService } from './services/core-ai.service.js';
46
+ import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
43
47
 
44
48
  /**
45
49
  * GraphQL resolver for AI prompts and connection management.
@@ -67,6 +71,13 @@ export class CoreAiResolver {
67
71
  protected readonly promptService: CoreAiPromptService,
68
72
  ) {}
69
73
 
74
+ /**
75
+ * Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
76
+ * written before it keeps compiling and still gets the settings endpoints.
77
+ */
78
+ @Inject(CoreAiUserSettingService)
79
+ protected readonly userSettingService: CoreAiUserSettingService;
80
+
70
81
  // ===================================================================================================================
71
82
  // Prompt
72
83
  // ===================================================================================================================
@@ -348,6 +359,31 @@ export class CoreAiResolver {
348
359
  return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
349
360
  }
350
361
 
362
+ // ===================================================================================================================
363
+ // Personal settings
364
+ // ===================================================================================================================
365
+
366
+ /**
367
+ * The current user's AI settings as they take effect.
368
+ */
369
+ @Query(() => CoreAiSettings, { description: "The current user's effective AI settings" })
370
+ @Roles(RoleEnum.S_USER)
371
+ async aiSettings(@GraphQLServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
372
+ return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
373
+ }
374
+
375
+ /**
376
+ * Change the current user's own AI settings (403 for a setting the server enforces).
377
+ */
378
+ @Mutation(() => CoreAiSettings, { description: "Change the current user's own AI settings" })
379
+ @Roles(RoleEnum.S_USER)
380
+ async updateAiSettings(
381
+ @GraphQLServiceOptions() serviceOptions: ServiceOptions,
382
+ @Args('input') input: CoreAiSettingsInput,
383
+ ): Promise<CoreAiSettings> {
384
+ return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
385
+ }
386
+
351
387
  /**
352
388
  * Create an AI budget limit (admin).
353
389
  */
@@ -125,9 +125,31 @@ export function mcpOAuthConsentHeaders(nonce: string): Record<string, string> {
125
125
  };
126
126
  }
127
127
 
128
+ /** Every text of the page in one locale. */
129
+ export type McpOAuthConsentTexts = (typeof TEXTS)[McpOAuthConsentLocale];
130
+
131
+ /**
132
+ * Project adjustments for {@link renderMcpOAuthConsentPage}: replacement texts per locale (merged over
133
+ * the built-in ones, so a project changes only what it needs, e.g. its form of address) and CSS
134
+ * appended after the built-in styles (its colours and font). Neither touches the markup, the scripts or
135
+ * the security headers — what makes the page safe stays the framework's.
136
+ *
137
+ * Fill both from code or configuration, never from user or tenant input. Texts are escaped like the
138
+ * built-in ones; styles cannot leave their `<style>` element (every `<` becomes the CSS escape `\3c `), and
139
+ * the page's CSP blocks what CSS could load from elsewhere (`img-src 'self' data:`, no font source, no
140
+ * `@import` without the nonce) — but CSS still decides what the page shows.
141
+ */
142
+ export interface IMcpOAuthConsentPageOptions {
143
+ styles?: string;
144
+ texts?: Partial<Record<McpOAuthConsentLocale, Partial<McpOAuthConsentTexts>>>;
145
+ }
146
+
128
147
  /** Renders the page. Every dynamic value goes through {@link escapeHtml}. */
129
- export function renderMcpOAuthConsentPage(view: IMcpOAuthConsentView): string {
130
- const t = TEXTS[view.locale];
148
+ export function renderMcpOAuthConsentPage(
149
+ view: IMcpOAuthConsentView,
150
+ options: IMcpOAuthConsentPageOptions = {},
151
+ ): string {
152
+ const t: Texts = { ...TEXTS[view.locale], ...options.texts?.[view.locale] };
131
153
  const clientName = view.client.name?.trim() || t.unnamedClient;
132
154
  // One pass: a client name is chosen by whoever registered the client, so a name containing
133
155
  // `{app}` must not be expanded by a second replace.
@@ -146,7 +168,7 @@ export function renderMcpOAuthConsentPage(view: IMcpOAuthConsentView): string {
146
168
  <meta name="robots" content="noindex">
147
169
  <link rel="icon" href="data:,">
148
170
  <title>${escapeHtml(t.title)} – ${escapeHtml(view.appHost)}</title>
149
- <style nonce="${escapeHtml(view.nonce)}">${STYLES}</style>
171
+ <style nonce="${escapeHtml(view.nonce)}">${STYLES}${confinedStyles(options.styles)}</style>
150
172
  </head>
151
173
  <body>
152
174
  <main class="card">
@@ -158,7 +180,16 @@ ${pageScript(view)}
158
180
  `;
159
181
  }
160
182
 
161
- type Texts = (typeof TEXTS)[McpOAuthConsentLocale];
183
+ type Texts = McpOAuthConsentTexts;
184
+
185
+ /**
186
+ * Project CSS that cannot end the `<style>` element: a `</style>` in it would continue as markup — an injected
187
+ * form, for one, since the page sets no `form-action`. `<` has no meaning in CSS outside strings and comments, and
188
+ * the CSS escape `\3c ` keeps it where it does.
189
+ */
190
+ function confinedStyles(styles: string | undefined): string {
191
+ return typeof styles === 'string' ? styles.replace(/</g, '\\3c ') : '';
192
+ }
162
193
 
163
194
  function consentView(view: IMcpOAuthConsentView, t: Texts, fill: (text: string) => string): string {
164
195
  const target = view.redirect.loopback
@@ -122,6 +122,7 @@ async function mountRouter(
122
122
  const { getOAuthProtectedResourceMetadataUrl, mcpAuthRouter } =
123
123
  await import('@modelcontextprotocol/sdk/server/auth/router.js');
124
124
  const provider = oauthService.buildOAuthProvider() as any;
125
+ markAuthorizeReached(provider);
125
126
 
126
127
  let router: unknown;
127
128
  try {
@@ -136,7 +137,7 @@ async function mountRouter(
136
137
  });
137
138
  }
138
139
 
139
- use(router);
140
+ use(guardErrorRedirects(router as (req: any, res: any, next: (error?: unknown) => void) => void));
140
141
  const mounted: IMountedMcpOAuth = {
141
142
  authBasePath: options.authBasePath,
142
143
  baseUrl: baseUrl.replace(/\/$/, ''),
@@ -153,6 +154,72 @@ async function mountRouter(
153
154
  return mounted;
154
155
  }
155
156
 
157
+ /** `res.locals` key: the SDK's authorize handler handed this request to the provider */
158
+ const AUTHORIZE_REACHED = Symbol('ltMcpOAuthAuthorizeReached');
159
+
160
+ /** Marks every request the provider's `authorize` receives, so {@link refuseUnvouchedErrorRedirects} lets its redirects pass */
161
+ function markAuthorizeReached(provider: { authorize?: (...args: any[]) => Promise<void> }): void {
162
+ const authorize = provider.authorize;
163
+ if (typeof authorize !== 'function') {
164
+ return;
165
+ }
166
+ provider.authorize = (client: unknown, params: unknown, res: any) => {
167
+ if (res) {
168
+ res.locals = res.locals ?? {};
169
+ res.locals[AUTHORIZE_REACHED] = true;
170
+ }
171
+ return authorize.call(provider, client, params, res);
172
+ };
173
+ }
174
+
175
+ /**
176
+ * The SDK router behind a guard on `/authorize`: an error the SDK finds after the client check (a missing
177
+ * `code_challenge`, a wrong `response_type`) is answered here, never sent to the client's redirect URI. Dynamic client
178
+ * registration takes any redirect URI — it must, for claude.ai, ChatGPT, Cursor and Claude Code — so that 302 made
179
+ * `/authorize` an open redirect on this domain for whoever registered a client (RFC 9700 §4.11.2). The answer has the
180
+ * shape of the SDK's own errors before the client check. What the provider does keeps its redirects: the signed-in
181
+ * user's Allow or Deny, which carry the consent form's token, and the provider's own refusals. One handler, so the
182
+ * router is still mounted with a single `use()`.
183
+ */
184
+ function guardErrorRedirects(
185
+ router: (req: any, res: any, next: (error?: unknown) => void) => void,
186
+ ): (req: any, res: any, next: (error?: unknown) => void) => void {
187
+ return (req, res, next) => {
188
+ if (
189
+ String(req.path ?? '')
190
+ .toLowerCase()
191
+ .replace(/\/+$/, '') === '/authorize'
192
+ ) {
193
+ refuseUnvouchedErrorRedirects(res);
194
+ }
195
+ router(req, res, next);
196
+ };
197
+ }
198
+
199
+ /** Answers an error redirect of the SDK directly unless the request reached the provider's `authorize` */
200
+ function refuseUnvouchedErrorRedirects(res: any): void {
201
+ const redirect = res.redirect.bind(res);
202
+ res.redirect = (...args: unknown[]) => {
203
+ const target = typeof args[0] === 'number' ? args[1] : args[0];
204
+ let error: null | string = null;
205
+ let description: null | string = null;
206
+ try {
207
+ const url = new URL(String(target));
208
+ error = url.searchParams.get('error');
209
+ description = url.searchParams.get('error_description');
210
+ } catch {
211
+ // Not an absolute URL: nothing the SDK's error redirect would produce
212
+ }
213
+ if (error && !res.locals?.[AUTHORIZE_REACHED]) {
214
+ res
215
+ .status(error === 'server_error' ? 500 : 400)
216
+ .json({ error, ...(description ? { error_description: description } : {}) });
217
+ return;
218
+ }
219
+ return redirect(...args);
220
+ };
221
+ }
222
+
156
223
  /** The Express instance behind a Nest application, or the object itself for any other shape. */
157
224
  function mountTargetOf(app: IAiMcpOAuthMountTarget): object {
158
225
  const instance = app.getHttpAdapter?.()?.getInstance?.();
@@ -0,0 +1,99 @@
1
+ import { Logger } from '@nestjs/common';
2
+
3
+ import { ConfigService } from '../../../common/services/config.service.js';
4
+ import type { AiToolAuthorization, AiToolContext, IAiTool } from '../interfaces/ai-tool.interface.js';
5
+
6
+ const logger = new Logger('AiToolAuthorization');
7
+
8
+ /**
9
+ * Runs a tool's `authorize()` for one call. Shared by the chat orchestrator (auto and plan mode) and the MCP
10
+ * endpoint, so a data-level check holds on every path a tool is executed on (12.3.0+).
11
+ *
12
+ * Fails closed: a thrown `authorize()`, a missing answer or an `allowed` that is not exactly `true` is a refusal. The
13
+ * error itself is logged, never handed to the model or the MCP client, because it may name records or internals.
14
+ * A tool without `authorize()` is allowed — the registry role filter has already decided that the user may see it.
15
+ */
16
+ export async function authorizeToolCall(
17
+ tool: IAiTool,
18
+ args: Record<string, any>,
19
+ context: AiToolContext,
20
+ ): Promise<AiToolAuthorization> {
21
+ if (!tool.authorize) {
22
+ return { allowed: true };
23
+ }
24
+ try {
25
+ const result = await tool.authorize(args, context);
26
+ if (typeof result === 'boolean') {
27
+ return { allowed: result };
28
+ }
29
+ if (!result || typeof result !== 'object') {
30
+ return { allowed: false };
31
+ }
32
+ return { ...result, allowed: result.allowed === true };
33
+ } catch (err) {
34
+ logger.warn(`authorize() of AI tool "${tool.name}" threw, call refused: ${(err as Error).message}`);
35
+ return { allowed: false, reason: 'The authorization check for this action failed.' };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Whether a call of this tool needs the user's confirmation before it runs: a `destructive` tool always, a
41
+ * `mutating` tool as the `ai.confirmation.mutating` policy says (`enforced`, else the caller's `requireConfirmation`,
42
+ * else `default`), any other tool never.
43
+ *
44
+ * `requireConfirmation` is the chat client's per-prompt wish; the MCP endpoint passes none, so there the policy's
45
+ * `default` decides.
46
+ */
47
+ export function toolRequiresConfirmation(tool: IAiTool, requireConfirmation?: boolean): boolean {
48
+ if (tool.destructive) {
49
+ return true;
50
+ }
51
+ if (!tool.mutating) {
52
+ return false;
53
+ }
54
+ const cfg = ConfigService.get<{ default?: boolean; enabled?: boolean; enforced?: boolean }>(
55
+ 'ai.confirmation.mutating',
56
+ );
57
+ if (cfg?.enforced === true) {
58
+ return true;
59
+ }
60
+ return requireConfirmation ?? cfg?.default === true;
61
+ }
62
+
63
+ /**
64
+ * The server's MCP confirmation policy, from `ai.mcp.confirmation` (12.3.0+): `default` is whether MCP asks a user
65
+ * who has not chosen for themselves, `enforced` whether users may choose at all.
66
+ */
67
+ export interface AiMcpConfirmationPolicy {
68
+ default: boolean;
69
+ enforced: boolean;
70
+ }
71
+
72
+ /**
73
+ * Reads `ai.mcp.confirmation`: `false` → asks nobody unless a user switches it on; `true`, `{}`, unset or any other
74
+ * value → asks everybody unless a user switches it off; `{ default, enforced }` as written. Only `false` switches the
75
+ * default off and only `true` enforces, so a misspelt value never relaxes the gate.
76
+ */
77
+ export function mcpConfirmationPolicy(): AiMcpConfirmationPolicy {
78
+ const mcp = ConfigService.get<unknown>('ai.mcp');
79
+ const setting = mcp && typeof mcp === 'object' ? (mcp as { confirmation?: unknown }).confirmation : undefined;
80
+ if (setting === false) {
81
+ return { default: false, enforced: false };
82
+ }
83
+ if (setting && typeof setting === 'object') {
84
+ const { default: defaultValue, enforced } = setting as { default?: unknown; enforced?: unknown };
85
+ return { default: defaultValue !== false, enforced: enforced === true };
86
+ }
87
+ return { default: true, enforced: false };
88
+ }
89
+
90
+ /**
91
+ * Whether MCP asks a user before a tool that needs confirmation: the user's own choice, unless the policy is
92
+ * enforced or the user has not chosen.
93
+ */
94
+ export function effectiveMcpConfirmation(policy: AiMcpConfirmationPolicy, userChoice?: boolean | null): boolean {
95
+ if (policy.enforced || typeof userChoice !== 'boolean') {
96
+ return policy.default;
97
+ }
98
+ return userChoice;
99
+ }
@@ -2,6 +2,20 @@
2
2
  export * from './core-ai-mcp-oauth.initializer.js';
3
3
  export * from './core-ai-mcp.controller.js';
4
4
  export * from './helpers/ai-mcp-oauth.helper.js';
5
+ // Named on purpose: the page's scripts and security headers stay internal, so a project cannot pin them
6
+ export {
7
+ MCP_OAUTH_CONSENT_DECISION_FIELD,
8
+ MCP_OAUTH_CONSENT_TOKEN_FIELD,
9
+ mcpOAuthConsentLocale,
10
+ renderMcpOAuthConsentPage,
11
+ } from './helpers/ai-mcp-oauth-consent.helper.js';
12
+ export type {
13
+ IMcpOAuthConsentPageOptions,
14
+ IMcpOAuthConsentView,
15
+ McpOAuthConsentLocale,
16
+ McpOAuthConsentTexts,
17
+ } from './helpers/ai-mcp-oauth-consent.helper.js';
18
+ export * from './helpers/ai-tool-authorization.helper.js';
5
19
  export * from './core-ai.controller.js';
6
20
  export * from './core-ai.module.js';
7
21
  export * from './core-ai.resolver.js';
@@ -19,6 +33,7 @@ export * from './inputs/core-ai-prompt-update.input.js';
19
33
  export * from './inputs/core-ai-slot-create.input.js';
20
34
  export * from './inputs/core-ai-slot-update.input.js';
21
35
  export * from './inputs/core-ai-prompt.input.js';
36
+ export * from './inputs/core-ai-settings.input.js';
22
37
  export * from './hooks/ai-hook.base.js';
23
38
  export * from './hooks/ai-hook.registry.js';
24
39
  export * from './interfaces/ai-interaction-record.interface.js';
@@ -41,6 +56,8 @@ export * from './models/core-ai-slot.model.js';
41
56
  export * from './models/core-ai-tool-grant.model.js';
42
57
  export * from './models/core-ai-mode.model.js';
43
58
  export * from './models/core-ai-tool-policy.model.js';
59
+ export * from './models/core-ai-settings.model.js';
60
+ export * from './models/core-ai-user-setting.model.js';
44
61
  export * from './models/core-ai-response.model.js';
45
62
  export * from './models/core-ai-usage-info.model.js';
46
63
  export * from './models/core-ai-usage.model.js';
@@ -65,6 +82,7 @@ export * from './services/core-ai-tool-grant.service.js';
65
82
  export * from './services/core-ai-mcp-client.service.js';
66
83
  export * from './services/core-ai-mode.service.js';
67
84
  export * from './services/core-ai-tool-policy.service.js';
85
+ export * from './services/core-ai-user-setting.service.js';
68
86
  export * from './services/core-ai.service.js';
69
87
  export * from './tools/ai-tool.base.js';
70
88
  export * from './tools/ai-tool.registry.js';
@@ -0,0 +1,24 @@
1
+ import { InputType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../../common/decorators/restricted.decorator.js';
4
+ import { UnifiedField } from '../../../common/decorators/unified-field.decorator.js';
5
+ import { RoleEnum } from '../../../common/enums/role.enum.js';
6
+
7
+ /**
8
+ * Changes to the current user's own AI settings (12.3.0+). A field left out stays as it is.
9
+ */
10
+ @InputType({ description: "Changes to the current user's AI settings" })
11
+ @Restricted(RoleEnum.S_USER)
12
+ export class CoreAiSettingsInput {
13
+ /**
14
+ * Whether MCP asks before a tool that needs confirmation. Refused with 403 while the server enforces the setting
15
+ * (`ai.mcp.confirmation.enforced`).
16
+ */
17
+ @UnifiedField({
18
+ description: 'Whether MCP asks before a tool that needs confirmation',
19
+ isOptional: true,
20
+ roles: RoleEnum.S_USER,
21
+ type: () => Boolean,
22
+ })
23
+ mcpConfirmation?: boolean = undefined;
24
+ }
@@ -39,7 +39,7 @@ export interface AiHookEvent {
39
39
  * **Security:** hooks can only ADD restrictions (block calls, redact args) — they
40
40
  * cannot relax the permission system. A hook returning no block does not bypass
41
41
  * `@Restricted`/`@Roles`/`securityCheck()`; those still apply on every path
42
- * (`authorize()` only runs in plan mode).
42
+ * (`authorize()` runs before every execution since 12.3.0, on the arguments after the PreToolUse hooks).
43
43
  */
44
44
  export interface IAiHook {
45
45
  /** Unique hook name (for diagnostics and deterministic ordering). */
@@ -12,7 +12,13 @@ export interface AiToolContext {
12
12
  /** Authenticated user (shortcut for `serviceOptions.currentUser`). */
13
13
  currentUser?: { [key: string]: any; id: string; roles?: string[] };
14
14
 
15
- /** Accept-Language of the original request. */
15
+ /**
16
+ * The decision {@link IAiTool.authorize} returned for THIS call, handed to `execute()` (12.3.0+). Lets a tool reuse
17
+ * what its check loaded (`data`) instead of loading it twice. Absent for a tool without `authorize()`.
18
+ */
19
+ authorization?: AiToolAuthorization;
20
+
21
+ /** Accept-Language of the original request. Absent over MCP, which carries no request language. */
16
22
  language?: string;
17
23
 
18
24
  /**
@@ -26,10 +32,19 @@ export interface AiToolContext {
26
32
  * Result of a tool's pre-flight authorization check ({@link IAiTool.authorize}).
27
33
  */
28
34
  export interface AiToolAuthorization {
29
- /** Whether the user may run the tool with these arguments. */
35
+ /** Whether the user may run the tool with these arguments. Only `true` allows; anything else refuses. */
30
36
  allowed: boolean;
31
37
 
32
- /** Optional human-readable reason when not allowed. */
38
+ /**
39
+ * Whatever the check loaded and `execute()` may reuse (e.g. the record whose ownership it verified). Reaches
40
+ * `execute()` as `context.authorization.data`; never sent to the model or the client.
41
+ */
42
+ data?: unknown;
43
+
44
+ /**
45
+ * Optional reason when not allowed. It reaches the model (chat) or the MCP client, so it must not name anything
46
+ * the user may not know.
47
+ */
33
48
  reason?: string;
34
49
  }
35
50
 
@@ -69,21 +84,15 @@ export interface IAiTool {
69
84
 
70
85
  /**
71
86
  * Whether the tool performs a destructive/irreversible action (delete, bulk
72
- * update, payment, …). In the CHAT orchestrator destructive tools always require
73
- * confirmation: they are NOT executed until the prompt is re-sent with
74
- * `confirm: true`; the first response lists them as `pendingActions` with
75
- * `requiresConfirmation: true`.
87
+ * update, payment, …). Such a tool always needs the user's confirmation:
76
88
  *
77
- * **No confirmation gate over MCP.** `CoreAiMcpService.mcpCallTool` consults
78
- * neither this flag nor {@link IAiTool.mutating}, so a destructive tool invoked
79
- * through `/ai/mcp` executes IMMEDIATELY, on the first call. This flag is
80
- * therefore a chat-orchestrator contract, not a global execution barrier. The
81
- * barriers that DO hold on every path are the registry role filter ({@link
82
- * IAiTool.roles}, applied by `forUser()` before `execute()`) and the authorization
83
- * inside `execute()` itself — so a destructive tool restricted to a real role stays
84
- * unreachable by lesser-privileged MCP clients; MCP only skips the extra confirmation
85
- * step for clients that may already see the tool. Expose MCP only to clients you trust
86
- * to obtain user consent themselves.
89
+ * - **Chat:** it is NOT executed until the prompt is re-sent with `confirm: true`;
90
+ * the first response lists it as `pendingActions` with `requiresConfirmation: true`.
91
+ * - **MCP (12.3.0+):** `tools/call` asks the user through MCP elicitation and runs the
92
+ * tool only after they accepted. A client that cannot ask (no `elicitation`
93
+ * capability) gets a refusal. Whether MCP asks is `ai.mcp.confirmation` (on by
94
+ * default), which each user may switch for themselves unless it is enforced;
95
+ * `tools/list` marks the tool with `destructiveHint: true`.
87
96
  */
88
97
  readonly destructive?: boolean;
89
98
 
@@ -93,22 +102,25 @@ export interface IAiTool {
93
102
  * optionally client-overridable, optionally enforced). `destructive` is the
94
103
  * stronger flag and always requires confirmation regardless of policy.
95
104
  *
96
- * Same MCP caveat as {@link IAiTool.destructive}: the confirmation policy is not
97
- * evaluated on the `/ai/mcp` path at all.
105
+ * Over MCP (12.3.0+) the same policy applies through elicitation, with the
106
+ * policy's `default` in place of the chat client's `requireConfirmation`.
98
107
  */
99
108
  readonly mutating?: boolean;
100
109
 
101
110
  /**
102
- * Optional pre-flight authorization check. MUST NOT mutate anything — it only
111
+ * Optional authorization check for one call. MUST NOT mutate anything — it only
103
112
  * decides whether the user may run the tool with these arguments (e.g. load the
104
113
  * target record and verify ownership).
105
114
  *
106
- * **Runs in PLAN MODE ONLY.** Auto mode (the default, `ai.defaultMode`) and the
107
- * MCP endpoint call {@link IAiTool.execute} directly, without consulting this
108
- * method. A data-level check placed ONLY here therefore does not run for most
109
- * callers — put ownership and tenant checks INSIDE `execute()`, routed through
110
- * `CrudService` with `context.serviceOptions`, and treat `authorize()` as the
111
- * plan-mode pre-flight that lets a whole plan be rejected before any step runs.
115
+ * **Runs before every execution (12.3.0+)**: in auto mode, in plan mode (once for
116
+ * the whole plan before any step runs, and again before each step) and over MCP. It
117
+ * sees the arguments that will be executed, i.e. after the PreToolUse hooks. A refusal,
118
+ * a thrown error or an `allowed` that is not `true` skips the call. Return `data` to
119
+ * hand what the check loaded to `execute()` (`context.authorization.data`).
120
+ *
121
+ * Still route the data access in `execute()` through `CrudService` with
122
+ * `context.serviceOptions`: `authorize()` decides whether to try, the service decides
123
+ * what the user may read and write.
112
124
  *
113
125
  * When omitted, only the registry role filter applies.
114
126
  */
@@ -19,7 +19,7 @@ export type AiModeDocument = CoreAiMode & Document;
19
19
  * domain-specialized assistants without forking the orchestrator. Like every
20
20
  * other ai layer, modes can only ADD restrictions; they cannot relax the
21
21
  * permission model (`@Restricted` / `@Roles` / `securityCheck()` still apply;
22
- * `authorize()` only in plan mode).
22
+ * `authorize()` before every execution).
23
23
  */
24
24
  @MongooseSchema({ collection: 'aiModes', timestamps: true })
25
25
  @ObjectType({ description: 'Named agent mode' })
@@ -0,0 +1,23 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../../common/decorators/restricted.decorator.js';
4
+ import { RoleEnum } from '../../../common/enums/role.enum.js';
5
+
6
+ /**
7
+ * The current user's AI settings as they take effect (12.3.0+): the user's own choice where the server allows one,
8
+ * else the server configuration. Answer of `GET/PUT /ai/settings`, `aiSettings` and `updateAiSettings`.
9
+ */
10
+ @ObjectType({ description: "The current user's effective AI settings" })
11
+ @Restricted(RoleEnum.S_EVERYONE)
12
+ export class CoreAiSettings {
13
+ /**
14
+ * Whether MCP asks this user before a tool that needs confirmation (`destructive`, or `mutating` under
15
+ * `ai.confirmation.mutating`). `false` works like a "bypass permissions" mode for this user's MCP clients.
16
+ */
17
+ @Field(() => Boolean, { description: 'Whether MCP asks before a tool that needs confirmation' })
18
+ mcpConfirmation: boolean;
19
+
20
+ /** Whether the server fixed {@link mcpConfirmation} (`ai.mcp.confirmation.enforced`), so the user cannot change it. */
21
+ @Field(() => Boolean, { description: 'Whether the server fixed mcpConfirmation, so it cannot be changed' })
22
+ mcpConfirmationEnforced: boolean;
23
+ }
@@ -51,7 +51,7 @@ export class CoreAiToolPolicyRule {
51
51
  * `user`. A more specific scope wins over a more generic one. Hints only ever
52
52
  * tighten or relax the confirmation gate; the underlying permission model
53
53
  * (`@Restricted`, `@Roles`, `securityCheck()`) is enforced regardless;
54
- * `authorize()` adds to that in plan mode only.
54
+ * `authorize()` adds to that before every execution. Rules apply in the chat and over MCP (12.3.0+).
55
55
  */
56
56
  @MongooseSchema({ collection: 'aiToolPolicies', timestamps: true })
57
57
  @ObjectType({ description: 'Admin-editable scoped policy for a tool call' })
@@ -0,0 +1,46 @@
1
+ import { ObjectType } from '@nestjs/graphql';
2
+ import { Schema as MongooseSchema, SchemaFactory } from '../../../common/persistence/mongo/mongoose-compat.js';
3
+ import type { Document } from 'mongoose';
4
+
5
+ import { Restricted } from '../../../common/decorators/restricted.decorator.js';
6
+ import { UnifiedField } from '../../../common/decorators/unified-field.decorator.js';
7
+ import { RoleEnum } from '../../../common/enums/role.enum.js';
8
+ import { CorePersistenceModel } from '../../../common/models/core-persistence.model.js';
9
+
10
+ export type AiUserSettingDocument = CoreAiUserSetting & Document;
11
+
12
+ /**
13
+ * Personal AI settings of one user (12.3.0+), one record per user. Only what the user chose is stored; an unset
14
+ * field follows the server configuration.
15
+ *
16
+ * Users read and change their own settings through `GET/PUT /ai/settings` and the `aiSettings` /
17
+ * `updateAiSettings` operations, which answer with {@link CoreAiSettings} (the effective values), never with this
18
+ * record — hence admin-only here.
19
+ */
20
+ @MongooseSchema({ collection: 'aiUserSettings', timestamps: true })
21
+ @ObjectType({ description: 'Personal AI settings of one user' })
22
+ @Restricted(RoleEnum.ADMIN)
23
+ export class CoreAiUserSetting extends CorePersistenceModel {
24
+ /**
25
+ * Whether MCP asks this user before a tool that needs confirmation. Unset follows `ai.mcp.confirmation`; ignored
26
+ * while that is `enforced`.
27
+ */
28
+ @UnifiedField({
29
+ description: 'Whether MCP asks this user before a tool that needs confirmation (unset = server default)',
30
+ isOptional: true,
31
+ mongoose: true,
32
+ roles: RoleEnum.ADMIN,
33
+ type: () => Boolean,
34
+ })
35
+ mcpConfirmation?: boolean = undefined;
36
+
37
+ /** The user the settings belong to. */
38
+ @UnifiedField({
39
+ description: 'The user the settings belong to',
40
+ mongoose: { unique: true },
41
+ roles: RoleEnum.ADMIN,
42
+ })
43
+ userId: string = undefined;
44
+ }
45
+
46
+ export const AiUserSettingSchema = SchemaFactory.createForClass(CoreAiUserSetting);