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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
  3. package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
  4. package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
  5. package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
  6. package/dist/generated/entities/channel_thread_token/index.js +17 -0
  7. package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
  8. package/dist/generated/entities/communication_channel/index.js +43 -0
  9. package/dist/generated/entities/communication_channel/index.js.map +7 -0
  10. package/dist/generated/entities/customer_interaction/index.js +4 -0
  11. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  12. package/dist/generated/entities/external_conversation/index.js +25 -0
  13. package/dist/generated/entities/external_conversation/index.js.map +7 -0
  14. package/dist/generated/entities/external_message/index.js +25 -0
  15. package/dist/generated/entities/external_message/index.js.map +7 -0
  16. package/dist/generated/entities/integration_credentials/index.js +3 -1
  17. package/dist/generated/entities/integration_credentials/index.js.map +2 -2
  18. package/dist/generated/entities/message/index.js +2 -0
  19. package/dist/generated/entities/message/index.js.map +2 -2
  20. package/dist/generated/entities/message_channel_link/index.js +33 -0
  21. package/dist/generated/entities/message_channel_link/index.js.map +7 -0
  22. package/dist/generated/entities/message_reaction/index.js +25 -0
  23. package/dist/generated/entities/message_reaction/index.js.map +7 -0
  24. package/dist/generated/entities.ids.generated.js +11 -0
  25. package/dist/generated/entities.ids.generated.js.map +2 -2
  26. package/dist/generated/entity-fields-registry.js +117 -0
  27. package/dist/generated/entity-fields-registry.js.map +2 -2
  28. package/dist/helpers/integration/authFixtures.js +2 -1
  29. package/dist/helpers/integration/authFixtures.js.map +2 -2
  30. package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
  31. package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
  32. package/dist/modules/communication_channels/acl.js +47 -0
  33. package/dist/modules/communication_channels/acl.js.map +7 -0
  34. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
  35. package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
  36. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
  37. package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
  38. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
  39. package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
  40. package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
  41. package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
  42. package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
  43. package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
  44. package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
  45. package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
  46. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
  47. package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
  48. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
  49. package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
  50. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
  51. package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
  52. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
  53. package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
  54. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
  55. package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
  56. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
  57. package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
  58. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
  59. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
  60. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
  61. package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
  62. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
  63. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
  64. package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
  65. package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
  66. package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
  67. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
  68. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
  69. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
  70. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
  71. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
  72. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
  73. package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
  74. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
  75. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
  76. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
  77. package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
  78. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
  79. package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
  80. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
  81. package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
  82. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
  83. package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
  84. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
  85. package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
  86. package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
  87. package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
  88. package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
  89. package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
  90. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
  91. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
  92. package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
  93. package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
  94. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
  95. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
  96. package/dist/modules/communication_channels/commands/interceptors.js +68 -0
  97. package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
  98. package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
  99. package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
  100. package/dist/modules/communication_channels/commands/push-register.js +146 -0
  101. package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
  102. package/dist/modules/communication_channels/commands/push-renew.js +23 -0
  103. package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
  104. package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
  105. package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
  106. package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
  107. package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
  108. package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
  109. package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
  110. package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
  111. package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
  112. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
  113. package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
  114. package/dist/modules/communication_channels/data/enrichers.js +286 -0
  115. package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
  116. package/dist/modules/communication_channels/data/entities.js +447 -0
  117. package/dist/modules/communication_channels/data/entities.js.map +7 -0
  118. package/dist/modules/communication_channels/data/extensions.js +67 -0
  119. package/dist/modules/communication_channels/data/extensions.js.map +7 -0
  120. package/dist/modules/communication_channels/data/validators.js +123 -0
  121. package/dist/modules/communication_channels/data/validators.js.map +7 -0
  122. package/dist/modules/communication_channels/di.js +35 -0
  123. package/dist/modules/communication_channels/di.js.map +7 -0
  124. package/dist/modules/communication_channels/encryption.js +12 -0
  125. package/dist/modules/communication_channels/encryption.js.map +7 -0
  126. package/dist/modules/communication_channels/events.js +124 -0
  127. package/dist/modules/communication_channels/events.js.map +7 -0
  128. package/dist/modules/communication_channels/index.js +20 -0
  129. package/dist/modules/communication_channels/index.js.map +7 -0
  130. package/dist/modules/communication_channels/lib/access-control.js +43 -0
  131. package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
  132. package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
  133. package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
  134. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
  135. package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
  136. package/dist/modules/communication_channels/lib/adapter.js +1 -0
  137. package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
  138. package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
  139. package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
  140. package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
  141. package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
  142. package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
  143. package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
  144. package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
  145. package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
  146. package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
  147. package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
  148. package/dist/modules/communication_channels/lib/email-contact.js +14 -0
  149. package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
  150. package/dist/modules/communication_channels/lib/email-mime.js +269 -0
  151. package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
  152. package/dist/modules/communication_channels/lib/error-classification.js +101 -0
  153. package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
  154. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
  155. package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
  156. package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
  157. package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
  158. package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
  159. package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
  160. package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
  161. package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
  162. package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
  163. package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
  164. package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
  165. package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
  166. package/dist/modules/communication_channels/lib/provider-health.js +24 -0
  167. package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
  168. package/dist/modules/communication_channels/lib/push-state.js +19 -0
  169. package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
  170. package/dist/modules/communication_channels/lib/queue.js +54 -0
  171. package/dist/modules/communication_channels/lib/queue.js.map +7 -0
  172. package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
  173. package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
  174. package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
  175. package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
  176. package/dist/modules/communication_channels/lib/registry.js +67 -0
  177. package/dist/modules/communication_channels/lib/registry.js.map +7 -0
  178. package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
  179. package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
  180. package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
  181. package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
  182. package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
  183. package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
  184. package/dist/modules/communication_channels/lib/system-user.js +22 -0
  185. package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
  186. package/dist/modules/communication_channels/lib/test-seed.js +68 -0
  187. package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
  188. package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
  189. package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
  190. package/dist/modules/communication_channels/lib/thread-token.js +219 -0
  191. package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
  192. package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
  193. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
  194. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
  195. package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
  196. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
  197. package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
  198. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
  199. package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
  200. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
  201. package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
  202. package/dist/modules/communication_channels/notifications.client.js +51 -0
  203. package/dist/modules/communication_channels/notifications.client.js.map +7 -0
  204. package/dist/modules/communication_channels/notifications.handlers.js +53 -0
  205. package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
  206. package/dist/modules/communication_channels/notifications.js +56 -0
  207. package/dist/modules/communication_channels/notifications.js.map +7 -0
  208. package/dist/modules/communication_channels/setup.js +105 -0
  209. package/dist/modules/communication_channels/setup.js.map +7 -0
  210. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
  211. package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
  212. package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
  213. package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
  214. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
  215. package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
  216. package/dist/modules/communication_channels/widgets/components.js +7 -0
  217. package/dist/modules/communication_channels/widgets/components.js.map +7 -0
  218. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
  219. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
  220. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
  221. package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
  222. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
  223. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
  224. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
  225. package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
  226. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
  227. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
  228. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
  229. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
  230. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
  231. package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
  232. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
  233. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
  234. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
  235. package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
  236. package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
  237. package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
  238. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
  239. package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
  240. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
  241. package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
  242. package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
  243. package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
  244. package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
  245. package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
  246. package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
  247. package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
  248. package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
  249. package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
  250. package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
  251. package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
  252. package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
  253. package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
  254. package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
  255. package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
  256. package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
  257. package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
  258. package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
  259. package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
  260. package/dist/modules/customers/acl.js +18 -0
  261. package/dist/modules/customers/acl.js.map +2 -2
  262. package/dist/modules/customers/api/activities/route.js +9 -0
  263. package/dist/modules/customers/api/activities/route.js.map +2 -2
  264. package/dist/modules/customers/api/companies/[id]/route.js +18 -7
  265. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  266. package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
  267. package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
  268. package/dist/modules/customers/api/interactions/counts/route.js +6 -0
  269. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  270. package/dist/modules/customers/api/interactions/route.js +26 -7
  271. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  272. package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
  273. package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
  274. package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
  275. package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
  276. package/dist/modules/customers/api/people/[id]/route.js +12 -4
  277. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  278. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
  279. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  280. package/dist/modules/customers/commands/deals.js +46 -5
  281. package/dist/modules/customers/commands/deals.js.map +2 -2
  282. package/dist/modules/customers/commands/interactions.js +16 -0
  283. package/dist/modules/customers/commands/interactions.js.map +2 -2
  284. package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
  285. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  286. package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
  287. package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
  288. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  289. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  290. package/dist/modules/customers/components/detail/DealsSection.js +10 -0
  291. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  292. package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
  293. package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
  294. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
  295. package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
  296. package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
  297. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  298. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
  299. package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
  300. package/dist/modules/customers/data/enrichers.js +133 -2
  301. package/dist/modules/customers/data/enrichers.js.map +2 -2
  302. package/dist/modules/customers/data/entities.js +18 -0
  303. package/dist/modules/customers/data/entities.js.map +2 -2
  304. package/dist/modules/customers/data/extensions.js +16 -0
  305. package/dist/modules/customers/data/extensions.js.map +7 -0
  306. package/dist/modules/customers/encryption.js +11 -0
  307. package/dist/modules/customers/encryption.js.map +2 -2
  308. package/dist/modules/customers/events.js +4 -1
  309. package/dist/modules/customers/events.js.map +2 -2
  310. package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
  311. package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
  312. package/dist/modules/customers/lib/kysely.js.map +2 -2
  313. package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
  314. package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
  315. package/dist/modules/customers/lib/personEmailThreads.js +205 -0
  316. package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
  317. package/dist/modules/customers/lib/visibilityFilter.js +51 -0
  318. package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
  319. package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
  320. package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
  321. package/dist/modules/customers/setup.js +2 -1
  322. package/dist/modules/customers/setup.js.map +2 -2
  323. package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
  324. package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
  325. package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
  326. package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
  327. package/dist/modules/integrations/data/entities.js +8 -1
  328. package/dist/modules/integrations/data/entities.js.map +2 -2
  329. package/dist/modules/integrations/lib/credentials-service.js +29 -14
  330. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  331. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
  332. package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
  333. package/dist/modules/messages/commands/messages.js +70 -8
  334. package/dist/modules/messages/commands/messages.js.map +2 -2
  335. package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
  336. package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
  337. package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
  338. package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
  339. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
  340. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  341. package/dist/modules/messages/data/entities.js +8 -1
  342. package/dist/modules/messages/data/entities.js.map +2 -2
  343. package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
  344. package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
  345. package/dist/modules/messages/widgets/injection-table.js +7 -0
  346. package/dist/modules/messages/widgets/injection-table.js.map +7 -0
  347. package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
  348. package/generated/entities/channel_thread_mapping/index.ts +11 -0
  349. package/generated/entities/channel_thread_token/index.ts +7 -0
  350. package/generated/entities/communication_channel/index.ts +20 -0
  351. package/generated/entities/customer_interaction/index.ts +2 -0
  352. package/generated/entities/external_conversation/index.ts +11 -0
  353. package/generated/entities/external_message/index.ts +11 -0
  354. package/generated/entities/integration_credentials/index.ts +1 -0
  355. package/generated/entities/message/index.ts +1 -0
  356. package/generated/entities/message_channel_link/index.ts +15 -0
  357. package/generated/entities/message_reaction/index.ts +11 -0
  358. package/generated/entities.ids.generated.ts +11 -0
  359. package/generated/entity-fields-registry.ts +117 -0
  360. package/package.json +9 -7
  361. package/src/helpers/integration/authFixtures.ts +4 -1
  362. package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
  363. package/src/modules/communication_channels/acl.ts +43 -0
  364. package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
  365. package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
  366. package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
  367. package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
  368. package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
  369. package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
  370. package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
  371. package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
  372. package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
  373. package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
  374. package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
  375. package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
  376. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
  377. package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
  378. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
  379. package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
  380. package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
  381. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
  382. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
  383. package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
  384. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
  385. package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
  386. package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
  387. package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
  388. package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
  389. package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
  390. package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
  391. package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
  392. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
  393. package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
  394. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
  395. package/src/modules/communication_channels/commands/interceptors.ts +104 -0
  396. package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
  397. package/src/modules/communication_channels/commands/push-register.ts +203 -0
  398. package/src/modules/communication_channels/commands/push-renew.ts +49 -0
  399. package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
  400. package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
  401. package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
  402. package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
  403. package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
  404. package/src/modules/communication_channels/data/enrichers.ts +413 -0
  405. package/src/modules/communication_channels/data/entities.ts +546 -0
  406. package/src/modules/communication_channels/data/extensions.ts +76 -0
  407. package/src/modules/communication_channels/data/validators.ts +138 -0
  408. package/src/modules/communication_channels/di.ts +40 -0
  409. package/src/modules/communication_channels/encryption.ts +44 -0
  410. package/src/modules/communication_channels/events.ts +122 -0
  411. package/src/modules/communication_channels/i18n/de.json +138 -0
  412. package/src/modules/communication_channels/i18n/en.json +138 -0
  413. package/src/modules/communication_channels/i18n/es.json +138 -0
  414. package/src/modules/communication_channels/i18n/pl.json +138 -0
  415. package/src/modules/communication_channels/index.ts +19 -0
  416. package/src/modules/communication_channels/lib/access-control.ts +110 -0
  417. package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
  418. package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
  419. package/src/modules/communication_channels/lib/adapter.ts +605 -0
  420. package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
  421. package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
  422. package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
  423. package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
  424. package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
  425. package/src/modules/communication_channels/lib/email-contact.ts +17 -0
  426. package/src/modules/communication_channels/lib/email-mime.ts +442 -0
  427. package/src/modules/communication_channels/lib/error-classification.ts +144 -0
  428. package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
  429. package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
  430. package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
  431. package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
  432. package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
  433. package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
  434. package/src/modules/communication_channels/lib/provider-health.ts +47 -0
  435. package/src/modules/communication_channels/lib/push-state.ts +38 -0
  436. package/src/modules/communication_channels/lib/queue.ts +66 -0
  437. package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
  438. package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
  439. package/src/modules/communication_channels/lib/registry.ts +99 -0
  440. package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
  441. package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
  442. package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
  443. package/src/modules/communication_channels/lib/system-user.ts +74 -0
  444. package/src/modules/communication_channels/lib/test-seed.ts +140 -0
  445. package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
  446. package/src/modules/communication_channels/lib/thread-token.ts +355 -0
  447. package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
  448. package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
  449. package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
  450. package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
  451. package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
  452. package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
  453. package/src/modules/communication_channels/notifications.client.ts +50 -0
  454. package/src/modules/communication_channels/notifications.handlers.ts +86 -0
  455. package/src/modules/communication_channels/notifications.ts +52 -0
  456. package/src/modules/communication_channels/setup.ts +158 -0
  457. package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
  458. package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
  459. package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
  460. package/src/modules/communication_channels/widgets/components.ts +36 -0
  461. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
  462. package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
  463. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
  464. package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
  465. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
  466. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
  467. package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
  468. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
  469. package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
  470. package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
  471. package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
  472. package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
  473. package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
  474. package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
  475. package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
  476. package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
  477. package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
  478. package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
  479. package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
  480. package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
  481. package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
  482. package/src/modules/customers/acl.ts +18 -0
  483. package/src/modules/customers/api/activities/route.ts +13 -0
  484. package/src/modules/customers/api/companies/[id]/route.ts +21 -1
  485. package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
  486. package/src/modules/customers/api/interactions/counts/route.ts +10 -0
  487. package/src/modules/customers/api/interactions/route.ts +51 -5
  488. package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
  489. package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
  490. package/src/modules/customers/api/people/[id]/route.ts +17 -2
  491. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
  492. package/src/modules/customers/commands/deals.ts +65 -6
  493. package/src/modules/customers/commands/interactions.ts +30 -0
  494. package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
  495. package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
  496. package/src/modules/customers/components/detail/DealForm.tsx +2 -1
  497. package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
  498. package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
  499. package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
  500. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
  501. package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
  502. package/src/modules/customers/data/enrichers.ts +252 -1
  503. package/src/modules/customers/data/entities.ts +46 -1
  504. package/src/modules/customers/data/extensions.ts +26 -0
  505. package/src/modules/customers/encryption.ts +11 -0
  506. package/src/modules/customers/events.ts +4 -0
  507. package/src/modules/customers/i18n/de.json +41 -0
  508. package/src/modules/customers/i18n/en.json +41 -0
  509. package/src/modules/customers/i18n/es.json +41 -0
  510. package/src/modules/customers/i18n/pl.json +41 -0
  511. package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
  512. package/src/modules/customers/lib/kysely.ts +16 -0
  513. package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
  514. package/src/modules/customers/lib/personEmailThreads.ts +325 -0
  515. package/src/modules/customers/lib/visibilityFilter.ts +152 -0
  516. package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
  517. package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
  518. package/src/modules/customers/setup.ts +1 -0
  519. package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
  520. package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
  521. package/src/modules/integrations/AGENTS.md +9 -0
  522. package/src/modules/integrations/data/entities.ts +21 -1
  523. package/src/modules/integrations/lib/credentials-service.ts +49 -13
  524. package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
  525. package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
  526. package/src/modules/messages/commands/messages.ts +101 -8
  527. package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
  528. package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
  529. package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
  530. package/src/modules/messages/data/entities.ts +11 -0
  531. package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
  532. package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
  533. package/src/modules/messages/widgets/injection-table.ts +29 -0
