@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4393.1.de282b5dfd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
- package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
- package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_token/index.js +17 -0
- package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
- package/dist/generated/entities/communication_channel/index.js +43 -0
- package/dist/generated/entities/communication_channel/index.js.map +7 -0
- package/dist/generated/entities/customer_interaction/index.js +4 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entities/external_conversation/index.js +25 -0
- package/dist/generated/entities/external_conversation/index.js.map +7 -0
- package/dist/generated/entities/external_message/index.js +25 -0
- package/dist/generated/entities/external_message/index.js.map +7 -0
- package/dist/generated/entities/integration_credentials/index.js +3 -1
- package/dist/generated/entities/integration_credentials/index.js.map +2 -2
- package/dist/generated/entities/message/index.js +2 -0
- package/dist/generated/entities/message/index.js.map +2 -2
- package/dist/generated/entities/message_channel_link/index.js +33 -0
- package/dist/generated/entities/message_channel_link/index.js.map +7 -0
- package/dist/generated/entities/message_reaction/index.js +25 -0
- package/dist/generated/entities/message_reaction/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +11 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +117 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authFixtures.js +2 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
- package/dist/modules/communication_channels/acl.js +47 -0
- package/dist/modules/communication_channels/acl.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
- package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
- package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/interceptors.js +68 -0
- package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-register.js +146 -0
- package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-renew.js +23 -0
- package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
- package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/data/enrichers.js +286 -0
- package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
- package/dist/modules/communication_channels/data/entities.js +447 -0
- package/dist/modules/communication_channels/data/entities.js.map +7 -0
- package/dist/modules/communication_channels/data/extensions.js +67 -0
- package/dist/modules/communication_channels/data/extensions.js.map +7 -0
- package/dist/modules/communication_channels/data/validators.js +123 -0
- package/dist/modules/communication_channels/data/validators.js.map +7 -0
- package/dist/modules/communication_channels/di.js +35 -0
- package/dist/modules/communication_channels/di.js.map +7 -0
- package/dist/modules/communication_channels/encryption.js +12 -0
- package/dist/modules/communication_channels/encryption.js.map +7 -0
- package/dist/modules/communication_channels/events.js +124 -0
- package/dist/modules/communication_channels/events.js.map +7 -0
- package/dist/modules/communication_channels/index.js +20 -0
- package/dist/modules/communication_channels/index.js.map +7 -0
- package/dist/modules/communication_channels/lib/access-control.js +43 -0
- package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter.js +1 -0
- package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
- package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
- package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
- package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-contact.js +14 -0
- package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-mime.js +259 -0
- package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
- package/dist/modules/communication_channels/lib/error-classification.js +101 -0
- package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
- package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
- package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
- package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/provider-health.js +24 -0
- package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-state.js +19 -0
- package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/queue.js +54 -0
- package/dist/modules/communication_channels/lib/queue.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
- package/dist/modules/communication_channels/lib/registry.js +67 -0
- package/dist/modules/communication_channels/lib/registry.js.map +7 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
- package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
- package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-user.js +22 -0
- package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +68 -0
- package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-token.js +219 -0
- package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.client.js +51 -0
- package/dist/modules/communication_channels/notifications.client.js.map +7 -0
- package/dist/modules/communication_channels/notifications.handlers.js +53 -0
- package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +56 -0
- package/dist/modules/communication_channels/notifications.js.map +7 -0
- package/dist/modules/communication_channels/setup.js +105 -0
- package/dist/modules/communication_channels/setup.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
- package/dist/modules/communication_channels/widgets/components.js +7 -0
- package/dist/modules/communication_channels/widgets/components.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
- package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
- package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
- package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
- package/dist/modules/customers/acl.js +18 -0
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +9 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +18 -7
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
- package/dist/modules/customers/api/interactions/counts/route.js +6 -0
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +26 -7
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
- package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/route.js +12 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +46 -5
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +16 -0
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +10 -0
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
- package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
- package/dist/modules/customers/data/enrichers.js +133 -2
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +18 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/extensions.js +16 -0
- package/dist/modules/customers/data/extensions.js.map +7 -0
- package/dist/modules/customers/encryption.js +11 -0
- package/dist/modules/customers/encryption.js.map +2 -2
- package/dist/modules/customers/events.js +4 -1
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js.map +2 -2
- package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
- package/dist/modules/customers/lib/personEmailThreads.js +205 -0
- package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
- package/dist/modules/customers/lib/visibilityFilter.js +51 -0
- package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
- package/dist/modules/customers/setup.js +2 -1
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
- package/dist/modules/integrations/data/entities.js +8 -1
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +29 -14
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +70 -8
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
- package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/data/entities.js +8 -1
- package/dist/modules/messages/data/entities.js.map +2 -2
- package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
- package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
- package/dist/modules/messages/widgets/injection-table.js +7 -0
- package/dist/modules/messages/widgets/injection-table.js.map +7 -0
- package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
- package/generated/entities/channel_thread_mapping/index.ts +11 -0
- package/generated/entities/channel_thread_token/index.ts +7 -0
- package/generated/entities/communication_channel/index.ts +20 -0
- package/generated/entities/customer_interaction/index.ts +2 -0
- package/generated/entities/external_conversation/index.ts +11 -0
- package/generated/entities/external_message/index.ts +11 -0
- package/generated/entities/integration_credentials/index.ts +1 -0
- package/generated/entities/message/index.ts +1 -0
- package/generated/entities/message_channel_link/index.ts +15 -0
- package/generated/entities/message_reaction/index.ts +11 -0
- package/generated/entities.ids.generated.ts +11 -0
- package/generated/entity-fields-registry.ts +117 -0
- package/package.json +9 -7
- package/src/helpers/integration/authFixtures.ts +4 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
- package/src/modules/communication_channels/acl.ts +43 -0
- package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
- package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
- package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
- package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
- package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
- package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
- package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
- package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
- package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
- package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
- package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
- package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
- package/src/modules/communication_channels/commands/interceptors.ts +104 -0
- package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
- package/src/modules/communication_channels/commands/push-register.ts +203 -0
- package/src/modules/communication_channels/commands/push-renew.ts +49 -0
- package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
- package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
- package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
- package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
- package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
- package/src/modules/communication_channels/data/enrichers.ts +413 -0
- package/src/modules/communication_channels/data/entities.ts +546 -0
- package/src/modules/communication_channels/data/extensions.ts +76 -0
- package/src/modules/communication_channels/data/validators.ts +138 -0
- package/src/modules/communication_channels/di.ts +40 -0
- package/src/modules/communication_channels/encryption.ts +44 -0
- package/src/modules/communication_channels/events.ts +122 -0
- package/src/modules/communication_channels/i18n/de.json +138 -0
- package/src/modules/communication_channels/i18n/en.json +138 -0
- package/src/modules/communication_channels/i18n/es.json +138 -0
- package/src/modules/communication_channels/i18n/pl.json +138 -0
- package/src/modules/communication_channels/index.ts +19 -0
- package/src/modules/communication_channels/lib/access-control.ts +110 -0
- package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
- package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
- package/src/modules/communication_channels/lib/adapter.ts +605 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
- package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
- package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
- package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
- package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
- package/src/modules/communication_channels/lib/email-contact.ts +17 -0
- package/src/modules/communication_channels/lib/email-mime.ts +425 -0
- package/src/modules/communication_channels/lib/error-classification.ts +144 -0
- package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
- package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
- package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
- package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
- package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
- package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
- package/src/modules/communication_channels/lib/provider-health.ts +47 -0
- package/src/modules/communication_channels/lib/push-state.ts +38 -0
- package/src/modules/communication_channels/lib/queue.ts +66 -0
- package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
- package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
- package/src/modules/communication_channels/lib/registry.ts +99 -0
- package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
- package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
- package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
- package/src/modules/communication_channels/lib/system-user.ts +74 -0
- package/src/modules/communication_channels/lib/test-seed.ts +140 -0
- package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
- package/src/modules/communication_channels/lib/thread-token.ts +355 -0
- package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
- package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
- package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
- package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
- package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
- package/src/modules/communication_channels/notifications.client.ts +50 -0
- package/src/modules/communication_channels/notifications.handlers.ts +86 -0
- package/src/modules/communication_channels/notifications.ts +52 -0
- package/src/modules/communication_channels/setup.ts +158 -0
- package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
- package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
- package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
- package/src/modules/communication_channels/widgets/components.ts +36 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
- package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
- package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
- package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
- package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
- package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
- package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
- package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
- package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
- package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
- package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
- package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
- package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
- package/src/modules/customers/acl.ts +18 -0
- package/src/modules/customers/api/activities/route.ts +13 -0
- package/src/modules/customers/api/companies/[id]/route.ts +21 -1
- package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
- package/src/modules/customers/api/interactions/counts/route.ts +10 -0
- package/src/modules/customers/api/interactions/route.ts +51 -5
- package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
- package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
- package/src/modules/customers/api/people/[id]/route.ts +17 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
- package/src/modules/customers/commands/deals.ts +65 -6
- package/src/modules/customers/commands/interactions.ts +30 -0
- package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
- package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
- package/src/modules/customers/components/detail/DealForm.tsx +2 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
- package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
- package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
- package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
- package/src/modules/customers/data/enrichers.ts +252 -1
- package/src/modules/customers/data/entities.ts +46 -1
- package/src/modules/customers/data/extensions.ts +26 -0
- package/src/modules/customers/encryption.ts +11 -0
- package/src/modules/customers/events.ts +4 -0
- package/src/modules/customers/i18n/de.json +41 -0
- package/src/modules/customers/i18n/en.json +41 -0
- package/src/modules/customers/i18n/es.json +41 -0
- package/src/modules/customers/i18n/pl.json +41 -0
- package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
- package/src/modules/customers/lib/kysely.ts +16 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
- package/src/modules/customers/lib/personEmailThreads.ts +325 -0
- package/src/modules/customers/lib/visibilityFilter.ts +152 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
- package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
- package/src/modules/customers/setup.ts +1 -0
- package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
- package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
- package/src/modules/integrations/AGENTS.md +9 -0
- package/src/modules/integrations/data/entities.ts +21 -1
- package/src/modules/integrations/lib/credentials-service.ts +49 -13
- package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
- package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
- package/src/modules/messages/commands/messages.ts +101 -8
- package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
- package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
- package/src/modules/messages/data/entities.ts +11 -0
- package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
- package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
- package/src/modules/messages/widgets/injection-table.ts +29 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/commands/disconnect-channel.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload as extractSharedUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { pushUnregister } from './push-unregister'\n\nconst disconnectChannelSchema = z.object({\n channelId: z.string().uuid(),\n userId: z.string().uuid(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type DisconnectChannelInput = z.infer<typeof disconnectChannelSchema>\n\nexport type DisconnectChannelResult =\n | {\n status: 'disconnected'\n channelId: string\n undo: DisconnectChannelUndoSnapshot\n }\n | { status: 'noop'; reason: string }\n | { status: 'not_owner'; reason: string }\n\nexport interface DisconnectChannelUndoSnapshot {\n channelId: string\n // Optional for backward compatibility with log entries written before tenant\n // scoping was added to the undo lookup; new snapshots always set it.\n tenantId?: string\n previousStatus: string\n previousIsActive: boolean\n previousIsPrimary: boolean\n previousCredentialsRef: string | null\n previousLastError: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID =\n 'communication_channels.channel.disconnect'\n\n/**\n * Disconnect a per-user channel.\n *\n * Setting `status='disconnected'` halts the polling worker (slice 3b filters by\n * status) and clears the credentials reference so the adapter can't pick the\n * channel up by accident. `isPrimary` is cleared too \u2014 keeping it set would\n * leave the user without a working primary and any future `send-as-user` call\n * would 404 the lookup. The `credentials_ref` row in `integration_credentials`\n * is left orphaned; the integrations module's retention policy sweeps it.\n *\n * The command is undoable: the `before` snapshot captures the four-tuple\n * (status, is_active, is_primary, credentials_ref) so undo can restore the\n * channel atomically. Undo is gated by the `beforeUndo` interceptor in\n * `commands/interceptors.ts` \u2014 if another channel became primary while this\n * one was disconnected, undo is blocked to avoid violating the partial-unique\n * \"one primary per user\" constraint.\n */\nconst disconnectChannelCommand: CommandHandler<\n DisconnectChannelInput,\n DisconnectChannelResult\n> = {\n id: COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,\n // Explicitly undoable (the bus also infers this from `undo` below, but\n // declaring it keeps undoability from silently dropping under a refactor).\n isUndoable: true,\n async execute(rawInput, ctx) {\n const input = disconnectChannelSchema.parse(rawInput) as DisconnectChannelInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n return { status: 'noop', reason: 'channel not found' }\n }\n if (channel.userId !== input.userId) {\n return { status: 'not_owner', reason: 'channel is not owned by the current user' }\n }\n if (channel.status === 'disconnected' && !channel.isActive) {\n return { status: 'noop', reason: 'channel is already disconnected' }\n }\n\n const undo: DisconnectChannelUndoSnapshot = {\n channelId: channel.id,\n tenantId: channel.tenantId,\n previousStatus: channel.status,\n previousIsActive: channel.isActive,\n previousIsPrimary: channel.isPrimary,\n previousCredentialsRef: channel.credentialsRef ?? null,\n previousLastError: channel.lastError ?? null,\n }\n\n // Spec C \u00A7 Phase C5 \u2014 tear down provider-side push delivery BEFORE we\n // clear `credentialsRef`. Best-effort: any failure (404, expired token,\n // adapter error) is logged inside `pushUnregister` and never re-raised.\n // The teardown needs valid credentials, which we still have at this\n // point \u2014 clearing them below would make it impossible.\n if (input.scope.organizationId) {\n try {\n await pushUnregister({\n container: ctx.container,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: input.userId,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n console.warn(\n `[disconnect-channel] push unregister failed for ${channel.id}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n }\n\n channel.status = 'disconnected'\n channel.isActive = false\n channel.isPrimary = false\n channel.credentialsRef = null\n channel.lastError = 'user-disconnected'\n channel.lastPolledAt = new Date()\n await em.flush()\n\n // Emit AFTER flush so subscribers observe a committed state. Persistent\n // delivery so workflows/audit/UI refresh can retry on failure.\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.disconnected',\n {\n channelId: channel.id,\n userId: input.userId,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return { status: 'disconnected', channelId: channel.id, undo }\n },\n // Persist the undo snapshot into the action log. Without this, the command bus\n // mints an undo token (so the UI offers \"Undo\") but the snapshot returned from\n // execute() is never stored, and undo() would silently no-op.\n async buildLog({ input, result }) {\n if (result.status !== 'disconnected') return null\n return {\n resourceKind: 'communication_channels.channel',\n resourceId: result.channelId,\n tenantId: result.undo.tenantId ?? input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n payload: { undo: result.undo },\n snapshotBefore: result.undo,\n }\n },\n async undo({ ctx, logEntry }) {\n const snapshot = extractSnapshotFromLog(logEntry)\n if (!snapshot) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n // Never resolve by bare id (cross-tenant). New snapshots always carry\n // tenantId; refuse the undo if a legacy snapshot lacks it.\n if (!snapshot.tenantId) return\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: snapshot.channelId, tenantId: snapshot.tenantId },\n undefined,\n { tenantId: snapshot.tenantId, organizationId: null },\n )\n if (!channel) return\n\n channel.status = snapshot.previousStatus as CommunicationChannel['status']\n channel.isActive = snapshot.previousIsActive\n channel.isPrimary = snapshot.previousIsPrimary\n channel.credentialsRef = snapshot.previousCredentialsRef\n channel.lastError = snapshot.previousLastError\n await em.flush()\n },\n}\n\n/**\n * Read the undo payload defensively \u2014 wraps the shared\n * `@open-mercato/shared/lib/commands/undo.ts` helper with a narrow-by-shape\n * validation so callers get a strongly-typed snapshot or `null`.\n *\n * Kept as a separate export for test ergonomics (tests can mock the snapshot\n * shape directly without round-tripping through a CommandLogEntry).\n */\nexport function extractUndoPayload(value: unknown): DisconnectChannelUndoSnapshot | null {\n if (!value || typeof value !== 'object') return null\n const candidate = (value as { undo?: unknown; channelId?: unknown }).undo ?? value\n if (!candidate || typeof candidate !== 'object') return null\n const obj = candidate as Record<string, unknown>\n if (typeof obj.channelId !== 'string') return null\n return {\n channelId: obj.channelId,\n tenantId: typeof obj.tenantId === 'string' ? obj.tenantId : undefined,\n previousStatus: typeof obj.previousStatus === 'string' ? obj.previousStatus : 'connected',\n previousIsActive: typeof obj.previousIsActive === 'boolean' ? obj.previousIsActive : true,\n previousIsPrimary: typeof obj.previousIsPrimary === 'boolean' ? obj.previousIsPrimary : false,\n previousCredentialsRef:\n typeof obj.previousCredentialsRef === 'string' ? obj.previousCredentialsRef : null,\n previousLastError: typeof obj.previousLastError === 'string' ? obj.previousLastError : null,\n }\n}\n\n/**\n * Pulls the disconnect snapshot from a command log entry \u2014 first via the\n * shared `extractUndoPayload` helper, then through the local shape-validator.\n * Always falls back to `null` so the undo handler can no-op safely.\n */\nexport function extractSnapshotFromLog(logEntry: unknown): DisconnectChannelUndoSnapshot | null {\n const undo = extractSharedUndoPayload<DisconnectChannelUndoSnapshot>(\n (logEntry ?? null) as never,\n )\n if (undo) return extractUndoPayload(undo)\n return extractUndoPayload(logEntry)\n}\n\nregisterCommand(disconnectChannelCommand)\n\nexport default disconnectChannelCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
4
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
|
+
import { emitCommunicationChannelsEvent } from "../events.js";
|
|
6
|
+
import { resolveContact } from "../lib/contact-resolver.js";
|
|
7
|
+
import { matchThread } from "../lib/thread-matcher.js";
|
|
8
|
+
import {
|
|
9
|
+
ChannelThreadMapping,
|
|
10
|
+
CommunicationChannel,
|
|
11
|
+
ExternalConversation,
|
|
12
|
+
ExternalMessage,
|
|
13
|
+
MessageChannelLink
|
|
14
|
+
} from "../data/entities.js";
|
|
15
|
+
import { normalizedInboundMessageSchema } from "../data/validators.js";
|
|
16
|
+
import { resolveCommunicationChannelsSystemUserId } from "../lib/system-user.js";
|
|
17
|
+
import { isUniqueViolation } from "../lib/pg-errors.js";
|
|
18
|
+
const ingestInputSchema = z.object({
|
|
19
|
+
channelId: z.string().uuid(),
|
|
20
|
+
providerKey: z.string().min(1),
|
|
21
|
+
channelType: z.string().min(1),
|
|
22
|
+
scope: z.object({
|
|
23
|
+
tenantId: z.string().uuid(),
|
|
24
|
+
organizationId: z.string().uuid().nullable()
|
|
25
|
+
}),
|
|
26
|
+
message: normalizedInboundMessageSchema
|
|
27
|
+
});
|
|
28
|
+
const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = "communication_channels.message.ingest_inbound";
|
|
29
|
+
const ingestInboundMessageCommand = {
|
|
30
|
+
id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
31
|
+
async execute(rawInput, ctx) {
|
|
32
|
+
const input = ingestInputSchema.parse(rawInput);
|
|
33
|
+
const em = ctx.container.resolve("em").fork();
|
|
34
|
+
const adapterRegistry = ctx.container.resolve("channelAdapterRegistry");
|
|
35
|
+
const dscope = {
|
|
36
|
+
tenantId: input.scope.tenantId,
|
|
37
|
+
organizationId: input.scope.organizationId ?? null
|
|
38
|
+
};
|
|
39
|
+
const existingExternal = await findOneWithDecryption(
|
|
40
|
+
em,
|
|
41
|
+
ExternalMessage,
|
|
42
|
+
{
|
|
43
|
+
channelId: input.channelId,
|
|
44
|
+
externalMessageId: input.message.externalMessageId,
|
|
45
|
+
tenantId: input.scope.tenantId,
|
|
46
|
+
organizationId: input.scope.organizationId ?? null
|
|
47
|
+
},
|
|
48
|
+
void 0,
|
|
49
|
+
dscope
|
|
50
|
+
);
|
|
51
|
+
if (existingExternal) {
|
|
52
|
+
return {
|
|
53
|
+
status: "duplicate",
|
|
54
|
+
externalConversationId: existingExternal.conversationId,
|
|
55
|
+
externalMessageId: existingExternal.id
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const incomingMessageId = (() => {
|
|
59
|
+
const fromMeta = input.message.channelMetadata?.messageId;
|
|
60
|
+
if (typeof fromMeta === "string" && fromMeta.length > 0) return fromMeta;
|
|
61
|
+
return null;
|
|
62
|
+
})();
|
|
63
|
+
if (incomingMessageId) {
|
|
64
|
+
try {
|
|
65
|
+
const sentFolderHit = await em.getConnection().execute(
|
|
66
|
+
`SELECT link.id FROM message_channel_links AS link
|
|
67
|
+
INNER JOIN external_conversations AS conv
|
|
68
|
+
ON conv.id = link.external_conversation_id
|
|
69
|
+
WHERE link.tenant_id = ?
|
|
70
|
+
AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)
|
|
71
|
+
AND conv.tenant_id = ?
|
|
72
|
+
AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)
|
|
73
|
+
AND conv.channel_id = ?
|
|
74
|
+
AND link.direction = 'outbound'
|
|
75
|
+
AND link.channel_metadata->>'messageId' = ?
|
|
76
|
+
LIMIT 1`,
|
|
77
|
+
[
|
|
78
|
+
input.scope.tenantId,
|
|
79
|
+
input.scope.organizationId ?? null,
|
|
80
|
+
input.scope.organizationId ?? null,
|
|
81
|
+
input.scope.tenantId,
|
|
82
|
+
input.scope.organizationId ?? null,
|
|
83
|
+
input.scope.organizationId ?? null,
|
|
84
|
+
input.channelId,
|
|
85
|
+
incomingMessageId
|
|
86
|
+
]
|
|
87
|
+
);
|
|
88
|
+
if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {
|
|
89
|
+
return {
|
|
90
|
+
status: "duplicate",
|
|
91
|
+
externalConversationId: input.message.externalConversationId,
|
|
92
|
+
externalMessageId: input.message.externalMessageId
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (dedupErr) {
|
|
96
|
+
console.warn(
|
|
97
|
+
"[communication_channels:ingest-inbound] sent-folder dedup query failed, continuing:",
|
|
98
|
+
dedupErr instanceof Error ? dedupErr.message : dedupErr
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const channel = await findOneWithDecryption(
|
|
103
|
+
em,
|
|
104
|
+
CommunicationChannel,
|
|
105
|
+
{
|
|
106
|
+
id: input.channelId,
|
|
107
|
+
tenantId: input.scope.tenantId,
|
|
108
|
+
organizationId: input.scope.organizationId ?? null,
|
|
109
|
+
deletedAt: null
|
|
110
|
+
},
|
|
111
|
+
void 0,
|
|
112
|
+
dscope
|
|
113
|
+
);
|
|
114
|
+
if (!channel) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!channel.isActive) {
|
|
120
|
+
throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`);
|
|
121
|
+
}
|
|
122
|
+
const adapter = adapterRegistry.get(input.providerKey);
|
|
123
|
+
if (!adapter) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. Check that the provider package is enabled in modules.ts.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const m = input.message;
|
|
129
|
+
let conversation = await findOneWithDecryption(
|
|
130
|
+
em,
|
|
131
|
+
ExternalConversation,
|
|
132
|
+
{
|
|
133
|
+
channelId: input.channelId,
|
|
134
|
+
externalConversationId: m.externalConversationId,
|
|
135
|
+
tenantId: input.scope.tenantId,
|
|
136
|
+
organizationId: input.scope.organizationId ?? null
|
|
137
|
+
},
|
|
138
|
+
void 0,
|
|
139
|
+
dscope
|
|
140
|
+
);
|
|
141
|
+
let conversationCreated = false;
|
|
142
|
+
if (!conversation) {
|
|
143
|
+
conversation = em.create(ExternalConversation, {
|
|
144
|
+
channelId: input.channelId,
|
|
145
|
+
externalConversationId: m.externalConversationId,
|
|
146
|
+
subject: m.subject ?? null,
|
|
147
|
+
tenantId: input.scope.tenantId,
|
|
148
|
+
organizationId: input.scope.organizationId ?? null,
|
|
149
|
+
lastMessageAt: m.timestamp ?? /* @__PURE__ */ new Date()
|
|
150
|
+
});
|
|
151
|
+
em.persist(conversation);
|
|
152
|
+
conversationCreated = true;
|
|
153
|
+
}
|
|
154
|
+
let mapping = await findOneWithDecryption(
|
|
155
|
+
em,
|
|
156
|
+
ChannelThreadMapping,
|
|
157
|
+
{
|
|
158
|
+
externalConversationId: conversation.id,
|
|
159
|
+
tenantId: input.scope.tenantId,
|
|
160
|
+
organizationId: input.scope.organizationId ?? null
|
|
161
|
+
},
|
|
162
|
+
void 0,
|
|
163
|
+
dscope
|
|
164
|
+
);
|
|
165
|
+
if (!conversationCreated && m.timestamp && (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)) {
|
|
166
|
+
conversation.lastMessageAt = m.timestamp;
|
|
167
|
+
}
|
|
168
|
+
await em.flush();
|
|
169
|
+
const metaForMatcher = m.channelMetadata ?? {};
|
|
170
|
+
let threadMatch = null;
|
|
171
|
+
try {
|
|
172
|
+
threadMatch = await matchThread(
|
|
173
|
+
{
|
|
174
|
+
channelId: input.channelId,
|
|
175
|
+
tenantId: input.scope.tenantId,
|
|
176
|
+
organizationId: input.scope.organizationId ?? null,
|
|
177
|
+
messageId: extractStringFromMeta(metaForMatcher, "messageId") ?? m.externalMessageId,
|
|
178
|
+
inReplyTo: m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, "inReplyTo"),
|
|
179
|
+
references: extractStringArrayFromMeta(metaForMatcher, "references"),
|
|
180
|
+
subject: m.subject ?? "",
|
|
181
|
+
fromAddress: extractStringFromMeta(metaForMatcher, "from") ?? m.senderIdentifier,
|
|
182
|
+
toAddresses: extractStringArrayFromMeta(metaForMatcher, "to"),
|
|
183
|
+
ccAddresses: extractStringArrayFromMeta(metaForMatcher, "cc"),
|
|
184
|
+
bodyPlain: m.bodyFormat === "html" ? null : m.body ?? null,
|
|
185
|
+
bodyHtml: m.bodyFormat === "html" ? m.body ?? null : null,
|
|
186
|
+
receivedAt: m.timestamp ?? /* @__PURE__ */ new Date()
|
|
187
|
+
},
|
|
188
|
+
{ em }
|
|
189
|
+
);
|
|
190
|
+
} catch (matcherErr) {
|
|
191
|
+
console.warn(
|
|
192
|
+
"[communication_channels:ingest-inbound] thread matcher failed, falling back to conversation mapping:",
|
|
193
|
+
matcherErr instanceof Error ? matcherErr.message : matcherErr
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
let contactHint = null;
|
|
197
|
+
try {
|
|
198
|
+
contactHint = await resolveContact(
|
|
199
|
+
{
|
|
200
|
+
adapter,
|
|
201
|
+
senderIdentifier: m.senderIdentifier,
|
|
202
|
+
senderDisplayName: m.senderDisplayName,
|
|
203
|
+
channelMetadata: m.channelMetadata,
|
|
204
|
+
credentials: {},
|
|
205
|
+
// credentials decrypted at the webhook route; resolver doesn't re-fetch
|
|
206
|
+
scope: {
|
|
207
|
+
tenantId: input.scope.tenantId,
|
|
208
|
+
organizationId: input.scope.organizationId ?? input.scope.tenantId
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{ container: ctx.container }
|
|
212
|
+
);
|
|
213
|
+
} catch (contactErr) {
|
|
214
|
+
console.warn(
|
|
215
|
+
"[communication_channels:ingest-inbound] contact resolution failed, continuing without a CRM match:",
|
|
216
|
+
contactErr instanceof Error ? contactErr.message : contactErr
|
|
217
|
+
);
|
|
218
|
+
contactHint = null;
|
|
219
|
+
}
|
|
220
|
+
const matchedPersonId = contactHint?.matchedPersonId ?? null;
|
|
221
|
+
if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {
|
|
222
|
+
conversation.contactPersonId = matchedPersonId;
|
|
223
|
+
await em.flush();
|
|
224
|
+
}
|
|
225
|
+
const MAX_COMPOSE_BODY = 5e4;
|
|
226
|
+
const TRUNCATE_MARKER = "\n\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]";
|
|
227
|
+
const rawBody = m.body ?? "";
|
|
228
|
+
const truncatedBody = rawBody.length > MAX_COMPOSE_BODY ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER : rawBody;
|
|
229
|
+
const safeSubject = (m.subject ?? "").trim() || "(no subject)";
|
|
230
|
+
const composeInput = {
|
|
231
|
+
type: `channel.${input.providerKey}`,
|
|
232
|
+
visibility: "public",
|
|
233
|
+
sourceEntityType: "communication_channels.external_conversation",
|
|
234
|
+
sourceEntityId: conversation.id,
|
|
235
|
+
externalEmail: contactHint?.email ?? void 0,
|
|
236
|
+
externalName: contactHint?.displayName ?? m.senderDisplayName,
|
|
237
|
+
recipients: mapping?.assignedUserId ? [{ userId: mapping.assignedUserId, type: "to" }] : [],
|
|
238
|
+
subject: safeSubject,
|
|
239
|
+
body: truncatedBody,
|
|
240
|
+
bodyFormat: m.bodyFormat === "html" ? "text" : m.bodyFormat,
|
|
241
|
+
priority: "normal",
|
|
242
|
+
sendViaEmail: false,
|
|
243
|
+
// Spec B: matcher-resolved thread id takes priority over the existing
|
|
244
|
+
// conversation-based mapping. Falls through to `mapping?.messageThreadId`
|
|
245
|
+
// when the matcher returned null (no token / JWZ / subject hit).
|
|
246
|
+
parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,
|
|
247
|
+
isDraft: false,
|
|
248
|
+
// Stable dedup key so a retried ingest (after a transient failure between
|
|
249
|
+
// compose and the ExternalMessage anchor insert) reuses the message
|
|
250
|
+
// composed by the first attempt instead of duplicating it. Mirrors the
|
|
251
|
+
// (channel, externalMessageId) ExternalMessage anchor's natural key.
|
|
252
|
+
idempotencyKey: m.externalMessageId ? `cc:${input.channelId}:${m.externalMessageId}` : void 0,
|
|
253
|
+
tenantId: input.scope.tenantId,
|
|
254
|
+
organizationId: input.scope.organizationId,
|
|
255
|
+
userId: await resolveCommunicationChannelsSystemUserId(
|
|
256
|
+
em,
|
|
257
|
+
input.scope.tenantId,
|
|
258
|
+
mapping?.assignedUserId ?? null
|
|
259
|
+
)
|
|
260
|
+
};
|
|
261
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
262
|
+
const composeResult = await commandBus.execute(
|
|
263
|
+
"messages.messages.compose",
|
|
264
|
+
{
|
|
265
|
+
input: composeInput,
|
|
266
|
+
ctx: passthroughCommandCtx(ctx, input.scope)
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
const message = composeResult.result;
|
|
270
|
+
if (!message?.id) {
|
|
271
|
+
throw new Error("messages.messages.compose did not return a message id");
|
|
272
|
+
}
|
|
273
|
+
if (!mapping) {
|
|
274
|
+
mapping = em.create(ChannelThreadMapping, {
|
|
275
|
+
externalConversationId: conversation.id,
|
|
276
|
+
messageThreadId: message.threadId ?? message.id,
|
|
277
|
+
channelId: input.channelId,
|
|
278
|
+
providerKey: input.providerKey,
|
|
279
|
+
externalThreadRef: m.externalConversationId,
|
|
280
|
+
tenantId: input.scope.tenantId,
|
|
281
|
+
organizationId: input.scope.organizationId ?? null
|
|
282
|
+
});
|
|
283
|
+
em.persist(mapping);
|
|
284
|
+
}
|
|
285
|
+
const externalMessageRowId = randomUUID();
|
|
286
|
+
const channelLinkRowId = randomUUID();
|
|
287
|
+
const externalMessage = em.create(ExternalMessage, {
|
|
288
|
+
id: externalMessageRowId,
|
|
289
|
+
channelId: input.channelId,
|
|
290
|
+
conversationId: conversation.id,
|
|
291
|
+
externalMessageId: m.externalMessageId,
|
|
292
|
+
direction: "inbound",
|
|
293
|
+
senderIdentifier: m.senderIdentifier,
|
|
294
|
+
senderDisplayName: m.senderDisplayName ?? null,
|
|
295
|
+
providerTimestamp: m.timestamp,
|
|
296
|
+
tenantId: input.scope.tenantId,
|
|
297
|
+
organizationId: input.scope.organizationId ?? null
|
|
298
|
+
});
|
|
299
|
+
em.persist(externalMessage);
|
|
300
|
+
const matcherAnnotatedMetadata = {
|
|
301
|
+
...m.channelMetadata ?? {},
|
|
302
|
+
threadMatchStrategy: threadMatch?.matchedBy ?? "new-thread",
|
|
303
|
+
threadMatchConfidence: threadMatch?.confidence ?? "low"
|
|
304
|
+
};
|
|
305
|
+
const channelLink = em.create(MessageChannelLink, {
|
|
306
|
+
id: channelLinkRowId,
|
|
307
|
+
messageId: message.id,
|
|
308
|
+
externalConversationId: conversation.id,
|
|
309
|
+
externalMessageId: externalMessageRowId,
|
|
310
|
+
providerKey: input.providerKey,
|
|
311
|
+
channelType: input.channelType,
|
|
312
|
+
direction: "inbound",
|
|
313
|
+
deliveryStatus: "received",
|
|
314
|
+
channelPayload: m.channelPayload,
|
|
315
|
+
channelContentType: m.channelContentType,
|
|
316
|
+
channelMetadata: matcherAnnotatedMetadata,
|
|
317
|
+
tenantId: input.scope.tenantId,
|
|
318
|
+
organizationId: input.scope.organizationId ?? null
|
|
319
|
+
});
|
|
320
|
+
em.persist(channelLink);
|
|
321
|
+
try {
|
|
322
|
+
await em.flush();
|
|
323
|
+
} catch (flushErr) {
|
|
324
|
+
if (isUniqueViolation(flushErr)) {
|
|
325
|
+
return {
|
|
326
|
+
status: "duplicate",
|
|
327
|
+
externalConversationId: conversation.id,
|
|
328
|
+
externalMessageId: m.externalMessageId
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
throw flushErr;
|
|
332
|
+
}
|
|
333
|
+
if (conversationCreated) {
|
|
334
|
+
await emitCommunicationChannelsEvent(
|
|
335
|
+
"communication_channels.conversation.created",
|
|
336
|
+
{
|
|
337
|
+
conversationId: conversation.id,
|
|
338
|
+
channelId: input.channelId,
|
|
339
|
+
providerKey: input.providerKey,
|
|
340
|
+
channelType: input.channelType,
|
|
341
|
+
externalConversationId: m.externalConversationId,
|
|
342
|
+
tenantId: input.scope.tenantId,
|
|
343
|
+
organizationId: input.scope.organizationId ?? null
|
|
344
|
+
},
|
|
345
|
+
{ persistent: true }
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (matchedPersonId) {
|
|
349
|
+
await emitCommunicationChannelsEvent(
|
|
350
|
+
"communication_channels.contact.resolved",
|
|
351
|
+
{
|
|
352
|
+
conversationId: conversation.id,
|
|
353
|
+
contactPersonId: matchedPersonId,
|
|
354
|
+
providerKey: input.providerKey,
|
|
355
|
+
tenantId: input.scope.tenantId,
|
|
356
|
+
organizationId: input.scope.organizationId ?? null
|
|
357
|
+
},
|
|
358
|
+
{ persistent: true }
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
await emitCommunicationChannelsEvent(
|
|
362
|
+
"communication_channels.message.received",
|
|
363
|
+
{
|
|
364
|
+
messageId: message.id,
|
|
365
|
+
externalMessageId: externalMessage.id,
|
|
366
|
+
channelLinkId: channelLink.id,
|
|
367
|
+
conversationId: conversation.id,
|
|
368
|
+
channelId: input.channelId,
|
|
369
|
+
providerKey: input.providerKey,
|
|
370
|
+
channelType: input.channelType,
|
|
371
|
+
direction: "inbound",
|
|
372
|
+
tenantId: input.scope.tenantId,
|
|
373
|
+
organizationId: input.scope.organizationId ?? null
|
|
374
|
+
},
|
|
375
|
+
{ persistent: true }
|
|
376
|
+
);
|
|
377
|
+
return {
|
|
378
|
+
status: "created",
|
|
379
|
+
messageId: message.id,
|
|
380
|
+
externalConversationId: conversation.id,
|
|
381
|
+
externalMessageId: externalMessage.id,
|
|
382
|
+
channelLinkId: channelLink.id,
|
|
383
|
+
threadMappingId: mapping.id,
|
|
384
|
+
contactPersonId: matchedPersonId
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
function passthroughCommandCtx(parent, scope) {
|
|
389
|
+
return {
|
|
390
|
+
container: parent.container,
|
|
391
|
+
auth: null,
|
|
392
|
+
organizationScope: null,
|
|
393
|
+
selectedOrganizationId: scope.organizationId ?? null,
|
|
394
|
+
organizationIds: scope.organizationId ? [scope.organizationId] : null
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function extractStringFromMeta(meta, key) {
|
|
398
|
+
const value = meta[key];
|
|
399
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
function extractStringArrayFromMeta(meta, key) {
|
|
403
|
+
const value = meta[key];
|
|
404
|
+
if (!Array.isArray(value)) return [];
|
|
405
|
+
return value.filter((item) => typeof item === "string");
|
|
406
|
+
}
|
|
407
|
+
registerCommand(ingestInboundMessageCommand);
|
|
408
|
+
var ingest_inbound_message_default = ingestInboundMessageCommand;
|
|
409
|
+
export {
|
|
410
|
+
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
411
|
+
ingest_inbound_message_default as default
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=ingest-inbound-message.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/commands/ingest-inbound-message.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { resolveContact } from '../lib/contact-resolver'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { matchThread, type ThreadMatch } from '../lib/thread-matcher'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\nimport { normalizedInboundMessageSchema } from '../data/validators'\nimport { resolveCommunicationChannelsSystemUserId } from '../lib/system-user'\nimport { isUniqueViolation } from '../lib/pg-errors'\n\nconst ingestInputSchema = z.object({\n channelId: z.string().uuid(),\n providerKey: z.string().min(1),\n channelType: z.string().min(1),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n message: normalizedInboundMessageSchema,\n})\n\nexport type IngestInboundMessageInput = z.infer<typeof ingestInputSchema>\n\nexport type IngestInboundMessageResult = {\n status: 'created' | 'duplicate'\n messageId?: string\n externalConversationId?: string\n externalMessageId?: string\n channelLinkId?: string\n threadMappingId?: string\n contactPersonId?: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = 'communication_channels.message.ingest_inbound'\n\n/**\n * Idempotently ingest a normalized inbound channel message.\n *\n * Steps (per SPEC-045d \u00A76):\n * 1. Dedup by `(channel_id, external_message_id)` \u2014 if a MessageChannelLink already\n * exists for that pair, return `{ status: 'duplicate' }` without side effects.\n * 2. Create or load `ExternalConversation` by `(channel_id, external_conversation_id)`.\n * 3. Create or load `ChannelThreadMapping` (1:1 with ExternalConversation).\n * 4. Resolve CRM contact via adapter + QueryEngine (best-effort).\n * 5. Compose the platform `Message` via `messages.messages.compose` (separate transaction).\n * 6. Create `ExternalMessage` + `MessageChannelLink`.\n * 7. Emit `communication_channels.message.received` (and `.conversation.created` / `.contact.resolved` when applicable).\n *\n * The two-transaction model (compose-message-then-record-link) is acceptable for v1;\n * the link's unique-on-message-id constraint is the safety net against orphans. See\n * the pre-implementation analysis for a discussion of single-transaction alternatives.\n */\nconst ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, IngestInboundMessageResult> = {\n id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = ingestInputSchema.parse(rawInput) as IngestInboundMessageInput\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n // (1) Dedup: short-circuit if we've already processed this provider message.\n // The unique constraint is on `messageId`, not (channel, externalMessageId).\n // We must dedup by joining against ExternalMessage which IS uniquely indexed by\n // (channel_id, external_message_id). Hub-side dedup is the authoritative gate.\n const existingExternal = await findOneWithDecryption(\n em,\n ExternalMessage,\n {\n channelId: input.channelId,\n externalMessageId: input.message.externalMessageId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (existingExternal) {\n return {\n status: 'duplicate',\n externalConversationId: existingExternal.conversationId,\n externalMessageId: existingExternal.id,\n }\n }\n\n // (1b) Spec B \u00A7 Sent-folder dedup.\n //\n // When an outbound message lands in the user's IMAP Sent folder (or\n // when Gmail's \"send and archive\" deposits it in All Mail), the next\n // poll will re-fetch it from INBOX as if it were inbound. Skip it\n // here using the RFC 5322 `Message-ID` header \u2014 we recorded it on the\n // outbound `MessageChannelLink.channelMetadata.messageId` at send time.\n //\n // We dedup ONLY on outbound links (direction='outbound') for the same\n // channel \u2014 that way an inbound copy of someone ELSE's email that\n // happens to share a Message-ID is still ingested normally.\n const incomingMessageId = (() => {\n const fromMeta = (input.message.channelMetadata as Record<string, unknown> | undefined)?.messageId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n return null\n })()\n if (incomingMessageId) {\n // MikroORM v7 dropped the Knex builder in favour of Kysely/raw SQL.\n // We use a positional-placeholder raw query for the JSONB\n // `channel_metadata->>messageId` comparison.\n try {\n const sentFolderHit = await em.getConnection().execute<Array<{ id: string }>>(\n `SELECT link.id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.direction = 'outbound'\n AND link.channel_metadata->>'messageId' = ?\n LIMIT 1`,\n [\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.channelId,\n incomingMessageId,\n ],\n )\n if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {\n return {\n status: 'duplicate',\n externalConversationId: input.message.externalConversationId,\n externalMessageId: input.message.externalMessageId,\n }\n }\n } catch (dedupErr) {\n // Sent-folder dedup is best-effort \u2014 a failure here must not abort\n // ingest (better a possible duplicate than a lost inbound message).\n console.warn(\n '[communication_channels:ingest-inbound] sent-folder dedup query failed, continuing:',\n dedupErr instanceof Error ? dedupErr.message : dedupErr,\n )\n }\n }\n\n // Channel + adapter lookup (the channel must exist + be active).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw new Error(\n `[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`,\n )\n }\n if (!channel.isActive) {\n throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`)\n }\n\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. ` +\n 'Check that the provider package is enabled in modules.ts.',\n )\n }\n\n // (2) ExternalConversation upsert by (channel_id, externalConversationId).\n const m = input.message\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n let conversationCreated = false\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n subject: m.subject ?? null,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n lastMessageAt: m.timestamp ?? new Date(),\n })\n em.persist(conversation)\n conversationCreated = true\n }\n\n // (3) ChannelThreadMapping upsert (1:1 with ExternalConversation per tenant).\n let mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n\n // Last-activity bump on an existing conversation. Applied AFTER the mapping\n // lookup, immediately before the flush, so the scalar mutation and its flush\n // stay adjacent with no query in between (core flush-ordering rule \u2014 a query\n // between a scalar mutation and `em.flush()` can drop the change under some\n // flush modes / subscriber configurations).\n if (\n !conversationCreated &&\n m.timestamp &&\n (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)\n ) {\n conversation.lastMessageAt = m.timestamp\n }\n // We'll fill `messageThreadId` after composing the platform Message (since the\n // first inbound message becomes the thread root in the messages module).\n await em.flush()\n\n // (3b) Spec B \u2014 layered thread match.\n //\n // Resolve the inbound message to an existing platform thread using\n // (in priority order):\n // 1. Crypto token in References / In-Reply-To header (high confidence)\n // 2. Crypto token in body hidden span or plain-text marker (high)\n // 3. JWZ on Message-Id \u2194 stored `MessageChannelLink.channelMetadata.messageId` (medium)\n // 4. Subject + participants in last 30 days, same channel (low)\n //\n // The matcher returns `null` when nothing hits \u2014 in that case we fall\n // back to the existing `ChannelThreadMapping`-by-conversation-id lookup\n // (which also returns null on first-ever inbound, in which case the\n // compose command opens a new thread).\n const metaForMatcher = (m.channelMetadata ?? {}) as Record<string, unknown>\n let threadMatch: ThreadMatch | null = null\n try {\n threadMatch = await matchThread(\n {\n channelId: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n messageId: extractStringFromMeta(metaForMatcher, 'messageId') ?? m.externalMessageId,\n inReplyTo:\n m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, 'inReplyTo'),\n references: extractStringArrayFromMeta(metaForMatcher, 'references'),\n subject: m.subject ?? '',\n fromAddress:\n extractStringFromMeta(metaForMatcher, 'from') ?? m.senderIdentifier,\n toAddresses: extractStringArrayFromMeta(metaForMatcher, 'to'),\n ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),\n bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,\n bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,\n receivedAt: m.timestamp ?? new Date(),\n },\n { em },\n )\n } catch (matcherErr) {\n // Matcher failure must not block ingest \u2014 fall back to the existing\n // conversation-based thread mapping so the message still lands.\n console.warn(\n '[communication_channels:ingest-inbound] thread matcher failed, falling back to conversation mapping:',\n matcherErr instanceof Error ? matcherErr.message : matcherErr,\n )\n }\n\n // (4) Contact resolution (best-effort, advisory).\n let contactHint: {\n matchedPersonId?: string | null\n email?: string\n displayName?: string\n } | null = null\n try {\n contactHint = await resolveContact(\n {\n adapter,\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName,\n channelMetadata: m.channelMetadata,\n credentials: {}, // credentials decrypted at the webhook route; resolver doesn't re-fetch\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n },\n { container: ctx.container },\n )\n } catch (contactErr) {\n // Best-effort: contact resolution is advisory and must not abort ingest.\n // Log like the sibling dedup/matcher catches so a misbehaving resolver is\n // visible in operator logs instead of failing silently.\n console.warn(\n '[communication_channels:ingest-inbound] contact resolution failed, continuing without a CRM match:',\n contactErr instanceof Error ? contactErr.message : contactErr,\n )\n contactHint = null\n }\n const matchedPersonId = contactHint?.matchedPersonId ?? null\n if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {\n conversation.contactPersonId = matchedPersonId\n // Flush this scalar mutation before the system-user lookup below queries the\n // same EntityManager. SPEC-018: a query between a scalar mutation and its\n // flush can silently discard the pending UPDATE (mirrors the lastMessageAt\n // bump above).\n await em.flush()\n }\n\n // (5) Compose the platform Message via the messages module command.\n //\n // Sanitize against the `messages` module's validators (max 50_000 char body\n // + non-empty subject) so real-world emails don't get rejected mid-ingest:\n // - HTML emails routinely exceed 50k (Gmail signatures, marketing\n // templates, RFC 5322 multipart). Truncate with a marker rather than\n // drop the whole message \u2014 the full raw body is still in\n // ExternalMessage.rawPayload if needed for forensic / forward use.\n // - Some legitimate messages have no subject (notifications, bounce\n // digests). Substitute a placeholder instead of failing ingest.\n const MAX_COMPOSE_BODY = 50_000\n const TRUNCATE_MARKER =\n '\\n\\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]'\n const rawBody = m.body ?? ''\n const truncatedBody =\n rawBody.length > MAX_COMPOSE_BODY\n ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER\n : rawBody\n const safeSubject = (m.subject ?? '').trim() || '(no subject)'\n\n const composeInput = {\n type: `channel.${input.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.external_conversation',\n sourceEntityId: conversation.id,\n externalEmail: contactHint?.email ?? undefined,\n externalName: contactHint?.displayName ?? m.senderDisplayName,\n recipients: mapping?.assignedUserId\n ? [{ userId: mapping.assignedUserId, type: 'to' as const }]\n : [],\n subject: safeSubject,\n body: truncatedBody,\n bodyFormat: (m.bodyFormat === 'html' ? 'text' : m.bodyFormat) as 'text' | 'markdown',\n priority: 'normal' as const,\n sendViaEmail: false,\n // Spec B: matcher-resolved thread id takes priority over the existing\n // conversation-based mapping. Falls through to `mapping?.messageThreadId`\n // when the matcher returned null (no token / JWZ / subject hit).\n parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,\n isDraft: false,\n // Stable dedup key so a retried ingest (after a transient failure between\n // compose and the ExternalMessage anchor insert) reuses the message\n // composed by the first attempt instead of duplicating it. Mirrors the\n // (channel, externalMessageId) ExternalMessage anchor's natural key.\n idempotencyKey: m.externalMessageId\n ? `cc:${input.channelId}:${m.externalMessageId}`\n : undefined,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: await resolveCommunicationChannelsSystemUserId(\n em,\n input.scope.tenantId,\n mapping?.assignedUserId ?? null,\n ),\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const composeResult = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: passthroughCommandCtx(ctx, input.scope),\n },\n )\n const message = composeResult.result\n if (!message?.id) {\n throw new Error('messages.messages.compose did not return a message id')\n }\n\n // (3 continued) Create or update ChannelThreadMapping now that we have a threadId.\n if (!mapping) {\n mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: message.threadId ?? message.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n externalThreadRef: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(mapping)\n }\n\n // (6) Create ExternalMessage + MessageChannelLink (hub-side records).\n //\n // The PrimaryKey for both uses `defaultRaw: 'gen_random_uuid()'` \u2014 a\n // Postgres-side default that doesn't populate the entity's `id` field\n // until after the INSERT returns. If we let MikroORM generate both, then\n // `em.create(MessageChannelLink, { externalMessageId: externalMessage.id })`\n // reads `undefined` for `externalMessage.id` (it hasn't been flushed yet)\n // and writes NULL to `message_channel_links.external_message_id`,\n // breaking the FK and causing downstream joins to silently return 0 rows.\n //\n // Pre-generating both UUIDs client-side fixes the cross-row reference\n // problem and keeps the single-transaction flush semantics intact.\n const externalMessageRowId = randomUUID()\n const channelLinkRowId = randomUUID()\n const externalMessage = em.create(ExternalMessage, {\n id: externalMessageRowId,\n channelId: input.channelId,\n conversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n direction: 'inbound',\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName ?? null,\n providerTimestamp: m.timestamp,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(externalMessage)\n\n // Spec B: annotate the link with which thread-matcher strategy resolved\n // this message (or `'new-thread'` when matcher returned null and we\n // opened a fresh thread). Surfaced to observability + future UI (\"this\n // thread match is low-confidence \u2014 confirm or move\").\n const matcherAnnotatedMetadata: Record<string, unknown> = {\n ...((m.channelMetadata as Record<string, unknown> | undefined) ?? {}),\n threadMatchStrategy: threadMatch?.matchedBy ?? 'new-thread',\n threadMatchConfidence: threadMatch?.confidence ?? 'low',\n }\n\n const channelLink = em.create(MessageChannelLink, {\n id: channelLinkRowId,\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessageRowId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n deliveryStatus: 'received',\n channelPayload: m.channelPayload,\n channelContentType: m.channelContentType,\n channelMetadata: matcherAnnotatedMetadata,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(channelLink)\n\n try {\n await em.flush()\n } catch (flushErr) {\n // Concurrency guard: the pre-check at (1) is not atomic with this insert,\n // so a poll re-fetch racing a push notification (or two push deliveries)\n // can both pass the check and reach here. The `(channel_id,\n // external_message_id)` unique index rejects the loser with a 23505. Treat\n // that as a duplicate \u2014 returning here (instead of throwing) prevents the\n // message from being dead-lettered and retried forever. The winning job\n // already recorded the message + link.\n if (isUniqueViolation(flushErr)) {\n return {\n status: 'duplicate',\n externalConversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n }\n }\n throw flushErr\n }\n\n // (7) Emit events \u2014 order matters for downstream subscribers.\n if (conversationCreated) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.created',\n {\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n if (matchedPersonId) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.contact.resolved',\n {\n conversationId: conversation.id,\n contactPersonId: matchedPersonId,\n providerKey: input.providerKey,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n messageId: message.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'created',\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n threadMappingId: mapping.id,\n contactPersonId: matchedPersonId,\n }\n },\n}\n\n\n/**\n * Build a runtime context for the nested `messages.messages.compose` call.\n *\n * The compose command expects a `CommandRuntimeContext`. For inbound webhook\n * processing there is no platform user; we pass `auth: null` and use the tenant\n * scope from our input.\n */\nfunction passthroughCommandCtx(\n parent: CommandRuntimeContext,\n scope: IngestInboundMessageInput['scope'],\n): CommandRuntimeContext {\n return {\n container: parent.container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n}\n\n/**\n * Pull a string value from the provider's `channelMetadata` map. Returns\n * `null` (not `undefined`) when the key is absent or the value isn't a\n * string \u2014 keeps the matcher's input shape predictable.\n */\nfunction extractStringFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string | null {\n const value = meta[key]\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * Pull a string[] value from the provider's `channelMetadata` map.\n * Filters out non-string entries defensively. Returns an empty array\n * when the key is absent or the value isn't an array.\n */\nfunction extractStringArrayFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string[] {\n const value = meta[key]\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === 'string')\n}\n\nregisterCommand(ingestInboundMessageCommand)\n\nexport default ingestInboundMessageCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
4
|
+
import {
|
|
5
|
+
COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,
|
|
6
|
+
extractSnapshotFromLog
|
|
7
|
+
} from "./disconnect-channel.js";
|
|
8
|
+
const interceptors = [
|
|
9
|
+
{
|
|
10
|
+
id: "communication_channels.disconnect-channel-before-undo",
|
|
11
|
+
targetCommand: COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,
|
|
12
|
+
priority: 50,
|
|
13
|
+
async beforeUndo(undoContext, ctxRuntime) {
|
|
14
|
+
const snapshot = extractSnapshotFromLog(
|
|
15
|
+
undoContext.logEntry
|
|
16
|
+
);
|
|
17
|
+
if (!snapshot) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!snapshot.previousIsPrimary) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const em = ctxRuntime.container.resolve("em").fork();
|
|
24
|
+
if (!snapshot.tenantId) return;
|
|
25
|
+
const ownedChannel = await findOneWithDecryption(
|
|
26
|
+
em,
|
|
27
|
+
CommunicationChannel,
|
|
28
|
+
{ id: snapshot.channelId, tenantId: snapshot.tenantId },
|
|
29
|
+
void 0,
|
|
30
|
+
{ tenantId: snapshot.tenantId, organizationId: null }
|
|
31
|
+
);
|
|
32
|
+
if (!ownedChannel) return;
|
|
33
|
+
const otherPrimary = await findOneWithDecryption(
|
|
34
|
+
em,
|
|
35
|
+
CommunicationChannel,
|
|
36
|
+
{
|
|
37
|
+
tenantId: ownedChannel.tenantId,
|
|
38
|
+
organizationId: ownedChannel.organizationId ?? null,
|
|
39
|
+
userId: ownedChannel.userId,
|
|
40
|
+
isPrimary: true,
|
|
41
|
+
deletedAt: null
|
|
42
|
+
},
|
|
43
|
+
void 0,
|
|
44
|
+
{
|
|
45
|
+
tenantId: ownedChannel.tenantId,
|
|
46
|
+
organizationId: ownedChannel.organizationId ?? null
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
if (otherPrimary && otherPrimary.id !== snapshot.channelId) {
|
|
50
|
+
const fallback = "Another channel is now primary for this user. Set it as non-primary before restoring the disconnected channel as primary.";
|
|
51
|
+
let message = fallback;
|
|
52
|
+
try {
|
|
53
|
+
const { translate } = await resolveTranslations();
|
|
54
|
+
message = translate("communication_channels.errors.undoBlockedPrimaryConflict", fallback);
|
|
55
|
+
} catch {
|
|
56
|
+
message = fallback;
|
|
57
|
+
}
|
|
58
|
+
return { ok: false, message };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
var interceptors_default = interceptors;
|
|
64
|
+
export {
|
|
65
|
+
interceptors_default as default,
|
|
66
|
+
interceptors
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=interceptors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/commands/interceptors.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type {\n CommandInterceptor,\n CommandInterceptorBeforeResult,\n CommandInterceptorUndoContext,\n} from '@open-mercato/shared/lib/commands/command-interceptor'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,\n extractSnapshotFromLog,\n type DisconnectChannelUndoSnapshot,\n} from './disconnect-channel'\n\n/**\n * Command interceptors for the Communications Hub.\n *\n * Phase 4 deliverable 5 of the email integration spec:\n * - `beforeUndo` interceptor on `communication_channels.channel.disconnect`.\n * If a different channel became primary while this one was disconnected,\n * blocking the undo is the right call \u2014 restoring `is_primary=true` would\n * violate the partial unique index `communication_channels_one_primary_per_user_uq`,\n * and silently demoting the *other* channel would surprise the user.\n *\n * Auto-discovered by `commands/interceptors.ts` convention; no DI registration\n * required.\n */\nexport const interceptors: CommandInterceptor[] = [\n {\n id: 'communication_channels.disconnect-channel-before-undo',\n targetCommand: COMMUNICATION_CHANNELS_DISCONNECT_CHANNEL_COMMAND_ID,\n priority: 50,\n async beforeUndo(\n undoContext: CommandInterceptorUndoContext,\n ctxRuntime,\n ): Promise<CommandInterceptorBeforeResult | void> {\n // Read the snapshot exactly as the command's own undo() handler does \u2014 from\n // the persisted action-log `commandPayload.undo` (written by buildLog). The\n // earlier `result.undo`/`resultJson`/`resultBody` shapes do not exist on the\n // ActionLog entity, so this guard previously never fired.\n const snapshot: DisconnectChannelUndoSnapshot | null = extractSnapshotFromLog(\n undoContext.logEntry,\n )\n if (!snapshot) {\n // No snapshot we can interpret \u2192 let undo proceed; the command's own\n // undo() handler will no-op if the channel can't be re-resolved.\n return\n }\n // Only enforce when the disconnected channel was the primary.\n if (!snapshot.previousIsPrimary) {\n return\n }\n\n const em = (ctxRuntime.container.resolve('em') as EntityManager).fork()\n // Resolve only within the snapshot's tenant \u2014 never by bare id, which\n // would cross tenant boundaries. New snapshots always carry tenantId.\n if (!snapshot.tenantId) return\n const ownedChannel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: snapshot.channelId, tenantId: snapshot.tenantId },\n undefined,\n { tenantId: snapshot.tenantId as string, organizationId: null },\n )\n if (!ownedChannel) return // command's undo() will silently no-op\n\n const otherPrimary = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n tenantId: ownedChannel.tenantId,\n organizationId: ownedChannel.organizationId ?? null,\n userId: ownedChannel.userId,\n isPrimary: true,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: ownedChannel.tenantId,\n organizationId: ownedChannel.organizationId ?? null,\n },\n )\n if (otherPrimary && otherPrimary.id !== snapshot.channelId) {\n // Operator-facing block reason. Localize via the request locale when\n // available, but undo can also run outside a request (queue worker / CLI)\n // where the i18n module registry is uninitialized and resolveTranslations()\n // throws \u2014 fall back to the English string rather than failing the block.\n const fallback =\n 'Another channel is now primary for this user. Set it as non-primary before restoring the disconnected channel as primary.'\n let message = fallback\n try {\n const { translate } = await resolveTranslations()\n message = translate('communication_channels.errors.undoBlockedPrimaryConflict', fallback)\n } catch {\n message = fallback\n }\n return { ok: false, message }\n }\n },\n },\n]\n\nexport default interceptors\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AAeA,MAAM,eAAqC;AAAA,EAChD;AAAA,IACE,IAAI;AAAA,IACJ,eAAe;AAAA,IACf,UAAU;AAAA,IACV,MAAM,WACJ,aACA,YACgD;AAKhD,YAAM,WAAiD;AAAA,QACrD,YAAY;AAAA,MACd;AACA,UAAI,CAAC,UAAU;AAGb;AAAA,MACF;AAEA,UAAI,CAAC,SAAS,mBAAmB;AAC/B;AAAA,MACF;AAEA,YAAM,KAAM,WAAW,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAGtE,UAAI,CAAC,SAAS,SAAU;AACxB,YAAM,eAAe,MAAM;AAAA,QACzB;AAAA,QACA;AAAA,QACA,EAAE,IAAI,SAAS,WAAW,UAAU,SAAS,SAAS;AAAA,QACtD;AAAA,QACA,EAAE,UAAU,SAAS,UAAoB,gBAAgB,KAAK;AAAA,MAChE;AACA,UAAI,CAAC,aAAc;AAEnB,YAAM,eAAe,MAAM;AAAA,QACzB;AAAA,QACA;AAAA,QACA;AAAA,UACE,UAAU,aAAa;AAAA,UACvB,gBAAgB,aAAa,kBAAkB;AAAA,UAC/C,QAAQ,aAAa;AAAA,UACrB,WAAW;AAAA,UACX,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,UACE,UAAU,aAAa;AAAA,UACvB,gBAAgB,aAAa,kBAAkB;AAAA,QACjD;AAAA,MACF;AACA,UAAI,gBAAgB,aAAa,OAAO,SAAS,WAAW;AAK1D,cAAM,WACJ;AACF,YAAI,UAAU;AACd,YAAI;AACF,gBAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,oBAAU,UAAU,4DAA4D,QAAQ;AAAA,QAC1F,QAAQ;AACN,oBAAU;AAAA,QACZ;AACA,eAAO,EAAE,IAAI,OAAO,QAAQ;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,uBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|