@open-mercato/core 0.6.5-develop.4384.1.ce2ec6eaaa → 0.6.5-develop.4397.1.9a65481757
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/channel_ingest_dead_letter/index.js +25 -0
- package/dist/generated/entities/channel_ingest_dead_letter/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_mapping/index.js +25 -0
- package/dist/generated/entities/channel_thread_mapping/index.js.map +7 -0
- package/dist/generated/entities/channel_thread_token/index.js +17 -0
- package/dist/generated/entities/channel_thread_token/index.js.map +7 -0
- package/dist/generated/entities/communication_channel/index.js +43 -0
- package/dist/generated/entities/communication_channel/index.js.map +7 -0
- package/dist/generated/entities/customer_interaction/index.js +4 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entities/external_conversation/index.js +25 -0
- package/dist/generated/entities/external_conversation/index.js.map +7 -0
- package/dist/generated/entities/external_message/index.js +25 -0
- package/dist/generated/entities/external_message/index.js.map +7 -0
- package/dist/generated/entities/integration_credentials/index.js +3 -1
- package/dist/generated/entities/integration_credentials/index.js.map +2 -2
- package/dist/generated/entities/message/index.js +2 -0
- package/dist/generated/entities/message/index.js.map +2 -2
- package/dist/generated/entities/message_channel_link/index.js +33 -0
- package/dist/generated/entities/message_channel_link/index.js.map +7 -0
- package/dist/generated/entities/message_reaction/index.js +25 -0
- package/dist/generated/entities/message_reaction/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +11 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +117 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/authFixtures.js +2 -1
- package/dist/helpers/integration/authFixtures.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +58 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +7 -0
- package/dist/modules/communication_channels/acl.js +47 -0
- package/dist/modules/communication_channels/acl.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js +113 -0
- package/dist/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +138 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +93 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/route.js +96 -0
- package/dist/modules/communication_channels/api/get/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js +82 -0
- package/dist/modules/communication_channels/api/get/me/channels/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +274 -0
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +168 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +143 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +127 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js +99 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/set-primary/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +197 -0
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +124 -0
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js +120 -0
- package/dist/modules/communication_channels/api/post/messages/[messageId]/reactions/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +157 -0
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js +115 -0
- package/dist/modules/communication_channels/api/post/send-as-user/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js +217 -0
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +175 -0
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +123 -0
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +7 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js +117 -0
- package/dist/modules/communication_channels/api/put/threads/[threadId]/assign/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js +180 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js +36 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +107 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +727 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +38 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +154 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/delete-channel.js +137 -0
- package/dist/modules/communication_channels/commands/delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +400 -0
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js +163 -0
- package/dist/modules/communication_channels/commands/disconnect-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +413 -0
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +7 -0
- package/dist/modules/communication_channels/commands/interceptors.js +68 -0
- package/dist/modules/communication_channels/commands/interceptors.js.map +7 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js +198 -0
- package/dist/modules/communication_channels/commands/process-inbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-register.js +146 -0
- package/dist/modules/communication_channels/commands/push-register.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-renew.js +23 -0
- package/dist/modules/communication_channels/commands/push-renew.js.map +7 -0
- package/dist/modules/communication_channels/commands/push-unregister.js +108 -0
- package/dist/modules/communication_channels/commands/push-unregister.js.map +7 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js +113 -0
- package/dist/modules/communication_channels/commands/queue-import-history.js.map +7 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js +193 -0
- package/dist/modules/communication_channels/commands/reassign-conversation.js.map +7 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js +114 -0
- package/dist/modules/communication_channels/commands/set-primary-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js +260 -0
- package/dist/modules/communication_channels/commands/toggle-outbound-reaction.js.map +7 -0
- package/dist/modules/communication_channels/data/enrichers.js +286 -0
- package/dist/modules/communication_channels/data/enrichers.js.map +7 -0
- package/dist/modules/communication_channels/data/entities.js +447 -0
- package/dist/modules/communication_channels/data/entities.js.map +7 -0
- package/dist/modules/communication_channels/data/extensions.js +67 -0
- package/dist/modules/communication_channels/data/extensions.js.map +7 -0
- package/dist/modules/communication_channels/data/validators.js +123 -0
- package/dist/modules/communication_channels/data/validators.js.map +7 -0
- package/dist/modules/communication_channels/di.js +35 -0
- package/dist/modules/communication_channels/di.js.map +7 -0
- package/dist/modules/communication_channels/encryption.js +12 -0
- package/dist/modules/communication_channels/encryption.js.map +7 -0
- package/dist/modules/communication_channels/events.js +124 -0
- package/dist/modules/communication_channels/events.js.map +7 -0
- package/dist/modules/communication_channels/index.js +20 -0
- package/dist/modules/communication_channels/index.js.map +7 -0
- package/dist/modules/communication_channels/lib/access-control.js +43 -0
- package/dist/modules/communication_channels/lib/access-control.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js +36 -0
- package/dist/modules/communication_channels/lib/adapter-compat.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js +22 -0
- package/dist/modules/communication_channels/lib/adapter-registry-singleton.js.map +7 -0
- package/dist/modules/communication_channels/lib/adapter.js +1 -0
- package/dist/modules/communication_channels/lib/adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/connect-channel.js +95 -0
- package/dist/modules/communication_channels/lib/connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js +79 -0
- package/dist/modules/communication_channels/lib/contact-resolver.js.map +7 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js +97 -0
- package/dist/modules/communication_channels/lib/credential-refresh.js.map +7 -0
- package/dist/modules/communication_channels/lib/dead-letter.js +62 -0
- package/dist/modules/communication_channels/lib/dead-letter.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js +47 -0
- package/dist/modules/communication_channels/lib/email-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-contact.js +14 -0
- package/dist/modules/communication_channels/lib/email-contact.js.map +7 -0
- package/dist/modules/communication_channels/lib/email-mime.js +269 -0
- package/dist/modules/communication_channels/lib/email-mime.js.map +7 -0
- package/dist/modules/communication_channels/lib/error-classification.js +101 -0
- package/dist/modules/communication_channels/lib/error-classification.js.map +7 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js +185 -0
- package/dist/modules/communication_channels/lib/gmail-pubsub-jwt.js.map +7 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js +114 -0
- package/dist/modules/communication_channels/lib/mutation-guards.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js +32 -0
- package/dist/modules/communication_channels/lib/oauth-client-config.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-state.js +128 -0
- package/dist/modules/communication_channels/lib/oauth-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-token.js +45 -0
- package/dist/modules/communication_channels/lib/oauth-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/pg-errors.js +11 -0
- package/dist/modules/communication_channels/lib/pg-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/provider-health.js +24 -0
- package/dist/modules/communication_channels/lib/provider-health.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-state.js +19 -0
- package/dist/modules/communication_channels/lib/push-state.js.map +7 -0
- package/dist/modules/communication_channels/lib/queue.js +54 -0
- package/dist/modules/communication_channels/lib/queue.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js +5 -0
- package/dist/modules/communication_channels/lib/reaction-processor-types.js.map +7 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js +11 -0
- package/dist/modules/communication_channels/lib/reaction-semantics.js.map +7 -0
- package/dist/modules/communication_channels/lib/registry.js +67 -0
- package/dist/modules/communication_channels/lib/registry.js.map +7 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js +43 -0
- package/dist/modules/communication_channels/lib/route-mutation-guard.js.map +7 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js +96 -0
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +7 -0
- package/dist/modules/communication_channels/lib/send-as-user.js +194 -0
- package/dist/modules/communication_channels/lib/send-as-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-user.js +22 -0
- package/dist/modules/communication_channels/lib/system-user.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +68 -0
- package/dist/modules/communication_channels/lib/test-seed.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js +263 -0
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +7 -0
- package/dist/modules/communication_channels/lib/thread-token.js +219 -0
- package/dist/modules/communication_channels/lib/thread-token.js.map +7 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js +61 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js +50 -0
- package/dist/modules/communication_channels/migrations/Migration20260526134719_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js +19 -0
- package/dist/modules/communication_channels/migrations/Migration20260527195446_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260529231848_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js +17 -0
- package/dist/modules/communication_channels/migrations/Migration20260531120000_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.client.js +51 -0
- package/dist/modules/communication_channels/notifications.client.js.map +7 -0
- package/dist/modules/communication_channels/notifications.handlers.js +53 -0
- package/dist/modules/communication_channels/notifications.handlers.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +56 -0
- package/dist/modules/communication_channels/notifications.js.map +7 -0
- package/dist/modules/communication_channels/setup.js +105 -0
- package/dist/modules/communication_channels/setup.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js +71 -0
- package/dist/modules/communication_channels/subscribers/channel-requires-reauth-notification.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js +103 -0
- package/dist/modules/communication_channels/subscribers/outbound-bridge.js.map +7 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js +51 -0
- package/dist/modules/communication_channels/subscribers/user-deleted-cascade.js.map +7 -0
- package/dist/modules/communication_channels/widgets/components.js +7 -0
- package/dist/modules/communication_channels/widgets/components.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js +18 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js +30 -0
- package/dist/modules/communication_channels/widgets/injection/channel-badge/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js +185 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-info-panel/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +44 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js +23 -0
- package/dist/modules/communication_channels/widgets/injection/profile-channels-menu/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js +141 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.client.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js +17 -0
- package/dist/modules/communication_channels/widgets/injection/reaction-bar/widget.js.map +7 -0
- package/dist/modules/communication_channels/widgets/injection-table.js +38 -0
- package/dist/modules/communication_channels/widgets/injection-table.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js +25 -0
- package/dist/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js +19 -0
- package/dist/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.js.map +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js +7 -0
- package/dist/modules/communication_channels/widgets/notifications/index.js.map +7 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js +185 -0
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js +154 -0
- package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +7 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js +95 -0
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +7 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js +56 -0
- package/dist/modules/communication_channels/workers/inbound-processor.js.map +7 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js +85 -0
- package/dist/modules/communication_channels/workers/outbound-delivery.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-channel.js +240 -0
- package/dist/modules/communication_channels/workers/poll-channel.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-tick.js +132 -0
- package/dist/modules/communication_channels/workers/poll-tick.js.map +7 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js +192 -0
- package/dist/modules/communication_channels/workers/reaction-processor.js.map +7 -0
- package/dist/modules/customers/acl.js +18 -0
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +9 -0
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +18 -7
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js +151 -0
- package/dist/modules/customers/api/interactions/[id]/visibility/route.js.map +7 -0
- package/dist/modules/customers/api/interactions/counts/route.js +6 -0
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +26 -7
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/email-threads/route.js +82 -0
- package/dist/modules/customers/api/people/[id]/email-threads/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js +157 -0
- package/dist/modules/customers/api/people/[id]/emails/route.js.map +7 -0
- package/dist/modules/customers/api/people/[id]/route.js +12 -4
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +10 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +46 -5
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +16 -0
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +32 -0
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js +242 -0
- package/dist/modules/customers/components/detail/ComposeEmailDialog.js.map +7 -0
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +10 -0
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/EmailCardActions.js +179 -0
- package/dist/modules/customers/components/detail/EmailCardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js +52 -0
- package/dist/modules/customers/components/detail/EmailReplyForwardActions.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +7 -1
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js +366 -0
- package/dist/modules/customers/components/detail/PersonEmailThreadsTab.js.map +7 -0
- package/dist/modules/customers/data/enrichers.js +133 -2
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +18 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/extensions.js +16 -0
- package/dist/modules/customers/data/extensions.js.map +7 -0
- package/dist/modules/customers/encryption.js +11 -0
- package/dist/modules/customers/encryption.js.map +2 -2
- package/dist/modules/customers/events.js +4 -1
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/findPeopleByAddresses.js +64 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js.map +2 -2
- package/dist/modules/customers/lib/link-channel-message-handler.js +303 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +7 -0
- package/dist/modules/customers/lib/personEmailThreads.js +205 -0
- package/dist/modules/customers/lib/personEmailThreads.js.map +7 -0
- package/dist/modules/customers/lib/visibilityFilter.js +51 -0
- package/dist/modules/customers/lib/visibilityFilter.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js +20 -0
- package/dist/modules/customers/migrations/Migration20260527012240_customers.js.map +7 -0
- package/dist/modules/customers/setup.js +2 -1
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/customers/subscribers/link-channel-message-received.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-received.js.map +7 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js +12 -0
- package/dist/modules/customers/subscribers/link-channel-message-sent.js.map +7 -0
- package/dist/modules/integrations/data/entities.js +8 -1
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +29 -14
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js +15 -0
- package/dist/modules/integrations/migrations/Migration20260526154136_integrations.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +70 -8
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/ComposeMessagePageClient.js +24 -13
- package/dist/modules/messages/components/ComposeMessagePageClient.js.map +2 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js +39 -2
- package/dist/modules/messages/components/MessageDetailPageClient.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/data/entities.js +8 -1
- package/dist/modules/messages/data/entities.js.map +2 -2
- package/dist/modules/messages/migrations/Migration20260531130000.js +15 -0
- package/dist/modules/messages/migrations/Migration20260531130000.js.map +7 -0
- package/dist/modules/messages/widgets/injection-table.js +7 -0
- package/dist/modules/messages/widgets/injection-table.js.map +7 -0
- package/generated/entities/channel_ingest_dead_letter/index.ts +11 -0
- package/generated/entities/channel_thread_mapping/index.ts +11 -0
- package/generated/entities/channel_thread_token/index.ts +7 -0
- package/generated/entities/communication_channel/index.ts +20 -0
- package/generated/entities/customer_interaction/index.ts +2 -0
- package/generated/entities/external_conversation/index.ts +11 -0
- package/generated/entities/external_message/index.ts +11 -0
- package/generated/entities/integration_credentials/index.ts +1 -0
- package/generated/entities/message/index.ts +1 -0
- package/generated/entities/message_channel_link/index.ts +15 -0
- package/generated/entities/message_reaction/index.ts +11 -0
- package/generated/entities.ids.generated.ts +11 -0
- package/generated/entity-fields-registry.ts +117 -0
- package/package.json +9 -7
- package/src/helpers/integration/authFixtures.ts +4 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +124 -0
- package/src/modules/communication_channels/acl.ts +43 -0
- package/src/modules/communication_channels/api/delete/channels/[id]/route.ts +163 -0
- package/src/modules/communication_channels/api/delete/messages/[messageId]/reactions/[reactionId]/route.ts +143 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +173 -0
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +111 -0
- package/src/modules/communication_channels/api/get/channels/route.ts +109 -0
- package/src/modules/communication_channels/api/get/me/channels/route.ts +100 -0
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +355 -0
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +206 -0
- package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +174 -0
- package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +158 -0
- package/src/modules/communication_channels/api/post/channels/[id]/set-primary/route.ts +114 -0
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +241 -0
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +134 -0
- package/src/modules/communication_channels/api/post/messages/[messageId]/reactions/route.ts +143 -0
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +192 -0
- package/src/modules/communication_channels/api/post/send-as-user/route.ts +125 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +267 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +227 -0
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +161 -0
- package/src/modules/communication_channels/api/put/threads/[threadId]/assign/route.ts +132 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.meta.ts +34 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/[id]/page.tsx +250 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +137 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +36 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +907 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +243 -0
- package/src/modules/communication_channels/commands/delete-channel.ts +193 -0
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +579 -0
- package/src/modules/communication_channels/commands/disconnect-channel.ts +241 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +602 -0
- package/src/modules/communication_channels/commands/interceptors.ts +104 -0
- package/src/modules/communication_channels/commands/process-inbound-reaction.ts +265 -0
- package/src/modules/communication_channels/commands/push-register.ts +203 -0
- package/src/modules/communication_channels/commands/push-renew.ts +49 -0
- package/src/modules/communication_channels/commands/push-unregister.ts +168 -0
- package/src/modules/communication_channels/commands/queue-import-history.ts +180 -0
- package/src/modules/communication_channels/commands/reassign-conversation.ts +273 -0
- package/src/modules/communication_channels/commands/set-primary-channel.ts +154 -0
- package/src/modules/communication_channels/commands/toggle-outbound-reaction.ts +347 -0
- package/src/modules/communication_channels/data/enrichers.ts +413 -0
- package/src/modules/communication_channels/data/entities.ts +546 -0
- package/src/modules/communication_channels/data/extensions.ts +76 -0
- package/src/modules/communication_channels/data/validators.ts +138 -0
- package/src/modules/communication_channels/di.ts +40 -0
- package/src/modules/communication_channels/encryption.ts +44 -0
- package/src/modules/communication_channels/events.ts +122 -0
- package/src/modules/communication_channels/i18n/de.json +138 -0
- package/src/modules/communication_channels/i18n/en.json +138 -0
- package/src/modules/communication_channels/i18n/es.json +138 -0
- package/src/modules/communication_channels/i18n/pl.json +138 -0
- package/src/modules/communication_channels/index.ts +19 -0
- package/src/modules/communication_channels/lib/access-control.ts +110 -0
- package/src/modules/communication_channels/lib/adapter-compat.ts +57 -0
- package/src/modules/communication_channels/lib/adapter-registry-singleton.ts +35 -0
- package/src/modules/communication_channels/lib/adapter.ts +605 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +163 -0
- package/src/modules/communication_channels/lib/contact-resolver.ts +162 -0
- package/src/modules/communication_channels/lib/credential-refresh.ts +197 -0
- package/src/modules/communication_channels/lib/dead-letter.ts +87 -0
- package/src/modules/communication_channels/lib/email-capabilities.ts +60 -0
- package/src/modules/communication_channels/lib/email-contact.ts +17 -0
- package/src/modules/communication_channels/lib/email-mime.ts +442 -0
- package/src/modules/communication_channels/lib/error-classification.ts +144 -0
- package/src/modules/communication_channels/lib/gmail-pubsub-jwt.ts +278 -0
- package/src/modules/communication_channels/lib/mutation-guards.ts +215 -0
- package/src/modules/communication_channels/lib/oauth-client-config.ts +79 -0
- package/src/modules/communication_channels/lib/oauth-state.ts +228 -0
- package/src/modules/communication_channels/lib/oauth-token.ts +81 -0
- package/src/modules/communication_channels/lib/pg-errors.ts +12 -0
- package/src/modules/communication_channels/lib/provider-health.ts +47 -0
- package/src/modules/communication_channels/lib/push-state.ts +38 -0
- package/src/modules/communication_channels/lib/queue.ts +66 -0
- package/src/modules/communication_channels/lib/reaction-processor-types.ts +51 -0
- package/src/modules/communication_channels/lib/reaction-semantics.ts +48 -0
- package/src/modules/communication_channels/lib/registry.ts +99 -0
- package/src/modules/communication_channels/lib/route-mutation-guard.ts +68 -0
- package/src/modules/communication_channels/lib/sanitize-channel-html.ts +129 -0
- package/src/modules/communication_channels/lib/send-as-user.ts +284 -0
- package/src/modules/communication_channels/lib/system-user.ts +74 -0
- package/src/modules/communication_channels/lib/test-seed.ts +140 -0
- package/src/modules/communication_channels/lib/thread-matcher.ts +430 -0
- package/src/modules/communication_channels/lib/thread-token.ts +355 -0
- package/src/modules/communication_channels/lib/use-connect-channel.ts +73 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +2142 -0
- package/src/modules/communication_channels/migrations/Migration20260526134719_communication_channels.ts +55 -0
- package/src/modules/communication_channels/migrations/Migration20260527195446_communication_channels.ts +20 -0
- package/src/modules/communication_channels/migrations/Migration20260529231848_communication_channels.ts +13 -0
- package/src/modules/communication_channels/migrations/Migration20260531120000_communication_channels.ts +24 -0
- package/src/modules/communication_channels/notifications.client.ts +50 -0
- package/src/modules/communication_channels/notifications.handlers.ts +86 -0
- package/src/modules/communication_channels/notifications.ts +52 -0
- package/src/modules/communication_channels/setup.ts +158 -0
- package/src/modules/communication_channels/subscribers/channel-requires-reauth-notification.ts +118 -0
- package/src/modules/communication_channels/subscribers/outbound-bridge.ts +175 -0
- package/src/modules/communication_channels/subscribers/user-deleted-cascade.ts +100 -0
- package/src/modules/communication_channels/widgets/components.ts +36 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.client.tsx +38 -0
- package/src/modules/communication_channels/widgets/injection/channel-badge/widget.ts +51 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.client.tsx +278 -0
- package/src/modules/communication_channels/widgets/injection/channel-info-panel/widget.ts +24 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +63 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +29 -0
- package/src/modules/communication_channels/widgets/injection/profile-channels-menu/widget.ts +34 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.client.tsx +177 -0
- package/src/modules/communication_channels/widgets/injection/reaction-bar/widget.ts +26 -0
- package/src/modules/communication_channels/widgets/injection-table.ts +47 -0
- package/src/modules/communication_channels/widgets/notifications/ChannelRequiresReauthRenderer.tsx +48 -0
- package/src/modules/communication_channels/widgets/notifications/MessageReceivedRenderer.tsx +45 -0
- package/src/modules/communication_channels/widgets/notifications/index.ts +2 -0
- package/src/modules/communication_channels/workers/channel-import-history.ts +252 -0
- package/src/modules/communication_channels/workers/gmail-history-sync.ts +223 -0
- package/src/modules/communication_channels/workers/gmail-renew-watch.ts +141 -0
- package/src/modules/communication_channels/workers/inbound-processor.ts +114 -0
- package/src/modules/communication_channels/workers/outbound-delivery.ts +155 -0
- package/src/modules/communication_channels/workers/poll-channel.ts +391 -0
- package/src/modules/communication_channels/workers/poll-tick.ts +210 -0
- package/src/modules/communication_channels/workers/reaction-processor.ts +264 -0
- package/src/modules/customers/acl.ts +18 -0
- package/src/modules/customers/api/activities/route.ts +13 -0
- package/src/modules/customers/api/companies/[id]/route.ts +21 -1
- package/src/modules/customers/api/interactions/[id]/visibility/route.ts +179 -0
- package/src/modules/customers/api/interactions/counts/route.ts +10 -0
- package/src/modules/customers/api/interactions/route.ts +51 -5
- package/src/modules/customers/api/people/[id]/email-threads/route.ts +92 -0
- package/src/modules/customers/api/people/[id]/emails/route.ts +184 -0
- package/src/modules/customers/api/people/[id]/route.ts +17 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +11 -1
- package/src/modules/customers/commands/deals.ts +65 -6
- package/src/modules/customers/commands/interactions.ts +30 -0
- package/src/modules/customers/components/detail/ActivityCard.tsx +48 -0
- package/src/modules/customers/components/detail/ComposeEmailDialog.tsx +329 -0
- package/src/modules/customers/components/detail/DealForm.tsx +2 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +26 -0
- package/src/modules/customers/components/detail/EmailCardActions.tsx +258 -0
- package/src/modules/customers/components/detail/EmailReplyForwardActions.tsx +53 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +8 -1
- package/src/modules/customers/components/detail/PersonEmailThreadsTab.tsx +448 -0
- package/src/modules/customers/data/enrichers.ts +252 -1
- package/src/modules/customers/data/entities.ts +46 -1
- package/src/modules/customers/data/extensions.ts +26 -0
- package/src/modules/customers/encryption.ts +11 -0
- package/src/modules/customers/events.ts +4 -0
- package/src/modules/customers/i18n/de.json +41 -0
- package/src/modules/customers/i18n/en.json +41 -0
- package/src/modules/customers/i18n/es.json +41 -0
- package/src/modules/customers/i18n/pl.json +41 -0
- package/src/modules/customers/lib/findPeopleByAddresses.ts +107 -0
- package/src/modules/customers/lib/kysely.ts +16 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +571 -0
- package/src/modules/customers/lib/personEmailThreads.ts +325 -0
- package/src/modules/customers/lib/visibilityFilter.ts +152 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +61 -0
- package/src/modules/customers/migrations/Migration20260527012240_customers.ts +23 -0
- package/src/modules/customers/setup.ts +1 -0
- package/src/modules/customers/subscribers/link-channel-message-received.ts +21 -0
- package/src/modules/customers/subscribers/link-channel-message-sent.ts +21 -0
- package/src/modules/integrations/AGENTS.md +9 -0
- package/src/modules/integrations/data/entities.ts +21 -1
- package/src/modules/integrations/lib/credentials-service.ts +49 -13
- package/src/modules/integrations/migrations/.snapshot-open-mercato.json +26 -1
- package/src/modules/integrations/migrations/Migration20260526154136_integrations.ts +15 -0
- package/src/modules/messages/commands/messages.ts +101 -8
- package/src/modules/messages/components/ComposeMessagePageClient.tsx +17 -0
- package/src/modules/messages/components/MessageDetailPageClient.tsx +43 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -0
- package/src/modules/messages/data/entities.ts +11 -0
- package/src/modules/messages/migrations/.snapshot-open-mercato.json +18 -0
- package/src/modules/messages/migrations/Migration20260531130000.ts +15 -0
- package/src/modules/messages/widgets/injection-table.ts +29 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import type { NormalizedInboundMessage, NormalizedAttachment } from './adapter'
|
|
3
|
+
import { EMAIL_MAX_ATTACHMENT_BYTES } from './email-capabilities'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Aggregate ceiling for all attachments on a single inbound message. Inbound
|
|
7
|
+
* mail is untrusted, so without a cap a malicious/large message would be fully
|
|
8
|
+
* base64-buffered in memory (~1.33x raw bytes) and persisted. Allow a small
|
|
9
|
+
* multiple of the per-attachment limit for legitimate multi-file emails.
|
|
10
|
+
*/
|
|
11
|
+
const TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES = EMAIL_MAX_ATTACHMENT_BYTES * 2
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared email MIME helpers for the email channel providers (Gmail, IMAP).
|
|
15
|
+
* Outbound assembly, inbound parsing, header/address normalization,
|
|
16
|
+
* and threading-id extraction all live here so every provider shares one
|
|
17
|
+
* correct implementation instead of copy-pasting (which previously let Gmail's
|
|
18
|
+
* `extractHeaders` drift into a Map-handling bug that IMAP had already fixed).
|
|
19
|
+
*
|
|
20
|
+
* Provider-specific transport (Gmail History API, IMAP UID sync)
|
|
21
|
+
* stays in each package — this module only owns the format-level plumbing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ── Outbound MIME helpers ─────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export function stringOrUndefined(value: unknown): string | undefined {
|
|
27
|
+
if (typeof value !== 'string') return undefined
|
|
28
|
+
const trimmed = value.trim()
|
|
29
|
+
return trimmed.length > 0 ? trimmed : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toAddressList(value: unknown): string[] {
|
|
33
|
+
if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter((v) => v.length > 0)
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
return value
|
|
36
|
+
.split(/[,;]\s*/)
|
|
37
|
+
.map((v) => v.trim())
|
|
38
|
+
.filter((v) => v.length > 0)
|
|
39
|
+
}
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function referencesFromMeta(value: unknown): string[] | undefined {
|
|
44
|
+
if (!Array.isArray(value)) return undefined
|
|
45
|
+
return value.filter((entry): entry is string => typeof entry === 'string')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Strip every `<tag>…</tag>` block (and its contents) for a single tag name.
|
|
50
|
+
* Inbound HTML is untrusted, so the matcher must resist evasion: the close tag
|
|
51
|
+
* allows trailing attributes/whitespace (`</script >`, `</style foo>`), the `i`
|
|
52
|
+
* flag covers mixed case, and the replacement loops until the string is stable
|
|
53
|
+
* so a payload split across nested or reconstructed tags cannot survive a
|
|
54
|
+
* single pass (`<scr<script>ipt>` collapsing back into `<script>`).
|
|
55
|
+
*/
|
|
56
|
+
function stripTagBlocks(html: string, tag: string): string {
|
|
57
|
+
const blockPattern = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}[^>]*>`, 'gi')
|
|
58
|
+
let previous: string
|
|
59
|
+
let current = html
|
|
60
|
+
do {
|
|
61
|
+
previous = current
|
|
62
|
+
current = current.replace(blockPattern, ' ')
|
|
63
|
+
} while (current !== previous)
|
|
64
|
+
return current
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function htmlToText(html: string): string {
|
|
68
|
+
return stripTagBlocks(stripTagBlocks(html, 'style'), 'script')
|
|
69
|
+
.replace(/<br\s*\/?>(?=\s*)/gi, '\n')
|
|
70
|
+
.replace(/<\/p\s*>/gi, '\n\n')
|
|
71
|
+
.replace(/<[^>]+>/g, '')
|
|
72
|
+
.replace(/ /gi, ' ')
|
|
73
|
+
.replace(/</gi, '<')
|
|
74
|
+
.replace(/>/gi, '>')
|
|
75
|
+
.replace(/"/gi, '"')
|
|
76
|
+
.replace(/&/gi, '&')
|
|
77
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
78
|
+
.trim()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function escapeQuotes(value: string): string {
|
|
82
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Collapse CR/LF/TAB in an email header value to a single space to prevent
|
|
87
|
+
* RFC 5322 header injection — e.g. a Subject smuggling an extra
|
|
88
|
+
* `Bcc:`/`Content-Type:` header or splitting the message into a body.
|
|
89
|
+
* Collapsing (rather than folding) is safe for the short structured headers
|
|
90
|
+
* we emit (Subject, addresses, Message-ID, References).
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeHeaderValue(value: string): string {
|
|
93
|
+
return value.replace(/[\r\n\t]+/g, ' ').trim()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function ensureBrackets(value: string): string {
|
|
97
|
+
const trimmed = sanitizeHeaderValue(value)
|
|
98
|
+
if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed
|
|
99
|
+
return `<${trimmed}>`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isPureAscii(value: string): boolean {
|
|
103
|
+
// eslint-disable-next-line no-control-regex
|
|
104
|
+
return /^[\x00-\x7F]*$/.test(value)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Encode a single header value as an RFC 2047 "B" (base64) encoded-word when it
|
|
109
|
+
* contains non-ASCII characters, so 8-bit text like "Café" survives strict MTAs
|
|
110
|
+
* that treat header bytes as 7-bit ASCII. Pure-ASCII values are returned
|
|
111
|
+
* unchanged. Apply only AFTER `sanitizeHeaderValue` so the CR/LF injection guard
|
|
112
|
+
* still runs against the raw value.
|
|
113
|
+
*/
|
|
114
|
+
export function encodeHeaderWord(value: string): string {
|
|
115
|
+
if (isPureAscii(value)) return value
|
|
116
|
+
return `=?utf-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Encode the display-name part of a single address header value, leaving the
|
|
121
|
+
* `<addr@domain>` untouched (per RFC 2047, encoded-words are not permitted
|
|
122
|
+
* inside the addr-spec). Inputs without a bracketed address are treated as a
|
|
123
|
+
* bare display name / address and encoded only when non-ASCII.
|
|
124
|
+
*/
|
|
125
|
+
export function encodeAddressHeaderWord(value: string): string {
|
|
126
|
+
if (isPureAscii(value)) return value
|
|
127
|
+
const match = value.match(/^(.*?)(\s*<[^>]*>)\s*$/)
|
|
128
|
+
if (match) {
|
|
129
|
+
const [, displayPart, addrPart] = match
|
|
130
|
+
const displayName = displayPart.replace(/^"|"$/g, '').trim()
|
|
131
|
+
if (!displayName) return value
|
|
132
|
+
return `${encodeHeaderWord(displayName)}${addrPart}`
|
|
133
|
+
}
|
|
134
|
+
return encodeHeaderWord(value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate an RFC 5322 Message-ID rooted in the sender's domain. Used as a
|
|
139
|
+
* downstream idempotency key, so entropy comes from `crypto.randomUUID()`
|
|
140
|
+
* rather than `Math.random()`.
|
|
141
|
+
*/
|
|
142
|
+
export function generateMessageId(fromAddress: string, fallbackDomain = 'localhost'): string {
|
|
143
|
+
const domain = fromAddress.split('@')[1] ?? fallbackDomain
|
|
144
|
+
return `<${crypto.randomUUID()}@${domain}>`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface AssembleRfc2822Input {
|
|
148
|
+
from: string
|
|
149
|
+
to: string[]
|
|
150
|
+
cc: string[]
|
|
151
|
+
bcc: string[]
|
|
152
|
+
subject: string | undefined
|
|
153
|
+
text: string | undefined
|
|
154
|
+
html: string | undefined
|
|
155
|
+
inReplyTo: string | undefined
|
|
156
|
+
references: string[] | undefined
|
|
157
|
+
messageId: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Render one MIME body part's CTE header + body content. Non-ASCII bodies are
|
|
162
|
+
* base64-encoded (CRLF-wrapped at 76 cols) and labelled
|
|
163
|
+
* `Content-Transfer-Encoding: base64` so 8-bit text survives strict MTAs;
|
|
164
|
+
* pure-ASCII bodies stay `7bit` and verbatim.
|
|
165
|
+
*/
|
|
166
|
+
function encodeBodyPart(content: string): { cte: string; body: string } {
|
|
167
|
+
if (isPureAscii(content)) return { cte: '7bit', body: content }
|
|
168
|
+
const base64 = Buffer.from(content, 'utf-8').toString('base64')
|
|
169
|
+
const wrapped = base64.match(/.{1,76}/g)?.join('\r\n') ?? base64
|
|
170
|
+
return { cte: 'base64', body: wrapped }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Assemble a raw RFC2822 message (used by transports that send the encoded
|
|
175
|
+
* message directly, e.g. Gmail `users.messages.send`). Emits a
|
|
176
|
+
* `multipart/alternative` body when both html and text are present, otherwise a
|
|
177
|
+
* single-part text or html body.
|
|
178
|
+
*/
|
|
179
|
+
export function assembleRfc2822(input: AssembleRfc2822Input): Buffer {
|
|
180
|
+
const boundary = `omc_${crypto.randomUUID()}`
|
|
181
|
+
const headers: string[] = []
|
|
182
|
+
headers.push(`From: ${encodeAddressHeaderWord(sanitizeHeaderValue(input.from))}`)
|
|
183
|
+
headers.push(`To: ${input.to.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)
|
|
184
|
+
if (input.cc.length) headers.push(`Cc: ${input.cc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)
|
|
185
|
+
if (input.bcc.length) headers.push(`Bcc: ${input.bcc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)
|
|
186
|
+
if (input.subject) headers.push(`Subject: ${encodeHeaderWord(sanitizeHeaderValue(input.subject))}`)
|
|
187
|
+
headers.push(`Message-ID: ${ensureBrackets(input.messageId)}`)
|
|
188
|
+
if (input.inReplyTo) headers.push(`In-Reply-To: ${ensureBrackets(input.inReplyTo)}`)
|
|
189
|
+
if (input.references && input.references.length) {
|
|
190
|
+
headers.push(`References: ${input.references.map(ensureBrackets).join(' ')}`)
|
|
191
|
+
}
|
|
192
|
+
headers.push('MIME-Version: 1.0')
|
|
193
|
+
headers.push(`Date: ${new Date().toUTCString()}`)
|
|
194
|
+
|
|
195
|
+
if (input.html && input.text) {
|
|
196
|
+
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`)
|
|
197
|
+
const textPart = encodeBodyPart(input.text)
|
|
198
|
+
const htmlPart = encodeBodyPart(input.html)
|
|
199
|
+
const body = [
|
|
200
|
+
'',
|
|
201
|
+
`--${boundary}`,
|
|
202
|
+
'Content-Type: text/plain; charset=utf-8',
|
|
203
|
+
`Content-Transfer-Encoding: ${textPart.cte}`,
|
|
204
|
+
'',
|
|
205
|
+
textPart.body,
|
|
206
|
+
`--${boundary}`,
|
|
207
|
+
'Content-Type: text/html; charset=utf-8',
|
|
208
|
+
`Content-Transfer-Encoding: ${htmlPart.cte}`,
|
|
209
|
+
'',
|
|
210
|
+
htmlPart.body,
|
|
211
|
+
`--${boundary}--`,
|
|
212
|
+
'',
|
|
213
|
+
].join('\r\n')
|
|
214
|
+
return Buffer.from(headers.join('\r\n') + body, 'utf-8')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (input.html) {
|
|
218
|
+
const htmlPart = encodeBodyPart(input.html)
|
|
219
|
+
headers.push('Content-Type: text/html; charset=utf-8')
|
|
220
|
+
headers.push(`Content-Transfer-Encoding: ${htmlPart.cte}`)
|
|
221
|
+
return Buffer.from(headers.join('\r\n') + '\r\n\r\n' + htmlPart.body, 'utf-8')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const textPart = encodeBodyPart(input.text ?? '')
|
|
225
|
+
headers.push('Content-Type: text/plain; charset=utf-8')
|
|
226
|
+
headers.push(`Content-Transfer-Encoding: ${textPart.cte}`)
|
|
227
|
+
return Buffer.from(headers.join('\r\n') + '\r\n\r\n' + textPart.body, 'utf-8')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Inbound MIME parsing ──────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export interface ParsedMail {
|
|
233
|
+
messageId?: string | null
|
|
234
|
+
inReplyTo?: string | null
|
|
235
|
+
references?: string | string[] | null
|
|
236
|
+
from?: { value?: Array<{ address?: string; name?: string }> }
|
|
237
|
+
to?: { value?: Array<{ address?: string; name?: string }> }
|
|
238
|
+
cc?: { value?: Array<{ address?: string; name?: string }> }
|
|
239
|
+
bcc?: { value?: Array<{ address?: string; name?: string }> }
|
|
240
|
+
subject?: string | null
|
|
241
|
+
html?: string | false
|
|
242
|
+
text?: string
|
|
243
|
+
date?: string | Date | null
|
|
244
|
+
attachments?: ParsedAttachment[]
|
|
245
|
+
headers?: Map<string, unknown> | Record<string, unknown>
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface ParsedAttachment {
|
|
249
|
+
content?: Buffer | Uint8Array
|
|
250
|
+
contentType?: string
|
|
251
|
+
filename?: string
|
|
252
|
+
size?: number
|
|
253
|
+
contentDisposition?: string
|
|
254
|
+
cid?: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function stripBrackets(value: string | undefined | null): string | undefined {
|
|
258
|
+
if (!value) return undefined
|
|
259
|
+
const trimmed = value.trim()
|
|
260
|
+
if (!trimmed) return undefined
|
|
261
|
+
if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1)
|
|
262
|
+
return trimmed
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function parseReferences(value: string | string[] | undefined | null): string[] {
|
|
266
|
+
if (!value) return []
|
|
267
|
+
if (Array.isArray(value)) return value.map((v) => stripBrackets(v)).filter((v): v is string => Boolean(v))
|
|
268
|
+
return value
|
|
269
|
+
.split(/\s+/)
|
|
270
|
+
.map((segment) => stripBrackets(segment))
|
|
271
|
+
.filter((segment): segment is string => Boolean(segment))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function normalizeAttachments(attachments: ParsedAttachment[]): NormalizedAttachment[] {
|
|
275
|
+
const out: NormalizedAttachment[] = []
|
|
276
|
+
let totalBytes = 0
|
|
277
|
+
for (const att of attachments) {
|
|
278
|
+
if (!att.content) continue
|
|
279
|
+
const byteLength = att.content.byteLength
|
|
280
|
+
if (byteLength > EMAIL_MAX_ATTACHMENT_BYTES) {
|
|
281
|
+
console.warn(
|
|
282
|
+
`[email-mime] dropping oversized inbound attachment "${att.filename ?? 'attachment'}" (${byteLength} bytes > ${EMAIL_MAX_ATTACHMENT_BYTES} cap)`,
|
|
283
|
+
)
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
if (totalBytes + byteLength > TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES) {
|
|
287
|
+
console.warn(
|
|
288
|
+
`[email-mime] aggregate inbound attachment size exceeded ${TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES} bytes; dropping remaining attachments`,
|
|
289
|
+
)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
totalBytes += byteLength
|
|
293
|
+
const base64 = Buffer.isBuffer(att.content)
|
|
294
|
+
? att.content.toString('base64')
|
|
295
|
+
: Buffer.from(att.content).toString('base64')
|
|
296
|
+
out.push({
|
|
297
|
+
url: `data:${att.contentType ?? 'application/octet-stream'};base64,${base64}`,
|
|
298
|
+
mimeType: att.contentType ?? 'application/octet-stream',
|
|
299
|
+
fileName: att.filename ?? 'attachment',
|
|
300
|
+
fileSize: att.size,
|
|
301
|
+
inline: Boolean(att.contentDisposition && /inline/i.test(att.contentDisposition)) || Boolean(att.cid),
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
return out
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function stringifyHeaderValue(value: unknown): string | undefined {
|
|
308
|
+
if (typeof value === 'string') return value
|
|
309
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).join(', ')
|
|
310
|
+
if (value instanceof Date) return value.toISOString()
|
|
311
|
+
if (value && typeof value === 'object') {
|
|
312
|
+
try {
|
|
313
|
+
return JSON.stringify(value)
|
|
314
|
+
} catch {
|
|
315
|
+
return undefined
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (value === undefined || value === null) return undefined
|
|
319
|
+
return String(value)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Flatten parsed MIME headers to a `Record<string, string>`.
|
|
324
|
+
*
|
|
325
|
+
* mailparser returns `headers` as a `Map<string, unknown>`. `Object.entries` on
|
|
326
|
+
* a Map yields an empty array (Maps have no enumerable own properties), so we
|
|
327
|
+
* iterate Map entries directly, with a Record fallback for test fakes.
|
|
328
|
+
*/
|
|
329
|
+
export function extractHeaders(headers: ParsedMail['headers']): Record<string, string> {
|
|
330
|
+
if (!headers) return {}
|
|
331
|
+
const out: Record<string, string> = {}
|
|
332
|
+
if (headers instanceof Map) {
|
|
333
|
+
for (const [key, value] of headers.entries()) {
|
|
334
|
+
const stringified = stringifyHeaderValue(value)
|
|
335
|
+
if (stringified !== undefined) out[String(key)] = stringified
|
|
336
|
+
}
|
|
337
|
+
return out
|
|
338
|
+
}
|
|
339
|
+
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
340
|
+
const stringified = stringifyHeaderValue(value)
|
|
341
|
+
if (stringified !== undefined) out[key] = stringified
|
|
342
|
+
}
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export interface NormalizeMimeInboundOptions {
|
|
347
|
+
/**
|
|
348
|
+
* The parsed MIME message. Providers parse with their own `mailparser`
|
|
349
|
+
* dependency (Gmail/IMAP) and pass the result here, so the hub stays free of a
|
|
350
|
+
* MIME-parser dependency while still owning the normalization logic.
|
|
351
|
+
*/
|
|
352
|
+
parsed: ParsedMail
|
|
353
|
+
/** External identifier of the receiving channel (typically the account's email). */
|
|
354
|
+
accountIdentifier: string
|
|
355
|
+
/** Deterministic id used when the MIME message carries no `Message-ID` header. */
|
|
356
|
+
fallbackMessageId: string
|
|
357
|
+
/** Compute the conversation grouping id from the resolved message id + references. */
|
|
358
|
+
resolveConversationId: (context: { messageId: string; references: string[] }) => string
|
|
359
|
+
/** Fallback timestamp when the parsed message has no Date header. */
|
|
360
|
+
fallbackDate?: Date
|
|
361
|
+
/** Provider-specific fields merged into `channelMetadata`. */
|
|
362
|
+
channelMetadata?: (parsed: ParsedMail) => Record<string, unknown>
|
|
363
|
+
/** Provider-specific fields merged into `channelPayload`. */
|
|
364
|
+
channelPayload?: (parsed: ParsedMail) => Record<string, unknown>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Build the hub's canonical `NormalizedInboundMessage` from a parsed MIME
|
|
369
|
+
* message. Providers supply the bits that genuinely differ (message-id
|
|
370
|
+
* fallback, conversation grouping, extra metadata) and inherit the shared
|
|
371
|
+
* threading / attachment / header logic.
|
|
372
|
+
*/
|
|
373
|
+
export function normalizeMimeInbound(options: NormalizeMimeInboundOptions): NormalizedInboundMessage {
|
|
374
|
+
const { parsed } = options
|
|
375
|
+
|
|
376
|
+
const messageId = stripBrackets(parsed.messageId) ?? options.fallbackMessageId
|
|
377
|
+
const inReplyTo = stripBrackets(parsed.inReplyTo)
|
|
378
|
+
const references = parseReferences(parsed.references)
|
|
379
|
+
const conversationId = options.resolveConversationId({ messageId, references })
|
|
380
|
+
|
|
381
|
+
const from = parsed.from?.value?.[0]
|
|
382
|
+
const subject = parsed.subject?.trim() || undefined
|
|
383
|
+
const bodyHtml = parsed.html && typeof parsed.html === 'string' ? parsed.html : undefined
|
|
384
|
+
const bodyText = typeof parsed.text === 'string' ? parsed.text : undefined
|
|
385
|
+
const body = bodyHtml ?? bodyText ?? ''
|
|
386
|
+
const bodyFormat: 'text' | 'html' = bodyHtml ? 'html' : 'text'
|
|
387
|
+
|
|
388
|
+
const attachments = normalizeAttachments(parsed.attachments ?? [])
|
|
389
|
+
|
|
390
|
+
const channelMetadata: Record<string, unknown> = {
|
|
391
|
+
...(options.channelMetadata?.(parsed) ?? {}),
|
|
392
|
+
messageId,
|
|
393
|
+
inReplyTo: inReplyTo ?? null,
|
|
394
|
+
references,
|
|
395
|
+
headers: extractHeaders(parsed.headers),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const channelPayload: Record<string, unknown> = {
|
|
399
|
+
subject,
|
|
400
|
+
from: from ? { address: from.address, name: from.name } : null,
|
|
401
|
+
to: parsed.to?.value ?? [],
|
|
402
|
+
cc: parsed.cc?.value ?? [],
|
|
403
|
+
bcc: parsed.bcc?.value ?? [],
|
|
404
|
+
html: bodyHtml ?? null,
|
|
405
|
+
text: bodyText ?? null,
|
|
406
|
+
messageId,
|
|
407
|
+
...(options.channelPayload?.(parsed) ?? {}),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
externalMessageId: messageId,
|
|
412
|
+
externalConversationId: conversationId,
|
|
413
|
+
senderIdentifier: from?.address ?? options.accountIdentifier,
|
|
414
|
+
senderDisplayName: from?.name?.trim() || undefined,
|
|
415
|
+
subject,
|
|
416
|
+
body,
|
|
417
|
+
bodyFormat,
|
|
418
|
+
attachments,
|
|
419
|
+
timestamp: parsed.date ? new Date(parsed.date) : options.fallbackDate ?? new Date(),
|
|
420
|
+
replyToExternalId: inReplyTo ?? undefined,
|
|
421
|
+
channelPayload,
|
|
422
|
+
channelContentType: 'email/mime',
|
|
423
|
+
channelMetadata,
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Provider sync cursor helpers ──────────────────────────────
|
|
428
|
+
|
|
429
|
+
/** Encode a provider channel-state object into an opaque base64 sync cursor. */
|
|
430
|
+
export function encodeCursor(state: unknown): string {
|
|
431
|
+
return Buffer.from(JSON.stringify(state)).toString('base64')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Decode a base64 sync cursor back into its object form, or `null` if malformed. */
|
|
435
|
+
export function decodeCursor(value: string | null | undefined): unknown {
|
|
436
|
+
if (!value) return null
|
|
437
|
+
try {
|
|
438
|
+
return JSON.parse(Buffer.from(value, 'base64').toString('utf-8'))
|
|
439
|
+
} catch {
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify provider errors for outbound retry decisions.
|
|
3
|
+
*
|
|
4
|
+
* Transient errors (network, 429, 5xx, timeout) → retry with backoff.
|
|
5
|
+
* Permanent errors (4xx auth/validation/quota) → fail fast, no retry.
|
|
6
|
+
*
|
|
7
|
+
* Adapters can also throw classified errors directly by setting `transient: false`
|
|
8
|
+
* on their thrown Error instance. This helper falls back to heuristics when an
|
|
9
|
+
* adapter throws plain Error instances.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ErrorClassification = {
|
|
13
|
+
transient: boolean
|
|
14
|
+
/** HTTP status if known; helps with backoff decisions (e.g., `Retry-After`) */
|
|
15
|
+
status?: number
|
|
16
|
+
/** Human-readable summary suitable for logging. */
|
|
17
|
+
message: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Postgres SQLSTATEs that are transient (the operation can succeed on retry).
|
|
24
|
+
* These reach the inbound-ingest classifier when a DB blip happens mid-ingest;
|
|
25
|
+
* treating them as permanent would dead-letter the message AND advance the IMAP
|
|
26
|
+
* cursor, silently losing inbound mail (the exact "cursor drift" failure the
|
|
27
|
+
* email spec set out to prevent). Driver errors expose the SQLSTATE on `.code`.
|
|
28
|
+
*/
|
|
29
|
+
const TRANSIENT_PG_SQLSTATES = new Set([
|
|
30
|
+
'40001', // serialization_failure
|
|
31
|
+
'40P01', // deadlock_detected
|
|
32
|
+
'55P03', // lock_not_available
|
|
33
|
+
'53300', // too_many_connections
|
|
34
|
+
'53400', // configuration_limit_exceeded
|
|
35
|
+
'57P01', // admin_shutdown
|
|
36
|
+
'57P02', // crash_shutdown
|
|
37
|
+
'57P03', // cannot_connect_now (db starting up)
|
|
38
|
+
'08000', // connection_exception
|
|
39
|
+
'08001', // sqlclient_unable_to_establish_sqlconnection
|
|
40
|
+
'08003', // connection_does_not_exist
|
|
41
|
+
'08006', // connection_failure
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const TRANSIENT_CODE_PATTERNS = [
|
|
45
|
+
/ECONNRESET/i,
|
|
46
|
+
/ETIMEDOUT/i,
|
|
47
|
+
/ECONNREFUSED/i,
|
|
48
|
+
/ENOTFOUND/i,
|
|
49
|
+
/EAI_AGAIN/i,
|
|
50
|
+
/socket hang up/i,
|
|
51
|
+
/network timeout/i,
|
|
52
|
+
/\btimed?\s*out\b/i,
|
|
53
|
+
/service unavailable/i,
|
|
54
|
+
/rate limit/i,
|
|
55
|
+
/too many requests/i,
|
|
56
|
+
/temporarily unavailable/i,
|
|
57
|
+
/try again later/i,
|
|
58
|
+
// imapflow surfaces these on TLS/socket-level drops that almost always
|
|
59
|
+
// recover on the next attempt. Treating them as permanent kills the
|
|
60
|
+
// channel after a single bad packet (regression observed during the demo:
|
|
61
|
+
// a network hiccup put a freshly-connected mailbox into a non-recoverable
|
|
62
|
+
// `error` state until the operator manually clicked "Retry").
|
|
63
|
+
/unexpected close/i,
|
|
64
|
+
/connection not available/i,
|
|
65
|
+
/connection closed/i,
|
|
66
|
+
/server closed connection/i,
|
|
67
|
+
// Postgres transient failures surfaced as text by the ORM wrapper (the
|
|
68
|
+
// SQLSTATE on `.code` is the primary signal; these catch wrapped errors that
|
|
69
|
+
// only carry the message). See TRANSIENT_PG_SQLSTATES.
|
|
70
|
+
/deadlock detected/i,
|
|
71
|
+
/could not serialize access/i,
|
|
72
|
+
/connection terminated/i,
|
|
73
|
+
/too many clients already/i,
|
|
74
|
+
/the database system is (starting up|shutting down|in recovery)/i,
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
export function classifyOutboundError(error: unknown): ErrorClassification {
|
|
78
|
+
if (!error) {
|
|
79
|
+
return { transient: false, message: 'Unknown error' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (error instanceof Error) {
|
|
83
|
+
// Explicit hint from a classification-aware adapter.
|
|
84
|
+
const explicit = (error as Error & { transient?: boolean; status?: number }).transient
|
|
85
|
+
const status = (error as Error & { status?: number }).status
|
|
86
|
+
if (explicit !== undefined) {
|
|
87
|
+
return { transient: Boolean(explicit), status, message: error.message }
|
|
88
|
+
}
|
|
89
|
+
// Postgres driver errors expose the SQLSTATE on `.code`. A transient DB
|
|
90
|
+
// failure during inbound ingest MUST classify as transient so the caller
|
|
91
|
+
// aborts without advancing the cursor (no mail loss) rather than dead-lettering.
|
|
92
|
+
const code = (error as Error & { code?: string }).code
|
|
93
|
+
if (typeof code === 'string' && TRANSIENT_PG_SQLSTATES.has(code)) {
|
|
94
|
+
return { transient: true, status, message: error.message }
|
|
95
|
+
}
|
|
96
|
+
if (typeof status === 'number') {
|
|
97
|
+
return {
|
|
98
|
+
transient: TRANSIENT_STATUS_CODES.has(status),
|
|
99
|
+
status,
|
|
100
|
+
message: error.message,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const haystack = `${error.name} ${error.message}`
|
|
104
|
+
const matchesPattern = TRANSIENT_CODE_PATTERNS.some((pattern) => pattern.test(haystack))
|
|
105
|
+
return { transient: matchesPattern, message: error.message }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { transient: false, message: String(error) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decide whether a classified provider error means the channel's stored
|
|
113
|
+
* credentials are no longer valid and the user must re-authorize.
|
|
114
|
+
*
|
|
115
|
+
* A 401, or an `invalid_grant` / `unauthorized` message, is unrecoverable by
|
|
116
|
+
* retry: the access token is rejected and (for OAuth) the refresh token is
|
|
117
|
+
* likely revoked too. Provider adapters may also surface the explicit
|
|
118
|
+
* `requires_reauth` sentinel instead of a status — e.g. Gmail
|
|
119
|
+
* `sendMessage` returns `{ status: 'failed', error: 'requires_reauth' }` on a
|
|
120
|
+
* 401, which the outbound command rethrows as a status-less `Error`. Match that
|
|
121
|
+
* sentinel too so the channel still flips to `requires_reauth`. Callers flip the
|
|
122
|
+
* channel so the operator gets a clear signal. Kept identical to the inbound
|
|
123
|
+
* poll path so inbound and outbound failures behave consistently.
|
|
124
|
+
*/
|
|
125
|
+
export function isReauthError(classification: ErrorClassification): boolean {
|
|
126
|
+
return (
|
|
127
|
+
classification.status === 401 ||
|
|
128
|
+
/unauthorized|invalid_grant|requires_reauth/i.test(classification.message)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Compute exponential backoff delay (ms) for the given attempt number.
|
|
134
|
+
*
|
|
135
|
+
* Attempt 1 (= first retry) → 1s, attempt 2 → 2s, attempt 3 → 4s, ...
|
|
136
|
+
* Capped at 60s. Plus a small jitter so concurrent failures don't all retry
|
|
137
|
+
* simultaneously.
|
|
138
|
+
*/
|
|
139
|
+
export function computeBackoffMs(attemptNumber: number): number {
|
|
140
|
+
const base = 1000 * Math.pow(2, Math.max(0, attemptNumber - 1))
|
|
141
|
+
const capped = Math.min(base, 60_000)
|
|
142
|
+
const jitter = Math.floor(Math.random() * 500)
|
|
143
|
+
return capped + jitter
|
|
144
|
+
}
|