@hed-hog/core 0.0.387 → 0.0.390

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 (535) hide show
  1. package/README.md +12 -11
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +4 -2
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai-attachment.fragments.d.ts +23 -0
  6. package/dist/ai/ai-attachment.fragments.d.ts.map +1 -0
  7. package/dist/ai/ai-attachment.fragments.js +34 -0
  8. package/dist/ai/ai-attachment.fragments.js.map +1 -0
  9. package/dist/ai/ai.controller.d.ts +7 -16
  10. package/dist/ai/ai.controller.d.ts.map +1 -1
  11. package/dist/ai/ai.prompts.d.ts +16 -0
  12. package/dist/ai/ai.prompts.d.ts.map +1 -0
  13. package/dist/ai/ai.prompts.js +37 -0
  14. package/dist/ai/ai.prompts.js.map +1 -0
  15. package/dist/ai/ai.service.d.ts +63 -12
  16. package/dist/ai/ai.service.d.ts.map +1 -1
  17. package/dist/ai/ai.service.js +215 -88
  18. package/dist/ai/ai.service.js.map +1 -1
  19. package/dist/ai/dto/chat.dto.d.ts +4 -1
  20. package/dist/ai/dto/chat.dto.d.ts.map +1 -1
  21. package/dist/ai/dto/chat.dto.js +23 -1
  22. package/dist/ai/dto/chat.dto.js.map +1 -1
  23. package/dist/ai/dto/create-agent.dto.d.ts +1 -1
  24. package/dist/ai/dto/create-agent.dto.d.ts.map +1 -1
  25. package/dist/ai/dto/create-agent.dto.js +1 -1
  26. package/dist/ai/dto/create-agent.dto.js.map +1 -1
  27. package/dist/ai/dto/update-agent.dto.d.ts +1 -1
  28. package/dist/ai/dto/update-agent.dto.d.ts.map +1 -1
  29. package/dist/ai/dto/update-agent.dto.js +1 -1
  30. package/dist/ai/dto/update-agent.dto.js.map +1 -1
  31. package/dist/ai-instruction/ai-instruction.controller.d.ts +61 -7
  32. package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -1
  33. package/dist/ai-instruction/ai-instruction.controller.js +63 -20
  34. package/dist/ai-instruction/ai-instruction.controller.js.map +1 -1
  35. package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -1
  36. package/dist/ai-instruction/ai-instruction.module.js +13 -2
  37. package/dist/ai-instruction/ai-instruction.module.js.map +1 -1
  38. package/dist/ai-instruction/ai-instruction.service.d.ts +131 -6
  39. package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -1
  40. package/dist/ai-instruction/ai-instruction.service.js +533 -60
  41. package/dist/ai-instruction/ai-instruction.service.js.map +1 -1
  42. package/dist/ai-instruction/ai-prompt-builder.service.d.ts +8 -0
  43. package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -1
  44. package/dist/ai-instruction/ai-prompt-builder.service.js +10 -1
  45. package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -1
  46. package/dist/ai-instruction/ai-prompt-registry.d.ts +45 -0
  47. package/dist/ai-instruction/ai-prompt-registry.d.ts.map +1 -0
  48. package/dist/ai-instruction/ai-prompt-registry.js +49 -0
  49. package/dist/ai-instruction/ai-prompt-registry.js.map +1 -0
  50. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +89 -0
  51. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -0
  52. package/dist/ai-instruction/ai-prompt-resolver.service.js +222 -0
  53. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -0
  54. package/dist/ai-instruction/ai-prompt-template.util.d.ts +70 -0
  55. package/dist/ai-instruction/ai-prompt-template.util.d.ts.map +1 -0
  56. package/dist/ai-instruction/ai-prompt-template.util.js +154 -0
  57. package/dist/ai-instruction/ai-prompt-template.util.js.map +1 -0
  58. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +5 -0
  59. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -1
  60. package/dist/ai-instruction/dto/create-ai-instruction.dto.js +27 -1
  61. package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -1
  62. package/dist/ai-instruction/dto/preview-ai-instruction.dto.d.ts +14 -0
  63. package/dist/ai-instruction/dto/preview-ai-instruction.dto.d.ts.map +1 -0
  64. package/dist/ai-instruction/dto/preview-ai-instruction.dto.js +42 -0
  65. package/dist/ai-instruction/dto/preview-ai-instruction.dto.js.map +1 -0
  66. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +11 -0
  67. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -1
  68. package/dist/ai-instruction/dto/update-ai-instruction.dto.js +34 -0
  69. package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -1
  70. package/dist/ai-instruction/types/ai-instruction.types.d.ts +13 -1
  71. package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -1
  72. package/dist/ai-instruction/types/ai-instruction.types.js +10 -0
  73. package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -1
  74. package/dist/auth/auth.controller.d.ts +24 -9
  75. package/dist/auth/auth.controller.d.ts.map +1 -1
  76. package/dist/auth/auth.controller.js +49 -21
  77. package/dist/auth/auth.controller.js.map +1 -1
  78. package/dist/auth/auth.service.d.ts +33 -8
  79. package/dist/auth/auth.service.d.ts.map +1 -1
  80. package/dist/auth/auth.service.js +78 -29
  81. package/dist/auth/auth.service.js.map +1 -1
  82. package/dist/captcha/captcha.module.d.ts +3 -0
  83. package/dist/captcha/captcha.module.d.ts.map +1 -0
  84. package/dist/captcha/captcha.module.js +23 -0
  85. package/dist/captcha/captcha.module.js.map +1 -0
  86. package/dist/captcha/captcha.service.d.ts +38 -0
  87. package/dist/captcha/captcha.service.d.ts.map +1 -0
  88. package/dist/captcha/captcha.service.js +144 -0
  89. package/dist/captcha/captcha.service.js.map +1 -0
  90. package/dist/challenge/challenge.service.d.ts +16 -7
  91. package/dist/challenge/challenge.service.d.ts.map +1 -1
  92. package/dist/challenge/challenge.service.js +64 -16
  93. package/dist/challenge/challenge.service.js.map +1 -1
  94. package/dist/core.module.d.ts +3 -1
  95. package/dist/core.module.d.ts.map +1 -1
  96. package/dist/core.module.js +29 -2
  97. package/dist/core.module.js.map +1 -1
  98. package/dist/dashboard/dashboard/dashboard.controller.d.ts +15 -15
  99. package/dist/dashboard/dashboard/dashboard.service.d.ts +15 -15
  100. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +6 -6
  101. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +6 -6
  102. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +20 -20
  103. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +20 -20
  104. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
  105. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +1 -1
  106. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +13 -13
  107. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +13 -13
  108. package/dist/file/file.service.d.ts +6 -0
  109. package/dist/file/file.service.d.ts.map +1 -1
  110. package/dist/file/file.service.js +44 -4
  111. package/dist/file/file.service.js.map +1 -1
  112. package/dist/incident/incident-notifier.service.d.ts +36 -0
  113. package/dist/incident/incident-notifier.service.d.ts.map +1 -0
  114. package/dist/incident/incident-notifier.service.js +208 -0
  115. package/dist/incident/incident-notifier.service.js.map +1 -0
  116. package/dist/incident/incident.controller.d.ts +23 -0
  117. package/dist/incident/incident.controller.d.ts.map +1 -0
  118. package/dist/incident/incident.controller.js +75 -0
  119. package/dist/incident/incident.controller.js.map +1 -0
  120. package/dist/incident/incident.module.d.ts +7 -0
  121. package/dist/incident/incident.module.d.ts.map +1 -0
  122. package/dist/incident/incident.module.js +32 -0
  123. package/dist/incident/incident.module.js.map +1 -0
  124. package/dist/incident/incident.service.d.ts +72 -0
  125. package/dist/incident/incident.service.d.ts.map +1 -0
  126. package/dist/incident/incident.service.js +184 -0
  127. package/dist/incident/incident.service.js.map +1 -0
  128. package/dist/index.d.ts +26 -0
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +30 -0
  131. package/dist/index.js.map +1 -1
  132. package/dist/integration-profile/dto/test.dto.d.ts +13 -0
  133. package/dist/integration-profile/dto/test.dto.d.ts.map +1 -0
  134. package/dist/integration-profile/dto/test.dto.js +26 -0
  135. package/dist/integration-profile/dto/test.dto.js.map +1 -0
  136. package/dist/integration-profile/integration-profile.controller.d.ts +12 -1
  137. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  138. package/dist/integration-profile/integration-profile.controller.js +9 -1
  139. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  140. package/dist/integration-profile/integration-profile.module.d.ts +6 -1
  141. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
  142. package/dist/integration-profile/integration-profile.module.js +26 -3
  143. package/dist/integration-profile/integration-profile.module.js.map +1 -1
  144. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  145. package/dist/integration-profile/integration-profile.secrets.js +9 -1
  146. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  147. package/dist/integration-profile/integration-profile.service.d.ts +26 -7
  148. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  149. package/dist/integration-profile/integration-profile.service.js +94 -91
  150. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  151. package/dist/integration-profile/integration-profile.utils.d.ts +13 -1
  152. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  153. package/dist/integration-profile/integration-profile.utils.js +20 -5
  154. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  155. package/dist/integration-profile/testers/ai.testers.d.ts +3 -0
  156. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -0
  157. package/dist/integration-profile/testers/ai.testers.js +125 -0
  158. package/dist/integration-profile/testers/ai.testers.js.map +1 -0
  159. package/dist/integration-profile/testers/captcha.testers.d.ts +3 -0
  160. package/dist/integration-profile/testers/captcha.testers.d.ts.map +1 -0
  161. package/dist/integration-profile/testers/captcha.testers.js +121 -0
  162. package/dist/integration-profile/testers/captcha.testers.js.map +1 -0
  163. package/dist/integration-profile/testers/email.testers.d.ts +3 -0
  164. package/dist/integration-profile/testers/email.testers.d.ts.map +1 -0
  165. package/dist/integration-profile/testers/email.testers.js +128 -0
  166. package/dist/integration-profile/testers/email.testers.js.map +1 -0
  167. package/dist/integration-profile/testers/http-probe.d.ts +57 -0
  168. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -0
  169. package/dist/integration-profile/testers/http-probe.js +140 -0
  170. package/dist/integration-profile/testers/http-probe.js.map +1 -0
  171. package/dist/integration-profile/testers/index.d.ts +12 -0
  172. package/dist/integration-profile/testers/index.d.ts.map +1 -0
  173. package/dist/integration-profile/testers/index.js +45 -0
  174. package/dist/integration-profile/testers/index.js.map +1 -0
  175. package/dist/integration-profile/testers/infrastructure.testers.d.ts +3 -0
  176. package/dist/integration-profile/testers/infrastructure.testers.d.ts.map +1 -0
  177. package/dist/integration-profile/testers/infrastructure.testers.js +105 -0
  178. package/dist/integration-profile/testers/infrastructure.testers.js.map +1 -0
  179. package/dist/integration-profile/testers/integration-tester.registry.d.ts +17 -0
  180. package/dist/integration-profile/testers/integration-tester.registry.d.ts.map +1 -0
  181. package/dist/integration-profile/testers/integration-tester.registry.js +52 -0
  182. package/dist/integration-profile/testers/integration-tester.registry.js.map +1 -0
  183. package/dist/integration-profile/testers/integration-tester.types.d.ts +55 -0
  184. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -0
  185. package/dist/integration-profile/testers/integration-tester.types.js +16 -0
  186. package/dist/integration-profile/testers/integration-tester.types.js.map +1 -0
  187. package/dist/integration-profile/testers/messaging.testers.d.ts +3 -0
  188. package/dist/integration-profile/testers/messaging.testers.d.ts.map +1 -0
  189. package/dist/integration-profile/testers/messaging.testers.js +70 -0
  190. package/dist/integration-profile/testers/messaging.testers.js.map +1 -0
  191. package/dist/integration-profile/testers/oauth.testers.d.ts +3 -0
  192. package/dist/integration-profile/testers/oauth.testers.d.ts.map +1 -0
  193. package/dist/integration-profile/testers/oauth.testers.js +285 -0
  194. package/dist/integration-profile/testers/oauth.testers.js.map +1 -0
  195. package/dist/integration-profile/testers/payment.testers.d.ts +3 -0
  196. package/dist/integration-profile/testers/payment.testers.d.ts.map +1 -0
  197. package/dist/integration-profile/testers/payment.testers.js +169 -0
  198. package/dist/integration-profile/testers/payment.testers.js.map +1 -0
  199. package/dist/integration-profile/testers/storage.testers.d.ts +3 -0
  200. package/dist/integration-profile/testers/storage.testers.d.ts.map +1 -0
  201. package/dist/integration-profile/testers/storage.testers.js +208 -0
  202. package/dist/integration-profile/testers/storage.testers.js.map +1 -0
  203. package/dist/language/en.json +4 -2
  204. package/dist/language/pt.json +4 -2
  205. package/dist/leaked-password/leaked-password.controller.d.ts +24 -0
  206. package/dist/leaked-password/leaked-password.controller.d.ts.map +1 -0
  207. package/dist/leaked-password/leaked-password.controller.js +78 -0
  208. package/dist/leaked-password/leaked-password.controller.js.map +1 -0
  209. package/dist/leaked-password/leaked-password.module.d.ts +8 -0
  210. package/dist/leaked-password/leaked-password.module.d.ts.map +1 -0
  211. package/dist/leaked-password/leaked-password.module.js +31 -0
  212. package/dist/leaked-password/leaked-password.module.js.map +1 -0
  213. package/dist/leaked-password/leaked-password.service.d.ts +64 -0
  214. package/dist/leaked-password/leaked-password.service.d.ts.map +1 -0
  215. package/dist/leaked-password/leaked-password.service.js +218 -0
  216. package/dist/leaked-password/leaked-password.service.js.map +1 -0
  217. package/dist/locale/locale.controller.d.ts +1 -1
  218. package/dist/locale/locale.service.d.ts +1 -1
  219. package/dist/mail/dto/preview.dto.d.ts +18 -0
  220. package/dist/mail/dto/preview.dto.d.ts.map +1 -0
  221. package/dist/mail/dto/preview.dto.js +66 -0
  222. package/dist/mail/dto/preview.dto.js.map +1 -0
  223. package/dist/mail/dto/send.dto.d.ts +7 -0
  224. package/dist/mail/dto/send.dto.d.ts.map +1 -1
  225. package/dist/mail/dto/send.dto.js +10 -0
  226. package/dist/mail/dto/send.dto.js.map +1 -1
  227. package/dist/mail/mail.controller.d.ts +4 -0
  228. package/dist/mail/mail.controller.d.ts.map +1 -1
  229. package/dist/mail/mail.controller.js +16 -2
  230. package/dist/mail/mail.controller.js.map +1 -1
  231. package/dist/mail/mail.service.d.ts +23 -2
  232. package/dist/mail/mail.service.d.ts.map +1 -1
  233. package/dist/mail/mail.service.js +102 -13
  234. package/dist/mail/mail.service.js.map +1 -1
  235. package/dist/mail-layout/dto/update.dto.d.ts +17 -0
  236. package/dist/mail-layout/dto/update.dto.d.ts.map +1 -0
  237. package/dist/mail-layout/dto/update.dto.js +27 -0
  238. package/dist/mail-layout/dto/update.dto.js.map +1 -0
  239. package/dist/mail-layout/mail-layout.controller.d.ts +15 -0
  240. package/dist/mail-layout/mail-layout.controller.d.ts.map +1 -0
  241. package/dist/mail-layout/mail-layout.controller.js +71 -0
  242. package/dist/mail-layout/mail-layout.controller.js.map +1 -0
  243. package/dist/mail-layout/mail-layout.module.d.ts +3 -0
  244. package/dist/mail-layout/mail-layout.module.d.ts.map +1 -0
  245. package/dist/mail-layout/mail-layout.module.js +35 -0
  246. package/dist/mail-layout/mail-layout.module.js.map +1 -0
  247. package/dist/mail-layout/mail-layout.service.d.ts +27 -0
  248. package/dist/mail-layout/mail-layout.service.d.ts.map +1 -0
  249. package/dist/mail-layout/mail-layout.service.js +80 -0
  250. package/dist/mail-layout/mail-layout.service.js.map +1 -0
  251. package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
  252. package/dist/mcp-chat/mcp-chat.service.d.ts +24 -2
  253. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  254. package/dist/mcp-chat/mcp-chat.service.js +80 -24
  255. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  256. package/dist/menu/menu.controller.d.ts +3 -3
  257. package/dist/menu/menu.service.d.ts +3 -3
  258. package/dist/notification/notification.controller.d.ts +12 -12
  259. package/dist/notification/notification.service.d.ts +12 -12
  260. package/dist/oauth/oauth-token-broker.service.d.ts +72 -0
  261. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -0
  262. package/dist/oauth/oauth-token-broker.service.js +258 -0
  263. package/dist/oauth/oauth-token-broker.service.js.map +1 -0
  264. package/dist/oauth/oauth.module.d.ts.map +1 -1
  265. package/dist/oauth/oauth.module.js +3 -1
  266. package/dist/oauth/oauth.module.js.map +1 -1
  267. package/dist/oauth/oauth.service.d.ts +2 -0
  268. package/dist/oauth/oauth.service.d.ts.map +1 -1
  269. package/dist/oauth/oauth.service.js +6 -3
  270. package/dist/oauth/oauth.service.js.map +1 -1
  271. package/dist/pdf/pdf.service.d.ts +8 -0
  272. package/dist/pdf/pdf.service.d.ts.map +1 -1
  273. package/dist/pdf/pdf.service.js +52 -0
  274. package/dist/pdf/pdf.service.js.map +1 -1
  275. package/dist/presence/presence.controller.d.ts +16 -0
  276. package/dist/presence/presence.controller.d.ts.map +1 -0
  277. package/dist/presence/presence.controller.js +60 -0
  278. package/dist/presence/presence.controller.js.map +1 -0
  279. package/dist/presence/presence.listener.d.ts +29 -0
  280. package/dist/presence/presence.listener.d.ts.map +1 -0
  281. package/dist/presence/presence.listener.js +82 -0
  282. package/dist/presence/presence.listener.js.map +1 -0
  283. package/dist/presence/presence.module.d.ts +3 -0
  284. package/dist/presence/presence.module.d.ts.map +1 -0
  285. package/dist/presence/presence.module.js +32 -0
  286. package/dist/presence/presence.module.js.map +1 -0
  287. package/dist/presence/presence.service.d.ts +42 -0
  288. package/dist/presence/presence.service.d.ts.map +1 -0
  289. package/dist/presence/presence.service.js +147 -0
  290. package/dist/presence/presence.service.js.map +1 -0
  291. package/dist/profile/dto/delete-account.dto.d.ts +8 -0
  292. package/dist/profile/dto/delete-account.dto.d.ts.map +1 -0
  293. package/dist/profile/dto/delete-account.dto.js +22 -0
  294. package/dist/profile/dto/delete-account.dto.js.map +1 -0
  295. package/dist/profile/events/profile-account-deleted.event.d.ts +12 -0
  296. package/dist/profile/events/profile-account-deleted.event.d.ts.map +1 -0
  297. package/dist/profile/events/profile-account-deleted.event.js +17 -0
  298. package/dist/profile/events/profile-account-deleted.event.js.map +1 -0
  299. package/dist/profile/profile.controller.d.ts +21 -15
  300. package/dist/profile/profile.controller.d.ts.map +1 -1
  301. package/dist/profile/profile.controller.js +75 -46
  302. package/dist/profile/profile.controller.js.map +1 -1
  303. package/dist/profile/profile.service.d.ts +30 -18
  304. package/dist/profile/profile.service.d.ts.map +1 -1
  305. package/dist/profile/profile.service.js +113 -37
  306. package/dist/profile/profile.service.js.map +1 -1
  307. package/dist/realtime/realtime.gateway.d.ts +48 -0
  308. package/dist/realtime/realtime.gateway.d.ts.map +1 -0
  309. package/dist/realtime/realtime.gateway.js +156 -0
  310. package/dist/realtime/realtime.gateway.js.map +1 -0
  311. package/dist/realtime/realtime.module.d.ts +8 -0
  312. package/dist/realtime/realtime.module.d.ts.map +1 -0
  313. package/dist/realtime/realtime.module.js +29 -0
  314. package/dist/realtime/realtime.module.js.map +1 -0
  315. package/dist/realtime/realtime.service.d.ts +51 -0
  316. package/dist/realtime/realtime.service.d.ts.map +1 -0
  317. package/dist/realtime/realtime.service.js +177 -0
  318. package/dist/realtime/realtime.service.js.map +1 -0
  319. package/dist/redis/redis-pubsub.d.ts +27 -0
  320. package/dist/redis/redis-pubsub.d.ts.map +1 -0
  321. package/dist/redis/redis-pubsub.js +140 -0
  322. package/dist/redis/redis-pubsub.js.map +1 -0
  323. package/dist/role/role.controller.d.ts +3 -3
  324. package/dist/role/role.service.d.ts +3 -3
  325. package/dist/setting/dto/create.dto.d.ts.map +1 -1
  326. package/dist/setting/dto/create.dto.js +9 -2
  327. package/dist/setting/dto/create.dto.js.map +1 -1
  328. package/dist/setting/setting.controller.d.ts +5 -5
  329. package/dist/setting/setting.service.d.ts +8 -5
  330. package/dist/setting/setting.service.d.ts.map +1 -1
  331. package/dist/setting/setting.service.js +25 -1
  332. package/dist/setting/setting.service.js.map +1 -1
  333. package/dist/user/dto/create-with-email-and-password.dto.d.ts +2 -0
  334. package/dist/user/dto/create-with-email-and-password.dto.d.ts.map +1 -1
  335. package/dist/user/dto/create-with-email-and-password.dto.js +5 -0
  336. package/dist/user/dto/create-with-email-and-password.dto.js.map +1 -1
  337. package/dist/user/dto/query-user-mail-list.dto.d.ts +5 -0
  338. package/dist/user/dto/query-user-mail-list.dto.d.ts.map +1 -0
  339. package/dist/user/dto/query-user-mail-list.dto.js +25 -0
  340. package/dist/user/dto/query-user-mail-list.dto.js.map +1 -0
  341. package/dist/user/user-credential.util.d.ts +19 -0
  342. package/dist/user/user-credential.util.d.ts.map +1 -0
  343. package/dist/user/user-credential.util.js +32 -0
  344. package/dist/user/user-credential.util.js.map +1 -0
  345. package/dist/user/user.controller.d.ts +17 -5
  346. package/dist/user/user.controller.d.ts.map +1 -1
  347. package/dist/user/user.controller.js +18 -3
  348. package/dist/user/user.controller.js.map +1 -1
  349. package/dist/user/user.service.d.ts +28 -6
  350. package/dist/user/user.service.d.ts.map +1 -1
  351. package/dist/user/user.service.js +204 -13
  352. package/dist/user/user.service.js.map +1 -1
  353. package/dist/validators/is-strong-password-with-settings.validator.d.ts.map +1 -1
  354. package/dist/validators/is-strong-password-with-settings.validator.js +11 -0
  355. package/dist/validators/is-strong-password-with-settings.validator.js.map +1 -1
  356. package/dist/validators/service-locator.d.ts +9 -0
  357. package/dist/validators/service-locator.d.ts.map +1 -1
  358. package/dist/validators/service-locator.js +12 -0
  359. package/dist/validators/service-locator.js.map +1 -1
  360. package/dist/whatsapp-sender/whatsapp-sender.module.d.ts +3 -0
  361. package/dist/whatsapp-sender/whatsapp-sender.module.d.ts.map +1 -0
  362. package/dist/whatsapp-sender/whatsapp-sender.module.js +23 -0
  363. package/dist/whatsapp-sender/whatsapp-sender.module.js.map +1 -0
  364. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +50 -0
  365. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -0
  366. package/dist/whatsapp-sender/whatsapp-sender.service.js +224 -0
  367. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -0
  368. package/hedhog/data/integration_provider.yaml +16 -0
  369. package/hedhog/data/mail.yaml +88 -61
  370. package/hedhog/data/mail_layout.yaml +188 -0
  371. package/hedhog/data/menu.yaml +67 -3
  372. package/hedhog/data/route.yaml +135 -0
  373. package/hedhog/data/setting_group.yaml +139 -1
  374. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +36 -0
  375. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +2 -0
  376. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +10 -7
  377. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +7 -4
  378. package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-form.tsx.ejs +580 -0
  379. package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-preview.tsx.ejs +150 -0
  380. package/hedhog/frontend/app/ai_instruction/_components/ai-instruction-versions.tsx.ejs +199 -0
  381. package/hedhog/frontend/app/ai_instruction/_components/prompt-diff.tsx.ejs +63 -0
  382. package/hedhog/frontend/app/ai_instruction/_lib/line-diff.spec.ts.ejs +59 -0
  383. package/hedhog/frontend/app/ai_instruction/_lib/line-diff.ts.ejs +72 -0
  384. package/hedhog/frontend/app/ai_instruction/_lib/prompt-template.spec.ts.ejs +170 -0
  385. package/hedhog/frontend/app/ai_instruction/_lib/prompt-template.ts.ejs +167 -0
  386. package/hedhog/frontend/app/ai_instruction/_lib/types.ts.ejs +99 -0
  387. package/hedhog/frontend/app/ai_instruction/page.tsx.ejs +422 -0
  388. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +51 -4
  389. package/hedhog/frontend/app/incidents/page.tsx.ejs +400 -0
  390. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +153 -84
  391. package/hedhog/frontend/app/mail/layout/page.tsx.ejs +344 -0
  392. package/hedhog/frontend/app/mail/template/page.tsx.ejs +155 -121
  393. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +129 -17
  394. package/hedhog/frontend/app/pages/_components/page-delete-dialog.tsx.ejs +343 -0
  395. package/hedhog/frontend/app/pages/_components/page-detail-sheet.tsx.ejs +438 -0
  396. package/hedhog/frontend/app/pages/_components/page-wizard-sheet.tsx.ejs +1009 -0
  397. package/hedhog/frontend/app/pages/_components/system-page-types.ts.ejs +56 -0
  398. package/hedhog/frontend/app/pages/_components/use-system-pages.ts.ejs +171 -0
  399. package/hedhog/frontend/app/pages/page.tsx.ejs +514 -0
  400. package/hedhog/frontend/app/users/page.tsx.ejs +119 -13
  401. package/hedhog/frontend/app/users/user-activity-timeline.tsx.ejs +231 -0
  402. package/hedhog/frontend/app/users/user-mails.tsx.ejs +399 -0
  403. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +60 -75
  404. package/hedhog/frontend/messages/en.json +499 -3
  405. package/hedhog/frontend/messages/pt.json +499 -3
  406. package/hedhog/table/ai_agent.yaml +1 -0
  407. package/hedhog/table/ai_instruction.yaml +15 -0
  408. package/hedhog/table/leaked_password.yaml +18 -0
  409. package/hedhog/table/mail_layout.yaml +35 -0
  410. package/hedhog/table/mail_var.yaml +4 -0
  411. package/hedhog/table/setting.yaml +1 -0
  412. package/hedhog/table/system_incident.yaml +59 -0
  413. package/hedhog/table/user_presence.yaml +36 -0
  414. package/package.json +8 -4
  415. package/src/access-log/access-log.interceptor.ts +4 -2
  416. package/src/ai/ai-attachment.fragments.spec.ts +319 -0
  417. package/src/ai/ai-attachment.fragments.ts +35 -0
  418. package/src/ai/ai.prompts.ts +38 -0
  419. package/src/ai/ai.service.spec.ts +280 -0
  420. package/src/ai/ai.service.ts +338 -82
  421. package/src/ai/dto/chat.dto.ts +33 -3
  422. package/src/ai/dto/create-agent.dto.ts +2 -2
  423. package/src/ai/dto/update-agent.dto.ts +2 -2
  424. package/src/ai-instruction/ai-instruction.controller.ts +40 -9
  425. package/src/ai-instruction/ai-instruction.module.ts +13 -2
  426. package/src/ai-instruction/ai-instruction.service.spec.ts +658 -0
  427. package/src/ai-instruction/ai-instruction.service.ts +661 -62
  428. package/src/ai-instruction/ai-prompt-builder.service.ts +10 -1
  429. package/src/ai-instruction/ai-prompt-registry.ts +87 -0
  430. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +375 -0
  431. package/src/ai-instruction/ai-prompt-resolver.service.ts +295 -0
  432. package/src/ai-instruction/ai-prompt-template.util.spec.ts +162 -0
  433. package/src/ai-instruction/ai-prompt-template.util.ts +199 -0
  434. package/src/ai-instruction/dto/create-ai-instruction.dto.ts +39 -3
  435. package/src/ai-instruction/dto/preview-ai-instruction.dto.ts +26 -0
  436. package/src/ai-instruction/dto/update-ai-instruction.dto.ts +42 -1
  437. package/src/ai-instruction/types/ai-instruction.types.ts +24 -1
  438. package/src/auth/auth.controller.ts +31 -5
  439. package/src/auth/auth.service.spec.ts +3 -1
  440. package/src/auth/auth.service.ts +101 -36
  441. package/src/captcha/captcha.module.ts +10 -0
  442. package/src/captcha/captcha.service.spec.ts +210 -0
  443. package/src/captcha/captcha.service.ts +191 -0
  444. package/src/challenge/challenge.service.spec.ts +39 -4
  445. package/src/challenge/challenge.service.ts +72 -17
  446. package/src/core.module.ts +27 -1
  447. package/src/file/file.service.spec.ts +105 -0
  448. package/src/file/file.service.ts +60 -3
  449. package/src/incident/incident-notifier.service.spec.ts +205 -0
  450. package/src/incident/incident-notifier.service.ts +241 -0
  451. package/src/incident/incident.controller.ts +38 -0
  452. package/src/incident/incident.module.ts +19 -0
  453. package/src/incident/incident.service.spec.ts +150 -0
  454. package/src/incident/incident.service.ts +209 -0
  455. package/src/index.ts +37 -0
  456. package/src/integration-profile/dto/test.dto.ts +19 -0
  457. package/src/integration-profile/integration-profile.controller.ts +10 -1
  458. package/src/integration-profile/integration-profile.module.spec.ts +51 -0
  459. package/src/integration-profile/integration-profile.module.ts +23 -4
  460. package/src/integration-profile/integration-profile.secrets.ts +9 -1
  461. package/src/integration-profile/integration-profile.service.spec.ts +279 -0
  462. package/src/integration-profile/integration-profile.service.ts +120 -124
  463. package/src/integration-profile/integration-profile.utils.spec.ts +22 -0
  464. package/src/integration-profile/integration-profile.utils.ts +40 -3
  465. package/src/integration-profile/testers/ai.testers.ts +165 -0
  466. package/src/integration-profile/testers/captcha.testers.spec.ts +92 -0
  467. package/src/integration-profile/testers/captcha.testers.ts +139 -0
  468. package/src/integration-profile/testers/email.testers.ts +161 -0
  469. package/src/integration-profile/testers/http-probe.spec.ts +153 -0
  470. package/src/integration-profile/testers/http-probe.ts +171 -0
  471. package/src/integration-profile/testers/index.ts +30 -0
  472. package/src/integration-profile/testers/infrastructure.testers.ts +122 -0
  473. package/src/integration-profile/testers/integration-tester.registry.spec.ts +75 -0
  474. package/src/integration-profile/testers/integration-tester.registry.ts +43 -0
  475. package/src/integration-profile/testers/integration-tester.types.ts +61 -0
  476. package/src/integration-profile/testers/messaging.testers.ts +102 -0
  477. package/src/integration-profile/testers/oauth.testers.spec.ts +220 -0
  478. package/src/integration-profile/testers/oauth.testers.ts +305 -0
  479. package/src/integration-profile/testers/payment.testers.ts +185 -0
  480. package/src/integration-profile/testers/remaining.testers.spec.ts +401 -0
  481. package/src/integration-profile/testers/storage.testers.spec.ts +185 -0
  482. package/src/integration-profile/testers/storage.testers.ts +226 -0
  483. package/src/integration-profile/testers/tester-coverage.spec.ts +58 -0
  484. package/src/language/en.json +4 -2
  485. package/src/language/pt.json +4 -2
  486. package/src/leaked-password/README.md +83 -0
  487. package/src/leaked-password/leaked-password.controller.ts +52 -0
  488. package/src/leaked-password/leaked-password.module.ts +18 -0
  489. package/src/leaked-password/leaked-password.service.ts +242 -0
  490. package/src/leaked-password/seed/build-seed-sql.mjs +180 -0
  491. package/src/leaked-password/seed/common-passwords.txt +335 -0
  492. package/src/mail/dto/preview.dto.ts +50 -0
  493. package/src/mail/dto/send.dto.ts +14 -1
  494. package/src/mail/mail-template.spec.ts +197 -0
  495. package/src/mail/mail.controller.ts +9 -2
  496. package/src/mail/mail.service.spec.ts +116 -1
  497. package/src/mail/mail.service.ts +118 -12
  498. package/src/mail-layout/dto/update.dto.ts +19 -0
  499. package/src/mail-layout/mail-layout.controller.ts +47 -0
  500. package/src/mail-layout/mail-layout.module.ts +22 -0
  501. package/src/mail-layout/mail-layout.service.ts +94 -0
  502. package/src/mcp-chat/mcp-chat.service.spec.ts +236 -0
  503. package/src/mcp-chat/mcp-chat.service.ts +122 -26
  504. package/src/oauth/oauth-token-broker.service.spec.ts +172 -0
  505. package/src/oauth/oauth-token-broker.service.ts +363 -0
  506. package/src/oauth/oauth.module.ts +3 -1
  507. package/src/oauth/oauth.service.ts +8 -2
  508. package/src/pdf/pdf.service.ts +75 -0
  509. package/src/presence/presence.controller.ts +29 -0
  510. package/src/presence/presence.listener.ts +53 -0
  511. package/src/presence/presence.module.ts +19 -0
  512. package/src/presence/presence.service.spec.ts +97 -0
  513. package/src/presence/presence.service.ts +160 -0
  514. package/src/profile/dto/delete-account.dto.ts +11 -0
  515. package/src/profile/events/profile-account-deleted.event.ts +13 -0
  516. package/src/profile/profile.controller.ts +46 -34
  517. package/src/profile/profile.service.ts +138 -38
  518. package/src/realtime/realtime.gateway.ts +138 -0
  519. package/src/realtime/realtime.module.ts +16 -0
  520. package/src/realtime/realtime.service.spec.ts +50 -0
  521. package/src/realtime/realtime.service.ts +187 -0
  522. package/src/redis/redis-pubsub.ts +179 -0
  523. package/src/setting/dto/create.dto.ts +13 -2
  524. package/src/setting/setting.service.spec.ts +56 -0
  525. package/src/setting/setting.service.ts +29 -1
  526. package/src/user/dto/create-with-email-and-password.dto.ts +6 -1
  527. package/src/user/dto/query-user-mail-list.dto.ts +10 -0
  528. package/src/user/user-credential.util.ts +35 -0
  529. package/src/user/user.controller.ts +17 -2
  530. package/src/user/user.service.spec.ts +153 -0
  531. package/src/user/user.service.ts +255 -12
  532. package/src/validators/is-strong-password-with-settings.validator.ts +12 -0
  533. package/src/validators/service-locator.ts +15 -0
  534. package/src/whatsapp-sender/whatsapp-sender.module.ts +10 -0
  535. package/src/whatsapp-sender/whatsapp-sender.service.ts +313 -0
