@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,114 @@
|
|
|
1
|
+
import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
2
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import type { ChannelAdapterRegistry } from '../lib/registry'
|
|
4
|
+
import type { InboundMessage, NormalizedInboundMessage } from '../lib/adapter'
|
|
5
|
+
import {
|
|
6
|
+
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
7
|
+
type IngestInboundMessageInput,
|
|
8
|
+
type IngestInboundMessageResult,
|
|
9
|
+
} from '../commands/ingest-inbound-message'
|
|
10
|
+
import { COMMUNICATION_CHANNELS_QUEUES } from '../lib/queue'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Job payload enqueued by the webhook route after successful signature verification.
|
|
14
|
+
*
|
|
15
|
+
* The route does the cheap signature-verification step synchronously and immediately
|
|
16
|
+
* returns 200. Normalization (`adapter.normalizeInbound`) + DB writes run here, in
|
|
17
|
+
* the worker, so a slow database or a long contact-resolution path can't time-out
|
|
18
|
+
* the provider's webhook.
|
|
19
|
+
*/
|
|
20
|
+
export type InboundProcessorPayload = {
|
|
21
|
+
providerKey: string
|
|
22
|
+
channelId: string
|
|
23
|
+
channelType: string
|
|
24
|
+
raw: InboundMessage
|
|
25
|
+
scope: {
|
|
26
|
+
tenantId: string
|
|
27
|
+
organizationId: string | null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const metadata: WorkerMeta = {
|
|
32
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.inbound,
|
|
33
|
+
id: 'communication_channels:inbound-processor',
|
|
34
|
+
concurrency: 10,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type HandlerContext = JobContext & {
|
|
38
|
+
resolve: <T = unknown>(name: string) => T
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Inbound channel message processor.
|
|
43
|
+
*
|
|
44
|
+
* Per SPEC-045d §6:
|
|
45
|
+
* - Resolves the channel adapter for the inbound providerKey.
|
|
46
|
+
* - Calls `adapter.normalizeInbound(raw)` to produce a `NormalizedInboundMessage`.
|
|
47
|
+
* - Hands off to the `communication_channels.message.ingest_inbound` command,
|
|
48
|
+
* which idempotently creates ExternalConversation, ChannelThreadMapping,
|
|
49
|
+
* ExternalMessage, MessageChannelLink, and composes the platform Message.
|
|
50
|
+
*
|
|
51
|
+
* Idempotency: handled by the command (dedup on `(channel_id, external_message_id)`).
|
|
52
|
+
* The worker can safely retry on transient failures.
|
|
53
|
+
*/
|
|
54
|
+
export default async function handle(
|
|
55
|
+
job: QueuedJob<InboundProcessorPayload>,
|
|
56
|
+
ctx: HandlerContext,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const { providerKey, channelId, channelType, raw, scope } = job.payload
|
|
59
|
+
|
|
60
|
+
const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')
|
|
61
|
+
const adapter = adapterRegistry?.get(providerKey)
|
|
62
|
+
if (!adapter) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`No ChannelAdapter registered for providerKey '${providerKey}' (worker job ${job.id}). ` +
|
|
65
|
+
'Check that the provider package is enabled in modules.ts.',
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Normalize the raw inbound payload into the hub's canonical shape.
|
|
70
|
+
const normalized: NormalizedInboundMessage = await adapter.normalizeInbound(raw)
|
|
71
|
+
if (!normalized?.externalMessageId || !normalized?.externalConversationId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Adapter '${providerKey}' returned a normalized message missing required fields ` +
|
|
74
|
+
`(externalMessageId, externalConversationId) for worker job ${job.id}`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const commandBus = ctx.resolve<CommandBus>('commandBus')
|
|
79
|
+
|
|
80
|
+
// The worker's JobContext exposes `.resolve(name)` — duck-type-compatible with the
|
|
81
|
+
// shape CommandBus uses (`container.resolve(...)`). We cast to AwilixContainer because
|
|
82
|
+
// CommandRuntimeContext requires that exact type; in practice CommandBus only touches
|
|
83
|
+
// `.resolve`, mirroring the pattern in `inbox_ops/lib/messagesIntegration.ts:148`.
|
|
84
|
+
const containerProxy = { resolve: ctx.resolve.bind(ctx) }
|
|
85
|
+
const commandCtx = {
|
|
86
|
+
container: containerProxy as never,
|
|
87
|
+
auth: null,
|
|
88
|
+
organizationScope: null,
|
|
89
|
+
selectedOrganizationId: scope.organizationId ?? null,
|
|
90
|
+
organizationIds: scope.organizationId ? [scope.organizationId] : null,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const input: IngestInboundMessageInput = {
|
|
94
|
+
channelId,
|
|
95
|
+
providerKey,
|
|
96
|
+
channelType,
|
|
97
|
+
scope,
|
|
98
|
+
message: normalized,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { result } = await commandBus.execute<IngestInboundMessageInput, IngestInboundMessageResult>(
|
|
102
|
+
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
103
|
+
{
|
|
104
|
+
input,
|
|
105
|
+
ctx: commandCtx as never,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if (result.status === 'duplicate') {
|
|
110
|
+
// Idempotent skip — provider sent the same webhook twice (common with at-least-once
|
|
111
|
+
// delivery semantics). Nothing to do; we already ingested this message earlier.
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
2
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import {
|
|
4
|
+
COMMUNICATION_CHANNELS_DELIVER_OUTBOUND_COMMAND_ID,
|
|
5
|
+
type DeliverOutboundMessageInput,
|
|
6
|
+
type DeliverOutboundMessageResult,
|
|
7
|
+
} from '../commands/deliver-outbound-message'
|
|
8
|
+
import { computeBackoffMs } from '../lib/error-classification'
|
|
9
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Worker payload — the subscriber enqueues `{ messageId, scope, attempt? }`.
|
|
13
|
+
*
|
|
14
|
+
* `attempt` starts at 1 on first enqueue. The worker increments it when
|
|
15
|
+
* re-enqueueing for retry, until `MAX_ATTEMPTS` is reached.
|
|
16
|
+
*/
|
|
17
|
+
export type OutboundDeliveryPayload = {
|
|
18
|
+
messageId: string
|
|
19
|
+
scope: {
|
|
20
|
+
tenantId: string
|
|
21
|
+
organizationId: string | null
|
|
22
|
+
}
|
|
23
|
+
/** Attempt number, 1-based. Set by the subscriber for the first try; the worker increments on retry. */
|
|
24
|
+
attempt?: number
|
|
25
|
+
/** Force credential refresh on this attempt — used after a 401 from the provider. */
|
|
26
|
+
forceCredentialRefresh?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const OUTBOUND_DELIVERY_MAX_ATTEMPTS = 3
|
|
30
|
+
|
|
31
|
+
export const metadata: WorkerMeta = {
|
|
32
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.outbound,
|
|
33
|
+
id: 'communication_channels:outbound-delivery',
|
|
34
|
+
concurrency: 10,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type HandlerContext = JobContext & {
|
|
38
|
+
resolve: <T = unknown>(name: string) => T
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Outbound delivery worker.
|
|
43
|
+
*
|
|
44
|
+
* Dispatches the `deliver_outbound_message` command which performs the actual
|
|
45
|
+
* send. The command returns a classified result:
|
|
46
|
+
* - `delivered` / `already_delivered` / `no_channel_link` → success, return.
|
|
47
|
+
* - `failed` with `transient: true` → re-enqueue with exponential backoff,
|
|
48
|
+
* incremented attempt, up to `OUTBOUND_DELIVERY_MAX_ATTEMPTS`.
|
|
49
|
+
* - `failed` with `transient: false` → permanent failure, no retry.
|
|
50
|
+
*
|
|
51
|
+
* The command already wrote the failure record + emitted `.delivery_failed`,
|
|
52
|
+
* so the worker just decides whether to schedule another attempt.
|
|
53
|
+
*
|
|
54
|
+
* We DO NOT throw on a recorded delivery-failure outcome — that would let the
|
|
55
|
+
* queue apply its own retry policy on top of ours, double-retrying. Explicit
|
|
56
|
+
* re-enqueue with delayMs is the portable, controllable pattern. The one
|
|
57
|
+
* exception is an *unexpected* exception from the command itself (e.g. a DB blip
|
|
58
|
+
* that stopped it from recording anything): we re-enqueue up to our max and then
|
|
59
|
+
* rethrow so the infrastructure failure surfaces to the queue's dead-letter
|
|
60
|
+
* instead of vanishing. The command's idempotency prevents a double-send.
|
|
61
|
+
*/
|
|
62
|
+
export default async function handle(
|
|
63
|
+
job: QueuedJob<OutboundDeliveryPayload>,
|
|
64
|
+
ctx: HandlerContext,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const { messageId, scope, attempt = 1, forceCredentialRefresh } = job.payload
|
|
67
|
+
|
|
68
|
+
const commandBus = ctx.resolve<CommandBus>('commandBus')
|
|
69
|
+
const containerProxy = { resolve: ctx.resolve.bind(ctx) }
|
|
70
|
+
const commandCtx = {
|
|
71
|
+
container: containerProxy as never,
|
|
72
|
+
auth: null,
|
|
73
|
+
organizationScope: null,
|
|
74
|
+
selectedOrganizationId: scope.organizationId ?? null,
|
|
75
|
+
organizationIds: scope.organizationId ? [scope.organizationId] : null,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let outcome: DeliverOutboundMessageResult
|
|
79
|
+
try {
|
|
80
|
+
const { result } = await commandBus.execute<
|
|
81
|
+
DeliverOutboundMessageInput,
|
|
82
|
+
DeliverOutboundMessageResult
|
|
83
|
+
>(
|
|
84
|
+
COMMUNICATION_CHANNELS_DELIVER_OUTBOUND_COMMAND_ID,
|
|
85
|
+
{
|
|
86
|
+
input: { messageId, scope, forceCredentialRefresh },
|
|
87
|
+
ctx: commandCtx as never,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
outcome = result
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Unexpected error inside the command itself (e.g. DB blip). Re-enqueue
|
|
93
|
+
// up to MAX_ATTEMPTS so we don't lose deliveries due to infrastructure flakes.
|
|
94
|
+
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
95
|
+
console.warn(
|
|
96
|
+
`[communication_channels:outbound-delivery] command threw on attempt ${attempt} for message ${messageId}: ${errorMessage}`,
|
|
97
|
+
)
|
|
98
|
+
if (attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS) {
|
|
99
|
+
await reenqueue(job.payload, attempt)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
// Attempts exhausted on an unexpected command exception — rethrow so the
|
|
103
|
+
// failure reaches the queue's dead-letter / observability (see header note).
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
switch (outcome.status) {
|
|
108
|
+
case 'delivered':
|
|
109
|
+
case 'already_delivered':
|
|
110
|
+
case 'no_channel_link':
|
|
111
|
+
return
|
|
112
|
+
case 'failed': {
|
|
113
|
+
// Reauth (401 / invalid_grant): the command already flipped the channel to
|
|
114
|
+
// `requires_reauth`. Give the credentials exactly one forced-refresh retry
|
|
115
|
+
// before giving up — a near-expiry access token whose proactive refresh
|
|
116
|
+
// was skipped can still recover here. If we already forced a refresh and
|
|
117
|
+
// still got a reauth error, the token is unrecoverable: stop (the operator
|
|
118
|
+
// must reconnect).
|
|
119
|
+
if (
|
|
120
|
+
outcome.requiresReauth &&
|
|
121
|
+
!forceCredentialRefresh &&
|
|
122
|
+
attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS
|
|
123
|
+
) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[communication_channels:outbound-delivery] reauth failure on attempt ${attempt} for message ${messageId} (${outcome.providerKey}): ${outcome.error}. Retrying once with a forced credential refresh.`,
|
|
126
|
+
)
|
|
127
|
+
await reenqueue({ ...job.payload, forceCredentialRefresh: true }, attempt)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (outcome.transient && attempt < OUTBOUND_DELIVERY_MAX_ATTEMPTS) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[communication_channels:outbound-delivery] transient failure on attempt ${attempt} for message ${messageId} (${outcome.providerKey}): ${outcome.error}. Re-enqueueing.`,
|
|
133
|
+
)
|
|
134
|
+
await reenqueue(job.payload, attempt)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
// Permanent or attempts exhausted — `.delivery_failed` was already emitted
|
|
138
|
+
// by the command, so we stop here.
|
|
139
|
+
console.error(
|
|
140
|
+
`[communication_channels:outbound-delivery] giving up on message ${messageId} after attempt ${attempt} (${outcome.providerKey}): ${outcome.error}`,
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function reenqueue(payload: OutboundDeliveryPayload, attempt: number): Promise<void> {
|
|
148
|
+
const next: OutboundDeliveryPayload = {
|
|
149
|
+
...payload,
|
|
150
|
+
attempt: attempt + 1,
|
|
151
|
+
}
|
|
152
|
+
const delayMs = computeBackoffMs(attempt)
|
|
153
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)
|
|
154
|
+
await queue.enqueue(next as unknown as Record<string, unknown>, { delayMs })
|
|
155
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'
|
|
3
|
+
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
4
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { CommunicationChannel } from '../data/entities'
|
|
6
|
+
import {
|
|
7
|
+
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
8
|
+
type IngestInboundMessageInput,
|
|
9
|
+
} from '../commands/ingest-inbound-message'
|
|
10
|
+
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'
|
|
11
|
+
import { preservePushState } from '../lib/push-state'
|
|
12
|
+
import { writeIngestDeadLetter } from '../lib/dead-letter'
|
|
13
|
+
import { classifyOutboundError, computeBackoffMs, isReauthError } from '../lib/error-classification'
|
|
14
|
+
import { refreshCredentialsIfNeeded } from '../lib/credential-refresh'
|
|
15
|
+
import { emitCommunicationChannelsEvent } from '../events'
|
|
16
|
+
import type { ChannelAdapterRegistry } from '../lib/registry'
|
|
17
|
+
import type { NormalizedInboundMessage } from '../lib/adapter'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Job payload for the `communication-channels-poll` queue.
|
|
21
|
+
*
|
|
22
|
+
* One job per channel per scheduler tick. The poll-tick worker (sibling file)
|
|
23
|
+
* enumerates due channels and enqueues these jobs.
|
|
24
|
+
*/
|
|
25
|
+
export type PollChannelJobPayload = {
|
|
26
|
+
channelId: string
|
|
27
|
+
scope: {
|
|
28
|
+
tenantId: string
|
|
29
|
+
organizationId: string | null
|
|
30
|
+
}
|
|
31
|
+
/** Attempt count, 1-based; used for retry-backoff decisions. */
|
|
32
|
+
attempt?: number
|
|
33
|
+
/** Self-re-enqueue drain counter (bounds the multi-page `hasMore` drain loop). */
|
|
34
|
+
drainPage?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const POLL_CHANNEL_MAX_ATTEMPTS = 3
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hard cap on `hasMore` self-re-enqueue drain pages — guards against an adapter
|
|
41
|
+
* that returns `hasMore: true` with a non-advancing (pinned) cursor, which would
|
|
42
|
+
* otherwise spin a tight, unthrottled re-enqueue loop. Mirrors the same guard in
|
|
43
|
+
* `gmail-history-sync`.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_DRAIN_PAGES = 100
|
|
46
|
+
|
|
47
|
+
export const metadata: WorkerMeta = {
|
|
48
|
+
queue: COMMUNICATION_CHANNELS_QUEUES.poll,
|
|
49
|
+
id: 'communication_channels:poll-channel',
|
|
50
|
+
concurrency: 10,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type HandlerContext = JobContext & {
|
|
54
|
+
resolve: <T = unknown>(name: string) => T
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type CredentialsServiceLike = {
|
|
58
|
+
resolve: (
|
|
59
|
+
integrationId: string,
|
|
60
|
+
scope: { organizationId: string; tenantId: string; userId?: string | null },
|
|
61
|
+
) => Promise<Record<string, unknown> | null>
|
|
62
|
+
save?: (
|
|
63
|
+
integrationId: string,
|
|
64
|
+
credentials: Record<string, unknown>,
|
|
65
|
+
scope: { organizationId: string; tenantId: string; userId?: string | null },
|
|
66
|
+
) => Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Poll a single channel for inbound messages.
|
|
71
|
+
*
|
|
72
|
+
* Per SPEC-045d § 6 (with email-spec § Hub Deltas → Delta 6 polling extensions):
|
|
73
|
+
* 1. Load the channel; skip when `is_active === false` or `status !== 'connected'`.
|
|
74
|
+
* 2. Skip when the adapter declares `realtimePush !== false` (it doesn't want polling).
|
|
75
|
+
* 3. Refresh credentials if OAuth and within the expiry window.
|
|
76
|
+
* 4. Call `adapter.fetchHistory({ channelId, credentials, since: lastPolledAt })`.
|
|
77
|
+
* 5. For each normalized message, dispatch `ingest_inbound_message` (idempotent on
|
|
78
|
+
* `(channel_id, external_message_id)`).
|
|
79
|
+
* 6. Update `channel.lastPolledAt = NOW()` on success.
|
|
80
|
+
* 7. On error: classify, set `channel.status` accordingly, set `last_error`, emit
|
|
81
|
+
* `channel.requires_reauth` on 401, retry transient failures up to MAX.
|
|
82
|
+
*
|
|
83
|
+
* The hub doesn't drain a remote mailbox indefinitely — `fetchHistory` returns a
|
|
84
|
+
* single page (provider decides the size). If the provider has more, the next tick
|
|
85
|
+
* picks it up after the configured `poll_interval_seconds`.
|
|
86
|
+
*/
|
|
87
|
+
export default async function handle(
|
|
88
|
+
job: QueuedJob<PollChannelJobPayload>,
|
|
89
|
+
ctx: HandlerContext,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload
|
|
92
|
+
const em = (ctx.resolve('em') as EntityManager).fork()
|
|
93
|
+
const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')
|
|
94
|
+
|
|
95
|
+
const channel = await findOneWithDecryption(
|
|
96
|
+
em,
|
|
97
|
+
CommunicationChannel,
|
|
98
|
+
{
|
|
99
|
+
id: channelId,
|
|
100
|
+
tenantId: scope.tenantId,
|
|
101
|
+
organizationId: scope.organizationId ?? null,
|
|
102
|
+
deletedAt: null,
|
|
103
|
+
},
|
|
104
|
+
undefined,
|
|
105
|
+
scope,
|
|
106
|
+
)
|
|
107
|
+
if (!channel) {
|
|
108
|
+
console.warn(
|
|
109
|
+
`[communication_channels:poll-channel] channel ${channelId} not found (skipping)`,
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (!channel.isActive) return
|
|
114
|
+
// Allow `connected` (normal poll) and `error` (Spec B § B5 auto-recovery
|
|
115
|
+
// sweep enqueues these intentionally — on a successful poll below we
|
|
116
|
+
// flip them back to `connected`). `requires_reauth` and `disconnected`
|
|
117
|
+
// are owned by the credential-refresh and disconnect flows.
|
|
118
|
+
if (channel.status !== 'connected' && channel.status !== 'error') return
|
|
119
|
+
|
|
120
|
+
const adapter = adapterRegistry?.get(channel.providerKey)
|
|
121
|
+
if (!adapter) {
|
|
122
|
+
console.warn(
|
|
123
|
+
`[communication_channels:poll-channel] no adapter for provider '${channel.providerKey}' (channel ${channelId})`,
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
// Adapter opted out of polling — webhook providers.
|
|
128
|
+
const capabilities = (channel.capabilities as { realtimePush?: boolean } | null) ?? null
|
|
129
|
+
if (capabilities?.realtimePush !== false) {
|
|
130
|
+
// realtimePush is `true` (default for back-compat) — don't poll push providers.
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (typeof adapter.fetchHistory !== 'function') {
|
|
134
|
+
// Adapter doesn't implement history fetching — nothing we can do.
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Credentials.
|
|
139
|
+
let credentialsService: CredentialsServiceLike | null = null
|
|
140
|
+
try {
|
|
141
|
+
credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')
|
|
142
|
+
} catch {
|
|
143
|
+
credentialsService = null
|
|
144
|
+
}
|
|
145
|
+
// Per-user credentials scope: pass `channel.userId` so the credentials
|
|
146
|
+
// service returns this user's row, not whoever connected the provider last.
|
|
147
|
+
// See review R2-C1 / N1 (2026-05-26).
|
|
148
|
+
const credentialsScope = {
|
|
149
|
+
tenantId: scope.tenantId,
|
|
150
|
+
organizationId: scope.organizationId ?? scope.tenantId,
|
|
151
|
+
userId: channel.userId ?? null,
|
|
152
|
+
}
|
|
153
|
+
let credentials: Record<string, unknown> = {}
|
|
154
|
+
if (channel.credentialsRef && credentialsService) {
|
|
155
|
+
try {
|
|
156
|
+
credentials =
|
|
157
|
+
(await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}
|
|
158
|
+
} catch {
|
|
159
|
+
credentials = {}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const refreshed = await refreshCredentialsIfNeeded(
|
|
163
|
+
{
|
|
164
|
+
adapter,
|
|
165
|
+
channelId: channel.id,
|
|
166
|
+
credentials,
|
|
167
|
+
scope: credentialsScope,
|
|
168
|
+
},
|
|
169
|
+
{ credentialsService },
|
|
170
|
+
)
|
|
171
|
+
credentials = refreshed.credentials
|
|
172
|
+
|
|
173
|
+
// Fetch a single page of history.
|
|
174
|
+
// `channelState` is the provider-specific resumption cursor — Gmail historyId,
|
|
175
|
+
// IMAP UIDVALIDITY+UIDNEXT, etc. We persist it across
|
|
176
|
+
// ticks on `channel.channelState` so each poll resumes from the prior one
|
|
177
|
+
// instead of running a full mailbox resync. Empty / NULL = "first poll;
|
|
178
|
+
// bootstrap the cursor from the provider".
|
|
179
|
+
let normalized: NormalizedInboundMessage[] = []
|
|
180
|
+
let nextCursor: string | undefined
|
|
181
|
+
let hasMore = false
|
|
182
|
+
try {
|
|
183
|
+
const result = await adapter.fetchHistory({
|
|
184
|
+
conversationId: channel.externalIdentifier ?? channel.id,
|
|
185
|
+
credentials,
|
|
186
|
+
cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : undefined,
|
|
187
|
+
channelState: (channel.channelState as Record<string, unknown> | null) ?? undefined,
|
|
188
|
+
scope: {
|
|
189
|
+
tenantId: scope.tenantId,
|
|
190
|
+
organizationId: scope.organizationId ?? scope.tenantId,
|
|
191
|
+
},
|
|
192
|
+
// `contactFilter.sinceDays` is a hint to provider adapters about how far
|
|
193
|
+
// back to look on first-poll bootstrap. For UID-incremental polls (every
|
|
194
|
+
// tick after the first), adapters ignore this and just fetch new mail
|
|
195
|
+
// since the persisted cursor.
|
|
196
|
+
contactFilter: { addresses: [], sinceDays: 7 },
|
|
197
|
+
})
|
|
198
|
+
normalized = Array.isArray(result?.messages) ? result.messages : []
|
|
199
|
+
nextCursor = result?.nextCursor
|
|
200
|
+
hasMore = result?.hasMore === true
|
|
201
|
+
} catch (err) {
|
|
202
|
+
await handlePollError(err, em, channel, scope, attempt, job.payload)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Dispatch ingest commands for each message.
|
|
207
|
+
const commandBus = ctx.resolve<CommandBus>('commandBus')
|
|
208
|
+
const containerProxy = { resolve: ctx.resolve.bind(ctx) }
|
|
209
|
+
const commandCtx = {
|
|
210
|
+
container: containerProxy as never,
|
|
211
|
+
auth: null,
|
|
212
|
+
organizationScope: null,
|
|
213
|
+
selectedOrganizationId: scope.organizationId ?? null,
|
|
214
|
+
organizationIds: scope.organizationId ? [scope.organizationId] : null,
|
|
215
|
+
}
|
|
216
|
+
// Spec B § Per-message commit + dead-letter:
|
|
217
|
+
// - Permanent failure (malformed MIME, schema, contract violation):
|
|
218
|
+
// write to channel_ingest_dead_letter, log, advance cursor anyway so
|
|
219
|
+
// the bad blob never stalls the channel again.
|
|
220
|
+
// - Transient failure (DB drop, network blip): abort the loop without
|
|
221
|
+
// advancing the cursor. The next tick re-fetches the same page;
|
|
222
|
+
// idempotency via the (channel_id, external_message_id) unique
|
|
223
|
+
// constraint means already-ingested messages no-op on the retry.
|
|
224
|
+
let transientIngestAbort = false
|
|
225
|
+
for (const message of normalized) {
|
|
226
|
+
try {
|
|
227
|
+
const input: IngestInboundMessageInput = {
|
|
228
|
+
channelId: channel.id,
|
|
229
|
+
providerKey: channel.providerKey,
|
|
230
|
+
channelType: channel.channelType,
|
|
231
|
+
scope: {
|
|
232
|
+
tenantId: scope.tenantId,
|
|
233
|
+
organizationId: scope.organizationId ?? null,
|
|
234
|
+
},
|
|
235
|
+
message,
|
|
236
|
+
}
|
|
237
|
+
await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {
|
|
238
|
+
input,
|
|
239
|
+
ctx: commandCtx as never,
|
|
240
|
+
})
|
|
241
|
+
} catch (err) {
|
|
242
|
+
const classification = classifyOutboundError(err)
|
|
243
|
+
if (classification.transient) {
|
|
244
|
+
console.warn(
|
|
245
|
+
`[communication_channels:poll-channel] transient ingest failure for channel ${channel.id}; aborting page so cursor is NOT advanced. Reason: ${classification.message}`,
|
|
246
|
+
)
|
|
247
|
+
transientIngestAbort = true
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
// Permanent — write to dead-letter so an operator can replay later.
|
|
251
|
+
// The shared helper is best-effort (never throws) and idempotent on
|
|
252
|
+
// `(channelId, externalMessageId)`, so a replayed page that fails the
|
|
253
|
+
// same message again does not insert a duplicate row.
|
|
254
|
+
await writeIngestDeadLetter({
|
|
255
|
+
em,
|
|
256
|
+
scope,
|
|
257
|
+
channel,
|
|
258
|
+
message,
|
|
259
|
+
err,
|
|
260
|
+
errorMessage: classification.message,
|
|
261
|
+
})
|
|
262
|
+
console.warn(
|
|
263
|
+
`[communication_channels:poll-channel] permanent ingest failure for channel ${channel.id}; recorded in dead-letter and advancing cursor past message ${message.externalMessageId}. Reason: ${classification.message}`,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Transient abort: keep prior cursor + lastPolledAt so the next tick
|
|
269
|
+
// re-fetches the same page (idempotent at the DB layer).
|
|
270
|
+
if (transientIngestAbort) {
|
|
271
|
+
channel.lastError = 'transient_ingest_failure'
|
|
272
|
+
await em.flush()
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update poll cursor + clear any stale error state.
|
|
277
|
+
channel.lastPolledAt = new Date()
|
|
278
|
+
if (channel.lastError) channel.lastError = null
|
|
279
|
+
// Recover the channel from any prior non-fatal error state. The previous
|
|
280
|
+
// poll(s) may have set status='error' after exhausting transient-retry
|
|
281
|
+
// attempts, but a fresh successful poll means the upstream is healthy
|
|
282
|
+
// again — flip it back to 'connected' so the scheduler keeps it in
|
|
283
|
+
// rotation and the user doesn't have to manually reconnect.
|
|
284
|
+
// We DON'T touch 'requires_reauth' here (that lifecycle state is owned
|
|
285
|
+
// by the credential-refresh / OAuth flow) or 'disconnected' (owned by
|
|
286
|
+
// the cascade-on-user-delete subscriber).
|
|
287
|
+
if (channel.status === 'error') {
|
|
288
|
+
channel.status = 'connected'
|
|
289
|
+
}
|
|
290
|
+
// Providers encode their cursor as base64-encoded JSON in `nextCursor`; we
|
|
291
|
+
// decode it back to an object so the next tick can pass it straight into
|
|
292
|
+
// `fetchHistory` as `channelState`. Decode failures fall back to the prior
|
|
293
|
+
// state (next tick bootstraps from there).
|
|
294
|
+
//
|
|
295
|
+
// Push-delivery state (Spec C) — watch/subscription identifiers and expiry — is
|
|
296
|
+
// owned by the push register/renew commands, not the sync cursor. A provider's
|
|
297
|
+
// `fetchHistory` returns only sync-cursor fields, so persisting the decoded
|
|
298
|
+
// cursor as a full replace would silently wipe push state and stop
|
|
299
|
+
// `gmail-renew-watch` from renewing it. Carry
|
|
300
|
+
// the hub-owned push keys forward whenever the new cursor omits them.
|
|
301
|
+
if (typeof nextCursor === 'string' && nextCursor.length > 0) {
|
|
302
|
+
const decoded = decodeChannelStateCursor(nextCursor)
|
|
303
|
+
if (decoded) {
|
|
304
|
+
channel.channelState = preservePushState(channel.channelState, decoded)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
await em.flush()
|
|
308
|
+
|
|
309
|
+
// Drain contract (review H1, 2026-05-26): adapters that have additional
|
|
310
|
+
// pages beyond this tick's batch (large mailboxes, mid-deltaLink walks,
|
|
311
|
+
// UID overflows) signal `hasMore: true`. The persisted `channelState`
|
|
312
|
+
// already encodes the mid-drain resumption token (Gmail `pendingHistoryPageToken`,
|
|
313
|
+
// IMAP non-terminal `uidNext`). Re-enqueue with
|
|
314
|
+
// a small delay so we keep draining without overrunning rate limits.
|
|
315
|
+
if (hasMore) {
|
|
316
|
+
// Bound the drain: an adapter that returns `hasMore: true` with a
|
|
317
|
+
// non-advancing cursor (e.g. a persistently-failing message that pins the
|
|
318
|
+
// Gmail cursor via `hardFailed`) must not spin an unthrottled loop. Stop
|
|
319
|
+
// at MAX_DRAIN_PAGES; the next scheduled poll tick re-checks the channel.
|
|
320
|
+
if (drainPage < MAX_DRAIN_PAGES) {
|
|
321
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)
|
|
322
|
+
await queue.enqueue(
|
|
323
|
+
{ channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 } as unknown as Record<string, unknown>,
|
|
324
|
+
{ delayMs: 250 },
|
|
325
|
+
)
|
|
326
|
+
} else {
|
|
327
|
+
console.warn(
|
|
328
|
+
`[communication_channels:poll-channel] drain page cap (${MAX_DRAIN_PAGES}) reached for channel ${channel.id}; stopping re-enqueue until the next scheduled tick`,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function decodeChannelStateCursor(cursor: string): Record<string, unknown> | null {
|
|
335
|
+
try {
|
|
336
|
+
const decoded = Buffer.from(cursor, 'base64').toString('utf8')
|
|
337
|
+
const parsed = JSON.parse(decoded) as unknown
|
|
338
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
339
|
+
return parsed as Record<string, unknown>
|
|
340
|
+
}
|
|
341
|
+
return null
|
|
342
|
+
} catch {
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function handlePollError(
|
|
348
|
+
err: unknown,
|
|
349
|
+
em: EntityManager,
|
|
350
|
+
channel: CommunicationChannel,
|
|
351
|
+
scope: PollChannelJobPayload['scope'],
|
|
352
|
+
attempt: number,
|
|
353
|
+
payload: PollChannelJobPayload,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
const classification = classifyOutboundError(err)
|
|
356
|
+
channel.lastError = classification.message
|
|
357
|
+
|
|
358
|
+
if (isReauthError(classification)) {
|
|
359
|
+
channel.status = 'requires_reauth'
|
|
360
|
+
await em.flush()
|
|
361
|
+
await emitCommunicationChannelsEvent(
|
|
362
|
+
'communication_channels.channel.requires_reauth',
|
|
363
|
+
{
|
|
364
|
+
channelId: channel.id,
|
|
365
|
+
providerKey: channel.providerKey,
|
|
366
|
+
channelType: channel.channelType,
|
|
367
|
+
reason: classification.message,
|
|
368
|
+
tenantId: scope.tenantId,
|
|
369
|
+
organizationId: scope.organizationId ?? null,
|
|
370
|
+
},
|
|
371
|
+
{ persistent: true },
|
|
372
|
+
)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {
|
|
377
|
+
// Transient error — re-enqueue with backoff. Channel status stays
|
|
378
|
+
// `connected` so the scheduler keeps it in rotation.
|
|
379
|
+
await em.flush()
|
|
380
|
+
const next: PollChannelJobPayload = { ...payload, attempt: attempt + 1 }
|
|
381
|
+
const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)
|
|
382
|
+
await queue.enqueue(next as unknown as Record<string, unknown>, {
|
|
383
|
+
delayMs: computeBackoffMs(attempt),
|
|
384
|
+
})
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Permanent or attempts exhausted — mark channel as error and stop the loop.
|
|
389
|
+
channel.status = 'error'
|
|
390
|
+
await em.flush()
|
|
391
|
+
}
|