@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,325 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
3
+ import { CustomerInteraction } from '../data/entities'
4
+ import { buildEmailVisibilityMikroFilter } from './visibilityFilter'
5
+
6
+ /**
7
+ * Read model that turns a Person's email `CustomerInteraction` rows into
8
+ * Gmail-style threads for the CRM Person page.
9
+ *
10
+ * The Person↔email anchor lives only on `CustomerInteraction`
11
+ * (`interactionType='email'`, `externalMessageId` → `MessageChannelLink.id`).
12
+ * The thread grouping key and the rich per-message content (From/To/Cc, body,
13
+ * direction) live in the communication_channels hub. We read the hub entities
14
+ * by their string class names — the same cross-module pattern used by
15
+ * `link-channel-message-handler.ts` — so the customers module never imports the
16
+ * hub's entity classes (root AGENTS.md: no direct ORM relationships between
17
+ * modules).
18
+ */
19
+
20
+ export type EmailThreadDirection = 'inbound' | 'outbound'
21
+
22
+ export type PersonEmailMessage = {
23
+ id: string
24
+ /** Open Mercato `messages.message` id — used as `parentMessageId` when replying so the reply joins this thread. */
25
+ messageId: string | null
26
+ /** RFC2822 Message-ID of this email — used as `In-Reply-To` on a reply. */
27
+ rfcMessageId: string | null
28
+ /** RFC2822 References chain for this email. */
29
+ references: string[]
30
+ direction: EmailThreadDirection
31
+ fromName: string | null
32
+ fromEmail: string | null
33
+ to: string[]
34
+ cc: string[]
35
+ subject: string | null
36
+ bodyText: string | null
37
+ sentAt: string
38
+ providerKey: string | null
39
+ }
40
+
41
+ export type PersonEmailThread = {
42
+ threadKey: string
43
+ subject: string | null
44
+ preview: string | null
45
+ participants: string[]
46
+ lastMessageAt: string
47
+ messageCount: number
48
+ providerKey: string | null
49
+ lastDirection: EmailThreadDirection
50
+ messages: PersonEmailMessage[]
51
+ }
52
+
53
+ export type BuildPersonEmailThreadsOptions = {
54
+ personId: string
55
+ tenantId: string
56
+ organizationId: string | null
57
+ viewerUserId: string | null
58
+ userFeatures: string[] | null | undefined
59
+ maxThreads?: number
60
+ maxMessagesPerThread?: number
61
+ }
62
+
63
+ const DEFAULT_MAX_THREADS = 50
64
+ const DEFAULT_MAX_MESSAGES_PER_THREAD = 200
65
+ const PREVIEW_LENGTH = 140
66
+
67
+ type JsonRecord = Record<string, unknown>
68
+
69
+ /** Extracts `{ email, name }[]` from the many shapes a channel address field can take. */
70
+ function extractAddresses(value: unknown): Array<{ email: string; name: string | null }> {
71
+ const out: Array<{ email: string; name: string | null }> = []
72
+ const pushOne = (item: unknown): void => {
73
+ if (typeof item === 'string') {
74
+ const trimmed = item.trim()
75
+ if (trimmed) out.push({ email: trimmed, name: null })
76
+ return
77
+ }
78
+ if (item && typeof item === 'object') {
79
+ const rec = item as JsonRecord
80
+ const address = typeof rec.address === 'string' ? rec.address.trim() : null
81
+ const name = typeof rec.name === 'string' && rec.name.trim() ? rec.name.trim() : null
82
+ if (address) out.push({ email: address, name })
83
+ }
84
+ }
85
+ if (Array.isArray(value)) {
86
+ for (const item of value) pushOne(item)
87
+ } else {
88
+ pushOne(value)
89
+ }
90
+ return out
91
+ }
92
+
93
+ function firstString(...values: unknown[]): string | null {
94
+ for (const value of values) {
95
+ if (typeof value === 'string' && value.trim().length > 0) return value
96
+ }
97
+ return null
98
+ }
99
+
100
+ function toStringArray(...values: unknown[]): string[] {
101
+ for (const value of values) {
102
+ if (Array.isArray(value)) {
103
+ const strings = value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
104
+ if (strings.length > 0) return strings
105
+ }
106
+ }
107
+ return []
108
+ }
109
+
110
+ function truncate(value: string | null): string | null {
111
+ if (!value) return value
112
+ const collapsed = value.replace(/\s+/g, ' ').trim()
113
+ if (collapsed.length <= PREVIEW_LENGTH) return collapsed
114
+ return `${collapsed.slice(0, PREVIEW_LENGTH - 1)}…`
115
+ }
116
+
117
+ /**
118
+ * Builds the per-Person email-thread read model. Pure data assembly — callers
119
+ * are responsible for auth and scoping (tenantId/organizationId required).
120
+ */
121
+ export async function buildPersonEmailThreads(
122
+ em: EntityManager,
123
+ opts: BuildPersonEmailThreadsOptions,
124
+ ): Promise<PersonEmailThread[]> {
125
+ const {
126
+ personId,
127
+ tenantId,
128
+ organizationId,
129
+ viewerUserId,
130
+ userFeatures,
131
+ maxThreads = DEFAULT_MAX_THREADS,
132
+ maxMessagesPerThread = DEFAULT_MAX_MESSAGES_PER_THREAD,
133
+ } = opts
134
+
135
+ // ── (1) Load the Person's email interactions (the Person↔email anchor) ──
136
+ const interactionWhere: JsonRecord = {
137
+ entity: personId,
138
+ interactionType: 'email',
139
+ deletedAt: null,
140
+ tenantId,
141
+ }
142
+ if (organizationId) interactionWhere.organizationId = organizationId
143
+
144
+ // Per-email visibility (v1: strict owner-only, no admin bypass) — the CRM
145
+ // Person page applies the SAME rule as every other interactions read path via
146
+ // `buildEmailVisibilityMikroFilter`, so the Emails tab and the `/interactions`
147
+ // timeline can never disagree about who sees an email:
148
+ // - `visibility = 'shared'` is visible to every user with CRM access to this
149
+ // Person (lets a teammate pick up a handed-off thread),
150
+ // - `visibility = 'private'` is visible ONLY to its author (the mailbox
151
+ // owner) — never to teammates, not even an admin/superadmin (team
152
+ // oversight is a deliberate v2 follow-up),
153
+ // - legacy/unset rows (`visibility IS NULL`) stay visible so pre-existing
154
+ // CRM history is never silently hidden.
155
+ // Fail-closed: a null viewer (API-key caller) never matches the author arm, so
156
+ // it only ever sees shared/legacy rows — never anyone's private email.
157
+ interactionWhere.$or = buildEmailVisibilityMikroFilter({
158
+ currentUserId: viewerUserId,
159
+ userFeatures,
160
+ }).$or
161
+
162
+ // `customer_interaction.title`/`body` are encrypted at rest, so reads go
163
+ // through `findWithDecryption` even though we only consume non-encrypted
164
+ // columns here — this keeps the encrypted-entity contract intact.
165
+ const dscope = { tenantId, organizationId: organizationId ?? null }
166
+ const interactions = (await findWithDecryption(
167
+ em,
168
+ CustomerInteraction,
169
+ interactionWhere as never,
170
+ { orderBy: { occurredAt: 'desc', createdAt: 'desc' }, limit: maxThreads * 20 },
171
+ dscope,
172
+ )) as CustomerInteraction[]
173
+
174
+ const linkIdByInteraction = new Map<string, { occurredAt: Date }>()
175
+ const linkIds: string[] = []
176
+ for (const interaction of interactions) {
177
+ const linkId = interaction.externalMessageId
178
+ if (!linkId || linkIdByInteraction.has(linkId)) continue
179
+ linkIdByInteraction.set(linkId, { occurredAt: interaction.occurredAt ?? interaction.createdAt })
180
+ linkIds.push(linkId)
181
+ }
182
+ if (linkIds.length === 0) return []
183
+
184
+ // ── (2) Resolve MessageChannelLink rows (rich content + direction) ──────
185
+ const linkWhere: JsonRecord = { id: { $in: linkIds }, tenantId }
186
+ if (organizationId) linkWhere.organizationId = organizationId
187
+ const links = (await findWithDecryption(
188
+ em,
189
+ 'MessageChannelLink' as never,
190
+ linkWhere as never,
191
+ undefined,
192
+ dscope,
193
+ )) as JsonRecord[]
194
+
195
+ // ── (3) Resolve hub Message rows (the authoritative thread grouping) ────
196
+ const messageIds = Array.from(
197
+ new Set(
198
+ links
199
+ .map((link) => (typeof link.messageId === 'string' ? (link.messageId as string) : null))
200
+ .filter((value): value is string => !!value),
201
+ ),
202
+ )
203
+ const messageById = new Map<string, JsonRecord>()
204
+ if (messageIds.length > 0) {
205
+ const messageWhere: JsonRecord = { id: { $in: messageIds }, tenantId }
206
+ if (organizationId) messageWhere.organizationId = organizationId
207
+ const messages = (await findWithDecryption(
208
+ em,
209
+ 'Message' as never,
210
+ messageWhere as never,
211
+ undefined,
212
+ dscope,
213
+ )) as JsonRecord[]
214
+ for (const message of messages) {
215
+ if (typeof message.id === 'string') messageById.set(message.id, message)
216
+ }
217
+ }
218
+
219
+ // ── (4) Build per-message DTOs grouped by thread ────────────────────────
220
+ const threadsByKey = new Map<string, PersonEmailThread>()
221
+
222
+ for (const link of links) {
223
+ const linkId = typeof link.id === 'string' ? (link.id as string) : null
224
+ if (!linkId) continue
225
+ const direction: EmailThreadDirection = link.direction === 'outbound' ? 'outbound' : 'inbound'
226
+ const providerKey = typeof link.providerKey === 'string' ? (link.providerKey as string) : null
227
+ const payload = (link.channelPayload ?? null) as JsonRecord | null
228
+ const meta = (link.channelMetadata ?? null) as JsonRecord | null
229
+ const messageId = typeof link.messageId === 'string' ? (link.messageId as string) : null
230
+ const message = messageId ? messageById.get(messageId) ?? null : null
231
+
232
+ // Outbound addresses live in channelMetadata; inbound in channelPayload.
233
+ // Prefer the direction-appropriate source, fall back to the other.
234
+ const primary = direction === 'outbound' ? meta : payload
235
+ const secondary = direction === 'outbound' ? payload : meta
236
+ const fromList = extractAddresses(primary?.from ?? secondary?.from)
237
+ const toList = extractAddresses(primary?.to ?? secondary?.to)
238
+ const ccList = extractAddresses(primary?.cc ?? secondary?.cc)
239
+
240
+ const subject = firstString(
241
+ primary?.subject,
242
+ secondary?.subject,
243
+ typeof message?.subject === 'string' ? message.subject : null,
244
+ )
245
+ const bodyText = firstString(
246
+ direction === 'outbound' ? meta?.bodyText : payload?.text,
247
+ direction === 'outbound' ? payload?.text : meta?.bodyText,
248
+ typeof message?.body === 'string' ? message.body : null,
249
+ )
250
+
251
+ const sentAtRaw =
252
+ (typeof message?.sentAt === 'string' || message?.sentAt instanceof Date
253
+ ? message.sentAt
254
+ : null) ??
255
+ (link.createdAt instanceof Date || typeof link.createdAt === 'string' ? link.createdAt : null) ??
256
+ linkIdByInteraction.get(linkId)?.occurredAt ??
257
+ new Date()
258
+ const sentAt = (sentAtRaw instanceof Date ? sentAtRaw : new Date(sentAtRaw)).toISOString()
259
+
260
+ const threadKey =
261
+ (typeof message?.threadId === 'string' && message.threadId ? message.threadId : null) ??
262
+ (messageId ? `message:${messageId}` : `link:${linkId}`)
263
+
264
+ const dto: PersonEmailMessage = {
265
+ id: linkId,
266
+ messageId,
267
+ rfcMessageId: firstString(primary?.messageId, secondary?.messageId),
268
+ references: toStringArray(primary?.references, secondary?.references),
269
+ direction,
270
+ fromName: fromList[0]?.name ?? null,
271
+ fromEmail: fromList[0]?.email ?? null,
272
+ to: toList.map((a) => a.email),
273
+ cc: ccList.map((a) => a.email),
274
+ subject,
275
+ bodyText,
276
+ sentAt,
277
+ providerKey,
278
+ }
279
+
280
+ const existing = threadsByKey.get(threadKey)
281
+ if (existing) {
282
+ existing.messages.push(dto)
283
+ } else {
284
+ threadsByKey.set(threadKey, {
285
+ threadKey,
286
+ subject,
287
+ preview: null,
288
+ participants: [],
289
+ lastMessageAt: sentAt,
290
+ messageCount: 0,
291
+ providerKey,
292
+ lastDirection: direction,
293
+ messages: [dto],
294
+ })
295
+ }
296
+ }
297
+
298
+ // ── (5) Finalize thread summaries ───────────────────────────────────────
299
+ const threads: PersonEmailThread[] = []
300
+ for (const thread of threadsByKey.values()) {
301
+ thread.messages.sort((a, b) => a.sentAt.localeCompare(b.sentAt))
302
+ if (thread.messages.length > maxMessagesPerThread) {
303
+ thread.messages = thread.messages.slice(thread.messages.length - maxMessagesPerThread)
304
+ }
305
+ const last = thread.messages[thread.messages.length - 1]
306
+ const firstWithSubject = thread.messages.find((m) => m.subject)
307
+ const participantSet = new Set<string>()
308
+ for (const message of thread.messages) {
309
+ // External counterpart = the "from" for inbound, the "to" for outbound.
310
+ if (message.direction === 'inbound' && message.fromEmail) participantSet.add(message.fromEmail)
311
+ if (message.direction === 'outbound') message.to.forEach((email) => participantSet.add(email))
312
+ }
313
+ thread.subject = firstWithSubject?.subject ?? thread.subject
314
+ thread.preview = truncate(last?.bodyText ?? null)
315
+ thread.participants = Array.from(participantSet)
316
+ thread.lastMessageAt = last?.sentAt ?? thread.lastMessageAt
317
+ thread.lastDirection = last?.direction ?? thread.lastDirection
318
+ thread.providerKey = last?.providerKey ?? thread.providerKey
319
+ thread.messageCount = thread.messages.length
320
+ threads.push(thread)
321
+ }
322
+
323
+ threads.sort((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt))
324
+ return threads.slice(0, maxThreads)
325
+ }
@@ -0,0 +1,152 @@
1
+ import type { FilterQuery } from '@mikro-orm/postgresql'
2
+ import { hasFeature } from '@open-mercato/shared/security/features'
3
+ import { CustomerInteraction } from '../data/entities'
4
+
5
+ /**
6
+ * The ACL feature that grants admins the right to see private emails authored
7
+ * by other users. Declared in `acl.ts` but granted to NO role in v1 (reserved
8
+ * for the v2 oversight feature — see `callerHasEmailViewPrivate`).
9
+ */
10
+ export const EMAIL_VIEW_PRIVATE_FEATURE = 'customers.email.view_private'
11
+
12
+ /**
13
+ * Returns true when the caller holds the admin override to see ALL private email
14
+ * interactions. Honours wildcards (`customers.*`, `*`).
15
+ *
16
+ * RESERVED FOR v2 — NOT wired in v1. The v1 model is strict owner-only with no
17
+ * admin bypass: the visibility filters and `canChangeEmailVisibility` ignore
18
+ * caller features, and `customers.email.view_private` is granted to no role.
19
+ * Kept (with {@link EMAIL_VIEW_PRIVATE_FEATURE}) so v2 oversight can opt back in
20
+ * without re-introducing the helper. Do NOT wire this into a read path without
21
+ * an explicit v2 spec.
22
+ */
23
+ export function callerHasEmailViewPrivate(userFeatures: string[] | null | undefined): boolean {
24
+ if (!Array.isArray(userFeatures) || userFeatures.length === 0) return false
25
+ return hasFeature(userFeatures, EMAIL_VIEW_PRIVATE_FEATURE)
26
+ }
27
+
28
+ /**
29
+ * Authorization predicate for CHANGING an email interaction's visibility.
30
+ *
31
+ * Personal mailbox privacy (v1: strict owner-only): ONLY the interaction's
32
+ * author may flip their own email between private/shared — there is no admin
33
+ * bypass. Non-email rows and no-op changes are always allowed. Mirrors the gate
34
+ * in the dedicated `PATCH .../visibility` route so the generic interaction-update
35
+ * path cannot bypass the privacy control. `userFeatures` is reserved for v2.
36
+ */
37
+ export function canChangeEmailVisibility(opts: {
38
+ interactionType: string
39
+ currentVisibility: string | null | undefined
40
+ nextVisibility: string | null | undefined
41
+ authorUserId: string | null | undefined
42
+ actorUserId: string | null | undefined
43
+ userFeatures: string[] | null | undefined
44
+ }): boolean {
45
+ if (opts.interactionType !== 'email') return true
46
+ if ((opts.nextVisibility ?? null) === (opts.currentVisibility ?? null)) return true
47
+ return Boolean(opts.actorUserId) && opts.authorUserId === opts.actorUserId
48
+ }
49
+
50
+ export interface ApplyEmailVisibilityFilterOptions {
51
+ currentUserId: string | null
52
+ userFeatures: string[] | null | undefined
53
+ }
54
+
55
+ /**
56
+ * Adds a `WHERE` predicate to a kysely query so that:
57
+ * - Non-email interactions (calls, meetings, tasks) pass through unchanged.
58
+ * - Email interactions with `visibility = 'shared'` are visible to all.
59
+ * - Email interactions with `visibility = 'private'` are visible ONLY to the
60
+ * `authorUserId` (channel owner).
61
+ *
62
+ * Personal mailbox privacy (v1: strict owner-only) — there is NO admin bypass:
63
+ * a private email is hidden from everyone except its author, including
64
+ * admins/superadmins. `opts.userFeatures` is retained for signature stability
65
+ * and reserved for the v2 admin-oversight feature.
66
+ *
67
+ * The function expects a kysely-style builder whose `.where()` accepts an
68
+ * expression-builder callback. Returns the same builder for chaining.
69
+ */
70
+ export function applyEmailVisibilityFilter<T extends { where: (...args: any[]) => T }>(
71
+ query: T,
72
+ opts: ApplyEmailVisibilityFilterOptions,
73
+ ): T {
74
+ const currentUserId = opts.currentUserId
75
+ // A row is hidden ONLY when it is an email explicitly marked `private` and the
76
+ // caller is not its author. Everything else stays visible, including:
77
+ // - non-email interactions (calls, meetings, tasks),
78
+ // - emails marked `shared`,
79
+ // - legacy/unset rows where `visibility IS NULL` (e.g. email-log entries
80
+ // created before per-email visibility shipped) — these must remain
81
+ // visible to avoid silently hiding pre-existing CRM history.
82
+ return query.where((eb: any) =>
83
+ eb.or([
84
+ eb('interaction_type', '!=', 'email'),
85
+ eb('visibility', 'is', null),
86
+ eb('visibility', '!=', 'private'),
87
+ currentUserId
88
+ ? eb('author_user_id', '=', currentUserId)
89
+ : eb.val(false),
90
+ ]),
91
+ )
92
+ }
93
+
94
+ type RbacServiceLike = {
95
+ getGrantedFeatures?: (
96
+ userId: string,
97
+ scope: { tenantId: string | null; organizationId: string | null },
98
+ ) => Promise<string[] | undefined>
99
+ }
100
+
101
+ /**
102
+ * Resolve the caller's granted features (wildcard-aware downstream) so a v2
103
+ * visibility filter could honour the `customers.email.view_private` admin
104
+ * override. Returns `undefined` when there is no user or the RBAC service is
105
+ * unavailable — callers MUST treat `undefined` as "no bypass" (fail closed).
106
+ *
107
+ * RESERVED FOR v2 — NOT called by any v1 read path. v1 is strict owner-only, so
108
+ * the read routes pass `userFeatures: undefined` to the filters rather than
109
+ * resolving features here (which would be a wasted RBAC round-trip). Re-wire
110
+ * only under an explicit v2 oversight spec.
111
+ */
112
+ export async function resolveCallerEmailFeatures(
113
+ container: { resolve: (name: string) => unknown },
114
+ userId: string | null,
115
+ tenantId: string | null,
116
+ organizationId: string | null,
117
+ ): Promise<string[] | undefined> {
118
+ if (!userId) return undefined
119
+ try {
120
+ const rbac = container.resolve('rbacService') as RbacServiceLike | undefined
121
+ if (!rbac?.getGrantedFeatures) return undefined
122
+ return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })
123
+ } catch {
124
+ return undefined
125
+ }
126
+ }
127
+
128
+ /**
129
+ * MikroORM equivalent of {@link applyEmailVisibilityFilter}. Returns a
130
+ * `FilterQuery` fragment to merge (implicit AND) into a `CustomerInteraction`
131
+ * where-clause so private email rows are excluded for non-owner, non-admin
132
+ * callers on MikroORM read paths (`findWithDecryption`/`em.find`/`em.count`).
133
+ *
134
+ * Mirrors the kysely predicate exactly, including the legacy `visibility IS NULL`
135
+ * passthrough so pre-existing CRM history is never hidden. Personal mailbox
136
+ * privacy (v1: strict owner-only): no admin bypass — a private email is hidden
137
+ * from everyone except its author. `opts.userFeatures` is reserved for v2.
138
+ */
139
+ export type EmailVisibilityMikroFilter = { $or?: FilterQuery<CustomerInteraction>[] }
140
+
141
+ export function buildEmailVisibilityMikroFilter(
142
+ opts: ApplyEmailVisibilityFilterOptions,
143
+ ): EmailVisibilityMikroFilter {
144
+ return {
145
+ $or: [
146
+ { interactionType: { $ne: 'email' } },
147
+ { visibility: null },
148
+ { visibility: { $ne: 'private' } },
149
+ ...(opts.currentUserId ? [{ authorUserId: opts.currentUserId }] : []),
150
+ ],
151
+ }
152
+ }
@@ -3703,6 +3703,23 @@
3703
3703
  "enumItems": [],