@@ -1,35 +1,100 @@
1
1
  import { PaginationDTO } from '@hed-hog/api-pagination';
2
2
  import { Prisma, PrismaService } from '@hed-hog/api-prisma';
3
3
  import {
4
+ ConflictException,
4
5
  forwardRef,
5
6
  Inject,
6
7
  Injectable,
7
8
  Logger,
8
9
  NotFoundException,
10
+ OnModuleDestroy,
9
11
  OnModuleInit,
12
+ UnprocessableEntityException,
10
13
  } from '@nestjs/common';
14
+ import { randomUUID } from 'node:crypto';
11
15
  import { DeleteDTO } from '../dto/delete.dto';
16
+ import { createRedisPubSub, type RedisPubSub } from '../redis/redis-pubsub';
17
+ import {
18
+ type AiPromptDefinition,
19
+ getAiPromptDefinition,
20
+ listAiPromptDefinitions,
21
+ } from './ai-prompt-registry';
22
+ import { estimateTokens, renderTemplate, validateTemplate } from './ai-prompt-template.util';
12
23
  import { CreateAiInstructionDTO } from './dto/create-ai-instruction.dto';
24
+ import { PreviewAiInstructionDTO } from './dto/preview-ai-instruction.dto';
13
25
  import { UpdateAiInstructionDTO } from './dto/update-ai-instruction.dto';
