@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,163 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { CommunicationChannel } from '../data/entities'
4
+ import type { ChannelAdapter } from './adapter'
5
+ import { isUniqueViolation } from './pg-errors'
6
+
7
+ /**
8
+ * Default poll cadence (seconds) for a polling-only channel — one whose adapter
9
+ * declares `realtimePush: false`. Push-capable channels use `null` (push-driven,
10
+ * no fixed poll). Shared so push teardown restores the same cadence connect uses.
11
+ */
12
+ export const POLLING_ONLY_DEFAULT_INTERVAL_SECONDS = 300
13
+
14
+ /**
15
+ * Thrown by {@link createConnectedChannelRow} when the same mailbox
16
+ * (`externalIdentifier`) is already connected for this user via a DIFFERENT
17
+ * provider. Both channels would poll the same inbox, and the per-channel
18
+ * `(channel_id, external_message_id)` dedup cannot dedupe the same email across
19
+ * channels — so every message would be ingested (and threaded) twice.
20
+ */
21
+ export class MailboxAlreadyConnectedError extends Error {
22
+ readonly externalIdentifier: string
23
+ readonly existingProviderKey: string
24
+ constructor(externalIdentifier: string, existingProviderKey: string) {
25
+ super(`Mailbox ${externalIdentifier} is already connected via ${existingProviderKey}`)
26
+ this.name = 'MailboxAlreadyConnectedError'
27
+ this.externalIdentifier = externalIdentifier
28
+ this.existingProviderKey = existingProviderKey
29
+ }
30
+ }
31
+
32
+ export interface CreateConnectedChannelRowArgs {
33
+ em: EntityManager
34
+ adapter: Pick<ChannelAdapter, 'channelType' | 'capabilities'>
35
+ providerKey: string
36
+ displayName: string
37
+ externalIdentifier: string | null
38
+ credentialsRefId: string | null
39
+ userId: string
40
+ scope: { tenantId: string; organizationId: string | null }
41
+ /**
42
+ * Explicit poll-interval override (seconds). When omitted, it is derived from
43
+ * the adapter's push capability (push-capable → null, polling-only → 300).
44
+ */
45
+ pollIntervalSeconds?: number | null
46
+ }
47
+
48
+ /**
49
+ * Create + persist the per-user `CommunicationChannel` row for a connect flow.
50
+ * Shared by the credential-connect command and the OAuth callback so both entry
51
+ * points use one channel-shape implementation instead of duplicating `em.create`.
52
+ *
53
+ * When credentials could not be persisted (`credentialsRefId === null`) the row
54
+ * is created in `requires_reauth` + `isActive=false` so workers don't poll a
55
+ * credential-less channel; the user reconnects to recover.
56
+ */
57
+ export async function createConnectedChannelRow(
58
+ args: CreateConnectedChannelRowArgs,
59
+ ): Promise<CommunicationChannel> {
60
+ const { em, adapter, providerKey, displayName, externalIdentifier, credentialsRefId, userId, scope } = args
61
+ const credentialsAvailable = credentialsRefId !== null
62
+ const pollIntervalSeconds =
63
+ args.pollIntervalSeconds !== undefined
64
+ ? args.pollIntervalSeconds
65
+ : adapter.capabilities?.realtimePush === false
66
+ ? POLLING_ONLY_DEFAULT_INTERVAL_SECONDS
67
+ : null
68
+ const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }
69
+
70
+ // Cross-provider duplicate guard: the same mailbox must not be connected via
71
+ // two providers for one user. Both channels would poll the same inbox, and the
72
+ // per-channel `(channel_id, external_message_id)` dedup cannot dedupe the same
73
+ // email across channels — so every message would be ingested (and threaded)
74
+ // twice. Reconnecting the SAME provider/mailbox is fine (healed below); this
75
+ // only blocks a DIFFERENT provider for an already-connected address.
76
+ if (externalIdentifier) {
77
+ const normalized = externalIdentifier.toLowerCase()
78
+ const userChannels = (await findWithDecryption(
79
+ em,
80
+ CommunicationChannel,
81
+ { tenantId: scope.tenantId, userId, deletedAt: null },
82
+ undefined,
83
+ dscope,
84
+ )) as CommunicationChannel[]
85
+ const conflict = userChannels.find(
86
+ (existing) =>
87
+ existing.providerKey !== providerKey &&
88
+ typeof existing.externalIdentifier === 'string' &&
89
+ existing.externalIdentifier.toLowerCase() === normalized,
90
+ )
91
+ if (conflict) {
92
+ throw new MailboxAlreadyConnectedError(externalIdentifier, conflict.providerKey)
93
+ }
94
+ }
95
+
96
+ const naturalKey = {
97
+ tenantId: scope.tenantId,
98
+ userId,
99
+ providerKey,
100
+ externalIdentifier,
101
+ deletedAt: null,
102
+ }
103
+
104
+ // Heal-on-reconnect: a channel for the same (tenant, user, provider, mailbox)
105
+ // already exists when the user re-runs OAuth / reconnects after a
106
+ // `requires_reauth`. Update it in place rather than inserting a duplicate row —
107
+ // a duplicate would stay `isActive` and keep polling + re-emitting reauth
108
+ // banners, and register a second competing push subscription. Only mailboxes
109
+ // with a known `externalIdentifier` participate (the unique index is partial).
110
+ const applyConnectionState = (target: CommunicationChannel): void => {
111
+ target.channelType = adapter.channelType
112
+ target.displayName = displayName
113
+ target.externalIdentifier = externalIdentifier ?? null
114
+ target.credentialsRef = credentialsRefId
115
+ target.capabilities = adapter.capabilities as unknown as Record<string, unknown>
116
+ target.isActive = credentialsAvailable
117
+ target.pollIntervalSeconds = pollIntervalSeconds
118
+ target.status = credentialsAvailable ? 'connected' : 'requires_reauth'
119
+ target.lastError = credentialsAvailable ? null : 'credentials_persist_failed'
120
+ }
121
+
122
+ if (externalIdentifier) {
123
+ const existing = await findOneWithDecryption(em, CommunicationChannel, naturalKey, undefined, dscope)
124
+ if (existing) {
125
+ applyConnectionState(existing)
126
+ await em.flush()
127
+ return existing
128
+ }
129
+ }
130
+
131
+ const channel = em.create(CommunicationChannel, {
132
+ providerKey,
133
+ channelType: adapter.channelType,
134
+ displayName,
135
+ externalIdentifier: externalIdentifier ?? null,
136
+ credentialsRef: credentialsRefId,
137
+ capabilities: adapter.capabilities as unknown as Record<string, unknown>,
138
+ isActive: credentialsAvailable,
139
+ userId,
140
+ isPrimary: false,
141
+ pollIntervalSeconds,
142
+ status: credentialsAvailable ? 'connected' : 'requires_reauth',
143
+ lastError: credentialsAvailable ? null : 'credentials_persist_failed',
144
+ tenantId: scope.tenantId,
145
+ organizationId: scope.organizationId ?? null,
146
+ })
147
+ em.persist(channel)
148
+ try {
149
+ await em.flush()
150
+ return channel
151
+ } catch (err) {
152
+ // Concurrent connect for the same mailbox won the race (partial unique index
153
+ // rejected ours). Re-select the winner on a clean fork and heal it so the
154
+ // caller still gets a single, connected channel.
155
+ if (!isUniqueViolation(err) || !externalIdentifier) throw err
156
+ const reEm = em.fork()
157
+ const winner = await findOneWithDecryption(reEm, CommunicationChannel, naturalKey, undefined, dscope)
158
+ if (!winner) throw err
159
+ applyConnectionState(winner)
160
+ await reEm.flush()
161
+ return winner
162
+ }
163
+ }
@@ -0,0 +1,162 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
3
+ import type { ChannelAdapter, ContactHint, TenantScope } from './adapter'
4
+
5
+ /**
6
+ * Contact resolver — used by the inbound bridge to attach an external sender
7
+ * to a CRM person.
8
+ *
9
+ * Flow (SPEC-045d §8.1):
10
+ * 1. If the adapter implements `resolveContact?(...)`, call it to get a
11
+ * provider-side ContactHint (display name, photo URL, possibly an email
12
+ * lookup the adapter performed against its own user directory).
13
+ * 2. If the hint includes an email or phone, query the CRM (`customers:customer_entity`)
14
+ * via the **QueryEngine** (NOT raw SQL — root AGENTS.md mandate). If a person
15
+ * matches, populate `matchedPersonId` on the returned hint.
16
+ * 3. Return the merged hint, or `null` if neither the adapter nor the CRM
17
+ * yields any identity information.
18
+ */
19
+
20
+ export interface ContactResolverInput {
21
+ adapter: ChannelAdapter
22
+ senderIdentifier: string
23
+ senderDisplayName?: string
24
+ channelMetadata?: Record<string, unknown>
25
+ credentials: Record<string, unknown>
26
+ scope: TenantScope
27
+ }
28
+
29
+ export interface ResolveContactDeps {
30
+ container: { resolve: <T = unknown>(name: string) => T }
31
+ }
32
+
33
+ type QueryEngineLike = {
34
+ query: (
35
+ entityType: string,
36
+ options: {
37
+ tenantId: string
38
+ organizationId?: string | null
39
+ filter?: Record<string, unknown>
40
+ limit?: number
41
+ offset?: number
42
+ },
43
+ ) => Promise<Record<string, unknown>[]>
44
+ }
45
+
46
+ /**
47
+ * Resolve a CRM contact from an external sender identifier.
48
+ *
49
+ * Returns `null` when no identity could be derived. Callers MUST treat the
50
+ * return value as advisory (a hint, not an authoritative match) — see spec
51
+ * § 8 "Contact resolution false positive" risk.
52
+ */
53
+ export async function resolveContact(
54
+ input: ContactResolverInput,
55
+ deps: ResolveContactDeps,
56
+ ): Promise<ContactHint | null> {
57
+ // Step 1 — adapter resolution (optional)
58
+ const adapterHint: ContactHint | null = await runAdapterResolve(input)
59
+ if (!adapterHint && !input.senderIdentifier) return null
60
+
61
+ // Step 2 — CRM lookup by email or phone, via QueryEngine (no raw SQL).
62
+ const lookupEmail = adapterHint?.email ?? heuristicEmail(input.senderIdentifier)
63
+ const lookupPhone = adapterHint?.phone ?? heuristicPhone(input.senderIdentifier)
64
+
65
+ let matchedPersonId: string | undefined
66
+ if ((lookupEmail || lookupPhone) && deps.container) {
67
+ matchedPersonId = await lookupCustomerPersonId({
68
+ container: deps.container,
69
+ scope: input.scope,
70
+ email: lookupEmail,
71
+ phone: lookupPhone,
72
+ })
73
+ }
74
+
75
+ return {
76
+ ...(adapterHint ?? {}),
77
+ email: lookupEmail,
78
+ phone: lookupPhone,
79
+ displayName: adapterHint?.displayName ?? input.senderDisplayName,
80
+ matchedPersonId: matchedPersonId ?? adapterHint?.matchedPersonId,
81
+ }
82
+ }
83
+
84
+ async function runAdapterResolve(input: ContactResolverInput): Promise<ContactHint | null> {
85
+ if (typeof input.adapter.resolveContact !== 'function') return null
86
+ try {
87
+ return (
88
+ (await input.adapter.resolveContact({
89
+ senderIdentifier: input.senderIdentifier,
90
+ senderDisplayName: input.senderDisplayName,
91
+ channelMetadata: input.channelMetadata,
92
+ credentials: input.credentials,
93
+ scope: input.scope,
94
+ })) ?? null
95
+ )
96
+ } catch {
97
+ // The adapter is best-effort. A failure does not block ingest; we just lose
98
+ // the optional CRM match. Errors are not propagated.
99
+ return null
100
+ }
101
+ }
102
+
103
+ async function lookupCustomerPersonId(params: {
104
+ container: ResolveContactDeps['container']
105
+ scope: TenantScope
106
+ email?: string
107
+ phone?: string
108
+ }): Promise<string | undefined> {
109
+ // Under tenant encryption, `primary_email`/`primary_phone` are stored as
110
+ // ciphertext, so a plaintext equality filter on the base column both hits the
111
+ // §16 "no querying an encrypted column by value" footgun and never matches.
112
+ // Skip the fast lookup in that case (it would only ever return nothing) — the
113
+ // authoritative CRM link is created by the customers `link-channel-message`
114
+ // subscriber, which does an in-memory decrypted comparison. A blind-index
115
+ // column is the proper fast-path fix here (same follow-up as
116
+ // `customers/lib/findPeopleByAddresses`).
117
+ if (isTenantDataEncryptionEnabled()) return undefined
118
+
119
+ let queryEngine: QueryEngineLike | null = null
120
+ try {
121
+ queryEngine = params.container.resolve<QueryEngineLike>('queryEngine')
122
+ } catch {
123
+ return undefined
124
+ }
125
+ if (!queryEngine || typeof queryEngine.query !== 'function') return undefined
126
+
127
+ const filter = buildPersonLookupFilter(params.email, params.phone)
128
+ if (!filter) return undefined
129
+
130
+ try {
131
+ const rows = await queryEngine.query('customers:customer_entity', {
132
+ tenantId: params.scope.tenantId,
133
+ organizationId: params.scope.organizationId,
134
+ filter,
135
+ limit: 1,
136
+ })
137
+ const first = rows?.[0]
138
+ const id = first && typeof first.id === 'string' ? (first.id as string) : undefined
139
+ return id
140
+ } catch {
141
+ return undefined
142
+ }
143
+ }
144
+
145
+ function buildPersonLookupFilter(email?: string, phone?: string): Record<string, unknown> | null {
146
+ if (email) return { primary_email: email, kind: 'person' }
147
+ if (phone) return { primary_phone: phone, kind: 'person' }
148
+ return null
149
+ }
150
+
151
+ function heuristicEmail(senderIdentifier: string): string | undefined {
152
+ if (!senderIdentifier.includes('@')) return undefined
153
+ // Don't second-guess provider-supplied data; just check it's email-shaped.
154
+ return senderIdentifier.includes('.') ? senderIdentifier : undefined
155
+ }
156
+
157
+ function heuristicPhone(senderIdentifier: string): string | undefined {
158
+ // Plain heuristic: +CC followed by 6+ digits. Real phone validation is the
159
+ // adapter's job (the email integration spec elaborates IMAP paths).
160
+ if (/^\+\d{6,}$/.test(senderIdentifier)) return senderIdentifier
161
+ return undefined
162
+ }
@@ -0,0 +1,197 @@
1
+ import type {
2
+ ChannelAdapter,
3
+ OAuthClientConfig,
4
+ RefreshedCredentials,
5
+ TenantScope,
6
+ } from './adapter'
7
+ import { resolveOAuthClientCredentials } from './oauth-client-config'
8
+
9
+ /**
10
+ * Optional credentials-service shape — matches the integrations module's
11
+ * `CredentialsService`. We keep this loose so the helper compiles even when
12
+ * the integrations module is disabled in a downstream app (the helper just
13
+ * skips persistence in that case).
14
+ */
15
+ /**
16
+ * Matches the real `CredentialsService.save(integrationId, credentials, scope)`
17
+ * signature from `packages/core/src/modules/integrations/lib/credentials-service.ts`.
18
+ * The legacy call sites had this argument order inverted; that bug was the root
19
+ * cause of C1 in the 2026-05-26 review.
20
+ *
21
+ * `scope.userId` (added 2026-05-26 for per-user channels) lets the credentials
22
+ * service write to a user-scoped row instead of overwriting the tenant-wide row.
23
+ */
24
+ type CredentialsScope = TenantScope & { userId?: string | null }
25
+ type CredentialsServiceLike = {
26
+ resolve: (integrationId: string, scope: CredentialsScope) => Promise<Record<string, unknown> | null>
27
+ save?: (integrationId: string, credentials: Record<string, unknown>, scope: CredentialsScope) => Promise<void>
28
+ }
29
+
30
+ export type RefreshCredentialsIfNeededInput = {
31
+ adapter: ChannelAdapter
32
+ channelId: string
33
+ /** Current decrypted credential blob. May contain `expiresAt` (Date or ISO string). */
34
+ credentials: Record<string, unknown>
35
+ scope: CredentialsScope
36
+ /** Refresh window — refresh when token expires within this many ms. Defaults to 60s. */
37
+ refreshWindowMs?: number
38
+ /** Force a refresh regardless of expiry — used after a 401 response from the provider. */
39
+ force?: boolean
40
+ }
41
+
42
+ export type RefreshCredentialsIfNeededResult = {
43
+ refreshed: boolean
44
+ /** The latest credential blob to use for the outbound call. */
45
+ credentials: Record<string, unknown>
46
+ }
47
+
48
+ const DEFAULT_REFRESH_WINDOW_MS = 60_000
49
+
50
+ /**
51
+ * In-process single-flight for credential refresh, keyed by `channelId`. The
52
+ * outbound-delivery worker runs at concurrency 10 in ONE process, so two
53
+ * concurrent sends on the same channel can both pass `shouldRefresh` and both
54
+ * call `adapter.refreshCredentials`. With rotating refresh-token providers
55
+ * (Gmail) the second exchange invalidates the first's token and
56
+ * flaps the channel to `requires_reauth`. Coalescing concurrent refreshes for
57
+ * the same channel onto one in-flight promise prevents that race for the common
58
+ * single-process case. Entries are deleted in `finally` once settled.
59
+ */
60
+ const inFlightRefreshes = new Map<string, Promise<RefreshCredentialsIfNeededResult>>()
61
+
62
+ /**
63
+ * Refresh OAuth credentials when an access token is near expiry, or when the
64
+ * caller forces it (e.g. after a 401 response).
65
+ *
66
+ * Behaviour:
67
+ * - No-op when the adapter does not implement `refreshCredentials?`.
68
+ * - No-op when the credential blob has no `expiresAt` AND `force !== true`.
69
+ * - Returns the refreshed credentials in-memory; persistence to
70
+ * `integration_credentials` happens via the `CredentialsService` if it
71
+ * is registered AND exposes `save()` (best-effort — failures are logged
72
+ * but don't block the outbound call).
73
+ */
74
+ export async function refreshCredentialsIfNeeded(
75
+ input: RefreshCredentialsIfNeededInput,
76
+ deps?: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void },
77
+ ): Promise<RefreshCredentialsIfNeededResult> {
78
+ const log = deps?.logger ?? (() => {})
79
+ if (typeof input.adapter.refreshCredentials !== 'function') {
80
+ return { refreshed: false, credentials: input.credentials }
81
+ }
82
+
83
+ const refreshWindow = input.refreshWindowMs ?? DEFAULT_REFRESH_WINDOW_MS
84
+ if (!input.force && !shouldRefresh(input.credentials, refreshWindow)) {
85
+ return { refreshed: false, credentials: input.credentials }
86
+ }
87
+
88
+ // Coalesce concurrent refreshes for the same channel onto one in-flight
89
+ // promise so rotating refresh tokens are not exchanged twice in parallel.
90
+ const existing = inFlightRefreshes.get(input.channelId)
91
+ if (existing) return existing
92
+
93
+ const refreshCredentials = input.adapter.refreshCredentials.bind(input.adapter)
94
+ const refreshPromise = runRefresh(input, refreshCredentials, deps, log).finally(() => {
95
+ inFlightRefreshes.delete(input.channelId)
96
+ })
97
+ inFlightRefreshes.set(input.channelId, refreshPromise)
98
+ return refreshPromise
99
+ }
100
+
101
+ async function runRefresh(
102
+ input: RefreshCredentialsIfNeededInput,
103
+ refreshCredentials: NonNullable<ChannelAdapter['refreshCredentials']>,
104
+ deps: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void } | undefined,
105
+ log: (...args: unknown[]) => void,
106
+ ): Promise<RefreshCredentialsIfNeededResult> {
107
+ // Resolve the tenant's OAuth client config (clientId/clientSecret) the admin
108
+ // stored under the `channel_<providerKey>` integration at TENANT scope
109
+ // (userId = null). The adapter uses it for the token-endpoint call. We always
110
+ // resolve at tenant scope here — never the channel's per-user scope — because
111
+ // the per-user row holds the user's tokens, not the client app credentials.
112
+ let oauthClient: OAuthClientConfig | undefined
113
+ if (deps?.credentialsService) {
114
+ try {
115
+ const raw = await resolveOAuthClientCredentials(
116
+ deps.credentialsService,
117
+ input.adapter.providerKey,
118
+ { tenantId: input.scope.tenantId, organizationId: input.scope.organizationId },
119
+ )
120
+ oauthClient = safeParseOAuthClient(raw)
121
+ } catch (resolveErr) {
122
+ log(
123
+ '[communication_channels] resolving OAuth client config failed:',
124
+ resolveErr instanceof Error ? resolveErr.message : resolveErr,
125
+ )
126
+ }
127
+ }
128
+
129
+ let result: RefreshedCredentials
130
+ try {
131
+ result = await refreshCredentials({
132
+ channelId: input.channelId,
133
+ credentials: input.credentials,
134
+ scope: input.scope,
135
+ oauthClient,
136
+ })
137
+ } catch (err) {
138
+ log('[communication_channels] refreshCredentials failed:', err instanceof Error ? err.message : err)
139
+ // Return current credentials — caller may still attempt the send with the old token.
140
+ return { refreshed: false, credentials: input.credentials }
141
+ }
142
+
143
+ const next = result?.credentials ?? input.credentials
144
+ if (deps?.credentialsService?.save) {
145
+ try {
146
+ await deps.credentialsService.save(
147
+ `channel_${input.adapter.providerKey}`,
148
+ result.expiresAt ? { ...next, expiresAt: result.expiresAt.toISOString() } : next,
149
+ input.scope,
150
+ )
151
+ } catch (saveErr) {
152
+ log('[communication_channels] persisting refreshed credentials failed:', saveErr instanceof Error ? saveErr.message : saveErr)
153
+ }
154
+ }
155
+
156
+ return { refreshed: true, credentials: next }
157
+ }
158
+
159
+ function shouldRefresh(credentials: Record<string, unknown>, windowMs: number): boolean {
160
+ const expiresAtRaw = credentials?.expiresAt
161
+ if (!expiresAtRaw) return false
162
+ const expiresAt = parseExpiresAt(expiresAtRaw)
163
+ if (!expiresAt) return false
164
+ return expiresAt.getTime() - Date.now() <= windowMs
165
+ }
166
+
167
+ function parseExpiresAt(raw: unknown): Date | null {
168
+ if (raw instanceof Date) return Number.isFinite(raw.getTime()) ? raw : null
169
+ if (typeof raw === 'string' || typeof raw === 'number') {
170
+ const date = new Date(raw)
171
+ return Number.isFinite(date.getTime()) ? date : null
172
+ }
173
+ return null
174
+ }
175
+
176
+ /**
177
+ * Parse a raw `channel_<provider>` client-credential row into the
178
+ * `OAuthClientConfig` shape adapters expect. Returns `undefined` when the row is
179
+ * missing or malformed — adapters then fall back to the deprecated
180
+ * `credentials._client` read path (one minor-release deprecation per Spec A).
181
+ */
182
+ function safeParseOAuthClient(raw: unknown): OAuthClientConfig | undefined {
183
+ if (!raw || typeof raw !== 'object') return undefined
184
+ const record = raw as Record<string, unknown>
185
+ const clientId = typeof record.clientId === 'string' ? record.clientId : undefined
186
+ if (!clientId) return undefined
187
+ const clientSecret =
188
+ typeof record.clientSecret === 'string' ? record.clientSecret : undefined
189
+ const scopes = Array.isArray(record.scopes)
190
+ ? record.scopes.filter((value): value is string => typeof value === 'string')
191
+ : undefined
192
+ return {
193
+ clientId,
194
+ ...(clientSecret !== undefined ? { clientSecret } : {}),
195
+ ...(scopes !== undefined ? { scopes } : {}),
196
+ }
197
+ }
@@ -0,0 +1,87 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { ChannelIngestDeadLetter } from '../data/entities'
4
+ import type { NormalizedInboundMessage } from './adapter'
5
+
6
+ const DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32_768
7
+
8
+ export function extractExternalUid(message: NormalizedInboundMessage): string | null {
9
+ const meta = message.channelMetadata as Record<string, unknown> | undefined
10
+ if (meta && typeof meta.uid === 'string') return meta.uid
11
+ if (meta && typeof meta.uid === 'number') return String(meta.uid)
12
+ return null
13
+ }
14
+
15
+ /**
16
+ * First N *bytes* of the raw body, for dead-letter forensics. Counts bytes (not
17
+ * UTF-16 code units) so a multi-byte body cannot blow past the intended cap.
18
+ */
19
+ export function truncateRawBody(message: NormalizedInboundMessage): string | null {
20
+ const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? '', 10)
21
+ const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT
22
+ const body = message.body ?? ''
23
+ if (body.length === 0) return null
24
+ const buf = Buffer.from(body, 'utf-8')
25
+ if (buf.byteLength <= cap) return body
26
+ return `${buf.subarray(0, cap).toString('utf-8')}…[truncated]`
27
+ }
28
+
29
+ export interface WriteIngestDeadLetterArgs {
30
+ em: EntityManager
31
+ scope: { tenantId: string; organizationId?: string | null }
32
+ channel: { id: string; providerKey: string }
33
+ message: NormalizedInboundMessage
34
+ err: unknown
35
+ errorMessage: string
36
+ }
37
+
38
+ /**
39
+ * Persist a `ChannelIngestDeadLetter` row for a permanently-failed inbound
40
+ * message so an operator can replay it later. Best-effort: a failure to write
41
+ * the dead-letter is logged but never thrown, so a bad message cannot block the
42
+ * caller from advancing its cursor.
43
+ *
44
+ * Idempotent on `(channelId, externalMessageId)`: a replayed page that fails the
45
+ * same message again is a no-op, so the dead-letter table never accumulates
46
+ * duplicate rows for the same poison message.
47
+ */
48
+ export async function writeIngestDeadLetter(args: WriteIngestDeadLetterArgs): Promise<void> {
49
+ const { em, scope, channel, message, err, errorMessage } = args
50
+ const externalMessageId = message.externalMessageId ?? null
51
+ try {
52
+ if (externalMessageId) {
53
+ const existing = await findOneWithDecryption(
54
+ em,
55
+ ChannelIngestDeadLetter,
56
+ {
57
+ tenantId: scope.tenantId,
58
+ organizationId: scope.organizationId ?? null,
59
+ channelId: channel.id,
60
+ externalMessageId,
61
+ },
62
+ undefined,
63
+ { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },
64
+ )
65
+ if (existing) return
66
+ }
67
+ const deadLetter = em.create(ChannelIngestDeadLetter, {
68
+ tenantId: scope.tenantId,
69
+ organizationId: scope.organizationId ?? null,
70
+ channelId: channel.id,
71
+ providerKey: channel.providerKey,
72
+ externalUid: extractExternalUid(message),
73
+ externalMessageId,
74
+ errorClass: err instanceof Error ? err.name : 'Error',
75
+ errorMessage,
76
+ rawBody: truncateRawBody(message),
77
+ })
78
+ em.persist(deadLetter)
79
+ await em.flush()
80
+ } catch (ddlErr) {
81
+ console.error(
82
+ `[communication_channels:dead-letter] failed to record dead-letter for channel ${channel.id}: ${
83
+ ddlErr instanceof Error ? ddlErr.message : String(ddlErr)
84
+ }`,
85
+ )
86
+ }
87
+ }
@@ -0,0 +1,60 @@
1
+ import type { ChannelCapabilities } from './adapter'
2
+
3
+ /**
4
+ * Shared default attachment ceiling for email providers (Gmail allows
5
+ * more, but larger uploads need resumable/upload-session APIs we don't use, so
6
+ * all providers cap at the same conservative value).
7
+ */
8
+ export const EMAIL_MAX_ATTACHMENT_BYTES = 25_000_000
9
+
10
+ /**
11
+ * Baseline capability profile shared by every email channel provider. Providers
12
+ * spread this and override only what genuinely differs (e.g. `deleteMessage`).
13
+ *
14
+ * `fileSharing: false` until a provider's `convertOutbound` stitches attachment
15
+ * bytes into the sent message — advertising `true` would silently drop bytes.
16
+ */
17
+ export const baseEmailCapabilities: ChannelCapabilities = {
18
+ // Core
19
+ threading: true,
20
+ richText: true,
21
+ fileSharing: false,
22
+ maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,
23
+ supportedMimeTypes: [
24
+ 'image/png',
25
+ 'image/jpeg',
26
+ 'image/gif',
27
+ 'image/webp',
28
+ 'application/pdf',
29
+ 'application/zip',
30
+ 'application/octet-stream',
31
+ 'text/plain',
32
+ 'text/html',
33
+ 'text/csv',
34
+ ],
35
+ readReceipts: false,
36
+ deliveryReceipts: false,
37
+ typingIndicators: false,
38
+
39
+ // Extended
40
+ reactions: false,
41
+ multiReactionPerUser: false,
42
+ editMessage: false,
43
+ deleteMessage: false,
44
+ presence: false,
45
+ richBlocks: false,
46
+ interactiveComponents: false,
47
+ inlineImages: true,
48
+ conversationHistory: true,
49
+ contactCards: false,
50
+ locationSharing: false,
51
+ voiceNotes: false,
52
+ stickers: false,
53
+
54
+ // Body formats
55
+ supportedBodyFormats: ['text', 'html'],
56
+ maxBodyLength: 5_000_000,
57
+
58
+ // Polling (real-time push deferred to v2 for all email providers)
59
+ realtimePush: false,
60
+ }
@@ -0,0 +1,17 @@
1
+ import type { ContactHint, ResolveContactInput } from './adapter'
2
+
3
+ /**
4
+ * Shared `resolveContact` for email providers. An email sender identifier *is*
5
+ * the contact email, so the hint is a direct passthrough; non-email identifiers
6
+ * (or empty senders) yield `null` and the hub falls back to its own resolution.
7
+ */
8
+ export async function emailResolveContact(input: ResolveContactInput): Promise<ContactHint | null> {
9
+ if (!input.senderIdentifier) return null
10
+ if (input.senderIdentifier.includes('@')) {
11
+ return {
12
+ email: input.senderIdentifier,
13
+ displayName: input.senderDisplayName,
14
+ }
15
+ }
16
+ return null
17
+ }