3704
3704
  "mappedType": "text"
3705
3705
  },
3706
+ "channel_provider_key": {
3707
+ "name": "channel_provider_key",
3708
+ "type": "text",
3709
+ "unsigned": false,
3710
+ "autoincrement": false,
3711
+ "primary": false,
3712
+ "nullable": true,
3713
+ "unique": false,
3714
+ "length": null,
3715
+ "precision": null,
3716
+ "scale": null,
3717
+ "default": null,
3718
+ "comment": null,
3719
+ "collation": null,
3720
+ "enumItems": [],
3721
+ "mappedType": "text"
3722
+ },
3706
3723
  "created_at": {
3707
3724
  "name": "created_at",
3708
3725
  "type": "timestamptz(6)",
@@ -3788,6 +3805,23 @@
3788
3805
  "enumItems": [],
3789
3806
  "mappedType": "uuid"
3790
3807
  },
3808
+ "external_message_id": {
3809
+ "name": "external_message_id",
3810
+ "type": "uuid",
3811
+ "unsigned": false,
3812
+ "autoincrement": false,
3813
+ "primary": false,
3814
+ "nullable": true,
3815
+ "unique": false,
3816
+ "length": null,
3817
+ "precision": null,
3818
+ "scale": null,
3819
+ "default": null,
3820
+ "comment": null,
3821
+ "collation": null,
3822
+ "enumItems": [],
3823
+ "mappedType": "uuid"
3824
+ },
3791
3825
  "guest_permissions": {
3792
3826
  "name": "guest_permissions",
3793
3827
  "type": "jsonb",
@@ -4194,6 +4228,33 @@
4194
4228
  "keyName": "customer_interactions_type_idx",
4195
4229
  "primary": false,
4196
4230
  "unique": false
4231
+ },
4232
+ {
4233
+ "columnNames": [],
4234
+ "composite": false,
4235
+ "constraint": false,
4236
+ "keyName": "customer_interactions_external_msg_idx",
4237
+ "primary": false,
4238
+ "unique": false,
4239
+ "expression": "create index \"customer_interactions_external_msg_idx\" on \"customer_interactions\" (\"external_message_id\") where \"external_message_id\" is not null"
4240
+ },
4241
+ {
4242
+ "columnNames": [],
4243
+ "composite": false,
4244
+ "constraint": false,
4245
+ "keyName": "customer_interactions_email_dedupe_uq",
4246
+ "primary": false,
4247
+ "unique": false,
4248
+ "expression": "create unique index \"customer_interactions_email_dedupe_uq\" on \"customer_interactions\" (\"entity_id\", \"external_message_id\") where \"external_message_id\" is not null and \"deleted_at\" is null"
4249
+ },
4250
+ {
4251
+ "columnNames": [],
4252
+ "composite": false,
4253
+ "constraint": false,
4254
+ "keyName": "customer_interactions_email_visibility_idx",
4255
+ "primary": false,
4256
+ "unique": false,
4257
+ "expression": "create index \"customer_interactions_email_visibility_idx\" on \"customer_interactions\" (\"entity_id\", \"interaction_type\", \"visibility\", \"author_user_id\") where \"interaction_type\" = 'email' and \"deleted_at\" is null"
4197
4258
  }