14
26
  import type {
15
27
  AiInstructionLayer,
28
+ AiInstructionListFilters,
16
29
  AiInstructionRecord,
17
30
  } from './types/ai-instruction.types';
18
31
 
19
- type CacheEntry = { content: string; fetchedAt: number };
20
-
32
+ /**
33
+ * `record: null` is a negative cache entry — the slug/layer does not exist.
34
+ * Without it, every lookup for a missing slug hits the database on every
35
+ * request (task prompts are resolved on every LLM call).
36
+ */
37
+ type CacheEntry = { record: AiInstructionRecord | null; fetchedAt: number; ttl: number };
38
+
39
+ /**
40
+ * Cross-pod invalidation happens through Redis, with the database version check
41
+ * as backup. These TTLs are the last resort for when both are unavailable.
42
+ */
21
43
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
22
-
23
- const FALLBACK_SYSTEM_PROMPT =
44
+ const TASK_CACHE_TTL_MS = 60 * 1000;
45
+ const NEGATIVE_CACHE_TTL_MS = 60 * 1000;
46
+
47
+ /**
48
+ * An empty `slugs` list means "drop everything" — the database version path
49
+ * knows a change happened but not which prompts it touched.
50
+ */
51
+ type AiInstructionInvalidationMessage = {
52
+ originInstanceId: string;
53
+ version: number;
54
+ slugs: string[];
55
+ };
56
+
57
+ /**
58
+ * Reuses the generic runtime-version table owned by the settings service. No
59
+ * column there is setting-specific, and the unique index on `scope` means a new
60
+ * scope needs no migration — the idempotent INSERT below creates its own row.
61
+ */
62
+ const RUNTIME_STATE_SCOPE = 'ai_instruction';
63
+
64
+ /**
65
+ * Columns are cast explicitly because `$queryRaw` returns DECIMAL as
66
+ * `Prisma.Decimal`, which serializes as a string over HTTP and becomes NaN in
67
+ * arithmetic. Keep every SELECT in this file using this list.
68
+ */
69
+ const INSTRUCTION_COLUMNS = Prisma.sql`
70
+ id, slug, layer, name, content, variables_schema, locale, module_slug,
71
+ is_active, version, model, temperature::float8 AS temperature, max_tokens,
72
+ is_locked, created_at, updated_at
73
+ `;
74
+
75
+ export const FALLBACK_SYSTEM_PROMPT =
24
76
  'You are a helpful assistant with access to tools from the HedHog system. ' +
