@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4393.1.de282b5dfd

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 +259 -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 +425 -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,263 @@
1
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
2
+ import { ChannelThreadMapping, ChannelThreadToken } from "../data/entities.js";
3
+ import { extractTokenFromBody, extractTokenFromHeaders } from "./thread-token.js";
4
+ const SUBJECT_PREFIX_PATTERN = /^\s*((?:re|fwd|fw|aw|wg|sv|tr|antw)\s*[:\-]\s*|\[[^\]]+\]\s*)+/i;
5
+ const PARTICIPANT_LOOKBACK_DAYS = 30;
6
+ const MAX_REFERENCES_TO_SCAN = 40;
7
+ function normalizeSubject(subject) {
8
+ if (typeof subject !== "string") return "";
9
+ let current = subject.trim();
10
+ let safety = 16;
11
+ while (safety > 0 && SUBJECT_PREFIX_PATTERN.test(current)) {
12
+ current = current.replace(SUBJECT_PREFIX_PATTERN, "").trim();
13
+ safety -= 1;
14
+ }
15
+ return current.toLowerCase();
16
+ }
17
+ async function matchThread(input, deps) {
18
+ const { em, now } = deps;
19
+ const tenantId = input.tenantId;
20
+ const dscope = { tenantId, organizationId: input.organizationId };
21
+ const headerToken = extractTokenFromHeaders(input.inReplyTo, input.references);
22
+ if (headerToken) {
23
+ const threadId = await resolveTokenThread(em, dscope, {
24
+ tenantId,
25
+ channelId: input.channelId,
26
+ token: headerToken,
27
+ now
28
+ });
29
+ if (threadId) {
30
+ return {
31
+ messageThreadId: threadId,
32
+ matchedBy: "token-references",
33
+ confidence: "high"
34
+ };
35
+ }
36
+ }
37
+ const bodyToken = extractTokenFromBody(input.bodyHtml, input.bodyPlain);
38
+ if (bodyToken) {
39
+ const threadId = await resolveTokenThread(em, dscope, {
40
+ tenantId,
41
+ channelId: input.channelId,
42
+ token: bodyToken,
43
+ now
44
+ });
45
+ if (threadId) {
46
+ return {
47
+ messageThreadId: threadId,
48
+ matchedBy: "token-body",
49
+ confidence: "high"
50
+ };
51
+ }
52
+ }
53
+ const candidates = collectReferenceCandidates(input.inReplyTo, input.references);
54
+ if (candidates.length > 0) {
55
+ const jwzMatch = await findThreadByMessageIds(em, {
56
+ tenantId,
57
+ organizationId: input.organizationId,
58
+ channelId: input.channelId,
59
+ messageIds: candidates
60
+ });
61
+ if (jwzMatch) {
62
+ return {
63
+ messageThreadId: jwzMatch,
64
+ matchedBy: "jwz-headers",
65
+ confidence: "medium"
66
+ };
67
+ }
68
+ }
69
+ const normalizedSubject = normalizeSubject(input.subject);
70
+ if (normalizedSubject.length > 0) {
71
+ const cutoff = subtractDays((now ?? (() => /* @__PURE__ */ new Date()))(), PARTICIPANT_LOOKBACK_DAYS);
72
+ const participants = collectParticipants(input);
73
+ if (participants.length > 0) {
74
+ const subjectMatch = await findThreadBySubjectParticipants(em, {
75
+ tenantId,
76
+ organizationId: input.organizationId,
77
+ channelId: input.channelId,
78
+ normalizedSubject,
79
+ participants,
80
+ cutoff
81
+ });
82
+ if (subjectMatch) {
83
+ return {
84
+ messageThreadId: subjectMatch,
85
+ matchedBy: "subject-participants",
86
+ confidence: "low"
87
+ };
88
+ }
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ async function resolveTokenThread(em, dscope, args) {
94
+ const row = await findOneWithDecryption(
95
+ em,
96
+ ChannelThreadToken,
97
+ {
98
+ tenantId: args.tenantId,
99
+ organizationId: dscope.organizationId,
100
+ token: args.token
101
+ },
102
+ void 0,
103
+ dscope
104
+ );
105
+ if (!row) return null;
106
+ const mapping = await findOneWithDecryption(
107
+ em,
108
+ ChannelThreadMapping,
109
+ {
110
+ tenantId: args.tenantId,
111
+ organizationId: dscope.organizationId,
112
+ messageThreadId: row.messageThreadId,
113
+ channelId: args.channelId
114
+ },
115
+ void 0,
116
+ dscope
117
+ );
118
+ if (!mapping) return null;
119
+ await em.getConnection().execute(
120
+ `UPDATE channel_thread_tokens
121
+ SET last_seen_at = ?
122
+ WHERE id = ?
123
+ AND tenant_id = ?
124
+ AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)`,
125
+ [
126
+ (args.now ?? (() => /* @__PURE__ */ new Date()))(),
127
+ row.id,
128
+ args.tenantId,
129
+ dscope.organizationId,
130
+ dscope.organizationId
131
+ ]
132
+ );
133
+ return row.messageThreadId;
134
+ }
135
+ function collectReferenceCandidates(inReplyTo, references) {
136
+ const out = [];
137
+ const push = (value) => {
138
+ if (typeof value !== "string") return;
139
+ const stripped = value.replace(/^<|>$/g, "").trim();
140
+ if (stripped.length > 0) out.push(stripped);
141
+ };
142
+ push(inReplyTo);
143
+ if (Array.isArray(references)) {
144
+ for (const ref of references) push(ref);
145
+ }
146
+ return Array.from(new Set(out)).slice(0, MAX_REFERENCES_TO_SCAN);
147
+ }
148
+ async function findThreadByMessageIds(em, args) {
149
+ if (args.messageIds.length === 0) return null;
150
+ const idArray = `{${args.messageIds.map((id) => `"${id.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")}}`;
151
+ const rows = await em.getConnection().execute(
152
+ `SELECT link.message_id FROM message_channel_links AS link
153
+ INNER JOIN external_conversations AS conv
154
+ ON conv.id = link.external_conversation_id
155
+ WHERE link.tenant_id = ?
156
+ AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)
157
+ AND conv.tenant_id = ?
158
+ AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)
159
+ AND conv.channel_id = ?
160
+ AND link.channel_metadata->>'messageId' = ANY(?::text[])
161
+ LIMIT 1`,
162
+ [
163
+ args.tenantId,
164
+ args.organizationId,
165
+ args.organizationId,
166
+ args.tenantId,
167
+ args.organizationId,
168
+ args.organizationId,
169
+ args.channelId,
170
+ idArray
171
+ ]
172
+ );
173
+ if (!rows || rows.length === 0) return null;
174
+ const messageId = rows[0].message_id;
175
+ const threadRows = await em.getConnection().execute(
176
+ `SELECT thread_id FROM messages
177
+ WHERE id = ?
178
+ AND tenant_id = ?
179
+ AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)
180
+ AND deleted_at IS NULL
181
+ LIMIT 1`,
182
+ [messageId, args.tenantId, args.organizationId, args.organizationId]
183
+ );
184
+ if (!threadRows || threadRows.length === 0) return null;
185
+ return threadRows[0].thread_id;
186
+ }
187
+ function collectParticipants(input) {
188
+ const set = /* @__PURE__ */ new Set();
189
+ const push = (value) => {
190
+ if (typeof value !== "string") return;
191
+ const cleaned = value.trim().toLowerCase();
192
+ if (cleaned.length > 0) set.add(cleaned);
193
+ };
194
+ push(input.fromAddress);
195
+ for (const addr of input.toAddresses) push(addr);
196
+ for (const addr of input.ccAddresses) push(addr);
197
+ return Array.from(set);
198
+ }
199
+ async function findThreadBySubjectParticipants(em, args) {
200
+ const subjectLowerLike = args.normalizedSubject;
201
+ const participantList = `{${args.participants.map((p) => `"${p.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")}}`;
202
+ const rows = await em.getConnection().execute(
203
+ `SELECT messages.thread_id
204
+ FROM message_channel_links AS link
205
+ INNER JOIN external_conversations AS conv
206
+ ON conv.id = link.external_conversation_id
207
+ INNER JOIN messages
208
+ ON messages.id = link.message_id
209
+ AND messages.tenant_id = link.tenant_id
210
+ AND ((link.organization_id IS NULL AND messages.organization_id IS NULL) OR messages.organization_id = link.organization_id)
211
+ AND messages.deleted_at IS NULL
212
+ WHERE link.tenant_id = ?
213
+ AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)
214
+ AND conv.tenant_id = ?
215
+ AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)
216
+ AND conv.channel_id = ?
217
+ AND link.created_at >= ?
218
+ AND lower(regexp_replace(coalesce(link.channel_metadata->>'subject', ''),
219
+ '^\\s*((re|fwd|fw|aw|wg|sv|tr|antw)\\s*[:\\-]\\s*|\\[[^\\]]+\\]\\s*)+',
220
+ '',
221
+ 'i'
222
+ )) = ?
223
+ AND (
224
+ lower(coalesce(link.channel_metadata->>'from', '')) = ANY(?::text[])
225
+ OR EXISTS (
226
+ SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'to', '[]'::jsonb)) AS t(addr)
227
+ WHERE lower(t.addr) = ANY(?::text[])
228
+ )
229
+ OR EXISTS (
230
+ SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'cc', '[]'::jsonb)) AS t(addr)
231
+ WHERE lower(t.addr) = ANY(?::text[])
232
+ )
233
+ )
234
+ ORDER BY link.created_at DESC
235
+ LIMIT 1`,
236
+ [
237
+ args.tenantId,
238
+ args.organizationId,
239
+ args.organizationId,
240
+ args.tenantId,
241
+ args.organizationId,
242
+ args.organizationId,
243
+ args.channelId,
244
+ args.cutoff,
245
+ subjectLowerLike,
246
+ participantList,
247
+ participantList,
248
+ participantList
249
+ ]
250
+ );
251
+ if (!rows || rows.length === 0) return null;
252
+ return rows[0].thread_id;
253
+ }
254
+ function subtractDays(from, days) {
255
+ const next = new Date(from);
256
+ next.setUTCDate(next.getUTCDate() - days);
257
+ return next;
258
+ }
259
+ export {
260
+ matchThread,
261
+ normalizeSubject
262
+ };
263
+ //# sourceMappingURL=thread-matcher.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/communication_channels/lib/thread-matcher.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadMapping, ChannelThreadToken, MessageChannelLink } from '../data/entities'\nimport { extractTokenFromBody, extractTokenFromHeaders } from './thread-token'\n\n/**\n * Layered thread-matching for inbound messages. Five ordered strategies;\n * first hit wins.\n *\n * 1. Token in References / In-Reply-To headers (high confidence)\n * 2. Token in body (high confidence)\n * 3. JWZ on Message-Id (medium confidence)\n * 4. Subject + participants (last 30 days) (low confidence)\n * 5. None \u2192 caller creates a new thread\n *\n * All DB queries are tenant-scoped. The matcher returns the resolved\n * thread id (or null) and lets the caller perform the actual ingest. It\n * never flushes the caller's unit of work: the only write is a scoped raw\n * `UPDATE` that bumps a matched token's `last_seen_at` (a future-GC hint),\n * which does not touch the caller's pending entities.\n *\n * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`\n * \u00A7 4 Threading Algorithm.\n */\n\nexport type ThreadMatchInput = {\n channelId: string\n tenantId: string\n organizationId: string | null\n\n /** RFC 5322 Message-ID of this inbound message (no angle brackets). */\n messageId: string | null\n /** In-Reply-To header value (no angle brackets). */\n inReplyTo: string | null\n /** References header, ordered root \u2192 most recent. */\n references: string[]\n\n /** Already-normalized subject is preferred; we re-normalize defensively. */\n subject: string\n fromAddress: string\n toAddresses: string[]\n ccAddresses: string[]\n\n bodyPlain: string | null\n bodyHtml: string | null\n\n receivedAt: Date\n}\n\nexport type ThreadMatchStrategy =\n | 'token-references'\n | 'token-body'\n | 'jwz-headers'\n | 'subject-participants'\n\nexport type ThreadMatchConfidence = 'high' | 'medium' | 'low'\n\nexport type ThreadMatch = {\n messageThreadId: string\n matchedBy: ThreadMatchStrategy\n confidence: ThreadMatchConfidence\n}\n\nexport type ThreadMatcherDeps = {\n em: EntityManager\n /** Stable reference for testing \u2014 defaults to `new Date()`. */\n now?: () => Date\n}\n\nconst SUBJECT_PREFIX_PATTERN = /^\\s*((?:re|fwd|fw|aw|wg|sv|tr|antw)\\s*[:\\-]\\s*|\\[[^\\]]+\\]\\s*)+/i\nconst PARTICIPANT_LOOKBACK_DAYS = 30\nconst MAX_REFERENCES_TO_SCAN = 40\n\n/**\n * Normalize a subject for low-confidence subject+participants matching:\n * - Trim whitespace\n * - Strip leading reply/forward prefixes (`Re:`, `RE:`, `Aw:`, `Fwd:`, `Tr:`, `WG:`, `Sv:`, etc.)\n * - Strip leading bracketed tags (`[EXTERNAL]`, `[Encrypted]`, `[SPAM?]`, \u2026)\n * - Repeat until the pattern no longer matches\n * - Lowercase the result\n *\n * Returns an empty string for `null`/empty input \u2014 the caller should\n * skip the subject+participants strategy in that case.\n */\nexport function normalizeSubject(subject: string | null | undefined): string {\n if (typeof subject !== 'string') return ''\n let current = subject.trim()\n // Loop until no prefix matches \u2014 handles `Re: Fwd: [EXTERNAL] Re: \u2026`.\n let safety = 16\n while (safety > 0 && SUBJECT_PREFIX_PATTERN.test(current)) {\n current = current.replace(SUBJECT_PREFIX_PATTERN, '').trim()\n safety -= 1\n }\n return current.toLowerCase()\n}\n\nexport async function matchThread(\n input: ThreadMatchInput,\n deps: ThreadMatcherDeps,\n): Promise<ThreadMatch | null> {\n const { em, now } = deps\n const tenantId = input.tenantId\n const dscope = { tenantId, organizationId: input.organizationId }\n\n // Strategy 1: token in References / In-Reply-To header.\n const headerToken = extractTokenFromHeaders(input.inReplyTo, input.references)\n if (headerToken) {\n const threadId = await resolveTokenThread(em, dscope, {\n tenantId,\n channelId: input.channelId,\n token: headerToken,\n now,\n })\n if (threadId) {\n return {\n messageThreadId: threadId,\n matchedBy: 'token-references',\n confidence: 'high',\n }\n }\n }\n\n // Strategy 2: token in body.\n const bodyToken = extractTokenFromBody(input.bodyHtml, input.bodyPlain)\n if (bodyToken) {\n const threadId = await resolveTokenThread(em, dscope, {\n tenantId,\n channelId: input.channelId,\n token: bodyToken,\n now,\n })\n if (threadId) {\n return {\n messageThreadId: threadId,\n matchedBy: 'token-body',\n confidence: 'high',\n }\n }\n }\n\n // Strategy 3: JWZ \u2014 find any MessageChannelLink in this channel whose\n // recorded `channelMetadata.messageId` matches In-Reply-To or any of the\n // References values. The first hit's `messages.message.threadId` is our\n // thread. Bounded by MAX_REFERENCES_TO_SCAN.\n const candidates = collectReferenceCandidates(input.inReplyTo, input.references)\n if (candidates.length > 0) {\n const jwzMatch = await findThreadByMessageIds(em, {\n tenantId,\n organizationId: input.organizationId,\n channelId: input.channelId,\n messageIds: candidates,\n })\n if (jwzMatch) {\n return {\n messageThreadId: jwzMatch,\n matchedBy: 'jwz-headers',\n confidence: 'medium',\n }\n }\n }\n\n // Strategy 4: subject + participants in the same channel within 30 days.\n // Low confidence \u2014 never used to overwrite a stronger token-based match\n // (we already returned above on hits). The caller may still choose to\n // create a new thread when confidence === 'low'.\n const normalizedSubject = normalizeSubject(input.subject)\n if (normalizedSubject.length > 0) {\n const cutoff = subtractDays((now ?? (() => new Date()))(), PARTICIPANT_LOOKBACK_DAYS)\n const participants = collectParticipants(input)\n if (participants.length > 0) {\n const subjectMatch = await findThreadBySubjectParticipants(em, {\n tenantId,\n organizationId: input.organizationId,\n channelId: input.channelId,\n normalizedSubject,\n participants,\n cutoff,\n })\n if (subjectMatch) {\n return {\n messageThreadId: subjectMatch,\n matchedBy: 'subject-participants',\n confidence: 'low',\n }\n }\n }\n }\n\n // No match \u2014 caller creates a new thread.\n return null\n}\n\n/**\n * Resolve a thread token to its `messageThreadId`, but ONLY when that thread\n * belongs to the channel that received the inbound message.\n *\n * The token is unguessable + HMAC-signed + tenant-scoped, so it cannot leak\n * across tenants. Within a tenant, though, the same external contact may\n * correspond with several users/channels \u2014 and a thread token that ends up in\n * a *different* mailbox (e.g. a forwarded thread) must not graft the inbound\n * message onto another channel's thread. The `ChannelThreadMapping`\n * (thread \u2194 channel) is the authoritative link; if there's no mapping joining\n * this thread to the receiving channel, we treat the token as a non-match and\n * let the lower-confidence strategies (or a fresh thread) take over.\n *\n * Returns the thread id on a verified hit (and bumps `last_seen_at`), else null.\n */\nasync function resolveTokenThread(\n em: EntityManager,\n dscope: { tenantId: string; organizationId: string | null },\n args: { tenantId: string; channelId: string; token: string; now?: () => Date },\n): Promise<string | null> {\n const row = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: dscope.organizationId,\n token: args.token,\n },\n undefined,\n dscope,\n )\n if (!row) return null\n\n const mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n tenantId: args.tenantId,\n organizationId: dscope.organizationId,\n messageThreadId: row.messageThreadId,\n channelId: args.channelId,\n },\n undefined,\n dscope,\n )\n if (!mapping) return null\n\n // Bump `last_seen_at` (future-GC hint) via a scoped raw UPDATE rather than\n // `em.flush()`. A flush here would commit the ENTIRE unit of work, including\n // any pending mutations the caller (`ingest-inbound-message`, shared by all\n // provider adapters) holds \u2014 turning this \"pure\" matcher into a hidden commit\n // boundary. The raw UPDATE keeps the matcher side-effect-free w.r.t. the\n // caller's EntityManager.\n await em.getConnection().execute(\n `UPDATE channel_thread_tokens\n SET last_seen_at = ?\n WHERE id = ?\n AND tenant_id = ?\n AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)`,\n [\n (args.now ?? (() => new Date()))(),\n row.id,\n args.tenantId,\n dscope.organizationId,\n dscope.organizationId,\n ],\n )\n return row.messageThreadId\n}\n\nfunction collectReferenceCandidates(\n inReplyTo: string | null,\n references: string[] | undefined,\n): string[] {\n const out: string[] = []\n const push = (value: string | null | undefined): void => {\n if (typeof value !== 'string') return\n const stripped = value.replace(/^<|>$/g, '').trim()\n if (stripped.length > 0) out.push(stripped)\n }\n push(inReplyTo)\n if (Array.isArray(references)) {\n for (const ref of references) push(ref)\n }\n return Array.from(new Set(out)).slice(0, MAX_REFERENCES_TO_SCAN)\n}\n\nasync function findThreadByMessageIds(\n em: EntityManager,\n args: { tenantId: string; organizationId: string | null; channelId: string; messageIds: string[] },\n): Promise<string | null> {\n if (args.messageIds.length === 0) return null\n // MikroORM v7 dropped the Knex builder; we use raw SQL via\n // `em.getConnection().execute()` with positional placeholders. The JSONB\n // lookup compares `channel_metadata->>'messageId'` against a Postgres\n // text array built from the candidate message-ids.\n // Escape backslash BEFORE quote \u2014 a Message-ID can legitimately contain `\\`\n // (RFC 5322), and an unescaped backslash yields a malformed Postgres array\n // literal that throws and silently defeats threading.\n const idArray = `{${args.messageIds\n .map((id) => `\"${id.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`)\n .join(',')}}`\n const rows = await em.getConnection().execute<Array<{ message_id: string }>>(\n `SELECT link.message_id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.channel_metadata->>'messageId' = ANY(?::text[])\n LIMIT 1`,\n [\n args.tenantId,\n args.organizationId,\n args.organizationId,\n args.tenantId,\n args.organizationId,\n args.organizationId,\n args.channelId,\n idArray,\n ],\n )\n if (!rows || rows.length === 0) return null\n\n const messageId = rows[0].message_id as string\n // Translate `messages.message.id` to `messages.message.thread_id`. We\n // intentionally avoid importing the messages entity (cross-module rule).\n const threadRows = await em.getConnection().execute<Array<{ thread_id: string | null }>>(\n `SELECT thread_id FROM messages\n WHERE id = ?\n AND tenant_id = ?\n AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)\n AND deleted_at IS NULL\n LIMIT 1`,\n [messageId, args.tenantId, args.organizationId, args.organizationId],\n )\n if (!threadRows || threadRows.length === 0) return null\n return threadRows[0].thread_id as string | null\n}\n\nfunction collectParticipants(input: ThreadMatchInput): string[] {\n const set = new Set<string>()\n const push = (value: string | null | undefined): void => {\n if (typeof value !== 'string') return\n const cleaned = value.trim().toLowerCase()\n if (cleaned.length > 0) set.add(cleaned)\n }\n push(input.fromAddress)\n for (const addr of input.toAddresses) push(addr)\n for (const addr of input.ccAddresses) push(addr)\n return Array.from(set)\n}\n\nasync function findThreadBySubjectParticipants(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n channelId: string\n normalizedSubject: string\n participants: string[]\n cutoff: Date\n },\n): Promise<string | null> {\n // MikroORM v7 raw SQL \u2014 see `findThreadByMessageIds` for the rationale.\n // We look at recent links from this channel whose channelMetadata subject\n // (server-side normalized) equals the inbound subject AND share at least\n // one participant. The first match wins.\n const subjectLowerLike = args.normalizedSubject\n // Escape backslash before quote (see findThreadByMessageIds) \u2014 participant\n // addresses are attacker-influenced inbound header values.\n const participantList = `{${args.participants\n .map((p) => `\"${p.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`)\n .join(',')}}`\n const rows = await em.getConnection().execute<Array<{ thread_id: string | null }>>(\n `SELECT messages.thread_id\n FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n INNER JOIN messages\n ON messages.id = link.message_id\n AND messages.tenant_id = link.tenant_id\n AND ((link.organization_id IS NULL AND messages.organization_id IS NULL) OR messages.organization_id = link.organization_id)\n AND messages.deleted_at IS NULL\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.created_at >= ?\n AND lower(regexp_replace(coalesce(link.channel_metadata->>'subject', ''),\n '^\\\\s*((re|fwd|fw|aw|wg|sv|tr|antw)\\\\s*[:\\\\-]\\\\s*|\\\\[[^\\\\]]+\\\\]\\\\s*)+',\n '',\n 'i'\n )) = ?\n AND (\n lower(coalesce(link.channel_metadata->>'from', '')) = ANY(?::text[])\n OR EXISTS (\n SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'to', '[]'::jsonb)) AS t(addr)\n WHERE lower(t.addr) = ANY(?::text[])\n )\n OR EXISTS (\n SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'cc', '[]'::jsonb)) AS t(addr)\n WHERE lower(t.addr) = ANY(?::text[])\n )\n )\n ORDER BY link.created_at DESC\n LIMIT 1`,\n [\n args.tenantId,\n args.organizationId,\n args.organizationId,\n args.tenantId,\n args.organizationId,\n args.organizationId,\n args.channelId,\n args.cutoff,\n subjectLowerLike,\n participantList,\n participantList,\n participantList,\n ],\n )\n if (!rows || rows.length === 0) return null\n return rows[0].thread_id as string | null\n}\n\nfunction subtractDays(from: Date, days: number): Date {\n const next = new Date(from)\n next.setUTCDate(next.getUTCDate() - days)\n return next\n}\n\n// Re-export `MessageChannelLink` so callers importing types from this module\n// don't have to reach into `data/entities.ts` indirectly.\nexport type { MessageChannelLink }\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,219 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { ChannelThreadToken } from "../data/entities.js";
4
+ import { isUniqueViolation } from "./pg-errors.js";
5
+ const TOKEN_PREFIX = "om_";
6
+ const RANDOM_BYTES = 16;
7
+ const HMAC_BYTES = 8;
8
+ const HMAC_KEY_ENV = "OM_THREAD_TOKEN_SECRET";
9
+ const HMAC_FALLBACK_KEY_ENV = "KMS_MASTER_KEY";
10
+ const HMAC_KEY_INFO = "thread-token";
11
+ const TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/;
12
+ let cachedKey = null;
13
+ function getKey() {
14
+ if (cachedKey) return cachedKey;
15
+ const primary = process.env[HMAC_KEY_ENV];
16
+ if (primary && primary.length > 0) {
17
+ cachedKey = Buffer.from(primary, "utf8");
18
+ return cachedKey;
19
+ }
20
+ const fallback = process.env[HMAC_FALLBACK_KEY_ENV];
21
+ if (fallback && fallback.length > 0) {
22
+ cachedKey = createHmac("sha256", fallback).update(HMAC_KEY_INFO).digest();
23
+ return cachedKey;
24
+ }
25
+ if (process.env.NODE_ENV === "production") {
26
+ throw new Error(
27
+ `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014 refusing to sign thread tokens with a static dev key in production.`
28
+ );
29
+ }
30
+ console.warn(
31
+ `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured. Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.`
32
+ );
33
+ cachedKey = createHash("sha256").update("open-mercato-thread-token-dev").digest();
34
+ return cachedKey;
35
+ }
36
+ function _resetThreadTokenKeyCache() {
37
+ cachedKey = null;
38
+ }
39
+ function base64urlEncode(buf) {
40
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
41
+ }
42
+ function base64urlDecode(value) {
43
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
44
+ const padded = value + "=".repeat((4 - value.length % 4) % 4);
45
+ try {
46
+ return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64");
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function computeHmacBytes(random) {
52
+ return createHmac("sha256", getKey()).update(random).digest().subarray(0, HMAC_BYTES);
53
+ }
54
+ function generateToken() {
55
+ const random = randomBytes(RANDOM_BYTES);
56
+ const hmac = computeHmacBytes(random);
57
+ return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`;
58
+ }
59
+ const RANDOM_B64_LEN = Math.ceil(RANDOM_BYTES * 4 / 3);
60
+ const HMAC_B64_LEN = Math.ceil(HMAC_BYTES * 4 / 3);
61
+ const TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN;
62
+ function verifyToken(token) {
63
+ if (typeof token !== "string") return false;
64
+ if (token.length !== TOKEN_TOTAL_LEN) return false;
65
+ if (!token.startsWith(TOKEN_PREFIX)) return false;
66
+ const randomStart = TOKEN_PREFIX.length;
67
+ const randomEnd = randomStart + RANDOM_B64_LEN;
68
+ const separator = token[randomEnd];
69
+ if (separator !== "_") return false;
70
+ const hmacStart = randomEnd + 1;
71
+ const randomPart = token.slice(randomStart, randomEnd);
72
+ const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN);
73
+ const random = base64urlDecode(randomPart);
74
+ if (!random || random.length !== RANDOM_BYTES) return false;
75
+ const provided = base64urlDecode(hmacPart);
76
+ if (!provided || provided.length !== HMAC_BYTES) return false;
77
+ const expected = computeHmacBytes(random);
78
+ try {
79
+ return timingSafeEqual(provided, expected);
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function buildReferencesId(token) {
85
+ return `<${token}@open-mercato.invalid>`;
86
+ }
87
+ function buildBodyFooter(token) {
88
+ return {
89
+ html: `<span style="display:none">[OM:${token}]</span>`,
90
+ plain: `
91
+
92
+ [OM:${token}]`
93
+ };
94
+ }
95
+ function applyOutboundThreadingToken(payload, token) {
96
+ if (!verifyToken(token)) {
97
+ throw new Error("applyOutboundThreadingToken: invalid token format/signature");
98
+ }
99
+ const refId = buildReferencesId(token);
100
+ const footer = buildBodyFooter(token);
101
+ const headers = { ...payload.headers ?? {} };
102
+ const existingRefs = headers["references"] ?? headers["References"];
103
+ let nextRefs;
104
+ if (Array.isArray(existingRefs)) {
105
+ nextRefs = existingRefs.includes(refId) ? existingRefs.join(" ") : [...existingRefs, refId].join(" ");
106
+ } else if (typeof existingRefs === "string" && existingRefs.length > 0) {
107
+ nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`;
108
+ } else {
109
+ nextRefs = refId;
110
+ }
111
+ delete headers["references"];
112
+ headers["References"] = nextRefs;
113
+ let bodyHtml = payload.bodyHtml;
114
+ if (typeof bodyHtml === "string") {
115
+ if (!bodyHtml.includes(`[OM:${token}]`)) {
116
+ const closing = bodyHtml.lastIndexOf("</body>");
117
+ if (closing >= 0) {
118
+ bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`;
119
+ } else {
120
+ bodyHtml = `${bodyHtml}${footer.html}`;
121
+ }
122
+ }
123
+ }
124
+ let bodyText = payload.bodyText;
125
+ if (typeof bodyText === "string") {
126
+ if (!bodyText.includes(`[OM:${token}]`)) {
127
+ bodyText = `${bodyText}${footer.plain}`;
128
+ }
129
+ }
130
+ return {
131
+ ...payload,
132
+ headers,
133
+ ...bodyHtml !== void 0 ? { bodyHtml } : {},
134
+ ...bodyText !== void 0 ? { bodyText } : {}
135
+ };
136
+ }
137
+ function extractTokenFromHeaders(inReplyTo, references) {
138
+ const haystack = [];
139
+ if (typeof inReplyTo === "string" && inReplyTo.length > 0) haystack.push(inReplyTo);
140
+ if (Array.isArray(references)) haystack.push(...references);
141
+ else if (typeof references === "string" && references.length > 0) haystack.push(references);
142
+ for (const candidate of haystack) {
143
+ const matches = candidate.match(new RegExp(TOKEN_REGEX, "g"));
144
+ if (!matches) continue;
145
+ for (const match of matches) {
146
+ if (verifyToken(match)) return match;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+ async function getOrCreateThreadToken(em, args) {
152
+ const dscope = { tenantId: args.tenantId, organizationId: args.organizationId };
153
+ const existing = await findOneWithDecryption(
154
+ em,
155
+ ChannelThreadToken,
156
+ {
157
+ tenantId: args.tenantId,
158
+ organizationId: args.organizationId,
159
+ messageThreadId: args.messageThreadId
160
+ },
161
+ void 0,
162
+ dscope
163
+ );
164
+ if (existing) {
165
+ return { token: existing.token, created: false };
166
+ }
167
+ const row = em.create(ChannelThreadToken, {
168
+ tenantId: args.tenantId,
169
+ organizationId: args.organizationId,
170
+ messageThreadId: args.messageThreadId,
171
+ token: generateToken()
172
+ });
173
+ em.persist(row);
174
+ try {
175
+ await em.flush();
176
+ return { token: row.token, created: true };
177
+ } catch (err) {
178
+ if (!isUniqueViolation(err)) throw err;
179
+ const winner = await findOneWithDecryption(
180
+ em.fork(),
181
+ ChannelThreadToken,
182
+ {
183
+ tenantId: args.tenantId,
184
+ organizationId: args.organizationId,
185
+ messageThreadId: args.messageThreadId
186
+ },
187
+ void 0,
188
+ dscope
189
+ );
190
+ if (winner) return { token: winner.token, created: false };
191
+ throw err;
192
+ }
193
+ }
194
+ function extractTokenFromBody(bodyHtml, bodyText) {
195
+ const haystacks = [bodyHtml, bodyText].filter(
196
+ (value) => typeof value === "string" && value.length > 0
197
+ );
198
+ const pattern = new RegExp(`\\[OM:(${TOKEN_REGEX.source})\\]`, "g");
199
+ for (const haystack of haystacks) {
200
+ let match;
201
+ pattern.lastIndex = 0;
202
+ while ((match = pattern.exec(haystack)) !== null) {
203
+ if (verifyToken(match[1])) return match[1];
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ export {
209
+ _resetThreadTokenKeyCache,
210
+ applyOutboundThreadingToken,
211
+ buildBodyFooter,
212
+ buildReferencesId,
213
+ extractTokenFromBody,
214
+ extractTokenFromHeaders,
215
+ generateToken,
216
+ getOrCreateThreadToken,
217
+ verifyToken
218
+ };
219
+ //# sourceMappingURL=thread-token.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/communication_channels/lib/thread-token.ts"],
4
+ "sourcesContent": ["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadToken } from '../data/entities'\nimport { isUniqueViolation } from './pg-errors'\n\n/**\n * Per-thread crypto token used by the layered thread-matcher to attach\n * inbound replies to the originating Open Mercato message thread, even\n * when the recipient's mail client strips RFC 5322 headers.\n *\n * Token format: `om_<22b64url>_<11b64url>` \u2014 16 random bytes followed by\n * 8 bytes of HMAC-SHA256(random, key), each base64url-encoded without\n * padding. Approximately 37 characters total.\n *\n * Tokens are stored on the `channel_thread_tokens` table keyed by\n * `(tenantId, token)` so that even if the HMAC key leaked, tenant\n * isolation still holds at the DB layer.\n *\n * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.\n */\n\nconst TOKEN_PREFIX = 'om_'\nconst RANDOM_BYTES = 16\nconst HMAC_BYTES = 8\nconst HMAC_KEY_ENV = 'OM_THREAD_TOKEN_SECRET'\nconst HMAC_FALLBACK_KEY_ENV = 'KMS_MASTER_KEY'\nconst HMAC_KEY_INFO = 'thread-token'\n\n/**\n * Pre-validated regex for parsing token candidates extracted from headers\n * or body content. Matches our exact format and rejects anything else\n * before HMAC verification \u2014 defense in depth.\n */\nconst TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/\n\nlet cachedKey: Buffer | null = null\n\n/** Resolve the HMAC key. Falls back through env vars per the spec. */\nfunction getKey(): Buffer {\n if (cachedKey) return cachedKey\n const primary = process.env[HMAC_KEY_ENV]\n if (primary && primary.length > 0) {\n cachedKey = Buffer.from(primary, 'utf8')\n return cachedKey\n }\n const fallback = process.env[HMAC_FALLBACK_KEY_ENV]\n if (fallback && fallback.length > 0) {\n // HKDF-style: derive a per-purpose subkey by HMAC-ing the fallback secret\n // with a constant info label so different purposes don't share a key.\n cachedKey = createHmac('sha256', fallback).update(HMAC_KEY_INFO).digest()\n return cachedKey\n }\n // No secret configured. Fail closed in production rather than signing thread\n // tokens with a public static key (which would let anyone forge a thread\n // token). In non-production we fall back to a dev-only static key and warn.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014` +\n ' refusing to sign thread tokens with a static dev key in production.',\n )\n }\n console.warn(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured.` +\n ' Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.',\n )\n cachedKey = createHash('sha256').update('open-mercato-thread-token-dev').digest()\n return cachedKey\n}\n\n/** Reset the cached key \u2014 for tests that mutate env vars. */\nexport function _resetThreadTokenKeyCache(): void {\n cachedKey = null\n}\n\nfunction base64urlEncode(buf: Buffer): string {\n return buf\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n}\n\nfunction base64urlDecode(value: string): Buffer | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) return null\n const padded = value + '='.repeat((4 - (value.length % 4)) % 4)\n try {\n return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')\n } catch {\n return null\n }\n}\n\nfunction computeHmacBytes(random: Buffer): Buffer {\n return createHmac('sha256', getKey()).update(random).digest().subarray(0, HMAC_BYTES)\n}\n\n/**\n * Generate a new HMAC-signed thread token. The 16 random bytes make a token\n * collision astronomically unlikely; the `(tenantId, token)` unique constraint\n * is the backstop. Per-thread deduplication (one token per thread) is handled\n * separately by `getOrCreateThreadToken` via the `(tenantId, messageThreadId)`\n * unique constraint \u2014 not here.\n */\nexport function generateToken(): string {\n const random = randomBytes(RANDOM_BYTES)\n const hmac = computeHmacBytes(random)\n return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`\n}\n\n// Fixed lengths of the base64url-encoded components, without padding.\n// Computed once: 16 bytes -> 22 chars, 8 bytes -> 11 chars.\nconst RANDOM_B64_LEN = Math.ceil((RANDOM_BYTES * 4) / 3)\nconst HMAC_B64_LEN = Math.ceil((HMAC_BYTES * 4) / 3)\nconst TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN\n\n/**\n * Verify the HMAC signature on a token. Returns `true` only when the\n * structural form is correct AND the HMAC matches under the current key.\n *\n * Does NOT verify the token exists in the DB \u2014 that lookup is the\n * matcher's responsibility (see `thread-matcher.ts`). Verifying here\n * lets us drop forged tokens before any DB I/O.\n *\n * Parsing note: base64url-encoded random/HMAC portions may themselves\n * contain `_` characters, so `split('_')` is unsafe. We parse positionally\n * using the fixed lengths declared above.\n */\nexport function verifyToken(token: string): boolean {\n if (typeof token !== 'string') return false\n if (token.length !== TOKEN_TOTAL_LEN) return false\n if (!token.startsWith(TOKEN_PREFIX)) return false\n const randomStart = TOKEN_PREFIX.length\n const randomEnd = randomStart + RANDOM_B64_LEN\n const separator = token[randomEnd]\n if (separator !== '_') return false\n const hmacStart = randomEnd + 1\n const randomPart = token.slice(randomStart, randomEnd)\n const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN)\n const random = base64urlDecode(randomPart)\n if (!random || random.length !== RANDOM_BYTES) return false\n const provided = base64urlDecode(hmacPart)\n if (!provided || provided.length !== HMAC_BYTES) return false\n const expected = computeHmacBytes(random)\n try {\n return timingSafeEqual(provided, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Build the synthetic RFC 5322 Message-ID we inject into outbound\n * `References:` headers. Uses the IANA-reserved `.invalid` TLD (RFC 6761\n * \u00A7 3) so RFC-compliant MTAs MUST accept it as syntactically valid.\n */\nexport function buildReferencesId(token: string): string {\n return `<${token}@open-mercato.invalid>`\n}\n\n/**\n * Build the hidden HTML body span + plain-text trailer used as the\n * token's secondary attachment point (in case `References` is stripped\n * by the recipient's MUA).\n */\nexport function buildBodyFooter(token: string): { html: string; plain: string } {\n return {\n html: `<span style=\"display:none\">[OM:${token}]</span>`,\n plain: `\\n\\n[OM:${token}]`,\n }\n}\n\n/**\n * Apply the thread token to an outbound MIME-like payload. Mutates the\n * input shape minimally and idempotently:\n * - `headers.references`: appends the synthetic `<om_TOKEN@\u2026>` id if not\n * already present (deduped).\n * - `bodyHtml`: injects a hidden `<span>` before the last `</body>` tag,\n * or appends if no `</body>` is present.\n * - `bodyText`: appends the plain-text trailer.\n *\n * Returns a NEW object \u2014 does not mutate the input. Callers that maintain\n * their own MIME structure can call the building blocks directly.\n */\nexport function applyOutboundThreadingToken<\n T extends {\n headers?: Record<string, string | string[] | undefined>\n bodyHtml?: string\n bodyText?: string\n },\n>(payload: T, token: string): T {\n if (!verifyToken(token)) {\n throw new Error('applyOutboundThreadingToken: invalid token format/signature')\n }\n const refId = buildReferencesId(token)\n const footer = buildBodyFooter(token)\n\n const headers = { ...(payload.headers ?? {}) } as Record<string, string | string[] | undefined>\n const existingRefs = headers['references'] ?? headers['References']\n let nextRefs: string\n if (Array.isArray(existingRefs)) {\n nextRefs = existingRefs.includes(refId) ? existingRefs.join(' ') : [...existingRefs, refId].join(' ')\n } else if (typeof existingRefs === 'string' && existingRefs.length > 0) {\n nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`\n } else {\n nextRefs = refId\n }\n // Normalise to the canonical RFC 5322 header name and drop any duplicate\n // lowercase entry so the MTA sees a single `References` header.\n delete headers['references']\n headers['References'] = nextRefs\n\n let bodyHtml = payload.bodyHtml\n if (typeof bodyHtml === 'string') {\n if (!bodyHtml.includes(`[OM:${token}]`)) {\n const closing = bodyHtml.lastIndexOf('</body>')\n if (closing >= 0) {\n bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`\n } else {\n bodyHtml = `${bodyHtml}${footer.html}`\n }\n }\n }\n\n let bodyText = payload.bodyText\n if (typeof bodyText === 'string') {\n if (!bodyText.includes(`[OM:${token}]`)) {\n bodyText = `${bodyText}${footer.plain}`\n }\n }\n\n return {\n ...payload,\n headers,\n ...(bodyHtml !== undefined ? { bodyHtml } : {}),\n ...(bodyText !== undefined ? { bodyText } : {}),\n }\n}\n\n/**\n * Extract token candidates from a `References` / `In-Reply-To` header\n * value (string or string[]) and return the FIRST one that HMAC-verifies.\n * Returns `null` if no valid token is present.\n */\nexport function extractTokenFromHeaders(\n inReplyTo: string | null | undefined,\n references: string[] | string | null | undefined,\n): string | null {\n const haystack: string[] = []\n if (typeof inReplyTo === 'string' && inReplyTo.length > 0) haystack.push(inReplyTo)\n if (Array.isArray(references)) haystack.push(...references)\n else if (typeof references === 'string' && references.length > 0) haystack.push(references)\n for (const candidate of haystack) {\n const matches = candidate.match(new RegExp(TOKEN_REGEX, 'g'))\n if (!matches) continue\n for (const match of matches) {\n if (verifyToken(match)) return match\n }\n }\n return null\n}\n\n/**\n * Idempotent get-or-create: return the existing `ChannelThreadToken` for the\n * given thread, or create + return a new one. Idempotency is enforced by the\n * `channel_thread_tokens_thread_uq` unique constraint on\n * `(tenant_id, message_thread_id)`: a concurrent double-create loses the race\n * with a unique violation, which we catch and resolve by re-selecting the\n * winner \u2014 so callers always converge on exactly one token per thread.\n *\n * Reads via the standard EntityManager (no encryption needed \u2014 the token\n * column itself is the HMAC-signed value, not encrypted at rest).\n *\n * Use cases:\n * - Outbound subscriber: get or create a token before injecting it\n * into the outbound MIME (`applyOutboundThreadingToken`).\n * - Future \"reset\" UI for tenant admins: explicit rotation by deleting\n * the row + calling this helper again.\n */\nexport async function getOrCreateThreadToken(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n messageThreadId: string\n },\n): Promise<{ token: string; created: boolean }> {\n const dscope = { tenantId: args.tenantId, organizationId: args.organizationId }\n const existing = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (existing) {\n return { token: existing.token, created: false }\n }\n const row = em.create(ChannelThreadToken, {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n token: generateToken(),\n })\n // MikroORM v7 removed `persistAndFlush` \u2014 split into persist + flush.\n em.persist(row)\n try {\n await em.flush()\n return { token: row.token, created: true }\n } catch (err) {\n // A concurrent create for the same (tenant, thread) won the race; the\n // unique constraint rejected ours. Re-select the winner on a clean fork so\n // we never return a half-persisted row or surface a spurious error.\n if (!isUniqueViolation(err)) throw err\n const winner = await findOneWithDecryption(\n em.fork(),\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (winner) return { token: winner.token, created: false }\n throw err\n }\n}\n\n/**\n * Extract a token candidate from an inbound body (HTML or plain text).\n * Scans for `[OM:om_\u2026]` markers and returns the first that HMAC-verifies.\n */\nexport function extractTokenFromBody(\n bodyHtml: string | null | undefined,\n bodyText: string | null | undefined,\n): string | null {\n const haystacks = [bodyHtml, bodyText].filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n )\n const pattern = new RegExp(`\\\\[OM:(${TOKEN_REGEX.source})\\\\]`, 'g')\n for (const haystack of haystacks) {\n let match: RegExpExecArray | null\n pattern.lastIndex = 0\n while ((match = pattern.exec(haystack)) !== null) {\n if (verifyToken(match[1])) return match[1]\n }\n }\n return null\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
4
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
5
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
6
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
7
+ function useConnectChannel(options) {
8
+ const { providerKey } = options;
9
+ const returnUrl = options.returnUrl ?? "/backend/profile/communication-channels";
10
+ const t = useT();
11
+ const [pending, setPending] = React.useState(false);
12
+ const { runMutation, retryLastMutation } = useGuardedMutation({
13
+ contextId: `channel-${providerKey}-connect`,
14
+ blockedMessage: t("communication_channels.profile.connect.blocked", "Connection blocked by validation")
15
+ });
16
+ const mutationContext = React.useMemo(
17
+ () => ({ providerKey, retryLastMutation }),
18
+ [providerKey, retryLastMutation]
19
+ );
20
+ const connect = React.useCallback(async () => {
21
+ if (pending) return;
22
+ setPending(true);
23
+ try {
24
+ const response = await runMutation({
25
+ context: mutationContext,
26
+ mutationPayload: { providerKey },
27
+ operation: () => apiCall(`/api/communication_channels/oauth/${providerKey}/initiate`, {
28
+ method: "POST",
29
+ headers: { "content-type": "application/json" },
30
+ body: JSON.stringify({ returnUrl })
31
+ })
32
+ });
33
+ const body = response.result;
34
+ if (!response.ok || !body?.authorizeUrl) {
35
+ if (body?.code === "oauth_client_not_configured") {
36
+ flash(
37
+ t(
38
+ "communication_channels.profile.connect.notConfigured",
39
+ "This provider is not configured yet. Ask an administrator to add the OAuth Client ID and Secret under Integrations before connecting a mailbox."
40
+ ),
41
+ "error"
42
+ );
43
+ return;
44
+ }
45
+ flash(
46
+ body?.error ?? t("communication_channels.profile.connect.oauthFailed", "Could not start OAuth connection."),
47
+ "error"
48
+ );
49
+ return;
50
+ }
51
+ window.location.assign(body.authorizeUrl);
52
+ } finally {
53
+ setPending(false);
54
+ }
55
+ }, [mutationContext, pending, providerKey, returnUrl, runMutation, t]);
56
+ return { connect, pending };
57
+ }
58
+ export {
59
+ useConnectChannel
60
+ };
61
+ //# sourceMappingURL=use-connect-channel.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/communication_channels/lib/use-connect-channel.ts"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\n\ntype InitiateResponse = { authorizeUrl?: string; error?: string; code?: string }\n\n/**\n * Shared OAuth \"connect\" flow for email channel provider widgets (Gmail and\n * other OAuth providers). Wraps the guarded-mutation contract + `/oauth/<provider>/initiate`\n * call + redirect, so each provider widget only supplies its own button chrome.\n */\nexport function useConnectChannel(options: {\n providerKey: string\n returnUrl?: string\n}): { connect: () => Promise<void>; pending: boolean } {\n const { providerKey } = options\n const returnUrl = options.returnUrl ?? '/backend/profile/communication-channels'\n const t = useT()\n const [pending, setPending] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation({\n contextId: `channel-${providerKey}-connect`,\n blockedMessage: t('communication_channels.profile.connect.blocked', 'Connection blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({ providerKey, retryLastMutation }),\n [providerKey, retryLastMutation],\n )\n\n const connect = React.useCallback(async () => {\n if (pending) return\n setPending(true)\n try {\n const response = await runMutation({\n context: mutationContext,\n mutationPayload: { providerKey },\n operation: () =>\n apiCall<InitiateResponse>(`/api/communication_channels/oauth/${providerKey}/initiate`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ returnUrl }),\n }),\n })\n const body = response.result as InitiateResponse | undefined\n if (!response.ok || !body?.authorizeUrl) {\n if (body?.code === 'oauth_client_not_configured') {\n flash(\n t(\n 'communication_channels.profile.connect.notConfigured',\n 'This provider is not configured yet. Ask an administrator to add the OAuth Client ID and Secret under Integrations before connecting a mailbox.',\n ),\n 'error',\n )\n return\n }\n flash(\n body?.error ??\n t('communication_channels.profile.connect.oauthFailed', 'Could not start OAuth connection.'),\n 'error',\n )\n return\n }\n window.location.assign(body.authorizeUrl)\n } finally {\n setPending(false)\n }\n }, [mutationContext, pending, providerKey, returnUrl, runMutation, t])\n\n return { connect, pending }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,YAAY;AACrB,SAAS,aAAa;AACtB,SAAS,0BAA0B;AACnC,SAAS,eAAe;AASjB,SAAS,kBAAkB,SAGqB;AACrD,QAAM,EAAE,YAAY,IAAI;AACxB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAAmB;AAAA,IAC5D,WAAW,WAAW,WAAW;AAAA,IACjC,gBAAgB,EAAE,kDAAkD,kCAAkC;AAAA,EACxG,CAAC;AACD,QAAM,kBAAkB,MAAM;AAAA,IAC5B,OAAO,EAAE,aAAa,kBAAkB;AAAA,IACxC,CAAC,aAAa,iBAAiB;AAAA,EACjC;AAEA,QAAM,UAAU,MAAM,YAAY,YAAY;AAC5C,QAAI,QAAS;AACb,eAAW,IAAI;AACf,QAAI;AACF,YAAM,WAAW,MAAM,YAAY;AAAA,QACjC,SAAS;AAAA,QACT,iBAAiB,EAAE,YAAY;AAAA,QAC/B,WAAW,MACT,QAA0B,qCAAqC,WAAW,aAAa;AAAA,UACrF,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,UAAU,CAAC;AAAA,QACpC,CAAC;AAAA,MACL,CAAC;AACD,YAAM,OAAO,SAAS;AACtB,UAAI,CAAC,SAAS,MAAM,CAAC,MAAM,cAAc;AACvC,YAAI,MAAM,SAAS,+BAA+B;AAChD;AAAA,YACE;AAAA,cACE;AAAA,cACA;AAAA,YACF;AAAA,YACA;AAAA,UACF;AACA;AAAA,QACF;AACA;AAAA,UACE,MAAM,SACJ,EAAE,sDAAsD,mCAAmC;AAAA,UAC7F;AAAA,QACF;AACA;AAAA,MACF;AACA,aAAO,SAAS,OAAO,KAAK,YAAY;AAAA,IAC1C,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,iBAAiB,SAAS,aAAa,WAAW,aAAa,CAAC,CAAC;AAErE,SAAO,EAAE,SAAS,QAAQ;AAC5B;",
6
+ "names": []
7
+ }