@open-mercato/core 0.10.1-develop.7717.1.2befe9c634 → 0.10.1-develop.7722.1.901c72b9e9

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 (384) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/staff_support_routing_rule/index.js +21 -0
  3. package/dist/generated/entities/staff_support_routing_rule/index.js.map +7 -0
  4. package/dist/generated/entities/staff_support_ticket/index.js +37 -0
  5. package/dist/generated/entities/staff_support_ticket/index.js.map +7 -0
  6. package/dist/generated/entities/staff_time_project/index.js +2 -0
  7. package/dist/generated/entities/staff_time_project/index.js.map +2 -2
  8. package/dist/generated/entities/staff_time_project_member/index.js +2 -0
  9. package/dist/generated/entities/staff_time_project_member/index.js.map +2 -2
  10. package/dist/generated/entities/staff_time_task/index.js +4 -0
  11. package/dist/generated/entities/staff_time_task/index.js.map +2 -2
  12. package/dist/generated/entities/staff_time_task_comment/index.js +9 -1
  13. package/dist/generated/entities/staff_time_task_comment/index.js.map +2 -2
  14. package/dist/generated/entities.ids.generated.js +2 -0
  15. package/dist/generated/entities.ids.generated.js.map +2 -2
  16. package/dist/generated/entity-fields-registry.js +38 -0
  17. package/dist/generated/entity-fields-registry.js.map +2 -2
  18. package/dist/helpers/integration/auth.js +1 -0
  19. package/dist/helpers/integration/auth.js.map +2 -2
  20. package/dist/helpers/integration/communicationChannelsFixtures.js +20 -0
  21. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  22. package/dist/modules/catalog/api/bulk-delete/route.js +12 -2
  23. package/dist/modules/catalog/api/bulk-delete/route.js.map +2 -2
  24. package/dist/modules/communication_channels/api/post/test-seed/route.js +17 -3
  25. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/test-seed.js +38 -5
  27. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  28. package/dist/modules/messages/commands/messages.js +2 -3
  29. package/dist/modules/messages/commands/messages.js.map +2 -2
  30. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +2 -2
  31. package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
  32. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js +299 -0
  33. package/dist/modules/staff/api/portal/support-tickets/[id]/attachments/route.js.map +7 -0
  34. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js +91 -0
  35. package/dist/modules/staff/api/portal/support-tickets/[id]/messages/route.js.map +7 -0
  36. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js +146 -0
  37. package/dist/modules/staff/api/portal/support-tickets/[id]/route.js.map +7 -0
  38. package/dist/modules/staff/api/portal/support-tickets/options/route.js +60 -0
  39. package/dist/modules/staff/api/portal/support-tickets/options/route.js.map +7 -0
  40. package/dist/modules/staff/api/portal/support-tickets/route.js +220 -0
  41. package/dist/modules/staff/api/portal/support-tickets/route.js.map +7 -0
  42. package/dist/modules/staff/api/timesheets/settings/route.js +7 -1
  43. package/dist/modules/staff/api/timesheets/settings/route.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js +122 -0
  45. package/dist/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.js.map +7 -0
  46. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js +180 -0
  47. package/dist/modules/staff/api/timesheets/support-routing-rules/preview/route.js.map +7 -0
  48. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js +147 -0
  49. package/dist/modules/staff/api/timesheets/support-routing-rules/route.js.map +7 -0
  50. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js +133 -0
  51. package/dist/modules/staff/api/timesheets/support-tickets/options/route.js.map +7 -0
  52. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js +33 -0
  53. package/dist/modules/staff/api/timesheets/tasks/[id]/claim/route.js.map +7 -0
  54. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js +25 -7
  55. package/dist/modules/staff/api/timesheets/tasks/[id]/comments/route.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js +32 -0
  57. package/dist/modules/staff/api/timesheets/tasks/[id]/release/route.js.map +7 -0
  58. package/dist/modules/staff/api/timesheets/tasks/route.js +94 -47
  59. package/dist/modules/staff/api/timesheets/tasks/route.js.map +2 -2
  60. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js +3 -0
  61. package/dist/modules/staff/api/timesheets/time-projects/[id]/employees/route.js.map +2 -2
  62. package/dist/modules/staff/api/timesheets/time-projects/route.js +3 -0
  63. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  64. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +1 -0
  65. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  66. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +54 -4
  67. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  68. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js +21 -5
  69. package/dist/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.js.map +2 -2
  70. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js +319 -254
  71. package/dist/modules/staff/backend/staff/time-tracking/settings/page.js.map +2 -2
  72. package/dist/modules/staff/backend/staff/time-tracking/support/page.js +363 -0
  73. package/dist/modules/staff/backend/staff/time-tracking/support/page.js.map +7 -0
  74. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js +18 -0
  75. package/dist/modules/staff/backend/staff/time-tracking/support/page.meta.js.map +7 -0
  76. package/dist/modules/staff/cli.js +27 -0
  77. package/dist/modules/staff/cli.js.map +2 -2
  78. package/dist/modules/staff/commands/index.js +3 -0
  79. package/dist/modules/staff/commands/index.js.map +2 -2
  80. package/dist/modules/staff/commands/shared.js +10 -1
  81. package/dist/modules/staff/commands/shared.js.map +2 -2
  82. package/dist/modules/staff/commands/support-routing-rules.js +505 -0
  83. package/dist/modules/staff/commands/support-routing-rules.js.map +7 -0
  84. package/dist/modules/staff/commands/support-tickets.js +513 -0
  85. package/dist/modules/staff/commands/support-tickets.js.map +7 -0
  86. package/dist/modules/staff/commands/timesheets-projects.js +72 -14
  87. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  88. package/dist/modules/staff/commands/timesheets-task-assignment.js +270 -0
  89. package/dist/modules/staff/commands/timesheets-task-assignment.js.map +7 -0
  90. package/dist/modules/staff/commands/timesheets-task-comments.js +58 -12
  91. package/dist/modules/staff/commands/timesheets-task-comments.js.map +2 -2
  92. package/dist/modules/staff/commands/timesheets-tasks.js +91 -9
  93. package/dist/modules/staff/commands/timesheets-tasks.js.map +3 -3
  94. package/dist/modules/staff/data/enrichers.js +141 -0
  95. package/dist/modules/staff/data/enrichers.js.map +2 -2
  96. package/dist/modules/staff/data/entities.js +143 -2
  97. package/dist/modules/staff/data/entities.js.map +2 -2
  98. package/dist/modules/staff/data/extensions.js +8 -0
  99. package/dist/modules/staff/data/extensions.js.map +2 -2
  100. package/dist/modules/staff/data/validators.js +34 -5
  101. package/dist/modules/staff/data/validators.js.map +2 -2
  102. package/dist/modules/staff/encryption.js +6 -0
  103. package/dist/modules/staff/encryption.js.map +2 -2
  104. package/dist/modules/staff/events.js +25 -0
  105. package/dist/modules/staff/events.js.map +2 -2
  106. package/dist/modules/staff/events.payloads.js +54 -0
  107. package/dist/modules/staff/events.payloads.js.map +2 -2
  108. package/dist/modules/staff/extension-points.js +1 -0
  109. package/dist/modules/staff/extension-points.js.map +2 -2
  110. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js +468 -0
  111. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.js.map +7 -0
  112. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js +12 -0
  113. package/dist/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.js.map +7 -0
  114. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js +316 -0
  115. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.js.map +7 -0
  116. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js +12 -0
  117. package/dist/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.js.map +7 -0
  118. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js +190 -0
  119. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.js.map +7 -0
  120. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js +19 -0
  121. package/dist/modules/staff/frontend/[orgSlug]/portal/support/page.meta.js.map +7 -0
  122. package/dist/modules/staff/index.js +1 -1
  123. package/dist/modules/staff/index.js.map +2 -2
  124. package/dist/modules/staff/lib/crud.js +2 -0
  125. package/dist/modules/staff/lib/crud.js.map +2 -2
  126. package/dist/modules/staff/lib/support/attachments.js +36 -0
  127. package/dist/modules/staff/lib/support/attachments.js.map +7 -0
  128. package/dist/modules/staff/lib/support/dictionaries.js +87 -0
  129. package/dist/modules/staff/lib/support/dictionaries.js.map +7 -0
  130. package/dist/modules/staff/lib/support/notifications.js +111 -0
  131. package/dist/modules/staff/lib/support/notifications.js.map +7 -0
  132. package/dist/modules/staff/lib/support/portalAttachments.js +100 -0
  133. package/dist/modules/staff/lib/support/portalAttachments.js.map +7 -0
  134. package/dist/modules/staff/lib/support/portalBroadcast.js +117 -0
  135. package/dist/modules/staff/lib/support/portalBroadcast.js.map +7 -0
  136. package/dist/modules/staff/lib/support/portalRoute.js +99 -0
  137. package/dist/modules/staff/lib/support/portalRoute.js.map +7 -0
  138. package/dist/modules/staff/lib/support/portalStatus.js +12 -0
  139. package/dist/modules/staff/lib/support/portalStatus.js.map +7 -0
  140. package/dist/modules/staff/lib/support/portalTickets.js +148 -0
  141. package/dist/modules/staff/lib/support/portalTickets.js.map +7 -0
  142. package/dist/modules/staff/lib/support/routing.js +81 -0
  143. package/dist/modules/staff/lib/support/routing.js.map +7 -0
  144. package/dist/modules/staff/lib/support/routingRuleDraft.js +25 -0
  145. package/dist/modules/staff/lib/support/routingRuleDraft.js.map +7 -0
  146. package/dist/modules/staff/lib/support/seedDefaults.js +114 -0
  147. package/dist/modules/staff/lib/support/seedDefaults.js.map +7 -0
  148. package/dist/modules/staff/lib/support/supportActivity.js +26 -0
  149. package/dist/modules/staff/lib/support/supportActivity.js.map +7 -0
  150. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js +23 -0
  151. package/dist/modules/staff/lib/support-ui/PortalSupportNotLinked.js.map +7 -0
  152. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js +112 -0
  153. package/dist/modules/staff/lib/support-ui/portalAttachmentValidation.js.map +7 -0
  154. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js +45 -0
  155. package/dist/modules/staff/lib/support-ui/portalPendingUploads.js.map +7 -0
  156. package/dist/modules/staff/lib/support-ui/portalSupport.js +51 -0
  157. package/dist/modules/staff/lib/support-ui/portalSupport.js.map +7 -0
  158. package/dist/modules/staff/lib/time-tracking/access.js +3 -39
  159. package/dist/modules/staff/lib/time-tracking/access.js.map +2 -2
  160. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js +54 -0
  161. package/dist/modules/staff/lib/time-tracking/listProjectAccess.js.map +7 -0
  162. package/dist/modules/staff/lib/time-tracking/portalRecipients.js +92 -2
  163. package/dist/modules/staff/lib/time-tracking/portalRecipients.js.map +2 -2
  164. package/dist/modules/staff/lib/time-tracking/projectAssignment.js +112 -0
  165. package/dist/modules/staff/lib/time-tracking/projectAssignment.js.map +7 -0
  166. package/dist/modules/staff/lib/time-tracking/settingKeys.js +14 -0
  167. package/dist/modules/staff/lib/time-tracking/settingKeys.js.map +2 -2
  168. package/dist/modules/staff/lib/time-tracking/settings.js +28 -1
  169. package/dist/modules/staff/lib/time-tracking/settings.js.map +2 -2
  170. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js +143 -0
  171. package/dist/modules/staff/lib/time-tracking/taskAssignmentRoute.js.map +7 -0
  172. package/dist/modules/staff/lib/time-tracking/workRecipients.js +92 -0
  173. package/dist/modules/staff/lib/time-tracking/workRecipients.js.map +7 -0
  174. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js +32 -3
  175. package/dist/modules/staff/lib/time-tracking-ui/BoardFilterChips.js.map +2 -2
  176. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js +29 -0
  177. package/dist/modules/staff/lib/time-tracking-ui/KanbanBoard.js.map +2 -2
  178. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js +4 -0
  179. package/dist/modules/staff/lib/time-tracking-ui/KanbanCard.js.map +2 -2
  180. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js +141 -1
  181. package/dist/modules/staff/lib/time-tracking-ui/ProjectFormSections.js.map +2 -2
  182. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js +153 -0
  183. package/dist/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.js.map +7 -0
  184. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js +67 -0
  185. package/dist/modules/staff/lib/time-tracking-ui/SupportRequestPanel.js.map +7 -0
  186. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js +3 -1
  187. package/dist/modules/staff/lib/time-tracking-ui/TaskBoardScreen.js.map +2 -2
  188. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js +80 -17
  189. package/dist/modules/staff/lib/time-tracking-ui/TaskCommentThread.js.map +2 -2
  190. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js +230 -41
  191. package/dist/modules/staff/lib/time-tracking-ui/TaskDrawer.js.map +2 -2
  192. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js +21 -1
  193. package/dist/modules/staff/lib/time-tracking-ui/boardDirectory.js.map +2 -2
  194. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js +16 -4
  195. package/dist/modules/staff/lib/time-tracking-ui/boardFilters.js.map +2 -2
  196. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js +3 -1
  197. package/dist/modules/staff/lib/time-tracking-ui/kanbanBoardData.js.map +2 -2
  198. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js +68 -0
  199. package/dist/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.js.map +7 -0
  200. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js +7 -0
  201. package/dist/modules/staff/lib/time-tracking-ui/settingsTabs.js.map +7 -0
  202. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js +50 -0
  203. package/dist/modules/staff/lib/time-tracking-ui/supportTicketData.js.map +7 -0
  204. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js +13 -1
  205. package/dist/modules/staff/lib/time-tracking-ui/taskDrawerData.js.map +2 -2
  206. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js +36 -2
  207. package/dist/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.js.map +2 -2
  208. package/dist/modules/staff/migrations/Migration20261009180543_staff.js +42 -0
  209. package/dist/modules/staff/migrations/Migration20261009180543_staff.js.map +7 -0
  210. package/dist/modules/staff/notifications.js +92 -0
  211. package/dist/modules/staff/notifications.js.map +2 -2
  212. package/dist/modules/staff/setup.js +6 -2
  213. package/dist/modules/staff/setup.js.map +2 -2
  214. package/dist/modules/staff/subscribers/support-portal-comment-created.js +26 -0
  215. package/dist/modules/staff/subscribers/support-portal-comment-created.js.map +7 -0
  216. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js +26 -0
  217. package/dist/modules/staff/subscribers/support-portal-comment-deleted.js.map +7 -0
  218. package/dist/modules/staff/subscribers/support-portal-comment-updated.js +26 -0
  219. package/dist/modules/staff/subscribers/support-portal-comment-updated.js.map +7 -0
  220. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js +24 -0
  221. package/dist/modules/staff/subscribers/support-portal-status-column-updated.js.map +7 -0
  222. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js +25 -0
  223. package/dist/modules/staff/subscribers/support-portal-task-status-changed.js.map +7 -0
  224. package/dist/modules/staff/subscribers/support-portal-task-updated.js +25 -0
  225. package/dist/modules/staff/subscribers/support-portal-task-updated.js.map +7 -0
  226. package/dist/modules/staff/subscribers/support-ticket-created-notification.js +25 -0
  227. package/dist/modules/staff/subscribers/support-ticket-created-notification.js.map +7 -0
  228. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js +25 -0
  229. package/dist/modules/staff/subscribers/support-ticket-customer-replied-notification.js.map +7 -0
  230. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js +26 -0
  231. package/dist/modules/staff/subscribers/support-ticket-unnotified-clear.js.map +7 -0
  232. package/dist/modules/staff/subscribers/time-task-assignee-notification.js +83 -0
  233. package/dist/modules/staff/subscribers/time-task-assignee-notification.js.map +7 -0
  234. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js +112 -0
  235. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.client.js.map +7 -0
  236. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js +17 -0
  237. package/dist/modules/staff/widgets/injection/portal-support-tickets/widget.js.map +7 -0
  238. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js +870 -0
  239. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.js.map +7 -0
  240. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js +17 -0
  241. package/dist/modules/staff/widgets/injection/time-tracking-settings-support/widget.js.map +7 -0
  242. package/dist/modules/staff/widgets/injection-table.js +16 -1
  243. package/dist/modules/staff/widgets/injection-table.js.map +2 -2
  244. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +4 -18
  245. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  246. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +4 -18
  247. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  248. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +4 -18
  249. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  250. package/dist/modules/wms/lib/inventoryCsvExport.js +19 -0
  251. package/dist/modules/wms/lib/inventoryCsvExport.js.map +7 -0
  252. package/generated/entities/staff_support_routing_rule/index.ts +9 -0
  253. package/generated/entities/staff_support_ticket/index.ts +17 -0
  254. package/generated/entities/staff_time_project/index.ts +1 -0
  255. package/generated/entities/staff_time_project_member/index.ts +1 -0
  256. package/generated/entities/staff_time_task/index.ts +2 -0
  257. package/generated/entities/staff_time_task_comment/index.ts +4 -0
  258. package/generated/entities.ids.generated.ts +2 -0
  259. package/generated/entity-fields-registry.ts +38 -0
  260. package/package.json +8 -8
  261. package/src/helpers/integration/auth.ts +1 -1
  262. package/src/helpers/integration/communicationChannelsFixtures.ts +60 -2
  263. package/src/modules/catalog/api/bulk-delete/route.ts +12 -2
  264. package/src/modules/communication_channels/api/post/test-seed/route.ts +29 -12
  265. package/src/modules/communication_channels/lib/test-seed.ts +49 -10
  266. package/src/modules/messages/commands/messages.ts +8 -10
  267. package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +2 -2
  268. package/src/modules/staff/AGENTS.md +22 -6
  269. package/src/modules/staff/api/portal/support-tickets/[id]/attachments/route.ts +327 -0
  270. package/src/modules/staff/api/portal/support-tickets/[id]/messages/route.ts +94 -0
  271. package/src/modules/staff/api/portal/support-tickets/[id]/route.ts +160 -0
  272. package/src/modules/staff/api/portal/support-tickets/options/route.ts +60 -0
  273. package/src/modules/staff/api/portal/support-tickets/route.ts +253 -0
  274. package/src/modules/staff/api/timesheets/settings/route.ts +7 -1
  275. package/src/modules/staff/api/timesheets/support-routing-rules/fallback-project/route.ts +131 -0
  276. package/src/modules/staff/api/timesheets/support-routing-rules/preview/route.ts +248 -0
  277. package/src/modules/staff/api/timesheets/support-routing-rules/route.ts +150 -0
  278. package/src/modules/staff/api/timesheets/support-tickets/options/route.ts +181 -0
  279. package/src/modules/staff/api/timesheets/tasks/[id]/claim/route.ts +31 -0
  280. package/src/modules/staff/api/timesheets/tasks/[id]/comments/route.ts +25 -7
  281. package/src/modules/staff/api/timesheets/tasks/[id]/release/route.ts +30 -0
  282. package/src/modules/staff/api/timesheets/tasks/route.ts +119 -67
  283. package/src/modules/staff/api/timesheets/time-projects/[id]/employees/route.ts +3 -0
  284. package/src/modules/staff/api/timesheets/time-projects/route.ts +3 -0
  285. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +1 -0
  286. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +67 -2
  287. package/src/modules/staff/backend/staff/time-tracking/projects/projectFormConfig.ts +22 -3
  288. package/src/modules/staff/backend/staff/time-tracking/settings/page.tsx +405 -308
  289. package/src/modules/staff/backend/staff/time-tracking/support/page.meta.ts +14 -0
  290. package/src/modules/staff/backend/staff/time-tracking/support/page.tsx +428 -0
  291. package/src/modules/staff/cli.ts +28 -0
  292. package/src/modules/staff/commands/index.ts +3 -0
  293. package/src/modules/staff/commands/shared.ts +22 -1
  294. package/src/modules/staff/commands/support-routing-rules.ts +595 -0
  295. package/src/modules/staff/commands/support-tickets.ts +580 -0
  296. package/src/modules/staff/commands/timesheets-projects.ts +84 -13
  297. package/src/modules/staff/commands/timesheets-task-assignment.ts +319 -0
  298. package/src/modules/staff/commands/timesheets-task-comments.ts +70 -15
  299. package/src/modules/staff/commands/timesheets-tasks.ts +106 -15
  300. package/src/modules/staff/data/enrichers.ts +203 -0
  301. package/src/modules/staff/data/entities.ts +133 -2
  302. package/src/modules/staff/data/extensions.ts +9 -1
  303. package/src/modules/staff/data/validators.ts +36 -0
  304. package/src/modules/staff/encryption.ts +6 -0
  305. package/src/modules/staff/events.payloads.ts +87 -1
  306. package/src/modules/staff/events.ts +26 -0
  307. package/src/modules/staff/extension-points.ts +1 -0
  308. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.meta.ts +10 -0
  309. package/src/modules/staff/frontend/[orgSlug]/portal/support/[id]/page.tsx +553 -0
  310. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.meta.ts +10 -0
  311. package/src/modules/staff/frontend/[orgSlug]/portal/support/new/page.tsx +346 -0
  312. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.meta.ts +17 -0
  313. package/src/modules/staff/frontend/[orgSlug]/portal/support/page.tsx +253 -0
  314. package/src/modules/staff/i18n/de.json +291 -0
  315. package/src/modules/staff/i18n/en.json +291 -0
  316. package/src/modules/staff/i18n/es.json +291 -0
  317. package/src/modules/staff/i18n/ko.json +291 -0
  318. package/src/modules/staff/i18n/pl.json +291 -0
  319. package/src/modules/staff/index.ts +1 -1
  320. package/src/modules/staff/lib/crud.ts +2 -1
  321. package/src/modules/staff/lib/support/attachments.ts +43 -0
  322. package/src/modules/staff/lib/support/dictionaries.ts +120 -0
  323. package/src/modules/staff/lib/support/notifications.ts +138 -0
  324. package/src/modules/staff/lib/support/portalAttachments.ts +140 -0
  325. package/src/modules/staff/lib/support/portalBroadcast.ts +161 -0
  326. package/src/modules/staff/lib/support/portalRoute.ts +108 -0
  327. package/src/modules/staff/lib/support/portalStatus.ts +15 -0
  328. package/src/modules/staff/lib/support/portalTickets.ts +233 -0
  329. package/src/modules/staff/lib/support/routing.ts +117 -0
  330. package/src/modules/staff/lib/support/routingRuleDraft.ts +45 -0
  331. package/src/modules/staff/lib/support/seedDefaults.ts +151 -0
  332. package/src/modules/staff/lib/support/supportActivity.ts +39 -0
  333. package/src/modules/staff/lib/support-ui/PortalSupportNotLinked.tsx +20 -0
  334. package/src/modules/staff/lib/support-ui/portalAttachmentValidation.ts +125 -0
  335. package/src/modules/staff/lib/support-ui/portalPendingUploads.ts +49 -0
  336. package/src/modules/staff/lib/support-ui/portalSupport.ts +116 -0
  337. package/src/modules/staff/lib/time-tracking/access.ts +2 -62
  338. package/src/modules/staff/lib/time-tracking/listProjectAccess.ts +78 -0
  339. package/src/modules/staff/lib/time-tracking/portalRecipients.ts +173 -5
  340. package/src/modules/staff/lib/time-tracking/projectAssignment.ts +172 -0
  341. package/src/modules/staff/lib/time-tracking/settingKeys.ts +11 -0
  342. package/src/modules/staff/lib/time-tracking/settings.ts +46 -1
  343. package/src/modules/staff/lib/time-tracking/taskAssignmentRoute.ts +165 -0
  344. package/src/modules/staff/lib/time-tracking/workRecipients.ts +183 -0
  345. package/src/modules/staff/lib/time-tracking-ui/BoardFilterChips.tsx +36 -1
  346. package/src/modules/staff/lib/time-tracking-ui/KanbanBoard.tsx +30 -0
  347. package/src/modules/staff/lib/time-tracking-ui/KanbanCard.tsx +9 -0
  348. package/src/modules/staff/lib/time-tracking-ui/ProjectFormSections.tsx +185 -6
  349. package/src/modules/staff/lib/time-tracking-ui/SupportAttachmentsSection.tsx +188 -0
  350. package/src/modules/staff/lib/time-tracking-ui/SupportRequestPanel.tsx +102 -0
  351. package/src/modules/staff/lib/time-tracking-ui/TaskBoardScreen.tsx +2 -0
  352. package/src/modules/staff/lib/time-tracking-ui/TaskCommentThread.tsx +105 -22
  353. package/src/modules/staff/lib/time-tracking-ui/TaskDrawer.tsx +278 -43
  354. package/src/modules/staff/lib/time-tracking-ui/boardDirectory.ts +21 -0
  355. package/src/modules/staff/lib/time-tracking-ui/boardFilters.ts +16 -2
  356. package/src/modules/staff/lib/time-tracking-ui/kanbanBoardData.ts +4 -0
  357. package/src/modules/staff/lib/time-tracking-ui/projectDefaultAssignee.ts +117 -0
  358. package/src/modules/staff/lib/time-tracking-ui/settingsTabs.ts +17 -0
  359. package/src/modules/staff/lib/time-tracking-ui/supportTicketData.ts +71 -0
  360. package/src/modules/staff/lib/time-tracking-ui/taskDrawerData.ts +15 -0
  361. package/src/modules/staff/lib/time-tracking-ui/timeTrackingSettingsForm.ts +48 -1
  362. package/src/modules/staff/migrations/.snapshot-open-mercato.json +714 -0
  363. package/src/modules/staff/migrations/Migration20261009180543_staff.ts +50 -0
  364. package/src/modules/staff/notifications.ts +92 -0
  365. package/src/modules/staff/setup.ts +6 -2
  366. package/src/modules/staff/subscribers/support-portal-comment-created.ts +35 -0
  367. package/src/modules/staff/subscribers/support-portal-comment-deleted.ts +35 -0
  368. package/src/modules/staff/subscribers/support-portal-comment-updated.ts +35 -0
  369. package/src/modules/staff/subscribers/support-portal-status-column-updated.ts +36 -0
  370. package/src/modules/staff/subscribers/support-portal-task-status-changed.ts +34 -0
  371. package/src/modules/staff/subscribers/support-portal-task-updated.ts +34 -0
  372. package/src/modules/staff/subscribers/support-ticket-created-notification.ts +27 -0
  373. package/src/modules/staff/subscribers/support-ticket-customer-replied-notification.ts +27 -0
  374. package/src/modules/staff/subscribers/support-ticket-unnotified-clear.ts +29 -0
  375. package/src/modules/staff/subscribers/time-task-assignee-notification.ts +102 -0
  376. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.client.tsx +151 -0
  377. package/src/modules/staff/widgets/injection/portal-support-tickets/widget.ts +16 -0
  378. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.client.tsx +1058 -0
  379. package/src/modules/staff/widgets/injection/time-tracking-settings-support/widget.ts +17 -0
  380. package/src/modules/staff/widgets/injection-table.ts +15 -0
  381. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -20
  382. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -20
  383. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -19
  384. package/src/modules/wms/lib/inventoryCsvExport.ts +18 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/communication_channels/api/post/test-seed/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n MessageChannelLink,\n} from '../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../lib/access-control'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../commands/connect-credential-channel'\nimport { emitCommunicationChannelsEvent } from '../../../events'\nimport {\n TEST_SEED_CHAT_PROVIDER_KEY,\n TEST_SEED_PROVIDER_KEY,\n clearTestSeedCapturedMessages,\n createTestSeedPlatformMessage,\n deleteTestSeedPlatformMessage,\n ensureTestSeedAdapterRegistered,\n isTestEmailCaptureAccessAuthorized,\n isTestChannelSeedingEnabled,\n listTestSeedCapturedMessages,\n} from '../../../lib/test-seed'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n type IngestInboundMessageResult,\n} from '../../../commands/ingest-inbound-message'\n\n/**\n * TEST-ONLY channel seeding endpoint.\n *\n * Gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` \u2014 when the flag is unset (the\n * production default) every request returns 404, so this route is invisible and\n * inert in production. See `lib/test-seed.ts` for the full rationale.\n *\n * Actions, all scoped to the caller's tenant/org:\n * - `connect-channel`: connect a network-free stub channel owned by the caller\n * (delegates to the real connect-credential command so the channel persists\n * credentials + lands in `status='connected'`). Enables the outbound\n * compose \u2192 deliver \u2192 `.sent` chain to complete in CI. `providerFlavor: 'chat'`\n * connects the non-email stub instead, for tests about sender identity, and\n * `labelAsProviderKey` relabels the connected row so a provider-scoped\n * listing has something to list without a live credential probe.\n * - `ingest-inbound`: run the REAL `ingest_inbound_message` command over an\n * adapter-normalized chat frame, so the platform compose path \u2014 and every\n * validation rule on it \u2014 actually executes. Use this for anything that\n * claims inbound works.\n * - `emit-inbound`: insert an inbound `MessageChannelLink` (+ a `messages.message`\n * row for threading) and emit `communication_channels.message.received` so the\n * customers link-channel-message subscriber runs against real Postgres. Enables\n * the inbound auto-link tests (TC-CRM-EMAIL-002..005). NOTE: this action\n * deliberately bypasses `messages.messages.compose` \u2014 it can never prove that\n * the hub accepts a message, only that downstream subscribers fire.\n * - `purge-inbound`: hard-delete the rows one `emit-inbound` call seeded, for\n * integration teardown.\n */\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/test-seed',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst addressObjectSchema = z.object({ address: z.string(), name: z.string().optional() })\nconst addressFieldSchema = z.union([\n z.string(),\n addressObjectSchema,\n z.array(z.union([z.string(), addressObjectSchema])),\n])\n\nconst connectChannelSchema = z.object({\n action: z.literal('connect-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n /**\n * Which stub provider to connect. `email` (default) keeps the historical\n * email-shaped channel; `chat` connects a channel whose senders have no\n * address, so a test can exercise the hub's non-email identity contract\n * without inventing one (#4975).\n */\n providerFlavor: z.enum(['email', 'chat']).optional(),\n /**\n * TEST-ONLY: rewrite the connected stub channel's `provider_key` to this value.\n *\n * Routes that a provider package owns filter on their own `provider_key`, and\n * a real channel for those providers cannot be connected in CI \u2014 the Discord\n * adapter's `validateCredentials` performs a live API call against a bot token\n * no test environment has. Without this, a provider-scoped listing has nothing\n * to list and can only be asserted against an empty result, which is exactly\n * the assertion that would have stayed green through #5602.\n *\n * It relabels a row; it registers nothing. The stub adapter is still the only\n * adapter present, so the channel remains network-free \u2014 but no test should\n * drive an outbound send through a relabelled channel, because delivery would\n * then resolve the real provider's adapter.\n */\n labelAsProviderKey: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9_-]+$/)\n .optional(),\n})\n\n/**\n * Drive the REAL `ingest_inbound_message` command with a chat-shaped frame.\n *\n * `emit-inbound` below deliberately bypasses the platform compose path (it\n * inserts the `messages` row with raw SQL) because it only ever needed a\n * landing zone for the CRM-link subscribers. That bypass is why an inbound test\n * could pass while `composeMessageSchema` rejected every real message (#4975).\n * This action takes the opposite approach: it hands the frame to the adapter and\n * the ingest command and asserts nothing on the way, so whatever the hub's\n * contract really is, the test feels it.\n */\nconst ingestInboundSchema = z.object({\n action: z.literal('ingest-inbound'),\n channelId: z.string().uuid(),\n /** Opaque sender handle \u2014 a Discord snowflake, a Slack member id, etc. */\n senderIdentifier: z.string().min(1).max(255),\n senderDisplayName: z.string().max(255).optional(),\n body: z.string().max(50_000).optional(),\n externalMessageId: z.string().min(1).max(255),\n externalConversationId: z.string().min(1).max(255),\n})\n\nconst seedSystemChannelSchema = z.object({\n action: z.literal('seed-system-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n})\n\nconst clearCaptureSchema = z.object({\n action: z.literal('clear-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst listCaptureSchema = z.object({\n action: z.literal('list-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst emitInboundSchema = z.object({\n action: z.literal('emit-inbound'),\n /** Channel that owns the inbound message; controls authorUserId + default visibility. */\n channelId: z.string().uuid(),\n /** Provider key persisted on the link (defaults to the stub provider). */\n providerKey: z.string().min(1).max(64).optional(),\n /** Normalized inbound addresses (stored under channelPayload). */\n from: addressFieldSchema.optional(),\n to: addressFieldSchema.optional(),\n cc: addressFieldSchema.optional(),\n subject: z.string().max(500).optional(),\n bodyText: z.string().max(200_000).optional(),\n /** RFC2822 Message-ID of this inbound message (for In-Reply-To matching). */\n messageId: z.string().max(500).optional(),\n /** RFC2822 In-Reply-To header (threading-inheritance fallback). */\n inReplyTo: z.string().max(500).optional(),\n references: z.array(z.string().max(500)).max(50).optional(),\n /**\n * Open Mercato `messages.message` thread id this inbound message belongs to.\n * When set, a `messages.message` row is created with this `threadId` so the\n * hub-thread inheritance join can resolve a Person from a sibling message.\n */\n messageThreadId: z.string().uuid().optional(),\n /**\n * Test-only: also create a `ChannelThreadMapping` for the seeded thread. The\n * reaction (`/messages/[id]/reactions`) and thread-assign\n * (`/threads/[id]/assign`) routes resolve the owning channel through this\n * mapping and return 409/404 without it. Opt-in so the existing CRM-link\n * seeds (which don't need a mapping) keep their current mapping-free shape.\n */\n createThreadMapping: z.boolean().optional(),\n})\n\n/**\n * Teardown for `emit-inbound`: hard-delete the rows one seed created (thread\n * mapping, channel link, synthetic conversation and the `messages` row), scoped\n * to the caller's channel. Disconnecting a channel deliberately retains its\n * conversations and messages, so without this every seed leaks into the shared\n * integration database.\n */\nconst purgeInboundSchema = z.object({\n action: z.literal('purge-inbound'),\n channelId: z.string().uuid(),\n channelLinkId: z.string().uuid(),\n messageId: z.string().uuid(),\n conversationId: z.string().uuid(),\n})\n\n/**\n * Read-only: report the channel-side rows a platform message carries.\n *\n * The threading contract lives in rows no API exposes \u2014 an integration test\n * asserting that a composer send really was routed through the hub can only\n * observe them here.\n */\nconst inspectMessageLinksSchema = z.object({\n action: z.literal('inspect-message-links'),\n messageId: z.string().uuid(),\n})\n\nconst bodySchema = z.discriminatedUnion('action', [\n connectChannelSchema,\n inspectMessageLinksSchema,\n ingestInboundSchema,\n emitInboundSchema,\n purgeInboundSchema,\n seedSystemChannelSchema,\n clearCaptureSchema,\n listCaptureSchema,\n])\n\nconst logger = createLogger('communication_channels').child({ component: 'test-seed' })\n\nconst TEST_SEED_CONVERSATION_PREFIX = 'inbound-seed:'\n\nfunction ingestFailureResponse(err: unknown): Response {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: err.message, issues: err.issues }, { status: 422 })\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('ingest-inbound failed', { err })\n return NextResponse.json(\n { error: err instanceof Error ? err.message : '[internal] ingest-inbound failed' },\n { status: 500 },\n )\n}\n\nexport async function POST(req: Request): Promise<Response> {\n // Fail-closed: invisible in production. Mirrors an unknown route (404) rather\n // than 403 so the surface leaks nothing when the flag is off.\n if (!isTestChannelSeedingEnabled()) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const tenantId = auth.tenantId as string\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n const userId = auth.sub as string\n const captureScope = { tenantId, organizationId }\n\n if (\n (body.action === 'clear-capture' || body.action === 'list-capture')\n && body.systemRecipient\n && (\n !body.captureCorrelationToken\n || !isTestEmailCaptureAccessAuthorized(req.headers.get('x-om-test-email-capture-access-token'))\n )\n ) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (body.action === 'clear-capture') {\n await clearTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })\n return NextResponse.json({ ok: true })\n }\n\n if (body.action === 'list-capture') {\n return NextResponse.json({\n items: (await listTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })).map(({ captureCorrelationToken: _captureCorrelationToken, ...message }) => message),\n })\n }\n\n const container = await createRequestContainer()\n // Defensive: make sure the stub adapter is registered for this process even if\n // a worker-only node skipped module di registration.\n ensureTestSeedAdapterRegistered()\n\n if (body.action === 'inspect-message-links') {\n const em = (container.resolve('em') as EntityManager).fork()\n const links = await em.find(MessageChannelLink, {\n messageId: body.messageId,\n tenantId,\n organizationId,\n })\n const conversationIds = links\n .map((link) => link.externalConversationId)\n .filter((id): id is string => Boolean(id))\n const mappings = conversationIds.length\n ? await em.find(ChannelThreadMapping, {\n externalConversationId: { $in: conversationIds },\n tenantId,\n organizationId,\n })\n : []\n return NextResponse.json({\n links: links.map((link) => ({\n id: link.id,\n direction: link.direction,\n providerKey: link.providerKey,\n channelType: link.channelType,\n deliveryStatus: link.deliveryStatus,\n externalConversationId: link.externalConversationId,\n })),\n threadMappings: mappings.map((mapping) => ({\n id: mapping.id,\n messageThreadId: mapping.messageThreadId,\n channelId: mapping.channelId,\n externalConversationId: mapping.externalConversationId,\n })),\n })\n }\n\n if (body.action === 'seed-system-channel') {\n const em = (container.resolve('em') as EntityManager).fork()\n const credentialsService = container.resolve('integrationCredentialsService') as {\n save: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n }\n await credentialsService.save(\n `channel_${TEST_SEED_PROVIDER_KEY}`,\n { testSeed: true },\n { tenantId, organizationId: organizationId ?? tenantId, userId: null },\n )\n let channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n tenantId,\n organizationId,\n userId: null,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!channel) {\n const stamp = Date.now()\n channel = em.create(CommunicationChannel, {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n displayName: body.displayName ?? `Test Seed System Email ${stamp}`,\n externalIdentifier: body.externalIdentifier ?? `system-${stamp}@test-seed.local`,\n userId: null,\n isPrimary: false,\n isActive: true,\n status: 'connected',\n tenantId,\n organizationId,\n })\n em.persist(channel)\n await em.flush()\n } else if (!channel.isActive || channel.status !== 'connected') {\n channel.isActive = true\n channel.status = 'connected'\n channel.lastError = null\n await em.flush()\n }\n\n return NextResponse.json({ channelId: channel.id }, { status: 201 })\n }\n\n if (body.action === 'connect-channel') {\n const stamp = Date.now()\n const commandBus = container.resolve('commandBus') as CommandBus\n const isChatFlavor = body.providerFlavor === 'chat'\n // A chat channel is deliberately connected WITHOUT an email-ish credential\n // key, so `connect-credential-channel` derives no `externalIdentifier` and\n // the row is shaped exactly like a real Discord channel (identifier NULL \u2014\n // the condition #4977 describes). Inventing one here would recreate the\n // fixture dishonesty that hid #4975.\n const credentials = isChatFlavor\n ? { handle: body.externalIdentifier ?? `test-seed-chat-${stamp}` }\n : {\n username: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n fromAddress: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n }\n const input: ConnectCredentialChannelInput = {\n providerKey: isChatFlavor ? TEST_SEED_CHAT_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY,\n displayName:\n body.displayName ?? `Test Seed ${isChatFlavor ? 'Chat ' : ''}Channel ${stamp}`,\n credentials,\n userId,\n scope: { tenantId, organizationId },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n if (result.status !== 'connected') {\n return NextResponse.json(\n { error: '[internal] test-seed connect failed', detail: result },\n { status: 500 },\n )\n }\n if (body.labelAsProviderKey && body.labelAsProviderKey !== input.providerKey) {\n const seedEm = (container.resolve('em') as EntityManager).fork()\n const connected = await findOneWithDecryption(\n seedEm,\n CommunicationChannel,\n { id: result.channelId, tenantId },\n undefined,\n { tenantId, organizationId },\n )\n if (!connected) {\n return NextResponse.json(\n { error: '[internal] test-seed could not reload the channel it just connected' },\n { status: 500 },\n )\n }\n connected.providerKey = body.labelAsProviderKey\n await seedEm.flush()\n }\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n providerKey: body.labelAsProviderKey ?? input.providerKey,\n },\n { status: 201 },\n )\n }\n\n // action === 'ingest-inbound' | 'emit-inbound' | 'purge-inbound' \u2014 all address an existing channel.\n const em = (container.resolve('em') as EntityManager).fork()\n // Only the `emit-inbound` branch stamps a caller-chosen provider key onto the\n // rows it seeds; `ingest-inbound` takes the channel's own (see below).\n const providerKey =\n body.action === 'emit-inbound' ? body.providerKey ?? TEST_SEED_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY\n\n // `channelId` is caller-supplied, so confirm it names a channel this tenant/org\n // actually owns before seeding rows that reference it. Mirrors the ownership\n // lookup every other per-channel route performs (see channels/[id]/test-send).\n const ownedChannel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: body.channelId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!ownedChannel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n // Tenant/org scope alone does not authorize: `connect_user_channel` is granted\n // broadly, so a same-tenant caller could otherwise seed against a colleague's\n // personal mailbox. Enforce the module's owner-only contract \u2014 personal\n // channels are owner-restricted, shared channels need `manage`.\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(userId, { tenantId, organizationId })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanManageChannel(\n { userId: ownedChannel.userId },\n userId,\n userFeatures,\n 'communication_channels.manage',\n )\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n const status = (err as { statusCode?: number }).statusCode ?? 403\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Access denied' },\n { status },\n )\n }\n\n if (body.action === 'purge-inbound') {\n const rowScope = { tenantId, organizationId }\n // Only rows hanging off a conversation `emit-inbound` minted are purgeable,\n // so a caller cannot use this action to hard-delete real channel traffic.\n const seededConversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n id: body.conversationId,\n channelId: body.channelId,\n externalConversationId: { $like: `${TEST_SEED_CONVERSATION_PREFIX}%` },\n ...rowScope,\n },\n undefined,\n rowScope,\n )\n if (!seededConversation) {\n return NextResponse.json({ error: 'seeded inbound conversation not found' }, { status: 404 })\n }\n await em.nativeDelete(ChannelThreadMapping, {\n externalConversationId: body.conversationId,\n channelId: body.channelId,\n ...rowScope,\n })\n await em.nativeDelete(MessageChannelLink, {\n id: body.channelLinkId,\n externalConversationId: body.conversationId,\n ...rowScope,\n })\n await em.nativeDelete(ExternalConversation, {\n id: body.conversationId,\n channelId: body.channelId,\n ...rowScope,\n })\n await deleteTestSeedPlatformMessage(em, {\n messageId: body.messageId,\n channelId: body.channelId,\n ...rowScope,\n })\n return NextResponse.json({ ok: true })\n }\n\n if (body.action === 'ingest-inbound') {\n // Everything below this point is the real path: the adapter normalizes the\n // frame and `ingest_inbound_message` composes the platform message through\n // `messages.messages.compose`. Nothing is short-circuited, so a hub contract\n // the provider cannot satisfy surfaces here as a failure instead of hiding\n // behind seeded rows (#4975).\n // The channel's own provider key, never a hardcoded one: ingest does not\n // check that `providerKey` matches the channel it names, so passing a\n // different one would silently stamp the wrong provider onto the link.\n const channelProviderKey = ownedChannel.providerKey\n if (channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY) {\n return NextResponse.json(\n {\n error:\n 'ingest-inbound requires a channel connected with providerFlavor: \"chat\"; ' +\n `channel ${body.channelId} is '${channelProviderKey}'`,\n },\n { status: 422 },\n )\n }\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const adapterRegistry = container.resolve('channelAdapterRegistry') as {\n get: (key: string) => { normalizeInbound: (raw: unknown) => Promise<unknown> } | undefined\n }\n const adapter = adapterRegistry.get(channelProviderKey)\n if (!adapter) {\n return NextResponse.json(\n { error: '[internal] test-seed chat adapter is not registered' },\n { status: 500 },\n )\n }\n\n let normalized: unknown\n try {\n normalized = await adapter.normalizeInbound({\n raw: {\n externalMessageId: body.externalMessageId,\n externalConversationId: body.externalConversationId,\n senderIdentifier: body.senderIdentifier,\n senderDisplayName: body.senderDisplayName,\n body: body.body ?? '',\n },\n eventType: 'message',\n metadata: {},\n })\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : '[internal] adapter rejected the inbound frame' },\n { status: 422 },\n )\n }\n\n const ingestInput = {\n channelId: body.channelId,\n providerKey: channelProviderKey,\n channelType: ownedChannel.channelType,\n scope: { tenantId, organizationId },\n message: normalized,\n } as IngestInboundMessageInput\n\n let result: IngestInboundMessageResult\n try {\n const execution = await commandBus.execute<\n IngestInboundMessageInput,\n IngestInboundMessageResult\n >(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input: ingestInput,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n result = execution.result\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return ingestFailureResponse(err)\n }\n\n return NextResponse.json(\n {\n status: result.status,\n messageId: result.messageId ?? null,\n conversationId: result.externalConversationId ?? null,\n channelLinkId: result.channelLinkId ?? null,\n channelType: ownedChannel.channelType,\n },\n { status: 201 },\n )\n }\n\n // A MessageChannelLink requires a non-null external_conversation_id (FK) and\n // message_id. Create a synthetic conversation + (optionally threaded) message\n // so the link is shaped like a real inbound row the subscriber can consume.\n const conversation = em.create(ExternalConversation, {\n channelId: body.channelId,\n externalConversationId: `${TEST_SEED_CONVERSATION_PREFIX}${Date.now()}:${Math.random().toString(16).slice(2, 8)}`,\n subject: body.subject ?? null,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n\n const messageId = await createTestSeedPlatformMessage(em, {\n providerKey,\n threadId: body.messageThreadId,\n senderUserId: userId,\n subject: body.subject,\n bodyText: body.bodyText,\n channelId: body.channelId,\n tenantId,\n organizationId,\n })\n if (!messageId) {\n return NextResponse.json({ error: '[internal] failed to seed message row' }, { status: 500 })\n }\n\n const link = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey,\n channelType: 'email',\n direction: 'inbound',\n deliveryStatus: 'delivered',\n channelPayload: {\n ...(body.from !== undefined ? { from: body.from } : {}),\n ...(body.to !== undefined ? { to: body.to } : {}),\n ...(body.cc !== undefined ? { cc: body.cc } : {}),\n ...(body.subject !== undefined ? { subject: body.subject } : {}),\n ...(body.bodyText !== undefined ? { text: body.bodyText } : {}),\n ...(body.inReplyTo !== undefined ? { inReplyTo: body.inReplyTo } : {}),\n ...(body.references !== undefined ? { references: body.references } : {}),\n },\n channelContentType: 'text/plain',\n channelMetadata: {\n ...(body.messageId !== undefined ? { messageId: body.messageId } : {}),\n },\n tenantId,\n organizationId,\n })\n em.persist(link)\n await em.flush()\n\n // Optionally mirror `ingest-inbound-message`: a real inbound message always\n // lands a ChannelThreadMapping that the reaction + thread-assign routes use to\n // resolve the owning channel. Seeded inbound messages skip it by default; opt\n // in for tests that exercise those routes. Keyed by `messageThreadId ?? messageId`\n // to match how those commands resolve the mapping (`message.threadId ?? message.id`).\n if (body.createThreadMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: body.messageThreadId ?? messageId,\n channelId: body.channelId,\n providerKey,\n externalThreadRef: conversation.externalConversationId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n await em.flush()\n }\n\n // Emit the hub event through the real event bus so the persistent customers\n // link-channel-message-received subscriber is enqueued to the `events` queue.\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n channelLinkId: link.id,\n channelId: body.channelId,\n providerKey,\n direction: 'inbound',\n tenantId,\n organizationId,\n },\n { persistent: true },\n )\n\n return NextResponse.json(\n { channelLinkId: link.id, messageId, conversationId: conversation.id },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary:\n 'Test-only: seed a connected channel, ingest a real inbound message, emit a seeded inbound link, or purge one (env-gated)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel seeded / inbound message emitted' },\n { status: 401, description: 'Unauthorized' },\n {\n status: 404,\n description:\n 'Test channel seeding disabled (production default), or the requested channel does not belong to the caller',\n },\n { status: 422, description: 'Invalid request body' },\n { status: 500, description: 'Seed failed' },\n ],\n },\n },\n}\n\nexport default POST\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../../../data/entities'\nimport { ChannelAccessDeniedError, assertCanManageChannel } from '../../../lib/access-control'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../commands/connect-credential-channel'\nimport { emitCommunicationChannelsEvent } from '../../../events'\nimport {\n TEST_SEED_CHAT_PROVIDER_KEY,\n TEST_SEED_PROVIDER_KEY,\n clearTestSeedCapturedMessages,\n createTestSeedPlatformMessage,\n deleteTestSeedPlatformMessage,\n ensureTestSeedAdapterRegistered,\n isTestEmailCaptureAccessAuthorized,\n isTestChannelSeedingEnabled,\n listTestSeedCapturedMessages,\n} from '../../../lib/test-seed'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n type IngestInboundMessageResult,\n} from '../../../commands/ingest-inbound-message'\n\n/**\n * TEST-ONLY channel seeding endpoint.\n *\n * Gated by `OM_ENABLE_TEST_CHANNEL_SEEDING` \u2014 when the flag is unset (the\n * production default) every request returns 404, so this route is invisible and\n * inert in production. See `lib/test-seed.ts` for the full rationale.\n *\n * Actions, all scoped to the caller's tenant/org:\n * - `connect-channel`: connect a network-free stub channel owned by the caller\n * (delegates to the real connect-credential command so the channel persists\n * credentials + lands in `status='connected'`). Enables the outbound\n * compose \u2192 deliver \u2192 `.sent` chain to complete in CI. `providerFlavor: 'chat'`\n * connects the non-email stub instead, for tests about sender identity, and\n * `labelAsProviderKey` relabels the connected row so a provider-scoped\n * listing has something to list without a live credential probe.\n * - `ingest-inbound`: run the REAL `ingest_inbound_message` command over an\n * adapter-normalized chat frame, so the platform compose path \u2014 and every\n * validation rule on it \u2014 actually executes. Use this for anything that\n * claims inbound works.\n * - `emit-inbound`: insert an inbound `MessageChannelLink` (+ a `messages.message`\n * row for threading) and emit `communication_channels.message.received` so the\n * customers link-channel-message subscriber runs against real Postgres. Enables\n * the inbound auto-link tests (TC-CRM-EMAIL-002..005). NOTE: this action\n * deliberately bypasses `messages.messages.compose` \u2014 it can never prove that\n * the hub accepts a message, only that downstream subscribers fire.\n * - `purge-inbound`: hard-delete the rows one test-seeded or test-ingested\n * inbound message created, for integration teardown.\n */\ntype RbacServiceLike = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<{ isSuperAdmin: boolean; features: string[]; organizations: string[] | null }>\n}\n\nexport const metadata = {\n path: '/communication_channels/test-seed',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_user_channel'],\n },\n}\n\nconst addressObjectSchema = z.object({ address: z.string(), name: z.string().optional() })\nconst addressFieldSchema = z.union([\n z.string(),\n addressObjectSchema,\n z.array(z.union([z.string(), addressObjectSchema])),\n])\n\nconst connectChannelSchema = z.object({\n action: z.literal('connect-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n /**\n * Which stub provider to connect. `email` (default) keeps the historical\n * email-shaped channel; `chat` connects a channel whose senders have no\n * address, so a test can exercise the hub's non-email identity contract\n * without inventing one (#4975).\n */\n providerFlavor: z.enum(['email', 'chat']).optional(),\n /**\n * TEST-ONLY: rewrite the connected stub channel's `provider_key` to this value.\n *\n * Routes that a provider package owns filter on their own `provider_key`, and\n * a real channel for those providers cannot be connected in CI \u2014 the Discord\n * adapter's `validateCredentials` performs a live API call against a bot token\n * no test environment has. Without this, a provider-scoped listing has nothing\n * to list and can only be asserted against an empty result, which is exactly\n * the assertion that would have stayed green through #5602.\n *\n * It relabels a row; it registers nothing. The stub adapter is still the only\n * adapter present, so the channel remains network-free \u2014 but no test should\n * drive an outbound send through a relabelled channel, because delivery would\n * then resolve the real provider's adapter.\n */\n labelAsProviderKey: z\n .string()\n .min(1)\n .max(64)\n .regex(/^[a-z0-9_-]+$/)\n .optional(),\n})\n\n/**\n * Drive the REAL `ingest_inbound_message` command with a chat-shaped frame.\n *\n * `emit-inbound` below deliberately bypasses the platform compose path (it\n * inserts the `messages` row with raw SQL) because it only ever needed a\n * landing zone for the CRM-link subscribers. That bypass is why an inbound test\n * could pass while `composeMessageSchema` rejected every real message (#4975).\n * This action takes the opposite approach: it hands the frame to the adapter and\n * the ingest command and asserts nothing on the way, so whatever the hub's\n * contract really is, the test feels it.\n */\nconst ingestInboundSchema = z.object({\n action: z.literal('ingest-inbound'),\n channelId: z.string().uuid(),\n /**\n * Opaque sender handle \u2014 a Discord snowflake, a Slack member id, etc. For a\n * channel connected with the default `email` flavor this is the sender's\n * address, and the email stub normalizes it into an email-shaped frame.\n */\n senderIdentifier: z.string().min(1).max(255),\n senderDisplayName: z.string().max(255).optional(),\n /** Honored by the email stub only; chat frames carry no subject. */\n subject: z.string().max(500).optional(),\n body: z.string().max(50_000).optional(),\n externalMessageId: z.string().min(1).max(255),\n externalConversationId: z.string().min(1).max(255),\n})\n\nconst seedSystemChannelSchema = z.object({\n action: z.literal('seed-system-channel'),\n displayName: z.string().min(1).max(255).optional(),\n externalIdentifier: z.string().min(1).max(255).optional(),\n})\n\nconst clearCaptureSchema = z.object({\n action: z.literal('clear-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst listCaptureSchema = z.object({\n action: z.literal('list-capture'),\n systemRecipient: z.string().email().max(320).optional(),\n captureCorrelationToken: z.string().min(32).max(512).optional(),\n})\n\nconst emitInboundSchema = z.object({\n action: z.literal('emit-inbound'),\n /** Channel that owns the inbound message; controls authorUserId + default visibility. */\n channelId: z.string().uuid(),\n /** Provider key persisted on the link (defaults to the stub provider). */\n providerKey: z.string().min(1).max(64).optional(),\n /** Normalized inbound addresses (stored under channelPayload). */\n from: addressFieldSchema.optional(),\n to: addressFieldSchema.optional(),\n cc: addressFieldSchema.optional(),\n subject: z.string().max(500).optional(),\n bodyText: z.string().max(200_000).optional(),\n /** RFC2822 Message-ID of this inbound message (for In-Reply-To matching). */\n messageId: z.string().max(500).optional(),\n /** RFC2822 In-Reply-To header (threading-inheritance fallback). */\n inReplyTo: z.string().max(500).optional(),\n references: z.array(z.string().max(500)).max(50).optional(),\n /**\n * Open Mercato `messages.message` thread id this inbound message belongs to.\n * When set, a `messages.message` row is created with this `threadId` so the\n * hub-thread inheritance join can resolve a Person from a sibling message.\n */\n messageThreadId: z.string().uuid().optional(),\n /**\n * Test-only: also create a `ChannelThreadMapping` for the seeded thread. The\n * reaction (`/messages/[id]/reactions`) and thread-assign\n * (`/threads/[id]/assign`) routes resolve the owning channel through this\n * mapping and return 409/404 without it. Opt-in so the existing CRM-link\n * seeds (which don't need a mapping) keep their current mapping-free shape.\n */\n createThreadMapping: z.boolean().optional(),\n})\n\n/**\n * Teardown for test-seeded or test-ingested inbound records: hard-delete the\n * thread mapping, channel link, external message, synthetic conversation and\n * platform message, scoped to the caller's channel. Disconnecting a channel\n * deliberately retains these records, so without this every test leaks into the\n * shared integration database.\n */\nconst purgeInboundSchema = z.object({\n action: z.literal('purge-inbound'),\n channelId: z.string().uuid(),\n channelLinkId: z.string().uuid(),\n messageId: z.string().uuid(),\n conversationId: z.string().uuid(),\n})\n\n/**\n * Read-only: report the channel-side rows a platform message carries.\n *\n * The threading contract lives in rows no API exposes \u2014 an integration test\n * asserting that a composer send really was routed through the hub can only\n * observe them here.\n */\nconst inspectMessageLinksSchema = z.object({\n action: z.literal('inspect-message-links'),\n messageId: z.string().uuid(),\n})\n\nconst bodySchema = z.discriminatedUnion('action', [\n connectChannelSchema,\n inspectMessageLinksSchema,\n ingestInboundSchema,\n emitInboundSchema,\n purgeInboundSchema,\n seedSystemChannelSchema,\n clearCaptureSchema,\n listCaptureSchema,\n])\n\nconst logger = createLogger('communication_channels').child({ component: 'test-seed' })\n\nconst TEST_SEED_CONVERSATION_PREFIX = 'inbound-seed:'\n\nfunction ingestFailureResponse(err: unknown): Response {\n if (err instanceof z.ZodError) {\n return NextResponse.json({ error: err.message, issues: err.issues }, { status: 422 })\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('ingest-inbound failed', { err })\n return NextResponse.json(\n { error: err instanceof Error ? err.message : '[internal] ingest-inbound failed' },\n { status: 500 },\n )\n}\n\nexport async function POST(req: Request): Promise<Response> {\n // Fail-closed: invisible in production. Mirrors an unknown route (404) rather\n // than 403 so the surface leaks nothing when the flag is off.\n if (!isTestChannelSeedingEnabled()) {\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n body = bodySchema.parse(await readJsonSafe(req, null))\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const tenantId = auth.tenantId as string\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n const userId = auth.sub as string\n const captureScope = { tenantId, organizationId }\n\n if (\n (body.action === 'clear-capture' || body.action === 'list-capture')\n && body.systemRecipient\n && (\n !body.captureCorrelationToken\n || !isTestEmailCaptureAccessAuthorized(req.headers.get('x-om-test-email-capture-access-token'))\n )\n ) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n\n if (body.action === 'clear-capture') {\n await clearTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })\n return NextResponse.json({ ok: true })\n }\n\n if (body.action === 'list-capture') {\n return NextResponse.json({\n items: (await listTestSeedCapturedMessages(captureScope, {\n systemRecipient: body.systemRecipient,\n captureCorrelationToken: body.captureCorrelationToken,\n })).map(({ captureCorrelationToken: _captureCorrelationToken, ...message }) => message),\n })\n }\n\n const container = await createRequestContainer()\n // Defensive: make sure the stub adapter is registered for this process even if\n // a worker-only node skipped module di registration.\n ensureTestSeedAdapterRegistered()\n\n if (body.action === 'inspect-message-links') {\n const em = (container.resolve('em') as EntityManager).fork()\n const links = await em.find(MessageChannelLink, {\n messageId: body.messageId,\n tenantId,\n organizationId,\n })\n const conversationIds = links\n .map((link) => link.externalConversationId)\n .filter((id): id is string => Boolean(id))\n const mappings = conversationIds.length\n ? await em.find(ChannelThreadMapping, {\n externalConversationId: { $in: conversationIds },\n tenantId,\n organizationId,\n })\n : []\n return NextResponse.json({\n links: links.map((link) => ({\n id: link.id,\n direction: link.direction,\n providerKey: link.providerKey,\n channelType: link.channelType,\n deliveryStatus: link.deliveryStatus,\n externalConversationId: link.externalConversationId,\n })),\n threadMappings: mappings.map((mapping) => ({\n id: mapping.id,\n messageThreadId: mapping.messageThreadId,\n channelId: mapping.channelId,\n externalConversationId: mapping.externalConversationId,\n })),\n })\n }\n\n if (body.action === 'seed-system-channel') {\n const em = (container.resolve('em') as EntityManager).fork()\n const credentialsService = container.resolve('integrationCredentialsService') as {\n save: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n }\n await credentialsService.save(\n `channel_${TEST_SEED_PROVIDER_KEY}`,\n { testSeed: true },\n { tenantId, organizationId: organizationId ?? tenantId, userId: null },\n )\n let channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n tenantId,\n organizationId,\n userId: null,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!channel) {\n const stamp = Date.now()\n channel = em.create(CommunicationChannel, {\n providerKey: TEST_SEED_PROVIDER_KEY,\n channelType: 'email',\n displayName: body.displayName ?? `Test Seed System Email ${stamp}`,\n externalIdentifier: body.externalIdentifier ?? `system-${stamp}@test-seed.local`,\n userId: null,\n isPrimary: false,\n isActive: true,\n status: 'connected',\n tenantId,\n organizationId,\n })\n em.persist(channel)\n await em.flush()\n } else if (!channel.isActive || channel.status !== 'connected') {\n channel.isActive = true\n channel.status = 'connected'\n channel.lastError = null\n await em.flush()\n }\n\n return NextResponse.json({ channelId: channel.id }, { status: 201 })\n }\n\n if (body.action === 'connect-channel') {\n const stamp = Date.now()\n const commandBus = container.resolve('commandBus') as CommandBus\n const isChatFlavor = body.providerFlavor === 'chat'\n // A chat channel is deliberately connected WITHOUT an email-ish credential\n // key, so `connect-credential-channel` derives no `externalIdentifier` and\n // the row is shaped exactly like a real Discord channel (identifier NULL \u2014\n // the condition #4977 describes). Inventing one here would recreate the\n // fixture dishonesty that hid #4975.\n const credentials = isChatFlavor\n ? { handle: body.externalIdentifier ?? `test-seed-chat-${stamp}` }\n : {\n username: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n fromAddress: body.externalIdentifier ?? `test-seed-${stamp}@test-seed.local`,\n }\n const input: ConnectCredentialChannelInput = {\n providerKey: isChatFlavor ? TEST_SEED_CHAT_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY,\n displayName:\n body.displayName ?? `Test Seed ${isChatFlavor ? 'Chat ' : ''}Channel ${stamp}`,\n credentials,\n userId,\n scope: { tenantId, organizationId },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n if (result.status !== 'connected') {\n return NextResponse.json(\n { error: '[internal] test-seed connect failed', detail: result },\n { status: 500 },\n )\n }\n if (body.labelAsProviderKey && body.labelAsProviderKey !== input.providerKey) {\n const seedEm = (container.resolve('em') as EntityManager).fork()\n const connected = await findOneWithDecryption(\n seedEm,\n CommunicationChannel,\n { id: result.channelId, tenantId },\n undefined,\n { tenantId, organizationId },\n )\n if (!connected) {\n return NextResponse.json(\n { error: '[internal] test-seed could not reload the channel it just connected' },\n { status: 500 },\n )\n }\n connected.providerKey = body.labelAsProviderKey\n await seedEm.flush()\n }\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n providerKey: body.labelAsProviderKey ?? input.providerKey,\n },\n { status: 201 },\n )\n }\n\n // action === 'ingest-inbound' | 'emit-inbound' | 'purge-inbound' \u2014 all address an existing channel.\n const em = (container.resolve('em') as EntityManager).fork()\n // Only the `emit-inbound` branch stamps a caller-chosen provider key onto the\n // rows it seeds; `ingest-inbound` takes the channel's own (see below).\n const providerKey =\n body.action === 'emit-inbound' ? body.providerKey ?? TEST_SEED_PROVIDER_KEY : TEST_SEED_PROVIDER_KEY\n\n // `channelId` is caller-supplied, so confirm it names a channel this tenant/org\n // actually owns before seeding rows that reference it. Mirrors the ownership\n // lookup every other per-channel route performs (see channels/[id]/test-send).\n const ownedChannel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: body.channelId,\n tenantId,\n organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId, organizationId },\n )\n if (!ownedChannel) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n // Tenant/org scope alone does not authorize: `connect_user_channel` is granted\n // broadly, so a same-tenant caller could otherwise seed against a colleague's\n // personal mailbox. Enforce the module's owner-only contract \u2014 personal\n // channels are owner-restricted, shared channels need `manage`.\n let userFeatures: string[] = []\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike\n const acl = await rbac.loadAcl(userId, { tenantId, organizationId })\n userFeatures = acl?.isSuperAdmin ? ['*'] : Array.isArray(acl?.features) ? acl.features : []\n } catch {\n userFeatures = []\n }\n try {\n assertCanManageChannel(\n { userId: ownedChannel.userId },\n userId,\n userFeatures,\n 'communication_channels.manage',\n )\n } catch (err) {\n if (err instanceof ChannelAccessDeniedError) {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n const status = (err as { statusCode?: number }).statusCode ?? 403\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Access denied' },\n { status },\n )\n }\n\n if (body.action === 'purge-inbound') {\n const rowScope = { tenantId, organizationId }\n // Only rows hanging off a test-prefixed conversation are purgeable, so a\n // caller cannot use this action to hard-delete real channel traffic.\n const seededConversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n id: body.conversationId,\n channelId: body.channelId,\n externalConversationId: { $like: `${TEST_SEED_CONVERSATION_PREFIX}%` },\n ...rowScope,\n },\n undefined,\n rowScope,\n )\n if (!seededConversation) {\n return NextResponse.json({ error: 'seeded inbound conversation not found' }, { status: 404 })\n }\n await em.nativeDelete(ChannelThreadMapping, {\n externalConversationId: body.conversationId,\n channelId: body.channelId,\n ...rowScope,\n })\n await em.nativeDelete(MessageChannelLink, {\n id: body.channelLinkId,\n externalConversationId: body.conversationId,\n ...rowScope,\n })\n await em.nativeDelete(ExternalMessage, {\n conversationId: body.conversationId,\n channelId: body.channelId,\n ...rowScope,\n })\n await em.nativeDelete(ExternalConversation, {\n id: body.conversationId,\n channelId: body.channelId,\n ...rowScope,\n })\n await deleteTestSeedPlatformMessage(em, {\n messageId: body.messageId,\n channelId: body.channelId,\n conversationId: body.conversationId,\n ...rowScope,\n })\n return NextResponse.json({ ok: true })\n }\n\n if (body.action === 'ingest-inbound') {\n // Everything below this point is the real path: the adapter normalizes the\n // frame and `ingest_inbound_message` composes the platform message through\n // `messages.messages.compose`. Nothing is short-circuited, so a hub contract\n // the provider cannot satisfy surfaces here as a failure instead of hiding\n // behind seeded rows (#4975).\n // The channel's own provider key, never a hardcoded one: ingest does not\n // check that `providerKey` matches the channel it names, so passing a\n // different one would silently stamp the wrong provider onto the link.\n const channelProviderKey = ownedChannel.providerKey\n if (\n channelProviderKey !== TEST_SEED_CHAT_PROVIDER_KEY\n && channelProviderKey !== TEST_SEED_PROVIDER_KEY\n ) {\n return NextResponse.json(\n {\n error:\n 'ingest-inbound requires a channel connected through the test-seed stub; ' +\n `channel ${body.channelId} is '${channelProviderKey}'`,\n },\n { status: 422 },\n )\n }\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const adapterRegistry = container.resolve('channelAdapterRegistry') as {\n get: (key: string) => { normalizeInbound: (raw: unknown) => Promise<unknown> } | undefined\n }\n const adapter = adapterRegistry.get(channelProviderKey)\n if (!adapter) {\n return NextResponse.json(\n { error: '[internal] test-seed chat adapter is not registered' },\n { status: 500 },\n )\n }\n\n let normalized: unknown\n try {\n normalized = await adapter.normalizeInbound({\n raw: {\n externalMessageId: body.externalMessageId,\n externalConversationId: body.externalConversationId,\n senderIdentifier: body.senderIdentifier,\n senderDisplayName: body.senderDisplayName,\n ...(body.subject !== undefined ? { subject: body.subject } : {}),\n body: body.body ?? '',\n },\n eventType: 'message',\n metadata: {},\n })\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : '[internal] adapter rejected the inbound frame' },\n { status: 422 },\n )\n }\n\n const ingestInput = {\n channelId: body.channelId,\n providerKey: channelProviderKey,\n channelType: ownedChannel.channelType,\n scope: { tenantId, organizationId },\n message: normalized,\n } as IngestInboundMessageInput\n\n let result: IngestInboundMessageResult\n try {\n const execution = await commandBus.execute<\n IngestInboundMessageInput,\n IngestInboundMessageResult\n >(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input: ingestInput,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n })\n result = execution.result\n } catch (err) {\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return ingestFailureResponse(err)\n }\n\n return NextResponse.json(\n {\n status: result.status,\n messageId: result.messageId ?? null,\n conversationId: result.externalConversationId ?? null,\n channelLinkId: result.channelLinkId ?? null,\n channelType: ownedChannel.channelType,\n },\n { status: 201 },\n )\n }\n\n // A MessageChannelLink requires a non-null external_conversation_id (FK) and\n // message_id. Create a synthetic conversation + (optionally threaded) message\n // so the link is shaped like a real inbound row the subscriber can consume.\n const conversation = em.create(ExternalConversation, {\n channelId: body.channelId,\n externalConversationId: `${TEST_SEED_CONVERSATION_PREFIX}${Date.now()}:${Math.random().toString(16).slice(2, 8)}`,\n subject: body.subject ?? null,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n\n const messageId = await createTestSeedPlatformMessage(em, {\n providerKey,\n threadId: body.messageThreadId,\n senderUserId: userId,\n subject: body.subject,\n bodyText: body.bodyText,\n channelId: body.channelId,\n tenantId,\n organizationId,\n })\n if (!messageId) {\n return NextResponse.json({ error: '[internal] failed to seed message row' }, { status: 500 })\n }\n\n const link = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey,\n channelType: 'email',\n direction: 'inbound',\n deliveryStatus: 'delivered',\n channelPayload: {\n ...(body.from !== undefined ? { from: body.from } : {}),\n ...(body.to !== undefined ? { to: body.to } : {}),\n ...(body.cc !== undefined ? { cc: body.cc } : {}),\n ...(body.subject !== undefined ? { subject: body.subject } : {}),\n ...(body.bodyText !== undefined ? { text: body.bodyText } : {}),\n ...(body.inReplyTo !== undefined ? { inReplyTo: body.inReplyTo } : {}),\n ...(body.references !== undefined ? { references: body.references } : {}),\n },\n channelContentType: 'text/plain',\n channelMetadata: {\n ...(body.messageId !== undefined ? { messageId: body.messageId } : {}),\n },\n tenantId,\n organizationId,\n })\n em.persist(link)\n await em.flush()\n\n // Optionally mirror `ingest-inbound-message`: a real inbound message always\n // lands a ChannelThreadMapping that the reaction + thread-assign routes use to\n // resolve the owning channel. Seeded inbound messages skip it by default; opt\n // in for tests that exercise those routes. Keyed by `messageThreadId ?? messageId`\n // to match how those commands resolve the mapping (`message.threadId ?? message.id`).\n if (body.createThreadMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: body.messageThreadId ?? messageId,\n channelId: body.channelId,\n providerKey,\n externalThreadRef: conversation.externalConversationId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n await em.flush()\n }\n\n // Emit the hub event through the real event bus so the persistent customers\n // link-channel-message-received subscriber is enqueued to the `events` queue.\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n channelLinkId: link.id,\n channelId: body.channelId,\n providerKey,\n direction: 'inbound',\n tenantId,\n organizationId,\n },\n { persistent: true },\n )\n\n return NextResponse.json(\n { channelLinkId: link.id, messageId, conversationId: conversation.id },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary:\n 'Test-only: seed a connected channel, ingest a real inbound message, emit a seeded inbound link, or purge one (env-gated)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel seeded / inbound message emitted' },\n { status: 401, description: 'Unauthorized' },\n {\n status: 404,\n description:\n 'Test channel seeding disabled (production default), or the requested channel does not belong to the caller',\n },\n { status: 422, description: 'Invalid request body' },\n { status: 500, description: 'Seed failed' },\n ],\n },\n },\n}\n\nexport default POST\n"],
5
+ "mappings": "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",
6
6
  "names": ["em", "link"]
7
7
  }
@@ -43,11 +43,20 @@ async function deleteTestSeedPlatformMessage(em, input) {
43
43
  await em.getConnection().execute(
44
44
  `DELETE FROM messages
45
45
  WHERE id = ?
46
- AND source_entity_type = 'communication_channels.test_seed_inbound'
47
- AND source_entity_id = ?
46
+ AND (
47
+ (source_entity_type = 'communication_channels.test_seed_inbound' AND source_entity_id = ?)
48
+ OR
49
+ (source_entity_type = 'communication_channels.external_conversation' AND source_entity_id = ?)
50
+ )
48
51
  AND tenant_id = ?
49
52
  AND organization_id IS NOT DISTINCT FROM ?`,
50
- [input.messageId, input.channelId, input.tenantId, input.organizationId]
53
+ [
54
+ input.messageId,
55
+ input.channelId,
56
+ input.conversationId,
57
+ input.tenantId,
58
+ input.organizationId
59
+ ]
51
60
  );
52
61
  }
53
62
  function normalizeToken(value) {
@@ -172,8 +181,32 @@ class TestSeedChannelAdapter {
172
181
  metadata: input.channelMetadata ?? {}
173
182
  };
174
183
  }
175
- async normalizeInbound(_raw) {
176
- throw new Error("[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness");
184
+ async normalizeInbound(raw) {
185
+ const frame = raw.raw ?? {};
186
+ const senderIdentifier = String(frame.senderIdentifier ?? "").trim();
187
+ if (!senderIdentifier.includes("@")) {
188
+ throw new Error("[internal] TestSeedChannelAdapter requires an email-shaped senderIdentifier");
189
+ }
190
+ const subject = typeof frame.subject === "string" ? frame.subject : void 0;
191
+ const body = typeof frame.body === "string" ? frame.body : "";
192
+ const senderDisplayName = typeof frame.senderDisplayName === "string" ? frame.senderDisplayName : void 0;
193
+ return {
194
+ externalMessageId: String(frame.externalMessageId ?? ""),
195
+ externalConversationId: String(frame.externalConversationId ?? ""),
196
+ senderIdentifier,
197
+ senderDisplayName,
198
+ subject,
199
+ body,
200
+ bodyFormat: "text",
201
+ timestamp: /* @__PURE__ */ new Date(),
202
+ channelPayload: {
203
+ from: { address: senderIdentifier, ...senderDisplayName ? { name: senderDisplayName } : {} },
204
+ ...subject !== void 0 ? { subject } : {},
205
+ text: body
206
+ },
207
+ channelContentType: "text/plain",
208
+ channelMetadata: {}
209
+ };
177
210
  }
178
211
  async validateCredentials(_input) {
179
212
  return { ok: true };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/communication_channels/lib/test-seed.ts"],
4
- "sourcesContent": ["import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { timingSafeEqual } from 'node:crypto'\nimport path from 'node:path'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type {\n ChannelAdapter,\n ChannelCapabilities,\n ChannelNativeContent,\n ConvertOutboundInput,\n GetMessageStatusInput,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n SendMessageInput,\n SendMessageResult,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from './adapter'\nimport { baseEmailCapabilities } from './email-capabilities'\nimport { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'\nimport { registerSystemEmailProviderConfigResolver } from './system-email-provider-config'\n\n/**\n * Test-only channel seeding support.\n *\n * The ephemeral integration harness cannot connect a REAL email channel:\n * - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server\n * (none exists in CI), so `POST /channels/connect/credentials` returns 422.\n * - Even with a connected channel, the outbound delivery worker calls the real\n * SMTP adapter, which fails with no server \u2014 so `communication_channels.message.sent`\n * never fires and the customers link subscriber never runs.\n *\n * To make the compose \u2192 deliver \u2192 `.sent` \u2192 CRM-link \u2192 cross-user-visibility chain\n * (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable\n * end-to-end against real Postgres, this module provides a network-free stub adapter\n * that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.\n *\n * Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};\n * when the env flag is unset (the production default) the adapter is never registered\n * and the `__test_seed__` provider key resolves to no adapter \u2014 so the connect route\n * returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated\n * test-seed API route enforces the same gate independently (fail-closed 404 in prod).\n */\n\n/** Provider key for the network-free test stub adapter. */\nexport const TEST_SEED_PROVIDER_KEY = '__test_seed__'\n\n/**\n * Provider key for the network-free stub adapter that stands in for a CHAT\n * provider \u2014 one whose senders are identified by an opaque handle and have no\n * email address at all (Discord, Slack, Telegram\u2026).\n *\n * It exists because the email-shaped stub above can only ever prove the hub\n * accepts email-shaped data. That is precisely how CI stayed green while every\n * real inbound Discord message was rejected (#4975): the fixture invented an\n * address the provider can never produce. Tests that need to prove the hub's\n * non-email identity contract MUST drive this provider instead.\n */\nexport const TEST_SEED_CHAT_PROVIDER_KEY = '__test_seed_chat__'\n\n/** Env flag that unlocks test-only channel seeding. Off in production. */\nexport const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'\nexport const TEST_EMAIL_CAPTURE_ACCESS_TOKEN_ENV = 'OM_TEST_EMAIL_CAPTURE_ACCESS_TOKEN'\nexport const TEST_EMAIL_CAPTURE_CORRELATION_TOKEN_ENV = 'OM_TEST_EMAIL_CAPTURE_CORRELATION_TOKEN'\n/**\n * Where the Hub's tenant-scoped capture is written.\n *\n * Deliberately not `OM_TEST_EMAIL_CAPTURE_PATH`: that one belongs to the unscoped capture in\n * `@open-mercato/shared/lib/email/send`, which writes a different record shape to a path the repo\n * also ships a committed fixture for. Pointing both mechanisms at one file made this module parse\n * foreign records and let `clear-capture` rewrite the other mechanism's fixtures. Two mechanisms,\n * two files.\n */\nexport const TEST_SYSTEM_EMAIL_CAPTURE_PATH_ENV = 'OM_TEST_SYSTEM_EMAIL_CAPTURE_PATH'\n\n/**\n * True only when the test-seeding env flag is explicitly enabled. Accepts the\n * usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a\n * plain `=true`. Any other value (including unset) is treated as disabled.\n */\nexport function isTestChannelSeedingEnabled(): boolean {\n const raw = process.env[TEST_CHANNEL_SEEDING_ENV]\n if (typeof raw !== 'string') return false\n return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())\n}\n\nexport type TestSeedCapturedMessage = {\n capturedAt: string\n externalMessageId: string\n conversationId?: string\n content: SendMessageInput['content']\n scope: SendMessageInput['scope']\n metadata?: SendMessageInput['metadata']\n captureCorrelationToken?: string\n}\n\ntype TestSeedCaptureScope = {\n tenantId: string\n organizationId: string | null\n}\n\nexport type TestSeedCaptureOptions = {\n systemRecipient?: string\n captureCorrelationToken?: string\n}\n\nexport async function createTestSeedPlatformMessage(\n em: EntityManager,\n input: {\n providerKey: string\n threadId?: string\n senderUserId: string\n subject?: string\n bodyText?: string\n channelId: string\n tenantId: string\n organizationId: string | null\n },\n): Promise<string | null> {\n const rows = (await em.getConnection().execute(\n `INSERT INTO messages\n (type, thread_id, sender_user_id, subject, body, body_format, priority, status,\n is_draft, sent_at, visibility, source_entity_type, source_entity_id,\n tenant_id, organization_id, created_at, updated_at)\n VALUES\n (?, ?, ?, ?, ?, 'text', 'normal', 'sent',\n false, now(), 'public', 'communication_channels.test_seed_inbound', ?,\n ?, ?, now(), now())\n RETURNING id`,\n [\n `channel.${input.providerKey}`,\n input.threadId ?? null,\n input.senderUserId,\n input.subject ?? '(no subject)',\n input.bodyText ?? '',\n input.channelId,\n input.tenantId,\n input.organizationId,\n ],\n )) as Array<{ id: string }>\n return rows[0]?.id ?? null\n}\n\n/**\n * Hard-delete a `messages` row seeded by {@link createTestSeedPlatformMessage}.\n * Matches only rows stamped as test-seed inbound for the given channel and\n * scope, so integration teardown can never remove a real message.\n */\nexport async function deleteTestSeedPlatformMessage(\n em: EntityManager,\n input: {\n messageId: string\n channelId: string\n tenantId: string\n organizationId: string | null\n },\n): Promise<void> {\n await em.getConnection().execute(\n `DELETE FROM messages\n WHERE id = ?\n AND source_entity_type = 'communication_channels.test_seed_inbound'\n AND source_entity_id = ?\n AND tenant_id = ?\n AND organization_id IS NOT DISTINCT FROM ?`,\n [input.messageId, input.channelId, input.tenantId, input.organizationId],\n )\n}\n\nfunction normalizeToken(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const normalized = value.trim()\n return normalized.length >= 32 ? normalized : null\n}\n\nexport function isTestEmailCaptureAccessAuthorized(providedToken: string | null): boolean {\n const expectedToken = normalizeToken(process.env[TEST_EMAIL_CAPTURE_ACCESS_TOKEN_ENV])\n const normalizedProvidedToken = normalizeToken(providedToken)\n if (!expectedToken || !normalizedProvidedToken) return false\n const expected = Buffer.from(expectedToken)\n const provided = Buffer.from(normalizedProvidedToken)\n return expected.length === provided.length && timingSafeEqual(expected, provided)\n}\n\nexport function resolveTestEmailCaptureCorrelationToken(): string | null {\n return normalizeToken(process.env[TEST_EMAIL_CAPTURE_CORRELATION_TOKEN_ENV])\n}\n\nfunction resolveCapturePath(): string {\n const explicit = process.env[TEST_SYSTEM_EMAIL_CAPTURE_PATH_ENV]?.trim()\n if (explicit) return path.resolve(explicit)\n const queueBaseDir = process.env.QUEUE_BASE_DIR?.trim()\n if (queueBaseDir) return path.resolve(queueBaseDir, '..', 'test-email-capture.jsonl')\n return path.resolve(process.cwd(), '.mercato', 'test-email-capture.jsonl')\n}\n\nfunction matchesCaptureScope(\n message: TestSeedCapturedMessage,\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): boolean {\n if (message.scope.tenantId === scope.tenantId) {\n const messageOrganizationId = message.scope.organizationId ?? null\n return messageOrganizationId === scope.organizationId || messageOrganizationId === scope.tenantId\n }\n\n if (\n message.scope.tenantId !== 'system' ||\n message.scope.organizationId !== 'system' ||\n !options.systemRecipient ||\n !options.captureCorrelationToken ||\n message.captureCorrelationToken !== options.captureCorrelationToken\n ) {\n return false\n }\n\n const expectedRecipient = options.systemRecipient.trim().toLowerCase()\n const matchesRecipient = (value: unknown): boolean => {\n if (typeof value === 'string') return value.trim().toLowerCase() === expectedRecipient\n if (Array.isArray(value)) return value.some(matchesRecipient)\n if (!value || typeof value !== 'object') return false\n return matchesRecipient((value as Record<string, unknown>).address)\n }\n\n return matchesRecipient(message.metadata?.to)\n}\n\nasync function readTestSeedCapturedMessages(): Promise<TestSeedCapturedMessage[]> {\n const capturePath = resolveCapturePath()\n const text = await readFile(capturePath, 'utf8').catch(() => '')\n return text\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line) => {\n try {\n return JSON.parse(line) as TestSeedCapturedMessage\n } catch {\n return null\n }\n })\n // A record without a scope is not ours. Skipping instead of throwing keeps a stray or\n // foreign-shaped line from turning every capture read into a 500.\n .filter((message): message is TestSeedCapturedMessage => Boolean(message?.scope))\n}\n\nexport async function clearTestSeedCapturedMessages(\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): Promise<void> {\n const capturePath = resolveCapturePath()\n const retained = (await readTestSeedCapturedMessages())\n .filter((message) => !matchesCaptureScope(message, scope, options))\n if (retained.length === 0) {\n await rm(capturePath, { force: true })\n return\n }\n await writeFile(\n capturePath,\n `${retained.map((message) => JSON.stringify(message)).join('\\n')}\\n`,\n 'utf8',\n )\n}\n\nexport async function listTestSeedCapturedMessages(\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): Promise<TestSeedCapturedMessage[]> {\n return (await readTestSeedCapturedMessages()).filter((message) =>\n matchesCaptureScope(message, scope, options),\n )\n}\n\nasync function captureTestSeedMessage(record: TestSeedCapturedMessage): Promise<void> {\n const capturePath = resolveCapturePath()\n await mkdir(path.dirname(capturePath), { recursive: true })\n await appendFile(capturePath, `${JSON.stringify(record)}\\n`, 'utf8')\n}\n\n/**\n * Capabilities for the stub: an email channel that supports neither reactions,\n * edit/delete, nor conversation history \u2014 so the strict registry validator\n * (`validateAdapterCapabilities`) requires only the core method surface.\n */\nconst testSeedCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n conversationHistory: false,\n realtimePush: false,\n}\n\n/**\n * A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any\n * network I/O. Used exclusively by the integration harness to let the outbound\n * delivery worker reach its success path and emit `communication_channels.message.sent`.\n */\nclass TestSeedChannelAdapter implements ChannelAdapter {\n // Widened to `string` rather than inferred as a literal so the chat-flavoured\n // subclass below can override both with its own provider key / channel type.\n readonly providerKey: string = TEST_SEED_PROVIDER_KEY\n readonly channelType: string = 'email'\n readonly capabilities = testSeedCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n // Synthesize a deterministic-looking RFC2822-style message id; never touches\n // the network. The delivery worker persists this as the external message id.\n const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`\n await captureTestSeedMessage({\n capturedAt: new Date().toISOString(),\n externalMessageId,\n conversationId: input.conversationId,\n content: input.content,\n scope: input.scope,\n metadata: input.metadata,\n captureCorrelationToken: resolveTestEmailCaptureCorrelationToken() ?? undefined,\n })\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'sent',\n metadata: { testSeed: true },\n }\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n // No real webhook \u2014 return the inert event so the generic webhook route 202s\n // without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).\n return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return {\n content: {\n text: input.body,\n bodyFormat: input.bodyFormat,\n },\n metadata: input.channelMetadata ?? {},\n }\n }\n\n async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // The test-seed inbound path seeds MessageChannelLink rows directly and emits\n // the hub event, so this adapter never normalizes a raw inbound payload.\n throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')\n }\n\n async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n // No real server to authenticate against \u2014 accept any credentials so the\n // connect command persists a connected channel.\n return { ok: true }\n }\n}\n\n/**\n * Capabilities for the chat stub. Identical to the email stub's except for the\n * recipient shape: a chat channel is addressed by a provider-issued identifier\n * (a Discord snowflake), not an email address.\n *\n * Without this override the chat stub inherited `baseEmailCapabilities`, so it\n * claimed `channelType: 'discord'` while validating recipients as email\n * addresses \u2014 a channel that could not be addressed the way the provider it\n * imitates actually is. That left the hub's `'provider-native'` branch\n * (#4976: per-provider recipient validation, and the omitted recipient that\n * falls back to the adapter's own target) reachable only with a live Discord\n * bot, so no integration test could cover it and CI never exercised it.\n */\nconst testSeedChatCapabilities: ChannelCapabilities = {\n ...testSeedCapabilities,\n recipientFormat: 'provider-native',\n}\n\n/**\n * Chat-flavoured twin of {@link TestSeedChannelAdapter}: same network-free\n * behaviour, but it declares a non-email `channelType`, so a channel connected\n * through it is shaped like a real chat channel \u2014 including an\n * `externalIdentifier` of NULL when no email-ish credential key is supplied.\n */\nclass TestSeedChatChannelAdapter extends TestSeedChannelAdapter {\n readonly providerKey: string = TEST_SEED_CHAT_PROVIDER_KEY\n readonly channelType: string = 'discord'\n readonly capabilities = testSeedChatCapabilities\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // Unlike the email stub, this one is reachable: the test-seed ingest action\n // feeds it a chat-shaped frame so the message travels the real ingest path\n // (and therefore the real compose validation) rather than a SQL shortcut.\n const frame = (raw.raw ?? {}) as Record<string, unknown>\n const senderIdentifier = String(frame.senderIdentifier ?? '')\n if (!senderIdentifier) {\n throw new Error('[internal] TestSeedChatChannelAdapter requires a senderIdentifier')\n }\n return {\n externalMessageId: String(frame.externalMessageId ?? ''),\n externalConversationId: String(frame.externalConversationId ?? ''),\n senderIdentifier,\n senderDisplayName:\n typeof frame.senderDisplayName === 'string' ? frame.senderDisplayName : undefined,\n body: typeof frame.body === 'string' ? frame.body : '',\n bodyFormat: 'text',\n timestamp: new Date(),\n channelPayload: {},\n channelContentType: 'text/plain',\n channelMetadata: {},\n }\n }\n}\n\nlet cachedTestSeedAdapter: TestSeedChannelAdapter | null = null\nlet cachedTestSeedChatAdapter: TestSeedChatChannelAdapter | null = null\n\nfunction getTestSeedChannelAdapter(): TestSeedChannelAdapter {\n if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()\n return cachedTestSeedAdapter\n}\n\nfunction getTestSeedChatChannelAdapter(): TestSeedChatChannelAdapter {\n if (!cachedTestSeedChatAdapter) cachedTestSeedChatAdapter = new TestSeedChatChannelAdapter()\n return cachedTestSeedChatAdapter\n}\n\n/**\n * Register the test-seed adapter exactly once, but ONLY when the env flag is set.\n * Idempotent and safe to call from every container creation (`di.register`) \u2014 a\n * no-op when seeding is disabled or the adapter is already registered.\n */\nexport function ensureTestSeedAdapterRegistered(): void {\n if (!isTestChannelSeedingEnabled()) return\n registerSystemEmailProviderConfigResolver({\n providerKey: TEST_SEED_PROVIDER_KEY,\n isConfigured: isTestChannelSeedingEnabled,\n resolveCredentials: ({ fromAddress }) => ({ testSeed: true, fromAddress }),\n })\n if (!hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChannelAdapter())\n }\n if (!hasChannelAdapter(TEST_SEED_CHAT_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChatChannelAdapter())\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { appendFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { timingSafeEqual } from 'node:crypto'\nimport path from 'node:path'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type {\n ChannelAdapter,\n ChannelCapabilities,\n ChannelNativeContent,\n ConvertOutboundInput,\n GetMessageStatusInput,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n SendMessageInput,\n SendMessageResult,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from './adapter'\nimport { baseEmailCapabilities } from './email-capabilities'\nimport { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'\nimport { registerSystemEmailProviderConfigResolver } from './system-email-provider-config'\n\n/**\n * Test-only channel seeding support.\n *\n * The ephemeral integration harness cannot connect a REAL email channel:\n * - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server\n * (none exists in CI), so `POST /channels/connect/credentials` returns 422.\n * - Even with a connected channel, the outbound delivery worker calls the real\n * SMTP adapter, which fails with no server \u2014 so `communication_channels.message.sent`\n * never fires and the customers link subscriber never runs.\n *\n * To make the compose \u2192 deliver \u2192 `.sent` \u2192 CRM-link \u2192 cross-user-visibility chain\n * (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable\n * end-to-end against real Postgres, this module provides a network-free stub adapter\n * that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.\n *\n * Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};\n * when the env flag is unset (the production default) the adapter is never registered\n * and the `__test_seed__` provider key resolves to no adapter \u2014 so the connect route\n * returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated\n * test-seed API route enforces the same gate independently (fail-closed 404 in prod).\n */\n\n/** Provider key for the network-free test stub adapter. */\nexport const TEST_SEED_PROVIDER_KEY = '__test_seed__'\n\n/**\n * Provider key for the network-free stub adapter that stands in for a CHAT\n * provider \u2014 one whose senders are identified by an opaque handle and have no\n * email address at all (Discord, Slack, Telegram\u2026).\n *\n * It exists because the email-shaped stub above can only ever prove the hub\n * accepts email-shaped data. That is precisely how CI stayed green while every\n * real inbound Discord message was rejected (#4975): the fixture invented an\n * address the provider can never produce. Tests that need to prove the hub's\n * non-email identity contract MUST drive this provider instead.\n */\nexport const TEST_SEED_CHAT_PROVIDER_KEY = '__test_seed_chat__'\n\n/** Env flag that unlocks test-only channel seeding. Off in production. */\nexport const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'\nexport const TEST_EMAIL_CAPTURE_ACCESS_TOKEN_ENV = 'OM_TEST_EMAIL_CAPTURE_ACCESS_TOKEN'\nexport const TEST_EMAIL_CAPTURE_CORRELATION_TOKEN_ENV = 'OM_TEST_EMAIL_CAPTURE_CORRELATION_TOKEN'\n/**\n * Where the Hub's tenant-scoped capture is written.\n *\n * Deliberately not `OM_TEST_EMAIL_CAPTURE_PATH`: that one belongs to the unscoped capture in\n * `@open-mercato/shared/lib/email/send`, which writes a different record shape to a path the repo\n * also ships a committed fixture for. Pointing both mechanisms at one file made this module parse\n * foreign records and let `clear-capture` rewrite the other mechanism's fixtures. Two mechanisms,\n * two files.\n */\nexport const TEST_SYSTEM_EMAIL_CAPTURE_PATH_ENV = 'OM_TEST_SYSTEM_EMAIL_CAPTURE_PATH'\n\n/**\n * True only when the test-seeding env flag is explicitly enabled. Accepts the\n * usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a\n * plain `=true`. Any other value (including unset) is treated as disabled.\n */\nexport function isTestChannelSeedingEnabled(): boolean {\n const raw = process.env[TEST_CHANNEL_SEEDING_ENV]\n if (typeof raw !== 'string') return false\n return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())\n}\n\nexport type TestSeedCapturedMessage = {\n capturedAt: string\n externalMessageId: string\n conversationId?: string\n content: SendMessageInput['content']\n scope: SendMessageInput['scope']\n metadata?: SendMessageInput['metadata']\n captureCorrelationToken?: string\n}\n\ntype TestSeedCaptureScope = {\n tenantId: string\n organizationId: string | null\n}\n\nexport type TestSeedCaptureOptions = {\n systemRecipient?: string\n captureCorrelationToken?: string\n}\n\nexport async function createTestSeedPlatformMessage(\n em: EntityManager,\n input: {\n providerKey: string\n threadId?: string\n senderUserId: string\n subject?: string\n bodyText?: string\n channelId: string\n tenantId: string\n organizationId: string | null\n },\n): Promise<string | null> {\n const rows = (await em.getConnection().execute(\n `INSERT INTO messages\n (type, thread_id, sender_user_id, subject, body, body_format, priority, status,\n is_draft, sent_at, visibility, source_entity_type, source_entity_id,\n tenant_id, organization_id, created_at, updated_at)\n VALUES\n (?, ?, ?, ?, ?, 'text', 'normal', 'sent',\n false, now(), 'public', 'communication_channels.test_seed_inbound', ?,\n ?, ?, now(), now())\n RETURNING id`,\n [\n `channel.${input.providerKey}`,\n input.threadId ?? null,\n input.senderUserId,\n input.subject ?? '(no subject)',\n input.bodyText ?? '',\n input.channelId,\n input.tenantId,\n input.organizationId,\n ],\n )) as Array<{ id: string }>\n return rows[0]?.id ?? null\n}\n\n/**\n * Hard-delete a platform message created by the test-seed harness. Matches only\n * the direct seed stamp or a test-prefixed conversation already verified by the\n * test-seed route, so integration teardown cannot remove real traffic.\n */\nexport async function deleteTestSeedPlatformMessage(\n em: EntityManager,\n input: {\n messageId: string\n channelId: string\n conversationId: string\n tenantId: string\n organizationId: string | null\n },\n): Promise<void> {\n await em.getConnection().execute(\n `DELETE FROM messages\n WHERE id = ?\n AND (\n (source_entity_type = 'communication_channels.test_seed_inbound' AND source_entity_id = ?)\n OR\n (source_entity_type = 'communication_channels.external_conversation' AND source_entity_id = ?)\n )\n AND tenant_id = ?\n AND organization_id IS NOT DISTINCT FROM ?`,\n [\n input.messageId,\n input.channelId,\n input.conversationId,\n input.tenantId,\n input.organizationId,\n ],\n )\n}\n\nfunction normalizeToken(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const normalized = value.trim()\n return normalized.length >= 32 ? normalized : null\n}\n\nexport function isTestEmailCaptureAccessAuthorized(providedToken: string | null): boolean {\n const expectedToken = normalizeToken(process.env[TEST_EMAIL_CAPTURE_ACCESS_TOKEN_ENV])\n const normalizedProvidedToken = normalizeToken(providedToken)\n if (!expectedToken || !normalizedProvidedToken) return false\n const expected = Buffer.from(expectedToken)\n const provided = Buffer.from(normalizedProvidedToken)\n return expected.length === provided.length && timingSafeEqual(expected, provided)\n}\n\nexport function resolveTestEmailCaptureCorrelationToken(): string | null {\n return normalizeToken(process.env[TEST_EMAIL_CAPTURE_CORRELATION_TOKEN_ENV])\n}\n\nfunction resolveCapturePath(): string {\n const explicit = process.env[TEST_SYSTEM_EMAIL_CAPTURE_PATH_ENV]?.trim()\n if (explicit) return path.resolve(explicit)\n const queueBaseDir = process.env.QUEUE_BASE_DIR?.trim()\n if (queueBaseDir) return path.resolve(queueBaseDir, '..', 'test-email-capture.jsonl')\n return path.resolve(process.cwd(), '.mercato', 'test-email-capture.jsonl')\n}\n\nfunction matchesCaptureScope(\n message: TestSeedCapturedMessage,\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): boolean {\n if (message.scope.tenantId === scope.tenantId) {\n const messageOrganizationId = message.scope.organizationId ?? null\n return messageOrganizationId === scope.organizationId || messageOrganizationId === scope.tenantId\n }\n\n if (\n message.scope.tenantId !== 'system' ||\n message.scope.organizationId !== 'system' ||\n !options.systemRecipient ||\n !options.captureCorrelationToken ||\n message.captureCorrelationToken !== options.captureCorrelationToken\n ) {\n return false\n }\n\n const expectedRecipient = options.systemRecipient.trim().toLowerCase()\n const matchesRecipient = (value: unknown): boolean => {\n if (typeof value === 'string') return value.trim().toLowerCase() === expectedRecipient\n if (Array.isArray(value)) return value.some(matchesRecipient)\n if (!value || typeof value !== 'object') return false\n return matchesRecipient((value as Record<string, unknown>).address)\n }\n\n return matchesRecipient(message.metadata?.to)\n}\n\nasync function readTestSeedCapturedMessages(): Promise<TestSeedCapturedMessage[]> {\n const capturePath = resolveCapturePath()\n const text = await readFile(capturePath, 'utf8').catch(() => '')\n return text\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line) => {\n try {\n return JSON.parse(line) as TestSeedCapturedMessage\n } catch {\n return null\n }\n })\n // A record without a scope is not ours. Skipping instead of throwing keeps a stray or\n // foreign-shaped line from turning every capture read into a 500.\n .filter((message): message is TestSeedCapturedMessage => Boolean(message?.scope))\n}\n\nexport async function clearTestSeedCapturedMessages(\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): Promise<void> {\n const capturePath = resolveCapturePath()\n const retained = (await readTestSeedCapturedMessages())\n .filter((message) => !matchesCaptureScope(message, scope, options))\n if (retained.length === 0) {\n await rm(capturePath, { force: true })\n return\n }\n await writeFile(\n capturePath,\n `${retained.map((message) => JSON.stringify(message)).join('\\n')}\\n`,\n 'utf8',\n )\n}\n\nexport async function listTestSeedCapturedMessages(\n scope: TestSeedCaptureScope,\n options: TestSeedCaptureOptions = {},\n): Promise<TestSeedCapturedMessage[]> {\n return (await readTestSeedCapturedMessages()).filter((message) =>\n matchesCaptureScope(message, scope, options),\n )\n}\n\nasync function captureTestSeedMessage(record: TestSeedCapturedMessage): Promise<void> {\n const capturePath = resolveCapturePath()\n await mkdir(path.dirname(capturePath), { recursive: true })\n await appendFile(capturePath, `${JSON.stringify(record)}\\n`, 'utf8')\n}\n\n/**\n * Capabilities for the stub: an email channel that supports neither reactions,\n * edit/delete, nor conversation history \u2014 so the strict registry validator\n * (`validateAdapterCapabilities`) requires only the core method surface.\n */\nconst testSeedCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n conversationHistory: false,\n realtimePush: false,\n}\n\n/**\n * A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any\n * network I/O. Used exclusively by the integration harness to let the outbound\n * delivery worker reach its success path and emit `communication_channels.message.sent`.\n */\nclass TestSeedChannelAdapter implements ChannelAdapter {\n // Widened to `string` rather than inferred as a literal so the chat-flavoured\n // subclass below can override both with its own provider key / channel type.\n readonly providerKey: string = TEST_SEED_PROVIDER_KEY\n readonly channelType: string = 'email'\n readonly capabilities = testSeedCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n // Synthesize a deterministic-looking RFC2822-style message id; never touches\n // the network. The delivery worker persists this as the external message id.\n const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`\n await captureTestSeedMessage({\n capturedAt: new Date().toISOString(),\n externalMessageId,\n conversationId: input.conversationId,\n content: input.content,\n scope: input.scope,\n metadata: input.metadata,\n captureCorrelationToken: resolveTestEmailCaptureCorrelationToken() ?? undefined,\n })\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'sent',\n metadata: { testSeed: true },\n }\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n // No real webhook \u2014 return the inert event so the generic webhook route 202s\n // without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).\n return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return {\n content: {\n text: input.body,\n bodyFormat: input.bodyFormat,\n },\n metadata: input.channelMetadata ?? {},\n }\n }\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // Email-shaped frame for the test-seed `ingest-inbound` action: the sender is\n // an address, so the message travels the REAL ingest path for an email\n // channel \u2014 `ingest_inbound_message` derives `externalEmail` from it and\n // composes through `messages.messages.compose` with `sendViaEmail: false`.\n // That is the only way a test can prove the hub does not echo inbound mail\n // back to its sender (#6089); `emit-inbound` bypasses compose entirely.\n const frame = (raw.raw ?? {}) as Record<string, unknown>\n const senderIdentifier = String(frame.senderIdentifier ?? '').trim()\n if (!senderIdentifier.includes('@')) {\n throw new Error('[internal] TestSeedChannelAdapter requires an email-shaped senderIdentifier')\n }\n const subject = typeof frame.subject === 'string' ? frame.subject : undefined\n const body = typeof frame.body === 'string' ? frame.body : ''\n const senderDisplayName =\n typeof frame.senderDisplayName === 'string' ? frame.senderDisplayName : undefined\n return {\n externalMessageId: String(frame.externalMessageId ?? ''),\n externalConversationId: String(frame.externalConversationId ?? ''),\n senderIdentifier,\n senderDisplayName,\n subject,\n body,\n bodyFormat: 'text',\n timestamp: new Date(),\n channelPayload: {\n from: { address: senderIdentifier, ...(senderDisplayName ? { name: senderDisplayName } : {}) },\n ...(subject !== undefined ? { subject } : {}),\n text: body,\n },\n channelContentType: 'text/plain',\n channelMetadata: {},\n }\n }\n\n async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n // No real server to authenticate against \u2014 accept any credentials so the\n // connect command persists a connected channel.\n return { ok: true }\n }\n}\n\n/**\n * Capabilities for the chat stub. Identical to the email stub's except for the\n * recipient shape: a chat channel is addressed by a provider-issued identifier\n * (a Discord snowflake), not an email address.\n *\n * Without this override the chat stub inherited `baseEmailCapabilities`, so it\n * claimed `channelType: 'discord'` while validating recipients as email\n * addresses \u2014 a channel that could not be addressed the way the provider it\n * imitates actually is. That left the hub's `'provider-native'` branch\n * (#4976: per-provider recipient validation, and the omitted recipient that\n * falls back to the adapter's own target) reachable only with a live Discord\n * bot, so no integration test could cover it and CI never exercised it.\n */\nconst testSeedChatCapabilities: ChannelCapabilities = {\n ...testSeedCapabilities,\n recipientFormat: 'provider-native',\n}\n\n/**\n * Chat-flavoured twin of {@link TestSeedChannelAdapter}: same network-free\n * behaviour, but it declares a non-email `channelType`, so a channel connected\n * through it is shaped like a real chat channel \u2014 including an\n * `externalIdentifier` of NULL when no email-ish credential key is supplied.\n */\nclass TestSeedChatChannelAdapter extends TestSeedChannelAdapter {\n readonly providerKey: string = TEST_SEED_CHAT_PROVIDER_KEY\n readonly channelType: string = 'discord'\n readonly capabilities = testSeedChatCapabilities\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // Unlike the email stub, this one is reachable: the test-seed ingest action\n // feeds it a chat-shaped frame so the message travels the real ingest path\n // (and therefore the real compose validation) rather than a SQL shortcut.\n const frame = (raw.raw ?? {}) as Record<string, unknown>\n const senderIdentifier = String(frame.senderIdentifier ?? '')\n if (!senderIdentifier) {\n throw new Error('[internal] TestSeedChatChannelAdapter requires a senderIdentifier')\n }\n return {\n externalMessageId: String(frame.externalMessageId ?? ''),\n externalConversationId: String(frame.externalConversationId ?? ''),\n senderIdentifier,\n senderDisplayName:\n typeof frame.senderDisplayName === 'string' ? frame.senderDisplayName : undefined,\n body: typeof frame.body === 'string' ? frame.body : '',\n bodyFormat: 'text',\n timestamp: new Date(),\n channelPayload: {},\n channelContentType: 'text/plain',\n channelMetadata: {},\n }\n }\n}\n\nlet cachedTestSeedAdapter: TestSeedChannelAdapter | null = null\nlet cachedTestSeedChatAdapter: TestSeedChatChannelAdapter | null = null\n\nfunction getTestSeedChannelAdapter(): TestSeedChannelAdapter {\n if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()\n return cachedTestSeedAdapter\n}\n\nfunction getTestSeedChatChannelAdapter(): TestSeedChatChannelAdapter {\n if (!cachedTestSeedChatAdapter) cachedTestSeedChatAdapter = new TestSeedChatChannelAdapter()\n return cachedTestSeedChatAdapter\n}\n\n/**\n * Register the test-seed adapter exactly once, but ONLY when the env flag is set.\n * Idempotent and safe to call from every container creation (`di.register`) \u2014 a\n * no-op when seeding is disabled or the adapter is already registered.\n */\nexport function ensureTestSeedAdapterRegistered(): void {\n if (!isTestChannelSeedingEnabled()) return\n registerSystemEmailProviderConfigResolver({\n providerKey: TEST_SEED_PROVIDER_KEY,\n isConfigured: isTestChannelSeedingEnabled,\n resolveCredentials: ({ fromAddress }) => ({ testSeed: true, fromAddress }),\n })\n if (!hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChannelAdapter())\n }\n if (!hasChannelAdapter(TEST_SEED_CHAT_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChatChannelAdapter())\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,OAAO,UAAU,IAAI,iBAAiB;AAC3D,SAAS,uBAAuB;AAChC,OAAO,UAAU;AAiBjB,SAAS,6BAA6B;AACtC,SAAS,mBAAmB,8BAA8B;AAC1D,SAAS,iDAAiD;AAyBnD,MAAM,yBAAyB;AAa/B,MAAM,8BAA8B;AAGpC,MAAM,2BAA2B;AACjC,MAAM,sCAAsC;AAC5C,MAAM,2CAA2C;AAUjD,MAAM,qCAAqC;AAO3C,SAAS,8BAAuC;AACrD,QAAM,MAAM,QAAQ,IAAI,wBAAwB;AAChD,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,SAAO,CAAC,KAAK,QAAQ,OAAO,IAAI,EAAE,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AACrE;AAsBA,eAAsB,8BACpB,IACA,OAUwB;AACxB,QAAM,OAAQ,MAAM,GAAG,cAAc,EAAE;AAAA,IACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA;AAAA,MACE,WAAW,MAAM,WAAW;AAAA,MAC5B,MAAM,YAAY;AAAA,MAClB,MAAM;AAAA,MACN,MAAM,WAAW;AAAA,MACjB,MAAM,YAAY;AAAA,MAClB,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,EACF;AACA,SAAO,KAAK,CAAC,GAAG,MAAM;AACxB;AAOA,eAAsB,8BACpB,IACA,OAOe;AACf,QAAM,GAAG,cAAc,EAAE;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA;AAAA,MACE,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,SAAS,eAAe,OAAiD;AACvE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,aAAa,MAAM,KAAK;AAC9B,SAAO,WAAW,UAAU,KAAK,aAAa;AAChD;AAEO,SAAS,mCAAmC,eAAuC;AACxF,QAAM,gBAAgB,eAAe,QAAQ,IAAI,mCAAmC,CAAC;AACrF,QAAM,0BAA0B,eAAe,aAAa;AAC5D,MAAI,CAAC,iBAAiB,CAAC,wBAAyB,QAAO;AACvD,QAAM,WAAW,OAAO,KAAK,aAAa;AAC1C,QAAM,WAAW,OAAO,KAAK,uBAAuB;AACpD,SAAO,SAAS,WAAW,SAAS,UAAU,gBAAgB,UAAU,QAAQ;AAClF;AAEO,SAAS,0CAAyD;AACvE,SAAO,eAAe,QAAQ,IAAI,wCAAwC,CAAC;AAC7E;AAEA,SAAS,qBAA6B;AACpC,QAAM,WAAW,QAAQ,IAAI,kCAAkC,GAAG,KAAK;AACvE,MAAI,SAAU,QAAO,KAAK,QAAQ,QAAQ;AAC1C,QAAM,eAAe,QAAQ,IAAI,gBAAgB,KAAK;AACtD,MAAI,aAAc,QAAO,KAAK,QAAQ,cAAc,MAAM,0BAA0B;AACpF,SAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,0BAA0B;AAC3E;AAEA,SAAS,oBACP,SACA,OACA,UAAkC,CAAC,GAC1B;AACT,MAAI,QAAQ,MAAM,aAAa,MAAM,UAAU;AAC7C,UAAM,wBAAwB,QAAQ,MAAM,kBAAkB;AAC9D,WAAO,0BAA0B,MAAM,kBAAkB,0BAA0B,MAAM;AAAA,EAC3F;AAEA,MACE,QAAQ,MAAM,aAAa,YAC3B,QAAQ,MAAM,mBAAmB,YACjC,CAAC,QAAQ,mBACT,CAAC,QAAQ,2BACT,QAAQ,4BAA4B,QAAQ,yBAC5C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,oBAAoB,QAAQ,gBAAgB,KAAK,EAAE,YAAY;AACrE,QAAM,mBAAmB,CAAC,UAA4B;AACpD,QAAI,OAAO,UAAU,SAAU,QAAO,MAAM,KAAK,EAAE,YAAY,MAAM;AACrE,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,KAAK,gBAAgB;AAC5D,QAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,WAAO,iBAAkB,MAAkC,OAAO;AAAA,EACpE;AAEA,SAAO,iBAAiB,QAAQ,UAAU,EAAE;AAC9C;AAEA,eAAe,+BAAmE;AAChF,QAAM,cAAc,mBAAmB;AACvC,QAAM,OAAO,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,MAAM,EAAE;AAC/D,SAAO,KACJ,MAAM,OAAO,EACb,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAChC,IAAI,CAAC,SAAS;AACb,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC,EAGA,OAAO,CAAC,YAAgD,QAAQ,SAAS,KAAK,CAAC;AACpF;AAEA,eAAsB,8BACpB,OACA,UAAkC,CAAC,GACpB;AACf,QAAM,cAAc,mBAAmB;AACvC,QAAM,YAAY,MAAM,6BAA6B,GAClD,OAAO,CAAC,YAAY,CAAC,oBAAoB,SAAS,OAAO,OAAO,CAAC;AACpE,MAAI,SAAS,WAAW,GAAG;AACzB,UAAM,GAAG,aAAa,EAAE,OAAO,KAAK,CAAC;AACrC;AAAA,EACF;AACA,QAAM;AAAA,IACJ;AAAA,IACA,GAAG,SAAS,IAAI,CAAC,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA;AAAA,IAChE;AAAA,EACF;AACF;AAEA,eAAsB,6BACpB,OACA,UAAkC,CAAC,GACC;AACpC,UAAQ,MAAM,6BAA6B,GAAG;AAAA,IAAO,CAAC,YACpD,oBAAoB,SAAS,OAAO,OAAO;AAAA,EAC7C;AACF;AAEA,eAAe,uBAAuB,QAAgD;AACpF,QAAM,cAAc,mBAAmB;AACvC,QAAM,MAAM,KAAK,QAAQ,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,QAAM,WAAW,aAAa,GAAG,KAAK,UAAU,MAAM,CAAC;AAAA,GAAM,MAAM;AACrE;AAOA,MAAM,uBAA4C;AAAA,EAChD,GAAG;AAAA,EACH,qBAAqB;AAAA,EACrB,cAAc;AAChB;AAOA,MAAM,uBAAiD;AAAA,EAAvD;AAGE;AAAA;AAAA,SAAS,cAAsB;AAC/B,SAAS,cAAsB;AAC/B,SAAS,eAAe;AAAA;AAAA,EAExB,MAAM,YAAY,OAAqD;AAGrE,UAAM,oBAAoB,aAAa,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC5F,UAAM,uBAAuB;AAAA,MAC3B,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACnC;AAAA,MACA,gBAAgB,MAAM;AAAA,MACtB,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,yBAAyB,wCAAwC,KAAK;AAAA,IACxE,CAAC;AACD,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB,MAAM;AAAA,MACtB,QAAQ;AAAA,MACR,UAAU,EAAE,UAAU,KAAK;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,QAAqD;AAGvE,WAAO,EAAE,KAAK,CAAC,GAAG,WAAW,SAAS,UAAU,EAAE,QAAQ,uBAAuB,EAAE;AAAA,EACrF;AAAA,EAEA,MAAM,UAAU,QAAuD;AACrE,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAAA,EAEA,MAAM,gBAAgB,OAA4D;AAChF,WAAO;AAAA,MACL,SAAS;AAAA,QACP,MAAM,MAAM;AAAA,QACZ,YAAY,MAAM;AAAA,MACpB;AAAA,MACA,UAAU,MAAM,mBAAmB,CAAC;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,KAAwD;AAO7E,UAAM,QAAS,IAAI,OAAO,CAAC;AAC3B,UAAM,mBAAmB,OAAO,MAAM,oBAAoB,EAAE,EAAE,KAAK;AACnE,QAAI,CAAC,iBAAiB,SAAS,GAAG,GAAG;AACnC,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AACA,UAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;AACpE,UAAM,OAAO,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAC3D,UAAM,oBACJ,OAAO,MAAM,sBAAsB,WAAW,MAAM,oBAAoB;AAC1E,WAAO;AAAA,MACL,mBAAmB,OAAO,MAAM,qBAAqB,EAAE;AAAA,MACvD,wBAAwB,OAAO,MAAM,0BAA0B,EAAE;AAAA,MACjE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY;AAAA,MACZ,WAAW,oBAAI,KAAK;AAAA,MACpB,gBAAgB;AAAA,QACd,MAAM,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE,MAAM,kBAAkB,IAAI,CAAC,EAAG;AAAA,QAC7F,GAAI,YAAY,SAAY,EAAE,QAAQ,IAAI,CAAC;AAAA,QAC3C,MAAM;AAAA,MACR;AAAA,MACA,oBAAoB;AAAA,MACpB,iBAAiB,CAAC;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAM,oBAAoB,QAAsE;AAG9F,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AACF;AAeA,MAAM,2BAAgD;AAAA,EACpD,GAAG;AAAA,EACH,iBAAiB;AACnB;AAQA,MAAM,mCAAmC,uBAAuB;AAAA,EAAhE;AAAA;AACE,SAAS,cAAsB;AAC/B,SAAS,cAAsB;AAC/B,SAAS,eAAe;AAAA;AAAA,EAExB,MAAM,iBAAiB,KAAwD;AAI7E,UAAM,QAAS,IAAI,OAAO,CAAC;AAC3B,UAAM,mBAAmB,OAAO,MAAM,oBAAoB,EAAE;AAC5D,QAAI,CAAC,kBAAkB;AACrB,YAAM,IAAI,MAAM,mEAAmE;AAAA,IACrF;AACA,WAAO;AAAA,MACL,mBAAmB,OAAO,MAAM,qBAAqB,EAAE;AAAA,MACvD,wBAAwB,OAAO,MAAM,0BAA0B,EAAE;AAAA,MACjE;AAAA,MACA,mBACE,OAAO,MAAM,sBAAsB,WAAW,MAAM,oBAAoB;AAAA,MAC1E,MAAM,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAAA,MACpD,YAAY;AAAA,MACZ,WAAW,oBAAI,KAAK;AAAA,MACpB,gBAAgB,CAAC;AAAA,MACjB,oBAAoB;AAAA,MACpB,iBAAiB,CAAC;AAAA,IACpB;AAAA,EACF;AACF;AAEA,IAAI,wBAAuD;AAC3D,IAAI,4BAA+D;AAEnE,SAAS,4BAAoD;AAC3D,MAAI,CAAC,sBAAuB,yBAAwB,IAAI,uBAAuB;AAC/E,SAAO;AACT;AAEA,SAAS,gCAA4D;AACnE,MAAI,CAAC,0BAA2B,6BAA4B,IAAI,2BAA2B;AAC3F,SAAO;AACT;AAOO,SAAS,kCAAwC;AACtD,MAAI,CAAC,4BAA4B,EAAG;AACpC,4CAA0C;AAAA,IACxC,aAAa;AAAA,IACb,cAAc;AAAA,IACd,oBAAoB,CAAC,EAAE,YAAY,OAAO,EAAE,UAAU,MAAM,YAAY;AAAA,EAC1E,CAAC;AACD,MAAI,CAAC,kBAAkB,sBAAsB,GAAG;AAC9C,2BAAuB,0BAA0B,CAAC;AAAA,EACpD;AACA,MAAI,CAAC,kBAAkB,2BAA2B,GAAG;AACnD,2BAAuB,8BAA8B,CAAC;AAAA,EACxD;AACF;",
6
6
  "names": []
7
7
  }
@@ -204,8 +204,7 @@ const composeMessageCommand = {
204
204
  void 0,
205
205
  scope
206
206
  ))?.threadId ?? input.parentMessageId : void 0;
207
- const isPublicVisibility = input.visibility === "public";
208
- const sendViaEmail = isPublicVisibility && !input.inboundFromChannel ? true : input.sendViaEmail;
207
+ const sendViaEmail = input.sendViaEmail;
209
208
  const message = trx.create(Message, {
210
209
  type: input.type,
211
210
  visibility: input.visibility ?? null,
@@ -300,7 +299,7 @@ const composeMessageCommand = {
300
299
  messageId,
301
300
  senderUserId: input.userId,
302
301
  recipientUserIds: input.recipients.map((recipient) => recipient.userId),
303
- sendViaEmail: input.visibility === "public" && !input.inboundFromChannel ? true : input.sendViaEmail,
302
+ sendViaEmail: input.sendViaEmail,
304
303
  inboundFromChannel: input.inboundFromChannel === true,
305
304
  externalEmail: responseExternalEmail,
306
305
  tenantId: input.tenantId,