@hed-hog/core 0.0.390 → 0.0.394

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 (528) hide show
  1. package/README.md +39 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -1
  3. package/dist/access-log/access-log.controller.js +1 -1
  4. package/dist/access-log/access-log.controller.js.map +1 -1
  5. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  6. package/dist/access-log/access-log.interceptor.js +8 -5
  7. package/dist/access-log/access-log.interceptor.js.map +1 -1
  8. package/dist/access-log/access-log.service.d.ts +4 -0
  9. package/dist/access-log/access-log.service.d.ts.map +1 -1
  10. package/dist/access-log/access-log.service.js +31 -4
  11. package/dist/access-log/access-log.service.js.map +1 -1
  12. package/dist/access-log/dto/query-access-log.dto.d.ts +2 -0
  13. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -1
  14. package/dist/access-log/dto/query-access-log.dto.js +5 -0
  15. package/dist/access-log/dto/query-access-log.dto.js.map +1 -1
  16. package/dist/ai/ai.controller.d.ts +12 -8
  17. package/dist/ai/ai.controller.d.ts.map +1 -1
  18. package/dist/ai/ai.controller.js +17 -2
  19. package/dist/ai/ai.controller.js.map +1 -1
  20. package/dist/ai/ai.module.d.ts.map +1 -1
  21. package/dist/ai/ai.module.js +3 -2
  22. package/dist/ai/ai.module.js.map +1 -1
  23. package/dist/ai/ai.service.d.ts +30 -1
  24. package/dist/ai/ai.service.d.ts.map +1 -1
  25. package/dist/ai/ai.service.js +97 -3
  26. package/dist/ai/ai.service.js.map +1 -1
  27. package/dist/ai/dto/richtext-edit-html.dto.d.ts +5 -0
  28. package/dist/ai/dto/richtext-edit-html.dto.d.ts.map +1 -0
  29. package/dist/ai/dto/richtext-edit-html.dto.js +27 -0
  30. package/dist/ai/dto/richtext-edit-html.dto.js.map +1 -0
  31. package/dist/ai/richtext-ai.prompts.d.ts +5 -0
  32. package/dist/ai/richtext-ai.prompts.d.ts.map +1 -0
  33. package/dist/ai/richtext-ai.prompts.js +57 -0
  34. package/dist/ai/richtext-ai.prompts.js.map +1 -0
  35. package/dist/ai/richtext-ai.service.d.ts +34 -0
  36. package/dist/ai/richtext-ai.service.d.ts.map +1 -0
  37. package/dist/ai/richtext-ai.service.js +95 -0
  38. package/dist/ai/richtext-ai.service.js.map +1 -0
  39. package/dist/ai-instruction/ai-instruction.controller.d.ts +5 -5
  40. package/dist/auth/auth.controller.d.ts +66 -7
  41. package/dist/auth/auth.controller.d.ts.map +1 -1
  42. package/dist/auth/auth.controller.js +67 -23
  43. package/dist/auth/auth.controller.js.map +1 -1
  44. package/dist/auth/auth.module.d.ts.map +1 -1
  45. package/dist/auth/auth.module.js +2 -0
  46. package/dist/auth/auth.module.js.map +1 -1
  47. package/dist/auth/auth.service.d.ts +136 -4
  48. package/dist/auth/auth.service.d.ts.map +1 -1
  49. package/dist/auth/auth.service.js +246 -48
  50. package/dist/auth/auth.service.js.map +1 -1
  51. package/dist/auth/dto/confirm-email.dto.d.ts +4 -0
  52. package/dist/auth/dto/confirm-email.dto.d.ts.map +1 -0
  53. package/dist/auth/dto/confirm-email.dto.js +22 -0
  54. package/dist/auth/dto/confirm-email.dto.js.map +1 -0
  55. package/dist/auth/dto/resend-confirmation.dto.d.ts +8 -0
  56. package/dist/auth/dto/resend-confirmation.dto.d.ts.map +1 -0
  57. package/dist/auth/dto/resend-confirmation.dto.js +36 -0
  58. package/dist/auth/dto/resend-confirmation.dto.js.map +1 -0
  59. package/dist/authorize/authorize.controller.d.ts +1 -1
  60. package/dist/authorize/authorize.controller.d.ts.map +1 -1
  61. package/dist/authorize/authorize.controller.js +4 -2
  62. package/dist/authorize/authorize.controller.js.map +1 -1
  63. package/dist/authorize/authorize.service.d.ts +7 -0
  64. package/dist/authorize/authorize.service.d.ts.map +1 -1
  65. package/dist/authorize/authorize.service.js +1 -1
  66. package/dist/authorize/authorize.service.js.map +1 -1
  67. package/dist/challenge/challenge.service.d.ts +18 -5
  68. package/dist/challenge/challenge.service.d.ts.map +1 -1
  69. package/dist/challenge/challenge.service.js +50 -3
  70. package/dist/challenge/challenge.service.js.map +1 -1
  71. package/dist/core.module.d.ts.map +1 -1
  72. package/dist/core.module.js +17 -0
  73. package/dist/core.module.js.map +1 -1
  74. package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -6
  75. package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -6
  76. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +4 -4
  77. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +4 -4
  78. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -1
  79. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -1
  80. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +2 -2
  81. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  82. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -2
  83. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -2
  84. package/dist/file/file.controller.d.ts +8 -1
  85. package/dist/file/file.controller.d.ts.map +1 -1
  86. package/dist/file/file.controller.js +45 -7
  87. package/dist/file/file.controller.js.map +1 -1
  88. package/dist/file/file.service.d.ts +128 -1
  89. package/dist/file/file.service.d.ts.map +1 -1
  90. package/dist/file/file.service.js +319 -8
  91. package/dist/file/file.service.js.map +1 -1
  92. package/dist/file/image-optimization.service.d.ts +43 -0
  93. package/dist/file/image-optimization.service.d.ts.map +1 -1
  94. package/dist/file/image-optimization.service.js +154 -0
  95. package/dist/file/image-optimization.service.js.map +1 -1
  96. package/dist/file/provider/abstract.provider.d.ts +16 -0
  97. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  98. package/dist/file/provider/abstract.provider.js +4 -0
  99. package/dist/file/provider/abstract.provider.js.map +1 -1
  100. package/dist/file/provider/gcs.provider.d.ts +2 -0
  101. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  102. package/dist/file/provider/gcs.provider.js +10 -0
  103. package/dist/file/provider/gcs.provider.js.map +1 -1
  104. package/dist/file/provider/local.provider.d.ts +1 -0
  105. package/dist/file/provider/local.provider.d.ts.map +1 -1
  106. package/dist/file/provider/local.provider.js +13 -0
  107. package/dist/file/provider/local.provider.js.map +1 -1
  108. package/dist/file/provider/s3.provider.d.ts +5 -0
  109. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  110. package/dist/file/provider/s3.provider.js +15 -0
  111. package/dist/file/provider/s3.provider.js.map +1 -1
  112. package/dist/file/provider/spaces.provider.d.ts +5 -0
  113. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  114. package/dist/file/provider/spaces.provider.js +16 -0
  115. package/dist/file/provider/spaces.provider.js.map +1 -1
  116. package/dist/impersonation/dto/redeem-impersonation.dto.d.ts +4 -0
  117. package/dist/impersonation/dto/redeem-impersonation.dto.d.ts.map +1 -0
  118. package/dist/impersonation/dto/redeem-impersonation.dto.js +23 -0
  119. package/dist/impersonation/dto/redeem-impersonation.dto.js.map +1 -0
  120. package/dist/impersonation/dto/start-impersonation.dto.d.ts +7 -0
  121. package/dist/impersonation/dto/start-impersonation.dto.d.ts.map +1 -0
  122. package/dist/impersonation/dto/start-impersonation.dto.js +27 -0
  123. package/dist/impersonation/dto/start-impersonation.dto.js.map +1 -0
  124. package/dist/impersonation/impersonation-restrictions.d.ts +26 -0
  125. package/dist/impersonation/impersonation-restrictions.d.ts.map +1 -0
  126. package/dist/impersonation/impersonation-restrictions.js +76 -0
  127. package/dist/impersonation/impersonation-restrictions.js.map +1 -0
  128. package/dist/impersonation/impersonation.controller.d.ts +90 -0
  129. package/dist/impersonation/impersonation.controller.d.ts.map +1 -0
  130. package/dist/impersonation/impersonation.controller.js +135 -0
  131. package/dist/impersonation/impersonation.controller.js.map +1 -0
  132. package/dist/impersonation/impersonation.module.d.ts +8 -0
  133. package/dist/impersonation/impersonation.module.d.ts.map +1 -0
  134. package/dist/impersonation/impersonation.module.js +44 -0
  135. package/dist/impersonation/impersonation.module.js.map +1 -0
  136. package/dist/impersonation/impersonation.service.d.ts +164 -0
  137. package/dist/impersonation/impersonation.service.d.ts.map +1 -0
  138. package/dist/impersonation/impersonation.service.js +408 -0
  139. package/dist/impersonation/impersonation.service.js.map +1 -0
  140. package/dist/index.d.ts +17 -0
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +19 -0
  143. package/dist/index.js.map +1 -1
  144. package/dist/integration/services/outbox-polling.coordinator.d.ts +2 -0
  145. package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -1
  146. package/dist/integration/services/outbox-polling.coordinator.js +50 -30
  147. package/dist/integration/services/outbox-polling.coordinator.js.map +1 -1
  148. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  149. package/dist/integration/services/outbox.service.js +5 -1
  150. package/dist/integration/services/outbox.service.js.map +1 -1
  151. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  152. package/dist/integration-profile/integration-profile.secrets.js +3 -0
  153. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  154. package/dist/integration-profile/integration-profile.utils.d.ts +14 -0
  155. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  156. package/dist/integration-profile/integration-profile.utils.js +26 -0
  157. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  158. package/dist/integration-profile/testers/email.testers.d.ts.map +1 -1
  159. package/dist/integration-profile/testers/email.testers.js +28 -0
  160. package/dist/integration-profile/testers/email.testers.js.map +1 -1
  161. package/dist/language/en.json +11 -0
  162. package/dist/language/pt.json +11 -0
  163. package/dist/mail/dto/send.dto.d.ts +38 -1
  164. package/dist/mail/dto/send.dto.d.ts.map +1 -1
  165. package/dist/mail/dto/send.dto.js +10 -0
  166. package/dist/mail/dto/send.dto.js.map +1 -1
  167. package/dist/mail/mail.controller.d.ts +1 -0
  168. package/dist/mail/mail.controller.d.ts.map +1 -1
  169. package/dist/mail/mail.module.d.ts.map +1 -1
  170. package/dist/mail/mail.module.js +2 -0
  171. package/dist/mail/mail.module.js.map +1 -1
  172. package/dist/mail/mail.service.d.ts +48 -4
  173. package/dist/mail/mail.service.d.ts.map +1 -1
  174. package/dist/mail/mail.service.js +187 -35
  175. package/dist/mail/mail.service.js.map +1 -1
  176. package/dist/mail-delivery/mail-delivery-provision.service.d.ts +21 -0
  177. package/dist/mail-delivery/mail-delivery-provision.service.d.ts.map +1 -0
  178. package/dist/mail-delivery/mail-delivery-provision.service.js +64 -0
  179. package/dist/mail-delivery/mail-delivery-provision.service.js.map +1 -0
  180. package/dist/mail-delivery/mail-delivery.constants.d.ts +17 -0
  181. package/dist/mail-delivery/mail-delivery.constants.d.ts.map +1 -0
  182. package/dist/mail-delivery/mail-delivery.constants.js +31 -0
  183. package/dist/mail-delivery/mail-delivery.constants.js.map +1 -0
  184. package/dist/mail-delivery/mail-delivery.controller.d.ts +19 -0
  185. package/dist/mail-delivery/mail-delivery.controller.d.ts.map +1 -0
  186. package/dist/mail-delivery/mail-delivery.controller.js +53 -0
  187. package/dist/mail-delivery/mail-delivery.controller.js.map +1 -0
  188. package/dist/mail-delivery/mail-delivery.module.d.ts +7 -0
  189. package/dist/mail-delivery/mail-delivery.module.d.ts.map +1 -0
  190. package/dist/mail-delivery/mail-delivery.module.js +36 -0
  191. package/dist/mail-delivery/mail-delivery.module.js.map +1 -0
  192. package/dist/mail-delivery/mail-delivery.service.d.ts +38 -0
  193. package/dist/mail-delivery/mail-delivery.service.d.ts.map +1 -0
  194. package/dist/mail-delivery/mail-delivery.service.js +140 -0
  195. package/dist/mail-delivery/mail-delivery.service.js.map +1 -0
  196. package/dist/mail-delivery/ses-event.parser.d.ts +27 -0
  197. package/dist/mail-delivery/ses-event.parser.d.ts.map +1 -0
  198. package/dist/mail-delivery/ses-event.parser.js +131 -0
  199. package/dist/mail-delivery/ses-event.parser.js.map +1 -0
  200. package/dist/mail-delivery/ses-webhook.service.d.ts +58 -0
  201. package/dist/mail-delivery/ses-webhook.service.d.ts.map +1 -0
  202. package/dist/mail-delivery/ses-webhook.service.js +214 -0
  203. package/dist/mail-delivery/ses-webhook.service.js.map +1 -0
  204. package/dist/mail-delivery/sns-message.util.d.ts +38 -0
  205. package/dist/mail-delivery/sns-message.util.d.ts.map +1 -0
  206. package/dist/mail-delivery/sns-message.util.js +109 -0
  207. package/dist/mail-delivery/sns-message.util.js.map +1 -0
  208. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts +5 -0
  209. package/dist/mail-sent/dto/query-mail-sent-list.dto.d.ts.map +1 -1
  210. package/dist/mail-sent/dto/query-mail-sent-list.dto.js +6 -0
  211. package/dist/mail-sent/dto/query-mail-sent-list.dto.js.map +1 -1
  212. package/dist/mail-sent/mail-sent.controller.d.ts +39 -0
  213. package/dist/mail-sent/mail-sent.controller.d.ts.map +1 -1
  214. package/dist/mail-sent/mail-sent.service.d.ts +39 -0
  215. package/dist/mail-sent/mail-sent.service.d.ts.map +1 -1
  216. package/dist/mail-sent/mail-sent.service.js +45 -4
  217. package/dist/mail-sent/mail-sent.service.js.map +1 -1
  218. package/dist/mailbox/mailbox-fetcher.registry.d.ts +100 -0
  219. package/dist/mailbox/mailbox-fetcher.registry.d.ts.map +1 -0
  220. package/dist/mailbox/mailbox-fetcher.registry.js +59 -0
  221. package/dist/mailbox/mailbox-fetcher.registry.js.map +1 -0
  222. package/dist/mailbox/mailbox.module.d.ts +8 -0
  223. package/dist/mailbox/mailbox.module.d.ts.map +1 -0
  224. package/dist/mailbox/mailbox.module.js +27 -0
  225. package/dist/mailbox/mailbox.module.js.map +1 -0
  226. package/dist/meeting/meeting-provider.registry.d.ts +155 -0
  227. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -0
  228. package/dist/meeting/meeting-provider.registry.js +53 -0
  229. package/dist/meeting/meeting-provider.registry.js.map +1 -0
  230. package/dist/meeting/meeting.constants.d.ts +47 -0
  231. package/dist/meeting/meeting.constants.d.ts.map +1 -0
  232. package/dist/meeting/meeting.constants.js +21 -0
  233. package/dist/meeting/meeting.constants.js.map +1 -0
  234. package/dist/meeting/meeting.module.d.ts +7 -0
  235. package/dist/meeting/meeting.module.d.ts.map +1 -0
  236. package/dist/meeting/meeting.module.js +26 -0
  237. package/dist/meeting/meeting.module.js.map +1 -0
  238. package/dist/middlewares/locale-injection.middleware.d.ts +12 -0
  239. package/dist/middlewares/locale-injection.middleware.d.ts.map +1 -1
  240. package/dist/middlewares/locale-injection.middleware.js +20 -4
  241. package/dist/middlewares/locale-injection.middleware.js.map +1 -1
  242. package/dist/oauth/providers/google.provider.d.ts +7 -0
  243. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  244. package/dist/oauth/providers/google.provider.js +43 -13
  245. package/dist/oauth/providers/google.provider.js.map +1 -1
  246. package/dist/observability/monitored-cron.d.ts +34 -0
  247. package/dist/observability/monitored-cron.d.ts.map +1 -0
  248. package/dist/observability/monitored-cron.js +118 -0
  249. package/dist/observability/monitored-cron.js.map +1 -0
  250. package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts +12 -0
  251. package/dist/pairing/dto/approve-reverse-pairing.dto.d.ts.map +1 -0
  252. package/dist/pairing/dto/approve-reverse-pairing.dto.js +30 -0
  253. package/dist/pairing/dto/approve-reverse-pairing.dto.js.map +1 -0
  254. package/dist/pairing/dto/claim-pairing.dto.d.ts +11 -0
  255. package/dist/pairing/dto/claim-pairing.dto.d.ts.map +1 -0
  256. package/dist/pairing/dto/claim-pairing.dto.js +35 -0
  257. package/dist/pairing/dto/claim-pairing.dto.js.map +1 -0
  258. package/dist/pairing/dto/confirm-pairing.dto.d.ts +10 -0
  259. package/dist/pairing/dto/confirm-pairing.dto.d.ts.map +1 -0
  260. package/dist/pairing/dto/confirm-pairing.dto.js +24 -0
  261. package/dist/pairing/dto/confirm-pairing.dto.js.map +1 -0
  262. package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts +5 -0
  263. package/dist/pairing/dto/poll-reverse-pairing.dto.d.ts.map +1 -0
  264. package/dist/pairing/dto/poll-reverse-pairing.dto.js +24 -0
  265. package/dist/pairing/dto/poll-reverse-pairing.dto.js.map +1 -0
  266. package/dist/pairing/dto/start-pairing.dto.d.ts +11 -0
  267. package/dist/pairing/dto/start-pairing.dto.d.ts.map +1 -0
  268. package/dist/pairing/dto/start-pairing.dto.js +30 -0
  269. package/dist/pairing/dto/start-pairing.dto.js.map +1 -0
  270. package/dist/pairing/dto/start-reverse-pairing.dto.d.ts +24 -0
  271. package/dist/pairing/dto/start-reverse-pairing.dto.d.ts.map +1 -0
  272. package/dist/pairing/dto/start-reverse-pairing.dto.js +47 -0
  273. package/dist/pairing/dto/start-reverse-pairing.dto.js.map +1 -0
  274. package/dist/pairing/pairing.controller.d.ts +118 -0
  275. package/dist/pairing/pairing.controller.d.ts.map +1 -0
  276. package/dist/pairing/pairing.controller.js +296 -0
  277. package/dist/pairing/pairing.controller.js.map +1 -0
  278. package/dist/pairing/pairing.module.d.ts +10 -0
  279. package/dist/pairing/pairing.module.d.ts.map +1 -0
  280. package/dist/pairing/pairing.module.js +40 -0
  281. package/dist/pairing/pairing.module.js.map +1 -0
  282. package/dist/pairing/pairing.service.d.ts +238 -0
  283. package/dist/pairing/pairing.service.d.ts.map +1 -0
  284. package/dist/pairing/pairing.service.js +495 -0
  285. package/dist/pairing/pairing.service.js.map +1 -0
  286. package/dist/pdf/pdf.service.d.ts +3 -6
  287. package/dist/pdf/pdf.service.d.ts.map +1 -1
  288. package/dist/pdf/pdf.service.js +8 -20
  289. package/dist/pdf/pdf.service.js.map +1 -1
  290. package/dist/profile/profile.controller.d.ts +11 -0
  291. package/dist/profile/profile.controller.d.ts.map +1 -1
  292. package/dist/profile/profile.service.d.ts +11 -0
  293. package/dist/profile/profile.service.d.ts.map +1 -1
  294. package/dist/profile/profile.service.js +20 -11
  295. package/dist/profile/profile.service.js.map +1 -1
  296. package/dist/redis/redis-pubsub.d.ts.map +1 -1
  297. package/dist/redis/redis-pubsub.js +44 -0
  298. package/dist/redis/redis-pubsub.js.map +1 -1
  299. package/dist/role/guards/role.guard.d.ts +5 -0
  300. package/dist/role/guards/role.guard.d.ts.map +1 -1
  301. package/dist/role/guards/role.guard.js +41 -39
  302. package/dist/role/guards/role.guard.js.map +1 -1
  303. package/dist/session/session.controller.d.ts +2 -0
  304. package/dist/session/session.controller.d.ts.map +1 -1
  305. package/dist/session/session.service.d.ts +39 -0
  306. package/dist/session/session.service.d.ts.map +1 -1
  307. package/dist/session/session.service.js +91 -8
  308. package/dist/session/session.service.js.map +1 -1
  309. package/dist/setting/brand-asset.service.d.ts +48 -0
  310. package/dist/setting/brand-asset.service.d.ts.map +1 -0
  311. package/dist/setting/brand-asset.service.js +130 -0
  312. package/dist/setting/brand-asset.service.js.map +1 -0
  313. package/dist/setting/setting.module.d.ts.map +1 -1
  314. package/dist/setting/setting.module.js +3 -2
  315. package/dist/setting/setting.module.js.map +1 -1
  316. package/dist/setting/setting.service.d.ts +33 -0
  317. package/dist/setting/setting.service.d.ts.map +1 -1
  318. package/dist/setting/setting.service.js +88 -0
  319. package/dist/setting/setting.service.js.map +1 -1
  320. package/dist/token/token.service.d.ts +23 -2
  321. package/dist/token/token.service.d.ts.map +1 -1
  322. package/dist/token/token.service.js +39 -4
  323. package/dist/token/token.service.js.map +1 -1
  324. package/dist/user/user.controller.d.ts +8 -22
  325. package/dist/user/user.controller.d.ts.map +1 -1
  326. package/dist/user/user.service.d.ts +44 -24
  327. package/dist/user/user.service.d.ts.map +1 -1
  328. package/dist/user/user.service.js +89 -6
  329. package/dist/user/user.service.js.map +1 -1
  330. package/dist/user-device/user-device.controller.d.ts +54 -0
  331. package/dist/user-device/user-device.controller.d.ts.map +1 -0
  332. package/dist/user-device/user-device.controller.js +103 -0
  333. package/dist/user-device/user-device.controller.js.map +1 -0
  334. package/dist/user-device/user-device.module.d.ts +3 -0
  335. package/dist/user-device/user-device.module.d.ts.map +1 -0
  336. package/dist/user-device/user-device.module.js +26 -0
  337. package/dist/user-device/user-device.module.js.map +1 -0
  338. package/dist/user-device/user-device.service.d.ts +71 -0
  339. package/dist/user-device/user-device.service.d.ts.map +1 -0
  340. package/dist/user-device/user-device.service.js +158 -0
  341. package/dist/user-device/user-device.service.js.map +1 -0
  342. package/dist/utils/device-key.d.ts +23 -0
  343. package/dist/utils/device-key.d.ts.map +1 -0
  344. package/dist/utils/device-key.js +31 -0
  345. package/dist/utils/device-key.js.map +1 -0
  346. package/dist/utils/geo-ip.d.ts +38 -0
  347. package/dist/utils/geo-ip.d.ts.map +1 -0
  348. package/dist/utils/geo-ip.js +122 -0
  349. package/dist/utils/geo-ip.js.map +1 -0
  350. package/dist/utils/locale-context.d.ts +10 -7
  351. package/dist/utils/locale-context.d.ts.map +1 -1
  352. package/dist/utils/locale-context.js +14 -8
  353. package/dist/utils/locale-context.js.map +1 -1
  354. package/dist/utils/security-event-vars.d.ts +56 -0
  355. package/dist/utils/security-event-vars.d.ts.map +1 -0
  356. package/dist/utils/security-event-vars.js +109 -0
  357. package/dist/utils/security-event-vars.js.map +1 -0
  358. package/dist/utils/user-agent.d.ts +19 -0
  359. package/dist/utils/user-agent.d.ts.map +1 -0
  360. package/dist/utils/user-agent.js +36 -0
  361. package/dist/utils/user-agent.js.map +1 -0
  362. package/dist/webhook-integration/webhook-integration.service.d.ts +5 -0
  363. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  364. package/dist/webhook-integration/webhook-integration.service.js +50 -4
  365. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  366. package/dist/webhook-integration/webhook-protocol.registry.d.ts +6 -0
  367. package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -1
  368. package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -1
  369. package/hedhog/data/integration_provider.yaml +9 -0
  370. package/hedhog/data/mail.yaml +198 -62
  371. package/hedhog/data/mail_layout.yaml +43 -33
  372. package/hedhog/data/menu.yaml +2 -0
  373. package/hedhog/data/role.yaml +16 -0
  374. package/hedhog/data/route.yaml +172 -3
  375. package/hedhog/data/setting_group.yaml +178 -2
  376. package/hedhog/data/setting_subgroup.yaml +8 -0
  377. package/hedhog/frontend/app/_lib/hooks/use-core-access.test.tsx.ejs +125 -0
  378. package/hedhog/frontend/app/_lib/hooks/use-core-access.ts.ejs +29 -0
  379. package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.test.ts.ejs +71 -0
  380. package/hedhog/frontend/app/_lib/hooks/use-impersonation-access.ts.ejs +32 -0
  381. package/hedhog/frontend/app/access-log/page.tsx.ejs +96 -22
  382. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +17 -1
  383. package/hedhog/frontend/app/account/components/known-devices.tsx.ejs +89 -0
  384. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +2 -1
  385. package/hedhog/frontend/app/account/components/user-devices-list.tsx.ejs +186 -0
  386. package/hedhog/frontend/app/account/devices/page.tsx.ejs +5 -0
  387. package/hedhog/frontend/app/account/layout.tsx.ejs +2 -0
  388. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +298 -0
  389. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +376 -1236
  390. package/hedhog/frontend/app/mail/log/page.tsx.ejs +210 -3
  391. package/hedhog/frontend/app/mail/template/page.tsx.ejs +190 -132
  392. package/hedhog/frontend/app/users/active-session.tsx.ejs +58 -11
  393. package/hedhog/frontend/app/users/page.tsx.ejs +69 -10
  394. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +202 -0
  395. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +170 -0
  396. package/hedhog/frontend/app/users/user-activity-timeline.tsx.ejs +19 -0
  397. package/hedhog/frontend/app/users/user-devices.tsx.ejs +89 -0
  398. package/hedhog/frontend/app/users/user-logs.tsx.ejs +13 -1
  399. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +46 -24
  400. package/hedhog/frontend/messages/en.json +146 -48
  401. package/hedhog/frontend/messages/pt.json +146 -48
  402. package/hedhog/table/access_log.yaml +7 -0
  403. package/hedhog/table/file.yaml +8 -0
  404. package/hedhog/table/mail_delivery_event.yaml +76 -0
  405. package/hedhog/table/mail_sent.yaml +23 -0
  406. package/hedhog/table/user_activity.yaml +9 -0
  407. package/hedhog/table/user_device.yaml +52 -0
  408. package/hedhog/table/user_session.yaml +20 -1
  409. package/package.json +6 -5
  410. package/src/access-log/access-log.controller.ts +1 -0
  411. package/src/access-log/access-log.interceptor.ts +5 -1
  412. package/src/access-log/access-log.service.spec.ts +33 -2
  413. package/src/access-log/access-log.service.ts +38 -0
  414. package/src/access-log/dto/query-access-log.dto.ts +5 -0
  415. package/src/ai/ai.controller.ts +9 -0
  416. package/src/ai/ai.module.ts +3 -2
  417. package/src/ai/ai.service.ts +150 -13
  418. package/src/ai/dto/richtext-edit-html.dto.ts +11 -0
  419. package/src/ai/richtext-ai.prompts.ts +57 -0
  420. package/src/ai/richtext-ai.service.ts +109 -0
  421. package/src/auth/auth.controller.ts +47 -8
  422. package/src/auth/auth.module.ts +2 -0
  423. package/src/auth/auth.service.spec.ts +383 -6
  424. package/src/auth/auth.service.ts +345 -56
  425. package/src/auth/dto/confirm-email.dto.ts +7 -0
  426. package/src/auth/dto/resend-confirmation.dto.ts +20 -0
  427. package/src/authorize/authorize.controller.ts +2 -0
  428. package/src/authorize/authorize.service.spec.ts +23 -0
  429. package/src/authorize/authorize.service.ts +8 -0
  430. package/src/challenge/challenge.service.spec.ts +73 -0
  431. package/src/challenge/challenge.service.ts +62 -3
  432. package/src/core.module.ts +19 -0
  433. package/src/file/file.controller.ts +54 -4
  434. package/src/file/file.service.spec.ts +275 -105
  435. package/src/file/file.service.ts +409 -14
  436. package/src/file/image-optimization.service.spec.ts +92 -0
  437. package/src/file/image-optimization.service.ts +202 -0
  438. package/src/file/provider/abstract.provider.ts +23 -0
  439. package/src/file/provider/gcs.provider.ts +17 -0
  440. package/src/file/provider/local.provider.ts +21 -0
  441. package/src/file/provider/s3.provider.ts +25 -0
  442. package/src/file/provider/spaces.provider.ts +26 -0
  443. package/src/impersonation/dto/redeem-impersonation.dto.ts +8 -0
  444. package/src/impersonation/dto/start-impersonation.dto.ts +13 -0
  445. package/src/impersonation/impersonation-restrictions.ts +84 -0
  446. package/src/impersonation/impersonation.controller.ts +109 -0
  447. package/src/impersonation/impersonation.module.ts +31 -0
  448. package/src/impersonation/impersonation.service.spec.ts +427 -0
  449. package/src/impersonation/impersonation.service.ts +577 -0
  450. package/src/index.ts +21 -0
  451. package/src/integration/services/outbox-polling.coordinator.spec.ts +90 -0
  452. package/src/integration/services/outbox-polling.coordinator.ts +62 -40
  453. package/src/integration/services/outbox.service.spec.ts +8 -0
  454. package/src/integration/services/outbox.service.ts +19 -13
  455. package/src/integration-profile/integration-profile.secrets.ts +3 -0
  456. package/src/integration-profile/integration-profile.utils.ts +38 -0
  457. package/src/integration-profile/testers/email.testers.ts +34 -1
  458. package/src/language/en.json +11 -0
  459. package/src/language/pt.json +11 -0
  460. package/src/mail/dto/send.dto.ts +46 -2
  461. package/src/mail/mail-template.spec.ts +79 -2
  462. package/src/mail/mail.module.ts +2 -0
  463. package/src/mail/mail.service.spec.ts +307 -6
  464. package/src/mail/mail.service.ts +227 -30
  465. package/src/mail-delivery/mail-delivery-provision.service.ts +66 -0
  466. package/src/mail-delivery/mail-delivery.constants.ts +31 -0
  467. package/src/mail-delivery/mail-delivery.controller.ts +38 -0
  468. package/src/mail-delivery/mail-delivery.module.ts +23 -0
  469. package/src/mail-delivery/mail-delivery.service.spec.ts +136 -0
  470. package/src/mail-delivery/mail-delivery.service.ts +152 -0
  471. package/src/mail-delivery/ses-event.parser.spec.ts +140 -0
  472. package/src/mail-delivery/ses-event.parser.ts +191 -0
  473. package/src/mail-delivery/ses-webhook.service.spec.ts +170 -0
  474. package/src/mail-delivery/ses-webhook.service.ts +240 -0
  475. package/src/mail-delivery/sns-message.util.spec.ts +163 -0
  476. package/src/mail-delivery/sns-message.util.ts +125 -0
  477. package/src/mail-sent/dto/query-mail-sent-list.dto.ts +15 -0
  478. package/src/mail-sent/mail-sent.service.ts +46 -2
  479. package/src/mailbox/mailbox-fetcher.registry.ts +132 -0
  480. package/src/mailbox/mailbox.module.ts +14 -0
  481. package/src/meeting/meeting-provider.registry.ts +193 -0
  482. package/src/meeting/meeting.constants.ts +53 -0
  483. package/src/meeting/meeting.module.ts +13 -0
  484. package/src/middlewares/locale-injection.middleware.ts +25 -6
  485. package/src/oauth/providers/google.provider.ts +49 -15
  486. package/src/observability/monitored-cron.spec.ts +102 -0
  487. package/src/observability/monitored-cron.ts +110 -0
  488. package/src/pairing/dto/approve-reverse-pairing.dto.ts +20 -0
  489. package/src/pairing/dto/claim-pairing.dto.ts +19 -0
  490. package/src/pairing/dto/confirm-pairing.dto.ts +14 -0
  491. package/src/pairing/dto/poll-reverse-pairing.dto.ts +9 -0
  492. package/src/pairing/dto/start-pairing.dto.ts +19 -0
  493. package/src/pairing/dto/start-reverse-pairing.dto.ts +45 -0
  494. package/src/pairing/pairing.controller.ts +246 -0
  495. package/src/pairing/pairing.module.ts +27 -0
  496. package/src/pairing/pairing.service.spec.ts +462 -0
  497. package/src/pairing/pairing.service.ts +614 -0
  498. package/src/pdf/pdf.service.ts +6 -24
  499. package/src/profile/profile.service.ts +26 -8
  500. package/src/redis/redis-pubsub.ts +11 -0
  501. package/src/role/guards/role.guard.spec.ts +75 -0
  502. package/src/role/guards/role.guard.ts +64 -46
  503. package/src/session/session.service.spec.ts +81 -0
  504. package/src/session/session.service.ts +93 -6
  505. package/src/setting/brand-asset.service.ts +137 -0
  506. package/src/setting/setting.module.ts +3 -2
  507. package/src/setting/setting.service.spec.ts +79 -1
  508. package/src/setting/setting.service.ts +109 -1
  509. package/src/token/token.service.spec.ts +62 -0
  510. package/src/token/token.service.ts +57 -3
  511. package/src/user/user.service.spec.ts +33 -2
  512. package/src/user/user.service.ts +108 -6
  513. package/src/user-device/user-device.controller.ts +77 -0
  514. package/src/user-device/user-device.module.ts +13 -0
  515. package/src/user-device/user-device.service.spec.ts +234 -0
  516. package/src/user-device/user-device.service.ts +168 -0
  517. package/src/utils/device-key.spec.ts +57 -0
  518. package/src/utils/device-key.ts +32 -0
  519. package/src/utils/geo-ip.spec.ts +134 -0
  520. package/src/utils/geo-ip.ts +93 -0
  521. package/src/utils/locale-context.spec.ts +27 -1
  522. package/src/utils/locale-context.ts +15 -12
  523. package/src/utils/security-event-vars.spec.ts +161 -0
  524. package/src/utils/security-event-vars.ts +153 -0
  525. package/src/utils/user-agent.spec.ts +37 -0
  526. package/src/utils/user-agent.ts +46 -0
  527. package/src/webhook-integration/webhook-integration.service.ts +57 -3
  528. package/src/webhook-integration/webhook-protocol.registry.ts +6 -0
