@hed-hog/core 0.0.390 → 0.0.393

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
@@ -0,0 +1,170 @@
1
+ import { SesWebhookService } from './ses-webhook.service';
2
+ import * as snsUtil from './sns-message.util';
3
+
4
+ describe('SesWebhookService', () => {
5
+ let prisma: any;
6
+ let registry: any;
7
+ let delivery: any;
8
+ let service: SesWebhookService;
9
+ let verifySpy: jest.SpyInstance;
10
+
11
+ const topicArn = 'arn:aws:sns:us-east-1:123456789012:ses-events';
12
+
13
+ const request = (body: any, profileId: number | null = 5) =>
14
+ ({
15
+ integration: { id: 9, slug: 'ses-delivery-hcode', integration_profile_id: profileId },
16
+ body,
17
+ query: {},
18
+ headers: {},
19
+ remoteIp: '1.2.3.4',
20
+ req: {} as any,
21
+ }) as any;
22
+
23
+ const notification = (message: any) => ({
24
+ Type: 'Notification',
25
+ TopicArn: topicArn,
26
+ Message: JSON.stringify(message),
27
+ });
28
+
29
+ beforeEach(() => {
30
+ prisma = {
31
+ integration_profile: {
32
+ findUnique: jest.fn().mockResolvedValue({ config: { sns_topic_arn: topicArn } }),
33
+ },
34
+ };
35
+ registry = { register: jest.fn() };
36
+ delivery = {
37
+ ingestSesNotification: jest
38
+ .fn()
39
+ .mockResolvedValue({ events: 1, stored: 1, matched: 1 }),
40
+ };
41
+ verifySpy = jest
42
+ .spyOn(snsUtil, 'verifySnsSignature')
43
+ .mockResolvedValue({ valid: true });
44
+
45
+ service = new SesWebhookService(prisma, registry, delivery);
46
+ global.fetch = jest.fn().mockResolvedValue({ ok: true, text: async () => '' }) as any;
47
+ });
48
+
49
+ afterEach(() => jest.restoreAllMocks());
50
+
51
+ it('registra o protocolo no boot', () => {
52
+ service.onModuleInit();
53
+ expect(registry.register).toHaveBeenCalledWith('aws-ses', service);
54
+ });
55
+
56
+ it('processa a notificacao e devolve o resultado da ingestao', async () => {
57
+ const response = await service.handle(
58
+ request(notification({ notificationType: 'Delivery' })),
59
+ );
60
+
61
+ expect(delivery.ingestSesNotification).toHaveBeenCalledWith({
62
+ notificationType: 'Delivery',
63
+ });
64
+ expect(response.status).toBe(200);
65
+ });
66
+
67
+ it('recusa quando a assinatura nao confere', async () => {
68
+ verifySpy.mockResolvedValue({ valid: false, reason: 'signature mismatch' });
69
+
70
+ const response = await service.handle(request(notification({})));
71
+
72
+ expect(response.status).toBe(403);
73
+ // O motivo fica so no log de chamadas: numa rota publica, explicar a recusa
74
+ // ajuda quem esta atacando tanto quanto quem esta configurando.
75
+ expect(response.body).toBe('');
76
+ expect(response.logReason).toContain('signature mismatch');
77
+ expect(delivery.ingestSesNotification).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('recusa evento de um topico que nao e o configurado', async () => {
81
+ // A assinatura prova apenas que saiu do SNS da AWS - de qualquer conta.
82
+ const response = await service.handle(
83
+ request({
84
+ Type: 'Notification',
85
+ TopicArn: 'arn:aws:sns:us-east-1:999:outro',
86
+ Message: '{}',
87
+ }),
88
+ );
89
+
90
+ expect(response.status).toBe(403);
91
+ expect(delivery.ingestSesNotification).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('aceita qualquer topico enquanto o perfil nao configurou o ARN', async () => {
95
+ prisma.integration_profile.findUnique.mockResolvedValue({ config: {} });
96
+
97
+ const response = await service.handle(
98
+ request({
99
+ Type: 'Notification',
100
+ TopicArn: 'arn:aws:sns:us-east-1:999:qualquer',
101
+ Message: '{"notificationType":"Delivery"}',
102
+ }),
103
+ );
104
+
105
+ expect(response.status).toBe(200);
106
+ });
107
+
108
+ it('confirma a subscription seguindo o SubscribeURL', async () => {
109
+ // Sem este passo a subscription fica em PendingConfirmation para sempre e
110
+ // nenhum evento chega.
111
+ const response = await service.handle(
112
+ request({
113
+ Type: 'SubscriptionConfirmation',
114
+ TopicArn: topicArn,
115
+ SubscribeURL: 'https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription',
116
+ }),
117
+ );
118
+
119
+ expect(global.fetch).toHaveBeenCalledWith(
120
+ 'https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription',
121
+ { method: 'GET' },
122
+ );
123
+ expect(response.status).toBe(200);
124
+ });
125
+
126
+ it('nao segue um SubscribeURL que nao e da AWS', async () => {
127
+ // Seguir uma URL escolhida por quem chama transformaria este endpoint num
128
+ // buscador de URLs arbitrarias.
129
+ const response = await service.handle(
130
+ request({
131
+ Type: 'SubscriptionConfirmation',
132
+ TopicArn: topicArn,
133
+ SubscribeURL: 'https://evil.com/steal',
134
+ }),
135
+ );
136
+
137
+ expect(response.status).toBe(403);
138
+ expect(global.fetch).not.toHaveBeenCalled();
139
+ });
140
+
141
+ it('aceita o envelope como string, que e como o SNS costuma mandar', async () => {
142
+ const response = await service.handle(
143
+ request(JSON.stringify(notification({ notificationType: 'Bounce' }))),
144
+ );
145
+
146
+ expect(response.status).toBe(200);
147
+ expect(delivery.ingestSesNotification).toHaveBeenCalledWith({
148
+ notificationType: 'Bounce',
149
+ });
150
+ });
151
+
152
+ it('recusa corpo que nao e um envelope do SNS', async () => {
153
+ const response = await service.handle(request({ foo: 'bar' }));
154
+
155
+ expect(response.status).toBe(403);
156
+ expect(verifySpy).not.toHaveBeenCalled();
157
+ });
158
+
159
+ it('devolve 400 quando o Message nao e JSON valido', async () => {
160
+ const response = await service.handle(
161
+ request({ Type: 'Notification', TopicArn: topicArn, Message: 'nao e json' }),
162
+ );
163
+
164
+ expect(response.status).toBe(400);
165
+ });
166
+
167
+ it('recusa GET: o SNS nao faz handshake', async () => {
168
+ await expect(service.verify()).resolves.toMatchObject({ status: 403 });
169
+ });
170
+ });
@@ -0,0 +1,240 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Inject, Injectable, Logger, OnModuleInit, forwardRef } from '@nestjs/common';
3
+ import {
4
+ IWebhookProtocolAdapter,
5
+ WebhookProtocolRegistry,
6
+ WebhookProtocolRequest,
7
+ WebhookProtocolResponse,
8
+ } from '../webhook-integration/webhook-protocol.registry';
9
+ import { MailDeliveryService } from './mail-delivery.service';
10
+ import { SES_DELIVERY_WEBHOOK_PROTOCOL } from './mail-delivery.constants';
11
+ import { SnsEnvelope, isAwsSigningUrl, verifySnsSignature } from './sns-message.util';
12
+
13
+ /**
14
+ * 403 sem corpo: numa rota publica, dizer o motivo ajuda a mapear o que existe.
15
+ * O `logReason` fica so no log de chamadas, que exige login para ser lido.
16
+ */
17
+ function forbidden(logReason: string): WebhookProtocolResponse {
18
+ return { status: 403, contentType: 'text/plain', body: '', logReason };
19
+ }
20
+
21
+ /** Certificados da AWS mudam raramente; baixar um por notificacao seria absurdo. */
22
+ const CERT_CACHE_MAX = 8;
23
+
24
+ /**
25
+ * Eventos de entrega do Amazon SES, publicados via SNS, sobre o webhook publico
26
+ * do core.
27
+ *
28
+ * Duas camadas de credencial, como o resto dos adaptadores:
29
+ *
30
+ * 1. o `public_uuid` da URL, que a requisicao ja provou conhecer para este
31
+ * adaptador sequer ser chamado;
32
+ * 2. a assinatura RSA do proprio SNS, verificada contra o certificado da AWS.
33
+ *
34
+ * A segunda existe porque a primeira e uma URL: ela vaza em log de proxy, em
35
+ * print de tela, em copiar-e-colar. Um evento de bounce forjado envenenaria o
36
+ * historico de entrega exatamente da tela que existe para ser confiavel.
37
+ */
38
+ @Injectable()
39
+ export class SesWebhookService implements OnModuleInit, IWebhookProtocolAdapter {
40
+ private readonly logger = new Logger(SesWebhookService.name);
41
+ private readonly certCache = new Map<string, string>();
42
+
43
+ constructor(
44
+ @Inject(forwardRef(() => PrismaService))
45
+ private readonly prisma: PrismaService,
46
+ private readonly protocolRegistry: WebhookProtocolRegistry,
47
+ private readonly delivery: MailDeliveryService,
48
+ ) {}
49
+
50
+ onModuleInit() {
51
+ this.protocolRegistry.register(SES_DELIVERY_WEBHOOK_PROTOCOL, this);
52
+ }
53
+
54
+ /** O SNS so faz POST; nao existe handshake por GET. */
55
+ async verify(): Promise<WebhookProtocolResponse> {
56
+ return forbidden('GET not supported');
57
+ }
58
+
59
+ async handle(request: WebhookProtocolRequest): Promise<WebhookProtocolResponse> {
60
+ const envelope = this.readEnvelope(request);
61
+ if (!envelope) return forbidden('body is not an SNS envelope');
62
+
63
+ const signature = await verifySnsSignature(envelope, (url) =>
64
+ this.fetchCertificate(url),
65
+ );
66
+ if (!signature.valid) {
67
+ this.logger.warn(
68
+ `[ses] rejected webhook=${request.integration.id}: ${signature.reason}`,
69
+ );
70
+ return forbidden(`invalid SNS signature: ${signature.reason}`);
71
+ }
72
+
73
+ const topic = await this.checkTopic(request, envelope);
74
+ if (!topic.ok) {
75
+ this.logger.warn(
76
+ `[ses] rejected webhook=${request.integration.id}: ${topic.reason}`,
77
+ );
78
+ return forbidden(topic.reason ?? 'topic mismatch');
79
+ }
80
+
81
+ switch (String(envelope.Type)) {
82
+ case 'SubscriptionConfirmation':
83
+ return this.confirmSubscription(envelope);
84
+
85
+ case 'UnsubscribeConfirmation':
86
+ this.logger.warn(
87
+ `[ses] subscription removed from topic ${envelope.TopicArn ?? '?'}`,
88
+ );
89
+ return { status: 200, body: { ok: true, type: 'UnsubscribeConfirmation' } };
90
+
91
+ case 'Notification':
92
+ return this.handleNotification(envelope);
93
+
94
+ default:
95
+ return forbidden(`unsupported SNS type "${envelope.Type}"`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Sem este passo a subscription fica em PendingConfirmation para sempre e
101
+ * nenhum evento chega - a falha silenciosa classica desse setup, que parece
102
+ * "a AWS nao esta mandando nada".
103
+ */
104
+ private async confirmSubscription(
105
+ envelope: SnsEnvelope,
106
+ ): Promise<WebhookProtocolResponse> {
107
+ const url = String(envelope.SubscribeURL ?? '');
108
+
109
+ // Mesma trava do certificado: seguir uma URL escolhida por quem chama
110
+ // transformaria este endpoint num buscador de URLs arbitrarias.
111
+ if (!isAwsSigningUrl(url)) {
112
+ return forbidden('SubscribeURL is not an AWS SNS endpoint');
113
+ }
114
+
115
+ try {
116
+ const response = await fetch(url, { method: 'GET' });
117
+ if (!response.ok) {
118
+ this.logger.error(
119
+ `[ses] subscription confirmation failed with HTTP ${response.status}`,
120
+ );
121
+ return { status: 502, body: { ok: false, reason: 'confirmation-failed' } };
122
+ }
123
+
124
+ this.logger.log(`[ses] subscription confirmed for topic ${envelope.TopicArn}`);
125
+ return { status: 200, body: { ok: true, type: 'SubscriptionConfirmation' } };
126
+ } catch (error) {
127
+ this.logger.error(
128
+ `[ses] subscription confirmation error: ${(error as Error)?.message}`,
129
+ );
130
+ return { status: 502, body: { ok: false, reason: 'confirmation-error' } };
131
+ }
132
+ }
133
+
134
+ private async handleNotification(
135
+ envelope: SnsEnvelope,
136
+ ): Promise<WebhookProtocolResponse> {
137
+ let payload: any;
138
+ try {
139
+ // `Message` e uma string com o JSON do evento: a dupla serializacao e do
140
+ // protocolo do SNS.
141
+ payload = JSON.parse(String(envelope.Message ?? ''));
142
+ } catch {
143
+ return { status: 400, body: { ok: false, reason: 'invalid-message-json' } };
144
+ }
145
+
146
+ const result = await this.delivery.ingestSesNotification(payload);
147
+ return { status: 200, body: { ok: true, ...result } };
148
+ }
149
+
150
+ /**
151
+ * Confere que o evento veio do topico esperado.
152
+ *
153
+ * A assinatura prova apenas que a mensagem saiu do SNS da AWS - de qualquer
154
+ * conta, de qualquer topico. Sem esta checagem, quem descobrisse a URL poderia
155
+ * publicar bounces forjados no proprio topico e envenenar o historico de
156
+ * entrega justamente da tela que existe para ser confiavel.
157
+ *
158
+ * Com `sns_topic_arn` vazio o evento passa, com aviso: e o estado do setup
159
+ * inicial, antes de o topico existir. Preencher o campo no perfil e o que
160
+ * fecha a porta.
161
+ */
162
+ private async checkTopic(
163
+ request: WebhookProtocolRequest,
164
+ envelope: SnsEnvelope,
165
+ ): Promise<{ ok: boolean; reason?: string }> {
166
+ const profileId = request.integration.integration_profile_id;
167
+ if (!profileId) return { ok: true };
168
+
169
+ const profile = await this.prisma.integration_profile.findUnique({
170
+ where: { id: profileId },
171
+ select: { config: true },
172
+ });
173
+
174
+ const expected = String(
175
+ (profile?.config as any)?.sns_topic_arn ?? '',
176
+ ).trim();
177
+
178
+ if (!expected) {
179
+ this.logger.warn(
180
+ `[ses] profile ${profileId} has no sns_topic_arn: accepting events from any SNS topic`,
181
+ );
182
+ return { ok: true };
183
+ }
184
+
185
+ const received = String(envelope.TopicArn ?? '').trim();
186
+ return received === expected
187
+ ? { ok: true }
188
+ : { ok: false, reason: 'TopicArn does not match the configured topic' };
189
+ }
190
+
191
+ /**
192
+ * O SNS envia `Content-Type: text/plain`, entao o body pode chegar como string
193
+ * mesmo com o parser de JSON configurado. Aceitar as duas formas evita depender
194
+ * de um detalhe do middleware.
195
+ */
196
+ private readEnvelope(request: WebhookProtocolRequest): SnsEnvelope | null {
197
+ const body = request.body;
198
+
199
+ if (body && typeof body === 'object' && !Buffer.isBuffer(body)) {
200
+ const envelope = body as SnsEnvelope;
201
+ return envelope.Type ? envelope : null;
202
+ }
203
+
204
+ const raw =
205
+ typeof body === 'string'
206
+ ? body
207
+ : request.rawBody
208
+ ? request.rawBody.toString('utf8')
209
+ : '';
210
+ if (!raw) return null;
211
+
212
+ try {
213
+ const parsed = JSON.parse(raw) as SnsEnvelope;
214
+ return parsed?.Type ? parsed : null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ private async fetchCertificate(url: string): Promise<string> {
221
+ const cached = this.certCache.get(url);
222
+ if (cached) return cached;
223
+
224
+ const response = await fetch(url);
225
+ if (!response.ok) {
226
+ throw new Error(`HTTP ${response.status}`);
227
+ }
228
+ const pem = await response.text();
229
+
230
+ // Cache pequeno com descarte do mais antigo: a URL do certificado muda de
231
+ // tempos em tempos e um Map sem teto viraria vazamento lento.
232
+ if (this.certCache.size >= CERT_CACHE_MAX) {
233
+ const oldest = this.certCache.keys().next().value;
234
+ if (oldest) this.certCache.delete(oldest);
235
+ }
236
+ this.certCache.set(url, pem);
237
+
238
+ return pem;
239
+ }
240
+ }
@@ -0,0 +1,163 @@
1
+ import { generateKeyPairSync, createSign } from 'crypto';
2
+ import {
3
+ buildStringToSign,
4
+ isAwsSigningUrl,
5
+ verifySnsSignature,
6
+ } from './sns-message.util';
7
+
8
+ describe('isAwsSigningUrl', () => {
9
+ it('aceita o endpoint do SNS de qualquer regiao', () => {
10
+ expect(
11
+ isAwsSigningUrl('https://sns.us-east-1.amazonaws.com/SimpleNotification-x.pem'),
12
+ ).toBe(true);
13
+ expect(isAwsSigningUrl('https://sns.cn-north-1.amazonaws.com.cn/a.pem')).toBe(
14
+ true,
15
+ );
16
+ });
17
+
18
+ it.each([
19
+ ['http://sns.us-east-1.amazonaws.com/a.pem', 'sem TLS'],
20
+ ['https://sns.us-east-1.amazonaws.com.evil.com/a.pem', 'sufixo de dominio'],
21
+ ['https://evil.com/sns.us-east-1.amazonaws.com/a.pem', 'host falso no path'],
22
+ ['https://s3.amazonaws.com/a.pem', 'outro servico da AWS'],
23
+ ['nao e url', 'lixo'],
24
+ ])('recusa %s (%s)', (url) => {
25
+ // Sem esta trava, quem manda o webhook escolhe de onde baixamos a chave
26
+ // publica - e passa a poder assinar o que quiser.
27
+ expect(isAwsSigningUrl(url)).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('buildStringToSign', () => {
32
+ it('usa a ordem exata da especificacao, nao a alfabetica do objeto', () => {
33
+ const out = buildStringToSign({
34
+ Type: 'Notification',
35
+ TopicArn: 'arn:aws:sns:us-east-1:1:t',
36
+ Timestamp: '2026-08-18T14:00:00.000Z',
37
+ MessageId: 'mid',
38
+ Message: 'corpo',
39
+ });
40
+
41
+ expect(out).toBe(
42
+ 'Message\ncorpo\n' +
43
+ 'MessageId\nmid\n' +
44
+ 'Timestamp\n2026-08-18T14:00:00.000Z\n' +
45
+ 'TopicArn\narn:aws:sns:us-east-1:1:t\n' +
46
+ 'Type\nNotification\n',
47
+ );
48
+ });
49
+
50
+ it('pula o campo ausente, como o Subject', () => {
51
+ const out = buildStringToSign({
52
+ Type: 'Notification',
53
+ Message: 'm',
54
+ MessageId: 'i',
55
+ Timestamp: 't',
56
+ TopicArn: 'a',
57
+ });
58
+ expect(out).not.toContain('Subject');
59
+ });
60
+
61
+ it('devolve null para um tipo que nao sabe assinar', () => {
62
+ expect(buildStringToSign({ Type: 'Coisa' })).toBeNull();
63
+ });
64
+ });
65
+
66
+ describe('verifySnsSignature', () => {
67
+ const { privateKey, certificate } = buildKeyPair();
68
+
69
+ const baseEnvelope = {
70
+ Type: 'Notification',
71
+ MessageId: 'mid-1',
72
+ TopicArn: 'arn:aws:sns:us-east-1:123456789012:ses-events',
73
+ Message: '{"notificationType":"Delivery"}',
74
+ Timestamp: '2026-08-18T14:02:27.000Z',
75
+ SignatureVersion: '1',
76
+ SigningCertURL: 'https://sns.us-east-1.amazonaws.com/SimpleNotification-a.pem',
77
+ };
78
+
79
+ const fetcher = jest.fn().mockResolvedValue(certificate);
80
+
81
+ beforeEach(() => fetcher.mockClear());
82
+
83
+ it('aceita uma assinatura valida (SignatureVersion 1)', async () => {
84
+ const envelope = { ...baseEnvelope, Signature: sign(privateKey, baseEnvelope, 'RSA-SHA1') };
85
+
86
+ await expect(verifySnsSignature(envelope, fetcher)).resolves.toEqual({
87
+ valid: true,
88
+ });
89
+ });
90
+
91
+ it('aceita SignatureVersion 2 (SHA256)', async () => {
92
+ const source = { ...baseEnvelope, SignatureVersion: '2' };
93
+ const envelope = { ...source, Signature: sign(privateKey, source, 'RSA-SHA256') };
94
+
95
+ await expect(verifySnsSignature(envelope, fetcher)).resolves.toEqual({
96
+ valid: true,
97
+ });
98
+ });
99
+
100
+ it('recusa quando o corpo foi trocado depois de assinado', async () => {
101
+ const envelope = {
102
+ ...baseEnvelope,
103
+ Signature: sign(privateKey, baseEnvelope, 'RSA-SHA1'),
104
+ Message: '{"notificationType":"Bounce"}',
105
+ };
106
+
107
+ const result = await verifySnsSignature(envelope, fetcher);
108
+ expect(result.valid).toBe(false);
109
+ expect(result.reason).toBe('signature mismatch');
110
+ });
111
+
112
+ it('nao baixa certificado de host que nao e da AWS', async () => {
113
+ const result = await verifySnsSignature(
114
+ { ...baseEnvelope, Signature: 'x', SigningCertURL: 'https://evil.com/a.pem' },
115
+ fetcher,
116
+ );
117
+
118
+ expect(result.valid).toBe(false);
119
+ expect(fetcher).not.toHaveBeenCalled();
120
+ });
121
+
122
+ it('recusa envelope sem assinatura', async () => {
123
+ const result = await verifySnsSignature(baseEnvelope, fetcher);
124
+ expect(result).toEqual({ valid: false, reason: 'missing Signature' });
125
+ });
126
+
127
+ it('recusa uma SignatureVersion desconhecida em vez de adivinhar', async () => {
128
+ const result = await verifySnsSignature(
129
+ { ...baseEnvelope, SignatureVersion: '9', Signature: 'x' },
130
+ fetcher,
131
+ );
132
+
133
+ expect(result.valid).toBe(false);
134
+ expect(result.reason).toContain('unsupported SignatureVersion');
135
+ });
136
+
137
+ it('recusa quando o certificado nao pode ser baixado', async () => {
138
+ const failing = jest.fn().mockRejectedValue(new Error('HTTP 503'));
139
+ const envelope = { ...baseEnvelope, Signature: sign(privateKey, baseEnvelope, 'RSA-SHA1') };
140
+
141
+ const result = await verifySnsSignature(envelope, failing);
142
+ expect(result.valid).toBe(false);
143
+ expect(result.reason).toContain('HTTP 503');
144
+ });
145
+ });
146
+
147
+ function sign(privateKey: string, envelope: any, algorithm: string): string {
148
+ const signer = createSign(algorithm);
149
+ signer.update(buildStringToSign(envelope) as string, 'utf8');
150
+ return signer.sign(privateKey, 'base64');
151
+ }
152
+
153
+ /** Par de chaves autoassinado, para o teste nao depender da AWS nem de rede. */
154
+ function buildKeyPair(): { privateKey: string; certificate: string } {
155
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
156
+ modulusLength: 2048,
157
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
158
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
159
+ });
160
+ // `crypto.verify` aceita a chave publica no lugar do certificado PEM, que e o
161
+ // que o SNS serve — o formato do wrapper nao muda a verificacao.
162
+ return { privateKey, certificate: publicKey };
163
+ }
@@ -0,0 +1,125 @@
1
+ import { createVerify } from 'crypto';
2
+
3
+ /**
4
+ * Envelope do SNS. `Message` e uma string com o JSON do evento de verdade -
5
+ * dupla serializacao e do protocolo, nao um engano.
6
+ */
7
+ export type SnsEnvelope = {
8
+ Type?: string;
9
+ MessageId?: string;
10
+ TopicArn?: string;
11
+ Subject?: string;
12
+ Message?: string;
13
+ Timestamp?: string;
14
+ Token?: string;
15
+ SubscribeURL?: string;
16
+ SignatureVersion?: string;
17
+ Signature?: string;
18
+ SigningCertURL?: string;
19
+ SigningCertUrl?: string;
20
+ };
21
+
22
+ /**
23
+ * Campos que entram na string assinada, na ordem exata definida pela AWS.
24
+ * A ordem nao e alfabetica por acaso: e a especificacao, e trocar dois campos
25
+ * de lugar faz toda assinatura valida ser recusada.
26
+ */
27
+ const SIGNED_KEYS: Record<string, string[]> = {
28
+ Notification: ['Message', 'MessageId', 'Subject', 'Timestamp', 'TopicArn', 'Type'],
29
+ SubscriptionConfirmation: [
30
+ 'Message',
31
+ 'MessageId',
32
+ 'SubscribeURL',
33
+ 'Timestamp',
34
+ 'Token',
35
+ 'TopicArn',
36
+ 'Type',
37
+ ],
38
+ };
39
+ SIGNED_KEYS.UnsubscribeConfirmation = SIGNED_KEYS.SubscriptionConfirmation;
40
+
41
+ /**
42
+ * So a AWS pode ser a origem do certificado.
43
+ *
44
+ * Sem esta trava, quem manda o webhook escolhe de onde baixamos a chave publica
45
+ * - e passa a poder assinar o que quiser. E a diferenca entre validar assinatura
46
+ * e encenar que valida.
47
+ */
48
+ export function isAwsSigningUrl(url: string): boolean {
49
+ try {
50
+ const parsed = new URL(url);
51
+ if (parsed.protocol !== 'https:') return false;
52
+ return /^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/.test(parsed.hostname);
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ /** Monta a string canonica `chave\nvalor\n` dos campos assinados. */
59
+ export function buildStringToSign(envelope: SnsEnvelope): string | null {
60
+ const keys = SIGNED_KEYS[String(envelope.Type ?? '')];
61
+ if (!keys) return null;
62
+
63
+ let out = '';
64
+ for (const key of keys) {
65
+ const value = (envelope as Record<string, unknown>)[key];
66
+ // Campo ausente e pulado (o `Subject` costuma nao vir); presente e vazio
67
+ // entra, porque foi assim que a AWS assinou.
68
+ if (value === undefined || value === null) continue;
69
+ out += `${key}\n${String(value)}\n`;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Verifica a assinatura do envelope contra o certificado da AWS.
76
+ *
77
+ * `certificateFetcher` e injetado para o teste nao depender de rede.
78
+ */
79
+ export async function verifySnsSignature(
80
+ envelope: SnsEnvelope,
81
+ certificateFetcher: (url: string) => Promise<string>,
82
+ ): Promise<{ valid: boolean; reason?: string }> {
83
+ const certUrl = String(envelope.SigningCertURL ?? envelope.SigningCertUrl ?? '');
84
+ if (!certUrl) return { valid: false, reason: 'missing SigningCertURL' };
85
+ if (!isAwsSigningUrl(certUrl)) {
86
+ return { valid: false, reason: 'SigningCertURL is not an AWS SNS endpoint' };
87
+ }
88
+ if (!envelope.Signature) return { valid: false, reason: 'missing Signature' };
89
+
90
+ const stringToSign = buildStringToSign(envelope);
91
+ if (!stringToSign) {
92
+ return { valid: false, reason: `unsupported SNS message type "${envelope.Type}"` };
93
+ }
94
+
95
+ // SignatureVersion 1 assina com SHA1; a 2, com SHA256. Qualquer outra e
96
+ // desconhecida e recusada em vez de adivinhada.
97
+ const version = String(envelope.SignatureVersion ?? '1');
98
+ const algorithm =
99
+ version === '2' ? 'RSA-SHA256' : version === '1' ? 'RSA-SHA1' : null;
100
+ if (!algorithm) {
101
+ return { valid: false, reason: `unsupported SignatureVersion "${version}"` };
102
+ }
103
+
104
+ let certificate: string;
105
+ try {
106
+ certificate = await certificateFetcher(certUrl);
107
+ } catch (error) {
108
+ return {
109
+ valid: false,
110
+ reason: `could not fetch signing certificate: ${(error as Error)?.message}`,
111
+ };
112
+ }
113
+
114
+ try {
115
+ const verifier = createVerify(algorithm);
116
+ verifier.update(stringToSign, 'utf8');
117
+ const valid = verifier.verify(certificate, String(envelope.Signature), 'base64');
118
+ return valid ? { valid: true } : { valid: false, reason: 'signature mismatch' };
119
+ } catch (error) {
120
+ return {
121
+ valid: false,
122
+ reason: `signature check failed: ${(error as Error)?.message}`,
123
+ };
124
+ }
125
+ }