4198
4259
  ],
4199
4260
  "checks": [],
@@ -0,0 +1,23 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260527012240_customers extends Migration {
4
+
5
+ override up(): void | Promise<void> {
6
+ this.addSql(`alter table "customer_interactions" add "external_message_id" uuid null, add "channel_provider_key" text null;`);
7
+ // Backfill: pre-existing email-log interactions predate per-email visibility
8
+ // and have null visibility. Mark them 'shared' so they keep their prior
9
+ // visible-to-all behavior once the visibility filter is enabled.
10
+ this.addSql(`update "customer_interactions" set "visibility" = 'shared' where "interaction_type" = 'email' and "visibility" is null and "external_message_id" is null;`);
11
+ this.addSql(`create index "customer_interactions_external_msg_idx" on "customer_interactions" ("external_message_id") where "external_message_id" is not null;`);
12
+ this.addSql(`create unique index "customer_interactions_email_dedupe_uq" on "customer_interactions" ("entity_id", "external_message_id") where "external_message_id" is not null and "deleted_at" is null;`);
13
+ this.addSql(`create index "customer_interactions_email_visibility_idx" on "customer_interactions" ("entity_id", "interaction_type", "visibility", "author_user_id") where "interaction_type" = 'email' and "deleted_at" is null;`);
14
+ }
15
+
16
+ override down(): void | Promise<void> {
17
+ this.addSql(`drop index if exists "customer_interactions_email_visibility_idx";`);
18
+ this.addSql(`drop index if exists "customer_interactions_email_dedupe_uq";`);
19
+ this.addSql(`drop index if exists "customer_interactions_external_msg_idx";`);
20
+ this.addSql(`alter table "customer_interactions" drop column "external_message_id", drop column "channel_provider_key";`);
21
+ }
22
+
23
+ }
@@ -102,6 +102,7 @@ export const setup: ModuleSetupConfig = {
102
102
  'customers.widgets.new-deals',
103
103
  'customers.roles.view',
104
104
  'customers.roles.manage',
105
+ 'customers.email.compose',
105
106
  ],