@@ -31,9 +31,12 @@ import {
31
31
  OnModuleInit
32
32
  } from '@nestjs/common';
33
33
  import { JwtService } from '@nestjs/jwt';
34
+ import type { Request } from 'express';
34
35
  import { createWriteStream, promises as fs } from 'fs';
35
- import { basename, dirname, extname } from 'path';
36
+ import { tmpdir } from 'os';
37
+ import { basename, dirname, extname, join } from 'path';
36
38
  import { pipeline } from 'stream/promises';
39
+ import { v4 } from 'uuid';
37
40
  import {
38
41
  buildStorageConfigFromIntegration,
39
42
  resolveFileProviderSlug,
@@ -41,8 +44,14 @@ import {
41
44
  } from '../integration-profile/integration-profile.utils';
42
45
  import { SettingService } from '../setting/setting.service';
43
46
  import { DeleteDTO } from './dto/delete.dto';
44
- import { ImageOptimizationService } from './image-optimization.service';
45
- import { AbstractProvider } from './provider/abstract.provider';
47
+ import {
48
+ ImageOptimizationService,
49
+ ImageVariantFormat,
50
+ } from './image-optimization.service';
51
+ import {
52
+ AbstractProvider,
53
+ supportsServerSideCopy,
54
+ } from './provider/abstract.provider';
46
55
  import { ProviderFactory } from './provider/provider.factory';
47
56
 
48
57
  type ResolvedProvider = {
@@ -63,6 +72,13 @@ const PROVIDER_CACHE_TTL_MS = Math.max(
63
72
  Number(process.env.FILE_PROVIDER_CACHE_TTL_MS ?? 30_000),
64
73
  );
65
74
 
75
+ /** Raw `?w=`/`?f=`/`?q=` query values for the public image endpoint. */
76
+ export type ImageVariantRequest = {
77
+ width?: number | string;
78
+ format?: string;
79
+ quality?: number | string;
80
+ };
81
+
66
82
  @Injectable()
67
83
  export class FileService implements OnModuleInit {
68
84
  private readonly logger = new Logger(FileService.name);
@@ -467,6 +483,50 @@ export class FileService implements OnModuleInit {
467
483
  };
468
484
  }
469
485
 
486
+ /**
487
+ * Renderiza um cartão social 1200x630 (JPEG) a partir de arquivos do acervo.
488
+ *
489
+ * Devolve `null` quando não há arquivo utilizável — quem chama deve responder
490
+ * 404 e deixar o consumidor cair na imagem padrão da marca, nunca 500: do
491
+ * outro lado costuma haver um robô de link preview.
492
+ */
493
+ async renderShareCard(options: {
494
+ backgroundFileId?: number | null;
495
+ overlayFileId?: number | null;
496
+ background?: string | null;
497
+ }): Promise<{ buffer: Buffer; contentType: string } | null> {
498
+ const load = async (fileId?: number | null): Promise<Buffer | null> => {
499
+ if (!fileId) return null;
500
+ try {
501
+ const { buffer } = await this.getBuffer(fileId);
502
+ return buffer;
503
+ } catch {
504
+ return null;
505
+ }
506
+ };
507
+
508
+ const [background, overlay] = await Promise.all([
509
+ load(options.backgroundFileId),
510
+ load(options.overlayFileId),
511
+ ]);
512
+
513
+ if (!background && !overlay) {
514
+ return null;
515
+ }
516
+
517
+ const buffer = await this.imageOptimizationService.composeShareCard(
518
+ background,
519
+ overlay,
520
+ { background: options.background },
521
+ );
522
+
523
+ if (!buffer) {
524
+ return null;
525
+ }
526
+
527
+ return { buffer, contentType: 'image/jpeg' };
528
+ }
529
+
470
530
  async getLocalFile(fileId: number, res: any){
471
531
  const { buffer, file } = await this.getBuffer(fileId);
472
532
  res.set({
@@ -483,7 +543,12 @@ export class FileService implements OnModuleInit {
483
543
  // Escape hatch para chamadores INTERNOS que já validaram por conta própria —
484
544
  // o mesmo par de flags que `uploadFromPath` sempre teve. O FileController não
485
545
  // repassa este argumento, então não há como acioná-lo por HTTP.
486
- options?: { skipMimeValidation?: boolean; skipSizeValidation?: boolean },
546
+ // `isPrivate` tira o arquivo das rotas que servem por id sem autenticação.
547
+ options?: {
548
+ skipMimeValidation?: boolean;
549
+ skipSizeValidation?: boolean;
550
+ isPrivate?: boolean;
551
+ },
487
552
  ) {
488
553
 
489
554
  if (!fileBuffer) {
@@ -544,6 +609,7 @@ export class FileService implements OnModuleInit {
544
609
  location: destination,
545
610
  mimetype_id: await this.getMimeType(optimized.mimetype),
546
611
  size: optimized.buffer.length,
612
+ is_private: options?.isPrivate ?? false,
547
613
  },
548
614
  });
549
615
 
@@ -561,6 +627,10 @@ export class FileService implements OnModuleInit {
561
627
  encoding?: string;
562
628
  skipMimeValidation?: boolean;
563
629
  skipSizeValidation?: boolean;
630
+ // Tira o arquivo das rotas que servem por id sem autenticação.
631
+ isPrivate?: boolean;
632
+ // Mesmo par maxWidth/maxHeight que `upload` aceita — só tem efeito em image/*.
633
+ resize?: { maxWidth?: number; maxHeight?: number };
564
634
  },
565
635
  ) {
566
636
  if (!filePath) {
@@ -616,6 +686,7 @@ export class FileService implements OnModuleInit {
616
686
  location: destination,
617
687
  mimetype_id: await this.getMimeType(mimetype),
618
688
  size,
689
+ is_private: metadata?.isPrivate ?? false,
619
690
  },
620
691
  });
621
692
  }
@@ -626,6 +697,10 @@ export class FileService implements OnModuleInit {
626
697
  rawBuffer,
627
698
  mimetype,
628
699
  originalname,
700
+ {
701
+ maxWidth: metadata?.resize?.maxWidth,
702
+ maxHeight: metadata?.resize?.maxHeight,
703
+ },
629
704
  );
630
705
 
631
706
  const url = await provider.upload(destination, {
@@ -648,6 +723,7 @@ export class FileService implements OnModuleInit {
648
723
  location: destination,
649
724
  mimetype_id: await this.getMimeType(optimized.mimetype),
650
725
  size: optimized.buffer.length,
726
+ is_private: metadata?.isPrivate ?? false,
651
727
  },
652
728
  });
653
729
 
@@ -804,10 +880,125 @@ export class FileService implements OnModuleInit {
804
880
  return file;
805
881
  }
806
882
 
883
+ /**
884
+ * Creates an independent copy of a stored file: new bytes in storage + a new `file` row.
885
+ *
886
+ * Unlike sharing a `file_id`, the result survives the original being deleted — which is what
887
+ * "duplicate files" means when copying course content between courses.
888
+ *
889
+ * `filename` and `destination` default to the source's, so a caller that only wants
890
+ * independent bytes gets them without thinking about paths. Passing `destination` matters for
891
+ * artifacts addressed by their location (HLS manifests and segments).
892
+ *
893
+ * The bytes are copied server-side when the active provider supports it; otherwise — and when
894
+ * the provider-side copy fails, e.g. an object above S3's 5GB CopyObject cap — they are
895
+ * streamed through a temp file, never buffered in RAM. Mime/size validation is skipped: the
896
+ * source already passed it at upload time.
897
+ */
898
+ async duplicate(
899
+ fileId: number,
900
+ options?: { destination?: string; filename?: string },
901
+ ) {
902
+ const source = await this.prismaService.file.findUnique({
903
+ where: { id: fileId },
904
+ include: { file_mimetype: true },
905
+ });
906
+
907
+ if (!source) {
908
+ throw new NotFoundException(`File not found: ${fileId}`);
909
+ }
910
+
911
+ const destination = options?.destination ?? source.location;
912
+ const filename = options?.filename ?? source.filename;
913
+ const sourcePath = this.resolveProviderPath(source);
914
+ const provider = await this.getProvider();
915
+
916
+ let storedFilename: string | null = null;
917
+
918
+ if (supportsServerSideCopy(provider)) {
919
+ try {
920
+ storedFilename = await provider.copy(sourcePath, destination, filename);
921
+ } catch (error) {
922
+ this.logger.warn(
923
+ `Server-side copy failed for file ${fileId}, falling back to streaming: ${
924
+ error instanceof Error ? error.message : String(error)
925
+ }`,
926
+ );
927
+ }
928
+ }
929
+
930
+ if (storedFilename === null) {
931
+ const tempPath = join(tmpdir(), `hedhog-file-copy-${v4()}`);
932
+ try {
933
+ await this.downloadToPath(fileId, tempPath);
934
+ storedFilename = await provider.uploadFromPath(destination, tempPath, {
935
+ filename,
936
+ mimetype: source.file_mimetype?.name,
937
+ });
938
+ } finally {
939
+ await fs.unlink(tempPath).catch(() => undefined);
940
+ }
941
+ }
942
+
943
+ return this.prismaService.file.create({
944
+ data: {
945
+ filename,
946
+ path: storedFilename,
947
+ provider_id: this.providerId,
948
+ location: destination,
949
+ mimetype_id: source.mimetype_id,
950
+ size: source.size ?? 0,
951
+ },
952
+ });
953
+ }
954
+
807
955
  async readStream(filepath: string) {
808
956
  return (await this.getProvider()).readStream(filepath);
809
957
  }
810
958
 
959
+ /**
960
+ * Confere se os bytes de um `file` estão mesmo no storage, devolvendo o tamanho
961
+ * relatado pelo provider ao lado do tamanho declarado no banco.
962
+ *
963
+ * Existe para quem precisa **provar** que a cópia sobreviveu antes de apagar o
964
+ * original em outro lugar: o registro em banco sozinho não é prova, porque é
965
+ * gravado depois do upload e nada reconcilia os dois se o insert falhar no meio
966
+ * — o blob fica órfão ou a linha aponta para o vazio, e ninguém percebe até
967
+ * alguém tentar ler.
968
+ *
969
+ * Não lança quando o objeto está ausente: devolve `storedSize: null`, para o
970
+ * chamador decidir o que fazer com a divergência.
971
+ */
972
+ async statStoredFile(
973
+ fileId: number,
974
+ ): Promise<{ storedSize: number | null; declaredSize: number }> {
975
+ if (!fileId) {
976
+ throw new BadRequestException(`No file ID provided`);
977
+ }
978
+
979
+ const file = await this.prismaService.file.findUnique({
980
+ where: { id: fileId },
981
+ });
982
+
983
+ if (!file) {
984
+ throw new NotFoundException(`File not found: ${fileId}`);
985
+ }
986
+
987
+ const provider = await this.getProvider();
988
+
989
+ try {
990
+ const metadata = await provider.metaData(this.resolveProviderPath(file));
991
+ return {
992
+ storedSize: this.getFileSizeFromMetadata(metadata) ?? null,
993
+ declaredSize: Number(file.size ?? 0),
994
+ };
995
+ } catch {
996
+ // Qualquer falha de leitura de metadado — ausente, sem permissão, provider
997
+ // fora do ar — é tratada como "não deu para provar", nunca como sucesso.
998
+ return { storedSize: null, declaredSize: Number(file.size ?? 0) };
999
+ }
1000
+ }
1001
+
811
1002
  private getFileSizeFromMetadata(metadata: any): number | undefined {
812
1003
  if (!metadata) {
813
1004
  return undefined;
@@ -910,13 +1101,25 @@ export class FileService implements OnModuleInit {
910
1101
  * Resolves the file record and its storage metadata (mimetype + size) without
911
1102
  * opening a read stream — so callers can answer a conditional 304 cheaply.
912
1103
  */
1104
+ /**
1105
+ * Metadados para servir um arquivo POR ID.
1106
+ *
1107
+ * Só os dois handlers anônimos passam por aqui (`serveFileById` e
1108
+ * `serveImageById`), e é por isso que a checagem de `is_private` mora neste
1109
+ * ponto e não em cada um: id de arquivo é sequencial, então servir por id é uma
1110
+ * porta enumerável. O caminho por token (`download`) não passa por aqui, então
1111
+ * um arquivo privado continua acessível ao admin via
1112
+ * `PUT /file/open/:id` -> token -> `GET /file/open/:token`.
1113
+ */
913
1114
  private async resolveFileMeta(locale: string, id: number) {
914
1115
  const file = await this.prismaService.file.findFirst({
915
1116
  where: { id },
916
1117
  include: { file_mimetype: true },
917
1118
  });
918
1119
 
919
- if (!file) {
1120
+ // Privado responde igual a inexistente: confirmar que o id existe já
1121
+ // entregaria metade do que a enumeração procura.
1122
+ if (!file || file.is_private) {
920
1123
  throw new NotFoundException(
921
1124
  getLocaleText('file.download.not_found', locale, `Not found: ${id}`),
922
1125
  );
@@ -1010,16 +1213,82 @@ export class FileService implements OnModuleInit {
1010
1213
  return mimetype ?? 'application/octet-stream';
1011
1214
  }
1012
1215
 
1216
+ /**
1217
+ * Widths this endpoint is willing to render. An allowlist is required, not a
1218
+ * range check: the endpoint is `@Public()`, and an arbitrary width means
1219
+ * arbitrary CPU and an unbounded number of cache entries per file.
1220
+ */
1221
+ private async getAllowedImageWidths(): Promise<number[]> {
1222
+ const DEFAULT_WIDTHS = [64, 128, 320, 400, 480, 640, 896, 1200];
1223
+ const settings = await this.settingService.getSettingValues(
1224
+ 'image-resize-allowed-widths',
1225
+ );
1226
+ try {
1227
+ const parsed = JSON.parse(settings['image-resize-allowed-widths'] ?? '');
1228
+ const widths = (Array.isArray(parsed) ? parsed : [])
1229
+ .map((w: unknown) => Math.round(Number(w)))
1230
+ .filter((w: number) => Number.isFinite(w) && w > 0);
1231
+ return widths.length ? widths : DEFAULT_WIDTHS;
1232
+ } catch {
1233
+ return DEFAULT_WIDTHS;
1234
+ }
1235
+ }
1236
+
1237
+ /**
1238
+ * Resolves the requested variant against the allowlist. Anything unknown or
1239
+ * out of the list collapses to "no variant", which serves the original bytes
1240
+ * instead of failing — a stale `?w=` in cached HTML must not 500.
1241
+ */
1242
+ private async resolveImageVariant(variant?: ImageVariantRequest) {
1243
+ const width = Math.round(Number(variant?.width) || 0);
1244
+ if (width <= 0) return null;
1245
+
1246
+ const allowed = await this.getAllowedImageWidths();
1247
+ if (!allowed.includes(width)) return null;
1248
+
1249
+ const format = ['webp', 'jpeg', 'png', 'avif'].includes(
1250
+ String(variant?.format),
1251
+ )
1252
+ ? (String(variant?.format) as ImageVariantFormat)
1253
+ : undefined;
1254
+
1255
+ const rawQuality = Math.round(Number(variant?.quality) || 0);
1256
+ const quality =
1257
+ rawQuality >= 50 && rawQuality <= 95 ? rawQuality : 80;
1258
+
1259
+ return { width, format, quality };
1260
+ }
1261
+
1262
+ /** Absolute path of the on-disk cache entry for a rendered variant. */
1263
+ private imageVariantCachePath(
1264
+ id: number,
1265
+ size: number | undefined,
1266
+ width: number,
1267
+ format: string,
1268
+ quality: number,
1269
+ ) {
1270
+ return join(
1271
+ tmpdir(),
1272
+ 'hedhog-image-variants',
1273
+ `${id}-${size ?? 0}-w${width}-${format}-q${quality}`,
1274
+ );
1275
+ }
1276
+
1013
1277
  /**
1014
1278
  * Serves an image by id with strong, immutable HTTP caching. The content of a
1015
1279
  * given fileId never changes (a new upload yields a new id), so the ETag is
1016
1280
  * derived from id+size instead of hashing the buffer, keeping it streaming.
1281
+ *
1282
+ * With a `variant` (`?w=`), the bytes are resized before being sent and the
1283
+ * ETag carries the variant parameters — otherwise a conditional request for
1284
+ * one width would get a 304 while holding the bytes of another.
1017
1285
  */
1018
1286
  async serveImageById(
1019
1287
  locale: string,
1020
1288
  id: number,
1021
1289
  ifNoneMatch: string | undefined,
1022
1290
  res: any,
1291
+ variant?: ImageVariantRequest,
1023
1292
  ) {
1024
1293
  const { provider, providerPath, size, mimetype, filename } =
1025
1294
  await this.resolveFileMeta(locale, id);
@@ -1041,26 +1310,122 @@ export class FileService implements OnModuleInit {
1041
1310
  );
1042
1311
  }
1043
1312
 
1044
- const etag = `"img-${id}-${size ?? 0}"`;
1313
+ const resolved = await this.resolveImageVariant(variant);
1314
+ const variantSuffix = resolved
1315
+ ? `-w${resolved.width}-${resolved.format ?? 'auto'}-q${resolved.quality}`
1316
+ : '';
1317
+ const etag = `"img-${id}-${size ?? 0}${variantSuffix}"`;
1045
1318
  if (ifNoneMatch && ifNoneMatch === etag) {
1046
1319
  res.status(304).end();
1047
1320
  return;
1048
1321
  }
1049
1322
 
1050
1323
  const ttl = await this.getCacheTtl('image-cache-ttl-seconds', 2592000);
1051
- this.setInlineHeaders(
1052
- res,
1053
- filename,
1054
- contentType,
1055
- size,
1056
- `public, max-age=${ttl}, immutable`,
1057
- etag,
1058
- );
1324
+ const cacheControl = `public, max-age=${ttl}, immutable`;
1325
+
1326
+ if (resolved) {
1327
+ const rendered = await this.renderImageVariant(
1328
+ provider,
1329
+ providerPath,
1330
+ contentType,
1331
+ id,
1332
+ size,
1333
+ resolved,
1334
+ );
1335
+ // A null render (unsupported format, sharp failure) falls through to the
1336
+ // original bytes below rather than erroring the request.
1337
+ if (rendered) {
1338
+ const ext = rendered.mimetype.replace('image/', '');
1339
+ this.setInlineHeaders(
1340
+ res,
1341
+ `${basename(filename, extname(filename))}.${ext === 'jpeg' ? 'jpg' : ext}`,
1342
+ rendered.mimetype,
1343
+ rendered.buffer.length,
1344
+ cacheControl,
1345
+ etag,
1346
+ );
1347
+ res.end(rendered.buffer);
1348
+ return;
1349
+ }
1350
+ }
1351
+
1352
+ this.setInlineHeaders(res, filename, contentType, size, cacheControl, etag);
1059
1353
 
1060
1354
  const stream = await provider.readStream(providerPath);
1061
1355
  stream.pipe(res);
1062
1356
  }
1063
1357
 
1358
+ /**
1359
+ * Produces (and memoizes on disk) a resized variant of a stored image.
1360
+ * The cache key includes the file size, so re-uploading under the same id —
1361
+ * which the storage layer does not normally do — cannot serve stale bytes.
1362
+ */
1363
+ private async renderImageVariant(
1364
+ provider: any,
1365
+ providerPath: string,
1366
+ contentType: string,
1367
+ id: number,
1368
+ size: number | undefined,
1369
+ variant: { width: number; format?: ImageVariantFormat; quality: number },
1370
+ ): Promise<{ buffer: Buffer; mimetype: string } | null> {
1371
+ const format =
1372
+ variant.format ??
1373
+ (contentType === 'image/png'
1374
+ ? 'png'
1375
+ : contentType === 'image/webp'
1376
+ ? 'webp'
1377
+ : contentType === 'image/avif'
1378
+ ? 'avif'
1379
+ : 'jpeg');
1380
+ const cachePath = this.imageVariantCachePath(
1381
+ id,
1382
+ size,
1383
+ variant.width,
1384
+ format,
1385
+ variant.quality,
1386
+ );
1387
+
1388
+ try {
1389
+ const cached = await fs.readFile(cachePath);
1390
+ return { buffer: cached, mimetype: `image/${format}` };
1391
+ } catch {
1392
+ // Cache miss is the normal path on a cold pod; fall through and render.
1393
+ }
1394
+
1395
+ try {
1396
+ const stream = await provider.readStream(providerPath);
1397
+ const chunks: Buffer[] = [];
1398
+ for await (const chunk of stream) {
1399
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1400
+ }
1401
+ const original = Buffer.concat(chunks);
1402
+
1403
+ const rendered = await this.imageOptimizationService.resizeBuffer(
1404
+ original,
1405
+ contentType,
1406
+ { width: variant.width, format: variant.format, quality: variant.quality },
1407
+ );
1408
+ if (!rendered) return null;
1409
+
1410
+ try {
1411
+ await fs.mkdir(dirname(cachePath), { recursive: true });
1412
+ await fs.writeFile(cachePath, rendered.buffer);
1413
+ } catch (err) {
1414
+ // A read-only or full tmpdir must not break serving the image.
1415
+ this.logger.warn(
1416
+ `Could not cache image variant ${cachePath}: ${err instanceof Error ? err.message : String(err)}`,
1417
+ );
1418
+ }
1419
+
1420
+ return { buffer: rendered.buffer, mimetype: rendered.mimetype };
1421
+ } catch (err) {
1422
+ this.logger.warn(
1423
+ `Could not render image variant for file ${id}: ${err instanceof Error ? err.message : String(err)}. Serving the original.`,
1424
+ );
1425
+ return null;
1426
+ }
1427
+ }
1428
+
1064
1429
  /**
1065
1430
  * Serves any file by id with configurable HTTP caching (not immutable, shorter
1066
1431
  * TTL than images). Used by the generic open endpoint's numeric-id branch.
@@ -1615,4 +1980,34 @@ export class FileService implements OnModuleInit {
1615
1980
  },
1616
1981
  );
1617
1982
  }
1983
+
1984
+ /**
1985
+ * Base pública para URLs de arquivo devolvidas ao navegador.
1986
+ *
1987
+ * Não dá para derivar do `Host` da requisição: apps que falam com a API por
1988
+ * proxy interno (o class, via rewrite `/api` do Next) fazem o Nest enxergar o
1989
+ * Service do cluster, e a URL sai como `http://hub-api:3100/...` — inútil no
1990
+ * navegador. O setting `api-url` é a URL pública canônica da API no repo; o
1991
+ * `Host` fica só como fallback para ambientes sem o setting configurado.
1992
+ */
1993
+ async resolvePublicBaseUrl(req: Request): Promise<string> {
1994
+ let configured = '';
1995
+
1996
+ try {
1997
+ const settings = await this.settingService.getSettingValues([
1998
+ 'api-url',
1999
+ 'url',
2000
+ ]);
2001
+ const value = settings?.['api-url'] || settings?.['url'];
2002
+ configured = typeof value === 'string' ? value.trim() : '';
2003
+ } catch {
2004
+ configured = '';
2005
+ }
2006
+
2007
+ if (/^https?:\/\/.+/i.test(configured)) {
2008
+ return configured.replace(/\/+$/, '');
2009
+ }
2010
+
2011
+ return `${req.protocol}://${req.get('host')}`;
2012
+ }
1618
2013
  }
@@ -7,6 +7,7 @@ jest.mock('sharp', () => {
7
7
  webp: jest.fn(),
8
8
  jpeg: jest.fn(),
9
9
  png: jest.fn(),
10
+ avif: jest.fn(),
10
11
  toBuffer: jest.fn(),
11
12
  };
12
13
  const fn: any = jest.fn(() => instance);
@@ -46,6 +47,7 @@ describe('ImageOptimizationService', () => {
46
47
  inst.webp.mockReset().mockReturnValue(inst);
47
48
  inst.jpeg.mockReset().mockReturnValue(inst);
48
49
  inst.png.mockReset().mockReturnValue(inst);
50
+ inst.avif.mockReset().mockReturnValue(inst);
49
51
  inst.toBuffer.mockReset();
50
52
  });
51
53
 
@@ -280,4 +282,94 @@ describe('ImageOptimizationService', () => {
280
282
  expect(mockSharp.__instance.jpeg).toHaveBeenCalledWith({ quality: 80 });
281
283
  });
282
284
  });
285
+
286
+ describe('resizeBuffer', () => {
287
+ it('redimensiona sem ampliar e converte para o formato pedido', async () => {
288
+ const { service } = makeService();
289
+ mockSharp.__instance.webp.mockReturnValue(mockSharp.__instance);
290
+ mockSharp.__instance.toBuffer.mockResolvedValue(Buffer.from('out'));
291
+
292
+ const result = await service.resizeBuffer(Buffer.from('img'), 'image/jpeg', {
293
+ width: 640,
294
+ format: 'webp',
295
+ quality: 82,
296
+ });
297
+
298
+ expect(mockSharp.__instance.resize).toHaveBeenCalledWith({
299
+ width: 640,
300
+ fit: 'inside',
301
+ withoutEnlargement: true,
302
+ });
303
+ expect(mockSharp.__instance.webp).toHaveBeenCalledWith({ quality: 82 });
304
+ expect(result).toEqual({
305
+ buffer: Buffer.from('out'),
306
+ mimetype: 'image/webp',
307
+ originalname: 'image.webp',
308
+ });
309
+ });
310
+
311
+ it('mantém o formato de origem quando nenhum é pedido', async () => {
312
+ const { service } = makeService();
313
+ mockSharp.__instance.toBuffer.mockResolvedValue(Buffer.from('out'));
314
+
315
+ const result = await service.resizeBuffer(Buffer.from('img'), 'image/png', {
316
+ width: 320,
317
+ });
318
+
319
+ expect(mockSharp.__instance.png).toHaveBeenCalled();
320
+ expect(result?.mimetype).toBe('image/png');
321
+ });
322
+
323
+ it('não redimensiona SVG nem GIF animado', async () => {
324
+ const { service } = makeService();
325
+
326
+ expect(
327
+ await service.resizeBuffer(Buffer.from('x'), 'image/svg+xml', { width: 640 }),
328
+ ).toBeNull();
329
+ expect(
330
+ await service.resizeBuffer(Buffer.from('x'), 'image/gif', { width: 640 }),
331
+ ).toBeNull();
332
+ expect(mockSharp).not.toHaveBeenCalled();
333
+ });
334
+
335
+ it('retorna null para largura inválida', async () => {
336
+ const { service } = makeService();
337
+
338
+ expect(
339
+ await service.resizeBuffer(Buffer.from('x'), 'image/jpeg', { width: 0 }),
340
+ ).toBeNull();
341
+ expect(
342
+ await service.resizeBuffer(Buffer.from('x'), 'image/jpeg', {
343
+ width: -10,
344
+ }),
345
+ ).toBeNull();
346
+ });
347
+
348
+ it('retorna null quando o sharp lança, para o caller servir o original', async () => {
349
+ const { service } = makeService();
350
+ const warnSpy = jest
351
+ .spyOn((service as any).logger, 'warn')
352
+ .mockImplementation(() => undefined);
353
+ mockSharp.__instance.toBuffer.mockRejectedValue(new Error('sharp boom'));
354
+
355
+ const result = await service.resizeBuffer(Buffer.from('img'), 'image/jpeg', {
356
+ width: 640,
357
+ });
358
+
359
+ expect(result).toBeNull();
360
+ expect(warnSpy).toHaveBeenCalled();
361
+ });
362
+
363
+ it('faz clamp da quality fora da faixa', async () => {
364
+ const { service } = makeService();
365
+ mockSharp.__instance.toBuffer.mockResolvedValue(Buffer.from('out'));
366
+
367
+ await service.resizeBuffer(Buffer.from('img'), 'image/jpeg', {
368
+ width: 640,
369
+ quality: 0,
370
+ });
371
+
372
+ expect(mockSharp.__instance.jpeg).toHaveBeenCalledWith({ quality: 80 });
373
+ });
374
+ });
283
375
  });