@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,163 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { CommunicationChannel } from '../data/entities'
|
|
4
|
+
import type { ChannelAdapter } from './adapter'
|
|
5
|
+
import { isUniqueViolation } from './pg-errors'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default poll cadence (seconds) for a polling-only channel — one whose adapter
|
|
9
|
+
* declares `realtimePush: false`. Push-capable channels use `null` (push-driven,
|
|
10
|
+
* no fixed poll). Shared so push teardown restores the same cadence connect uses.
|
|
11
|
+
*/
|
|
12
|
+
export const POLLING_ONLY_DEFAULT_INTERVAL_SECONDS = 300
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Thrown by {@link createConnectedChannelRow} when the same mailbox
|
|
16
|
+
* (`externalIdentifier`) is already connected for this user via a DIFFERENT
|
|
17
|
+
* provider. Both channels would poll the same inbox, and the per-channel
|
|
18
|
+
* `(channel_id, external_message_id)` dedup cannot dedupe the same email across
|
|
19
|
+
* channels — so every message would be ingested (and threaded) twice.
|
|
20
|
+
*/
|
|
21
|
+
export class MailboxAlreadyConnectedError extends Error {
|
|
22
|
+
readonly externalIdentifier: string
|
|
23
|
+
readonly existingProviderKey: string
|
|
24
|
+
constructor(externalIdentifier: string, existingProviderKey: string) {
|
|
25
|
+
super(`Mailbox ${externalIdentifier} is already connected via ${existingProviderKey}`)
|
|
26
|
+
this.name = 'MailboxAlreadyConnectedError'
|
|
27
|
+
this.externalIdentifier = externalIdentifier
|
|
28
|
+
this.existingProviderKey = existingProviderKey
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CreateConnectedChannelRowArgs {
|
|
33
|
+
em: EntityManager
|
|
34
|
+
adapter: Pick<ChannelAdapter, 'channelType' | 'capabilities'>
|
|
35
|
+
providerKey: string
|
|
36
|
+
displayName: string
|
|
37
|
+
externalIdentifier: string | null
|
|
38
|
+
credentialsRefId: string | null
|
|
39
|
+
userId: string
|
|
40
|
+
scope: { tenantId: string; organizationId: string | null }
|
|
41
|
+
/**
|
|
42
|
+
* Explicit poll-interval override (seconds). When omitted, it is derived from
|
|
43
|
+
* the adapter's push capability (push-capable → null, polling-only → 300).
|
|
44
|
+
*/
|
|
45
|
+
pollIntervalSeconds?: number | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create + persist the per-user `CommunicationChannel` row for a connect flow.
|
|
50
|
+
* Shared by the credential-connect command and the OAuth callback so both entry
|
|
51
|
+
* points use one channel-shape implementation instead of duplicating `em.create`.
|
|
52
|
+
*
|
|
53
|
+
* When credentials could not be persisted (`credentialsRefId === null`) the row
|
|
54
|
+
* is created in `requires_reauth` + `isActive=false` so workers don't poll a
|
|
55
|
+
* credential-less channel; the user reconnects to recover.
|
|
56
|
+
*/
|
|
57
|
+
export async function createConnectedChannelRow(
|
|
58
|
+
args: CreateConnectedChannelRowArgs,
|
|
59
|
+
): Promise<CommunicationChannel> {
|
|
60
|
+
const { em, adapter, providerKey, displayName, externalIdentifier, credentialsRefId, userId, scope } = args
|
|
61
|
+
const credentialsAvailable = credentialsRefId !== null
|
|
62
|
+
const pollIntervalSeconds =
|
|
63
|
+
args.pollIntervalSeconds !== undefined
|
|
64
|
+
? args.pollIntervalSeconds
|
|
65
|
+
: adapter.capabilities?.realtimePush === false
|
|
66
|
+
? POLLING_ONLY_DEFAULT_INTERVAL_SECONDS
|
|
67
|
+
: null
|
|
68
|
+
const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }
|
|
69
|
+
|
|
70
|
+
// Cross-provider duplicate guard: the same mailbox must not be connected via
|
|
71
|
+
// two providers for one user. Both channels would poll the same inbox, and the
|
|
72
|
+
// per-channel `(channel_id, external_message_id)` dedup cannot dedupe the same
|
|
73
|
+
// email across channels — so every message would be ingested (and threaded)
|
|
74
|
+
// twice. Reconnecting the SAME provider/mailbox is fine (healed below); this
|
|
75
|
+
// only blocks a DIFFERENT provider for an already-connected address.
|
|
76
|
+
if (externalIdentifier) {
|
|
77
|
+
const normalized = externalIdentifier.toLowerCase()
|
|
78
|
+
const userChannels = (await findWithDecryption(
|
|
79
|
+
em,
|
|
80
|
+
CommunicationChannel,
|
|
81
|
+
{ tenantId: scope.tenantId, userId, deletedAt: null },
|
|
82
|
+
undefined,
|
|
83
|
+
dscope,
|
|
84
|
+
)) as CommunicationChannel[]
|
|
85
|
+
const conflict = userChannels.find(
|
|
86
|
+
(existing) =>
|
|
87
|
+
existing.providerKey !== providerKey &&
|
|
88
|
+
typeof existing.externalIdentifier === 'string' &&
|
|
89
|
+
existing.externalIdentifier.toLowerCase() === normalized,
|
|
90
|
+
)
|
|
91
|
+
if (conflict) {
|
|
92
|
+
throw new MailboxAlreadyConnectedError(externalIdentifier, conflict.providerKey)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const naturalKey = {
|
|
97
|
+
tenantId: scope.tenantId,
|
|
98
|
+
userId,
|
|
99
|
+
providerKey,
|
|
100
|
+
externalIdentifier,
|
|
101
|
+
deletedAt: null,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Heal-on-reconnect: a channel for the same (tenant, user, provider, mailbox)
|
|
105
|
+
// already exists when the user re-runs OAuth / reconnects after a
|
|
106
|
+
// `requires_reauth`. Update it in place rather than inserting a duplicate row —
|
|
107
|
+
// a duplicate would stay `isActive` and keep polling + re-emitting reauth
|
|
108
|
+
// banners, and register a second competing push subscription. Only mailboxes
|
|
109
|
+
// with a known `externalIdentifier` participate (the unique index is partial).
|
|
110
|
+
const applyConnectionState = (target: CommunicationChannel): void => {
|
|
111
|
+
target.channelType = adapter.channelType
|
|
112
|
+
target.displayName = displayName
|
|
113
|
+
target.externalIdentifier = externalIdentifier ?? null
|
|
114
|
+
target.credentialsRef = credentialsRefId
|
|
115
|
+
target.capabilities = adapter.capabilities as unknown as Record<string, unknown>
|
|
116
|
+
target.isActive = credentialsAvailable
|
|
117
|
+
target.pollIntervalSeconds = pollIntervalSeconds
|
|
118
|
+
target.status = credentialsAvailable ? 'connected' : 'requires_reauth'
|
|
119
|
+
target.lastError = credentialsAvailable ? null : 'credentials_persist_failed'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (externalIdentifier) {
|
|
123
|
+
const existing = await findOneWithDecryption(em, CommunicationChannel, naturalKey, undefined, dscope)
|
|
124
|
+
if (existing) {
|
|
125
|
+
applyConnectionState(existing)
|
|
126
|
+
await em.flush()
|
|
127
|
+
return existing
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const channel = em.create(CommunicationChannel, {
|
|
132
|
+
providerKey,
|
|
133
|
+
channelType: adapter.channelType,
|
|
134
|
+
displayName,
|
|
135
|
+
externalIdentifier: externalIdentifier ?? null,
|
|
136
|
+
credentialsRef: credentialsRefId,
|
|
137
|
+
capabilities: adapter.capabilities as unknown as Record<string, unknown>,
|
|
138
|
+
isActive: credentialsAvailable,
|
|
139
|
+
userId,
|
|
140
|
+
isPrimary: false,
|
|
141
|
+
pollIntervalSeconds,
|
|
142
|
+
status: credentialsAvailable ? 'connected' : 'requires_reauth',
|
|
143
|
+
lastError: credentialsAvailable ? null : 'credentials_persist_failed',
|
|
144
|
+
tenantId: scope.tenantId,
|
|
145
|
+
organizationId: scope.organizationId ?? null,
|
|
146
|
+
})
|
|
147
|
+
em.persist(channel)
|
|
148
|
+
try {
|
|
149
|
+
await em.flush()
|
|
150
|
+
return channel
|
|
151
|
+
} catch (err) {
|
|
152
|
+
// Concurrent connect for the same mailbox won the race (partial unique index
|
|
153
|
+
// rejected ours). Re-select the winner on a clean fork and heal it so the
|
|
154
|
+
// caller still gets a single, connected channel.
|
|
155
|
+
if (!isUniqueViolation(err) || !externalIdentifier) throw err
|
|
156
|
+
const reEm = em.fork()
|
|
157
|
+
const winner = await findOneWithDecryption(reEm, CommunicationChannel, naturalKey, undefined, dscope)
|
|
158
|
+
if (!winner) throw err
|
|
159
|
+
applyConnectionState(winner)
|
|
160
|
+
await reEm.flush()
|
|
161
|
+
return winner
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
|
|
3
|
+
import type { ChannelAdapter, ContactHint, TenantScope } from './adapter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contact resolver — used by the inbound bridge to attach an external sender
|
|
7
|
+
* to a CRM person.
|
|
8
|
+
*
|
|
9
|
+
* Flow (SPEC-045d §8.1):
|
|
10
|
+
* 1. If the adapter implements `resolveContact?(...)`, call it to get a
|
|
11
|
+
* provider-side ContactHint (display name, photo URL, possibly an email
|
|
12
|
+
* lookup the adapter performed against its own user directory).
|
|
13
|
+
* 2. If the hint includes an email or phone, query the CRM (`customers:customer_entity`)
|
|
14
|
+
* via the **QueryEngine** (NOT raw SQL — root AGENTS.md mandate). If a person
|
|
15
|
+
* matches, populate `matchedPersonId` on the returned hint.
|
|
16
|
+
* 3. Return the merged hint, or `null` if neither the adapter nor the CRM
|
|
17
|
+
* yields any identity information.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface ContactResolverInput {
|
|
21
|
+
adapter: ChannelAdapter
|
|
22
|
+
senderIdentifier: string
|
|
23
|
+
senderDisplayName?: string
|
|
24
|
+
channelMetadata?: Record<string, unknown>
|
|
25
|
+
credentials: Record<string, unknown>
|
|
26
|
+
scope: TenantScope
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResolveContactDeps {
|
|
30
|
+
container: { resolve: <T = unknown>(name: string) => T }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type QueryEngineLike = {
|
|
34
|
+
query: (
|
|
35
|
+
entityType: string,
|
|
36
|
+
options: {
|
|
37
|
+
tenantId: string
|
|
38
|
+
organizationId?: string | null
|
|
39
|
+
filter?: Record<string, unknown>
|
|
40
|
+
limit?: number
|
|
41
|
+
offset?: number
|
|
42
|
+
},
|
|
43
|
+
) => Promise<Record<string, unknown>[]>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a CRM contact from an external sender identifier.
|
|
48
|
+
*
|
|
49
|
+
* Returns `null` when no identity could be derived. Callers MUST treat the
|
|
50
|
+
* return value as advisory (a hint, not an authoritative match) — see spec
|
|
51
|
+
* § 8 "Contact resolution false positive" risk.
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveContact(
|
|
54
|
+
input: ContactResolverInput,
|
|
55
|
+
deps: ResolveContactDeps,
|
|
56
|
+
): Promise<ContactHint | null> {
|
|
57
|
+
// Step 1 — adapter resolution (optional)
|
|
58
|
+
const adapterHint: ContactHint | null = await runAdapterResolve(input)
|
|
59
|
+
if (!adapterHint && !input.senderIdentifier) return null
|
|
60
|
+
|
|
61
|
+
// Step 2 — CRM lookup by email or phone, via QueryEngine (no raw SQL).
|
|
62
|
+
const lookupEmail = adapterHint?.email ?? heuristicEmail(input.senderIdentifier)
|
|
63
|
+
const lookupPhone = adapterHint?.phone ?? heuristicPhone(input.senderIdentifier)
|
|
64
|
+
|
|
65
|
+
let matchedPersonId: string | undefined
|
|
66
|
+
if ((lookupEmail || lookupPhone) && deps.container) {
|
|
67
|
+
matchedPersonId = await lookupCustomerPersonId({
|
|
68
|
+
container: deps.container,
|
|
69
|
+
scope: input.scope,
|
|
70
|
+
email: lookupEmail,
|
|
71
|
+
phone: lookupPhone,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...(adapterHint ?? {}),
|
|
77
|
+
email: lookupEmail,
|
|
78
|
+
phone: lookupPhone,
|
|
79
|
+
displayName: adapterHint?.displayName ?? input.senderDisplayName,
|
|
80
|
+
matchedPersonId: matchedPersonId ?? adapterHint?.matchedPersonId,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runAdapterResolve(input: ContactResolverInput): Promise<ContactHint | null> {
|
|
85
|
+
if (typeof input.adapter.resolveContact !== 'function') return null
|
|
86
|
+
try {
|
|
87
|
+
return (
|
|
88
|
+
(await input.adapter.resolveContact({
|
|
89
|
+
senderIdentifier: input.senderIdentifier,
|
|
90
|
+
senderDisplayName: input.senderDisplayName,
|
|
91
|
+
channelMetadata: input.channelMetadata,
|
|
92
|
+
credentials: input.credentials,
|
|
93
|
+
scope: input.scope,
|
|
94
|
+
})) ?? null
|
|
95
|
+
)
|
|
96
|
+
} catch {
|
|
97
|
+
// The adapter is best-effort. A failure does not block ingest; we just lose
|
|
98
|
+
// the optional CRM match. Errors are not propagated.
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function lookupCustomerPersonId(params: {
|
|
104
|
+
container: ResolveContactDeps['container']
|
|
105
|
+
scope: TenantScope
|
|
106
|
+
email?: string
|
|
107
|
+
phone?: string
|
|
108
|
+
}): Promise<string | undefined> {
|
|
109
|
+
// Under tenant encryption, `primary_email`/`primary_phone` are stored as
|
|
110
|
+
// ciphertext, so a plaintext equality filter on the base column both hits the
|
|
111
|
+
// §16 "no querying an encrypted column by value" footgun and never matches.
|
|
112
|
+
// Skip the fast lookup in that case (it would only ever return nothing) — the
|
|
113
|
+
// authoritative CRM link is created by the customers `link-channel-message`
|
|
114
|
+
// subscriber, which does an in-memory decrypted comparison. A blind-index
|
|
115
|
+
// column is the proper fast-path fix here (same follow-up as
|
|
116
|
+
// `customers/lib/findPeopleByAddresses`).
|
|
117
|
+
if (isTenantDataEncryptionEnabled()) return undefined
|
|
118
|
+
|
|
119
|
+
let queryEngine: QueryEngineLike | null = null
|
|
120
|
+
try {
|
|
121
|
+
queryEngine = params.container.resolve<QueryEngineLike>('queryEngine')
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
if (!queryEngine || typeof queryEngine.query !== 'function') return undefined
|
|
126
|
+
|
|
127
|
+
const filter = buildPersonLookupFilter(params.email, params.phone)
|
|
128
|
+
if (!filter) return undefined
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const rows = await queryEngine.query('customers:customer_entity', {
|
|
132
|
+
tenantId: params.scope.tenantId,
|
|
133
|
+
organizationId: params.scope.organizationId,
|
|
134
|
+
filter,
|
|
135
|
+
limit: 1,
|
|
136
|
+
})
|
|
137
|
+
const first = rows?.[0]
|
|
138
|
+
const id = first && typeof first.id === 'string' ? (first.id as string) : undefined
|
|
139
|
+
return id
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildPersonLookupFilter(email?: string, phone?: string): Record<string, unknown> | null {
|
|
146
|
+
if (email) return { primary_email: email, kind: 'person' }
|
|
147
|
+
if (phone) return { primary_phone: phone, kind: 'person' }
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function heuristicEmail(senderIdentifier: string): string | undefined {
|
|
152
|
+
if (!senderIdentifier.includes('@')) return undefined
|
|
153
|
+
// Don't second-guess provider-supplied data; just check it's email-shaped.
|
|
154
|
+
return senderIdentifier.includes('.') ? senderIdentifier : undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function heuristicPhone(senderIdentifier: string): string | undefined {
|
|
158
|
+
// Plain heuristic: +CC followed by 6+ digits. Real phone validation is the
|
|
159
|
+
// adapter's job (the email integration spec elaborates IMAP paths).
|
|
160
|
+
if (/^\+\d{6,}$/.test(senderIdentifier)) return senderIdentifier
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelAdapter,
|
|
3
|
+
OAuthClientConfig,
|
|
4
|
+
RefreshedCredentials,
|
|
5
|
+
TenantScope,
|
|
6
|
+
} from './adapter'
|
|
7
|
+
import { resolveOAuthClientCredentials } from './oauth-client-config'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Optional credentials-service shape — matches the integrations module's
|
|
11
|
+
* `CredentialsService`. We keep this loose so the helper compiles even when
|
|
12
|
+
* the integrations module is disabled in a downstream app (the helper just
|
|
13
|
+
* skips persistence in that case).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Matches the real `CredentialsService.save(integrationId, credentials, scope)`
|
|
17
|
+
* signature from `packages/core/src/modules/integrations/lib/credentials-service.ts`.
|
|
18
|
+
* The legacy call sites had this argument order inverted; that bug was the root
|
|
19
|
+
* cause of C1 in the 2026-05-26 review.
|
|
20
|
+
*
|
|
21
|
+
* `scope.userId` (added 2026-05-26 for per-user channels) lets the credentials
|
|
22
|
+
* service write to a user-scoped row instead of overwriting the tenant-wide row.
|
|
23
|
+
*/
|
|
24
|
+
type CredentialsScope = TenantScope & { userId?: string | null }
|
|
25
|
+
type CredentialsServiceLike = {
|
|
26
|
+
resolve: (integrationId: string, scope: CredentialsScope) => Promise<Record<string, unknown> | null>
|
|
27
|
+
save?: (integrationId: string, credentials: Record<string, unknown>, scope: CredentialsScope) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RefreshCredentialsIfNeededInput = {
|
|
31
|
+
adapter: ChannelAdapter
|
|
32
|
+
channelId: string
|
|
33
|
+
/** Current decrypted credential blob. May contain `expiresAt` (Date or ISO string). */
|
|
34
|
+
credentials: Record<string, unknown>
|
|
35
|
+
scope: CredentialsScope
|
|
36
|
+
/** Refresh window — refresh when token expires within this many ms. Defaults to 60s. */
|
|
37
|
+
refreshWindowMs?: number
|
|
38
|
+
/** Force a refresh regardless of expiry — used after a 401 response from the provider. */
|
|
39
|
+
force?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type RefreshCredentialsIfNeededResult = {
|
|
43
|
+
refreshed: boolean
|
|
44
|
+
/** The latest credential blob to use for the outbound call. */
|
|
45
|
+
credentials: Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_REFRESH_WINDOW_MS = 60_000
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* In-process single-flight for credential refresh, keyed by `channelId`. The
|
|
52
|
+
* outbound-delivery worker runs at concurrency 10 in ONE process, so two
|
|
53
|
+
* concurrent sends on the same channel can both pass `shouldRefresh` and both
|
|
54
|
+
* call `adapter.refreshCredentials`. With rotating refresh-token providers
|
|
55
|
+
* (Gmail) the second exchange invalidates the first's token and
|
|
56
|
+
* flaps the channel to `requires_reauth`. Coalescing concurrent refreshes for
|
|
57
|
+
* the same channel onto one in-flight promise prevents that race for the common
|
|
58
|
+
* single-process case. Entries are deleted in `finally` once settled.
|
|
59
|
+
*/
|
|
60
|
+
const inFlightRefreshes = new Map<string, Promise<RefreshCredentialsIfNeededResult>>()
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Refresh OAuth credentials when an access token is near expiry, or when the
|
|
64
|
+
* caller forces it (e.g. after a 401 response).
|
|
65
|
+
*
|
|
66
|
+
* Behaviour:
|
|
67
|
+
* - No-op when the adapter does not implement `refreshCredentials?`.
|
|
68
|
+
* - No-op when the credential blob has no `expiresAt` AND `force !== true`.
|
|
69
|
+
* - Returns the refreshed credentials in-memory; persistence to
|
|
70
|
+
* `integration_credentials` happens via the `CredentialsService` if it
|
|
71
|
+
* is registered AND exposes `save()` (best-effort — failures are logged
|
|
72
|
+
* but don't block the outbound call).
|
|
73
|
+
*/
|
|
74
|
+
export async function refreshCredentialsIfNeeded(
|
|
75
|
+
input: RefreshCredentialsIfNeededInput,
|
|
76
|
+
deps?: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void },
|
|
77
|
+
): Promise<RefreshCredentialsIfNeededResult> {
|
|
78
|
+
const log = deps?.logger ?? (() => {})
|
|
79
|
+
if (typeof input.adapter.refreshCredentials !== 'function') {
|
|
80
|
+
return { refreshed: false, credentials: input.credentials }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const refreshWindow = input.refreshWindowMs ?? DEFAULT_REFRESH_WINDOW_MS
|
|
84
|
+
if (!input.force && !shouldRefresh(input.credentials, refreshWindow)) {
|
|
85
|
+
return { refreshed: false, credentials: input.credentials }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Coalesce concurrent refreshes for the same channel onto one in-flight
|
|
89
|
+
// promise so rotating refresh tokens are not exchanged twice in parallel.
|
|
90
|
+
const existing = inFlightRefreshes.get(input.channelId)
|
|
91
|
+
if (existing) return existing
|
|
92
|
+
|
|
93
|
+
const refreshCredentials = input.adapter.refreshCredentials.bind(input.adapter)
|
|
94
|
+
const refreshPromise = runRefresh(input, refreshCredentials, deps, log).finally(() => {
|
|
95
|
+
inFlightRefreshes.delete(input.channelId)
|
|
96
|
+
})
|
|
97
|
+
inFlightRefreshes.set(input.channelId, refreshPromise)
|
|
98
|
+
return refreshPromise
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runRefresh(
|
|
102
|
+
input: RefreshCredentialsIfNeededInput,
|
|
103
|
+
refreshCredentials: NonNullable<ChannelAdapter['refreshCredentials']>,
|
|
104
|
+
deps: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void } | undefined,
|
|
105
|
+
log: (...args: unknown[]) => void,
|
|
106
|
+
): Promise<RefreshCredentialsIfNeededResult> {
|
|
107
|
+
// Resolve the tenant's OAuth client config (clientId/clientSecret) the admin
|
|
108
|
+
// stored under the `channel_<providerKey>` integration at TENANT scope
|
|
109
|
+
// (userId = null). The adapter uses it for the token-endpoint call. We always
|
|
110
|
+
// resolve at tenant scope here — never the channel's per-user scope — because
|
|
111
|
+
// the per-user row holds the user's tokens, not the client app credentials.
|
|
112
|
+
let oauthClient: OAuthClientConfig | undefined
|
|
113
|
+
if (deps?.credentialsService) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = await resolveOAuthClientCredentials(
|
|
116
|
+
deps.credentialsService,
|
|
117
|
+
input.adapter.providerKey,
|
|
118
|
+
{ tenantId: input.scope.tenantId, organizationId: input.scope.organizationId },
|
|
119
|
+
)
|
|
120
|
+
oauthClient = safeParseOAuthClient(raw)
|
|
121
|
+
} catch (resolveErr) {
|
|
122
|
+
log(
|
|
123
|
+
'[communication_channels] resolving OAuth client config failed:',
|
|
124
|
+
resolveErr instanceof Error ? resolveErr.message : resolveErr,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let result: RefreshedCredentials
|
|
130
|
+
try {
|
|
131
|
+
result = await refreshCredentials({
|
|
132
|
+
channelId: input.channelId,
|
|
133
|
+
credentials: input.credentials,
|
|
134
|
+
scope: input.scope,
|
|
135
|
+
oauthClient,
|
|
136
|
+
})
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log('[communication_channels] refreshCredentials failed:', err instanceof Error ? err.message : err)
|
|
139
|
+
// Return current credentials — caller may still attempt the send with the old token.
|
|
140
|
+
return { refreshed: false, credentials: input.credentials }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const next = result?.credentials ?? input.credentials
|
|
144
|
+
if (deps?.credentialsService?.save) {
|
|
145
|
+
try {
|
|
146
|
+
await deps.credentialsService.save(
|
|
147
|
+
`channel_${input.adapter.providerKey}`,
|
|
148
|
+
result.expiresAt ? { ...next, expiresAt: result.expiresAt.toISOString() } : next,
|
|
149
|
+
input.scope,
|
|
150
|
+
)
|
|
151
|
+
} catch (saveErr) {
|
|
152
|
+
log('[communication_channels] persisting refreshed credentials failed:', saveErr instanceof Error ? saveErr.message : saveErr)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { refreshed: true, credentials: next }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function shouldRefresh(credentials: Record<string, unknown>, windowMs: number): boolean {
|
|
160
|
+
const expiresAtRaw = credentials?.expiresAt
|
|
161
|
+
if (!expiresAtRaw) return false
|
|
162
|
+
const expiresAt = parseExpiresAt(expiresAtRaw)
|
|
163
|
+
if (!expiresAt) return false
|
|
164
|
+
return expiresAt.getTime() - Date.now() <= windowMs
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseExpiresAt(raw: unknown): Date | null {
|
|
168
|
+
if (raw instanceof Date) return Number.isFinite(raw.getTime()) ? raw : null
|
|
169
|
+
if (typeof raw === 'string' || typeof raw === 'number') {
|
|
170
|
+
const date = new Date(raw)
|
|
171
|
+
return Number.isFinite(date.getTime()) ? date : null
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse a raw `channel_<provider>` client-credential row into the
|
|
178
|
+
* `OAuthClientConfig` shape adapters expect. Returns `undefined` when the row is
|
|
179
|
+
* missing or malformed — adapters then fall back to the deprecated
|
|
180
|
+
* `credentials._client` read path (one minor-release deprecation per Spec A).
|
|
181
|
+
*/
|
|
182
|
+
function safeParseOAuthClient(raw: unknown): OAuthClientConfig | undefined {
|
|
183
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
184
|
+
const record = raw as Record<string, unknown>
|
|
185
|
+
const clientId = typeof record.clientId === 'string' ? record.clientId : undefined
|
|
186
|
+
if (!clientId) return undefined
|
|
187
|
+
const clientSecret =
|
|
188
|
+
typeof record.clientSecret === 'string' ? record.clientSecret : undefined
|
|
189
|
+
const scopes = Array.isArray(record.scopes)
|
|
190
|
+
? record.scopes.filter((value): value is string => typeof value === 'string')
|
|
191
|
+
: undefined
|
|
192
|
+
return {
|
|
193
|
+
clientId,
|
|
194
|
+
...(clientSecret !== undefined ? { clientSecret } : {}),
|
|
195
|
+
...(scopes !== undefined ? { scopes } : {}),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { ChannelIngestDeadLetter } from '../data/entities'
|
|
4
|
+
import type { NormalizedInboundMessage } from './adapter'
|
|
5
|
+
|
|
6
|
+
const DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32_768
|
|
7
|
+
|
|
8
|
+
export function extractExternalUid(message: NormalizedInboundMessage): string | null {
|
|
9
|
+
const meta = message.channelMetadata as Record<string, unknown> | undefined
|
|
10
|
+
if (meta && typeof meta.uid === 'string') return meta.uid
|
|
11
|
+
if (meta && typeof meta.uid === 'number') return String(meta.uid)
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* First N *bytes* of the raw body, for dead-letter forensics. Counts bytes (not
|
|
17
|
+
* UTF-16 code units) so a multi-byte body cannot blow past the intended cap.
|
|
18
|
+
*/
|
|
19
|
+
export function truncateRawBody(message: NormalizedInboundMessage): string | null {
|
|
20
|
+
const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? '', 10)
|
|
21
|
+
const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT
|
|
22
|
+
const body = message.body ?? ''
|
|
23
|
+
if (body.length === 0) return null
|
|
24
|
+
const buf = Buffer.from(body, 'utf-8')
|
|
25
|
+
if (buf.byteLength <= cap) return body
|
|
26
|
+
return `${buf.subarray(0, cap).toString('utf-8')}…[truncated]`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WriteIngestDeadLetterArgs {
|
|
30
|
+
em: EntityManager
|
|
31
|
+
scope: { tenantId: string; organizationId?: string | null }
|
|
32
|
+
channel: { id: string; providerKey: string }
|
|
33
|
+
message: NormalizedInboundMessage
|
|
34
|
+
err: unknown
|
|
35
|
+
errorMessage: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Persist a `ChannelIngestDeadLetter` row for a permanently-failed inbound
|
|
40
|
+
* message so an operator can replay it later. Best-effort: a failure to write
|
|
41
|
+
* the dead-letter is logged but never thrown, so a bad message cannot block the
|
|
42
|
+
* caller from advancing its cursor.
|
|
43
|
+
*
|
|
44
|
+
* Idempotent on `(channelId, externalMessageId)`: a replayed page that fails the
|
|
45
|
+
* same message again is a no-op, so the dead-letter table never accumulates
|
|
46
|
+
* duplicate rows for the same poison message.
|
|
47
|
+
*/
|
|
48
|
+
export async function writeIngestDeadLetter(args: WriteIngestDeadLetterArgs): Promise<void> {
|
|
49
|
+
const { em, scope, channel, message, err, errorMessage } = args
|
|
50
|
+
const externalMessageId = message.externalMessageId ?? null
|
|
51
|
+
try {
|
|
52
|
+
if (externalMessageId) {
|
|
53
|
+
const existing = await findOneWithDecryption(
|
|
54
|
+
em,
|
|
55
|
+
ChannelIngestDeadLetter,
|
|
56
|
+
{
|
|
57
|
+
tenantId: scope.tenantId,
|
|
58
|
+
organizationId: scope.organizationId ?? null,
|
|
59
|
+
channelId: channel.id,
|
|
60
|
+
externalMessageId,
|
|
61
|
+
},
|
|
62
|
+
undefined,
|
|
63
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },
|
|
64
|
+
)
|
|
65
|
+
if (existing) return
|
|
66
|
+
}
|
|
67
|
+
const deadLetter = em.create(ChannelIngestDeadLetter, {
|
|
68
|
+
tenantId: scope.tenantId,
|
|
69
|
+
organizationId: scope.organizationId ?? null,
|
|
70
|
+
channelId: channel.id,
|
|
71
|
+
providerKey: channel.providerKey,
|
|
72
|
+
externalUid: extractExternalUid(message),
|
|
73
|
+
externalMessageId,
|
|
74
|
+
errorClass: err instanceof Error ? err.name : 'Error',
|
|
75
|
+
errorMessage,
|
|
76
|
+
rawBody: truncateRawBody(message),
|
|
77
|
+
})
|
|
78
|
+
em.persist(deadLetter)
|
|
79
|
+
await em.flush()
|
|
80
|
+
} catch (ddlErr) {
|
|
81
|
+
console.error(
|
|
82
|
+
`[communication_channels:dead-letter] failed to record dead-letter for channel ${channel.id}: ${
|
|
83
|
+
ddlErr instanceof Error ? ddlErr.message : String(ddlErr)
|
|
84
|
+
}`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChannelCapabilities } from './adapter'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared default attachment ceiling for email providers (Gmail allows
|
|
5
|
+
* more, but larger uploads need resumable/upload-session APIs we don't use, so
|
|
6
|
+
* all providers cap at the same conservative value).
|
|
7
|
+
*/
|
|
8
|
+
export const EMAIL_MAX_ATTACHMENT_BYTES = 25_000_000
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Baseline capability profile shared by every email channel provider. Providers
|
|
12
|
+
* spread this and override only what genuinely differs (e.g. `deleteMessage`).
|
|
13
|
+
*
|
|
14
|
+
* `fileSharing: false` until a provider's `convertOutbound` stitches attachment
|
|
15
|
+
* bytes into the sent message — advertising `true` would silently drop bytes.
|
|
16
|
+
*/
|
|
17
|
+
export const baseEmailCapabilities: ChannelCapabilities = {
|
|
18
|
+
// Core
|
|
19
|
+
threading: true,
|
|
20
|
+
richText: true,
|
|
21
|
+
fileSharing: false,
|
|
22
|
+
maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,
|
|
23
|
+
supportedMimeTypes: [
|
|
24
|
+
'image/png',
|
|
25
|
+
'image/jpeg',
|
|
26
|
+
'image/gif',
|
|
27
|
+
'image/webp',
|
|
28
|
+
'application/pdf',
|
|
29
|
+
'application/zip',
|
|
30
|
+
'application/octet-stream',
|
|
31
|
+
'text/plain',
|
|
32
|
+
'text/html',
|
|
33
|
+
'text/csv',
|
|
34
|
+
],
|
|
35
|
+
readReceipts: false,
|
|
36
|
+
deliveryReceipts: false,
|
|
37
|
+
typingIndicators: false,
|
|
38
|
+
|
|
39
|
+
// Extended
|
|
40
|
+
reactions: false,
|
|
41
|
+
multiReactionPerUser: false,
|
|
42
|
+
editMessage: false,
|
|
43
|
+
deleteMessage: false,
|
|
44
|
+
presence: false,
|
|
45
|
+
richBlocks: false,
|
|
46
|
+
interactiveComponents: false,
|
|
47
|
+
inlineImages: true,
|
|
48
|
+
conversationHistory: true,
|
|
49
|
+
contactCards: false,
|
|
50
|
+
locationSharing: false,
|
|
51
|
+
voiceNotes: false,
|
|
52
|
+
stickers: false,
|
|
53
|
+
|
|
54
|
+
// Body formats
|
|
55
|
+
supportedBodyFormats: ['text', 'html'],
|
|
56
|
+
maxBodyLength: 5_000_000,
|
|
57
|
+
|
|
58
|
+
// Polling (real-time push deferred to v2 for all email providers)
|
|
59
|
+
realtimePush: false,
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ContactHint, ResolveContactInput } from './adapter'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared `resolveContact` for email providers. An email sender identifier *is*
|
|
5
|
+
* the contact email, so the hint is a direct passthrough; non-email identifiers
|
|
6
|
+
* (or empty senders) yield `null` and the hub falls back to its own resolution.
|
|
7
|
+
*/
|
|
8
|
+
export async function emailResolveContact(input: ResolveContactInput): Promise<ContactHint | null> {
|
|
9
|
+
if (!input.senderIdentifier) return null
|
|
10
|
+
if (input.senderIdentifier.includes('@')) {
|
|
11
|
+
return {
|
|
12
|
+
email: input.senderIdentifier,
|
|
13
|
+
displayName: input.senderDisplayName,
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null
|
|
17
|
+
}
|