106
107
  },
107
108
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Persistent subscriber for `communication_channels.message.received`.
3
+ *
4
+ * When an inbound email arrives, resolves People by address from the linked
5
+ * MessageChannelLink and creates CustomerInteraction rows (one per match).
6
+ * Falls back to threading-inheritance (In-Reply-To chain) when no direct
7
+ * address match is found.
8
+ *
9
+ * Logic lives in `../lib/link-channel-message-handler.ts`; both this file
10
+ * and `link-channel-message-sent.ts` delegate there so the two subscriber
11
+ * registrations can share a single implementation.
12
+ */
13
+ import linkChannelMessageHandler from '../lib/link-channel-message-handler'
14
+
15
+ export const metadata = {
16
+ event: 'communication_channels.message.received',
17
+ persistent: true,
18
+ id: 'customers:link-channel-message-received',
19
+ }
20
+
21
+ export default linkChannelMessageHandler
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Persistent subscriber for `communication_channels.message.sent`.
3
+ *
4
+ * When an outbound email is delivered, resolves People by address (or by the
5
+ * `crmPersonId` hint written by the compose route) and creates CustomerInteraction
6
+ * rows. Reads `crmVisibility` from `channelMetadata` to set the per-row
7
+ * visibility ('private' | 'shared').
8
+ *
9
+ * Logic lives in `../lib/link-channel-message-handler.ts`; both this file
10
+ * and `link-channel-message-received.ts` delegate there so the two subscriber
11
+ * registrations can share a single implementation.
12
+ */
13
+ import linkChannelMessageHandler from '../lib/link-channel-message-handler'
14
+
15
+ export const metadata = {
16
+ event: 'communication_channels.message.sent',
17
+ persistent: true,
18
+ id: 'customers:link-channel-message-sent',
19
+ }
20
+
21
+ export default linkChannelMessageHandler
@@ -132,6 +132,15 @@ For platform connectors with multiple integrations (e.g., MedusaJS):
132
132
  2. If `bundleId` is set, fallback to bundle's credentials
133
133
  3. Return `null` if neither exists
134
134
 
135
+ ## Per-User Credential Scoping
136
+
137
+ `IntegrationScope` carries an optional `userId?: string | null` (added 2026-05-26 for per-user email channels). Every `createCredentialsService` method scopes by it:
138
+
139
+ - **Omit `scope.userId`** (or pass `null`) for tenant-wide credentials (shared API keys, e.g. Stripe/Akeneo) — the filter pins `user_id IS NULL`, the historical behaviour.
140
+ - **Pass `scope.userId`** for per-user credentials (Gmail/IMAP mailboxes) — reads and writes land on that user's own row.
141
+
142
+ Uniqueness across `(integration_id, organization_id, tenant_id, user_id)` is enforced by the partial unique index `integration_credentials_user_lookup_idx` (`WHERE user_id IS NOT NULL AND deleted_at IS NULL`). **Callers MUST thread the correct `userId` on every per-user read AND write** — a tenant-wide scope can never read a user-scoped row and vice versa, so a missing `userId` silently resolves the wrong (or no) credentials.
143
+
135
144
  ## Events
136
145
 
137
146
  | Event ID | Emitted When |