@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4397.1.9a65481757

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 (533) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
  3. package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
  4. package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
  5. package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
  6. package/dist/generated/entities/channel_thread_token/index.js +17 -0
  7. package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
  8. package/dist/generated/entities/communication_channel/index.js +43 -0
  9. package/dist/generated/entities/communication_channel/index.js.map +7 -0
  10. package/dist/generated/entities/customer_interaction/index.js +4 -0
  11. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  12. package/dist/generated/entities/external_conversation/index.js +25 -0
  13. package/dist/generated/entities/external_conversation/index.js.map +7 -0
  14. package/dist/generated/entities/external_message/index.js +25 -0
  15. package/dist/generated/entities/external_message/index.js.map +7 -0
  16. package/dist/generated/entities/integration_credentials/index.js +3 -1
  17. package/dist/generated/entities/integration_credentials/index.js.map +2 -2
  18. package/dist/generated/entities/message/index.js +2 -0
  19. package/dist/generated/entities/message/index.js.map +2 -2
  20. package/dist/generated/entities/message_channel_link/index.js +33 -0
  21. package/dist/generated/entities/message_channel_link/index.js.map +7 -0
  22. package/dist/generated/entities/message_reaction/index.js +25 -0
  23. package/dist/generated/entities/message_reaction/index.js.map +7 -0
  24. package/dist/generated/entities.ids.generated.js +11 -0
  25. package/dist/generated/entities.ids.generated.js.map +2 -2
  26. package/dist/generated/entity-fields-registry.js +117 -0
  27. package/dist/generated/entity-fields-registry.js.map +2 -2
  28. package/dist/helpers/integration/authFixtures.js +2 -1
  29. package/dist/helpers/integration/authFixtures.js.map +2 -2
  30. package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
  31. package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
  32. package/dist/modules/communication_channels/acl.js +47 -0
  33. package/dist/modules/communication_channels/acl.js.map +7 -0
  34. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
  37. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
  38. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
  39. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
  40. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
  41. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
  42. package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
  43. package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
  44. package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
  45. package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
  46. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
  47. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
  48. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
  49. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
  50. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
  51. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
  52. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
  53. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
  54. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
  55. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
  56. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
  57. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
  58. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
  59. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
  60. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
  61. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
  62. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
  63. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
  64. package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
  65. package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
  66. package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
  67. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
  68. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
  69. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
  70. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
  71. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
  72. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
  73. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
  74. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
  75. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
  76. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
  77. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
  78. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
  79. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
  80. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
  81. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
  82. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
  83. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
  84. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
  85. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
  86. package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
  87. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
  88. package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
  89. package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
  90. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
  91. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
  92. package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
  93. package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
  94. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
  95. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
  96. package/dist/modules/communication_channels/commands/interceptors.js +68 -0
  97. package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
  98. package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
  99. package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
  100. package/dist/modules/communication_channels/commands/push-register.js +146 -0
  101. package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
  102. package/dist/modules/communication_channels/commands/push-renew.js +23 -0
  103. package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
  104. package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
  105. package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
  106. package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
  107. package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
  108. package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
  109. package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
  110. package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
  111. package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
  112. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
  113. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
  114. package/dist/modules/communication_channels/data/enrichers.js +286 -0
  115. package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
  116. package/dist/modules/communication_channels/data/entities.js +447 -0
  117. package/dist/modules/communication_channels/data/entities.js.map +7 -0
  118. package/dist/modules/communication_channels/data/extensions.js +67 -0
  119. package/dist/modules/communication_channels/data/extensions.js.map +7 -0
  120. package/dist/modules/communication_channels/data/validators.js +123 -0
  121. package/dist/modules/communication_channels/data/validators.js.map +7 -0
  122. package/dist/modules/communication_channels/di.js +35 -0
  123. package/dist/modules/communication_channels/di.js.map +7 -0
  124. package/dist/modules/communication_channels/encryption.js +12 -0
  125. package/dist/modules/communication_channels/encryption.js.map +7 -0
  126. package/dist/modules/communication_channels/events.js +124 -0
  127. package/dist/modules/communication_channels/events.js.map +7 -0
  128. package/dist/modules/communication_channels/index.js +20 -0
  129. package/dist/modules/communication_channels/index.js.map +7 -0
  130. package/dist/modules/communication_channels/lib/access-control.js +43 -0
  131. package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
  132. package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
  133. package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
  134. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
  135. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
  136. package/dist/modules/communication_channels/lib/adapter.js +1 -0
  137. package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
  138. package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
  139. package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
  140. package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
  141. package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
  142. package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
  143. package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
  144. package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
  145. package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
  146. package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
  147. package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
  148. package/dist/modules/communication_channels/lib/email-contact.js +14 -0
  149. package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
  150. package/dist/modules/communication_channels/lib/email-mime.js +269 -0
  151. package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
  152. package/dist/modules/communication_channels/lib/error-classification.js +101 -0
  153. package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
  154. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
  155. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
  156. package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
  157. package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
  158. package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
  159. package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
  160. package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
  161. package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
  162. package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
  163. package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
  164. package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
  165. package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
  166. package/dist/modules/communication_channels/lib/provider-health.js +24 -0
  167. package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
  168. package/dist/modules/communication_channels/lib/push-state.js +19 -0
  169. package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
  170. package/dist/modules/communication_channels/lib/queue.js +54 -0
  171. package/dist/modules/communication_channels/lib/queue.js.map +7 -0
  172. package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
  173. package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
  174. package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
  175. package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
  176. package/dist/modules/communication_channels/lib/registry.js +67 -0
  177. package/dist/modules/communication_channels/lib/registry.js.map +7 -0
  178. package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
  179. package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
  180. package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
  181. package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
  182. package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
  183. package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
  184. package/dist/modules/communication_channels/lib/system-user.js +22 -0
  185. package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
  186. package/dist/modules/communication_channels/lib/test-seed.js +68 -0
  187. package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
  188. package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
  189. package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
  190. package/dist/modules/communication_channels/lib/thread-token.js +219 -0
  191. package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
  192. package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
  193. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
  194. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
  195. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
  196. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
  197. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
  198. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
  199. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
  200. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
  201. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
  202. package/dist/modules/communication_channels/notifications.client.js +51 -0
  203. package/dist/modules/communication_channels/notifications.client.js.map +7 -0
  204. package/dist/modules/communication_channels/notifications.handlers.js +53 -0
  205. package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
  206. package/dist/modules/communication_channels/notifications.js +56 -0
  207. package/dist/modules/communication_channels/notifications.js.map +7 -0
  208. package/dist/modules/communication_channels/setup.js +105 -0
  209. package/dist/modules/communication_channels/setup.js.map +7 -0
  210. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
  211. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
  212. package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
  213. package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
  214. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
  215. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
  216. package/dist/modules/communication_channels/widgets/components.js +7 -0
  217. package/dist/modules/communication_channels/widgets/components.js.map +7 -0
  218. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
  219. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
  220. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
  221. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
  222. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
  223. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
  224. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
  225. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
  226. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
  227. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
  228. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
  229. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
  230. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
  231. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
  232. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
  233. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
  234. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
  235. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
  236. package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
  237. package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
  238. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
  239. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
  240. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
  241. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
  242. package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
  243. package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
  244. package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
  245. package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
  246. package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
  247. package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
  248. package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
  249. package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
  250. package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
  251. package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
  252. package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
  253. package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
  254. package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
  255. package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
  256. package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
  257. package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
  258. package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
  259. package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
  260. package/dist/modules/customers/acl.js +18 -0
  261. package/dist/modules/customers/acl.js.map +2 -2
  262. package/dist/modules/customers/api/activities/route.js +9 -0
  263. package/dist/modules/customers/api/activities/route.js.map +2 -2
  264. package/dist/modules/customers/api/companies/[id]/route.js +18 -7
  265. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  266. package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
  267. package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
  268. package/dist/modules/customers/api/interactions/counts/route.js +6 -0
  269. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  270. package/dist/modules/customers/api/interactions/route.js +26 -7
  271. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  272. package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
  273. package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
  274. package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
  275. package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
  276. package/dist/modules/customers/api/people/[id]/route.js +12 -4
  277. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  278. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
  279. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  280. package/dist/modules/customers/commands/deals.js +46 -5
  281. package/dist/modules/customers/commands/deals.js.map +2 -2
  282. package/dist/modules/customers/commands/interactions.js +16 -0
  283. package/dist/modules/customers/commands/interactions.js.map +2 -2
  284. package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
  285. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  286. package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
  287. package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
  288. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  289. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  290. package/dist/modules/customers/components/detail/DealsSection.js +10 -0
  291. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  292. package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
  293. package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
  294. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
  295. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
  296. package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
  297. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  298. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
  299. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
  300. package/dist/modules/customers/data/enrichers.js +133 -2
  301. package/dist/modules/customers/data/enrichers.js.map +2 -2
  302. package/dist/modules/customers/data/entities.js +18 -0
  303. package/dist/modules/customers/data/entities.js.map +2 -2
  304. package/dist/modules/customers/data/extensions.js +16 -0
  305. package/dist/modules/customers/data/extensions.js.map +7 -0
  306. package/dist/modules/customers/encryption.js +11 -0
  307. package/dist/modules/customers/encryption.js.map +2 -2
  308. package/dist/modules/customers/events.js +4 -1
  309. package/dist/modules/customers/events.js.map +2 -2
  310. package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
  311. package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
  312. package/dist/modules/customers/lib/kysely.js.map +2 -2
  313. package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
  314. package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
  315. package/dist/modules/customers/lib/personEmailThreads.js +205 -0
  316. package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
  317. package/dist/modules/customers/lib/visibilityFilter.js +51 -0
  318. package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
  319. package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
  320. package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
  321. package/dist/modules/customers/setup.js +2 -1
  322. package/dist/modules/customers/setup.js.map +2 -2
  323. package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
  324. package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
  325. package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
  326. package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
  327. package/dist/modules/integrations/data/entities.js +8 -1
  328. package/dist/modules/integrations/data/entities.js.map +2 -2
  329. package/dist/modules/integrations/lib/credentials-service.js +29 -14
  330. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  331. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
  332. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
  333. package/dist/modules/messages/commands/messages.js +70 -8
  334. package/dist/modules/messages/commands/messages.js.map +2 -2
  335. package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
  336. package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
  337. package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
  338. package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
  339. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
  340. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  341. package/dist/modules/messages/data/entities.js +8 -1
  342. package/dist/modules/messages/data/entities.js.map +2 -2
  343. package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
  344. package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
  345. package/dist/modules/messages/widgets/injection-table.js +7 -0
  346. package/dist/modules/messages/widgets/injection-table.js.map +7 -0
  347. package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
  348. package/generated/entities/channel_thread_mapping/index.ts +11 -0
  349. package/generated/entities/channel_thread_token/index.ts +7 -0
  350. package/generated/entities/communication_channel/index.ts +20 -0
  351. package/generated/entities/customer_interaction/index.ts +2 -0
  352. package/generated/entities/external_conversation/index.ts +11 -0
  353. package/generated/entities/external_message/index.ts +11 -0
  354. package/generated/entities/integration_credentials/index.ts +1 -0
  355. package/generated/entities/message/index.ts +1 -0
  356. package/generated/entities/message_channel_link/index.ts +15 -0
  357. package/generated/entities/message_reaction/index.ts +11 -0
  358. package/generated/entities.ids.generated.ts +11 -0
  359. package/generated/entity-fields-registry.ts +117 -0
  360. package/package.json +9 -7
  361. package/src/helpers/integration/authFixtures.ts +4 -1
  362. package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
  363. package/src/modules/communication_channels/acl.ts +43 -0
  364. package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
  365. package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
  366. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
  367. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
  368. package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
  369. package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
  370. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
  371. package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
  372. package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
  373. package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
  374. package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
  375. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
  376. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
  377. package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
  378. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
  379. package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
  380. package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
  381. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
  382. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
  383. package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
  384. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
  385. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
  386. package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
  387. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
  388. package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
  389. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
  390. package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
  391. package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
  392. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
  393. package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
  394. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
  395. package/src/modules/communication_channels/commands/interceptors.ts +104 -0
  396. package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
  397. package/src/modules/communication_channels/commands/push-register.ts +203 -0
  398. package/src/modules/communication_channels/commands/push-renew.ts +49 -0
  399. package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
  400. package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
  401. package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
  402. package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
  403. package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
  404. package/src/modules/communication_channels/data/enrichers.ts +413 -0
  405. package/src/modules/communication_channels/data/entities.ts +546 -0
  406. package/src/modules/communication_channels/data/extensions.ts +76 -0
  407. package/src/modules/communication_channels/data/validators.ts +138 -0
  408. package/src/modules/communication_channels/di.ts +40 -0
  409. package/src/modules/communication_channels/encryption.ts +44 -0
  410. package/src/modules/communication_channels/events.ts +122 -0
  411. package/src/modules/communication_channels/i18n/de.json +138 -0
  412. package/src/modules/communication_channels/i18n/en.json +138 -0
  413. package/src/modules/communication_channels/i18n/es.json +138 -0
  414. package/src/modules/communication_channels/i18n/pl.json +138 -0
  415. package/src/modules/communication_channels/index.ts +19 -0
  416. package/src/modules/communication_channels/lib/access-control.ts +110 -0
  417. package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
  418. package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
  419. package/src/modules/communication_channels/lib/adapter.ts +605 -0
  420. package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
  421. package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
  422. package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
  423. package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
  424. package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
  425. package/src/modules/communication_channels/lib/email-contact.ts +17 -0
  426. package/src/modules/communication_channels/lib/email-mime.ts +442 -0
  427. package/src/modules/communication_channels/lib/error-classification.ts +144 -0
  428. package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
  429. package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
  430. package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
  431. package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
  432. package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
  433. package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
  434. package/src/modules/communication_channels/lib/provider-health.ts +47 -0
  435. package/src/modules/communication_channels/lib/push-state.ts +38 -0
  436. package/src/modules/communication_channels/lib/queue.ts +66 -0
  437. package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
  438. package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
  439. package/src/modules/communication_channels/lib/registry.ts +99 -0
  440. package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
  441. package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
  442. package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
  443. package/src/modules/communication_channels/lib/system-user.ts +74 -0
  444. package/src/modules/communication_channels/lib/test-seed.ts +140 -0
  445. package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
  446. package/src/modules/communication_channels/lib/thread-token.ts +355 -0
  447. package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
  448. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
  449. package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
  450. package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
  451. package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
  452. package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
  453. package/src/modules/communication_channels/notifications.client.ts +50 -0
  454. package/src/modules/communication_channels/notifications.handlers.ts +86 -0
  455. package/src/modules/communication_channels/notifications.ts +52 -0
  456. package/src/modules/communication_channels/setup.ts +158 -0
  457. package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
  458. package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
  459. package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
  460. package/src/modules/communication_channels/widgets/components.ts +36 -0
  461. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
  462. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
  463. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
  464. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
  465. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
  466. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
  467. package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
  468. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
  469. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
  470. package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
  471. package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
  472. package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
  473. package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
  474. package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
  475. package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
  476. package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
  477. package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
  478. package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
  479. package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
  480. package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
  481. package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
  482. package/src/modules/customers/acl.ts +18 -0
  483. package/src/modules/customers/api/activities/route.ts +13 -0
  484. package/src/modules/customers/api/companies/[id]/route.ts +21 -1
  485. package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
  486. package/src/modules/customers/api/interactions/counts/route.ts +10 -0
  487. package/src/modules/customers/api/interactions/route.ts +51 -5
  488. package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
  489. package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
  490. package/src/modules/customers/api/people/[id]/route.ts +17 -2
  491. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
  492. package/src/modules/customers/commands/deals.ts +65 -6
  493. package/src/modules/customers/commands/interactions.ts +30 -0
  494. package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
  495. package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
  496. package/src/modules/customers/components/detail/DealForm.tsx +2 -1
  497. package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
  498. package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
  499. package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
  500. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
  501. package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
  502. package/src/modules/customers/data/enrichers.ts +252 -1
  503. package/src/modules/customers/data/entities.ts +46 -1
  504. package/src/modules/customers/data/extensions.ts +26 -0
  505. package/src/modules/customers/encryption.ts +11 -0
  506. package/src/modules/customers/events.ts +4 -0
  507. package/src/modules/customers/i18n/de.json +41 -0
  508. package/src/modules/customers/i18n/en.json +41 -0
  509. package/src/modules/customers/i18n/es.json +41 -0
  510. package/src/modules/customers/i18n/pl.json +41 -0
  511. package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
  512. package/src/modules/customers/lib/kysely.ts +16 -0
  513. package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
  514. package/src/modules/customers/lib/personEmailThreads.ts +325 -0
  515. package/src/modules/customers/lib/visibilityFilter.ts +152 -0
  516. package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
  517. package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
  518. package/src/modules/customers/setup.ts +1 -0
  519. package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
  520. package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
  521. package/src/modules/integrations/AGENTS.md +9 -0
  522. package/src/modules/integrations/data/entities.ts +21 -1
  523. package/src/modules/integrations/lib/credentials-service.ts +49 -13
  524. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
  525. package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
  526. package/src/modules/messages/commands/messages.ts +101 -8
  527. package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
  528. package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
  529. package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
  530. package/src/modules/messages/data/entities.ts +11 -0
  531. package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
  532. package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
  533. package/src/modules/messages/widgets/injection-table.ts +29 -0
