@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,95 @@
|
|
|
1
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
3
|
+
import { isUniqueViolation } from "./pg-errors.js";
|
|
4
|
+
const POLLING_ONLY_DEFAULT_INTERVAL_SECONDS = 300;
|
|
5
|
+
class MailboxAlreadyConnectedError extends Error {
|
|
6
|
+
constructor(externalIdentifier, existingProviderKey) {
|
|
7
|
+
super(`Mailbox ${externalIdentifier} is already connected via ${existingProviderKey}`);
|
|
8
|
+
this.name = "MailboxAlreadyConnectedError";
|
|
9
|
+
this.externalIdentifier = externalIdentifier;
|
|
10
|
+
this.existingProviderKey = existingProviderKey;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function createConnectedChannelRow(args) {
|
|
14
|
+
const { em, adapter, providerKey, displayName, externalIdentifier, credentialsRefId, userId, scope } = args;
|
|
15
|
+
const credentialsAvailable = credentialsRefId !== null;
|
|
16
|
+
const pollIntervalSeconds = args.pollIntervalSeconds !== void 0 ? args.pollIntervalSeconds : adapter.capabilities?.realtimePush === false ? POLLING_ONLY_DEFAULT_INTERVAL_SECONDS : null;
|
|
17
|
+
const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null };
|
|
18
|
+
if (externalIdentifier) {
|
|
19
|
+
const normalized = externalIdentifier.toLowerCase();
|
|
20
|
+
const userChannels = await findWithDecryption(
|
|
21
|
+
em,
|
|
22
|
+
CommunicationChannel,
|
|
23
|
+
{ tenantId: scope.tenantId, userId, deletedAt: null },
|
|
24
|
+
void 0,
|
|
25
|
+
dscope
|
|
26
|
+
);
|
|
27
|
+
const conflict = userChannels.find(
|
|
28
|
+
(existing) => existing.providerKey !== providerKey && typeof existing.externalIdentifier === "string" && existing.externalIdentifier.toLowerCase() === normalized
|
|
29
|
+
);
|
|
30
|
+
if (conflict) {
|
|
31
|
+
throw new MailboxAlreadyConnectedError(externalIdentifier, conflict.providerKey);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const naturalKey = {
|
|
35
|
+
tenantId: scope.tenantId,
|
|
36
|
+
userId,
|
|
37
|
+
providerKey,
|
|
38
|
+
externalIdentifier,
|
|
39
|
+
deletedAt: null
|
|
40
|
+
};
|
|
41
|
+
const applyConnectionState = (target) => {
|
|
42
|
+
target.channelType = adapter.channelType;
|
|
43
|
+
target.displayName = displayName;
|
|
44
|
+
target.externalIdentifier = externalIdentifier ?? null;
|
|
45
|
+
target.credentialsRef = credentialsRefId;
|
|
46
|
+
target.capabilities = adapter.capabilities;
|
|
47
|
+
target.isActive = credentialsAvailable;
|
|
48
|
+
target.pollIntervalSeconds = pollIntervalSeconds;
|
|
49
|
+
target.status = credentialsAvailable ? "connected" : "requires_reauth";
|
|
50
|
+
target.lastError = credentialsAvailable ? null : "credentials_persist_failed";
|
|
51
|
+
};
|
|
52
|
+
if (externalIdentifier) {
|
|
53
|
+
const existing = await findOneWithDecryption(em, CommunicationChannel, naturalKey, void 0, dscope);
|
|
54
|
+
if (existing) {
|
|
55
|
+
applyConnectionState(existing);
|
|
56
|
+
await em.flush();
|
|
57
|
+
return existing;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const channel = em.create(CommunicationChannel, {
|
|
61
|
+
providerKey,
|
|
62
|
+
channelType: adapter.channelType,
|
|
63
|
+
displayName,
|
|
64
|
+
externalIdentifier: externalIdentifier ?? null,
|
|
65
|
+
credentialsRef: credentialsRefId,
|
|
66
|
+
capabilities: adapter.capabilities,
|
|
67
|
+
isActive: credentialsAvailable,
|
|
68
|
+
userId,
|
|
69
|
+
isPrimary: false,
|
|
70
|
+
pollIntervalSeconds,
|
|
71
|
+
status: credentialsAvailable ? "connected" : "requires_reauth",
|
|
72
|
+
lastError: credentialsAvailable ? null : "credentials_persist_failed",
|
|
73
|
+
tenantId: scope.tenantId,
|
|
74
|
+
organizationId: scope.organizationId ?? null
|
|
75
|
+
});
|
|
76
|
+
em.persist(channel);
|
|
77
|
+
try {
|
|
78
|
+
await em.flush();
|
|
79
|
+
return channel;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (!isUniqueViolation(err) || !externalIdentifier) throw err;
|
|
82
|
+
const reEm = em.fork();
|
|
83
|
+
const winner = await findOneWithDecryption(reEm, CommunicationChannel, naturalKey, void 0, dscope);
|
|
84
|
+
if (!winner) throw err;
|
|
85
|
+
applyConnectionState(winner);
|
|
86
|
+
await reEm.flush();
|
|
87
|
+
return winner;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
MailboxAlreadyConnectedError,
|
|
92
|
+
POLLING_ONLY_DEFAULT_INTERVAL_SECONDS,
|
|
93
|
+
createConnectedChannelRow
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=connect-channel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/connect-channel.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport type { ChannelAdapter } from './adapter'\nimport { isUniqueViolation } from './pg-errors'\n\n/**\n * Default poll cadence (seconds) for a polling-only channel \u2014 one whose adapter\n * declares `realtimePush: false`. Push-capable channels use `null` (push-driven,\n * no fixed poll). Shared so push teardown restores the same cadence connect uses.\n */\nexport const POLLING_ONLY_DEFAULT_INTERVAL_SECONDS = 300\n\n/**\n * Thrown by {@link createConnectedChannelRow} when the same mailbox\n * (`externalIdentifier`) is already connected for this user via a DIFFERENT\n * provider. Both channels would poll the same inbox, and the per-channel\n * `(channel_id, external_message_id)` dedup cannot dedupe the same email across\n * channels \u2014 so every message would be ingested (and threaded) twice.\n */\nexport class MailboxAlreadyConnectedError extends Error {\n readonly externalIdentifier: string\n readonly existingProviderKey: string\n constructor(externalIdentifier: string, existingProviderKey: string) {\n super(`Mailbox ${externalIdentifier} is already connected via ${existingProviderKey}`)\n this.name = 'MailboxAlreadyConnectedError'\n this.externalIdentifier = externalIdentifier\n this.existingProviderKey = existingProviderKey\n }\n}\n\nexport interface CreateConnectedChannelRowArgs {\n em: EntityManager\n adapter: Pick<ChannelAdapter, 'channelType' | 'capabilities'>\n providerKey: string\n displayName: string\n externalIdentifier: string | null\n credentialsRefId: string | null\n userId: string\n scope: { tenantId: string; organizationId: string | null }\n /**\n * Explicit poll-interval override (seconds). When omitted, it is derived from\n * the adapter's push capability (push-capable \u2192 null, polling-only \u2192 300).\n */\n pollIntervalSeconds?: number | null\n}\n\n/**\n * Create + persist the per-user `CommunicationChannel` row for a connect flow.\n * Shared by the credential-connect command and the OAuth callback so both entry\n * points use one channel-shape implementation instead of duplicating `em.create`.\n *\n * When credentials could not be persisted (`credentialsRefId === null`) the row\n * is created in `requires_reauth` + `isActive=false` so workers don't poll a\n * credential-less channel; the user reconnects to recover.\n */\nexport async function createConnectedChannelRow(\n args: CreateConnectedChannelRowArgs,\n): Promise<CommunicationChannel> {\n const { em, adapter, providerKey, displayName, externalIdentifier, credentialsRefId, userId, scope } = args\n const credentialsAvailable = credentialsRefId !== null\n const pollIntervalSeconds =\n args.pollIntervalSeconds !== undefined\n ? args.pollIntervalSeconds\n : adapter.capabilities?.realtimePush === false\n ? POLLING_ONLY_DEFAULT_INTERVAL_SECONDS\n : null\n const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }\n\n // Cross-provider duplicate guard: the same mailbox must not be connected via\n // two providers for one user. Both channels would poll the same inbox, and the\n // per-channel `(channel_id, external_message_id)` dedup cannot dedupe the same\n // email across channels \u2014 so every message would be ingested (and threaded)\n // twice. Reconnecting the SAME provider/mailbox is fine (healed below); this\n // only blocks a DIFFERENT provider for an already-connected address.\n if (externalIdentifier) {\n const normalized = externalIdentifier.toLowerCase()\n const userChannels = (await findWithDecryption(\n em,\n CommunicationChannel,\n { tenantId: scope.tenantId, userId, deletedAt: null },\n undefined,\n dscope,\n )) as CommunicationChannel[]\n const conflict = userChannels.find(\n (existing) =>\n existing.providerKey !== providerKey &&\n typeof existing.externalIdentifier === 'string' &&\n existing.externalIdentifier.toLowerCase() === normalized,\n )\n if (conflict) {\n throw new MailboxAlreadyConnectedError(externalIdentifier, conflict.providerKey)\n }\n }\n\n const naturalKey = {\n tenantId: scope.tenantId,\n userId,\n providerKey,\n externalIdentifier,\n deletedAt: null,\n }\n\n // Heal-on-reconnect: a channel for the same (tenant, user, provider, mailbox)\n // already exists when the user re-runs OAuth / reconnects after a\n // `requires_reauth`. Update it in place rather than inserting a duplicate row \u2014\n // a duplicate would stay `isActive` and keep polling + re-emitting reauth\n // banners, and register a second competing push subscription. Only mailboxes\n // with a known `externalIdentifier` participate (the unique index is partial).\n const applyConnectionState = (target: CommunicationChannel): void => {\n target.channelType = adapter.channelType\n target.displayName = displayName\n target.externalIdentifier = externalIdentifier ?? null\n target.credentialsRef = credentialsRefId\n target.capabilities = adapter.capabilities as unknown as Record<string, unknown>\n target.isActive = credentialsAvailable\n target.pollIntervalSeconds = pollIntervalSeconds\n target.status = credentialsAvailable ? 'connected' : 'requires_reauth'\n target.lastError = credentialsAvailable ? null : 'credentials_persist_failed'\n }\n\n if (externalIdentifier) {\n const existing = await findOneWithDecryption(em, CommunicationChannel, naturalKey, undefined, dscope)\n if (existing) {\n applyConnectionState(existing)\n await em.flush()\n return existing\n }\n }\n\n const channel = em.create(CommunicationChannel, {\n providerKey,\n channelType: adapter.channelType,\n displayName,\n externalIdentifier: externalIdentifier ?? null,\n credentialsRef: credentialsRefId,\n capabilities: adapter.capabilities as unknown as Record<string, unknown>,\n isActive: credentialsAvailable,\n userId,\n isPrimary: false,\n pollIntervalSeconds,\n status: credentialsAvailable ? 'connected' : 'requires_reauth',\n lastError: credentialsAvailable ? null : 'credentials_persist_failed',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n })\n em.persist(channel)\n try {\n await em.flush()\n return channel\n } catch (err) {\n // Concurrent connect for the same mailbox won the race (partial unique index\n // rejected ours). Re-select the winner on a clean fork and heal it so the\n // caller still gets a single, connected channel.\n if (!isUniqueViolation(err) || !externalIdentifier) throw err\n const reEm = em.fork()\n const winner = await findOneWithDecryption(reEm, CommunicationChannel, naturalKey, undefined, dscope)\n if (!winner) throw err\n applyConnectionState(winner)\n await reEm.flush()\n return winner\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,4BAA4B;AAErC,SAAS,yBAAyB;AAO3B,MAAM,wCAAwC;AAS9C,MAAM,qCAAqC,MAAM;AAAA,EAGtD,YAAY,oBAA4B,qBAA6B;AACnE,UAAM,WAAW,kBAAkB,6BAA6B,mBAAmB,EAAE;AACrF,SAAK,OAAO;AACZ,SAAK,qBAAqB;AAC1B,SAAK,sBAAsB;AAAA,EAC7B;AACF;AA2BA,eAAsB,0BACpB,MAC+B;AAC/B,QAAM,EAAE,IAAI,SAAS,aAAa,aAAa,oBAAoB,kBAAkB,QAAQ,MAAM,IAAI;AACvG,QAAM,uBAAuB,qBAAqB;AAClD,QAAM,sBACJ,KAAK,wBAAwB,SACzB,KAAK,sBACL,QAAQ,cAAc,iBAAiB,QACrC,wCACA;AACR,QAAM,SAAS,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,kBAAkB,KAAK;AAQxF,MAAI,oBAAoB;AACtB,UAAM,aAAa,mBAAmB,YAAY;AAClD,UAAM,eAAgB,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,EAAE,UAAU,MAAM,UAAU,QAAQ,WAAW,KAAK;AAAA,MACpD;AAAA,MACA;AAAA,IACF;AACA,UAAM,WAAW,aAAa;AAAA,MAC5B,CAAC,aACC,SAAS,gBAAgB,eACzB,OAAO,SAAS,uBAAuB,YACvC,SAAS,mBAAmB,YAAY,MAAM;AAAA,IAClD;AACA,QAAI,UAAU;AACZ,YAAM,IAAI,6BAA6B,oBAAoB,SAAS,WAAW;AAAA,IACjF;AAAA,EACF;AAEA,QAAM,aAAa;AAAA,IACjB,UAAU,MAAM;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AAQA,QAAM,uBAAuB,CAAC,WAAuC;AACnE,WAAO,cAAc,QAAQ;AAC7B,WAAO,cAAc;AACrB,WAAO,qBAAqB,sBAAsB;AAClD,WAAO,iBAAiB;AACxB,WAAO,eAAe,QAAQ;AAC9B,WAAO,WAAW;AAClB,WAAO,sBAAsB;AAC7B,WAAO,SAAS,uBAAuB,cAAc;AACrD,WAAO,YAAY,uBAAuB,OAAO;AAAA,EACnD;AAEA,MAAI,oBAAoB;AACtB,UAAM,WAAW,MAAM,sBAAsB,IAAI,sBAAsB,YAAY,QAAW,MAAM;AACpG,QAAI,UAAU;AACZ,2BAAqB,QAAQ;AAC7B,YAAM,GAAG,MAAM;AACf,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,UAAU,GAAG,OAAO,sBAAsB;AAAA,IAC9C;AAAA,IACA,aAAa,QAAQ;AAAA,IACrB;AAAA,IACA,oBAAoB,sBAAsB;AAAA,IAC1C,gBAAgB;AAAA,IAChB,cAAc,QAAQ;AAAA,IACtB,UAAU;AAAA,IACV;AAAA,IACA,WAAW;AAAA,IACX;AAAA,IACA,QAAQ,uBAAuB,cAAc;AAAA,IAC7C,WAAW,uBAAuB,OAAO;AAAA,IACzC,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM,kBAAkB;AAAA,EAC1C,CAAC;AACD,KAAG,QAAQ,OAAO;AAClB,MAAI;AACF,UAAM,GAAG,MAAM;AACf,WAAO;AAAA,EACT,SAAS,KAAK;AAIZ,QAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC,mBAAoB,OAAM;AAC1D,UAAM,OAAO,GAAG,KAAK;AACrB,UAAM,SAAS,MAAM,sBAAsB,MAAM,sBAAsB,YAAY,QAAW,MAAM;AACpG,QAAI,CAAC,OAAQ,OAAM;AACnB,yBAAqB,MAAM;AAC3B,UAAM,KAAK,MAAM;AACjB,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
|
|
2
|
+
async function resolveContact(input, deps) {
|
|
3
|
+
const adapterHint = await runAdapterResolve(input);
|
|
4
|
+
if (!adapterHint && !input.senderIdentifier) return null;
|
|
5
|
+
const lookupEmail = adapterHint?.email ?? heuristicEmail(input.senderIdentifier);
|
|
6
|
+
const lookupPhone = adapterHint?.phone ?? heuristicPhone(input.senderIdentifier);
|
|
7
|
+
let matchedPersonId;
|
|
8
|
+
if ((lookupEmail || lookupPhone) && deps.container) {
|
|
9
|
+
matchedPersonId = await lookupCustomerPersonId({
|
|
10
|
+
container: deps.container,
|
|
11
|
+
scope: input.scope,
|
|
12
|
+
email: lookupEmail,
|
|
13
|
+
phone: lookupPhone
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
...adapterHint ?? {},
|
|
18
|
+
email: lookupEmail,
|
|
19
|
+
phone: lookupPhone,
|
|
20
|
+
displayName: adapterHint?.displayName ?? input.senderDisplayName,
|
|
21
|
+
matchedPersonId: matchedPersonId ?? adapterHint?.matchedPersonId
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function runAdapterResolve(input) {
|
|
25
|
+
if (typeof input.adapter.resolveContact !== "function") return null;
|
|
26
|
+
try {
|
|
27
|
+
return await input.adapter.resolveContact({
|
|
28
|
+
senderIdentifier: input.senderIdentifier,
|
|
29
|
+
senderDisplayName: input.senderDisplayName,
|
|
30
|
+
channelMetadata: input.channelMetadata,
|
|
31
|
+
credentials: input.credentials,
|
|
32
|
+
scope: input.scope
|
|
33
|
+
}) ?? null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function lookupCustomerPersonId(params) {
|
|
39
|
+
if (isTenantDataEncryptionEnabled()) return void 0;
|
|
40
|
+
let queryEngine = null;
|
|
41
|
+
try {
|
|
42
|
+
queryEngine = params.container.resolve("queryEngine");
|
|
43
|
+
} catch {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
if (!queryEngine || typeof queryEngine.query !== "function") return void 0;
|
|
47
|
+
const filter = buildPersonLookupFilter(params.email, params.phone);
|
|
48
|
+
if (!filter) return void 0;
|
|
49
|
+
try {
|
|
50
|
+
const rows = await queryEngine.query("customers:customer_entity", {
|
|
51
|
+
tenantId: params.scope.tenantId,
|
|
52
|
+
organizationId: params.scope.organizationId,
|
|
53
|
+
filter,
|
|
54
|
+
limit: 1
|
|
55
|
+
});
|
|
56
|
+
const first = rows?.[0];
|
|
57
|
+
const id = first && typeof first.id === "string" ? first.id : void 0;
|
|
58
|
+
return id;
|
|
59
|
+
} catch {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function buildPersonLookupFilter(email, phone) {
|
|
64
|
+
if (email) return { primary_email: email, kind: "person" };
|
|
65
|
+
if (phone) return { primary_phone: phone, kind: "person" };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function heuristicEmail(senderIdentifier) {
|
|
69
|
+
if (!senderIdentifier.includes("@")) return void 0;
|
|
70
|
+
return senderIdentifier.includes(".") ? senderIdentifier : void 0;
|
|
71
|
+
}
|
|
72
|
+
function heuristicPhone(senderIdentifier) {
|
|
73
|
+
if (/^\+\d{6,}$/.test(senderIdentifier)) return senderIdentifier;
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
resolveContact
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=contact-resolver.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/contact-resolver.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport type { ChannelAdapter, ContactHint, TenantScope } from './adapter'\n\n/**\n * Contact resolver \u2014 used by the inbound bridge to attach an external sender\n * to a CRM person.\n *\n * Flow (SPEC-045d \u00A78.1):\n * 1. If the adapter implements `resolveContact?(...)`, call it to get a\n * provider-side ContactHint (display name, photo URL, possibly an email\n * lookup the adapter performed against its own user directory).\n * 2. If the hint includes an email or phone, query the CRM (`customers:customer_entity`)\n * via the **QueryEngine** (NOT raw SQL \u2014 root AGENTS.md mandate). If a person\n * matches, populate `matchedPersonId` on the returned hint.\n * 3. Return the merged hint, or `null` if neither the adapter nor the CRM\n * yields any identity information.\n */\n\nexport interface ContactResolverInput {\n adapter: ChannelAdapter\n senderIdentifier: string\n senderDisplayName?: string\n channelMetadata?: Record<string, unknown>\n credentials: Record<string, unknown>\n scope: TenantScope\n}\n\nexport interface ResolveContactDeps {\n container: { resolve: <T = unknown>(name: string) => T }\n}\n\ntype QueryEngineLike = {\n query: (\n entityType: string,\n options: {\n tenantId: string\n organizationId?: string | null\n filter?: Record<string, unknown>\n limit?: number\n offset?: number\n },\n ) => Promise<Record<string, unknown>[]>\n}\n\n/**\n * Resolve a CRM contact from an external sender identifier.\n *\n * Returns `null` when no identity could be derived. Callers MUST treat the\n * return value as advisory (a hint, not an authoritative match) \u2014 see spec\n * \u00A7 8 \"Contact resolution false positive\" risk.\n */\nexport async function resolveContact(\n input: ContactResolverInput,\n deps: ResolveContactDeps,\n): Promise<ContactHint | null> {\n // Step 1 \u2014 adapter resolution (optional)\n const adapterHint: ContactHint | null = await runAdapterResolve(input)\n if (!adapterHint && !input.senderIdentifier) return null\n\n // Step 2 \u2014 CRM lookup by email or phone, via QueryEngine (no raw SQL).\n const lookupEmail = adapterHint?.email ?? heuristicEmail(input.senderIdentifier)\n const lookupPhone = adapterHint?.phone ?? heuristicPhone(input.senderIdentifier)\n\n let matchedPersonId: string | undefined\n if ((lookupEmail || lookupPhone) && deps.container) {\n matchedPersonId = await lookupCustomerPersonId({\n container: deps.container,\n scope: input.scope,\n email: lookupEmail,\n phone: lookupPhone,\n })\n }\n\n return {\n ...(adapterHint ?? {}),\n email: lookupEmail,\n phone: lookupPhone,\n displayName: adapterHint?.displayName ?? input.senderDisplayName,\n matchedPersonId: matchedPersonId ?? adapterHint?.matchedPersonId,\n }\n}\n\nasync function runAdapterResolve(input: ContactResolverInput): Promise<ContactHint | null> {\n if (typeof input.adapter.resolveContact !== 'function') return null\n try {\n return (\n (await input.adapter.resolveContact({\n senderIdentifier: input.senderIdentifier,\n senderDisplayName: input.senderDisplayName,\n channelMetadata: input.channelMetadata,\n credentials: input.credentials,\n scope: input.scope,\n })) ?? null\n )\n } catch {\n // The adapter is best-effort. A failure does not block ingest; we just lose\n // the optional CRM match. Errors are not propagated.\n return null\n }\n}\n\nasync function lookupCustomerPersonId(params: {\n container: ResolveContactDeps['container']\n scope: TenantScope\n email?: string\n phone?: string\n}): Promise<string | undefined> {\n // Under tenant encryption, `primary_email`/`primary_phone` are stored as\n // ciphertext, so a plaintext equality filter on the base column both hits the\n // \u00A716 \"no querying an encrypted column by value\" footgun and never matches.\n // Skip the fast lookup in that case (it would only ever return nothing) \u2014 the\n // authoritative CRM link is created by the customers `link-channel-message`\n // subscriber, which does an in-memory decrypted comparison. A blind-index\n // column is the proper fast-path fix here (same follow-up as\n // `customers/lib/findPeopleByAddresses`).\n if (isTenantDataEncryptionEnabled()) return undefined\n\n let queryEngine: QueryEngineLike | null = null\n try {\n queryEngine = params.container.resolve<QueryEngineLike>('queryEngine')\n } catch {\n return undefined\n }\n if (!queryEngine || typeof queryEngine.query !== 'function') return undefined\n\n const filter = buildPersonLookupFilter(params.email, params.phone)\n if (!filter) return undefined\n\n try {\n const rows = await queryEngine.query('customers:customer_entity', {\n tenantId: params.scope.tenantId,\n organizationId: params.scope.organizationId,\n filter,\n limit: 1,\n })\n const first = rows?.[0]\n const id = first && typeof first.id === 'string' ? (first.id as string) : undefined\n return id\n } catch {\n return undefined\n }\n}\n\nfunction buildPersonLookupFilter(email?: string, phone?: string): Record<string, unknown> | null {\n if (email) return { primary_email: email, kind: 'person' }\n if (phone) return { primary_phone: phone, kind: 'person' }\n return null\n}\n\nfunction heuristicEmail(senderIdentifier: string): string | undefined {\n if (!senderIdentifier.includes('@')) return undefined\n // Don't second-guess provider-supplied data; just check it's email-shaped.\n return senderIdentifier.includes('.') ? senderIdentifier : undefined\n}\n\nfunction heuristicPhone(senderIdentifier: string): string | undefined {\n // Plain heuristic: +CC followed by 6+ digits. Real phone validation is the\n // adapter's job (the email integration spec elaborates IMAP paths).\n if (/^\\+\\d{6,}$/.test(senderIdentifier)) return senderIdentifier\n return undefined\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,qCAAqC;AAmD9C,eAAsB,eACpB,OACA,MAC6B;AAE7B,QAAM,cAAkC,MAAM,kBAAkB,KAAK;AACrE,MAAI,CAAC,eAAe,CAAC,MAAM,iBAAkB,QAAO;AAGpD,QAAM,cAAc,aAAa,SAAS,eAAe,MAAM,gBAAgB;AAC/E,QAAM,cAAc,aAAa,SAAS,eAAe,MAAM,gBAAgB;AAE/E,MAAI;AACJ,OAAK,eAAe,gBAAgB,KAAK,WAAW;AAClD,sBAAkB,MAAM,uBAAuB;AAAA,MAC7C,WAAW,KAAK;AAAA,MAChB,OAAO,MAAM;AAAA,MACb,OAAO;AAAA,MACP,OAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL,GAAI,eAAe,CAAC;AAAA,IACpB,OAAO;AAAA,IACP,OAAO;AAAA,IACP,aAAa,aAAa,eAAe,MAAM;AAAA,IAC/C,iBAAiB,mBAAmB,aAAa;AAAA,EACnD;AACF;AAEA,eAAe,kBAAkB,OAA0D;AACzF,MAAI,OAAO,MAAM,QAAQ,mBAAmB,WAAY,QAAO;AAC/D,MAAI;AACF,WACG,MAAM,MAAM,QAAQ,eAAe;AAAA,MAClC,kBAAkB,MAAM;AAAA,MACxB,mBAAmB,MAAM;AAAA,MACzB,iBAAiB,MAAM;AAAA,MACvB,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,IACf,CAAC,KAAM;AAAA,EAEX,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,uBAAuB,QAKN;AAS9B,MAAI,8BAA8B,EAAG,QAAO;AAE5C,MAAI,cAAsC;AAC1C,MAAI;AACF,kBAAc,OAAO,UAAU,QAAyB,aAAa;AAAA,EACvE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,eAAe,OAAO,YAAY,UAAU,WAAY,QAAO;AAEpE,QAAM,SAAS,wBAAwB,OAAO,OAAO,OAAO,KAAK;AACjE,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI;AACF,UAAM,OAAO,MAAM,YAAY,MAAM,6BAA6B;AAAA,MAChE,UAAU,OAAO,MAAM;AAAA,MACvB,gBAAgB,OAAO,MAAM;AAAA,MAC7B;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AACD,UAAM,QAAQ,OAAO,CAAC;AACtB,UAAM,KAAK,SAAS,OAAO,MAAM,OAAO,WAAY,MAAM,KAAgB;AAC1E,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,wBAAwB,OAAgB,OAAgD;AAC/F,MAAI,MAAO,QAAO,EAAE,eAAe,OAAO,MAAM,SAAS;AACzD,MAAI,MAAO,QAAO,EAAE,eAAe,OAAO,MAAM,SAAS;AACzD,SAAO;AACT;AAEA,SAAS,eAAe,kBAA8C;AACpE,MAAI,CAAC,iBAAiB,SAAS,GAAG,EAAG,QAAO;AAE5C,SAAO,iBAAiB,SAAS,GAAG,IAAI,mBAAmB;AAC7D;AAEA,SAAS,eAAe,kBAA8C;AAGpE,MAAI,aAAa,KAAK,gBAAgB,EAAG,QAAO;AAChD,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolveOAuthClientCredentials } from "./oauth-client-config.js";
|
|
2
|
+
const DEFAULT_REFRESH_WINDOW_MS = 6e4;
|
|
3
|
+
const inFlightRefreshes = /* @__PURE__ */ new Map();
|
|
4
|
+
async function refreshCredentialsIfNeeded(input, deps) {
|
|
5
|
+
const log = deps?.logger ?? (() => {
|
|
6
|
+
});
|
|
7
|
+
if (typeof input.adapter.refreshCredentials !== "function") {
|
|
8
|
+
return { refreshed: false, credentials: input.credentials };
|
|
9
|
+
}
|
|
10
|
+
const refreshWindow = input.refreshWindowMs ?? DEFAULT_REFRESH_WINDOW_MS;
|
|
11
|
+
if (!input.force && !shouldRefresh(input.credentials, refreshWindow)) {
|
|
12
|
+
return { refreshed: false, credentials: input.credentials };
|
|
13
|
+
}
|
|
14
|
+
const existing = inFlightRefreshes.get(input.channelId);
|
|
15
|
+
if (existing) return existing;
|
|
16
|
+
const refreshCredentials = input.adapter.refreshCredentials.bind(input.adapter);
|
|
17
|
+
const refreshPromise = runRefresh(input, refreshCredentials, deps, log).finally(() => {
|
|
18
|
+
inFlightRefreshes.delete(input.channelId);
|
|
19
|
+
});
|
|
20
|
+
inFlightRefreshes.set(input.channelId, refreshPromise);
|
|
21
|
+
return refreshPromise;
|
|
22
|
+
}
|
|
23
|
+
async function runRefresh(input, refreshCredentials, deps, log) {
|
|
24
|
+
let oauthClient;
|
|
25
|
+
if (deps?.credentialsService) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await resolveOAuthClientCredentials(
|
|
28
|
+
deps.credentialsService,
|
|
29
|
+
input.adapter.providerKey,
|
|
30
|
+
{ tenantId: input.scope.tenantId, organizationId: input.scope.organizationId }
|
|
31
|
+
);
|
|
32
|
+
oauthClient = safeParseOAuthClient(raw);
|
|
33
|
+
} catch (resolveErr) {
|
|
34
|
+
log(
|
|
35
|
+
"[communication_channels] resolving OAuth client config failed:",
|
|
36
|
+
resolveErr instanceof Error ? resolveErr.message : resolveErr
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let result;
|
|
41
|
+
try {
|
|
42
|
+
result = await refreshCredentials({
|
|
43
|
+
channelId: input.channelId,
|
|
44
|
+
credentials: input.credentials,
|
|
45
|
+
scope: input.scope,
|
|
46
|
+
oauthClient
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log("[communication_channels] refreshCredentials failed:", err instanceof Error ? err.message : err);
|
|
50
|
+
return { refreshed: false, credentials: input.credentials };
|
|
51
|
+
}
|
|
52
|
+
const next = result?.credentials ?? input.credentials;
|
|
53
|
+
if (deps?.credentialsService?.save) {
|
|
54
|
+
try {
|
|
55
|
+
await deps.credentialsService.save(
|
|
56
|
+
`channel_${input.adapter.providerKey}`,
|
|
57
|
+
result.expiresAt ? { ...next, expiresAt: result.expiresAt.toISOString() } : next,
|
|
58
|
+
input.scope
|
|
59
|
+
);
|
|
60
|
+
} catch (saveErr) {
|
|
61
|
+
log("[communication_channels] persisting refreshed credentials failed:", saveErr instanceof Error ? saveErr.message : saveErr);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { refreshed: true, credentials: next };
|
|
65
|
+
}
|
|
66
|
+
function shouldRefresh(credentials, windowMs) {
|
|
67
|
+
const expiresAtRaw = credentials?.expiresAt;
|
|
68
|
+
if (!expiresAtRaw) return false;
|
|
69
|
+
const expiresAt = parseExpiresAt(expiresAtRaw);
|
|
70
|
+
if (!expiresAt) return false;
|
|
71
|
+
return expiresAt.getTime() - Date.now() <= windowMs;
|
|
72
|
+
}
|
|
73
|
+
function parseExpiresAt(raw) {
|
|
74
|
+
if (raw instanceof Date) return Number.isFinite(raw.getTime()) ? raw : null;
|
|
75
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
76
|
+
const date = new Date(raw);
|
|
77
|
+
return Number.isFinite(date.getTime()) ? date : null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function safeParseOAuthClient(raw) {
|
|
82
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
83
|
+
const record = raw;
|
|
84
|
+
const clientId = typeof record.clientId === "string" ? record.clientId : void 0;
|
|
85
|
+
if (!clientId) return void 0;
|
|
86
|
+
const clientSecret = typeof record.clientSecret === "string" ? record.clientSecret : void 0;
|
|
87
|
+
const scopes = Array.isArray(record.scopes) ? record.scopes.filter((value) => typeof value === "string") : void 0;
|
|
88
|
+
return {
|
|
89
|
+
clientId,
|
|
90
|
+
...clientSecret !== void 0 ? { clientSecret } : {},
|
|
91
|
+
...scopes !== void 0 ? { scopes } : {}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
refreshCredentialsIfNeeded
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=credential-refresh.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/credential-refresh.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n ChannelAdapter,\n OAuthClientConfig,\n RefreshedCredentials,\n TenantScope,\n} from './adapter'\nimport { resolveOAuthClientCredentials } from './oauth-client-config'\n\n/**\n * Optional credentials-service shape \u2014 matches the integrations module's\n * `CredentialsService`. We keep this loose so the helper compiles even when\n * the integrations module is disabled in a downstream app (the helper just\n * skips persistence in that case).\n */\n/**\n * Matches the real `CredentialsService.save(integrationId, credentials, scope)`\n * signature from `packages/core/src/modules/integrations/lib/credentials-service.ts`.\n * The legacy call sites had this argument order inverted; that bug was the root\n * cause of C1 in the 2026-05-26 review.\n *\n * `scope.userId` (added 2026-05-26 for per-user channels) lets the credentials\n * service write to a user-scoped row instead of overwriting the tenant-wide row.\n */\ntype CredentialsScope = TenantScope & { userId?: string | null }\ntype CredentialsServiceLike = {\n resolve: (integrationId: string, scope: CredentialsScope) => Promise<Record<string, unknown> | null>\n save?: (integrationId: string, credentials: Record<string, unknown>, scope: CredentialsScope) => Promise<void>\n}\n\nexport type RefreshCredentialsIfNeededInput = {\n adapter: ChannelAdapter\n channelId: string\n /** Current decrypted credential blob. May contain `expiresAt` (Date or ISO string). */\n credentials: Record<string, unknown>\n scope: CredentialsScope\n /** Refresh window \u2014 refresh when token expires within this many ms. Defaults to 60s. */\n refreshWindowMs?: number\n /** Force a refresh regardless of expiry \u2014 used after a 401 response from the provider. */\n force?: boolean\n}\n\nexport type RefreshCredentialsIfNeededResult = {\n refreshed: boolean\n /** The latest credential blob to use for the outbound call. */\n credentials: Record<string, unknown>\n}\n\nconst DEFAULT_REFRESH_WINDOW_MS = 60_000\n\n/**\n * In-process single-flight for credential refresh, keyed by `channelId`. The\n * outbound-delivery worker runs at concurrency 10 in ONE process, so two\n * concurrent sends on the same channel can both pass `shouldRefresh` and both\n * call `adapter.refreshCredentials`. With rotating refresh-token providers\n * (Gmail) the second exchange invalidates the first's token and\n * flaps the channel to `requires_reauth`. Coalescing concurrent refreshes for\n * the same channel onto one in-flight promise prevents that race for the common\n * single-process case. Entries are deleted in `finally` once settled.\n */\nconst inFlightRefreshes = new Map<string, Promise<RefreshCredentialsIfNeededResult>>()\n\n/**\n * Refresh OAuth credentials when an access token is near expiry, or when the\n * caller forces it (e.g. after a 401 response).\n *\n * Behaviour:\n * - No-op when the adapter does not implement `refreshCredentials?`.\n * - No-op when the credential blob has no `expiresAt` AND `force !== true`.\n * - Returns the refreshed credentials in-memory; persistence to\n * `integration_credentials` happens via the `CredentialsService` if it\n * is registered AND exposes `save()` (best-effort \u2014 failures are logged\n * but don't block the outbound call).\n */\nexport async function refreshCredentialsIfNeeded(\n input: RefreshCredentialsIfNeededInput,\n deps?: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void },\n): Promise<RefreshCredentialsIfNeededResult> {\n const log = deps?.logger ?? (() => {})\n if (typeof input.adapter.refreshCredentials !== 'function') {\n return { refreshed: false, credentials: input.credentials }\n }\n\n const refreshWindow = input.refreshWindowMs ?? DEFAULT_REFRESH_WINDOW_MS\n if (!input.force && !shouldRefresh(input.credentials, refreshWindow)) {\n return { refreshed: false, credentials: input.credentials }\n }\n\n // Coalesce concurrent refreshes for the same channel onto one in-flight\n // promise so rotating refresh tokens are not exchanged twice in parallel.\n const existing = inFlightRefreshes.get(input.channelId)\n if (existing) return existing\n\n const refreshCredentials = input.adapter.refreshCredentials.bind(input.adapter)\n const refreshPromise = runRefresh(input, refreshCredentials, deps, log).finally(() => {\n inFlightRefreshes.delete(input.channelId)\n })\n inFlightRefreshes.set(input.channelId, refreshPromise)\n return refreshPromise\n}\n\nasync function runRefresh(\n input: RefreshCredentialsIfNeededInput,\n refreshCredentials: NonNullable<ChannelAdapter['refreshCredentials']>,\n deps: { credentialsService?: CredentialsServiceLike | null; logger?: (...args: unknown[]) => void } | undefined,\n log: (...args: unknown[]) => void,\n): Promise<RefreshCredentialsIfNeededResult> {\n // Resolve the tenant's OAuth client config (clientId/clientSecret) the admin\n // stored under the `channel_<providerKey>` integration at TENANT scope\n // (userId = null). The adapter uses it for the token-endpoint call. We always\n // resolve at tenant scope here \u2014 never the channel's per-user scope \u2014 because\n // the per-user row holds the user's tokens, not the client app credentials.\n let oauthClient: OAuthClientConfig | undefined\n if (deps?.credentialsService) {\n try {\n const raw = await resolveOAuthClientCredentials(\n deps.credentialsService,\n input.adapter.providerKey,\n { tenantId: input.scope.tenantId, organizationId: input.scope.organizationId },\n )\n oauthClient = safeParseOAuthClient(raw)\n } catch (resolveErr) {\n log(\n '[communication_channels] resolving OAuth client config failed:',\n resolveErr instanceof Error ? resolveErr.message : resolveErr,\n )\n }\n }\n\n let result: RefreshedCredentials\n try {\n result = await refreshCredentials({\n channelId: input.channelId,\n credentials: input.credentials,\n scope: input.scope,\n oauthClient,\n })\n } catch (err) {\n log('[communication_channels] refreshCredentials failed:', err instanceof Error ? err.message : err)\n // Return current credentials \u2014 caller may still attempt the send with the old token.\n return { refreshed: false, credentials: input.credentials }\n }\n\n const next = result?.credentials ?? input.credentials\n if (deps?.credentialsService?.save) {\n try {\n await deps.credentialsService.save(\n `channel_${input.adapter.providerKey}`,\n result.expiresAt ? { ...next, expiresAt: result.expiresAt.toISOString() } : next,\n input.scope,\n )\n } catch (saveErr) {\n log('[communication_channels] persisting refreshed credentials failed:', saveErr instanceof Error ? saveErr.message : saveErr)\n }\n }\n\n return { refreshed: true, credentials: next }\n}\n\nfunction shouldRefresh(credentials: Record<string, unknown>, windowMs: number): boolean {\n const expiresAtRaw = credentials?.expiresAt\n if (!expiresAtRaw) return false\n const expiresAt = parseExpiresAt(expiresAtRaw)\n if (!expiresAt) return false\n return expiresAt.getTime() - Date.now() <= windowMs\n}\n\nfunction parseExpiresAt(raw: unknown): Date | null {\n if (raw instanceof Date) return Number.isFinite(raw.getTime()) ? raw : null\n if (typeof raw === 'string' || typeof raw === 'number') {\n const date = new Date(raw)\n return Number.isFinite(date.getTime()) ? date : null\n }\n return null\n}\n\n/**\n * Parse a raw `channel_<provider>` client-credential row into the\n * `OAuthClientConfig` shape adapters expect. Returns `undefined` when the row is\n * missing or malformed \u2014 adapters then fall back to the deprecated\n * `credentials._client` read path (one minor-release deprecation per Spec A).\n */\nfunction safeParseOAuthClient(raw: unknown): OAuthClientConfig | undefined {\n if (!raw || typeof raw !== 'object') return undefined\n const record = raw as Record<string, unknown>\n const clientId = typeof record.clientId === 'string' ? record.clientId : undefined\n if (!clientId) return undefined\n const clientSecret =\n typeof record.clientSecret === 'string' ? record.clientSecret : undefined\n const scopes = Array.isArray(record.scopes)\n ? record.scopes.filter((value): value is string => typeof value === 'string')\n : undefined\n return {\n clientId,\n ...(clientSecret !== undefined ? { clientSecret } : {}),\n ...(scopes !== undefined ? { scopes } : {}),\n }\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,qCAAqC;AAyC9C,MAAM,4BAA4B;AAYlC,MAAM,oBAAoB,oBAAI,IAAuD;AAcrF,eAAsB,2BACpB,OACA,MAC2C;AAC3C,QAAM,MAAM,MAAM,WAAW,MAAM;AAAA,EAAC;AACpC,MAAI,OAAO,MAAM,QAAQ,uBAAuB,YAAY;AAC1D,WAAO,EAAE,WAAW,OAAO,aAAa,MAAM,YAAY;AAAA,EAC5D;AAEA,QAAM,gBAAgB,MAAM,mBAAmB;AAC/C,MAAI,CAAC,MAAM,SAAS,CAAC,cAAc,MAAM,aAAa,aAAa,GAAG;AACpE,WAAO,EAAE,WAAW,OAAO,aAAa,MAAM,YAAY;AAAA,EAC5D;AAIA,QAAM,WAAW,kBAAkB,IAAI,MAAM,SAAS;AACtD,MAAI,SAAU,QAAO;AAErB,QAAM,qBAAqB,MAAM,QAAQ,mBAAmB,KAAK,MAAM,OAAO;AAC9E,QAAM,iBAAiB,WAAW,OAAO,oBAAoB,MAAM,GAAG,EAAE,QAAQ,MAAM;AACpF,sBAAkB,OAAO,MAAM,SAAS;AAAA,EAC1C,CAAC;AACD,oBAAkB,IAAI,MAAM,WAAW,cAAc;AACrD,SAAO;AACT;AAEA,eAAe,WACb,OACA,oBACA,MACA,KAC2C;AAM3C,MAAI;AACJ,MAAI,MAAM,oBAAoB;AAC5B,QAAI;AACF,YAAM,MAAM,MAAM;AAAA,QAChB,KAAK;AAAA,QACL,MAAM,QAAQ;AAAA,QACd,EAAE,UAAU,MAAM,MAAM,UAAU,gBAAgB,MAAM,MAAM,eAAe;AAAA,MAC/E;AACA,oBAAc,qBAAqB,GAAG;AAAA,IACxC,SAAS,YAAY;AACnB;AAAA,QACE;AAAA,QACA,sBAAsB,QAAQ,WAAW,UAAU;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,mBAAmB;AAAA,MAChC,WAAW,MAAM;AAAA,MACjB,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,uDAAuD,eAAe,QAAQ,IAAI,UAAU,GAAG;AAEnG,WAAO,EAAE,WAAW,OAAO,aAAa,MAAM,YAAY;AAAA,EAC5D;AAEA,QAAM,OAAO,QAAQ,eAAe,MAAM;AAC1C,MAAI,MAAM,oBAAoB,MAAM;AAClC,QAAI;AACF,YAAM,KAAK,mBAAmB;AAAA,QAC5B,WAAW,MAAM,QAAQ,WAAW;AAAA,QACpC,OAAO,YAAY,EAAE,GAAG,MAAM,WAAW,OAAO,UAAU,YAAY,EAAE,IAAI;AAAA,QAC5E,MAAM;AAAA,MACR;AAAA,IACF,SAAS,SAAS;AAChB,UAAI,qEAAqE,mBAAmB,QAAQ,QAAQ,UAAU,OAAO;AAAA,IAC/H;AAAA,EACF;AAEA,SAAO,EAAE,WAAW,MAAM,aAAa,KAAK;AAC9C;AAEA,SAAS,cAAc,aAAsC,UAA2B;AACtF,QAAM,eAAe,aAAa;AAClC,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,YAAY,eAAe,YAAY;AAC7C,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,UAAU,QAAQ,IAAI,KAAK,IAAI,KAAK;AAC7C;AAEA,SAAS,eAAe,KAA2B;AACjD,MAAI,eAAe,KAAM,QAAO,OAAO,SAAS,IAAI,QAAQ,CAAC,IAAI,MAAM;AACvE,MAAI,OAAO,QAAQ,YAAY,OAAO,QAAQ,UAAU;AACtD,UAAM,OAAO,IAAI,KAAK,GAAG;AACzB,WAAO,OAAO,SAAS,KAAK,QAAQ,CAAC,IAAI,OAAO;AAAA,EAClD;AACA,SAAO;AACT;AAQA,SAAS,qBAAqB,KAA6C;AACzE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,SAAS;AACf,QAAM,WAAW,OAAO,OAAO,aAAa,WAAW,OAAO,WAAW;AACzE,MAAI,CAAC,SAAU,QAAO;AACtB,QAAM,eACJ,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;AAClE,QAAM,SAAS,MAAM,QAAQ,OAAO,MAAM,IACtC,OAAO,OAAO,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,IAC1E;AACJ,SAAO;AAAA,IACL;AAAA,IACA,GAAI,iBAAiB,SAAY,EAAE,aAAa,IAAI,CAAC;AAAA,IACrD,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;AAAA,EAC3C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { ChannelIngestDeadLetter } from "../data/entities.js";
|
|
3
|
+
const DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32768;
|
|
4
|
+
function extractExternalUid(message) {
|
|
5
|
+
const meta = message.channelMetadata;
|
|
6
|
+
if (meta && typeof meta.uid === "string") return meta.uid;
|
|
7
|
+
if (meta && typeof meta.uid === "number") return String(meta.uid);
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function truncateRawBody(message) {
|
|
11
|
+
const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? "", 10);
|
|
12
|
+
const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT;
|
|
13
|
+
const body = message.body ?? "";
|
|
14
|
+
if (body.length === 0) return null;
|
|
15
|
+
const buf = Buffer.from(body, "utf-8");
|
|
16
|
+
if (buf.byteLength <= cap) return body;
|
|
17
|
+
return `${buf.subarray(0, cap).toString("utf-8")}\u2026[truncated]`;
|
|
18
|
+
}
|
|
19
|
+
async function writeIngestDeadLetter(args) {
|
|
20
|
+
const { em, scope, channel, message, err, errorMessage } = args;
|
|
21
|
+
const externalMessageId = message.externalMessageId ?? null;
|
|
22
|
+
try {
|
|
23
|
+
if (externalMessageId) {
|
|
24
|
+
const existing = await findOneWithDecryption(
|
|
25
|
+
em,
|
|
26
|
+
ChannelIngestDeadLetter,
|
|
27
|
+
{
|
|
28
|
+
tenantId: scope.tenantId,
|
|
29
|
+
organizationId: scope.organizationId ?? null,
|
|
30
|
+
channelId: channel.id,
|
|
31
|
+
externalMessageId
|
|
32
|
+
},
|
|
33
|
+
void 0,
|
|
34
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }
|
|
35
|
+
);
|
|
36
|
+
if (existing) return;
|
|
37
|
+
}
|
|
38
|
+
const deadLetter = em.create(ChannelIngestDeadLetter, {
|
|
39
|
+
tenantId: scope.tenantId,
|
|
40
|
+
organizationId: scope.organizationId ?? null,
|
|
41
|
+
channelId: channel.id,
|
|
42
|
+
providerKey: channel.providerKey,
|
|
43
|
+
externalUid: extractExternalUid(message),
|
|
44
|
+
externalMessageId,
|
|
45
|
+
errorClass: err instanceof Error ? err.name : "Error",
|
|
46
|
+
errorMessage,
|
|
47
|
+
rawBody: truncateRawBody(message)
|
|
48
|
+
});
|
|
49
|
+
em.persist(deadLetter);
|
|
50
|
+
await em.flush();
|
|
51
|
+
} catch (ddlErr) {
|
|
52
|
+
console.error(
|
|
53
|
+
`[communication_channels:dead-letter] failed to record dead-letter for channel ${channel.id}: ${ddlErr instanceof Error ? ddlErr.message : String(ddlErr)}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
extractExternalUid,
|
|
59
|
+
truncateRawBody,
|
|
60
|
+
writeIngestDeadLetter
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=dead-letter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/dead-letter.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelIngestDeadLetter } from '../data/entities'\nimport type { NormalizedInboundMessage } from './adapter'\n\nconst DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32_768\n\nexport function extractExternalUid(message: NormalizedInboundMessage): string | null {\n const meta = message.channelMetadata as Record<string, unknown> | undefined\n if (meta && typeof meta.uid === 'string') return meta.uid\n if (meta && typeof meta.uid === 'number') return String(meta.uid)\n return null\n}\n\n/**\n * First N *bytes* of the raw body, for dead-letter forensics. Counts bytes (not\n * UTF-16 code units) so a multi-byte body cannot blow past the intended cap.\n */\nexport function truncateRawBody(message: NormalizedInboundMessage): string | null {\n const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? '', 10)\n const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT\n const body = message.body ?? ''\n if (body.length === 0) return null\n const buf = Buffer.from(body, 'utf-8')\n if (buf.byteLength <= cap) return body\n return `${buf.subarray(0, cap).toString('utf-8')}\u2026[truncated]`\n}\n\nexport interface WriteIngestDeadLetterArgs {\n em: EntityManager\n scope: { tenantId: string; organizationId?: string | null }\n channel: { id: string; providerKey: string }\n message: NormalizedInboundMessage\n err: unknown\n errorMessage: string\n}\n\n/**\n * Persist a `ChannelIngestDeadLetter` row for a permanently-failed inbound\n * message so an operator can replay it later. Best-effort: a failure to write\n * the dead-letter is logged but never thrown, so a bad message cannot block the\n * caller from advancing its cursor.\n *\n * Idempotent on `(channelId, externalMessageId)`: a replayed page that fails the\n * same message again is a no-op, so the dead-letter table never accumulates\n * duplicate rows for the same poison message.\n */\nexport async function writeIngestDeadLetter(args: WriteIngestDeadLetterArgs): Promise<void> {\n const { em, scope, channel, message, err, errorMessage } = args\n const externalMessageId = message.externalMessageId ?? null\n try {\n if (externalMessageId) {\n const existing = await findOneWithDecryption(\n em,\n ChannelIngestDeadLetter,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n externalMessageId,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },\n )\n if (existing) return\n }\n const deadLetter = em.create(ChannelIngestDeadLetter, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n providerKey: channel.providerKey,\n externalUid: extractExternalUid(message),\n externalMessageId,\n errorClass: err instanceof Error ? err.name : 'Error',\n errorMessage,\n rawBody: truncateRawBody(message),\n })\n em.persist(deadLetter)\n await em.flush()\n } catch (ddlErr) {\n console.error(\n `[communication_channels:dead-letter] failed to record dead-letter for channel ${channel.id}: ${\n ddlErr instanceof Error ? ddlErr.message : String(ddlErr)\n }`,\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AAGxC,MAAM,yCAAyC;AAExC,SAAS,mBAAmB,SAAkD;AACnF,QAAM,OAAO,QAAQ;AACrB,MAAI,QAAQ,OAAO,KAAK,QAAQ,SAAU,QAAO,KAAK;AACtD,MAAI,QAAQ,OAAO,KAAK,QAAQ,SAAU,QAAO,OAAO,KAAK,GAAG;AAChE,SAAO;AACT;AAMO,SAAS,gBAAgB,SAAkD;AAChF,QAAM,SAAS,OAAO,SAAS,QAAQ,IAAI,6CAA6C,IAAI,EAAE;AAC9F,QAAM,MAAM,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC7D,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,MAAM,OAAO,KAAK,MAAM,OAAO;AACrC,MAAI,IAAI,cAAc,IAAK,QAAO;AAClC,SAAO,GAAG,IAAI,SAAS,GAAG,GAAG,EAAE,SAAS,OAAO,CAAC;AAClD;AAqBA,eAAsB,sBAAsB,MAAgD;AAC1F,QAAM,EAAE,IAAI,OAAO,SAAS,SAAS,KAAK,aAAa,IAAI;AAC3D,QAAM,oBAAoB,QAAQ,qBAAqB;AACvD,MAAI;AACF,QAAI,mBAAmB;AACrB,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,UACE,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM,kBAAkB;AAAA,UACxC,WAAW,QAAQ;AAAA,UACnB;AAAA,QACF;AAAA,QACA;AAAA,QACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,kBAAkB,KAAK;AAAA,MAC3E;AACA,UAAI,SAAU;AAAA,IAChB;AACA,UAAM,aAAa,GAAG,OAAO,yBAAyB;AAAA,MACpD,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,WAAW,QAAQ;AAAA,MACnB,aAAa,QAAQ;AAAA,MACrB,aAAa,mBAAmB,OAAO;AAAA,MACvC;AAAA,MACA,YAAY,eAAe,QAAQ,IAAI,OAAO;AAAA,MAC9C;AAAA,MACA,SAAS,gBAAgB,OAAO;AAAA,IAClC,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAAA,EACjB,SAAS,QAAQ;AACf,YAAQ;AAAA,MACN,iFAAiF,QAAQ,EAAE,KACzF,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,CAC1D;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const EMAIL_MAX_ATTACHMENT_BYTES = 25e6;
|
|
2
|
+
const baseEmailCapabilities = {
|
|
3
|
+
// Core
|
|
4
|
+
threading: true,
|
|
5
|
+
richText: true,
|
|
6
|
+
fileSharing: false,
|
|
7
|
+
maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,
|
|
8
|
+
supportedMimeTypes: [
|
|
9
|
+
"image/png",
|
|
10
|
+
"image/jpeg",
|
|
11
|
+
"image/gif",
|
|
12
|
+
"image/webp",
|
|
13
|
+
"application/pdf",
|
|
14
|
+
"application/zip",
|
|
15
|
+
"application/octet-stream",
|
|
16
|
+
"text/plain",
|
|
17
|
+
"text/html",
|
|
18
|
+
"text/csv"
|
|
19
|
+
],
|
|
20
|
+
readReceipts: false,
|
|
21
|
+
deliveryReceipts: false,
|
|
22
|
+
typingIndicators: false,
|
|
23
|
+
// Extended
|
|
24
|
+
reactions: false,
|
|
25
|
+
multiReactionPerUser: false,
|
|
26
|
+
editMessage: false,
|
|
27
|
+
deleteMessage: false,
|
|
28
|
+
presence: false,
|
|
29
|
+
richBlocks: false,
|
|
30
|
+
interactiveComponents: false,
|
|
31
|
+
inlineImages: true,
|
|
32
|
+
conversationHistory: true,
|
|
33
|
+
contactCards: false,
|
|
34
|
+
locationSharing: false,
|
|
35
|
+
voiceNotes: false,
|
|
36
|
+
stickers: false,
|
|
37
|
+
// Body formats
|
|
38
|
+
supportedBodyFormats: ["text", "html"],
|
|
39
|
+
maxBodyLength: 5e6,
|
|
40
|
+
// Polling (real-time push deferred to v2 for all email providers)
|
|
41
|
+
realtimePush: false
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
EMAIL_MAX_ATTACHMENT_BYTES,
|
|
45
|
+
baseEmailCapabilities
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=email-capabilities.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/email-capabilities.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ChannelCapabilities } from './adapter'\n\n/**\n * Shared default attachment ceiling for email providers (Gmail allows\n * more, but larger uploads need resumable/upload-session APIs we don't use, so\n * all providers cap at the same conservative value).\n */\nexport const EMAIL_MAX_ATTACHMENT_BYTES = 25_000_000\n\n/**\n * Baseline capability profile shared by every email channel provider. Providers\n * spread this and override only what genuinely differs (e.g. `deleteMessage`).\n *\n * `fileSharing: false` until a provider's `convertOutbound` stitches attachment\n * bytes into the sent message \u2014 advertising `true` would silently drop bytes.\n */\nexport const baseEmailCapabilities: ChannelCapabilities = {\n // Core\n threading: true,\n richText: true,\n fileSharing: false,\n maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,\n supportedMimeTypes: [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'application/zip',\n 'application/octet-stream',\n 'text/plain',\n 'text/html',\n 'text/csv',\n ],\n readReceipts: false,\n deliveryReceipts: false,\n typingIndicators: false,\n\n // Extended\n reactions: false,\n multiReactionPerUser: false,\n editMessage: false,\n deleteMessage: false,\n presence: false,\n richBlocks: false,\n interactiveComponents: false,\n inlineImages: true,\n conversationHistory: true,\n contactCards: false,\n locationSharing: false,\n voiceNotes: false,\n stickers: false,\n\n // Body formats\n supportedBodyFormats: ['text', 'html'],\n maxBodyLength: 5_000_000,\n\n // Polling (real-time push deferred to v2 for all email providers)\n realtimePush: false,\n}\n"],
|
|
5
|
+
"mappings": "AAOO,MAAM,6BAA6B;AASnC,MAAM,wBAA6C;AAAA;AAAA,EAExD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,aAAa;AAAA,EACb,aAAa;AAAA,EACb,oBAAoB;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,kBAAkB;AAAA;AAAA,EAGlB,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,uBAAuB;AAAA,EACvB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,sBAAsB,CAAC,QAAQ,MAAM;AAAA,EACrC,eAAe;AAAA;AAAA,EAGf,cAAc;AAChB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
async function emailResolveContact(input) {
|
|
2
|
+
if (!input.senderIdentifier) return null;
|
|
3
|
+
if (input.senderIdentifier.includes("@")) {
|
|
4
|
+
return {
|
|
5
|
+
email: input.senderIdentifier,
|
|
6
|
+
displayName: input.senderDisplayName
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
emailResolveContact
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=email-contact.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/email-contact.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ContactHint, ResolveContactInput } from './adapter'\n\n/**\n * Shared `resolveContact` for email providers. An email sender identifier *is*\n * the contact email, so the hint is a direct passthrough; non-email identifiers\n * (or empty senders) yield `null` and the hub falls back to its own resolution.\n */\nexport async function emailResolveContact(input: ResolveContactInput): Promise<ContactHint | null> {\n if (!input.senderIdentifier) return null\n if (input.senderIdentifier.includes('@')) {\n return {\n email: input.senderIdentifier,\n displayName: input.senderDisplayName,\n }\n }\n return null\n}\n"],
|
|
5
|
+
"mappings": "AAOA,eAAsB,oBAAoB,OAAyD;AACjG,MAAI,CAAC,MAAM,iBAAkB,QAAO;AACpC,MAAI,MAAM,iBAAiB,SAAS,GAAG,GAAG;AACxC,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MACb,aAAa,MAAM;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|