@@ -0,0 +1,68 @@
1
+ import type { AwilixContainer } from 'awilix'
2
+ import {
3
+ runCrudMutationGuardAfterSuccess,
4
+ validateCrudMutationGuard,
5
+ type CrudMutationGuardValidationResult,
6
+ } from '@open-mercato/shared/lib/crud/mutation-guard'
7
+
8
+ type RouteAuth = {
9
+ sub?: string | null
10
+ tenantId?: string | null
11
+ orgId?: string | null
12
+ }
13
+
14
+ type RouteMutationGuardInput = {
15
+ resourceKind: string
16
+ resourceId: string
17
+ operation?: 'create' | 'update' | 'delete' | 'custom'
18
+ mutationPayload?: Record<string, unknown> | null
19
+ }
20
+
21
+ export type RouteMutationGuardContext = {
22
+ result: CrudMutationGuardValidationResult | null
23
+ afterSuccess: () => Promise<void>
24
+ }
25
+
26
+ export async function validateRouteMutationGuard(params: {
27
+ container: AwilixContainer
28
+ req: Request
29
+ auth: RouteAuth
30
+ input: RouteMutationGuardInput
31
+ }): Promise<RouteMutationGuardContext | { response: Response }> {
32
+ const { auth, container, input, req } = params
33
+ if (!auth.sub || !auth.tenantId) {
34
+ return { result: null, afterSuccess: async () => undefined }
35
+ }
36
+
37
+ const base = {
38
+ tenantId: auth.tenantId,
39
+ organizationId: auth.orgId ?? null,
40
+ userId: auth.sub,
41
+ resourceKind: input.resourceKind,
42
+ resourceId: input.resourceId,
43
+ operation: input.operation ?? 'custom',
44
+ requestMethod: req.method,
45
+ requestHeaders: req.headers,
46
+ } as const
47
+
48
+ const result = await validateCrudMutationGuard(container, {
49
+ ...base,
50
+ mutationPayload: input.mutationPayload ?? null,
51
+ })
52
+ if (result && !result.ok) {
53
+ return {
54
+ response: Response.json(result.body, { status: result.status }),
55
+ }
56
+ }
57
+
58
+ return {
59
+ result,
60
+ afterSuccess: async () => {
61
+ if (!result?.ok || !result.shouldRunAfterSuccess) return
62
+ await runCrudMutationGuardAfterSuccess(container, {
63
+ ...base,
64
+ metadata: result.metadata ?? null,
65
+ })
66
+ },
67
+ }
68
+ }
@@ -0,0 +1,129 @@
1
+ import sanitizeHtml from 'sanitize-html'
2
+
3
+ /**
4
+ * HTML sanitizer for channel-supplied payloads (email MIME, Slack rich-text, future channel types).
5
+ *
6
+ * The hub owns this helper; every widget that renders channel HTML — in the hub itself or in
7
+ * downstream provider packages — imports it. The Messages module's `channel-payload-renderer`
8
+ * widget calls this before any `dangerouslySetInnerHTML`-style render.
9
+ *
10
+ * Implementation note: backed by `sanitize-html` (CommonJS, server-friendly) rather than
11
+ * DOMPurify so the same code runs in Jest without ESM transform gymnastics. SPEC-045d §4.6
12
+ * specifies "DOMPurify or equivalent" — `sanitize-html` is an allowlist-based equivalent
13
+ * widely used for the same sanitization shape.
14
+ *
15
+ * Allowlist is tuned for email + chat HTML:
16
+ * - Preserves typical email layout primitives (table-based layouts, inline images, basic typography).
17
+ * - Strips `<script>`, `<style>`, `<iframe>`, all event-handler attributes (`on*`).
18
+ * - Blocks `javascript:` and `data:` URLs except `data:image/*;base64,…` (inline base64 images, common in email).
19
+ */
20
+
21
+ const ALLOWED_TAGS = [
22
+ 'a',
23
+ 'img',
24
+ 'table',
25
+ 'thead',
26
+ 'tbody',
27
+ 'tfoot',
28
+ 'tr',
29
+ 'td',
30
+ 'th',
31
+ 'p',
32
+ 'br',
33
+ 'hr',
34
+ 'ul',
35
+ 'ol',
36
+ 'li',
37
+ 'h1',
38
+ 'h2',
39
+ 'h3',
40
+ 'h4',
41
+ 'h5',
42
+ 'h6',
43
+ 'strong',
44
+ 'em',
45
+ 'b',
46
+ 'i',
47
+ 'u',
48
+ 's',
49
+ 'blockquote',
50
+ 'code',
51
+ 'pre',
52
+ 'span',
53
+ 'div',
54
+ ]
55
+
56
+ const ALLOWED_SCHEMES = ['http', 'https', 'mailto', 'tel']
57
+ const ALLOWED_SCHEMES_BY_TAG = {
58
+ img: ['http', 'https', 'data'],
59
+ }
60
+
61
+ /**
62
+ * Safe CSS color value shapes only: hex (#rgb / #rgba / #rrggbb / #rrggbbaa),
63
+ * rgb()/rgba(), hsl()/hsla(), and bare named colors (`red`, `transparent`,
64
+ * `currentcolor`, …). Deliberately rejects any value containing `url(` or
65
+ * `expression(` so a `color`/`background-color` declaration can never smuggle a
66
+ * CSS-based tracking beacon or legacy-IE script expression past the sanitizer.
67
+ */
68
+ const SAFE_CSS_COLOR =
69
+ /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\(\s*[\d.,\s%]+\)$|^hsla?\(\s*[\d.,\s%]+\)$|^[a-z]+$/i
70
+
71
+ const SANITIZE_OPTIONS: sanitizeHtml.IOptions = {
72
+ allowedTags: ALLOWED_TAGS,
73
+ allowedAttributes: {
74
+ a: ['href', 'title', 'class'],
75
+ img: ['src', 'alt', 'title', 'class', 'width', 'height'],
76
+ '*': ['class', 'style', 'colspan', 'rowspan', 'width', 'height'],
77
+ },
78
+ allowedSchemes: ALLOWED_SCHEMES,
79
+ allowedSchemesByTag: ALLOWED_SCHEMES_BY_TAG,
80
+ allowedSchemesAppliedToAttributes: ['href', 'src'],
81
+ allowProtocolRelative: false,
82
+ // disable inline scripts in style attributes
83
+ allowedStyles: {
84
+ '*': {
85
+ 'background-color': [SAFE_CSS_COLOR],
86
+ color: [SAFE_CSS_COLOR],
87
+ 'text-align': [/^left$|^right$|^center$|^justify$/i],
88
+ 'font-size': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
89
+ 'font-weight': [/^\d{3}$|^bold$|^normal$/],
90
+ 'font-style': [/^italic$|^normal$/],
91
+ 'text-decoration': [/^underline$|^line-through$|^none$/],
92
+ padding: [/^[\d\s.]+(?:px|em|rem|%)?$/],
93
+ margin: [/^[\d\s.]+(?:px|em|rem|%)?$/],
94
+ border: [/^[\d\s\w.]+$/],
95
+ 'border-radius': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
96
+ // Permit only `display:none` so the hidden thread-token footer span
97
+ // (`buildBodyFooter`) stays hidden when a sent email body is re-rendered.
98
+ // Every other display value is stripped.
99
+ display: [/^none$/],
100
+ },
101
+ },
102
+ /**
103
+ * data:image/<mime>;base64,... URLs in <img src=...> are explicitly allowed so inline
104
+ * email images render. sanitize-html validates the base64 payload shape; if a future
105
+ * payload tries to smuggle a non-image MIME type via data URL we strip it.
106
+ */
107
+ exclusiveFilter: (frame) => {
108
+ if (frame.tag === 'img' && frame.attribs?.src) {
109
+ const src = frame.attribs.src
110
+ if (src.startsWith('data:') && !/^data:image\/(?:png|jpe?g|gif|webp);base64,/i.test(src)) {
111
+ return true
112
+ }
113
+ }
114
+ return false
115
+ },
116
+ }
117
+
118
+ /**
119
+ * Sanitize an HTML string for safe rendering.
120
+ *
121
+ * @param html Raw HTML — may originate from an external channel (email body, Slack mrkdwn rendered to HTML, …).
122
+ * @returns Sanitized HTML safe for `dangerouslySetInnerHTML`.
123
+ */
124
+ export function sanitizeChannelHtml(html: string): string {
125
+ if (!html) return ''
126
+ return sanitizeHtml(html, SANITIZE_OPTIONS)
127
+ }
128
+
129
+ export default sanitizeChannelHtml
@@ -0,0 +1,284 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import {
6
+ ChannelThreadMapping,
7
+ CommunicationChannel,
8
+ ExternalConversation,
9
+ MessageChannelLink,
10
+ } from '../data/entities'
11
+ import { ChannelMutationBlockedError, guardOutboundCreate } from './mutation-guards'
12
+ import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from './queue'
13
+ import type { OutboundDeliveryPayload } from '../workers/outbound-delivery'
14
+ import { htmlToText } from './email-mime'
15
+
16
+ export type SendAsUserActor = {
17
+ userId: string
18
+ tenantId: string
19
+ organizationId: string | null
20
+ /** Forwarded as the messages compose command `ctx.auth`. */
21
+ auth?: unknown
22
+ }
23
+
24
+ export type SendAsUserInput = {
25
+ userChannelId: string
26
+ to: string[]
27
+ cc?: string[]
28
+ bcc?: string[]
29
+ subject: string
30
+ body: { plain?: string; html?: string }
31
+ inReplyTo?: string
32
+ references?: string[]
33
+ /**
34
+ * Open Mercato `messages.message` id of the message being replied to. When
35
+ * set, the composed message joins that message's thread (the messages module
36
+ * derives `threadId` from the parent), so a CRM reply continues the existing
37
+ * conversation instead of starting a new thread. Optional — omitted for new
38
+ * threads.
39
+ */
40
+ parentMessageId?: string
41
+ /**
42
+ * Free-form metadata persisted on the resulting MessageChannelLink. Used by
43
+ * downstream subscribers (e.g. the customers module's link-channel-message
44
+ * subscriber) to anchor the sent message back to a CRM Person or honor a
45
+ * caller-specified visibility flag. The hub does not interpret these keys.
46
+ */
47
+ channelMetadata?: Record<string, unknown>
48
+ }
49
+
50
+ export type SendAsUserResult =
51
+ | {
52
+ ok: true
53
+ messageId: string
54
+ threadId: string
55
+ channelId: string
56
+ providerKey: string
57
+ }
58
+ | { ok: false; status: number; error: string; fieldErrors?: Record<string, string> }
59
+
60
+ /**
61
+ * In-process send-as-user facade.
62
+ *
63
+ * Validates the actor owns `userChannelId`, composes the Message via the
64
+ * messages module command, persists the outbound MessageChannelLink + thread
65
+ * mapping, and enqueues delivery. Returns a discriminated result instead of an
66
+ * HTTP Response so it can be invoked both by the `/send-as-user` route and by
67
+ * other modules via DI (`container.resolve('communicationChannelsSendAsUser')`)
68
+ * — no HTTP self-call required.
69
+ */
70
+ export async function sendAsUser(
71
+ container: AppContainer,
72
+ actor: SendAsUserActor,
73
+ input: SendAsUserInput,
74
+ ): Promise<SendAsUserResult> {
75
+ if (!input.body.plain && !input.body.html) {
76
+ return { ok: false, status: 422, error: 'Either body.plain or body.html is required' }
77
+ }
78
+ // Re-validate here (not only in the HTTP route's zod schema) because this
79
+ // facade is also reachable via DI; `input.to[0]` below would otherwise become
80
+ // `undefined` for an empty recipient list.
81
+ if (!Array.isArray(input.to) || input.to.length === 0) {
82
+ return { ok: false, status: 422, error: 'At least one recipient is required' }
83
+ }
84
+
85
+ const em = (container.resolve('em') as EntityManager).fork()
86
+ const { tenantId } = actor
87
+ const organizationId = actor.organizationId
88
+ const dscope = { tenantId, organizationId }
89
+
90
+ const channel = await findOneWithDecryption(
91
+ em,
92
+ CommunicationChannel,
93
+ { id: input.userChannelId, tenantId, organizationId, deletedAt: null },
94
+ undefined,
95
+ dscope,
96
+ )
97
+ if (!channel) return { ok: false, status: 404, error: 'Channel not found' }
98
+ if (channel.userId !== actor.userId) {
99
+ return { ok: false, status: 403, error: 'You can only send through channels you own' }
100
+ }
101
+
102
+ // Hub mutation guard: map known non-deliverable states (`requires_reauth`,
103
+ // `disconnected`) to a 422 with field-level errors; other transitional states
104
+ // fall through to the 409 below.
105
+ try {
106
+ await guardOutboundCreate(em, { channelId: channel.id, scope: dscope })
107
+ } catch (err) {
108
+ if (err instanceof ChannelMutationBlockedError) {
109
+ return { ok: false, status: 422, error: err.message, fieldErrors: err.errors }
110
+ }
111
+ throw err
112
+ }
113
+ if (!channel.isActive || channel.status !== 'connected') {
114
+ return {
115
+ ok: false,
116
+ status: 409,
117
+ error: `Channel is in status '${channel.status}' (not connected)`,
118
+ }
119
+ }
120
+
121
+ // Create the Message via the messages module compose command. The outbound
122
+ // subscriber picks it up via `messages.message.sent` and routes through the
123
+ // adapter chain.
124
+ const commandBus = container.resolve('commandBus') as CommandBus
125
+ const messageBody = input.body.plain ?? htmlToText(input.body.html ?? '')
126
+ const composeInput = {
127
+ type: `channel.${channel.providerKey}`,
128
+ visibility: 'public' as const,
129
+ sourceEntityType: 'communication_channels.send_as_user',
130
+ sourceEntityId: channel.id,
131
+ externalEmail: input.to[0],
132
+ externalName: input.subject,
133
+ recipients: [],
134
+ subject: input.subject,
135
+ body: messageBody,
136
+ bodyFormat: 'text' as const,
137
+ priority: 'normal' as const,
138
+ sendViaEmail: false,
139
+ parentMessageId: input.parentMessageId,
140
+ isDraft: false,
141
+ tenantId,
142
+ organizationId,
143
+ userId: actor.userId,
144
+ }
145
+ let result
146
+ try {
147
+ result = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(
148
+ 'messages.messages.compose',
149
+ {
150
+ input: composeInput,
151
+ ctx: {
152
+ container,
153
+ auth: actor.auth as never,
154
+ organizationScope: null,
155
+ selectedOrganizationId: organizationId,
156
+ organizationIds: organizationId ? [organizationId] : null,
157
+ },
158
+ },
159
+ )
160
+ } catch (err) {
161
+ console.error('[communication_channels] send-as-user compose failed', err)
162
+ return { ok: false, status: 500, error: '[internal] compose failed' }
163
+ }
164
+
165
+ const messageId = result.result.id
166
+ const messageThreadId = result.result.threadId ?? messageId
167
+ const externalThreadRef = `outbound:${messageThreadId}`
168
+
169
+ // Persist the conversation, thread mapping, and channel link as one unit. The
170
+ // conversation's id is DB-generated (`gen_random_uuid()`), so it must be
171
+ // flushed before the mapping/link can reference it — hence the eager flushes
172
+ // rather than `withAtomicFlush` (single trailing flush). `em.transactional`
173
+ // wraps the whole sequence so a mid-way failure rolls back all three writes
174
+ // instead of leaving an orphaned conversation row.
175
+ await em.transactional(async () => {
176
+ let conversation = await findOneWithDecryption(
177
+ em,
178
+ ExternalConversation,
179
+ {
180
+ channelId: channel.id,
181
+ externalConversationId: externalThreadRef,
182
+ tenantId,
183
+ organizationId,
184
+ },
185
+ undefined,
186
+ dscope,
187
+ )
188
+ if (!conversation) {
189
+ conversation = em.create(ExternalConversation, {
190
+ channelId: channel.id,
191
+ externalConversationId: externalThreadRef,
192
+ subject: input.subject,
193
+ assignedUserId: actor.userId,
194
+ tenantId,
195
+ organizationId,
196
+ lastMessageAt: new Date(),
197
+ })
198
+ em.persist(conversation)
199
+ await em.flush()
200
+ } else {
201
+ conversation.subject = conversation.subject ?? input.subject
202
+ conversation.assignedUserId = conversation.assignedUserId ?? actor.userId
203
+ conversation.lastMessageAt = new Date()
204
+ // Flush the scalar updates BEFORE the ChannelThreadMapping lookup below: a
205
+ // find/findOne on the same EntityManager between a scalar mutation and flush
206
+ // can silently drop the pending UPDATE (core AGENTS.md → Entity Update Safety).
207
+ await em.flush()
208
+ }
209
+
210
+ const existingMapping = await findOneWithDecryption(
211
+ em,
212
+ ChannelThreadMapping,
213
+ {
214
+ externalConversationId: conversation.id,
215
+ tenantId,
216
+ organizationId,
217
+ },
218
+ undefined,
219
+ dscope,
220
+ )
221
+ if (!existingMapping) {
222
+ const mapping = em.create(ChannelThreadMapping, {
223
+ externalConversationId: conversation.id,
224
+ messageThreadId,
225
+ channelId: channel.id,
226
+ providerKey: channel.providerKey,
227
+ externalThreadRef,
228
+ assignedUserId: actor.userId,
229
+ tenantId,
230
+ organizationId,
231
+ })
232
+ em.persist(mapping)
233
+ }
234
+
235
+ const channelLink = em.create(MessageChannelLink, {
236
+ messageId,
237
+ externalConversationId: conversation.id,
238
+ providerKey: channel.providerKey,
239
+ channelType: channel.channelType,
240
+ direction: 'outbound',
241
+ deliveryStatus: 'pending',
242
+ channelPayload: {
243
+ text: input.body.plain ?? messageBody,
244
+ ...(input.body.html ? { html: input.body.html } : {}),
245
+ },
246
+ channelContentType: input.body.html ? 'text/html' : 'text/plain',
247
+ channelMetadata: {
248
+ // Caller-supplied pass-through metadata merged FIRST so the validated
249
+ // routing fields below always win — a caller cannot override the
250
+ // recipients/subject/threading headers via `channelMetadata`.
251
+ ...(input.channelMetadata ?? {}),
252
+ to: input.to,
253
+ cc: input.cc ?? [],
254
+ bcc: input.bcc ?? [],
255
+ subject: input.subject,
256
+ inReplyTo: input.inReplyTo ?? null,
257
+ references: input.references ?? [],
258
+ },
259
+ tenantId,
260
+ organizationId,
261
+ })
262
+ em.persist(channelLink)
263
+ await em.flush()
264
+ })
265
+
266
+ const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)
267
+ const deliveryJob: OutboundDeliveryPayload = {
268
+ messageId,
269
+ scope: dscope,
270
+ attempt: 1,
271
+ }
272
+ await queue.enqueue(deliveryJob as unknown as Record<string, unknown>)
273
+
274
+ return {
275
+ ok: true,
276
+ messageId,
277
+ threadId: messageThreadId,
278
+ channelId: channel.id,
279
+ providerKey: channel.providerKey,
280
+ }
281
+ }
282
+
283
+ /** DI service type for cross-module callers (resolve `communicationChannelsSendAsUser`). */
284
+ export type SendAsUserService = typeof sendAsUser
@@ -0,0 +1,74 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+
3
+ /**
4
+ * Sentinel UUID — used as a last-resort `senderUserId` for inbound channel
5
+ * messages when no per-tenant system user is available. Matches the pattern
6
+ * in `inbox_ops/lib/messagesIntegration.ts`.
7
+ */
8
+ export const COMMUNICATION_CHANNELS_SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'
9
+
10
+ /**
11
+ * Configurable email pattern for per-tenant channel-bot users. Implementations
12
+ * (provider packages, onboarding scripts) may create a real `auth.user` row
13
+ * matching this convention so inbound channel messages get a meaningful sender
14
+ * display name in the unified inbox.
15
+ *
16
+ * Format: `system+communication_channels@<tenantId>.local`
17
+ */
18
+ export function systemUserEmail(tenantId: string): string {
19
+ return `system+communication_channels@${tenantId}.local`
20
+ }
21
+
22
+ /**
23
+ * Resolve a tenant-scoped system user id to attribute inbound channel messages to.
24
+ *
25
+ * Lookup order:
26
+ * 1. Per-tenant channel-bot user (by convention email — see `systemUserEmail`).
27
+ * 2. (Optional, caller-supplied) seed fallback — a specific override id.
28
+ * 3. Sentinel UUID (`00000000-...`) — backward-compatible default.
29
+ *
30
+ * The function is fail-soft: when the lookup throws, it falls back to the
31
+ * sentinel. The inbound-processor must never refuse to ingest a message
32
+ * because the channel-bot user doesn't exist.
33
+ *
34
+ * @param em EntityManager scoped to the tenant.
35
+ * @param tenantId Tenant id for which to resolve the system user.
36
+ * @param fallbackId Optional caller-supplied fallback (e.g., the channel's
37
+ * assigned user) used when the channel-bot lookup misses.
38
+ */
39
+ export async function resolveCommunicationChannelsSystemUserId(
40
+ em: EntityManager,
41
+ tenantId: string,
42
+ fallbackId?: string | null,
43
+ ): Promise<string> {
44
+ try {
45
+ const expectedEmail = systemUserEmail(tenantId)
46
+ // Untyped QB by design — the helper is intentionally cross-module
47
+ // (resolving an `auth.user` from the hub) and must not pull the User
48
+ // entity class. MikroORM v7's typed builder requires an entity ref;
49
+ // we keep the lookup table-name-driven so the helper compiles without
50
+ // a cross-module import. The mocks in `__tests__/system-user.test.ts`
51
+ // exercise this code path through a duck-typed `createQueryBuilder` stub.
52
+ type RawQueryBuilder = {
53
+ select: (fields: string[]) => RawQueryBuilder
54
+ where: (cond: Record<string, unknown>) => RawQueryBuilder
55
+ limit: (count: number) => RawQueryBuilder
56
+ execute: (mode: string) => Promise<unknown>
57
+ }
58
+ const qb = (
59
+ em as unknown as { createQueryBuilder: (table: string, alias: string) => RawQueryBuilder }
60
+ ).createQueryBuilder('auth.users', 'u')
61
+ const row = await qb
62
+ .select(['u.id'])
63
+ .where({ email: expectedEmail, tenant_id: tenantId })
64
+ .limit(1)
65
+ .execute('get')
66
+ .catch(() => null)
67
+ const id = (row as { id?: string } | null)?.id
68
+ if (typeof id === 'string' && id.length > 0) return id
69
+ } catch {
70
+ // ignore — fall through to fallback
71
+ }
72
+ if (typeof fallbackId === 'string' && fallbackId.length > 0) return fallbackId
73
+ return COMMUNICATION_CHANNELS_SYSTEM_USER_ID
74
+ }
@@ -0,0 +1,140 @@
1
+ import type {
2
+ ChannelAdapter,
3
+ ChannelCapabilities,
4
+ ChannelNativeContent,
5
+ ConvertOutboundInput,
6
+ GetMessageStatusInput,
7
+ InboundMessage,
8
+ MessageStatus,
9
+ NormalizedInboundMessage,
10
+ SendMessageInput,
11
+ SendMessageResult,
12
+ ValidateCredentialsInput,
13
+ ValidateCredentialsResult,
14
+ VerifyWebhookInput,
15
+ } from './adapter'
16
+ import { baseEmailCapabilities } from './email-capabilities'
17
+ import { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'
18
+
19
+ /**
20
+ * Test-only channel seeding support.
21
+ *
22
+ * The ephemeral integration harness cannot connect a REAL email channel:
23
+ * - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server
24
+ * (none exists in CI), so `POST /channels/connect/credentials` returns 422.
25
+ * - Even with a connected channel, the outbound delivery worker calls the real
26
+ * SMTP adapter, which fails with no server — so `communication_channels.message.sent`
27
+ * never fires and the customers link subscriber never runs.
28
+ *
29
+ * To make the compose → deliver → `.sent` → CRM-link → cross-user-visibility chain
30
+ * (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable
31
+ * end-to-end against real Postgres, this module provides a network-free stub adapter
32
+ * that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.
33
+ *
34
+ * Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};
35
+ * when the env flag is unset (the production default) the adapter is never registered
36
+ * and the `__test_seed__` provider key resolves to no adapter — so the connect route
37
+ * returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated
38
+ * test-seed API route enforces the same gate independently (fail-closed 404 in prod).
39
+ */
40
+
41
+ /** Provider key for the network-free test stub adapter. */
42
+ export const TEST_SEED_PROVIDER_KEY = '__test_seed__'
43
+
44
+ /** Env flag that unlocks test-only channel seeding. Off in production. */
45
+ export const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'
46
+
47
+ /**
48
+ * True only when the test-seeding env flag is explicitly enabled. Accepts the
49
+ * usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a
50
+ * plain `=true`. Any other value (including unset) is treated as disabled.
51
+ */
52
+ export function isTestChannelSeedingEnabled(): boolean {
53
+ const raw = process.env[TEST_CHANNEL_SEEDING_ENV]
54
+ if (typeof raw !== 'string') return false
55
+ return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())
56
+ }
57
+
58
+ /**
59
+ * Capabilities for the stub: an email channel that supports neither reactions,
60
+ * edit/delete, nor conversation history — so the strict registry validator
61
+ * (`validateAdapterCapabilities`) requires only the core method surface.
62
+ */
63
+ const testSeedCapabilities: ChannelCapabilities = {
64
+ ...baseEmailCapabilities,
65
+ conversationHistory: false,
66
+ realtimePush: false,
67
+ }
68
+
69
+ /**
70
+ * A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any
71
+ * network I/O. Used exclusively by the integration harness to let the outbound
72
+ * delivery worker reach its success path and emit `communication_channels.message.sent`.
73
+ */
74
+ class TestSeedChannelAdapter implements ChannelAdapter {
75
+ readonly providerKey = TEST_SEED_PROVIDER_KEY
76
+ readonly channelType = 'email'
77
+ readonly capabilities = testSeedCapabilities
78
+
79
+ async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {
80
+ // Synthesize a deterministic-looking RFC2822-style message id; never touches
81
+ // the network. The delivery worker persists this as the external message id.
82
+ const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`
83
+ return {
84
+ externalMessageId,
85
+ conversationId: input.conversationId,
86
+ status: 'sent',
87
+ metadata: { testSeed: true },
88
+ }
89
+ }
90
+
91
+ async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {
92
+ // No real webhook — return the inert event so the generic webhook route 202s
93
+ // without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).
94
+ return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }
95
+ }
96
+
97
+ async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {
98
+ return { status: 'sent' }
99
+ }
100
+
101
+ async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {
102
+ return {
103
+ content: {
104
+ text: input.body,
105
+ bodyFormat: input.bodyFormat,
106
+ },
107
+ metadata: input.channelMetadata ?? {},
108
+ }
109
+ }
110
+
111
+ async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {
112
+ // The test-seed inbound path seeds MessageChannelLink rows directly and emits
113
+ // the hub event, so this adapter never normalizes a raw inbound payload.
114
+ throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')
115
+ }
116
+
117
+ async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {
118
+ // No real server to authenticate against — accept any credentials so the
119
+ // connect command persists a connected channel.
120
+ return { ok: true }
121
+ }
122
+ }
123
+
124
+ let cachedTestSeedAdapter: TestSeedChannelAdapter | null = null
125
+
126
+ function getTestSeedChannelAdapter(): TestSeedChannelAdapter {
127
+ if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()
128
+ return cachedTestSeedAdapter
129
+ }
130
+
131
+ /**
132
+ * Register the test-seed adapter exactly once, but ONLY when the env flag is set.
133
+ * Idempotent and safe to call from every container creation (`di.register`) — a
134
+ * no-op when seeding is disabled or the adapter is already registered.
135
+ */
136
+ export function ensureTestSeedAdapterRegistered(): void {
137
+ if (!isTestChannelSeedingEnabled()) return
138
+ if (hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) return
139
+ registerChannelAdapter(getTestSeedChannelAdapter())
140
+ }