@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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/gmail-pubsub-jwt.ts"],
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\n\n/**\n * Spec C \u00A7 Phase C2 \u2014 Verify a Gmail Pub/Sub push request.\n *\n * Pub/Sub push subscriptions authenticate with a Google-signed RS256 JWT\n * passed in the `Authorization: Bearer \u2026` header. The token's claims contain\n * - `iss`: `https://accounts.google.com`\n * - `aud`: the configured audience (typically the webhook URL)\n * - `email`: the publishing service-account address (e.g.\n * `gmail-api-push@system.gserviceaccount.com` for Gmail watch)\n * - `email_verified: true`\n *\n * The default verifier downloads Google's public x509 certs and caches them\n * for an hour. Tests inject a mock verifier via `setGmailPubSubVerifier(...)`.\n */\n\nconst GOOGLE_CERTS_URL = 'https://www.googleapis.com/oauth2/v1/certs'\nconst CERT_CACHE_TTL_MS = 60 * 60 * 1000\nconst CERT_FETCH_TIMEOUT_MS = 5000\n// Google mints OIDC tokens with one of these two issuer strings.\nconst GOOGLE_ACCEPTED_ISSUERS = new Set(['https://accounts.google.com', 'accounts.google.com'])\n\nexport interface GmailPubSubJwtClaims {\n iss: string\n aud: string | string[]\n email?: string\n emailVerified?: boolean\n exp: number\n iat: number\n sub?: string\n}\n\nexport interface GmailPubSubVerifyInput {\n authorizationHeader: string | null | undefined\n expectedAudience: string\n expectedEmail: string\n}\n\nexport interface GmailPubSubVerifier {\n verify(input: GmailPubSubVerifyInput): Promise<GmailPubSubJwtClaims>\n}\n\nexport class GmailPubSubJwtError extends Error {\n readonly code: 'missing_token' | 'invalid_format' | 'invalid_signature' | 'expired' | 'wrong_issuer' | 'wrong_audience' | 'wrong_email' | 'fetch_certs_failed'\n constructor(message: string, code: GmailPubSubJwtError['code']) {\n super(message)\n this.name = 'GmailPubSubJwtError'\n this.code = code\n }\n}\n\ninterface CertCacheEntry {\n certs: Record<string, string>\n fetchedAt: number\n}\n\nclass FetchGmailPubSubVerifier implements GmailPubSubVerifier {\n private certCache: CertCacheEntry | null = null\n\n async verify(input: GmailPubSubVerifyInput): Promise<GmailPubSubJwtClaims> {\n const token = extractBearer(input.authorizationHeader)\n if (!token) throw new GmailPubSubJwtError('Missing Authorization bearer token', 'missing_token')\n\n const parts = token.split('.')\n if (parts.length !== 3) {\n throw new GmailPubSubJwtError('JWT must have three dot-separated parts', 'invalid_format')\n }\n const [headerB64, payloadB64, signatureB64] = parts\n let header: Record<string, unknown>\n let claims: GmailPubSubJwtClaims\n try {\n header = JSON.parse(Buffer.from(headerB64, 'base64url').toString('utf-8')) as Record<string, unknown>\n claims = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf-8')) as GmailPubSubJwtClaims\n } catch {\n throw new GmailPubSubJwtError('JWT header/payload not parseable', 'invalid_format')\n }\n const kid = typeof header.kid === 'string' ? header.kid : null\n const alg = typeof header.alg === 'string' ? header.alg : null\n if (!kid || alg !== 'RS256') {\n throw new GmailPubSubJwtError(`Unsupported JWT alg/kid: alg=${alg ?? '?'} kid=${kid ?? '?'}`, 'invalid_format')\n }\n\n const certs = await this.getCerts()\n const cert = certs[kid]\n if (!cert) {\n // Cert rotated; refetch once and retry.\n this.certCache = null\n const refreshed = await this.getCerts()\n const fresh = refreshed[kid]\n if (!fresh) throw new GmailPubSubJwtError(`No cert for kid=${kid}`, 'invalid_signature')\n verifySignature(`${headerB64}.${payloadB64}`, signatureB64, fresh)\n } else {\n verifySignature(`${headerB64}.${payloadB64}`, signatureB64, cert)\n }\n\n validateClaims(claims, input)\n return claims\n }\n\n private async getCerts(): Promise<Record<string, string>> {\n if (this.certCache && Date.now() - this.certCache.fetchedAt < CERT_CACHE_TTL_MS) {\n return this.certCache.certs\n }\n let res: Response\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), CERT_FETCH_TIMEOUT_MS)\n try {\n res = await fetch(GOOGLE_CERTS_URL, { signal: controller.signal })\n } catch (err) {\n throw new GmailPubSubJwtError(\n `Failed to fetch Google certs: ${err instanceof Error ? err.message : String(err)}`,\n 'fetch_certs_failed',\n )\n } finally {\n clearTimeout(timer)\n }\n if (!res.ok) {\n throw new GmailPubSubJwtError(`Google certs endpoint returned ${res.status}`, 'fetch_certs_failed')\n }\n let parsed: unknown\n try {\n parsed = await res.json()\n } catch (err) {\n throw new GmailPubSubJwtError(\n `Google certs endpoint returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`,\n 'fetch_certs_failed',\n )\n }\n const certs = toCertMap(parsed)\n if (!certs) {\n // Never cache an empty/malformed payload \u2014 a single bad 200 would otherwise\n // disable Gmail push verification for the whole CERT_CACHE_TTL_MS window.\n throw new GmailPubSubJwtError('Google certs endpoint returned an unexpected shape', 'fetch_certs_failed')\n }\n this.certCache = { certs, fetchedAt: Date.now() }\n return certs\n }\n}\n\n/**\n * Validates that a parsed Google certs response is a non-empty `kid \u2192 PEM` map.\n * Returns the typed map on success, or `null` when the shape is unusable so the\n * caller can fail closed instead of caching garbage.\n */\nfunction toCertMap(value: unknown): Record<string, string> | null {\n if (value == null || typeof value !== 'object' || Array.isArray(value)) return null\n const entries = Object.entries(value as Record<string, unknown>)\n if (entries.length === 0) return null\n const certs: Record<string, string> = {}\n for (const [kid, pem] of entries) {\n if (typeof pem !== 'string' || pem.length === 0) return null\n certs[kid] = pem\n }\n return certs\n}\n\nfunction extractBearer(header: string | null | undefined): string | null {\n if (!header) return null\n const match = /^Bearer\\s+(.+)$/i.exec(header.trim())\n return match ? match[1].trim() : null\n}\n\nfunction verifySignature(input: string, signatureB64: string, cert: string): void {\n const verifier = crypto.createVerify('RSA-SHA256')\n verifier.update(input)\n verifier.end()\n const signature = Buffer.from(signatureB64, 'base64url')\n const ok = verifier.verify(cert, signature)\n if (!ok) {\n throw new GmailPubSubJwtError('JWT signature verification failed', 'invalid_signature')\n }\n}\n\nfunction validateClaims(claims: GmailPubSubJwtClaims, input: GmailPubSubVerifyInput): void {\n const now = Math.floor(Date.now() / 1000)\n // Fail closed: a token without a numeric `exp` has no expiry, so a captured\n // push JWT could otherwise be replayed indefinitely. Require `exp` and reject\n // anything already past it (with a small clock-skew allowance).\n if (typeof claims.exp !== 'number' || claims.exp < now - 5) {\n throw new GmailPubSubJwtError('JWT expired or missing exp', 'expired')\n }\n // Reject a future-dated `iat` beyond the clock-skew allowance: a token\n // \"issued\" in the future signals a forged token or a badly-skewed clock.\n // Reuses the `expired` code (both are temporal-validity failures \u2192 401).\n if (typeof claims.iat === 'number' && claims.iat > now + 5) {\n throw new GmailPubSubJwtError('JWT issued in the future', 'expired')\n }\n // Verify the issuer is Google (defense-in-depth alongside the signature +\n // service-account email checks; the file header documents this requirement).\n if (typeof claims.iss !== 'string' || !GOOGLE_ACCEPTED_ISSUERS.has(claims.iss)) {\n throw new GmailPubSubJwtError(\n `JWT issuer not accepted (got ${typeof claims.iss === 'string' ? claims.iss : 'none'})`,\n 'wrong_issuer',\n )\n }\n const audOk = Array.isArray(claims.aud)\n ? claims.aud.includes(input.expectedAudience)\n : claims.aud === input.expectedAudience\n if (!audOk) {\n throw new GmailPubSubJwtError(\n `JWT audience mismatch (expected ${input.expectedAudience})`,\n 'wrong_audience',\n )\n }\n // Google uses `email_verified` in the wire format; our type uses camelCase.\n // Accept either to be defensive.\n const emailVerified =\n claims.emailVerified === true ||\n (claims as unknown as { email_verified?: boolean }).email_verified === true\n if (!emailVerified || claims.email !== input.expectedEmail) {\n throw new GmailPubSubJwtError(\n `JWT email mismatch (expected ${input.expectedEmail})`,\n 'wrong_email',\n )\n }\n}\n\nlet cachedVerifier: GmailPubSubVerifier | null = null\n\nexport function getGmailPubSubVerifier(): GmailPubSubVerifier {\n if (!cachedVerifier) cachedVerifier = new FetchGmailPubSubVerifier()\n return cachedVerifier\n}\n\nexport function setGmailPubSubVerifier(verifier: GmailPubSubVerifier | null): void {\n cachedVerifier = verifier\n}\n\n/**\n * Decode a Pub/Sub envelope from the webhook body.\n *\n * Shape: `{ message: { data: base64<JSON>, messageId, publishTime, attributes }, subscription }`.\n *\n * Gmail's payload (`data` field) decodes to `{ emailAddress, historyId }`.\n */\nexport interface PubSubEnvelope {\n message: {\n data: string\n messageId: string\n publishTime?: string\n attributes?: Record<string, string>\n }\n subscription?: string\n}\n\nexport interface GmailPushPayload {\n emailAddress: string\n historyId: string | number\n}\n\nexport function decodeGmailPubSubBody(rawBody: string): GmailPushPayload {\n let envelope: PubSubEnvelope\n try {\n envelope = JSON.parse(rawBody) as PubSubEnvelope\n } catch {\n throw new GmailPubSubJwtError('Body is not valid JSON', 'invalid_format')\n }\n if (!envelope.message?.data) {\n throw new GmailPubSubJwtError('Envelope missing message.data', 'invalid_format')\n }\n let payloadText: string\n try {\n payloadText = Buffer.from(envelope.message.data, 'base64').toString('utf-8')\n } catch {\n throw new GmailPubSubJwtError('message.data not base64 decodable', 'invalid_format')\n }\n let payload: GmailPushPayload\n try {\n payload = JSON.parse(payloadText) as GmailPushPayload\n } catch {\n throw new GmailPubSubJwtError('message.data JSON not parseable', 'invalid_format')\n }\n if (!payload.emailAddress || payload.historyId === undefined) {\n throw new GmailPubSubJwtError('message.data missing emailAddress or historyId', 'invalid_format')\n }\n return payload\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import {
|
|
3
|
+
CommunicationChannel,
|
|
4
|
+
ExternalConversation,
|
|
5
|
+
MessageChannelLink
|
|
6
|
+
} from "../data/entities.js";
|
|
7
|
+
class ChannelMutationBlockedError extends Error {
|
|
8
|
+
constructor(reason, channelId, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ChannelMutationBlockedError";
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
this.channelId = channelId;
|
|
13
|
+
this.errors = { channelId: message };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function guardChannelDelete(em, input) {
|
|
17
|
+
const channel = await findOneWithDecryption(
|
|
18
|
+
em,
|
|
19
|
+
CommunicationChannel,
|
|
20
|
+
{
|
|
21
|
+
id: input.channelId,
|
|
22
|
+
tenantId: input.scope.tenantId,
|
|
23
|
+
organizationId: input.scope.organizationId ?? null,
|
|
24
|
+
deletedAt: null
|
|
25
|
+
},
|
|
26
|
+
void 0,
|
|
27
|
+
input.scope
|
|
28
|
+
);
|
|
29
|
+
if (!channel) {
|
|
30
|
+
throw new ChannelMutationBlockedError(
|
|
31
|
+
"channel_not_found",
|
|
32
|
+
input.channelId,
|
|
33
|
+
"Channel not found in this tenant scope."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (input.force) return;
|
|
37
|
+
const inboundCount = await countInboundLinksForChannel(em, input.channelId, input.scope);
|
|
38
|
+
if (inboundCount > 0) {
|
|
39
|
+
throw new ChannelMutationBlockedError(
|
|
40
|
+
"channel_has_inbound_history",
|
|
41
|
+
input.channelId,
|
|
42
|
+
`Channel has ${inboundCount} inbound message${inboundCount === 1 ? "" : "s"} in history. Pass force=true to delete anyway.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function guardOutboundCreate(em, input) {
|
|
47
|
+
const channel = await findOneWithDecryption(
|
|
48
|
+
em,
|
|
49
|
+
CommunicationChannel,
|
|
50
|
+
{
|
|
51
|
+
id: input.channelId,
|
|
52
|
+
tenantId: input.scope.tenantId,
|
|
53
|
+
organizationId: input.scope.organizationId ?? null,
|
|
54
|
+
deletedAt: null
|
|
55
|
+
},
|
|
56
|
+
void 0,
|
|
57
|
+
input.scope
|
|
58
|
+
);
|
|
59
|
+
if (!channel) {
|
|
60
|
+
throw new ChannelMutationBlockedError(
|
|
61
|
+
"channel_not_found",
|
|
62
|
+
input.channelId,
|
|
63
|
+
"Channel not found in this tenant scope."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (channel.status === "requires_reauth") {
|
|
67
|
+
throw new ChannelMutationBlockedError(
|
|
68
|
+
"channel_requires_reauth",
|
|
69
|
+
input.channelId,
|
|
70
|
+
"This channel needs reconnection before it can send messages. Open Profile -> Communication channels to reconnect."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (channel.status === "disconnected") {
|
|
74
|
+
throw new ChannelMutationBlockedError(
|
|
75
|
+
"channel_disconnected",
|
|
76
|
+
input.channelId,
|
|
77
|
+
"This channel is disconnected and cannot send messages."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function countInboundLinksForChannel(em, channelId, scope) {
|
|
82
|
+
const conversations = await findWithDecryption(
|
|
83
|
+
em,
|
|
84
|
+
ExternalConversation,
|
|
85
|
+
{
|
|
86
|
+
channelId,
|
|
87
|
+
tenantId: scope.tenantId,
|
|
88
|
+
organizationId: scope.organizationId ?? null
|
|
89
|
+
},
|
|
90
|
+
{ fields: ["id"] },
|
|
91
|
+
scope
|
|
92
|
+
);
|
|
93
|
+
if (conversations.length === 0) return 0;
|
|
94
|
+
const conversationIds = conversations.map((c) => c.id);
|
|
95
|
+
const count = await em.count(
|
|
96
|
+
MessageChannelLink,
|
|
97
|
+
{
|
|
98
|
+
externalConversationId: { $in: conversationIds },
|
|
99
|
+
direction: "inbound",
|
|
100
|
+
tenantId: scope.tenantId,
|
|
101
|
+
organizationId: scope.organizationId ?? null
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
return typeof count === "number" ? count : 0;
|
|
105
|
+
}
|
|
106
|
+
const countUnreadInboundForChannel = countInboundLinksForChannel;
|
|
107
|
+
export {
|
|
108
|
+
ChannelMutationBlockedError,
|
|
109
|
+
countInboundLinksForChannel,
|
|
110
|
+
countUnreadInboundForChannel,
|
|
111
|
+
guardChannelDelete,
|
|
112
|
+
guardOutboundCreate
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=mutation-guards.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/mutation-guards.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\n\n/**\n * Hub-side mutation guards (Phase 4 of the email integration spec).\n *\n * These guards are plain functions that any caller (CRUD route, command,\n * subscriber) can invoke to enforce hub invariants before mutating state.\n * They throw `ChannelMutationBlockedError` on violations so the caller can\n * map the error to an HTTP 4xx (typically 422) without leaking internals.\n *\n * Why functions, not the generic `validateCrudMutationGuard`:\n * The hub's writes are channel-shaped rather than entity-shaped \u2014 \"deleting a\n * channel with unread inbound\" is not a per-entity invariant the generic\n * CRUD guard layer can express. These functions encode hub semantics directly\n * and stay invocation-site agnostic.\n */\n\nexport type ChannelMutationGuardReason =\n | 'channel_has_inbound_history'\n | 'channel_requires_reauth'\n | 'channel_disconnected'\n | 'channel_not_found'\n /**\n * @deprecated Use `channel_has_inbound_history`. Kept for one minor release\n * so external callers/tests catch a transition window. Round-2 F8 rename.\n */\n | 'channel_has_unread_inbound'\n\nexport class ChannelMutationBlockedError extends Error {\n readonly reason: ChannelMutationGuardReason\n readonly channelId: string\n /** Field-level error message keyed by `channelId` \u2014 for `createCrudFormError`. */\n readonly errors: Record<string, string>\n\n constructor(reason: ChannelMutationGuardReason, channelId: string, message: string) {\n super(message)\n this.name = 'ChannelMutationBlockedError'\n this.reason = reason\n this.channelId = channelId\n this.errors = { channelId: message }\n }\n}\n\nexport interface ChannelScope {\n tenantId: string\n organizationId?: string | null\n}\n\nexport interface GuardChannelDeleteInput {\n channelId: string\n scope: ChannelScope\n /** Bypass the unread-inbound check; used by admin \"force delete\" actions. */\n force?: boolean\n}\n\n/**\n * Guard `channel.delete`.\n *\n * Blocks when the channel still has ANY inbound `MessageChannelLink` rows.\n * Implementation note: the hub doesn't track per-message read state \u2014 that\n * lives on the messages module's `MessageRecipient.read_at`. Counting unread\n * across the module boundary would require either raw cross-module SQL\n * (forbidden by AGENTS.md) or a QueryEngine round-trip per delete. We pick\n * the simpler safety contract: block delete when ANY inbound link exists,\n * with `force: true` as the escape hatch.\n *\n * Pass `force: true` to bypass \u2014 exposed to admins so they can hard-delete a\n * channel whose mailbox they no longer care about (e.g. ex-employee\n * offboarding).\n */\nexport async function guardChannelDelete(\n em: EntityManager,\n input: GuardChannelDeleteInput,\n): Promise<void> {\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n input.scope,\n )\n if (!channel) {\n throw new ChannelMutationBlockedError(\n 'channel_not_found',\n input.channelId,\n 'Channel not found in this tenant scope.',\n )\n }\n if (input.force) return\n\n const inboundCount = await countInboundLinksForChannel(em, input.channelId, input.scope)\n if (inboundCount > 0) {\n throw new ChannelMutationBlockedError(\n 'channel_has_inbound_history',\n input.channelId,\n `Channel has ${inboundCount} inbound message${inboundCount === 1 ? '' : 's'} in history. Pass force=true to delete anyway.`,\n )\n }\n}\n\nexport interface GuardOutboundCreateInput {\n channelId: string\n scope: ChannelScope\n}\n\n/**\n * Guard `message.create` for outbound sends.\n *\n * Blocks when the target channel is in `requires_reauth` or `disconnected` \u2014\n * outbound sends through a re-auth-needed channel will fail at the provider\n * anyway, and blocking here gives a deterministic 422 with a field-level error\n * instead of an opaque 500. The hub's `mark_channel_requires_reauth` command\n * sets the status when refresh fails (slice 3b).\n */\nexport async function guardOutboundCreate(\n em: EntityManager,\n input: GuardOutboundCreateInput,\n): Promise<void> {\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n input.scope,\n )\n if (!channel) {\n throw new ChannelMutationBlockedError(\n 'channel_not_found',\n input.channelId,\n 'Channel not found in this tenant scope.',\n )\n }\n if (channel.status === 'requires_reauth') {\n throw new ChannelMutationBlockedError(\n 'channel_requires_reauth',\n input.channelId,\n 'This channel needs reconnection before it can send messages. Open Profile -> Communication channels to reconnect.',\n )\n }\n if (channel.status === 'disconnected') {\n throw new ChannelMutationBlockedError(\n 'channel_disconnected',\n input.channelId,\n 'This channel is disconnected and cannot send messages.',\n )\n }\n}\n\n/**\n * Returns the count of inbound `MessageChannelLink` rows the bridge created for\n * this channel. Used as the safety check before allowing a channel delete.\n *\n * Path: `external_conversations` (by channelId) \u2192 `message_channel_links` (by\n * externalConversationId, direction='inbound'). Both tables are hub-owned, so\n * we stay on the right side of the cross-module isolation rule. No raw SQL.\n *\n * Round-2 F8 rename (2026-05-26): was `countUnreadInboundForChannel`, but the\n * function never counted \"unread\" \u2014 it always counted all inbound links. The\n * old name is preserved as a `@deprecated` alias for one minor version.\n */\nexport async function countInboundLinksForChannel(\n em: EntityManager,\n channelId: string,\n scope: ChannelScope,\n): Promise<number> {\n const conversations = await findWithDecryption(\n em,\n ExternalConversation,\n {\n channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n { fields: ['id'] },\n scope,\n )\n if (conversations.length === 0) return 0\n const conversationIds = (conversations as Array<{ id: string }>).map((c) => c.id)\n const count = await em.count(\n MessageChannelLink,\n {\n externalConversationId: { $in: conversationIds },\n direction: 'inbound',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n )\n return typeof count === 'number' ? count : 0\n}\n\n/**\n * @deprecated Use `countInboundLinksForChannel`. Kept for one minor release so\n * external callers catch the rename window. Removed in the next minor.\n */\nexport const countUnreadInboundForChannel = countInboundLinksForChannel\n\n// Re-export entity types so callers can keep their typing tight.\nexport type { CommunicationChannel, ExternalMessage, MessageChannelLink }\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,uBAAuB,0BAA0B;AAC1D;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AA4BA,MAAM,oCAAoC,MAAM;AAAA,EAMrD,YAAY,QAAoC,WAAmB,SAAiB;AAClF,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,YAAY;AACjB,SAAK,SAAS,EAAE,WAAW,QAAQ;AAAA,EACrC;AACF;AA6BA,eAAsB,mBACpB,IACA,OACe;AACf,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,MAAM;AAAA,MACV,UAAU,MAAM,MAAM;AAAA,MACtB,gBAAgB,MAAM,MAAM,kBAAkB;AAAA,MAC9C,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,MAAM;AAAA,EACR;AACA,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,MAAI,MAAM,MAAO;AAEjB,QAAM,eAAe,MAAM,4BAA4B,IAAI,MAAM,WAAW,MAAM,KAAK;AACvF,MAAI,eAAe,GAAG;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN,eAAe,YAAY,mBAAmB,iBAAiB,IAAI,KAAK,GAAG;AAAA,IAC7E;AAAA,EACF;AACF;AAgBA,eAAsB,oBACpB,IACA,OACe;AACf,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,MAAM;AAAA,MACV,UAAU,MAAM,MAAM;AAAA,MACtB,gBAAgB,MAAM,MAAM,kBAAkB;AAAA,MAC9C,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,MAAM;AAAA,EACR;AACA,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,MAAI,QAAQ,WAAW,mBAAmB;AACxC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACA,MAAI,QAAQ,WAAW,gBAAgB;AACrC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AAAA,EACF;AACF;AAcA,eAAsB,4BACpB,IACA,WACA,OACiB;AACjB,QAAM,gBAAgB,MAAM;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,MACE;AAAA,MACA,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,IAC1C;AAAA,IACA,EAAE,QAAQ,CAAC,IAAI,EAAE;AAAA,IACjB;AAAA,EACF;AACA,MAAI,cAAc,WAAW,EAAG,QAAO;AACvC,QAAM,kBAAmB,cAAwC,IAAI,CAAC,MAAM,EAAE,EAAE;AAChF,QAAM,QAAQ,MAAM,GAAG;AAAA,IACrB;AAAA,IACA;AAAA,MACE,wBAAwB,EAAE,KAAK,gBAAgB;AAAA,MAC/C,WAAW;AAAA,MACX,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,IAC1C;AAAA,EACF;AACA,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAMO,MAAM,+BAA+B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
async function resolveOAuthClientCredentials(credentialsService, providerKey, scope) {
|
|
2
|
+
if (!credentialsService) return null;
|
|
3
|
+
const integrationId = `channel_${providerKey}`;
|
|
4
|
+
const organizations = [scope.organizationId, null];
|
|
5
|
+
const tried = /* @__PURE__ */ new Set();
|
|
6
|
+
for (const organizationId of organizations) {
|
|
7
|
+
if (tried.has(organizationId)) continue;
|
|
8
|
+
tried.add(organizationId);
|
|
9
|
+
let row = null;
|
|
10
|
+
try {
|
|
11
|
+
row = await credentialsService.resolve(integrationId, {
|
|
12
|
+
tenantId: scope.tenantId,
|
|
13
|
+
organizationId,
|
|
14
|
+
userId: null
|
|
15
|
+
});
|
|
16
|
+
} catch (resolveErr) {
|
|
17
|
+
console.warn(
|
|
18
|
+
"[internal] [communication_channels] resolveOAuthClientCredentials: credential resolve failed for an org scope:",
|
|
19
|
+
resolveErr instanceof Error ? resolveErr.message : resolveErr
|
|
20
|
+
);
|
|
21
|
+
row = null;
|
|
22
|
+
}
|
|
23
|
+
if (row && typeof row.clientId === "string" && row.clientId.length > 0) {
|
|
24
|
+
return row;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
resolveOAuthClientCredentials
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=oauth-client-config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/oauth-client-config.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Minimal shape of the integrations credentials service `resolve` we depend on.\n * Declared locally so this helper compiles even when the integrations module is\n * disabled in a downstream app.\n */\ntype CredentialsResolver = {\n resolve: (\n integrationId: string,\n scope: { tenantId: string; organizationId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n}\n\nexport type OAuthClientCredentialsScope = {\n tenantId: string\n organizationId: string | null\n}\n\n/**\n * Resolve a provider's tenant-level OAuth *client application* credentials\n * (clientId / clientSecret / scopes) configured by an admin under the\n * `channel_<provider>` integration in the Integrations UI.\n *\n * Why `channel_<provider>` and NOT `oauth_<provider>`: each provider package\n * registers its OAuth client-credential fields on the `channel_<provider>`\n * integration \u2014 that is the row the admin edits and the health check reads.\n * Earlier code resolved a phantom `oauth_<provider>` id that nothing ever\n * writes, so every connect / code-exchange / refresh failed with\n * \"Invalid \u2026 OAuth client credentials: expected string, received undefined\"\n * even while the integration showed as configured and healthy.\n *\n * Scoping: the client app config is stored at TENANT scope (`userId = null`),\n * distinct from the per-user OAuth *tokens* that live under the SAME\n * `channel_<provider>` id at USER scope. We therefore always resolve at\n * `userId: null`, trying the caller's organization first and then the\n * organization-agnostic (`organizationId: null`) row, so a single platform /\n * tenant OAuth app can serve every organization (and so a config saved while\n * the admin had no active organization is still found).\n *\n * Returns `null` when no usable client row exists \u2014 callers MUST surface an\n * actionable \"provider not configured\" error instead of handing an empty\n * object to the adapter.\n */\nexport async function resolveOAuthClientCredentials(\n credentialsService: CredentialsResolver | null | undefined,\n providerKey: string,\n scope: OAuthClientCredentialsScope,\n): Promise<Record<string, unknown> | null> {\n if (!credentialsService) return null\n const integrationId = `channel_${providerKey}`\n const organizations: Array<string | null> = [scope.organizationId, null]\n const tried = new Set<string | null>()\n for (const organizationId of organizations) {\n if (tried.has(organizationId)) continue\n tried.add(organizationId)\n let row: Record<string, unknown> | null = null\n try {\n // `organizationId` may be `null` to match the organization-agnostic row;\n // the credentials filter translates `null` into a SQL `IS NULL` match.\n row = await credentialsService.resolve(integrationId, {\n tenantId: scope.tenantId,\n organizationId: organizationId as unknown as string,\n userId: null,\n })\n } catch (resolveErr) {\n // A resolve error (e.g. a transient DB issue) for this org scope shouldn't\n // abort the lookup \u2014 fall through to the next scope \u2014 but surface it so a\n // real misconfiguration isn't silently swallowed.\n console.warn(\n '[internal] [communication_channels] resolveOAuthClientCredentials: credential resolve failed for an org scope:',\n resolveErr instanceof Error ? resolveErr.message : resolveErr,\n )\n row = null\n }\n if (row && typeof row.clientId === 'string' && row.clientId.length > 0) {\n return row\n }\n }\n return null\n}\n"],
|
|
5
|
+
"mappings": "AA0CA,eAAsB,8BACpB,oBACA,aACA,OACyC;AACzC,MAAI,CAAC,mBAAoB,QAAO;AAChC,QAAM,gBAAgB,WAAW,WAAW;AAC5C,QAAM,gBAAsC,CAAC,MAAM,gBAAgB,IAAI;AACvE,QAAM,QAAQ,oBAAI,IAAmB;AACrC,aAAW,kBAAkB,eAAe;AAC1C,QAAI,MAAM,IAAI,cAAc,EAAG;AAC/B,UAAM,IAAI,cAAc;AACxB,QAAI,MAAsC;AAC1C,QAAI;AAGF,YAAM,MAAM,mBAAmB,QAAQ,eAAe;AAAA,QACpD,UAAU,MAAM;AAAA,QAChB;AAAA,QACA,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,SAAS,YAAY;AAInB,cAAQ;AAAA,QACN;AAAA,QACA,sBAAsB,QAAQ,WAAW,UAAU;AAAA,MACrD;AACA,YAAM;AAAA,IACR;AACA,QAAI,OAAO,OAAO,IAAI,aAAa,YAAY,IAAI,SAAS,SAAS,GAAG;AACtE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const ALGORITHM = "aes-256-gcm";
|
|
3
|
+
const IV_LENGTH = 12;
|
|
4
|
+
const TAG_LENGTH = 16;
|
|
5
|
+
const COMMUNICATION_CHANNELS_OAUTH_STATE_TTL_MS = 5 * 60 * 1e3;
|
|
6
|
+
const HKDF_SALT = Buffer.from("open-mercato-channels-oauth-state-v1");
|
|
7
|
+
const HKDF_INFO = Buffer.from("communication_channels-oauth-state-cookie");
|
|
8
|
+
const COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME = "om_cc_oauth_state";
|
|
9
|
+
const DEFAULT_OAUTH_RETURN_URL = "/backend/profile/communication-channels";
|
|
10
|
+
class OAuthStateError extends Error {
|
|
11
|
+
constructor(message, code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "OAuthStateError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isSafeOAuthReturnUrl(value) {
|
|
18
|
+
if (typeof value !== "string") return false;
|
|
19
|
+
if (value.length === 0 || value.length > 2048) return false;
|
|
20
|
+
if (!value.startsWith("/") || value.startsWith("//")) return false;
|
|
21
|
+
if (value.includes("\\")) return false;
|
|
22
|
+
try {
|
|
23
|
+
const base = new URL("https://open-mercato.local");
|
|
24
|
+
const parsed = new URL(value, base);
|
|
25
|
+
return parsed.origin === base.origin && parsed.pathname.startsWith("/");
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function normalizeOAuthReturnUrl(value, fallback = DEFAULT_OAUTH_RETURN_URL) {
|
|
31
|
+
return isSafeOAuthReturnUrl(value) ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
function deriveKey(secret) {
|
|
34
|
+
return Buffer.from(crypto.hkdfSync("sha256", secret, HKDF_SALT, HKDF_INFO, 32));
|
|
35
|
+
}
|
|
36
|
+
function getSecret() {
|
|
37
|
+
const dedicated = process.env.OM_HUB_OAUTH_STATE_KEY ?? process.env.KMS_MASTER_KEY;
|
|
38
|
+
if (dedicated) return dedicated;
|
|
39
|
+
if (process.env.NODE_ENV === "production") {
|
|
40
|
+
throw new Error("[internal] OM_HUB_OAUTH_STATE_KEY or KMS_MASTER_KEY required in production");
|
|
41
|
+
}
|
|
42
|
+
const fallback = process.env.JWT_SECRET;
|
|
43
|
+
if (!fallback) {
|
|
44
|
+
throw new OAuthStateError(
|
|
45
|
+
"OM_HUB_OAUTH_STATE_KEY (or fallback KMS_MASTER_KEY / JWT_SECRET) must be set",
|
|
46
|
+
"missing_secret"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
function encryptOAuthState(payload) {
|
|
52
|
+
const key = deriveKey(getSecret());
|
|
53
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
54
|
+
const json = JSON.stringify(payload);
|
|
55
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
56
|
+
const ciphertext = Buffer.concat([cipher.update(json, "utf8"), cipher.final()]);
|
|
57
|
+
const tag = cipher.getAuthTag();
|
|
58
|
+
return Buffer.concat([iv, tag, ciphertext]).toString("base64url");
|
|
59
|
+
}
|
|
60
|
+
function decryptOAuthState(cookie) {
|
|
61
|
+
try {
|
|
62
|
+
const key = deriveKey(getSecret());
|
|
63
|
+
const combined = Buffer.from(cookie, "base64url");
|
|
64
|
+
if (combined.length < IV_LENGTH + TAG_LENGTH) return null;
|
|
65
|
+
const iv = combined.subarray(0, IV_LENGTH);
|
|
66
|
+
const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
67
|
+
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH);
|
|
68
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
69
|
+
decipher.setAuthTag(tag);
|
|
70
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
71
|
+
return JSON.parse(decrypted);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function verifyOAuthState(input) {
|
|
77
|
+
if (!input.cookie) {
|
|
78
|
+
throw new OAuthStateError("Missing state cookie", "invalid_cookie");
|
|
79
|
+
}
|
|
80
|
+
const payload = decryptOAuthState(input.cookie);
|
|
81
|
+
if (!payload) {
|
|
82
|
+
throw new OAuthStateError("Invalid state cookie", "decrypt_failed");
|
|
83
|
+
}
|
|
84
|
+
const now = input.now ?? Date.now();
|
|
85
|
+
if (payload.expiresAt < now) {
|
|
86
|
+
throw new OAuthStateError("State cookie expired", "expired");
|
|
87
|
+
}
|
|
88
|
+
if (payload.userId !== input.expectedUserId) {
|
|
89
|
+
throw new OAuthStateError("State cookie userId mismatch", "user_mismatch");
|
|
90
|
+
}
|
|
91
|
+
if (input.expectedProviderKey && payload.providerKey !== input.expectedProviderKey) {
|
|
92
|
+
throw new OAuthStateError("State cookie providerKey mismatch", "invalid_cookie");
|
|
93
|
+
}
|
|
94
|
+
if (input.expectedState && payload.state !== input.expectedState) {
|
|
95
|
+
throw new OAuthStateError("State cookie state nonce mismatch", "invalid_cookie");
|
|
96
|
+
}
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
function createOAuthState(params) {
|
|
100
|
+
const state = crypto.randomBytes(32).toString("base64url");
|
|
101
|
+
const nonce = crypto.randomBytes(16).toString("base64url");
|
|
102
|
+
const payload = {
|
|
103
|
+
state,
|
|
104
|
+
nonce,
|
|
105
|
+
userId: params.userId,
|
|
106
|
+
tenantId: params.tenantId,
|
|
107
|
+
organizationId: params.organizationId ?? null,
|
|
108
|
+
providerKey: params.providerKey,
|
|
109
|
+
returnUrl: params.returnUrl,
|
|
110
|
+
extra: params.extra,
|
|
111
|
+
expiresAt: Date.now() + COMMUNICATION_CHANNELS_OAUTH_STATE_TTL_MS
|
|
112
|
+
};
|
|
113
|
+
const cookie = encryptOAuthState(payload);
|
|
114
|
+
return { payload, cookie, stateParam: state };
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME,
|
|
118
|
+
COMMUNICATION_CHANNELS_OAUTH_STATE_TTL_MS,
|
|
119
|
+
DEFAULT_OAUTH_RETURN_URL,
|
|
120
|
+
OAuthStateError,
|
|
121
|
+
createOAuthState,
|
|
122
|
+
decryptOAuthState,
|
|
123
|
+
encryptOAuthState,
|
|
124
|
+
isSafeOAuthReturnUrl,
|
|
125
|
+
normalizeOAuthReturnUrl,
|
|
126
|
+
verifyOAuthState
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=oauth-state.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/oauth-state.ts"],
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\n\n/**\n * OAuth state-cookie helper for the communication_channels hub.\n *\n * **Ported (re-implemented locally), NOT imported, from `packages/enterprise/src/modules/sso/lib/state-cookie.ts`.**\n * Root `AGENTS.md` rule: `@open-mercato/core` MUST NOT import from `@open-mercato/enterprise`.\n *\n * Design (per email integration spec \u00A7 OSS Independence + \u00A7 Hub Deltas \u2192 Delta 7):\n * - AES-256-GCM payload encryption.\n * - HKDF (SHA-256) key derivation from `OM_HUB_OAUTH_STATE_KEY` (falling back to\n * `KMS_MASTER_KEY`). In production those dedicated keys are required; only in\n * dev/test do we fall back to `JWT_SECRET` so envs that configure one secret\n * still work. Production refuses the `JWT_SECRET` fallback so a session-secret\n * leak cannot also forge OAuth-state cookies.\n * - 5-minute TTL \u2014 short window to bound replay surface.\n * - Payload binds the initiating `userId` so the callback rejects state cookies\n * used by a different session.\n *\n * The output is a base64url string that we set on an HttpOnly + SameSite=Lax cookie.\n * Forgery requires the encryption key (KMS-managed in production).\n */\n\nconst ALGORITHM = 'aes-256-gcm'\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nexport const COMMUNICATION_CHANNELS_OAUTH_STATE_TTL_MS = 5 * 60 * 1000\nconst HKDF_SALT = Buffer.from('open-mercato-channels-oauth-state-v1')\nconst HKDF_INFO = Buffer.from('communication_channels-oauth-state-cookie')\n\nexport const COMMUNICATION_CHANNELS_OAUTH_STATE_COOKIE_NAME =\n 'om_cc_oauth_state'\n\nexport const DEFAULT_OAUTH_RETURN_URL = '/backend/profile/communication-channels'\n\n/** Errors thrown by the helpers. Stable for tests + route mapping. */\nexport class OAuthStateError extends Error {\n override name = 'OAuthStateError'\n constructor(\n message: string,\n readonly code:\n | 'missing_secret'\n | 'invalid_cookie'\n | 'expired'\n | 'user_mismatch'\n | 'decrypt_failed',\n ) {\n super(message)\n }\n}\n\n/**\n * Canonical state-cookie payload \u2014 provider-agnostic. Each OAuth provider\n * adapter (Gmail, \u2026) packs its own per-flow nonce / verifier into\n * the `extra` field rather than extending this shape.\n */\nexport interface OAuthStatePayload {\n /** Nonce-like opaque value mirrored into the OAuth `state` query parameter. */\n state: string\n /** Per-flow CSRF nonce, returned alongside the OAuth response. */\n nonce: string\n /** Tenant-scoped user that initiated the flow. Validated on callback. */\n userId: string\n /** Tenant scope so the callback can pin the channel to the same tenant. */\n tenantId: string\n /** Optional organization id for multi-org tenants. */\n organizationId?: string | null\n /** Provider key (e.g. `gmail`) \u2014 routes the callback. */\n providerKey: string\n /** Where to redirect on success. Defaults to the profile page in the route. */\n returnUrl?: string\n /** Wall-clock expiry (ms since epoch). */\n expiresAt: number\n /** Provider-specific extras (PKCE code_verifier, scopes, login_hint, \u2026). */\n extra?: Record<string, unknown>\n}\n\nexport function isSafeOAuthReturnUrl(value: string | null | undefined): value is string {\n if (typeof value !== 'string') return false\n if (value.length === 0 || value.length > 2048) return false\n if (!value.startsWith('/') || value.startsWith('//')) return false\n if (value.includes('\\\\')) return false\n try {\n const base = new URL('https://open-mercato.local')\n const parsed = new URL(value, base)\n return parsed.origin === base.origin && parsed.pathname.startsWith('/')\n } catch {\n return false\n }\n}\n\nexport function normalizeOAuthReturnUrl(\n value: string | null | undefined,\n fallback: string = DEFAULT_OAUTH_RETURN_URL,\n): string {\n return isSafeOAuthReturnUrl(value) ? value : fallback\n}\n\nfunction deriveKey(secret: string): Buffer {\n return Buffer.from(crypto.hkdfSync('sha256', secret, HKDF_SALT, HKDF_INFO, 32))\n}\n\nfunction getSecret(): string {\n const dedicated = process.env.OM_HUB_OAUTH_STATE_KEY ?? process.env.KMS_MASTER_KEY\n if (dedicated) return dedicated\n // No dedicated key configured. Fail closed in production rather than deriving\n // the state-cookie key from JWT_SECRET (the platform session-signing secret) \u2014\n // sharing that key means a JWT_SECRET leak also lets an attacker forge OAuth\n // state cookies, bypassing the userId/tenant binding. In non-production we fall\n // back to JWT_SECRET so dev/test envs that only configure one secret still work.\n if (process.env.NODE_ENV === 'production') {\n throw new Error('[internal] OM_HUB_OAUTH_STATE_KEY or KMS_MASTER_KEY required in production')\n }\n const fallback = process.env.JWT_SECRET\n if (!fallback) {\n throw new OAuthStateError(\n 'OM_HUB_OAUTH_STATE_KEY (or fallback KMS_MASTER_KEY / JWT_SECRET) must be set',\n 'missing_secret',\n )\n }\n return fallback\n}\n\n/** Encrypt + sign a state payload. Output is a base64url string suitable for a cookie. */\nexport function encryptOAuthState(payload: OAuthStatePayload): string {\n const key = deriveKey(getSecret())\n const iv = crypto.randomBytes(IV_LENGTH)\n const json = JSON.stringify(payload)\n\n const cipher = crypto.createCipheriv(ALGORITHM, key, iv)\n const ciphertext = Buffer.concat([cipher.update(json, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n\n return Buffer.concat([iv, tag, ciphertext]).toString('base64url')\n}\n\n/**\n * Decrypt + verify a state cookie. Returns the payload or `null` if the cookie\n * is malformed / tampered. Returns the payload (NOT null) when the cookie has\n * expired \u2014 callers should check `expiresAt` themselves with the verification\n * helper below for stable status codes.\n */\nexport function decryptOAuthState(cookie: string): OAuthStatePayload | null {\n try {\n const key = deriveKey(getSecret())\n const combined = Buffer.from(cookie, 'base64url')\n if (combined.length < IV_LENGTH + TAG_LENGTH) return null\n\n const iv = combined.subarray(0, IV_LENGTH)\n const tag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH)\n const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH)\n\n const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)\n decipher.setAuthTag(tag)\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')\n\n return JSON.parse(decrypted) as OAuthStatePayload\n } catch {\n return null\n }\n}\n\n/**\n * Verify a state cookie against the current session.\n *\n * Throws an {@link OAuthStateError} with a stable `code` field on any check\n * failure so route handlers can map to consistent HTTP responses + redirect\n * flash codes.\n */\nexport function verifyOAuthState(input: {\n cookie: string | null | undefined\n expectedUserId: string\n expectedProviderKey?: string\n expectedState?: string\n now?: number\n}): OAuthStatePayload {\n if (!input.cookie) {\n throw new OAuthStateError('Missing state cookie', 'invalid_cookie')\n }\n const payload = decryptOAuthState(input.cookie)\n if (!payload) {\n throw new OAuthStateError('Invalid state cookie', 'decrypt_failed')\n }\n const now = input.now ?? Date.now()\n if (payload.expiresAt < now) {\n throw new OAuthStateError('State cookie expired', 'expired')\n }\n if (payload.userId !== input.expectedUserId) {\n throw new OAuthStateError('State cookie userId mismatch', 'user_mismatch')\n }\n if (input.expectedProviderKey && payload.providerKey !== input.expectedProviderKey) {\n throw new OAuthStateError('State cookie providerKey mismatch', 'invalid_cookie')\n }\n if (input.expectedState && payload.state !== input.expectedState) {\n throw new OAuthStateError('State cookie state nonce mismatch', 'invalid_cookie')\n }\n return payload\n}\n\n/**\n * Create a fresh state payload + matching `state` query parameter. PKCE\n * verifiers are NOT generated here \u2014 the provider adapter decides whether it\n * needs PKCE and packs the verifier into `extra` itself.\n */\nexport function createOAuthState(params: {\n userId: string\n tenantId: string\n organizationId?: string | null\n providerKey: string\n returnUrl?: string\n extra?: Record<string, unknown>\n}): { payload: OAuthStatePayload; cookie: string; stateParam: string } {\n const state = crypto.randomBytes(32).toString('base64url')\n const nonce = crypto.randomBytes(16).toString('base64url')\n const payload: OAuthStatePayload = {\n state,\n nonce,\n userId: params.userId,\n tenantId: params.tenantId,\n organizationId: params.organizationId ?? null,\n providerKey: params.providerKey,\n returnUrl: params.returnUrl,\n extra: params.extra,\n expiresAt: Date.now() + COMMUNICATION_CHANNELS_OAUTH_STATE_TTL_MS,\n }\n const cookie = encryptOAuthState(payload)\n return { payload, cookie, stateParam: state }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function tokenResponseToExpiresAt(token, nowMs = Date.now()) {
|
|
2
|
+
if (typeof token.expires_in !== "number") return void 0;
|
|
3
|
+
return new Date(nowMs + token.expires_in * 1e3);
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_OAUTH_TOKEN_TIMEOUT_MS = 1e4;
|
|
6
|
+
function resolveTokenTimeoutMs() {
|
|
7
|
+
const fromEnv = Number.parseInt(process.env.OM_OAUTH_TOKEN_TIMEOUT_MS ?? "", 10);
|
|
8
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_OAUTH_TOKEN_TIMEOUT_MS;
|
|
9
|
+
}
|
|
10
|
+
async function requestOAuthToken(tokenUrl, params, options) {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeout = setTimeout(() => controller.abort(), resolveTokenTimeoutMs());
|
|
13
|
+
let res;
|
|
14
|
+
try {
|
|
15
|
+
res = await fetch(tokenUrl, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
18
|
+
body: params.toString(),
|
|
19
|
+
signal: controller.signal
|
|
20
|
+
});
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
23
|
+
throw new Error(`${options.errorLabel}: token endpoint timed out`);
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`${options.errorLabel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
26
|
+
} finally {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
}
|
|
29
|
+
const raw = await res.text();
|
|
30
|
+
let body;
|
|
31
|
+
try {
|
|
32
|
+
body = JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`${options.errorLabel}: non-JSON response (status ${res.status})`);
|
|
35
|
+
}
|
|
36
|
+
if (!res.ok || body.error) {
|
|
37
|
+
throw new Error(`${options.errorLabel}: ${body.error_description ?? body.error ?? res.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
requestOAuthToken,
|
|
43
|
+
tokenResponseToExpiresAt
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=oauth-token.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/oauth-token.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared OAuth2 token primitives for email channel providers. The authorize-URL\n * shape, PKCE usage, and userinfo handling differ per provider (e.g. Gmail)\n * and stay in each package \u2014 but the token response shape, the\n * form-urlencoded token POST, and the expiry computation are identical, so they\n * live here.\n */\n\nexport interface OAuthTokenResponse {\n access_token: string\n refresh_token?: string\n expires_in?: number\n scope?: string\n token_type?: string\n id_token?: string\n error?: string\n error_description?: string\n}\n\n/** Compute the absolute access-token expiry from `expires_in`, or `undefined` when absent. */\nexport function tokenResponseToExpiresAt(\n token: OAuthTokenResponse,\n nowMs: number = Date.now(),\n): Date | undefined {\n if (typeof token.expires_in !== 'number') return undefined\n return new Date(nowMs + token.expires_in * 1000)\n}\n\n/** Hard timeout for a token endpoint round-trip (ms). Overridable via env. */\nconst DEFAULT_OAUTH_TOKEN_TIMEOUT_MS = 10_000\n\nfunction resolveTokenTimeoutMs(): number {\n const fromEnv = Number.parseInt(process.env.OM_OAUTH_TOKEN_TIMEOUT_MS ?? '', 10)\n return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_OAUTH_TOKEN_TIMEOUT_MS\n}\n\n/**\n * POST a form-urlencoded body to an OAuth token endpoint and return the parsed\n * token response. Throws `${errorLabel}: <reason>` when the endpoint returns a\n * non-2xx status, an `error` field, a non-JSON body, or does not respond within\n * the timeout. Bounding the request matters because token refresh sits on the\n * critical path of every poll/send \u2014 a hung token endpoint must fail fast, not\n * block the worker, and a proxy HTML error page must surface the real status\n * rather than a confusing JSON `SyntaxError`.\n */\nexport async function requestOAuthToken(\n tokenUrl: string,\n params: URLSearchParams,\n options: { errorLabel: string },\n): Promise<OAuthTokenResponse> {\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), resolveTokenTimeoutMs())\n let res: Response\n try {\n res = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: params.toString(),\n signal: controller.signal,\n })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new Error(`${options.errorLabel}: token endpoint timed out`)\n }\n throw new Error(`${options.errorLabel}: ${err instanceof Error ? err.message : String(err)}`)\n } finally {\n clearTimeout(timeout)\n }\n\n const raw = await res.text()\n let body: OAuthTokenResponse\n try {\n body = JSON.parse(raw) as OAuthTokenResponse\n } catch {\n throw new Error(`${options.errorLabel}: non-JSON response (status ${res.status})`)\n }\n if (!res.ok || body.error) {\n throw new Error(`${options.errorLabel}: ${body.error_description ?? body.error ?? res.statusText}`)\n }\n return body\n}\n"],
|
|
5
|
+
"mappings": "AAoBO,SAAS,yBACd,OACA,QAAgB,KAAK,IAAI,GACP;AAClB,MAAI,OAAO,MAAM,eAAe,SAAU,QAAO;AACjD,SAAO,IAAI,KAAK,QAAQ,MAAM,aAAa,GAAI;AACjD;AAGA,MAAM,iCAAiC;AAEvC,SAAS,wBAAgC;AACvC,QAAM,UAAU,OAAO,SAAS,QAAQ,IAAI,6BAA6B,IAAI,EAAE;AAC/E,SAAO,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU;AAC7D;AAWA,eAAsB,kBACpB,UACA,QACA,SAC6B;AAC7B,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,sBAAsB,CAAC;AAC5E,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,UAAU;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,OAAO,SAAS;AAAA,MACtB,QAAQ,WAAW;AAAA,IACrB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,YAAM,IAAI,MAAM,GAAG,QAAQ,UAAU,4BAA4B;AAAA,IACnE;AACA,UAAM,IAAI,MAAM,GAAG,QAAQ,UAAU,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,EAC9F,UAAE;AACA,iBAAa,OAAO;AAAA,EACtB;AAEA,QAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,UAAM,IAAI,MAAM,GAAG,QAAQ,UAAU,+BAA+B,IAAI,MAAM,GAAG;AAAA,EACnF;AACA,MAAI,CAAC,IAAI,MAAM,KAAK,OAAO;AACzB,UAAM,IAAI,MAAM,GAAG,QAAQ,UAAU,KAAK,KAAK,qBAAqB,KAAK,SAAS,IAAI,UAAU,EAAE;AAAA,EACpG;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function isUniqueViolation(err) {
|
|
2
|
+
if (!err || typeof err !== "object") return false;
|
|
3
|
+
const code = err.code;
|
|
4
|
+
if (code === "23505") return true;
|
|
5
|
+
const message = err.message;
|
|
6
|
+
return typeof message === "string" && /duplicate key value|unique constraint/i.test(message);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
isUniqueViolation
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=pg-errors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/pg-errors.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Detect a Postgres unique-constraint violation (SQLSTATE 23505) regardless of\n * the ORM/driver layer that surfaces it. Shared across the hub's commands and\n * lib helpers so duplicate-insert handling stays consistent module-wide.\n */\nexport function isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== 'object') return false\n const code = (err as { code?: string }).code\n if (code === '23505') return true // Postgres unique_violation\n const message = (err as { message?: string }).message\n return typeof message === 'string' && /duplicate key value|unique constraint/i.test(message)\n}\n"],
|
|
5
|
+
"mappings": "AAKO,SAAS,kBAAkB,KAAuB;AACvD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,OAAQ,IAA0B;AACxC,MAAI,SAAS,QAAS,QAAO;AAC7B,QAAM,UAAW,IAA6B;AAC9C,SAAO,OAAO,YAAY,YAAY,yCAAyC,KAAK,OAAO;AAC7F;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function makeClientConfigHealthCheck(options) {
|
|
2
|
+
return {
|
|
3
|
+
async check(credentials) {
|
|
4
|
+
const parsed = options.schema.safeParse(credentials ?? {});
|
|
5
|
+
if (!parsed.success) {
|
|
6
|
+
const first = parsed.error.issues[0];
|
|
7
|
+
return {
|
|
8
|
+
status: "unhealthy",
|
|
9
|
+
message: `${options.providerLabel} OAuth client config invalid: ${first?.message ?? "unknown validation error"}`,
|
|
10
|
+
details: { reason: "invalid_oauth_client" }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
status: "healthy",
|
|
15
|
+
message: `${options.providerLabel} OAuth client configured`,
|
|
16
|
+
details: { clientIdConfigured: true, ...options.healthyDetails?.(parsed.data) ?? {} }
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
makeClientConfigHealthCheck
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=provider-health.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/provider-health.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ZodType } from 'zod'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\n\nexport type EmailHealthStatus = 'healthy' | 'degraded' | 'unhealthy'\n\nexport interface EmailHealthCheckResult {\n status: EmailHealthStatus\n message?: string\n details?: Record<string, unknown>\n}\n\nexport interface EmailHealthCheck {\n check: (credentials: Record<string, unknown> | null, scope: IntegrationScope) => Promise<EmailHealthCheckResult>\n}\n\n/**\n * Build a liveness probe for an OAuth-client-config integration. There is no\n * access token at this layer (the hub passes the tenant-scoped OAuth client\n * config, not per-user channel tokens), so a network call would always 401. The\n * cheap, deterministic probe is: confirm the client config is present and\n * well-formed. Per-user token validity is exercised on the channel itself\n * (send / poll surface `requires_reauth`).\n */\nexport function makeClientConfigHealthCheck<T>(options: {\n schema: ZodType<T>\n providerLabel: string\n healthyDetails?: (parsed: T) => Record<string, unknown>\n}): EmailHealthCheck {\n return {\n async check(credentials) {\n const parsed = options.schema.safeParse(credentials ?? {})\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n return {\n status: 'unhealthy',\n message: `${options.providerLabel} OAuth client config invalid: ${first?.message ?? 'unknown validation error'}`,\n details: { reason: 'invalid_oauth_client' },\n }\n }\n return {\n status: 'healthy',\n message: `${options.providerLabel} OAuth client configured`,\n details: { clientIdConfigured: true, ...(options.healthyDetails?.(parsed.data) ?? {}) },\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAuBO,SAAS,4BAA+B,SAI1B;AACnB,SAAO;AAAA,IACL,MAAM,MAAM,aAAa;AACvB,YAAM,SAAS,QAAQ,OAAO,UAAU,eAAe,CAAC,CAAC;AACzD,UAAI,CAAC,OAAO,SAAS;AACnB,cAAM,QAAQ,OAAO,MAAM,OAAO,CAAC;AACnC,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,GAAG,QAAQ,aAAa,iCAAiC,OAAO,WAAW,0BAA0B;AAAA,UAC9G,SAAS,EAAE,QAAQ,uBAAuB;AAAA,QAC5C;AAAA,MACF;AACA,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,GAAG,QAAQ,aAAa;AAAA,QACjC,SAAS,EAAE,oBAAoB,MAAM,GAAI,QAAQ,iBAAiB,OAAO,IAAI,KAAK,CAAC,EAAG;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const PUSH_STATE_KEYS = [
|
|
2
|
+
"pushStatus",
|
|
3
|
+
"watchExpirationMs",
|
|
4
|
+
"pubsubTopic",
|
|
5
|
+
"lastPushError"
|
|
6
|
+
];
|
|
7
|
+
function preservePushState(previous, next) {
|
|
8
|
+
const prev = previous && typeof previous === "object" && !Array.isArray(previous) ? previous : {};
|
|
9
|
+
const merged = { ...next };
|
|
10
|
+
for (const key of PUSH_STATE_KEYS) {
|
|
11
|
+
if (!(key in merged) && key in prev) merged[key] = prev[key];
|
|
12
|
+
}
|
|
13
|
+
return merged;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
PUSH_STATE_KEYS,
|
|
17
|
+
preservePushState
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=push-state.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/push-state.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Channel-state keys owned by the push-delivery lifecycle (Spec C), written by the\n * push register/renew commands rather than the sync cursor. They must survive a\n * sync-cursor replacement so watch/subscription renewal keeps working.\n */\nexport const PUSH_STATE_KEYS = [\n 'pushStatus',\n 'watchExpirationMs',\n 'pubsubTopic',\n 'lastPushError',\n] as const\n\n/**\n * Replace the channel sync-cursor state with the adapter's freshly decoded cursor\n * while carrying forward the hub-owned push keys the cursor does not manage.\n *\n * This MUST be a full replace, never a `{ ...previous, ...next }` spread: adapters\n * signal \"drain finished\" by OMITTING the mid-drain resumption tokens\n * (`pendingHistoryPageToken`, `pendingMessagesPageToken`) from\n * `next` (they are set to `undefined`, which `JSON.stringify` drops from the encoded\n * cursor). A spread would retain a stale token from `previous` and mis-route the\n * next push/poll cycle, so the poll worker and both push-sync workers share this\n * helper to stay consistent.\n */\nexport function preservePushState(\n previous: unknown,\n next: Record<string, unknown>,\n): Record<string, unknown> {\n const prev =\n previous && typeof previous === 'object' && !Array.isArray(previous)\n ? (previous as Record<string, unknown>)\n : {}\n const merged: Record<string, unknown> = { ...next }\n for (const key of PUSH_STATE_KEYS) {\n if (!(key in merged) && key in prev) merged[key] = prev[key]\n }\n return merged\n}\n"],
|
|
5
|
+
"mappings": "AAKO,MAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAcO,SAAS,kBACd,UACA,MACyB;AACzB,QAAM,OACJ,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,IAC9D,WACD,CAAC;AACP,QAAM,SAAkC,EAAE,GAAG,KAAK;AAClD,aAAW,OAAO,iBAAiB;AACjC,QAAI,EAAE,OAAO,WAAW,OAAO,KAAM,QAAO,GAAG,IAAI,KAAK,GAAG;AAAA,EAC7D;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createModuleQueue } from "@open-mercato/queue";
|
|
2
|
+
const queues = /* @__PURE__ */ new Map();
|
|
3
|
+
function getCommunicationChannelsQueue(queueName) {
|
|
4
|
+
const existing = queues.get(queueName);
|
|
5
|
+
if (existing) return existing;
|
|
6
|
+
const concurrency = Math.min(
|
|
7
|
+
20,
|
|
8
|
+
Math.max(
|
|
9
|
+
1,
|
|
10
|
+
Number.parseInt(process.env.COMMUNICATION_CHANNELS_QUEUE_CONCURRENCY ?? "10", 10) || 10
|
|
11
|
+
)
|
|
12
|
+
);
|
|
13
|
+
const created = createModuleQueue(queueName, { concurrency });
|
|
14
|
+
queues.set(queueName, created);
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
const COMMUNICATION_CHANNELS_QUEUES = {
|
|
18
|
+
inbound: "communication-channels-inbound",
|
|
19
|
+
outbound: "communication-channels-outbound",
|
|
20
|
+
reactions: "communication-channels-reactions",
|
|
21
|
+
/**
|
|
22
|
+
* Per-channel polling queue (email integration spec — Phase 0 Delta 6).
|
|
23
|
+
* Populated by `poll-tick` every scheduler tick; one entry per due channel.
|
|
24
|
+
* Processed by `workers/poll-channel.ts`.
|
|
25
|
+
*/
|
|
26
|
+
poll: "communication-channels-poll",
|
|
27
|
+
/**
|
|
28
|
+
* Hub-internal tick queue (email integration spec — Phase 0 Delta 6).
|
|
29
|
+
* One job per scheduler tick (60s default); worker enumerates due channels
|
|
30
|
+
* and fans out to the `poll` queue.
|
|
31
|
+
*/
|
|
32
|
+
pollTick: "communication-channels-poll-tick",
|
|
33
|
+
/**
|
|
34
|
+
* Operator-triggered channel-history import queue (Spec B § Phase B6).
|
|
35
|
+
* One job per `/import-history` call; worker `channel-import-history` runs
|
|
36
|
+
* with concurrency 1 to avoid hammering the provider with parallel scans.
|
|
37
|
+
*/
|
|
38
|
+
importHistory: "communication-channels-import-history",
|
|
39
|
+
/**
|
|
40
|
+
* Spec C § Phase C2 — Gmail Pub/Sub push delivery. The webhook enqueues
|
|
41
|
+
* one job per verified notification; the worker calls
|
|
42
|
+
* `adapter.applyPushNotification` (which delegates to `history.list`).
|
|
43
|
+
*/
|
|
44
|
+
gmailHistorySync: "communication-channels-gmail-history-sync",
|
|
45
|
+
/**
|
|
46
|
+
* Spec C § Phase C4 — Renewal cron queues (daily / 2h cadence).
|
|
47
|
+
*/
|
|
48
|
+
gmailRenewWatch: "communication-channels-gmail-renew-watch"
|
|
49
|
+
};
|
|
50
|
+
export {
|
|
51
|
+
COMMUNICATION_CHANNELS_QUEUES,
|
|
52
|
+
getCommunicationChannelsQueue
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=queue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/queue.ts"],
|
|
4
|
+
"sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\n\n/**\n * Queue helper for the communication_channels hub. Mirrors the\n * shipping_carriers pattern (`getShippingCarrierQueue`) so the route\n * and the worker share the same queue instance.\n *\n * Worker concurrency is also tunable via env (`COMMUNICATION_CHANNELS_QUEUE_CONCURRENCY`)\n * with a sensible default of 10 (per SPEC-045d \u00A76 inbound flow) and a hard ceiling of\n * 20 (ARCHITECTURE \u00A719 caps queue/worker concurrency at 20).\n */\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nexport function getCommunicationChannelsQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.min(\n 20,\n Math.max(\n 1,\n Number.parseInt(process.env.COMMUNICATION_CHANNELS_QUEUE_CONCURRENCY ?? '10', 10) || 10,\n ),\n )\n const created = createModuleQueue<Record<string, unknown>>(queueName, { concurrency })\n queues.set(queueName, created)\n return created\n}\n\n/** Canonical queue names exposed by the hub. */\nexport const COMMUNICATION_CHANNELS_QUEUES = {\n inbound: 'communication-channels-inbound',\n outbound: 'communication-channels-outbound',\n reactions: 'communication-channels-reactions',\n /**\n * Per-channel polling queue (email integration spec \u2014 Phase 0 Delta 6).\n * Populated by `poll-tick` every scheduler tick; one entry per due channel.\n * Processed by `workers/poll-channel.ts`.\n */\n poll: 'communication-channels-poll',\n /**\n * Hub-internal tick queue (email integration spec \u2014 Phase 0 Delta 6).\n * One job per scheduler tick (60s default); worker enumerates due channels\n * and fans out to the `poll` queue.\n */\n pollTick: 'communication-channels-poll-tick',\n /**\n * Operator-triggered channel-history import queue (Spec B \u00A7 Phase B6).\n * One job per `/import-history` call; worker `channel-import-history` runs\n * with concurrency 1 to avoid hammering the provider with parallel scans.\n */\n importHistory: 'communication-channels-import-history',\n /**\n * Spec C \u00A7 Phase C2 \u2014 Gmail Pub/Sub push delivery. The webhook enqueues\n * one job per verified notification; the worker calls\n * `adapter.applyPushNotification` (which delegates to `history.list`).\n */\n gmailHistorySync: 'communication-channels-gmail-history-sync',\n /**\n * Spec C \u00A7 Phase C4 \u2014 Renewal cron queues (daily / 2h cadence).\n */\n gmailRenewWatch: 'communication-channels-gmail-renew-watch',\n} as const\n\nexport type CommunicationChannelsQueueName =\n (typeof COMMUNICATION_CHANNELS_QUEUES)[keyof typeof COMMUNICATION_CHANNELS_QUEUES]\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAqC;AAW9C,MAAM,SAAS,oBAAI,IAA4C;AAExD,SAAS,8BAA8B,WAAmD;AAC/F,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA,KAAK;AAAA,MACH;AAAA,MACA,OAAO,SAAS,QAAQ,IAAI,4CAA4C,MAAM,EAAE,KAAK;AAAA,IACvF;AAAA,EACF;AACA,QAAM,UAAU,kBAA2C,WAAW,EAAE,YAAY,CAAC;AACrF,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;AAGO,MAAM,gCAAgC;AAAA,EAC3C,SAAS;AAAA,EACT,UAAU;AAAA,EACV,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMX,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMN,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMV,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMf,kBAAkB;AAAA;AAAA;AAAA;AAAA,EAIlB,iBAAiB;AACnB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/reaction-processor-types.ts"],
|
|
4
|
+
"sourcesContent": ["import type { InboundReactionEvent } from './adapter'\n\n/**\n * Discriminated union of reaction-queue job payloads.\n *\n * Split into its own file so command modules can reference these types without\n * forming a circular import with the worker module (which references commands).\n */\n\nexport type ReactionScope = {\n tenantId: string\n organizationId: string | null\n}\n\nexport type ReactionJobBase = {\n providerKey: string\n channelId: string\n scope: ReactionScope\n /** Attempt number, 1-based. */\n attempt?: number\n}\n\nexport type ReactionInboundJob = ReactionJobBase & {\n kind: 'inbound'\n channelType: string\n event: InboundReactionEvent\n}\n\nexport type ReactionOutboundSendJob = ReactionJobBase & {\n kind: 'outbound_send'\n messageId: string\n reactionId: string\n emoji: string\n /** External conversation reference for the provider call (e.g. Slack thread_ts). */\n conversationId?: string\n}\n\nexport type ReactionOutboundRemoveJob = ReactionJobBase & {\n kind: 'outbound_remove'\n messageId: string\n emoji: string\n externalReactionId: string | null\n conversationId?: string\n}\n\nexport type ReactionProcessorPayload =\n | ReactionInboundJob\n | ReactionOutboundSendJob\n | ReactionOutboundRemoveJob\n\nexport const REACTION_PROCESSOR_MAX_ATTEMPTS = 3\n"],
|
|
5
|
+
"mappings": "AAkDO,MAAM,kCAAkC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|