@@ -0,0 +1,114 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
3
+ import type { ChannelAdapterRegistry } from '../lib/registry'
4
+ import type { InboundMessage, NormalizedInboundMessage } from '../lib/adapter'
5
+ import {
6
+ COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
7
+ type IngestInboundMessageInput,
8
+ type IngestInboundMessageResult,
9
+ } from '../commands/ingest-inbound-message'
10
+ import { COMMUNICATION_CHANNELS_QUEUES } from '../lib/queue'
11
+
12
+ /**
13
+ * Job payload enqueued by the webhook route after successful signature verification.
14
+ *
15
+ * The route does the cheap signature-verification step synchronously and immediately
16
+ * returns 200. Normalization (`adapter.normalizeInbound`) + DB writes run here, in
17
+ * the worker, so a slow database or a long contact-resolution path can't time-out
18
+ * the provider's webhook.
19
+ */
20
+ export type InboundProcessorPayload = {
21
+ providerKey: string
22
+ channelId: string
23
+ channelType: string
24
+ raw: InboundMessage
25
+ scope: {
26
+ tenantId: string
27
+ organizationId: string | null
28
+ }
29
+ }
30
+
31
+ export const metadata: WorkerMeta = {
32
+ queue: COMMUNICATION_CHANNELS_QUEUES.inbound,
33
+ id: 'communication_channels:inbound-processor',
34
+ concurrency: 10,
35
+ }
36
+
37
+ type HandlerContext = JobContext & {
38
+ resolve: <T = unknown>(name: string) => T
39
+ }
40
+
41
+ /**
42
+ * Inbound channel message processor.
43
+ *
44
+ * Per SPEC-045d §6:
45
+ * - Resolves the channel adapter for the inbound providerKey.
46
+ * - Calls `adapter.normalizeInbound(raw)` to produce a `NormalizedInboundMessage`.
47
+ * - Hands off to the `communication_channels.message.ingest_inbound` command,
48
+ * which idempotently creates ExternalConversation, ChannelThreadMapping,
49
+ * ExternalMessage, MessageChannelLink, and composes the platform Message.
50
+ *
51
+ * Idempotency: handled by the command (dedup on `(channel_id, external_message_id)`).
52
+ * The worker can safely retry on transient failures.
53
+ */
54
+ export default async function handle(
55
+ job: QueuedJob<InboundProcessorPayload>,
56
+ ctx: HandlerContext,
57
+ ): Promise<void> {
58
+ const { providerKey, channelId, channelType, raw, scope } = job.payload
59
+
60
+ const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')
61
+ const adapter = adapterRegistry?.get(providerKey)
62
+ if (!adapter) {
63
+ throw new Error(
64
+ `No ChannelAdapter registered for providerKey '${providerKey}' (worker job ${job.id}). ` +
65
+ 'Check that the provider package is enabled in modules.ts.',
66
+ )
67
+ }
68
+
69
+ // Normalize the raw inbound payload into the hub's canonical shape.
70
+ const normalized: NormalizedInboundMessage = await adapter.normalizeInbound(raw)
71
+ if (!normalized?.externalMessageId || !normalized?.externalConversationId) {
72
+ throw new Error(
73
+ `Adapter '${providerKey}' returned a normalized message missing required fields ` +
74
+ `(externalMessageId, externalConversationId) for worker job ${job.id}`,
75
+ )
76
+ }
77
+
78
+ const commandBus = ctx.resolve<CommandBus>('commandBus')
79
+
80
+ // The worker's JobContext exposes `.resolve(name)` — duck-type-compatible with the
81
+ // shape CommandBus uses (`container.resolve(...)`). We cast to AwilixContainer because
82
+ // CommandRuntimeContext requires that exact type; in practice CommandBus only touches
83
+ // `.resolve`, mirroring the pattern in `inbox_ops/lib/messagesIntegration.ts:148`.
84
+ const containerProxy = { resolve: ctx.resolve.bind(ctx) }
85
+ const commandCtx = {
86
+ container: containerProxy as never,
87
+ auth: null,
88
+ organizationScope: null,
89
+ selectedOrganizationId: scope.organizationId ?? null,
90
+ organizationIds: scope.organizationId ? [scope.organizationId] : null,
91
+ }
92
+
93
+ const input: IngestInboundMessageInput = {
94
+ channelId,
95
+ providerKey,
96
+ channelType,
97
+ scope,
98
+ message: normalized,
99
+ }
100
+
101
+ const { result } = await commandBus.execute<IngestInboundMessageInput, IngestInboundMessageResult>(
102
+ COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
103
+ {
104
+ input,
105
+ ctx: commandCtx as never,
106
+ },
107
+ )
108
+
109
+ if (result.status === 'duplicate') {
110
+ // Idempotent skip — provider sent the same webhook twice (common with at-least-once
111
+ // delivery semantics). Nothing to do; we already ingested this message earlier.
112
+ return
113
+ }
114
+ }
@@ -0,0 +1,155 @@
1
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
2
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
3
+ import {
4
+ COMMUNICATION_CHANNELS_DELIVER_OUTBOUND_COMMAND_ID,
5
+ type DeliverOutboundMessageInput,
6
+ type DeliverOutboundMessageResult,
7
+ } from '../commands/deliver-outbound-message'
8
+ import { computeBackoffMs } from '../lib/error-classification'
9
+ import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'
10
+
11
+ /**
12
+ * Worker payload — the subscriber enqueues `{ messageId, scope, attempt? }`.
13
+ *
14
+ * `attempt` starts at 1 on first enqueue. The worker increments it when
15
+ * re-enqueueing for retry, until `MAX_ATTEMPTS` is reached.
16
+ */
17
+ export type OutboundDeliveryPayload = {
18
+ messageId: string
19
+ scope: {
20
+ tenantId: string
21
+ organizationId: string | null
22
+ }
23
+ /** Attempt number, 1-based. Set by the subscriber for the first try; the worker increments on retry. */
24
+ attempt?: number
25
+ /** Force credential refresh on this attempt — used after a 401 from the provider. */
26
+ forceCredentialRefresh?: boolean
27
+ }
28
+
29
+ export const OUTBOUND_DELIVERY_MAX_ATTEMPTS = 3
30
+
31
+ export const metadata: WorkerMeta = {
32
+ queue: COMMUNICATION_CHANNELS_QUEUES.outbound,
33
+ id: 'communication_channels:outbound-delivery',
34
+ concurrency: 10,
35
+ }
36
+
37
+ type HandlerContext = JobContext & {
38
+ resolve: <T = unknown>(name: string) => T
39
+ }
40
+
41
+ /**
42
+ * Outbound delivery worker.
43
+ *
44
+ * Dispatches the `deliver_outbound_message` command which performs the actual
45
+ * send. The command returns a classified result:
46
+ * - `delivered` / `already_delivered` / `no_channel_link` → success, return.
47
+ * - `failed` with `transient: true` → re-enqueue with exponential backoff,
48
+ * incremented attempt, up to `OUTBOUND_DELIVERY_MAX_ATTEMPTS`.
49
+ * - `failed` with `transient: false` → permanent failure, no retry.
50
+ *
51
+ * The command already wrote the failure record + emitted `.delivery_failed`,
52
+ * so the worker just decides whether to schedule another attempt.
53
+ *
54
+ * We DO NOT throw on a recorded delivery-failure outcome — that would let the
55
+ * queue apply its own retry policy on top of ours, double-retrying. Explicit
56
+ * re-enqueue with delayMs is the portable, controllable pattern. The one
57
+ * exception is an *unexpected* exception from the command itself (e.g. a DB blip
58
+ * that stopped it from recording anything): we re-enqueue up to our max and then
59
+ * rethrow so the infrastructure failure surfaces to the queue's dead-letter
60
+ * instead of vanishing. The command's idempotency prevents a double-send.
61
+ */
62
+ export default async function handle(
63
+ job: QueuedJob<OutboundDeliveryPayload>,
64
+ ctx: HandlerContext,
65
+ ): Promise<void> {
66
+ const { messageId, scope, attempt = 1, forceCredentialRefresh } = job.payload
67
+
68
+ const commandBus = ctx.resolve<CommandBus>('commandBus')
69
+ const containerProxy = { resolve: ctx.resolve.bind(ctx) }
70
+ const commandCtx = {
71
+ container: containerProxy as never,
72
+ auth: null,
73
+ organizationScope: null,
74
+ selectedOrganizationId: scope.organizationId ?? null,
75
+ organizationIds: scope.organizationId ? [scope.organizationId] : null,
76
+ }
77
+
78
+ let outcome: DeliverOutboundMessageResult
79
+ try {
80
+ const { result } = await commandBus.execute<
81
+ DeliverOutboundMessageInput,
82
+ DeliverOutboundMessageResult
83
+ >(
84
+ COMMUNICATION_CHANNELS_DELIVER_OUTBOUND_COMMAND_ID,
85
+ {
86
+ input: { messageId, scope, forceCredentialRefresh },
87
+ ctx: commandCtx as never,
88
+ },
89
+ )
90
+ outcome = result
91
+ } catch (err) {
92
+ // Unexpected error inside the command itself (e.g. DB blip). Re-enqueue
93
+ // up to MAX_ATTEMPTS so we don't lose deliveries due to infrastructure flakes.
94
+ const errorMessage = err instanceof Error ? err.message : String(err)
95
+ console.warn(
96
+ `[communication_channels:outbound-delivery] command threw on attempt ${attempt} for message ${messageId}: ${errorMessage}`,
97
+ )
98
+ if (attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS) {
99
+ await reenqueue(job.payload, attempt)
100
+ return
101
+ }
102
+ // Attempts exhausted on an unexpected command exception — rethrow so the
103
+ // failure reaches the queue's dead-letter / observability (see header note).
104
+ throw err
105
+ }
106
+
107
+ switch (outcome.status) {
108
+ case 'delivered':
109
+ case 'already_delivered':
110
+ case 'no_channel_link':
111
+ return
112
+ case 'failed': {
113
+ // Reauth (401 / invalid_grant): the command already flipped the channel to
114
+ // `requires_reauth`. Give the credentials exactly one forced-refresh retry
115
+ // before giving up — a near-expiry access token whose proactive refresh
116
+ // was skipped can still recover here. If we already forced a refresh and
117
+ // still got a reauth error, the token is unrecoverable: stop (the operator
118
+ // must reconnect).
119
+ if (
120
+ outcome.requiresReauth &&
121
+ !forceCredentialRefresh &&
122
+ attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS
123
+ ) {
124
+ console.warn(
125
+ `[communication_channels:outbound-delivery] reauth failure on attempt ${attempt} for message ${messageId} (${outcome.providerKey}): ${outcome.error}. Retrying once with a forced credential refresh.`,
126
+ )
127
+ await reenqueue({ ...job.payload, forceCredentialRefresh: true }, attempt)
128
+ return
129
+ }
130
+ if (outcome.transient && attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS) {
131
+ console.warn(
132
+ `[communication_channels:outbound-delivery] transient failure on attempt ${attempt} for message ${messageId} (${outcome.providerKey}): ${outcome.error}. Re-enqueueing.`,
133
+ )
134
+ await reenqueue(job.payload, attempt)
135
+ return
136
+ }
137
+ // Permanent or attempts exhausted — `.delivery_failed` was already emitted
138
+ // by the command, so we stop here.
139
+ console.error(
140
+ `[communication_channels:outbound-delivery] giving up on message ${messageId} after attempt ${attempt} (${outcome.providerKey}): ${outcome.error}`,
141
+ )
142
+ return
143
+ }
144
+ }
145
+ }
146
+
147
+ async function reenqueue(payload: OutboundDeliveryPayload, attempt: number): Promise<void> {
148
+ const next: OutboundDeliveryPayload = {
149
+ ...payload,
150
+ attempt: attempt + 1,
151
+ }
152
+ const delayMs = computeBackoffMs(attempt)
153
+ const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)
154
+ await queue.enqueue(next as unknown as Record<string, unknown>, { delayMs })
155
+ }
@@ -0,0 +1,391 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
3
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { CommunicationChannel } from '../data/entities'
6
+ import {
7
+ COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
8
+ type IngestInboundMessageInput,
9
+ } from '../commands/ingest-inbound-message'
10
+ import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'
11
+ import { preservePushState } from '../lib/push-state'
12
+ import { writeIngestDeadLetter } from '../lib/dead-letter'
13
+ import { classifyOutboundError, computeBackoffMs, isReauthError } from '../lib/error-classification'
14
+ import { refreshCredentialsIfNeeded } from '../lib/credential-refresh'
15
+ import { emitCommunicationChannelsEvent } from '../events'
16
+ import type { ChannelAdapterRegistry } from '../lib/registry'
17
+ import type { NormalizedInboundMessage } from '../lib/adapter'
18
+
19
+ /**
20
+ * Job payload for the `communication-channels-poll` queue.
21
+ *
22
+ * One job per channel per scheduler tick. The poll-tick worker (sibling file)
23
+ * enumerates due channels and enqueues these jobs.
24
+ */
25
+ export type PollChannelJobPayload = {
26
+ channelId: string
27
+ scope: {
28
+ tenantId: string
29
+ organizationId: string | null
30
+ }
31
+ /** Attempt count, 1-based; used for retry-backoff decisions. */
32
+ attempt?: number
33
+ /** Self-re-enqueue drain counter (bounds the multi-page `hasMore` drain loop). */
34
+ drainPage?: number
35
+ }
36
+
37
+ export const POLL_CHANNEL_MAX_ATTEMPTS = 3
38
+
39
+ /**
40
+ * Hard cap on `hasMore` self-re-enqueue drain pages — guards against an adapter
41
+ * that returns `hasMore: true` with a non-advancing (pinned) cursor, which would
42
+ * otherwise spin a tight, unthrottled re-enqueue loop. Mirrors the same guard in
43
+ * `gmail-history-sync`.
44
+ */
45
+ const MAX_DRAIN_PAGES = 100
46
+
47
+ export const metadata: WorkerMeta = {
48
+ queue: COMMUNICATION_CHANNELS_QUEUES.poll,
49
+ id: 'communication_channels:poll-channel',
50
+ concurrency: 10,
51
+ }
52
+
53
+ type HandlerContext = JobContext & {
54
+ resolve: <T = unknown>(name: string) => T
55
+ }
56
+
57
+ type CredentialsServiceLike = {
58
+ resolve: (
59
+ integrationId: string,
60
+ scope: { organizationId: string; tenantId: string; userId?: string | null },
61
+ ) => Promise<Record<string, unknown> | null>
62
+ save?: (
63
+ integrationId: string,
64
+ credentials: Record<string, unknown>,
65
+ scope: { organizationId: string; tenantId: string; userId?: string | null },
66
+ ) => Promise<void>
67
+ }
68
+
69
+ /**
70
+ * Poll a single channel for inbound messages.
71
+ *
72
+ * Per SPEC-045d § 6 (with email-spec § Hub Deltas → Delta 6 polling extensions):
73
+ * 1. Load the channel; skip when `is_active === false` or `status !== 'connected'`.
74
+ * 2. Skip when the adapter declares `realtimePush !== false` (it doesn't want polling).
75
+ * 3. Refresh credentials if OAuth and within the expiry window.
76
+ * 4. Call `adapter.fetchHistory({ channelId, credentials, since: lastPolledAt })`.
77
+ * 5. For each normalized message, dispatch `ingest_inbound_message` (idempotent on
78
+ * `(channel_id, external_message_id)`).
79
+ * 6. Update `channel.lastPolledAt = NOW()` on success.
80
+ * 7. On error: classify, set `channel.status` accordingly, set `last_error`, emit
81
+ * `channel.requires_reauth` on 401, retry transient failures up to MAX.
82
+ *
83
+ * The hub doesn't drain a remote mailbox indefinitely — `fetchHistory` returns a
84
+ * single page (provider decides the size). If the provider has more, the next tick
85
+ * picks it up after the configured `poll_interval_seconds`.
86
+ */
87
+ export default async function handle(
88
+ job: QueuedJob<PollChannelJobPayload>,
89
+ ctx: HandlerContext,
90
+ ): Promise<void> {
91
+ const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload
92
+ const em = (ctx.resolve('em') as EntityManager).fork()
93
+ const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')
94
+
95
+ const channel = await findOneWithDecryption(
96
+ em,
97
+ CommunicationChannel,
98
+ {
99
+ id: channelId,
100
+ tenantId: scope.tenantId,
101
+ organizationId: scope.organizationId ?? null,
102
+ deletedAt: null,
103
+ },
104
+ undefined,
105
+ scope,
106
+ )
107
+ if (!channel) {
108
+ console.warn(
109
+ `[communication_channels:poll-channel] channel ${channelId} not found (skipping)`,
110
+ )
111
+ return
112
+ }
113
+ if (!channel.isActive) return
114
+ // Allow `connected` (normal poll) and `error` (Spec B § B5 auto-recovery
115
+ // sweep enqueues these intentionally — on a successful poll below we
116
+ // flip them back to `connected`). `requires_reauth` and `disconnected`
117
+ // are owned by the credential-refresh and disconnect flows.
118
+ if (channel.status !== 'connected' && channel.status !== 'error') return
119
+
120
+ const adapter = adapterRegistry?.get(channel.providerKey)
121
+ if (!adapter) {
122
+ console.warn(
123
+ `[communication_channels:poll-channel] no adapter for provider '${channel.providerKey}' (channel ${channelId})`,
124
+ )
125
+ return
126
+ }
127
+ // Adapter opted out of polling — webhook providers.
128
+ const capabilities = (channel.capabilities as { realtimePush?: boolean } | null) ?? null
129
+ if (capabilities?.realtimePush !== false) {
130
+ // realtimePush is `true` (default for back-compat) — don't poll push providers.
131
+ return
132
+ }
133
+ if (typeof adapter.fetchHistory !== 'function') {
134
+ // Adapter doesn't implement history fetching — nothing we can do.
135
+ return
136
+ }
137
+
138
+ // Credentials.
139
+ let credentialsService: CredentialsServiceLike | null = null
140
+ try {
141
+ credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')
142
+ } catch {
143
+ credentialsService = null
144
+ }
145
+ // Per-user credentials scope: pass `channel.userId` so the credentials
146
+ // service returns this user's row, not whoever connected the provider last.
147
+ // See review R2-C1 / N1 (2026-05-26).
148
+ const credentialsScope = {
149
+ tenantId: scope.tenantId,
150
+ organizationId: scope.organizationId ?? scope.tenantId,
151
+ userId: channel.userId ?? null,
152
+ }
153
+ let credentials: Record<string, unknown> = {}
154
+ if (channel.credentialsRef && credentialsService) {
155
+ try {
156
+ credentials =
157
+ (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}
158
+ } catch {
159
+ credentials = {}
160
+ }
161
+ }
162
+ const refreshed = await refreshCredentialsIfNeeded(
163
+ {
164
+ adapter,
165
+ channelId: channel.id,
166
+ credentials,
167
+ scope: credentialsScope,
168
+ },
169
+ { credentialsService },
170
+ )
171
+ credentials = refreshed.credentials
172
+
173
+ // Fetch a single page of history.
174
+ // `channelState` is the provider-specific resumption cursor — Gmail historyId,
175
+ // IMAP UIDVALIDITY+UIDNEXT, etc. We persist it across
176
+ // ticks on `channel.channelState` so each poll resumes from the prior one
177
+ // instead of running a full mailbox resync. Empty / NULL = "first poll;
178
+ // bootstrap the cursor from the provider".
179
+ let normalized: NormalizedInboundMessage[] = []
180
+ let nextCursor: string | undefined
181
+ let hasMore = false
182
+ try {
183
+ const result = await adapter.fetchHistory({
184
+ conversationId: channel.externalIdentifier ?? channel.id,
185
+ credentials,
186
+ cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : undefined,
187
+ channelState: (channel.channelState as Record<string, unknown> | null) ?? undefined,
188
+ scope: {
189
+ tenantId: scope.tenantId,
190
+ organizationId: scope.organizationId ?? scope.tenantId,
191
+ },
192
+ // `contactFilter.sinceDays` is a hint to provider adapters about how far
193
+ // back to look on first-poll bootstrap. For UID-incremental polls (every
194
+ // tick after the first), adapters ignore this and just fetch new mail
195
+ // since the persisted cursor.
196
+ contactFilter: { addresses: [], sinceDays: 7 },
197
+ })
198
+ normalized = Array.isArray(result?.messages) ? result.messages : []
199
+ nextCursor = result?.nextCursor
200
+ hasMore = result?.hasMore === true
201
+ } catch (err) {
202
+ await handlePollError(err, em, channel, scope, attempt, job.payload)
203
+ return
204
+ }
205
+
206
+ // Dispatch ingest commands for each message.
207
+ const commandBus = ctx.resolve<CommandBus>('commandBus')
208
+ const containerProxy = { resolve: ctx.resolve.bind(ctx) }
209
+ const commandCtx = {
210
+ container: containerProxy as never,
211
+ auth: null,
212
+ organizationScope: null,
213
+ selectedOrganizationId: scope.organizationId ?? null,
214
+ organizationIds: scope.organizationId ? [scope.organizationId] : null,
215
+ }
216
+ // Spec B § Per-message commit + dead-letter:
217
+ // - Permanent failure (malformed MIME, schema, contract violation):
218
+ // write to channel_ingest_dead_letter, log, advance cursor anyway so
219
+ // the bad blob never stalls the channel again.
220
+ // - Transient failure (DB drop, network blip): abort the loop without
221
+ // advancing the cursor. The next tick re-fetches the same page;
222
+ // idempotency via the (channel_id, external_message_id) unique
223
+ // constraint means already-ingested messages no-op on the retry.
224
+ let transientIngestAbort = false
225
+ for (const message of normalized) {
226
+ try {
227
+ const input: IngestInboundMessageInput = {
228
+ channelId: channel.id,
229
+ providerKey: channel.providerKey,
230
+ channelType: channel.channelType,
231
+ scope: {
232
+ tenantId: scope.tenantId,
233
+ organizationId: scope.organizationId ?? null,
234
+ },
235
+ message,
236
+ }
237
+ await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {
238
+ input,
239
+ ctx: commandCtx as never,
240
+ })
241
+ } catch (err) {
242
+ const classification = classifyOutboundError(err)
243
+ if (classification.transient) {
244
+ console.warn(
245
+ `[communication_channels:poll-channel] transient ingest failure for channel ${channel.id}; aborting page so cursor is NOT advanced. Reason: ${classification.message}`,
246
+ )
247
+ transientIngestAbort = true
248
+ break
249
+ }
250
+ // Permanent — write to dead-letter so an operator can replay later.
251
+ // The shared helper is best-effort (never throws) and idempotent on
252
+ // `(channelId, externalMessageId)`, so a replayed page that fails the
253
+ // same message again does not insert a duplicate row.
254
+ await writeIngestDeadLetter({
255
+ em,
256
+ scope,
257
+ channel,
258
+ message,
259
+ err,
260
+ errorMessage: classification.message,
261
+ })
262
+ console.warn(
263
+ `[communication_channels:poll-channel] permanent ingest failure for channel ${channel.id}; recorded in dead-letter and advancing cursor past message ${message.externalMessageId}. Reason: ${classification.message}`,
264
+ )
265
+ }
266
+ }
267
+
268
+ // Transient abort: keep prior cursor + lastPolledAt so the next tick
269
+ // re-fetches the same page (idempotent at the DB layer).
270
+ if (transientIngestAbort) {
271
+ channel.lastError = 'transient_ingest_failure'
272
+ await em.flush()
273
+ return
274
+ }
275
+
276
+ // Update poll cursor + clear any stale error state.
277
+ channel.lastPolledAt = new Date()
278
+ if (channel.lastError) channel.lastError = null
279
+ // Recover the channel from any prior non-fatal error state. The previous
280
+ // poll(s) may have set status='error' after exhausting transient-retry
281
+ // attempts, but a fresh successful poll means the upstream is healthy
282
+ // again — flip it back to 'connected' so the scheduler keeps it in
283
+ // rotation and the user doesn't have to manually reconnect.
284
+ // We DON'T touch 'requires_reauth' here (that lifecycle state is owned
285
+ // by the credential-refresh / OAuth flow) or 'disconnected' (owned by
286
+ // the cascade-on-user-delete subscriber).
287
+ if (channel.status === 'error') {
288
+ channel.status = 'connected'
289
+ }
290
+ // Providers encode their cursor as base64-encoded JSON in `nextCursor`; we
291
+ // decode it back to an object so the next tick can pass it straight into
292
+ // `fetchHistory` as `channelState`. Decode failures fall back to the prior
293
+ // state (next tick bootstraps from there).
294
+ //
295
+ // Push-delivery state (Spec C) — watch/subscription identifiers and expiry — is
296
+ // owned by the push register/renew commands, not the sync cursor. A provider's
297
+ // `fetchHistory` returns only sync-cursor fields, so persisting the decoded
298
+ // cursor as a full replace would silently wipe push state and stop
299
+ // `gmail-renew-watch` from renewing it. Carry
300
+ // the hub-owned push keys forward whenever the new cursor omits them.
301
+ if (typeof nextCursor === 'string' && nextCursor.length > 0) {
302
+ const decoded = decodeChannelStateCursor(nextCursor)
303
+ if (decoded) {
304
+ channel.channelState = preservePushState(channel.channelState, decoded)
305
+ }
306
+ }
307
+ await em.flush()
308
+
309
+ // Drain contract (review H1, 2026-05-26): adapters that have additional
310
+ // pages beyond this tick's batch (large mailboxes, mid-deltaLink walks,
311
+ // UID overflows) signal `hasMore: true`. The persisted `channelState`
312
+ // already encodes the mid-drain resumption token (Gmail `pendingHistoryPageToken`,
313
+ // IMAP non-terminal `uidNext`). Re-enqueue with
314
+ // a small delay so we keep draining without overrunning rate limits.
315
+ if (hasMore) {
316
+ // Bound the drain: an adapter that returns `hasMore: true` with a
317
+ // non-advancing cursor (e.g. a persistently-failing message that pins the
318
+ // Gmail cursor via `hardFailed`) must not spin an unthrottled loop. Stop
319
+ // at MAX_DRAIN_PAGES; the next scheduled poll tick re-checks the channel.
320
+ if (drainPage < MAX_DRAIN_PAGES) {
321
+ const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)
322
+ await queue.enqueue(
323
+ { channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 } as unknown as Record<string, unknown>,
324
+ { delayMs: 250 },
325
+ )
326
+ } else {
327
+ console.warn(
328
+ `[communication_channels:poll-channel] drain page cap (${MAX_DRAIN_PAGES}) reached for channel ${channel.id}; stopping re-enqueue until the next scheduled tick`,
329
+ )
330
+ }
331
+ }
332
+ }
333
+
334
+ function decodeChannelStateCursor(cursor: string): Record<string, unknown> | null {
335
+ try {
336
+ const decoded = Buffer.from(cursor, 'base64').toString('utf8')
337
+ const parsed = JSON.parse(decoded) as unknown
338
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
339
+ return parsed as Record<string, unknown>
340
+ }
341
+ return null
342
+ } catch {
343
+ return null
344
+ }
345
+ }
346
+
347
+ async function handlePollError(
348
+ err: unknown,
349
+ em: EntityManager,
350
+ channel: CommunicationChannel,
351
+ scope: PollChannelJobPayload['scope'],
352
+ attempt: number,
353
+ payload: PollChannelJobPayload,
354
+ ): Promise<void> {
355
+ const classification = classifyOutboundError(err)
356
+ channel.lastError = classification.message
357
+
358
+ if (isReauthError(classification)) {
359
+ channel.status = 'requires_reauth'
360
+ await em.flush()
361
+ await emitCommunicationChannelsEvent(
362
+ 'communication_channels.channel.requires_reauth',
363
+ {
364
+ channelId: channel.id,
365
+ providerKey: channel.providerKey,
366
+ channelType: channel.channelType,
367
+ reason: classification.message,
368
+ tenantId: scope.tenantId,
369
+ organizationId: scope.organizationId ?? null,
370
+ },
371
+ { persistent: true },
372
+ )
373
+ return
374
+ }
375
+
376
+ if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {
377
+ // Transient error — re-enqueue with backoff. Channel status stays
378
+ // `connected` so the scheduler keeps it in rotation.
379
+ await em.flush()
380
+ const next: PollChannelJobPayload = { ...payload, attempt: attempt + 1 }
381
+ const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)
382
+ await queue.enqueue(next as unknown as Record<string, unknown>, {
383
+ delayMs: computeBackoffMs(attempt),
384
+ })
385
+ return
386
+ }
387
+
388
+ // Permanent or attempts exhausted — mark channel as error and stop the loop.
389
+ channel.status = 'error'
390
+ await em.flush()
391
+ }