25
77
  "Use the available tools to answer the user's questions accurately. " +
26
78
  'Always respond in the same language the user writes in.';
27
79
 
28
80
  @Injectable()
29
- export class AiInstructionService implements OnModuleInit {
81
+ export class AiInstructionService implements OnModuleInit, OnModuleDestroy {
30
82
  private readonly logger = new Logger(AiInstructionService.name);
31
83
  private readonly cache = new Map<string, CacheEntry>();
32
84
 
85
+ private readonly instanceId = randomUUID();
86
+ private readonly redisChannel = 'core:ai-instruction:invalidated';
87
+ private readonly versionCheckIntervalMs = Math.max(
88
+ 1000,
89
+ Number(process.env.AI_INSTRUCTION_VERSION_CHECK_INTERVAL_MS || 5000),
90
+ );
91
+ private localCacheVersion = 0;
92
+ private lastVersionCheckAt = 0;
93
+ private versionCheckInFlight: Promise<void> | null = null;
94
+ private versionStateEnsured = false;
95
+ private lastVersionCheckErrorLogAt = 0;
96
+ private pubsub: RedisPubSub | null = null;
97
+
33
98
  constructor(
34
99
  @Inject(forwardRef(() => PrismaService))
35
100
  private readonly prisma: PrismaService,
@@ -38,10 +103,46 @@ export class AiInstructionService implements OnModuleInit {
38
103
  async onModuleInit() {
39
104
  // Warm up global and product layers on startup
40
105
  try {
106
+ // Before the warm-up: starting at version 0 would make the first read
107
+ // think it is behind and throw the freshly warmed cache away.
108
+ await this.syncCacheVersionFromDatabase();
41
109
  await this.getLayer('system');
42
110
  await this.getLayer('product');
111
+ await this.warnAboutUnseededPrompts();
43
112
  } catch {
44
- // DB may not be ready yet; cache will fill on first request
113
+ // DB may not be ready yet; cache and version fill in on first request
114
+ }
115
+
116
+ // Outside the try: createRedisPubSub already handles its own failures.
117
+ this.pubsub = await createRedisPubSub({
118
+ channel: this.redisChannel,
119
+ logger: this.logger,
120
+ label: 'ai instruction',
121
+ onMessage: (raw) => this.handleInvalidationMessage(raw),
122
+ });
123
+ }
124
+
125
+ async onModuleDestroy() {
126
+ await this.pubsub?.close();
127
+ }
128
+
129
+ /**
130
+ * Reports prompts declared in code that have no row yet. Without this, a
131
+ * forgotten seed only surfaces the first time that AI flow runs — which for
132
+ * essay grading means the first graded exam.
133
+ */
134
+ private async warnAboutUnseededPrompts() {
135
+ const definitions = listAiPromptDefinitions();
136
+ if (!definitions.length) return;
137
+
138
+ const records = await this.getRecordsBySlugs(definitions.map((d) => d.slug));
139
+ const missing = definitions.filter((d) => !records.get(d.slug)).map((d) => d.slug);
140
+
141
+ if (missing.length) {
142
+ this.logger.warn(
143
+ `${missing.length} AI prompt(s) declared in code have no ai_instruction row ` +
144
+ `and will use the code fallback: ${missing.join(', ')}`,
145
+ );
45
146
  }
46
147
  }
47
148
 
@@ -50,70 +151,349 @@ export class AiInstructionService implements OnModuleInit {
50
151
  async getLayer(layer: AiInstructionLayer, moduleSlug?: string): Promise<string> {
51
152
  const cacheKey = moduleSlug ? `${layer}:${moduleSlug}` : layer;
52
153
 
53
- const cached = this.cache.get(cacheKey);
54
- if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
55
- return cached.content;
154
+ await this.ensureCacheVersionFresh();
155
+
156
+ const cached = this.readCache(cacheKey);
157
+ if (cached !== undefined) {
158
+ return cached?.content ?? (layer === 'system' ? FALLBACK_SYSTEM_PROMPT : '');
56
159
  }
57
160
 
58
161
  const instruction = await this.findActiveByLayer(layer, moduleSlug);
162
+ this.writeCache(cacheKey, instruction);
163
+
59
164
  if (!instruction) {
60
- if (layer === 'system') {
61
- return FALLBACK_SYSTEM_PROMPT;
62
- }
63
- return '';
165
+ return layer === 'system' ? FALLBACK_SYSTEM_PROMPT : '';
64
166
  }
65
-
66
- this.cache.set(cacheKey, { content: instruction.content, fetchedAt: Date.now() });
67
167
  return instruction.content;
68
168
  }
69
169
 
70
170
  async getAgentInstruction(agentSlug: string): Promise<string> {
71
171
  const cacheKey = `agent:${agentSlug}`;
72
- const cached = this.cache.get(cacheKey);
73
- if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
74
- return cached.content;
75
- }
172
+
173
+ await this.ensureCacheVersionFresh();
174
+
175
+ const cached = this.readCache(cacheKey);
176
+ if (cached !== undefined) return cached?.content ?? '';
76
177
 
77
178
  const instruction = await this.findActiveBySlug(`agent.${agentSlug}`);
78
- if (!instruction) return '';
179
+ this.writeCache(cacheKey, instruction);
79
180
 
80
- this.cache.set(cacheKey, { content: instruction.content, fetchedAt: Date.now() });
81
- return instruction.content;
181
+ return instruction?.content ?? '';
82
182
  }
83
183
 
84
- invalidateCache(slug?: string) {
85
- if (slug) {
86
- // Cache keys use format "layer" or "layer:identifier"
87
- // Slugs use format "layer.identifier" (e.g., "module.lms", "agent.admin-support")
88
- const dotIndex = slug.indexOf('.');
89
- if (dotIndex === -1) {
90
- this.cache.delete(slug);
184
+ /**
185
+ * Full record by exact slug, cached. This is what `AiPromptResolverService`
186
+ * uses for task prompts — it needs the model params and `is_locked`, not just
187
+ * the content.
188
+ */
189
+ async getRecordBySlug(slug: string): Promise<AiInstructionRecord | null> {
190
+ const cacheKey = `slug:${slug}`;
191
+
192
+ await this.ensureCacheVersionFresh();
193
+
194
+ const cached = this.readCache(cacheKey);
195
+ if (cached !== undefined) return cached;
196
+
197
+ const record = await this.findBySlug(slug);
198
+ this.writeCache(cacheKey, record);
199
+
200
+ return record;
201
+ }
202
+
203
+ /** One query for pipelines that resolve several prompts per call. */
204
+ async getRecordsBySlugs(slugs: string[]): Promise<Map<string, AiInstructionRecord | null>> {
205
+ const result = new Map<string, AiInstructionRecord | null>();
206
+ const missing: string[] = [];
207
+
208
+ await this.ensureCacheVersionFresh();
209
+
210
+ for (const slug of slugs) {
211
+ const cached = this.readCache(`slug:${slug}`);
212
+ if (cached !== undefined) {
213
+ result.set(slug, cached);
91
214
  } else {
92
- const layer = slug.slice(0, dotIndex);
93
- const identifier = slug.slice(dotIndex + 1);
94
- this.cache.delete(layer);
95
- this.cache.delete(`${layer}:${identifier}`);
215
+ missing.push(slug);
96
216
  }
97
- } else {
217
+ }
218
+
219
+ if (missing.length) {
220
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
221
+ SELECT ${INSTRUCTION_COLUMNS}
222
+ FROM ai_instruction WHERE slug IN (${Prisma.join(missing)})
223
+ `);
224
+
225
+ const found = new Map(rows.map((row) => [row.slug, row]));
226
+ for (const slug of missing) {
227
+ const record = found.get(slug) || null;
228
+ this.writeCache(`slug:${slug}`, record);
229
+ result.set(slug, record);
230
+ }
231
+ }
232
+
233
+ return result;
234
+ }
235
+
236
+ invalidateCache(slug?: string) {
237
+ if (!slug) {
98
238
  this.cache.clear();
239
+ return;
240
+ }
241
+
242
+ // Exact-slug key used by getRecordBySlug/getRecordsBySlugs.
243
+ this.cache.delete(`slug:${slug}`);
244
+
245
+ // Layer keys use format "layer" or "layer:identifier", while slugs use
246
+ // "layer.identifier" (e.g. "module.lms", "agent.admin-support").
247
+ const dotIndex = slug.indexOf('.');
248
+ if (dotIndex === -1) {
249
+ this.cache.delete(slug);
250
+ } else {
251
+ const layer = slug.slice(0, dotIndex);
252
+ const identifier = slug.slice(dotIndex + 1);
253
+ this.cache.delete(layer);
254
+ this.cache.delete(`${layer}:${identifier}`);
255
+ }
256
+ }
257
+
258
+ /** `undefined` = cache miss/expired. `null` = cached absence. */
259
+ private readCache(cacheKey: string): AiInstructionRecord | null | undefined {
260
+ const cached = this.cache.get(cacheKey);
261
+ if (!cached) return undefined;
262
+ if (Date.now() - cached.fetchedAt >= cached.ttl) {
263
+ this.cache.delete(cacheKey);
264
+ return undefined;
265
+ }
266
+ return cached.record;
267
+ }
268
+
269
+ private writeCache(cacheKey: string, record: AiInstructionRecord | null) {
270
+ const ttl = !record
271
+ ? NEGATIVE_CACHE_TTL_MS
272
+ : record.layer === 'task'
273
+ ? TASK_CACHE_TTL_MS
274
+ : CACHE_TTL_MS;
275
+
276
+ this.cache.set(cacheKey, { record, fetchedAt: Date.now(), ttl });
277
+ }
278
+
279
+ // ── Cross-pod invalidation ───────────────────────────────────────────────
280
+
281
+ private handleInvalidationMessage(raw: string) {
282
+ let payload: AiInstructionInvalidationMessage;
283
+
284
+ try {
285
+ payload = JSON.parse(raw) as AiInstructionInvalidationMessage;
286
+ } catch {
287
+ return;
288
+ }
289
+
290
+ if (!payload || payload.originInstanceId === this.instanceId) {
291
+ return;
292
+ }
293
+
294
+ // The version is only a watermark, so the lazy check does not re-clear a
295
+ // cache this message already fixed selectively. The invalidation itself is
296
+ // applied unconditionally: dropping an out-of-order message — this pod may
297
+ // have bumped past it on a write of its own — would leave the slug stale
298
+ // here until its TTL, which is exactly the bug this whole path fixes.
299
+ if (Number.isFinite(payload.version)) {
300
+ this.localCacheVersion = Math.max(this.localCacheVersion, payload.version);
301
+ this.lastVersionCheckAt = Date.now();
302
+ }
303
+
304
+ const slugs = Array.isArray(payload.slugs) ? payload.slugs.filter(Boolean) : [];
305
+ if (!slugs.length) {
306
+ this.invalidateCache();
307
+ return;
308
+ }
309
+
310
+ for (const slug of slugs) {
311
+ this.invalidateCache(slug);
312
+ }
313
+ }
314
+
315
+ private async ensureVersionState() {
316
+ // The row is created once and never removed; repeating the INSERT on every
317
+ // version check would double the queries of a path that runs every 5s.
318
+ if (this.versionStateEnsured) {
319
+ return;
320
+ }
321
+
322
+ await this.prisma.$executeRawUnsafe(`
323
+ INSERT INTO "setting_runtime_state" ("scope", "version", "updated_at")
324
+ VALUES ('${RUNTIME_STATE_SCOPE}', 0, NOW())
325
+ ON CONFLICT ("scope") DO NOTHING
326
+ `);
327
+ this.versionStateEnsured = true;
328
+ }
329
+
330
+ /** BIGINT comes back from `$queryRawUnsafe` as a `bigint`, not a number. */
331
+ private parseVersionValue(value: unknown): number {
332
+ if (typeof value === 'number') {
333
+ return value;
334
+ }
335
+
336
+ if (typeof value === 'bigint') {
337
+ return Number(value);
338
+ }
339
+
340
+ if (typeof value === 'string') {
341
+ return Number(value);
342
+ }
343
+
344
+ return 0;
345
+ }
346
+
347
+ private async getCacheVersionFromDatabase(): Promise<number> {
348
+ await this.ensureVersionState();
349
+
350
+ const rows = await this.prisma.$queryRawUnsafe<Array<{ version: unknown }>>(`
351
+ SELECT "version"
352
+ FROM "setting_runtime_state"
353
+ WHERE "scope" = '${RUNTIME_STATE_SCOPE}'
354
+ LIMIT 1
355
+ `);
356
+
357
+ return this.parseVersionValue(rows[0]?.version);
358
+ }
359
+
360
+ private async syncCacheVersionFromDatabase() {
361
+ this.localCacheVersion = await this.getCacheVersionFromDatabase();
362
+ this.lastVersionCheckAt = Date.now();
363
+ }
364
+
365
+ private async bumpCacheVersion(): Promise<number> {
366
+ await this.ensureVersionState();
367
+
368
+ const rows = await this.prisma.$queryRawUnsafe<Array<{ version: unknown }>>(`
369
+ UPDATE "setting_runtime_state"
370
+ SET "version" = "version" + 1,
371
+ "updated_at" = NOW()
372
+ WHERE "scope" = '${RUNTIME_STATE_SCOPE}'
373
+ RETURNING "version"
374
+ `);
375
+
376
+ const nextVersion = this.parseVersionValue(rows[0]?.version);
377
+ this.localCacheVersion = nextVersion;
378
+ this.lastVersionCheckAt = Date.now();
379
+ return nextVersion;
380
+ }
381
+
382
+ /**
383
+ * Catches up with writes from other pods when Redis is unavailable. Throttled
384
+ * and coalesced, so a burst of prompt resolutions costs one query at most.
385
+ */
386
+ private async ensureCacheVersionFresh() {
387
+ const now = Date.now();
388
+ if (now - this.lastVersionCheckAt < this.versionCheckIntervalMs) {
389
+ return;
390
+ }
391
+
392
+ // `lastVersionCheckAt` only advances after the await, so parallel reads
393
+ // would each fire the same query and tie up the connection pool.
394
+ if (this.versionCheckInFlight) {
395
+ return this.versionCheckInFlight;
396
+ }
397
+
398
+ this.versionCheckInFlight = this.runVersionCheck(now).finally(() => {
399
+ this.versionCheckInFlight = null;
400
+ });
401
+
402
+ return this.versionCheckInFlight;
403
+ }
404
+
405
+ private async runVersionCheck(startedAt: number) {
406
+ try {
407
+ const currentVersion = await this.getCacheVersionFromDatabase();
408
+ this.lastVersionCheckAt = startedAt;
409
+
410
+ if (currentVersion > this.localCacheVersion) {
411
+ this.localCacheVersion = currentVersion;
412
+ // This path knows a change happened but not which slugs it touched.
413
+ this.cache.clear();
414
+ }
415
+ } catch (error) {
416
+ // Bookkeeping must never break prompt resolution: the resolver handles a
417
+ // throw by falling back to the code prompt, so a slow database would
418
+ // silently downgrade prompt quality. Serving the current cache is better.
419
+ this.lastVersionCheckAt = startedAt; // and do not hammer a sick database
420
+ this.logVersionCheckError(error);
421
+ }
422
+ }
423
+
424
+ private logVersionCheckError(error: unknown) {
425
+ const now = Date.now();
426
+ if (now - this.lastVersionCheckErrorLogAt < 30000) {
427
+ return;
428
+ }
429
+ this.lastVersionCheckErrorLogAt = now;
430
+ this.logger.warn(
431
+ `Failed to check ai instruction cache version: ${error instanceof Error ? error.message : String(error)}`,
432
+ );
433
+ }
434
+
435
+ /**
436
+ * Tells the other pods to drop these slugs. Bumps the database version first
437
+ * so pods without Redis still catch up on their next version check.
438
+ */
439
+ private async publishInvalidation(slugs: string[]) {
440
+ const filteredSlugs = Array.from(new Set(slugs.filter(Boolean)));
441
+ if (filteredSlugs.length === 0) {
442
+ return;
443
+ }
444
+
445
+ let version = this.localCacheVersion;
446
+
447
+ try {
448
+ version = await this.bumpCacheVersion();
449
+ } catch (error) {
450
+ // The prompt write already committed; failing here must not turn a
451
+ // successful edit into a 500.
452
+ this.logger.warn(
453
+ `Failed to bump ai instruction cache version: ${error instanceof Error ? error.message : String(error)}`,
454
+ );
99
455
  }
456
+
457
+ // A stale version here is harmless: receivers take the max, and the lazy
458
+ // check compares against the database, not against this number.
459
+ await this.pubsub?.publish({
460
+ originInstanceId: this.instanceId,
461
+ version,
462
+ slugs: filteredSlugs,
463
+ } satisfies AiInstructionInvalidationMessage);
100
464
  }
101
465
 
102
466
  // ── CRUD ─────────────────────────────────────────────────────────────────
103
467
 
104
- async list(paginationParams: PaginationDTO) {
468
+ async list(paginationParams: PaginationDTO, filters?: AiInstructionListFilters) {
105
469
  const page = Math.max(Number(paginationParams?.page || 1), 1);
106
470
  const pageSize = Math.min(Math.max(Number(paginationParams?.pageSize || 20), 1), 100);
107
471
  const offset = (page - 1) * pageSize;
108
472
  const search = String(paginationParams?.search || '').trim();
109
473
 
110
- const whereClause = search
111
- ? Prisma.sql`WHERE (LOWER(slug) LIKE LOWER(${`%${search}%`}) OR LOWER(name) LIKE LOWER(${`%${search}%`}))`
474
+ const conditions: Prisma.Sql[] = [];
475
+
476
+ if (search) {
477
+ conditions.push(
478
+ Prisma.sql`(LOWER(slug) LIKE LOWER(${`%${search}%`}) OR LOWER(name) LIKE LOWER(${`%${search}%`}))`,
479
+ );
480
+ }
481
+ if (filters?.layer) {
482
+ conditions.push(Prisma.sql`layer = CAST(${filters.layer} AS ai_instruction_layer_enum)`);
483
+ }
484
+ if (filters?.module_slug) {
485
+ conditions.push(Prisma.sql`module_slug = ${filters.module_slug}`);
486
+ }
487
+ if (filters?.is_active !== undefined) {
488
+ conditions.push(Prisma.sql`is_active = ${filters.is_active}`);
489
+ }
490
+
491
+ const whereClause = conditions.length
492
+ ? Prisma.sql`WHERE ${Prisma.join(conditions, ' AND ')}`
112
493
  : Prisma.empty;
113
494
 
114
495
  const data = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
115
- SELECT id, slug, layer, name, content, variables_schema, locale, module_slug,
116
- is_active, version, created_at, updated_at
496
+ SELECT ${INSTRUCTION_COLUMNS}
117
497
  FROM ai_instruction
118
498
  ${whereClause}
119
499
  ORDER BY layer, slug
@@ -136,14 +516,95 @@ export class AiInstructionService implements OnModuleInit {
136
516
  };
137
517
  }
138
518
 
519
+ /** Distinct module slugs, for the module filter in the admin. */
520
+ async listModules(): Promise<string[]> {
521
+ const rows = await this.prisma.$queryRaw<Array<{ module_slug: string }>>(Prisma.sql`
522
+ SELECT DISTINCT module_slug FROM ai_instruction
523
+ WHERE module_slug IS NOT NULL
524
+ ORDER BY module_slug
525
+ `);
526
+ return rows.map((row) => row.module_slug);
527
+ }
528
+
529
+ /**
530
+ * The prompts declared in code, paired with what the database currently holds.
531
+ *
532
+ * `dbMatchesFallback` is what lets the admin show "default" vs "customized"
533
+ * and offer "restore default"; it is also the fidelity check after a seed —
534
+ * every freshly seeded prompt must report `true`.
535
+ */
536
+ async listRegistry(moduleSlug?: string) {
537
+ const definitions = listAiPromptDefinitions(moduleSlug);
538
+ const records = await this.getRecordsBySlugs(definitions.map((d) => d.slug));
539
+
540
+ return definitions.map((definition) => {
541
+ const record = records.get(definition.slug) || null;
542
+
543
+ return {
544
+ slug: definition.slug,
545
+ name: definition.name,
546
+ module_slug: definition.moduleSlug,
547
+ locale: definition.locale || 'pt',
548
+ fallback: definition.fallback,
549
+ variables: definition.variables,
550
+ security_sensitive: Boolean(definition.securitySensitive),
551
+ required_phrases: definition.requiredPhrases || [],
552
+ defaults: definition.defaults || {},
553
+ exists_in_db: Boolean(record),
554
+ db_matches_fallback: record ? record.content === definition.fallback : false,
555
+ };
556
+ });
557
+ }
558
+
559
+ /** Renders a draft without saving it, so the admin can check interpolation. */
560
+ previewPrompt(input: PreviewAiInstructionDTO) {
561
+ const definition = input.slug ? getAiPromptDefinition(input.slug) : undefined;
562
+ const schema = definition?.variables ?? input.variables_schema ?? null;
563
+ const validation = validateTemplate(input.content, schema);
564
+
565
+ let rendered = '';
566
+ let renderError: string | null = null;
567
+ try {
568
+ rendered = renderTemplate(input.content, input.variables || {});
569
+ } catch (error) {
570
+ renderError = error instanceof Error ? error.message : String(error);
571
+ }
572
+
573
+ const warnings: string[] = [];
574
+ if (validation.unknownPlaceholders.length) {
575
+ warnings.push(
576
+ `Variáveis não declaradas: ${validation.unknownPlaceholders.join(', ')}. ` +
577
+ 'Elas serão renderizadas literalmente.',
578
+ );
579
+ }
580
+ warnings.push(...this.missingPhraseWarnings(definition, input.content));
581
+
582
+ return {
583
+ rendered,
584
+ render_error: renderError,
585
+ char_count: rendered.length,
586
+ estimated_tokens: estimateTokens(rendered),
587
+ missing_required: validation.missingRequired,
588
+ unknown_placeholders: validation.unknownPlaceholders,
589
+ unbalanced_blocks: validation.unbalancedBlocks,
590
+ warnings,
591
+ };
592
+ }
593
+
139
594
  async getById(id: number): Promise<AiInstructionRecord> {
140
595
  const record = await this.findById(id);
141
596
  if (!record) throw new NotFoundException(`AI instruction ${id} not found.`);
142
597
  return record;
143
598
  }
144
599
 
145
- async getBySlug(slug: string): Promise<AiInstructionRecord> {
146
- const record = await this.findActiveBySlug(slug);
600
+ /**
601
+ * Inactive records are returned on purpose: the admin needs to open one to
602
+ * reactivate it. Pass `activeOnly` for the resolution path.
603
+ */
604
+ async getBySlug(slug: string, activeOnly = false): Promise<AiInstructionRecord> {
605
+ const record = activeOnly
606
+ ? await this.findActiveBySlug(slug)
607
+ : await this.findBySlug(slug);
147
608
  if (!record) throw new NotFoundException(`AI instruction "${slug}" not found.`);
148
609
  return record;
149
610
  }
@@ -152,8 +613,18 @@ export class AiInstructionService implements OnModuleInit {
152
613
  const locale = dto.locale || 'en';
153
614
  const isActive = dto.is_active ?? true;
154
615
 
616
+ const existing = await this.findBySlug(dto.slug);
617
+ if (existing) {
618
+ throw new ConflictException(`AI instruction "${dto.slug}" already exists.`);
619
+ }
620
+
621
+ this.validateContent(dto.slug, dto.content, {
622
+ fallbackSchema: dto.variables_schema ?? null,
623
+ allowMissingVariables: dto.allow_missing_variables,
624
+ });
625
+
155
626
  await this.prisma.$executeRaw`
156
- INSERT INTO ai_instruction (slug, layer, name, content, variables_schema, locale, module_slug, is_active, version)
627
+ INSERT INTO ai_instruction (slug, layer, name, content, variables_schema, locale, module_slug, is_active, version, model, temperature, max_tokens)
157
628
  VALUES (
158
629
  ${dto.slug},
159
630
  CAST(${dto.layer} AS ai_instruction_layer_enum),
@@ -163,28 +634,61 @@ export class AiInstructionService implements OnModuleInit {
163
634
  ${locale},
164
635
  ${dto.module_slug || null},
165
636
  ${isActive},
166
- 1
637
+ 1,
638
+ ${dto.model ?? null},
639
+ ${dto.temperature ?? null},
640
+ ${dto.max_tokens ?? null}
167
641
  )
168
642
  `;
169
643
 
170
- const created = await this.findActiveBySlug(dto.slug);
644
+ const created = await this.findBySlug(dto.slug);
171
645
 
172
646
  // Record version 1
173
647
  if (created) {
174
648
  await this.saveVersion(created.id, 1, dto.content, 'Initial version', createdById);
175
649
  }
176
650
 
651
+ // Negative cache entries for this slug must go, otherwise the prompt stays
652
+ // invisible to the resolver until the TTL expires. Kept separate from the
653
+ // publish below so a Redis outage never leaves this pod itself stale.
654
+ this.invalidateCache(dto.slug);
655
+ await this.publishInvalidation([dto.slug]);
656
+
177
657
  return created;
178
658
  }
179
659
 
180
660
  async update(id: number, dto: UpdateAiInstructionDTO, updatedById?: number) {
181
661
  const current = await this.getById(id);
182
662
 
663
+ // Locked records are security-sensitive prompts (the essay-grading
664
+ // injection defence). Still editable, but never by accident.
665
+ if (current.is_locked && !dto.acknowledge_security_review) {
666
+ throw new ConflictException(
667
+ `AI instruction "${current.slug}" is a security-sensitive prompt. ` +
668
+ 'Send acknowledge_security_review=true to confirm you reviewed the change.',
669
+ );
670
+ }
671
+
183
672
  const newContent = dto.content ?? current.content;
184
673
  const newVersion = dto.content && dto.content !== current.content
185
674
  ? current.version + 1
186
675
  : current.version;
187
676
 
677
+ const warnings = dto.content !== undefined
678
+ ? this.validateContent(current.slug, dto.content, {
679
+ fallbackSchema: current.variables_schema,
680
+ allowMissingVariables: dto.allow_missing_variables,
681
+ })
682
+ : [];
683
+
684
+ // Losing every guard phrase is recorded in the immutable version row, not
685
+ // just in the log — the resolver will also refuse the stored copy.
686
+ const changeNote = warnings.length
687
+ ? [dto.change_note, `[warnings: ${warnings.join(' | ')}]`].filter(Boolean).join(' ').slice(0, 500)
688
+ : dto.change_note;
689
+
690
+ // `undefined` means "leave alone", `null` means "clear" — `??` collapses the
691
+ // two, so the model params use an explicit undefined check.
188
692
  await this.prisma.$executeRaw`
189
693
  UPDATE ai_instruction
190
694
  SET
@@ -194,6 +698,9 @@ export class AiInstructionService implements OnModuleInit {
194
698
  locale = ${dto.locale ?? current.locale},
195
699
  module_slug = ${dto.module_slug !== undefined ? dto.module_slug : current.module_slug},
196
700
  is_active = ${dto.is_active ?? current.is_active},
701
+ model = ${dto.model !== undefined ? dto.model : current.model},
702
+ temperature = ${dto.temperature !== undefined ? dto.temperature : current.temperature},
703
+ max_tokens = ${dto.max_tokens !== undefined ? dto.max_tokens : current.max_tokens},
197
704
  version = ${newVersion},
198
705
  updated_at = NOW()
199
706
  WHERE id = ${id}
@@ -201,12 +708,13 @@ export class AiInstructionService implements OnModuleInit {
201
708
 
202
709
  // Save new version if content changed
203
710
  if (dto.content && dto.content !== current.content) {
204
- await this.saveVersion(id, newVersion, newContent, dto.change_note, updatedById);
711
+ await this.saveVersion(id, newVersion, newContent, changeNote, updatedById);
205
712
  }
206
713
 
207
714
  this.invalidateCache(current.slug);
715
+ await this.publishInvalidation([current.slug]);
208
716
 
209
- return this.getById(id);
717
+ return { ...(await this.getById(id)), warnings };
210
718
  }
211
719
 
212
720
  async delete({ ids }: DeleteDTO) {
@@ -224,6 +732,8 @@ export class AiInstructionService implements OnModuleInit {
224
732
  `;
225
733
 
226
734
  existing.forEach((r) => this.invalidateCache(r.slug));
735
+ // One bump and one message for the whole batch.
736
+ await this.publishInvalidation(existing.map((r) => r.slug));
227
737
 
228
738
  return { count: ids.length };
229
739
  }
@@ -270,27 +780,116 @@ export class AiInstructionService implements OnModuleInit {
270
780
  );
271
781
 
272
782
  this.invalidateCache(current.slug);
783
+ await this.publishInvalidation([current.slug]);
273
784
 
274
785
  return this.getById(id);
275
786
  }
276
787
 
277
788
  // ── Private helpers ───────────────────────────────────────────────────────
278
789
 
790
+ /**
791
+ * Rejects content that would break at render time, and returns non-fatal
792
+ * warnings for the caller to surface.
793
+ *
794
+ * An unclosed block is a hard 422: it silently swallows the rest of the
795
+ * prompt. A removed required variable is also a 422, overridable with
796
+ * `allow_missing_variables` — sometimes a rewrite genuinely no longer needs a
797
+ * variable. An unknown placeholder is only a warning: it renders literally,
798
+ * which is ugly but harmless.
799
+ */
800
+ private validateContent(
801
+ slug: string,
802
+ content: string,
803
+ options: {
804
+ fallbackSchema?: Record<string, any> | null;
805
+ allowMissingVariables?: boolean;
806
+ },
807
+ ): string[] {
808
+ const definition = getAiPromptDefinition(slug);
809
+ const schema = definition?.variables ?? options.fallbackSchema ?? null;
810
+ const { missingRequired, unknownPlaceholders, unbalancedBlocks } = validateTemplate(
811
+ content,
812
+ schema,
813
+ );
814
+
815
+ if (unbalancedBlocks.length) {
816
+ throw new UnprocessableEntityException(
817
+ `Bloco de template não fechado: ${unbalancedBlocks
818
+ .map((name) => `{{#${name}}}`)
819
+ .join(', ')}. Isso descartaria o resto do prompt.`,
820
+ );
821
+ }
822
+
823
+ if (missingRequired.length && !options.allowMissingVariables) {
824
+ throw new UnprocessableEntityException(
825
+ `Variáveis obrigatórias ausentes do conteúdo: ${missingRequired.join(', ')}. ` +
826
+ 'Envie allow_missing_variables=true para salvar mesmo assim.',
827
+ );
828
+ }
829
+
830
+ const warnings: string[] = [];
831
+ if (missingRequired.length) {
832
+ warnings.push(`missing-variables: ${missingRequired.join(', ')}`);
833
+ }
834
+ if (unknownPlaceholders.length) {
835
+ warnings.push(`unknown-variables: ${unknownPlaceholders.join(', ')}`);
836
+ }
837
+ warnings.push(...this.missingPhraseWarnings(definition, content));
838
+
839
+ return warnings;
840
+ }
841
+
842
+ /**
843
+ * Guard phrases are the runtime protection for security-sensitive prompts: if
844
+ * the stored content contains none of them, `AiPromptResolverService` discards
845
+ * it. Warn loudly here so the editor learns it before the first graded essay.
846
+ */
847
+ private missingPhraseWarnings(
848
+ definition: AiPromptDefinition | undefined,
849
+ content: string,
850
+ ): string[] {
851
+ const phrases = definition?.requiredPhrases;
852
+ if (!phrases?.length) return [];
853
+
854
+ const missing = phrases.filter((phrase) => !content.includes(phrase));
855
+ if (!missing.length) return [];
856
+
857
+ const hasAny = missing.length < phrases.length;
858
+ const warning = `security-phrases-removed: ${missing.join(', ')}`;
859
+
860
+ if (hasAny) {
861
+ this.logger.warn(`AI prompt "${definition!.slug}" lost safety phrases: ${missing.join(', ')}`);
862
+ return [warning];
863
+ }
864
+
865
+ this.logger.error(
866
+ `AI prompt "${definition!.slug}" lost ALL of its safety phrases; the resolver ` +
867
+ 'will ignore the stored content and use the code fallback.',
868
+ );
869
+ return [`${warning} (todas — o prompt do banco será ignorado em runtime)`];
870
+ }
871
+
279
872
  private async findById(id: number): Promise<AiInstructionRecord | null> {
280
- const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>`
281
- SELECT id, slug, layer, name, content, variables_schema, locale, module_slug,
282
- is_active, version, created_at, updated_at
873
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
874
+ SELECT ${INSTRUCTION_COLUMNS}
283
875
  FROM ai_instruction WHERE id = ${id} LIMIT 1
284
- `;
876
+ `);
877
+ return rows?.[0] || null;
878
+ }
879
+
880
+ private async findBySlug(slug: string): Promise<AiInstructionRecord | null> {
881
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
882
+ SELECT ${INSTRUCTION_COLUMNS}
883
+ FROM ai_instruction WHERE slug = ${slug} LIMIT 1
884
+ `);
285
885
  return rows?.[0] || null;
286
886
  }
287
887
 
288
888
  private async findActiveBySlug(slug: string): Promise<AiInstructionRecord | null> {
289
- const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>`
290
- SELECT id, slug, layer, name, content, variables_schema, locale, module_slug,
291
- is_active, version, created_at, updated_at
889
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
890
+ SELECT ${INSTRUCTION_COLUMNS}
292
891
  FROM ai_instruction WHERE slug = ${slug} AND is_active = TRUE LIMIT 1
293
- `;
892
+ `);
294
893
  return rows?.[0] || null;
295
894
  }
296
895
 
@@ -299,27 +898,27 @@ export class AiInstructionService implements OnModuleInit {
299
898
  moduleSlug?: string,
300
899
  ): Promise<AiInstructionRecord | null> {
301
900
  if (moduleSlug) {
302
- const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>`
303
- SELECT id, slug, layer, name, content, variables_schema, locale, module_slug,
304
- is_active, version, created_at, updated_at
901
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
902
+ SELECT ${INSTRUCTION_COLUMNS}
305
903
  FROM ai_instruction
306
904
  WHERE layer = CAST(${layer} AS ai_instruction_layer_enum)
307
905
  AND module_slug = ${moduleSlug}
308
906
  AND is_active = TRUE
907
+ ORDER BY id
309
908
  LIMIT 1
310
- `;
909
+ `);
311
910
  if (rows?.[0]) return rows[0];
312
911
  }
313
912
 
314
- const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>`
315
- SELECT id, slug, layer, name, content, variables_schema, locale, module_slug,
316
- is_active, version, created_at, updated_at
913
+ const rows = await this.prisma.$queryRaw<AiInstructionRecord[]>(Prisma.sql`
914
+ SELECT ${INSTRUCTION_COLUMNS}
317
915
  FROM ai_instruction
318
916
  WHERE layer = CAST(${layer} AS ai_instruction_layer_enum)
319
917
  AND module_slug IS NULL
320
918
  AND is_active = TRUE
919
+ ORDER BY id
321
920
  LIMIT 1
322
- `;
921
+ `);
323
922
  return rows?.[0] || null;
324
923
  }
325
924