@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4397.1.9a65481757
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +269 -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 +442 -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,68 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import {
|
|
3
|
+
runCrudMutationGuardAfterSuccess,
|
|
4
|
+
validateCrudMutationGuard,
|
|
5
|
+
type CrudMutationGuardValidationResult,
|
|
6
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
7
|
+
|
|
8
|
+
type RouteAuth = {
|
|
9
|
+
sub?: string | null
|
|
10
|
+
tenantId?: string | null
|
|
11
|
+
orgId?: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RouteMutationGuardInput = {
|
|
15
|
+
resourceKind: string
|
|
16
|
+
resourceId: string
|
|
17
|
+
operation?: 'create' | 'update' | 'delete' | 'custom'
|
|
18
|
+
mutationPayload?: Record<string, unknown> | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type RouteMutationGuardContext = {
|
|
22
|
+
result: CrudMutationGuardValidationResult | null
|
|
23
|
+
afterSuccess: () => Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validateRouteMutationGuard(params: {
|
|
27
|
+
container: AwilixContainer
|
|
28
|
+
req: Request
|
|
29
|
+
auth: RouteAuth
|
|
30
|
+
input: RouteMutationGuardInput
|
|
31
|
+
}): Promise<RouteMutationGuardContext | { response: Response }> {
|
|
32
|
+
const { auth, container, input, req } = params
|
|
33
|
+
if (!auth.sub || !auth.tenantId) {
|
|
34
|
+
return { result: null, afterSuccess: async () => undefined }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const base = {
|
|
38
|
+
tenantId: auth.tenantId,
|
|
39
|
+
organizationId: auth.orgId ?? null,
|
|
40
|
+
userId: auth.sub,
|
|
41
|
+
resourceKind: input.resourceKind,
|
|
42
|
+
resourceId: input.resourceId,
|
|
43
|
+
operation: input.operation ?? 'custom',
|
|
44
|
+
requestMethod: req.method,
|
|
45
|
+
requestHeaders: req.headers,
|
|
46
|
+
} as const
|
|
47
|
+
|
|
48
|
+
const result = await validateCrudMutationGuard(container, {
|
|
49
|
+
...base,
|
|
50
|
+
mutationPayload: input.mutationPayload ?? null,
|
|
51
|
+
})
|
|
52
|
+
if (result && !result.ok) {
|
|
53
|
+
return {
|
|
54
|
+
response: Response.json(result.body, { status: result.status }),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
result,
|
|
60
|
+
afterSuccess: async () => {
|
|
61
|
+
if (!result?.ok || !result.shouldRunAfterSuccess) return
|
|
62
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
63
|
+
...base,
|
|
64
|
+
metadata: result.metadata ?? null,
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTML sanitizer for channel-supplied payloads (email MIME, Slack rich-text, future channel types).
|
|
5
|
+
*
|
|
6
|
+
* The hub owns this helper; every widget that renders channel HTML — in the hub itself or in
|
|
7
|
+
* downstream provider packages — imports it. The Messages module's `channel-payload-renderer`
|
|
8
|
+
* widget calls this before any `dangerouslySetInnerHTML`-style render.
|
|
9
|
+
*
|
|
10
|
+
* Implementation note: backed by `sanitize-html` (CommonJS, server-friendly) rather than
|
|
11
|
+
* DOMPurify so the same code runs in Jest without ESM transform gymnastics. SPEC-045d §4.6
|
|
12
|
+
* specifies "DOMPurify or equivalent" — `sanitize-html` is an allowlist-based equivalent
|
|
13
|
+
* widely used for the same sanitization shape.
|
|
14
|
+
*
|
|
15
|
+
* Allowlist is tuned for email + chat HTML:
|
|
16
|
+
* - Preserves typical email layout primitives (table-based layouts, inline images, basic typography).
|
|
17
|
+
* - Strips `<script>`, `<style>`, `<iframe>`, all event-handler attributes (`on*`).
|
|
18
|
+
* - Blocks `javascript:` and `data:` URLs except `data:image/*;base64,…` (inline base64 images, common in email).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const ALLOWED_TAGS = [
|
|
22
|
+
'a',
|
|
23
|
+
'img',
|
|
24
|
+
'table',
|
|
25
|
+
'thead',
|
|
26
|
+
'tbody',
|
|
27
|
+
'tfoot',
|
|
28
|
+
'tr',
|
|
29
|
+
'td',
|
|
30
|
+
'th',
|
|
31
|
+
'p',
|
|
32
|
+
'br',
|
|
33
|
+
'hr',
|
|
34
|
+
'ul',
|
|
35
|
+
'ol',
|
|
36
|
+
'li',
|
|
37
|
+
'h1',
|
|
38
|
+
'h2',
|
|
39
|
+
'h3',
|
|
40
|
+
'h4',
|
|
41
|
+
'h5',
|
|
42
|
+
'h6',
|
|
43
|
+
'strong',
|
|
44
|
+
'em',
|
|
45
|
+
'b',
|
|
46
|
+
'i',
|
|
47
|
+
'u',
|
|
48
|
+
's',
|
|
49
|
+
'blockquote',
|
|
50
|
+
'code',
|
|
51
|
+
'pre',
|
|
52
|
+
'span',
|
|
53
|
+
'div',
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const ALLOWED_SCHEMES = ['http', 'https', 'mailto', 'tel']
|
|
57
|
+
const ALLOWED_SCHEMES_BY_TAG = {
|
|
58
|
+
img: ['http', 'https', 'data'],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Safe CSS color value shapes only: hex (#rgb / #rgba / #rrggbb / #rrggbbaa),
|
|
63
|
+
* rgb()/rgba(), hsl()/hsla(), and bare named colors (`red`, `transparent`,
|
|
64
|
+
* `currentcolor`, …). Deliberately rejects any value containing `url(` or
|
|
65
|
+
* `expression(` so a `color`/`background-color` declaration can never smuggle a
|
|
66
|
+
* CSS-based tracking beacon or legacy-IE script expression past the sanitizer.
|
|
67
|
+
*/
|
|
68
|
+
const SAFE_CSS_COLOR =
|
|
69
|
+
/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\(\s*[\d.,\s%]+\)$|^hsla?\(\s*[\d.,\s%]+\)$|^[a-z]+$/i
|
|
70
|
+
|
|
71
|
+
const SANITIZE_OPTIONS: sanitizeHtml.IOptions = {
|
|
72
|
+
allowedTags: ALLOWED_TAGS,
|
|
73
|
+
allowedAttributes: {
|
|
74
|
+
a: ['href', 'title', 'class'],
|
|
75
|
+
img: ['src', 'alt', 'title', 'class', 'width', 'height'],
|
|
76
|
+
'*': ['class', 'style', 'colspan', 'rowspan', 'width', 'height'],
|
|
77
|
+
},
|
|
78
|
+
allowedSchemes: ALLOWED_SCHEMES,
|
|
79
|
+
allowedSchemesByTag: ALLOWED_SCHEMES_BY_TAG,
|
|
80
|
+
allowedSchemesAppliedToAttributes: ['href', 'src'],
|
|
81
|
+
allowProtocolRelative: false,
|
|
82
|
+
// disable inline scripts in style attributes
|
|
83
|
+
allowedStyles: {
|
|
84
|
+
'*': {
|
|
85
|
+
'background-color': [SAFE_CSS_COLOR],
|
|
86
|
+
color: [SAFE_CSS_COLOR],
|
|
87
|
+
'text-align': [/^left$|^right$|^center$|^justify$/i],
|
|
88
|
+
'font-size': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
89
|
+
'font-weight': [/^\d{3}$|^bold$|^normal$/],
|
|
90
|
+
'font-style': [/^italic$|^normal$/],
|
|
91
|
+
'text-decoration': [/^underline$|^line-through$|^none$/],
|
|
92
|
+
padding: [/^[\d\s.]+(?:px|em|rem|%)?$/],
|
|
93
|
+
margin: [/^[\d\s.]+(?:px|em|rem|%)?$/],
|
|
94
|
+
border: [/^[\d\s\w.]+$/],
|
|
95
|
+
'border-radius': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
96
|
+
// Permit only `display:none` so the hidden thread-token footer span
|
|
97
|
+
// (`buildBodyFooter`) stays hidden when a sent email body is re-rendered.
|
|
98
|
+
// Every other display value is stripped.
|
|
99
|
+
display: [/^none$/],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* data:image/<mime>;base64,... URLs in <img src=...> are explicitly allowed so inline
|
|
104
|
+
* email images render. sanitize-html validates the base64 payload shape; if a future
|
|
105
|
+
* payload tries to smuggle a non-image MIME type via data URL we strip it.
|
|
106
|
+
*/
|
|
107
|
+
exclusiveFilter: (frame) => {
|
|
108
|
+
if (frame.tag === 'img' && frame.attribs?.src) {
|
|
109
|
+
const src = frame.attribs.src
|
|
110
|
+
if (src.startsWith('data:') && !/^data:image\/(?:png|jpe?g|gif|webp);base64,/i.test(src)) {
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sanitize an HTML string for safe rendering.
|
|
120
|
+
*
|
|
121
|
+
* @param html Raw HTML — may originate from an external channel (email body, Slack mrkdwn rendered to HTML, …).
|
|
122
|
+
* @returns Sanitized HTML safe for `dangerouslySetInnerHTML`.
|
|
123
|
+
*/
|
|
124
|
+
export function sanitizeChannelHtml(html: string): string {
|
|
125
|
+
if (!html) return ''
|
|
126
|
+
return sanitizeHtml(html, SANITIZE_OPTIONS)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default sanitizeChannelHtml
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
4
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import {
|
|
6
|
+
ChannelThreadMapping,
|
|
7
|
+
CommunicationChannel,
|
|
8
|
+
ExternalConversation,
|
|
9
|
+
MessageChannelLink,
|
|
10
|
+
} from '../data/entities'
|
|
11
|
+
import { ChannelMutationBlockedError, guardOutboundCreate } from './mutation-guards'
|
|
12
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from './queue'
|
|
13
|
+
import type { OutboundDeliveryPayload } from '../workers/outbound-delivery'
|
|
14
|
+
import { htmlToText } from './email-mime'
|
|
15
|
+
|
|
16
|
+
export type SendAsUserActor = {
|
|
17
|
+
userId: string
|
|
18
|
+
tenantId: string
|
|
19
|
+
organizationId: string | null
|
|
20
|
+
/** Forwarded as the messages compose command `ctx.auth`. */
|
|
21
|
+
auth?: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SendAsUserInput = {
|
|
25
|
+
userChannelId: string
|
|
26
|
+
to: string[]
|
|
27
|
+
cc?: string[]
|
|
28
|
+
bcc?: string[]
|
|
29
|
+
subject: string
|
|
30
|
+
body: { plain?: string; html?: string }
|
|
31
|
+
inReplyTo?: string
|
|
32
|
+
references?: string[]
|
|
33
|
+
/**
|
|
34
|
+
* Open Mercato `messages.message` id of the message being replied to. When
|
|
35
|
+
* set, the composed message joins that message's thread (the messages module
|
|
36
|
+
* derives `threadId` from the parent), so a CRM reply continues the existing
|
|
37
|
+
* conversation instead of starting a new thread. Optional — omitted for new
|
|
38
|
+
* threads.
|
|
39
|
+
*/
|
|
40
|
+
parentMessageId?: string
|
|
41
|
+
/**
|
|
42
|
+
* Free-form metadata persisted on the resulting MessageChannelLink. Used by
|
|
43
|
+
* downstream subscribers (e.g. the customers module's link-channel-message
|
|
44
|
+
* subscriber) to anchor the sent message back to a CRM Person or honor a
|
|
45
|
+
* caller-specified visibility flag. The hub does not interpret these keys.
|
|
46
|
+
*/
|
|
47
|
+
channelMetadata?: Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SendAsUserResult =
|
|
51
|
+
| {
|
|
52
|
+
ok: true
|
|
53
|
+
messageId: string
|
|
54
|
+
threadId: string
|
|
55
|
+
channelId: string
|
|
56
|
+
providerKey: string
|
|
57
|
+
}
|
|
58
|
+
| { ok: false; status: number; error: string; fieldErrors?: Record<string, string> }
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* In-process send-as-user facade.
|
|
62
|
+
*
|
|
63
|
+
* Validates the actor owns `userChannelId`, composes the Message via the
|
|
64
|
+
* messages module command, persists the outbound MessageChannelLink + thread
|
|
65
|
+
* mapping, and enqueues delivery. Returns a discriminated result instead of an
|
|
66
|
+
* HTTP Response so it can be invoked both by the `/send-as-user` route and by
|
|
67
|
+
* other modules via DI (`container.resolve('communicationChannelsSendAsUser')`)
|
|
68
|
+
* — no HTTP self-call required.
|
|
69
|
+
*/
|
|
70
|
+
export async function sendAsUser(
|
|
71
|
+
container: AppContainer,
|
|
72
|
+
actor: SendAsUserActor,
|
|
73
|
+
input: SendAsUserInput,
|
|
74
|
+
): Promise<SendAsUserResult> {
|
|
75
|
+
if (!input.body.plain && !input.body.html) {
|
|
76
|
+
return { ok: false, status: 422, error: 'Either body.plain or body.html is required' }
|
|
77
|
+
}
|
|
78
|
+
// Re-validate here (not only in the HTTP route's zod schema) because this
|
|
79
|
+
// facade is also reachable via DI; `input.to[0]` below would otherwise become
|
|
80
|
+
// `undefined` for an empty recipient list.
|
|
81
|
+
if (!Array.isArray(input.to) || input.to.length === 0) {
|
|
82
|
+
return { ok: false, status: 422, error: 'At least one recipient is required' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
86
|
+
const { tenantId } = actor
|
|
87
|
+
const organizationId = actor.organizationId
|
|
88
|
+
const dscope = { tenantId, organizationId }
|
|
89
|
+
|
|
90
|
+
const channel = await findOneWithDecryption(
|
|
91
|
+
em,
|
|
92
|
+
CommunicationChannel,
|
|
93
|
+
{ id: input.userChannelId, tenantId, organizationId, deletedAt: null },
|
|
94
|
+
undefined,
|
|
95
|
+
dscope,
|
|
96
|
+
)
|
|
97
|
+
if (!channel) return { ok: false, status: 404, error: 'Channel not found' }
|
|
98
|
+
if (channel.userId !== actor.userId) {
|
|
99
|
+
return { ok: false, status: 403, error: 'You can only send through channels you own' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Hub mutation guard: map known non-deliverable states (`requires_reauth`,
|
|
103
|
+
// `disconnected`) to a 422 with field-level errors; other transitional states
|
|
104
|
+
// fall through to the 409 below.
|
|
105
|
+
try {
|
|
106
|
+
await guardOutboundCreate(em, { channelId: channel.id, scope: dscope })
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof ChannelMutationBlockedError) {
|
|
109
|
+
return { ok: false, status: 422, error: err.message, fieldErrors: err.errors }
|
|
110
|
+
}
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
if (!channel.isActive || channel.status !== 'connected') {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
status: 409,
|
|
117
|
+
error: `Channel is in status '${channel.status}' (not connected)`,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create the Message via the messages module compose command. The outbound
|
|
122
|
+
// subscriber picks it up via `messages.message.sent` and routes through the
|
|
123
|
+
// adapter chain.
|
|
124
|
+
const commandBus = container.resolve('commandBus') as CommandBus
|
|
125
|
+
const messageBody = input.body.plain ?? htmlToText(input.body.html ?? '')
|
|
126
|
+
const composeInput = {
|
|
127
|
+
type: `channel.${channel.providerKey}`,
|
|
128
|
+
visibility: 'public' as const,
|
|
129
|
+
sourceEntityType: 'communication_channels.send_as_user',
|
|
130
|
+
sourceEntityId: channel.id,
|
|
131
|
+
externalEmail: input.to[0],
|
|
132
|
+
externalName: input.subject,
|
|
133
|
+
recipients: [],
|
|
134
|
+
subject: input.subject,
|
|
135
|
+
body: messageBody,
|
|
136
|
+
bodyFormat: 'text' as const,
|
|
137
|
+
priority: 'normal' as const,
|
|
138
|
+
sendViaEmail: false,
|
|
139
|
+
parentMessageId: input.parentMessageId,
|
|
140
|
+
isDraft: false,
|
|
141
|
+
tenantId,
|
|
142
|
+
organizationId,
|
|
143
|
+
userId: actor.userId,
|
|
144
|
+
}
|
|
145
|
+
let result
|
|
146
|
+
try {
|
|
147
|
+
result = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(
|
|
148
|
+
'messages.messages.compose',
|
|
149
|
+
{
|
|
150
|
+
input: composeInput,
|
|
151
|
+
ctx: {
|
|
152
|
+
container,
|
|
153
|
+
auth: actor.auth as never,
|
|
154
|
+
organizationScope: null,
|
|
155
|
+
selectedOrganizationId: organizationId,
|
|
156
|
+
organizationIds: organizationId ? [organizationId] : null,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('[communication_channels] send-as-user compose failed', err)
|
|
162
|
+
return { ok: false, status: 500, error: '[internal] compose failed' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const messageId = result.result.id
|
|
166
|
+
const messageThreadId = result.result.threadId ?? messageId
|
|
167
|
+
const externalThreadRef = `outbound:${messageThreadId}`
|
|
168
|
+
|
|
169
|
+
// Persist the conversation, thread mapping, and channel link as one unit. The
|
|
170
|
+
// conversation's id is DB-generated (`gen_random_uuid()`), so it must be
|
|
171
|
+
// flushed before the mapping/link can reference it — hence the eager flushes
|
|
172
|
+
// rather than `withAtomicFlush` (single trailing flush). `em.transactional`
|
|
173
|
+
// wraps the whole sequence so a mid-way failure rolls back all three writes
|
|
174
|
+
// instead of leaving an orphaned conversation row.
|
|
175
|
+
await em.transactional(async () => {
|
|
176
|
+
let conversation = await findOneWithDecryption(
|
|
177
|
+
em,
|
|
178
|
+
ExternalConversation,
|
|
179
|
+
{
|
|
180
|
+
channelId: channel.id,
|
|
181
|
+
externalConversationId: externalThreadRef,
|
|
182
|
+
tenantId,
|
|
183
|
+
organizationId,
|
|
184
|
+
},
|
|
185
|
+
undefined,
|
|
186
|
+
dscope,
|
|
187
|
+
)
|
|
188
|
+
if (!conversation) {
|
|
189
|
+
conversation = em.create(ExternalConversation, {
|
|
190
|
+
channelId: channel.id,
|
|
191
|
+
externalConversationId: externalThreadRef,
|
|
192
|
+
subject: input.subject,
|
|
193
|
+
assignedUserId: actor.userId,
|
|
194
|
+
tenantId,
|
|
195
|
+
organizationId,
|
|
196
|
+
lastMessageAt: new Date(),
|
|
197
|
+
})
|
|
198
|
+
em.persist(conversation)
|
|
199
|
+
await em.flush()
|
|
200
|
+
} else {
|
|
201
|
+
conversation.subject = conversation.subject ?? input.subject
|
|
202
|
+
conversation.assignedUserId = conversation.assignedUserId ?? actor.userId
|
|
203
|
+
conversation.lastMessageAt = new Date()
|
|
204
|
+
// Flush the scalar updates BEFORE the ChannelThreadMapping lookup below: a
|
|
205
|
+
// find/findOne on the same EntityManager between a scalar mutation and flush
|
|
206
|
+
// can silently drop the pending UPDATE (core AGENTS.md → Entity Update Safety).
|
|
207
|
+
await em.flush()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const existingMapping = await findOneWithDecryption(
|
|
211
|
+
em,
|
|
212
|
+
ChannelThreadMapping,
|
|
213
|
+
{
|
|
214
|
+
externalConversationId: conversation.id,
|
|
215
|
+
tenantId,
|
|
216
|
+
organizationId,
|
|
217
|
+
},
|
|
218
|
+
undefined,
|
|
219
|
+
dscope,
|
|
220
|
+
)
|
|
221
|
+
if (!existingMapping) {
|
|
222
|
+
const mapping = em.create(ChannelThreadMapping, {
|
|
223
|
+
externalConversationId: conversation.id,
|
|
224
|
+
messageThreadId,
|
|
225
|
+
channelId: channel.id,
|
|
226
|
+
providerKey: channel.providerKey,
|
|
227
|
+
externalThreadRef,
|
|
228
|
+
assignedUserId: actor.userId,
|
|
229
|
+
tenantId,
|
|
230
|
+
organizationId,
|
|
231
|
+
})
|
|
232
|
+
em.persist(mapping)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const channelLink = em.create(MessageChannelLink, {
|
|
236
|
+
messageId,
|
|
237
|
+
externalConversationId: conversation.id,
|
|
238
|
+
providerKey: channel.providerKey,
|
|
239
|
+
channelType: channel.channelType,
|
|
240
|
+
direction: 'outbound',
|
|
241
|
+
deliveryStatus: 'pending',
|
|
242
|
+
channelPayload: {
|
|
243
|
+
text: input.body.plain ?? messageBody,
|
|
244
|
+
...(input.body.html ? { html: input.body.html } : {}),
|
|
245
|
+
},
|
|
246
|
+
channelContentType: input.body.html ? 'text/html' : 'text/plain',
|
|
247
|
+
channelMetadata: {
|
|
248
|
+
// Caller-supplied pass-through metadata merged FIRST so the validated
|
|
249
|
+
// routing fields below always win — a caller cannot override the
|
|
250
|
+
// recipients/subject/threading headers via `channelMetadata`.
|
|
251
|
+
...(input.channelMetadata ?? {}),
|
|
252
|
+
to: input.to,
|
|
253
|
+
cc: input.cc ?? [],
|
|
254
|
+
bcc: input.bcc ?? [],
|
|
255
|
+
subject: input.subject,
|
|
256
|
+
inReplyTo: input.inReplyTo ?? null,
|
|
257
|
+
references: input.references ?? [],
|
|
258
|
+
},
|
|
259
|
+
tenantId,
|
|
260
|
+
organizationId,
|
|
261
|
+
})
|
|
262
|
+
em.persist(channelLink)
|
|
263
|
+
await em.flush()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)
|
|
267
|
+
const deliveryJob: OutboundDeliveryPayload = {
|
|
268
|
+
messageId,
|
|
269
|
+
scope: dscope,
|
|
270
|
+
attempt: 1,
|
|
271
|
+
}
|
|
272
|
+
await queue.enqueue(deliveryJob as unknown as Record<string, unknown>)
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
messageId,
|
|
277
|
+
threadId: messageThreadId,
|
|
278
|
+
channelId: channel.id,
|
|
279
|
+
providerKey: channel.providerKey,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** DI service type for cross-module callers (resolve `communicationChannelsSendAsUser`). */
|
|
284
|
+
export type SendAsUserService = typeof sendAsUser
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sentinel UUID — used as a last-resort `senderUserId` for inbound channel
|
|
5
|
+
* messages when no per-tenant system user is available. Matches the pattern
|
|
6
|
+
* in `inbox_ops/lib/messagesIntegration.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export const COMMUNICATION_CHANNELS_SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configurable email pattern for per-tenant channel-bot users. Implementations
|
|
12
|
+
* (provider packages, onboarding scripts) may create a real `auth.user` row
|
|
13
|
+
* matching this convention so inbound channel messages get a meaningful sender
|
|
14
|
+
* display name in the unified inbox.
|
|
15
|
+
*
|
|
16
|
+
* Format: `system+communication_channels@<tenantId>.local`
|
|
17
|
+
*/
|
|
18
|
+
export function systemUserEmail(tenantId: string): string {
|
|
19
|
+
return `system+communication_channels@${tenantId}.local`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a tenant-scoped system user id to attribute inbound channel messages to.
|
|
24
|
+
*
|
|
25
|
+
* Lookup order:
|
|
26
|
+
* 1. Per-tenant channel-bot user (by convention email — see `systemUserEmail`).
|
|
27
|
+
* 2. (Optional, caller-supplied) seed fallback — a specific override id.
|
|
28
|
+
* 3. Sentinel UUID (`00000000-...`) — backward-compatible default.
|
|
29
|
+
*
|
|
30
|
+
* The function is fail-soft: when the lookup throws, it falls back to the
|
|
31
|
+
* sentinel. The inbound-processor must never refuse to ingest a message
|
|
32
|
+
* because the channel-bot user doesn't exist.
|
|
33
|
+
*
|
|
34
|
+
* @param em EntityManager scoped to the tenant.
|
|
35
|
+
* @param tenantId Tenant id for which to resolve the system user.
|
|
36
|
+
* @param fallbackId Optional caller-supplied fallback (e.g., the channel's
|
|
37
|
+
* assigned user) used when the channel-bot lookup misses.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveCommunicationChannelsSystemUserId(
|
|
40
|
+
em: EntityManager,
|
|
41
|
+
tenantId: string,
|
|
42
|
+
fallbackId?: string | null,
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
try {
|
|
45
|
+
const expectedEmail = systemUserEmail(tenantId)
|
|
46
|
+
// Untyped QB by design — the helper is intentionally cross-module
|
|
47
|
+
// (resolving an `auth.user` from the hub) and must not pull the User
|
|
48
|
+
// entity class. MikroORM v7's typed builder requires an entity ref;
|
|
49
|
+
// we keep the lookup table-name-driven so the helper compiles without
|
|
50
|
+
// a cross-module import. The mocks in `__tests__/system-user.test.ts`
|
|
51
|
+
// exercise this code path through a duck-typed `createQueryBuilder` stub.
|
|
52
|
+
type RawQueryBuilder = {
|
|
53
|
+
select: (fields: string[]) => RawQueryBuilder
|
|
54
|
+
where: (cond: Record<string, unknown>) => RawQueryBuilder
|
|
55
|
+
limit: (count: number) => RawQueryBuilder
|
|
56
|
+
execute: (mode: string) => Promise<unknown>
|
|
57
|
+
}
|
|
58
|
+
const qb = (
|
|
59
|
+
em as unknown as { createQueryBuilder: (table: string, alias: string) => RawQueryBuilder }
|
|
60
|
+
).createQueryBuilder('auth.users', 'u')
|
|
61
|
+
const row = await qb
|
|
62
|
+
.select(['u.id'])
|
|
63
|
+
.where({ email: expectedEmail, tenant_id: tenantId })
|
|
64
|
+
.limit(1)
|
|
65
|
+
.execute('get')
|
|
66
|
+
.catch(() => null)
|
|
67
|
+
const id = (row as { id?: string } | null)?.id
|
|
68
|
+
if (typeof id === 'string' && id.length > 0) return id
|
|
69
|
+
} catch {
|
|
70
|
+
// ignore — fall through to fallback
|
|
71
|
+
}
|
|
72
|
+
if (typeof fallbackId === 'string' && fallbackId.length > 0) return fallbackId
|
|
73
|
+
return COMMUNICATION_CHANNELS_SYSTEM_USER_ID
|
|
74
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelAdapter,
|
|
3
|
+
ChannelCapabilities,
|
|
4
|
+
ChannelNativeContent,
|
|
5
|
+
ConvertOutboundInput,
|
|
6
|
+
GetMessageStatusInput,
|
|
7
|
+
InboundMessage,
|
|
8
|
+
MessageStatus,
|
|
9
|
+
NormalizedInboundMessage,
|
|
10
|
+
SendMessageInput,
|
|
11
|
+
SendMessageResult,
|
|
12
|
+
ValidateCredentialsInput,
|
|
13
|
+
ValidateCredentialsResult,
|
|
14
|
+
VerifyWebhookInput,
|
|
15
|
+
} from './adapter'
|
|
16
|
+
import { baseEmailCapabilities } from './email-capabilities'
|
|
17
|
+
import { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Test-only channel seeding support.
|
|
21
|
+
*
|
|
22
|
+
* The ephemeral integration harness cannot connect a REAL email channel:
|
|
23
|
+
* - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server
|
|
24
|
+
* (none exists in CI), so `POST /channels/connect/credentials` returns 422.
|
|
25
|
+
* - Even with a connected channel, the outbound delivery worker calls the real
|
|
26
|
+
* SMTP adapter, which fails with no server — so `communication_channels.message.sent`
|
|
27
|
+
* never fires and the customers link subscriber never runs.
|
|
28
|
+
*
|
|
29
|
+
* To make the compose → deliver → `.sent` → CRM-link → cross-user-visibility chain
|
|
30
|
+
* (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable
|
|
31
|
+
* end-to-end against real Postgres, this module provides a network-free stub adapter
|
|
32
|
+
* that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.
|
|
33
|
+
*
|
|
34
|
+
* Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};
|
|
35
|
+
* when the env flag is unset (the production default) the adapter is never registered
|
|
36
|
+
* and the `__test_seed__` provider key resolves to no adapter — so the connect route
|
|
37
|
+
* returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated
|
|
38
|
+
* test-seed API route enforces the same gate independently (fail-closed 404 in prod).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** Provider key for the network-free test stub adapter. */
|
|
42
|
+
export const TEST_SEED_PROVIDER_KEY = '__test_seed__'
|
|
43
|
+
|
|
44
|
+
/** Env flag that unlocks test-only channel seeding. Off in production. */
|
|
45
|
+
export const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True only when the test-seeding env flag is explicitly enabled. Accepts the
|
|
49
|
+
* usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a
|
|
50
|
+
* plain `=true`. Any other value (including unset) is treated as disabled.
|
|
51
|
+
*/
|
|
52
|
+
export function isTestChannelSeedingEnabled(): boolean {
|
|
53
|
+
const raw = process.env[TEST_CHANNEL_SEEDING_ENV]
|
|
54
|
+
if (typeof raw !== 'string') return false
|
|
55
|
+
return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Capabilities for the stub: an email channel that supports neither reactions,
|
|
60
|
+
* edit/delete, nor conversation history — so the strict registry validator
|
|
61
|
+
* (`validateAdapterCapabilities`) requires only the core method surface.
|
|
62
|
+
*/
|
|
63
|
+
const testSeedCapabilities: ChannelCapabilities = {
|
|
64
|
+
...baseEmailCapabilities,
|
|
65
|
+
conversationHistory: false,
|
|
66
|
+
realtimePush: false,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any
|
|
71
|
+
* network I/O. Used exclusively by the integration harness to let the outbound
|
|
72
|
+
* delivery worker reach its success path and emit `communication_channels.message.sent`.
|
|
73
|
+
*/
|
|
74
|
+
class TestSeedChannelAdapter implements ChannelAdapter {
|
|
75
|
+
readonly providerKey = TEST_SEED_PROVIDER_KEY
|
|
76
|
+
readonly channelType = 'email'
|
|
77
|
+
readonly capabilities = testSeedCapabilities
|
|
78
|
+
|
|
79
|
+
async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {
|
|
80
|
+
// Synthesize a deterministic-looking RFC2822-style message id; never touches
|
|
81
|
+
// the network. The delivery worker persists this as the external message id.
|
|
82
|
+
const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`
|
|
83
|
+
return {
|
|
84
|
+
externalMessageId,
|
|
85
|
+
conversationId: input.conversationId,
|
|
86
|
+
status: 'sent',
|
|
87
|
+
metadata: { testSeed: true },
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {
|
|
92
|
+
// No real webhook — return the inert event so the generic webhook route 202s
|
|
93
|
+
// without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).
|
|
94
|
+
return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {
|
|
98
|
+
return { status: 'sent' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {
|
|
102
|
+
return {
|
|
103
|
+
content: {
|
|
104
|
+
text: input.body,
|
|
105
|
+
bodyFormat: input.bodyFormat,
|
|
106
|
+
},
|
|
107
|
+
metadata: input.channelMetadata ?? {},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {
|
|
112
|
+
// The test-seed inbound path seeds MessageChannelLink rows directly and emits
|
|
113
|
+
// the hub event, so this adapter never normalizes a raw inbound payload.
|
|
114
|
+
throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {
|
|
118
|
+
// No real server to authenticate against — accept any credentials so the
|
|
119
|
+
// connect command persists a connected channel.
|
|
120
|
+
return { ok: true }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let cachedTestSeedAdapter: TestSeedChannelAdapter | null = null
|
|
125
|
+
|
|
126
|
+
function getTestSeedChannelAdapter(): TestSeedChannelAdapter {
|
|
127
|
+
if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()
|
|
128
|
+
return cachedTestSeedAdapter
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Register the test-seed adapter exactly once, but ONLY when the env flag is set.
|
|
133
|
+
* Idempotent and safe to call from every container creation (`di.register`) — a
|
|
134
|
+
* no-op when seeding is disabled or the adapter is already registered.
|
|
135
|
+
*/
|
|
136
|
+
export function ensureTestSeedAdapterRegistered(): void {
|
|
137
|
+
if (!isTestChannelSeedingEnabled()) return
|
|
138
|
+
if (hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) return
|
|
139
|
+
registerChannelAdapter(getTestSeedChannelAdapter())
|
|
140
|
+
}
|