@open-mercato/core 0.6.6-develop.6465.1.019f0fb26f → 0.6.6-develop.6471.1.9ab5bdd79a
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/dist/bootstrap.js +13 -11
- package/dist/bootstrap.js.map +2 -2
- package/dist/helpers/integration/queue-runner.js +4 -2
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/modules/api_keys/services/apiKeyService.js +5 -3
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +4 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +3 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +8 -6
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +5 -3
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/attachments/lib/assignmentDetails.js +3 -1
- package/dist/modules/attachments/lib/assignmentDetails.js.map +2 -2
- package/dist/modules/attachments/lib/drivers/driverFactory.js +3 -1
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +2 -2
- package/dist/modules/attachments/lib/ocrQueue.js +10 -11
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +6 -4
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js +3 -1
- package/dist/modules/audit_logs/api/audit-logs/actions/undo/route.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +3 -1
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/audit_logs/components/AuditLogsActions.js +4 -2
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +4 -2
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +7 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/auth/api/profile/route.js +4 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +3 -1
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +4 -2
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +3 -1
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/sidebar/variants/route.js +3 -1
- package/dist/modules/auth/api/sidebar/variants/route.js.map +2 -2
- package/dist/modules/auth/api/users/resend-invite/route.js +4 -2
- package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +3 -1
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js +3 -1
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js +3 -1
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +5 -3
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +4 -2
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/cli.js +10 -0
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/commands/users.js +4 -2
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/lib/consentIntegrity.js +3 -3
- package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +9 -10
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/lib/tenantAccess.js +3 -1
- package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
- package/dist/modules/auth/lib/tokenHash.js +3 -3
- package/dist/modules/auth/lib/tokenHash.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +4 -2
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +7 -5
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +5 -3
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +11 -12
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js +3 -1
- package/dist/modules/business_rules/subscribers/crud-rule-trigger.js.map +2 -2
- package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js +3 -1
- package/dist/modules/business_rules/subscribers/rule-execution-failed-notification.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +4 -5
- package/dist/modules/catalog/ai-agents-context.js.map +2 -2
- package/dist/modules/catalog/ai-tools/_shared.js +3 -1
- package/dist/modules/catalog/ai-tools/_shared.js.map +2 -2
- package/dist/modules/catalog/ai-tools/authoring-pack.js +3 -1
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +2 -2
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +3 -1
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +2 -2
- package/dist/modules/catalog/api/dictionaries/[key]/route.js +3 -1
- package/dist/modules/catalog/api/dictionaries/[key]/route.js.map +2 -2
- package/dist/modules/catalog/api/prices/route.js +3 -1
- package/dist/modules/catalog/api/prices/route.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +5 -3
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/api/settings/route.js +4 -2
- package/dist/modules/catalog/api/settings/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +14 -12
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +8 -6
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +5 -3
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +5 -6
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/optionSchemaClient.js +3 -1
- package/dist/modules/catalog/backend/catalog/products/optionSchemaClient.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +5 -3
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/components/UnitPriceDisplaySettings.js +4 -2
- package/dist/modules/catalog/components/UnitPriceDisplaySettings.js.map +2 -2
- package/dist/modules/catalog/inbox-actions.js +4 -2
- package/dist/modules/catalog/inbox-actions.js.map +2 -2
- package/dist/modules/catalog/seed/examples.js +4 -5
- package/dist/modules/catalog/seed/examples.js.map +2 -2
- package/dist/modules/catalog/subscribers/low-stock-notification.js +3 -1
- package/dist/modules/catalog/subscribers/low-stock-notification.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/state.js +3 -1
- package/dist/modules/catalog/widgets/injection/product-seo/state.js.map +2 -2
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +12 -11
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js +3 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/import-history/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js +3 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/push/register/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +5 -3
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +6 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +4 -6
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/delete-channel.js +3 -3
- package/dist/modules/communication_channels/commands/delete-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +4 -5
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/disconnect-channel.js +3 -3
- package/dist/modules/communication_channels/commands/disconnect-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +5 -12
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-unregister.js +3 -3
- package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
- package/dist/modules/communication_channels/commands/reassign-conversation.js +5 -3
- package/dist/modules/communication_channels/commands/reassign-conversation.js.map +2 -2
- package/dist/modules/communication_channels/lib/dead-letter.js +3 -3
- package/dist/modules/communication_channels/lib/dead-letter.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-mime.js +4 -6
- package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-client-config.js +5 -3
- package/dist/modules/communication_channels/lib/oauth-client-config.js.map +2 -2
- package/dist/modules/communication_channels/lib/send-as-user.js +3 -1
- package/dist/modules/communication_channels/lib/send-as-user.js.map +2 -2
- package/dist/modules/communication_channels/lib/thread-token.js +4 -2
- package/dist/modules/communication_channels/lib/thread-token.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +3 -4
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/communication_channels/workers/channel-import-history.js +4 -6
- package/dist/modules/communication_channels/workers/channel-import-history.js.map +2 -2
- package/dist/modules/communication_channels/workers/gmail-history-sync.js +8 -16
- package/dist/modules/communication_channels/workers/gmail-history-sync.js.map +2 -2
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js +5 -7
- package/dist/modules/communication_channels/workers/gmail-renew-watch.js.map +2 -2
- package/dist/modules/communication_channels/workers/outbound-delivery.js +6 -12
- package/dist/modules/communication_channels/workers/outbound-delivery.js.map +2 -2
- package/dist/modules/communication_channels/workers/poll-channel.js +7 -15
- package/dist/modules/communication_channels/workers/poll-channel.js.map +2 -2
- package/dist/modules/communication_channels/workers/poll-tick.js +5 -6
- package/dist/modules/communication_channels/workers/poll-tick.js.map +2 -2
- package/dist/modules/communication_channels/workers/reaction-processor.js +5 -9
- package/dist/modules/communication_channels/workers/reaction-processor.js.map +2 -2
- package/dist/modules/configs/api/cache/route.js +4 -2
- package/dist/modules/configs/api/cache/route.js.map +2 -2
- package/dist/modules/configs/api/system-status/route.js +4 -2
- package/dist/modules/configs/api/system-status/route.js.map +2 -2
- package/dist/modules/configs/api/upgrade-actions/route.js +4 -2
- package/dist/modules/configs/api/upgrade-actions/route.js.map +2 -2
- package/dist/modules/configs/components/ModuleTelemetryPanel.js +3 -1
- package/dist/modules/configs/components/ModuleTelemetryPanel.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +2 -1
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +2 -2
- package/dist/modules/configs/services/upgradeActionsService.js +6 -3
- package/dist/modules/configs/services/upgradeActionsService.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +3 -1
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js +3 -1
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +2 -2
- package/dist/modules/currencies/services/providers/nbp.js +6 -4
- package/dist/modules/currencies/services/providers/nbp.js.map +2 -2
- package/dist/modules/currencies/services/providers/raiffeisen.js +7 -7
- package/dist/modules/currencies/services/providers/raiffeisen.js.map +2 -2
- package/dist/modules/customer_accounts/api/domain-resolve/all.js +6 -5
- package/dist/modules/customer_accounts/api/domain-resolve/all.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +4 -2
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuthServer.js +3 -1
- package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +3 -1
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js +3 -1
- package/dist/modules/customer_accounts/subscribers/autoLinkCrmReverse.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/unlinkOnCompanyDelete.js +3 -1
- package/dist/modules/customer_accounts/subscribers/unlinkOnCompanyDelete.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/unlinkOnPersonDelete.js +3 -1
- package/dist/modules/customer_accounts/subscribers/unlinkOnPersonDelete.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +4 -5
- package/dist/modules/customers/ai-agents-context.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +6 -4
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/comments/route.js +3 -1
- package/dist/modules/customers/api/comments/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +3 -1
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +5 -3
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +5 -3
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js +3 -1
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/new-customers/route.js +3 -1
- package/dist/modules/customers/api/dashboard/widgets/new-customers/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/new-deals/route.js +3 -1
- package/dist/modules/customers/api/dashboard/widgets/new-deals/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/next-interactions/route.js +3 -1
- package/dist/modules/customers/api/dashboard/widgets/next-interactions/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +3 -1
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +3 -1
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +3 -1
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js +7 -5
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +2 -2
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js +7 -5
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +3 -1
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +3 -1
- package/dist/modules/customers/api/deals/summary/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +4 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +5 -3
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/cache.js +3 -1
- package/dist/modules/customers/api/dictionaries/cache.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/context.js +3 -1
- package/dist/modules/customers/api/dictionaries/context.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/currency/route.js +3 -1
- package/dist/modules/customers/api/dictionaries/currency/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js +4 -2
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +7 -5
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/interactions/cancel/route.js +3 -1
- package/dist/modules/customers/api/interactions/cancel/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/complete/route.js +3 -1
- package/dist/modules/customers/api/interactions/complete/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/conflicts/route.js +3 -1
- package/dist/modules/customers/api/interactions/conflicts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +3 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +7 -4
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +3 -1
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/api/labels/assign/route.js +3 -1
- package/dist/modules/customers/api/labels/assign/route.js.map +2 -2
- package/dist/modules/customers/api/labels/route.js +6 -3
- package/dist/modules/customers/api/labels/route.js.map +2 -2
- package/dist/modules/customers/api/labels/unassign/route.js +3 -1
- package/dist/modules/customers/api/labels/unassign/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -1
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +5 -3
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +5 -3
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +3 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +6 -4
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +6 -4
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +4 -2
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/settings/dictionary-sort-modes/route.js +4 -2
- package/dist/modules/customers/api/settings/dictionary-sort-modes/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +4 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +3 -1
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +3 -1
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/api/todos/route.js +6 -4
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +5 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +3 -1
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +3 -1
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js +4 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +4 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +5 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +3 -1
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/dictionaries.js +3 -1
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +4 -2
- package/dist/modules/customers/commands/interactions.js.map +2 -2
- package/dist/modules/customers/commands/shared.js +4 -2
- package/dist/modules/customers/commands/shared.js.map +2 -2
- package/dist/modules/customers/commands/todos.js +3 -1
- package/dist/modules/customers/commands/todos.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +5 -3
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySortSettings.js +3 -1
- package/dist/modules/customers/components/DictionarySortSettings.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +3 -1
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +3 -1
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +6 -4
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +3 -1
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ChangelogTab.js +3 -1
- package/dist/modules/customers/components/detail/ChangelogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyCard.js +3 -1
- package/dist/modules/customers/components/detail/CompanyCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +3 -1
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/EntityTagsDialog.js +3 -1
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/InlineActivityComposer.js +3 -1
- package/dist/modules/customers/components/detail/InlineActivityComposer.js.map +2 -2
- package/dist/modules/customers/components/detail/ManageTagsDialog.js +3 -1
- package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js +5 -3
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
- package/dist/modules/customers/components/detail/RolesSection.js +4 -2
- package/dist/modules/customers/components/detail/RolesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js +5 -3
- package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js.map +2 -2
- package/dist/modules/customers/components/linking/LinkEntityDialog.js +4 -2
- package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
- package/dist/modules/customers/inbox-actions.js +3 -1
- package/dist/modules/customers/inbox-actions.js.map +2 -2
- package/dist/modules/customers/lib/bulkDeals.js +3 -1
- package/dist/modules/customers/lib/bulkDeals.js.map +2 -2
- package/dist/modules/customers/lib/dealClosureNotification.js +4 -2
- package/dist/modules/customers/lib/dealClosureNotification.js.map +2 -2
- package/dist/modules/customers/lib/dealStageTransitionTable.js +3 -1
- package/dist/modules/customers/lib/dealStageTransitionTable.js.map +2 -2
- package/dist/modules/customers/lib/interactionFeatureFlags.js +3 -1
- package/dist/modules/customers/lib/interactionFeatureFlags.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +3 -1
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/search.js +14 -7
- package/dist/modules/customers/search.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +3 -1
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js +3 -1
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js +3 -1
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js +3 -1
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +3 -1
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +3 -1
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +5 -3
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +3 -1
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +3 -1
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +1 -1
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/data_sync/lib/adapter-registry.js +11 -4
- package/dist/modules/data_sync/lib/adapter-registry.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +23 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +3 -1
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +3 -1
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +4 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js +3 -1
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +4 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js +3 -1
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js +5 -3
- package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
- package/dist/modules/dictionaries/api/context.js +3 -1
- package/dist/modules/dictionaries/api/context.js.map +2 -2
- package/dist/modules/dictionaries/api/route.js +4 -2
- package/dist/modules/dictionaries/api/route.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionariesManager.js +5 -3
- package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +4 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +4 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionarySelectControl.js +3 -1
- package/dist/modules/dictionaries/components/DictionarySelectControl.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +3 -1
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +3 -1
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/api/organization-switcher/route.js +3 -1
- package/dist/modules/directory/api/organization-switcher/route.js.map +2 -2
- package/dist/modules/directory/commands/tenants.js +5 -3
- package/dist/modules/directory/commands/tenants.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +6 -4
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/definitions.cache.js +3 -1
- package/dist/modules/entities/api/definitions.cache.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +7 -5
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -8
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +3 -1
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/entities/lib/makeActivityRoute.js +3 -1
- package/dist/modules/entities/lib/makeActivityRoute.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +3 -1
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/feature_toggles/lib/feature-flag-check.js +5 -9
- package/dist/modules/feature_toggles/lib/feature-flag-check.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -2
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +4 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/route.js +3 -1
- package/dist/modules/inbox_ops/api/emails/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +3 -1
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +3 -1
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js +4 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js +4 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/route.js +3 -1
- package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/routeHelpers.js +3 -1
- package/dist/modules/inbox_ops/api/routeHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +3 -1
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/webhook/inbound.js +7 -5
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/inbox_ops/lib/cache.js +4 -2
- package/dist/modules/inbox_ops/lib/cache.js.map +2 -2
- package/dist/modules/inbox_ops/lib/catalogLookup.js +3 -1
- package/dist/modules/inbox_ops/lib/catalogLookup.js.map +2 -2
- package/dist/modules/inbox_ops/lib/eventBus.js +3 -4
- package/dist/modules/inbox_ops/lib/eventBus.js.map +2 -2
- package/dist/modules/inbox_ops/lib/messagesIntegration.js +4 -8
- package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/executionAuditor.js +3 -1
- package/dist/modules/inbox_ops/subscribers/executionAuditor.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js +10 -8
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/proposalNotifier.js +3 -1
- package/dist/modules/inbox_ops/subscribers/proposalNotifier.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +3 -4
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +3 -1
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +3 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/lib/email-sender.js +3 -7
- package/dist/modules/messages/lib/email-sender.js.map +2 -2
- package/dist/modules/messages/lib/message-objects-registry.js +3 -1
- package/dist/modules/messages/lib/message-objects-registry.js.map +2 -2
- package/dist/modules/messages/lib/message-types-registry.js +3 -1
- package/dist/modules/messages/lib/message-types-registry.js.map +2 -2
- package/dist/modules/messages/workers/send-email.worker.js +6 -4
- package/dist/modules/messages/workers/send-email.worker.js.map +2 -2
- package/dist/modules/notifications/lib/notificationMapper.js +3 -4
- package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +4 -5
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/subscribers/deliver-notification.js +8 -13
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +3 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/perspectives/services/perspectiveService.js +3 -3
- package/dist/modules/perspectives/services/perspectiveService.js.map +2 -2
- package/dist/modules/planner/api/access.js +3 -3
- package/dist/modules/planner/api/access.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +3 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/api/availability-weekly.js +3 -1
- package/dist/modules/planner/api/availability-weekly.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +4 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +3 -1
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +3 -1
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +5 -3
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +4 -2
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +3 -1
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +13 -18
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +8 -6
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +3 -1
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_refresh.js +3 -1
- package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
- package/dist/modules/resources/api/comments.js +3 -1
- package/dist/modules/resources/api/comments.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/assign/route.js +3 -1
- package/dist/modules/resources/api/resources/tags/assign/route.js.map +2 -2
- package/dist/modules/resources/api/resources/tags/unassign/route.js +3 -1
- package/dist/modules/resources/api/resources/tags/unassign/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +3 -1
- package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +4 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/commands/activities.js +6 -5
- package/dist/modules/resources/commands/activities.js.map +2 -2
- package/dist/modules/resources/commands/comments.js +6 -4
- package/dist/modules/resources/commands/comments.js.map +2 -2
- package/dist/modules/resources/commands/shared.js +24 -1
- package/dist/modules/resources/commands/shared.js.map +2 -2
- package/dist/modules/sales/api/channels/route.js +3 -1
- package/dist/modules/sales/api/channels/route.js.map +2 -2
- package/dist/modules/sales/api/document-history/route.js +3 -1
- package/dist/modules/sales/api/document-history/route.js.map +2 -2
- package/dist/modules/sales/api/document-numbers/route.js +3 -1
- package/dist/modules/sales/api/document-numbers/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/accept/route.js +4 -2
- package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/convert/route.js +3 -1
- package/dist/modules/sales/api/quotes/convert/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/public/[token]/route.js +3 -1
- package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/send/route.js +3 -1
- package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
- package/dist/modules/sales/api/returns/[id]/route.js +3 -1
- package/dist/modules/sales/api/returns/[id]/route.js.map +2 -2
- package/dist/modules/sales/api/settings/document-numbers/route.js +4 -2
- package/dist/modules/sales/api/settings/document-numbers/route.js.map +2 -2
- package/dist/modules/sales/api/settings/order-editing/route.js +4 -2
- package/dist/modules/sales/api/settings/order-editing/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +3 -1
- package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -4
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +4 -2
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +20 -18
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +4 -8
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +3 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/dist/modules/sales/commands/shipments.js +3 -1
- package/dist/modules/sales/commands/shipments.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +5 -3
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +4 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +4 -2
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/PaymentMethodsSettings.js +5 -3
- package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
- package/dist/modules/sales/components/ShippingMethodsSettings.js +5 -3
- package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +5 -3
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/TaxRatesSettings.js +5 -3
- package/dist/modules/sales/components/TaxRatesSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/ChannelOfferForm.js +16 -14
- package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +4 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +5 -3
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentDialog.js +3 -1
- package/dist/modules/sales/components/documents/AdjustmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +5 -3
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +6 -4
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +8 -6
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +5 -3
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +4 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +14 -12
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +5 -3
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +9 -7
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +4 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js +4 -2
- package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
- package/dist/modules/sales/seed/examples.js +7 -9
- package/dist/modules/sales/seed/examples.js.map +2 -2
- package/dist/modules/sales/subscribers/quote-expiring-notification.js +3 -1
- package/dist/modules/sales/subscribers/quote-expiring-notification.js.map +2 -2
- package/dist/modules/sales/subscribers/reconcileOnAddressDelete.js +3 -1
- package/dist/modules/sales/subscribers/reconcileOnAddressDelete.js.map +2 -2
- package/dist/modules/sales/subscribers/reconcileOnCompanyDelete.js +3 -1
- package/dist/modules/sales/subscribers/reconcileOnCompanyDelete.js.map +2 -2
- package/dist/modules/sales/subscribers/reconcileOnCustomerDelete.js +3 -1
- package/dist/modules/sales/subscribers/reconcileOnCustomerDelete.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +5 -3
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +3 -1
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +3 -1
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/route.js +5 -0
- package/dist/modules/shipping_carriers/api/tracking/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +3 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +41 -19
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/upstream-error-response.js +3 -1
- package/dist/modules/shipping_carriers/lib/upstream-error-response.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/webhook-processor.js +3 -5
- package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +2 -2
- package/dist/modules/staff/api/comments.js +3 -1
- package/dist/modules/staff/api/comments.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/accept/route.js +3 -1
- package/dist/modules/staff/api/leave-requests/accept/route.js.map +2 -2
- package/dist/modules/staff/api/leave-requests/reject/route.js +3 -1
- package/dist/modules/staff/api/leave-requests/reject/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/assignable/route.js +3 -1
- package/dist/modules/staff/api/team-members/assignable/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/self/route.js +4 -2
- package/dist/modules/staff/api/team-members/self/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/assign/route.js +3 -1
- package/dist/modules/staff/api/team-members/tags/assign/route.js.map +2 -2
- package/dist/modules/staff/api/team-members/tags/unassign/route.js +3 -1
- package/dist/modules/staff/api/team-members/tags/unassign/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js +3 -1
- package/dist/modules/staff/api/timesheets/my-projects/[projectId]/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/my-projects/route.js +3 -1
- package/dist/modules/staff/api/timesheets/my-projects/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/projects/kpis/route.js +3 -1
- package/dist/modules/staff/api/timesheets/projects/kpis/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +3 -1
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +4 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +4 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js +3 -1
- package/dist/modules/staff/api/timesheets/time-entries/bulk/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js +3 -1
- package/dist/modules/staff/api/timesheets/time-entries/start-timer/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js +4 -2
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js +4 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +4 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js +4 -2
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/page.js +7 -5
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js +3 -1
- package/dist/modules/staff/backend/staff/timesheets/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +4 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-entries.js +3 -1
- package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js +3 -1
- package/dist/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.js.map +2 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +5 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +3 -1
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +5 -3
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/get/locales.js +3 -1
- package/dist/modules/translations/api/get/locales.js.map +2 -2
- package/dist/modules/translations/api/put/locales.js +3 -1
- package/dist/modules/translations/api/put/locales.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +3 -1
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/translations/subscribers/cleanup.js +3 -1
- package/dist/modules/translations/subscribers/cleanup.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js +4 -2
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +4 -2
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +6 -4
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +4 -2
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/api/events/[id]/route.js +3 -1
- package/dist/modules/workflows/api/events/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/events/route.js +3 -1
- package/dist/modules/workflows/api/events/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/advance/route.js +3 -1
- package/dist/modules/workflows/api/instances/[id]/advance/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/cancel/route.js +3 -1
- package/dist/modules/workflows/api/instances/[id]/cancel/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/events/route.js +3 -1
- package/dist/modules/workflows/api/instances/[id]/events/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/retry/route.js +3 -1
- package/dist/modules/workflows/api/instances/[id]/retry/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/route.js +3 -1
- package/dist/modules/workflows/api/instances/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/signal/route.js +5 -7
- package/dist/modules/workflows/api/instances/[id]/signal/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/route.js +5 -3
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/validate-start/route.js +3 -1
- package/dist/modules/workflows/api/instances/validate-start/route.js.map +2 -2
- package/dist/modules/workflows/api/signals/route.js +3 -1
- package/dist/modules/workflows/api/signals/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/claim/route.js +3 -1
- package/dist/modules/workflows/api/tasks/[id]/claim/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +3 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/route.js +3 -1
- package/dist/modules/workflows/api/tasks/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/route.js +3 -1
- package/dist/modules/workflows/api/tasks/route.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +4 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.js +5 -3
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +3 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/components/BusinessRulesSelector.js +5 -3
- package/dist/modules/workflows/components/BusinessRulesSelector.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowSelector.js +5 -3
- package/dist/modules/workflows/components/WorkflowSelector.js.map +2 -2
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js +3 -1
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js +3 -1
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/WorkflowSelectorField.js +3 -1
- package/dist/modules/workflows/components/fields/WorkflowSelectorField.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -7
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +25 -21
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-worker-handler.js +8 -15
- package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
- package/dist/modules/workflows/lib/code-registry.js +7 -7
- package/dist/modules/workflows/lib/code-registry.js.map +2 -2
- package/dist/modules/workflows/lib/compensation-handler.js +4 -2
- package/dist/modules/workflows/lib/compensation-handler.js.map +2 -2
- package/dist/modules/workflows/lib/edgeFormTransforms.js +3 -1
- package/dist/modules/workflows/lib/edgeFormTransforms.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +5 -3
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/dist/modules/workflows/lib/nodeFormTransforms.js +3 -1
- package/dist/modules/workflows/lib/nodeFormTransforms.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +16 -2
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +3 -1
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +9 -3
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +3 -1
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +3 -1
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +5 -3
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +27 -15
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/subscribers/event-trigger.js +21 -18
- package/dist/modules/workflows/subscribers/event-trigger.js.map +2 -2
- package/dist/modules/workflows/subscribers/task-assigned-notification.js +3 -1
- package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-activities.worker.js +30 -20
- package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
- package/package.json +7 -7
- package/src/bootstrap.ts +14 -11
- package/src/helpers/integration/queue-runner.ts +2 -2
- package/src/modules/api_keys/services/apiKeyService.ts +6 -3
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +5 -2
- package/src/modules/attachments/api/library/[id]/route.ts +4 -1
- package/src/modules/attachments/api/route.ts +9 -6
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +6 -3
- package/src/modules/attachments/lib/assignmentDetails.ts +4 -1
- package/src/modules/attachments/lib/drivers/driverFactory.ts +4 -1
- package/src/modules/attachments/lib/ocrQueue.ts +11 -11
- package/src/modules/attachments/lib/ocrService.ts +7 -4
- package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +4 -1
- package/src/modules/audit_logs/api/audit-logs/actions/undo/route.ts +4 -1
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +4 -1
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +5 -2
- package/src/modules/audit_logs/services/accessLogService.ts +5 -4
- package/src/modules/audit_logs/services/actionLogService.ts +5 -2
- package/src/modules/auth/api/profile/route.ts +5 -2
- package/src/modules/auth/api/reset/confirm.ts +4 -1
- package/src/modules/auth/api/reset.ts +5 -2
- package/src/modules/auth/api/roles/route.ts +4 -1
- package/src/modules/auth/api/sidebar/variants/route.ts +4 -2
- package/src/modules/auth/api/users/resend-invite/route.ts +5 -2
- package/src/modules/auth/api/users/route.ts +4 -1
- package/src/modules/auth/backend/auth/profile/page.tsx +4 -1
- package/src/modules/auth/backend/profile/change-password/page.tsx +4 -1
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +6 -3
- package/src/modules/auth/backend/users/create/page.tsx +5 -2
- package/src/modules/auth/cli.ts +10 -0
- package/src/modules/auth/commands/users.ts +5 -2
- package/src/modules/auth/lib/consentIntegrity.ts +4 -4
- package/src/modules/auth/lib/setup-app.ts +10 -10
- package/src/modules/auth/lib/tenantAccess.ts +4 -1
- package/src/modules/auth/lib/tokenHash.ts +4 -4
- package/src/modules/business_rules/api/rules/route.ts +5 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +8 -6
- package/src/modules/business_rules/lib/rule-engine.ts +6 -3
- package/src/modules/business_rules/lib/rule-evaluator.ts +11 -11
- package/src/modules/business_rules/subscribers/crud-rule-trigger.ts +4 -1
- package/src/modules/business_rules/subscribers/rule-execution-failed-notification.ts +4 -1
- package/src/modules/catalog/ai-agents-context.ts +5 -5
- package/src/modules/catalog/ai-tools/_shared.ts +4 -1
- package/src/modules/catalog/ai-tools/authoring-pack.ts +4 -1
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +4 -1
- package/src/modules/catalog/api/dictionaries/[key]/route.ts +4 -1
- package/src/modules/catalog/api/prices/route.ts +4 -1
- package/src/modules/catalog/api/products/route.ts +6 -3
- package/src/modules/catalog/api/settings/route.ts +5 -2
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +15 -12
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +9 -6
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +6 -3
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +6 -6
- package/src/modules/catalog/backend/catalog/products/optionSchemaClient.ts +4 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +6 -3
- package/src/modules/catalog/components/UnitPriceDisplaySettings.tsx +5 -2
- package/src/modules/catalog/inbox-actions.ts +5 -2
- package/src/modules/catalog/seed/examples.ts +5 -5
- package/src/modules/catalog/subscribers/low-stock-notification.ts +4 -1
- package/src/modules/catalog/widgets/injection/product-seo/state.ts +4 -1
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +13 -13
- package/src/modules/communication_channels/api/post/channels/[id]/import-history/route.ts +4 -1
- package/src/modules/communication_channels/api/post/channels/[id]/push/register/route.ts +4 -1
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +6 -3
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +7 -6
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +5 -10
- package/src/modules/communication_channels/commands/delete-channel.ts +4 -5
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +5 -5
- package/src/modules/communication_channels/commands/disconnect-channel.ts +4 -5
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +6 -12
- package/src/modules/communication_channels/commands/push-unregister.ts +4 -3
- package/src/modules/communication_channels/commands/reassign-conversation.ts +6 -3
- package/src/modules/communication_channels/lib/dead-letter.ts +4 -5
- package/src/modules/communication_channels/lib/email-mime.ts +5 -6
- package/src/modules/communication_channels/lib/oauth-client-config.ts +7 -3
- package/src/modules/communication_channels/lib/send-as-user.ts +4 -1
- package/src/modules/communication_channels/lib/thread-token.ts +5 -2
- package/src/modules/communication_channels/setup.ts +4 -4
- package/src/modules/communication_channels/workers/channel-import-history.ts +5 -8
- package/src/modules/communication_channels/workers/gmail-history-sync.ts +9 -18
- package/src/modules/communication_channels/workers/gmail-renew-watch.ts +6 -9
- package/src/modules/communication_channels/workers/outbound-delivery.ts +7 -12
- package/src/modules/communication_channels/workers/poll-channel.ts +8 -15
- package/src/modules/communication_channels/workers/poll-tick.ts +6 -6
- package/src/modules/communication_channels/workers/reaction-processor.ts +6 -9
- package/src/modules/configs/api/cache/route.ts +5 -2
- package/src/modules/configs/api/system-status/route.ts +5 -2
- package/src/modules/configs/api/upgrade-actions/route.ts +5 -2
- package/src/modules/configs/components/ModuleTelemetryPanel.tsx +4 -1
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +1 -1
- package/src/modules/configs/services/upgradeActionsService.ts +7 -3
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +4 -1
- package/src/modules/currencies/lib/exchangeRateFormConfig.ts +4 -1
- package/src/modules/currencies/services/providers/nbp.ts +7 -4
- package/src/modules/currencies/services/providers/raiffeisen.ts +8 -7
- package/src/modules/customer_accounts/api/domain-resolve/all.ts +7 -5
- package/src/modules/customer_accounts/api/signup.ts +5 -2
- package/src/modules/customer_accounts/lib/customerAuthServer.ts +4 -1
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +4 -1
- package/src/modules/customer_accounts/subscribers/autoLinkCrmReverse.ts +4 -1
- package/src/modules/customer_accounts/subscribers/unlinkOnCompanyDelete.ts +4 -1
- package/src/modules/customer_accounts/subscribers/unlinkOnPersonDelete.ts +4 -1
- package/src/modules/customers/ai-agents-context.ts +5 -5
- package/src/modules/customers/api/activities/route.ts +7 -4
- package/src/modules/customers/api/comments/route.ts +4 -1
- package/src/modules/customers/api/companies/[id]/people/route.ts +4 -1
- package/src/modules/customers/api/companies/[id]/route.ts +6 -3
- package/src/modules/customers/api/companies/route.ts +6 -3
- package/src/modules/customers/api/dashboard/widgets/customer-todos/route.ts +4 -1
- package/src/modules/customers/api/dashboard/widgets/new-customers/route.ts +4 -1
- package/src/modules/customers/api/dashboard/widgets/new-deals/route.ts +4 -1
- package/src/modules/customers/api/dashboard/widgets/next-interactions/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/companies/route.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +4 -1
- package/src/modules/customers/api/deals/aggregate/route.ts +4 -1
- package/src/modules/customers/api/deals/bulk-update-owner/route.ts +8 -5
- package/src/modules/customers/api/deals/bulk-update-stage/route.ts +8 -5
- package/src/modules/customers/api/deals/route.ts +4 -1
- package/src/modules/customers/api/deals/summary/route.ts +4 -1
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +5 -2
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +6 -3
- package/src/modules/customers/api/dictionaries/cache.ts +4 -1
- package/src/modules/customers/api/dictionaries/context.ts +4 -1
- package/src/modules/customers/api/dictionaries/currency/route.ts +4 -1
- package/src/modules/customers/api/dictionaries/kind-settings/route.ts +5 -2
- package/src/modules/customers/api/entity-roles-factory.ts +8 -5
- package/src/modules/customers/api/interactions/cancel/route.ts +4 -1
- package/src/modules/customers/api/interactions/complete/route.ts +4 -1
- package/src/modules/customers/api/interactions/conflicts/route.ts +4 -1
- package/src/modules/customers/api/interactions/counts/route.ts +4 -1
- package/src/modules/customers/api/interactions/route.ts +8 -4
- package/src/modules/customers/api/interactions/tasks/route.ts +4 -1
- package/src/modules/customers/api/labels/assign/route.ts +4 -1
- package/src/modules/customers/api/labels/route.ts +7 -3
- package/src/modules/customers/api/labels/unassign/route.ts +4 -1
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +4 -1
- package/src/modules/customers/api/people/[id]/route.ts +6 -3
- package/src/modules/customers/api/people/route.ts +6 -3
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +4 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +7 -4
- package/src/modules/customers/api/pipelines/route.ts +7 -4
- package/src/modules/customers/api/settings/address-format/route.ts +5 -2
- package/src/modules/customers/api/settings/dictionary-sort-modes/route.ts +5 -2
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +5 -2
- package/src/modules/customers/api/tags/assign/route.ts +4 -1
- package/src/modules/customers/api/tags/unassign/route.ts +4 -1
- package/src/modules/customers/api/todos/route.ts +7 -4
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +6 -3
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +4 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +4 -1
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/page.tsx +5 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +6 -3
- package/src/modules/customers/commands/deals.ts +4 -1
- package/src/modules/customers/commands/dictionaries.ts +4 -1
- package/src/modules/customers/commands/interactions.ts +5 -2
- package/src/modules/customers/commands/shared.ts +5 -2
- package/src/modules/customers/commands/todos.ts +4 -1
- package/src/modules/customers/components/DictionarySettings.tsx +6 -3
- package/src/modules/customers/components/DictionarySortSettings.tsx +4 -1
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +4 -1
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +4 -1
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +7 -4
- package/src/modules/customers/components/detail/ActivityCard.tsx +4 -1
- package/src/modules/customers/components/detail/ChangelogTab.tsx +4 -1
- package/src/modules/customers/components/detail/CompanyCard.tsx +4 -1
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +4 -1
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +4 -1
- package/src/modules/customers/components/detail/InlineActivityComposer.tsx +4 -1
- package/src/modules/customers/components/detail/ManageTagsDialog.tsx +4 -1
- package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +6 -3
- package/src/modules/customers/components/detail/RolesSection.tsx +5 -2
- package/src/modules/customers/components/detail/hooks/useInteractionMutations.ts +6 -3
- package/src/modules/customers/components/linking/LinkEntityDialog.tsx +5 -2
- package/src/modules/customers/inbox-actions.ts +4 -1
- package/src/modules/customers/lib/bulkDeals.ts +4 -1
- package/src/modules/customers/lib/dealClosureNotification.ts +5 -2
- package/src/modules/customers/lib/dealStageTransitionTable.ts +4 -1
- package/src/modules/customers/lib/interactionFeatureFlags.ts +5 -1
- package/src/modules/customers/lib/todoCompatibility.ts +4 -1
- package/src/modules/customers/search.ts +15 -7
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +4 -1
- package/src/modules/customers/widgets/dashboard/new-customers/widget.client.tsx +4 -1
- package/src/modules/customers/widgets/dashboard/new-deals/widget.client.tsx +4 -1
- package/src/modules/customers/widgets/dashboard/next-interactions/widget.client.tsx +4 -1
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -1
- package/src/modules/dashboards/api/widgets/data/route.ts +4 -1
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +6 -3
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +4 -1
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +4 -1
- package/src/modules/data_sync/api/run.ts +4 -1
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +3 -1
- package/src/modules/data_sync/lib/adapter-registry.ts +16 -4
- package/src/modules/data_sync/lib/sync-engine.ts +31 -2
- package/src/modules/data_sync/workers/sync-export.ts +4 -1
- package/src/modules/data_sync/workers/sync-import.ts +4 -1
- package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +5 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.ts +4 -1
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +5 -2
- package/src/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.ts +4 -1
- package/src/modules/dictionaries/api/[dictionaryId]/route.ts +6 -3
- package/src/modules/dictionaries/api/context.ts +4 -1
- package/src/modules/dictionaries/api/route.ts +5 -2
- package/src/modules/dictionaries/components/DictionariesManager.tsx +6 -3
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +5 -2
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +5 -2
- package/src/modules/dictionaries/components/DictionarySelectControl.tsx +4 -1
- package/src/modules/dictionaries/fields/dictionary.tsx +4 -1
- package/src/modules/directory/api/organization-branding/route.ts +4 -1
- package/src/modules/directory/api/organization-switcher/route.ts +4 -1
- package/src/modules/directory/commands/tenants.ts +6 -3
- package/src/modules/directory/utils/organizationScope.ts +7 -4
- package/src/modules/entities/api/definitions.cache.ts +4 -1
- package/src/modules/entities/api/definitions.ts +8 -5
- package/src/modules/entities/api/records.ts +5 -2
- package/src/modules/entities/components/EncryptionManager.tsx +4 -1
- package/src/modules/entities/lib/makeActivityRoute.ts +4 -1
- package/src/modules/feature_toggles/api/overrides/route.ts +4 -1
- package/src/modules/feature_toggles/lib/feature-flag-check.ts +6 -9
- package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +5 -2
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +5 -2
- package/src/modules/inbox_ops/api/emails/route.ts +4 -1
- package/src/modules/inbox_ops/api/extract/route.ts +4 -1
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +4 -1
- package/src/modules/inbox_ops/api/proposals/[id]/replies/[replyId]/send/route.ts +5 -2
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +5 -2
- package/src/modules/inbox_ops/api/proposals/route.ts +4 -1
- package/src/modules/inbox_ops/api/routeHelpers.ts +4 -1
- package/src/modules/inbox_ops/api/settings/route.ts +4 -1
- package/src/modules/inbox_ops/api/webhook/inbound.ts +8 -5
- package/src/modules/inbox_ops/lib/cache.ts +5 -2
- package/src/modules/inbox_ops/lib/catalogLookup.ts +4 -1
- package/src/modules/inbox_ops/lib/eventBus.ts +4 -4
- package/src/modules/inbox_ops/lib/messagesIntegration.ts +5 -8
- package/src/modules/inbox_ops/subscribers/executionAuditor.ts +5 -1
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +11 -8
- package/src/modules/inbox_ops/subscribers/proposalNotifier.ts +4 -1
- package/src/modules/messages/api/[id]/route.ts +4 -4
- package/src/modules/messages/commands/actions.ts +4 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +4 -1
- package/src/modules/messages/lib/email-sender.ts +4 -7
- package/src/modules/messages/lib/message-objects-registry.ts +4 -1
- package/src/modules/messages/lib/message-types-registry.ts +4 -1
- package/src/modules/messages/workers/send-email.worker.ts +7 -4
- package/src/modules/notifications/lib/notificationMapper.ts +4 -4
- package/src/modules/notifications/lib/notificationService.ts +4 -5
- package/src/modules/notifications/subscribers/deliver-notification.ts +9 -14
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +4 -1
- package/src/modules/perspectives/services/perspectiveService.ts +4 -3
- package/src/modules/planner/api/access.ts +4 -3
- package/src/modules/planner/api/availability-date-specific.ts +4 -1
- package/src/modules/planner/api/availability-weekly.ts +4 -1
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +5 -2
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +4 -1
- package/src/modules/progress/api/jobs/route.ts +4 -1
- package/src/modules/query_index/components/QueryIndexesTable.tsx +6 -3
- package/src/modules/query_index/lib/batch.ts +5 -2
- package/src/modules/query_index/lib/coverage.ts +4 -1
- package/src/modules/query_index/lib/engine.ts +14 -17
- package/src/modules/query_index/lib/reindexer.ts +9 -6
- package/src/modules/query_index/lib/search-tokens.ts +4 -2
- package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -1
- package/src/modules/resources/api/comments.ts +4 -1
- package/src/modules/resources/api/resources/tags/assign/route.ts +4 -1
- package/src/modules/resources/api/resources/tags/unassign/route.ts +4 -1
- package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +4 -1
- package/src/modules/resources/backend/resources/resource-types/page.tsx +5 -2
- package/src/modules/resources/commands/activities.ts +5 -4
- package/src/modules/resources/commands/comments.ts +6 -4
- package/src/modules/resources/commands/shared.ts +36 -0
- package/src/modules/sales/api/channels/route.ts +4 -1
- package/src/modules/sales/api/document-history/route.ts +4 -1
- package/src/modules/sales/api/document-numbers/route.ts +4 -1
- package/src/modules/sales/api/quotes/accept/route.ts +5 -2
- package/src/modules/sales/api/quotes/convert/route.ts +4 -1
- package/src/modules/sales/api/quotes/public/[token]/route.ts +4 -1
- package/src/modules/sales/api/quotes/send/route.ts +4 -1
- package/src/modules/sales/api/returns/[id]/route.ts +4 -1
- package/src/modules/sales/api/settings/document-numbers/route.ts +5 -2
- package/src/modules/sales/api/settings/order-editing/route.ts +5 -2
- package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +4 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -4
- package/src/modules/sales/backend/sales/channels/page.tsx +5 -2
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +21 -18
- package/src/modules/sales/commands/documents.ts +5 -8
- package/src/modules/sales/commands/payments.ts +4 -1
- package/src/modules/sales/commands/shipments.ts +4 -2
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +6 -3
- package/src/modules/sales/components/DocumentNumberSettings.tsx +5 -2
- package/src/modules/sales/components/OrderEditingSettings.tsx +5 -2
- package/src/modules/sales/components/PaymentMethodsSettings.tsx +6 -3
- package/src/modules/sales/components/ShippingMethodsSettings.tsx +6 -3
- package/src/modules/sales/components/StatusSettings.tsx +6 -3
- package/src/modules/sales/components/TaxRatesSettings.tsx +6 -3
- package/src/modules/sales/components/channels/ChannelOfferForm.tsx +17 -14
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +5 -2
- package/src/modules/sales/components/documents/AddressesSection.tsx +6 -3
- package/src/modules/sales/components/documents/AdjustmentDialog.tsx +4 -1
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -3
- package/src/modules/sales/components/documents/ItemsSection.tsx +7 -4
- package/src/modules/sales/components/documents/LineItemDialog.tsx +9 -6
- package/src/modules/sales/components/documents/PaymentDialog.tsx +6 -3
- package/src/modules/sales/components/documents/PaymentsSection.tsx +5 -2
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +15 -12
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +6 -3
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +10 -7
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +5 -2
- package/src/modules/sales/frontend/quote/[token]/page.tsx +5 -2
- package/src/modules/sales/seed/examples.ts +8 -9
- package/src/modules/sales/subscribers/quote-expiring-notification.ts +4 -1
- package/src/modules/sales/subscribers/reconcileOnAddressDelete.ts +4 -1
- package/src/modules/sales/subscribers/reconcileOnCompanyDelete.ts +4 -1
- package/src/modules/sales/subscribers/reconcileOnCustomerDelete.ts +4 -1
- package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +6 -3
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +274 -271
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +280 -277
- package/src/modules/shipping_carriers/api/tracking/route.ts +5 -1
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +4 -1
- package/src/modules/shipping_carriers/lib/shipping-service.ts +47 -22
- package/src/modules/shipping_carriers/lib/upstream-error-response.ts +4 -1
- package/src/modules/shipping_carriers/workers/webhook-processor.ts +4 -5
- package/src/modules/staff/api/comments.ts +4 -1
- package/src/modules/staff/api/leave-requests/accept/route.ts +4 -1
- package/src/modules/staff/api/leave-requests/reject/route.ts +4 -1
- package/src/modules/staff/api/team-members/assignable/route.ts +4 -1
- package/src/modules/staff/api/team-members/self/route.ts +5 -2
- package/src/modules/staff/api/team-members/tags/assign/route.ts +4 -1
- package/src/modules/staff/api/team-members/tags/unassign/route.ts +4 -1
- package/src/modules/staff/api/timesheets/my-projects/[projectId]/route.ts +4 -1
- package/src/modules/staff/api/timesheets/my-projects/route.ts +4 -1
- package/src/modules/staff/api/timesheets/projects/kpis/route.ts +4 -1
- package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +4 -1
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +5 -2
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +5 -2
- package/src/modules/staff/api/timesheets/time-entries/bulk/route.ts +4 -1
- package/src/modules/staff/api/timesheets/time-entries/start-timer/route.ts +4 -1
- package/src/modules/staff/backend/staff/team-members/page.tsx +5 -2
- package/src/modules/staff/backend/staff/team-roles/page.tsx +5 -2
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +5 -2
- package/src/modules/staff/backend/staff/teams/page.tsx +5 -2
- package/src/modules/staff/backend/staff/timesheets/page.tsx +8 -5
- package/src/modules/staff/backend/staff/timesheets/projects/[id]/page.tsx +4 -1
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +5 -2
- package/src/modules/staff/commands/timesheets-entries.ts +4 -1
- package/src/modules/staff/widgets/dashboard/timesheets-hours-by-project/widget.client.tsx +4 -1
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +6 -3
- package/src/modules/sync_excel/api/import/route.ts +4 -1
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +6 -3
- package/src/modules/translations/api/get/locales.ts +4 -1
- package/src/modules/translations/api/put/locales.ts +4 -1
- package/src/modules/translations/components/TranslationManager.tsx +4 -1
- package/src/modules/translations/subscribers/cleanup.ts +4 -1
- package/src/modules/workflows/api/definitions/[id]/customize/route.ts +5 -2
- package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +5 -2
- package/src/modules/workflows/api/definitions/[id]/route.ts +7 -4
- package/src/modules/workflows/api/definitions/route.ts +5 -2
- package/src/modules/workflows/api/events/[id]/route.ts +4 -1
- package/src/modules/workflows/api/events/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/advance/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/cancel/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/events/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/retry/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/route.ts +4 -1
- package/src/modules/workflows/api/instances/[id]/signal/route.ts +6 -7
- package/src/modules/workflows/api/instances/route.ts +6 -3
- package/src/modules/workflows/api/instances/validate-start/route.ts +4 -1
- package/src/modules/workflows/api/signals/route.ts +4 -1
- package/src/modules/workflows/api/tasks/[id]/claim/route.ts +4 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +4 -1
- package/src/modules/workflows/api/tasks/[id]/route.ts +4 -1
- package/src/modules/workflows/api/tasks/route.ts +4 -1
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +5 -2
- package/src/modules/workflows/backend/instances/[id]/page.tsx +6 -3
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +4 -1
- package/src/modules/workflows/components/BusinessRulesSelector.tsx +6 -3
- package/src/modules/workflows/components/WorkflowSelector.tsx +6 -3
- package/src/modules/workflows/components/fields/BusinessRuleConditionsEditor.tsx +4 -1
- package/src/modules/workflows/components/fields/StartPreConditionsEditor.tsx +4 -1
- package/src/modules/workflows/components/fields/WorkflowSelectorField.tsx +4 -1
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +9 -7
- package/src/modules/workflows/lib/activity-executor.ts +26 -21
- package/src/modules/workflows/lib/activity-worker-handler.ts +9 -15
- package/src/modules/workflows/lib/code-registry.ts +8 -7
- package/src/modules/workflows/lib/compensation-handler.ts +5 -2
- package/src/modules/workflows/lib/edgeFormTransforms.ts +4 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +6 -3
- package/src/modules/workflows/lib/nodeFormTransforms.ts +4 -1
- package/src/modules/workflows/lib/seeds.ts +17 -2
- package/src/modules/workflows/lib/signal-handler.ts +4 -1
- package/src/modules/workflows/lib/start-validator.ts +10 -3
- package/src/modules/workflows/lib/step-handler.ts +4 -1
- package/src/modules/workflows/lib/task-handler.ts +4 -1
- package/src/modules/workflows/lib/transition-handler.ts +6 -3
- package/src/modules/workflows/lib/workflow-executor.ts +28 -15
- package/src/modules/workflows/subscribers/event-trigger.ts +22 -24
- package/src/modules/workflows/subscribers/task-assigned-notification.ts +4 -1
- package/src/modules/workflows/workers/workflow-activities.worker.ts +31 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/commands/reassign-conversation.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload as extractSharedUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadMapping, ExternalConversation } from '../data/entities'\nimport { emitCommunicationChannelsEvent } from '../events'\n\nconst reassignConversationSchema = z.object({\n threadId: z.string().uuid(),\n /** Set to `null` to unassign the conversation. */\n assignedUserId: z.string().uuid().nullable(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type ReassignConversationInput = z.infer<typeof reassignConversationSchema>\n\nexport type ReassignConversationResult =\n | {\n status: 'reassigned'\n threadId: string\n previousAssignedUserId: string | null\n nextAssignedUserId: string | null\n conversationId: string\n undo: ReassignConversationUndoSnapshot\n }\n | { status: 'no_channel_link'; reason: string }\n | { status: 'invalid_assignee'; reason: string }\n | { status: 'noop'; reason: string }\n\nexport interface ReassignConversationUndoSnapshot {\n threadMappingId: string\n conversationId: string\n // Optional for backward compatibility with log entries written before tenant\n // scoping was added to the undo lookup; new snapshots always set it.\n tenantId?: string\n previousAssignedUserId: string | null\n newAssignedUserId: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID =\n 'communication_channels.conversation.reassign'\n\n/**\n * Reassign the owning user of a channel-linked conversation.\n *\n * Updates both `ChannelThreadMapping.assignedUserId` and the linked\n * `ExternalConversation.assignedUserId` so subscribers (notification handlers,\n * future dashboards) see a consistent owner. No external provider call \u2014\n * reassignment is an internal-routing concern.\n *\n * Idempotent: when the new owner matches the existing one, returns `noop`.\n *\n * The command is undoable: the `before` snapshot captures the previous owner on\n * both rows so undo can restore them atomically.\n */\nconst reassignConversationCommand: CommandHandler<\n ReassignConversationInput,\n ReassignConversationResult\n> = {\n id: COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID,\n // Explicitly undoable (the bus also infers this from `undo` below, but\n // declaring it keeps undoability from silently dropping under a refactor).\n isUndoable: true,\n async execute(rawInput, ctx) {\n const input = reassignConversationSchema.parse(rawInput) as ReassignConversationInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n const mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n messageThreadId: input.threadId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n // `message_thread_id` is non-unique (only `(external_conversation_id,\n // tenant_id)` is unique). The 1:1 thread\u2194mapping invariant holds in v1,\n // but order deterministically so a future many-conversations-per-thread\n // merge can never resolve an arbitrary mapping here.\n { orderBy: { createdAt: 'asc' } },\n dscope,\n )\n if (!mapping) {\n return {\n status: 'no_channel_link',\n reason: `no ChannelThreadMapping for thread ${input.threadId}`,\n }\n }\n\n const previousAssignedUserId = mapping.assignedUserId ?? null\n if (previousAssignedUserId === input.assignedUserId) {\n return { status: 'noop', reason: 'assigned user unchanged' }\n }\n\n // Reject an assignee that is not a live user of this tenant \u2014 a UUID-shaped\n // body alone must not create a cross-tenant / dangling owner reference.\n if (input.assignedUserId) {\n // Reference the `auth` user row by string entity name so this command does\n // not import the auth module's entities (module independence); `as never`\n // matches the codebase pattern for cross-module decrypted reads.\n const assignee = await findOneWithDecryption(\n em,\n 'User' as never,\n { id: input.assignedUserId, tenantId: input.scope.tenantId, deletedAt: null } as never,\n undefined,\n dscope,\n )\n if (!assignee) {\n return {\n status: 'invalid_assignee',\n reason: 'assigned user is not a member of this tenant',\n }\n }\n }\n\n const conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n id: mapping.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (!conversation) {\n return {\n status: 'no_channel_link',\n reason: `no ExternalConversation for thread ${input.threadId}`,\n }\n }\n\n // `assignedUserId` is an advisory routing pointer, not a DB foreign key\n // (modules don't share ORM relations). The assignee-existence check above\n // and this write are not atomic, so a user deleted in between leaves a\n // harmless dangling pointer \u2014 consistent with the module's no-FK design.\n mapping.assignedUserId = input.assignedUserId\n conversation.assignedUserId = input.assignedUserId\n await em.flush()\n\n try {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.reassigned',\n {\n conversationId: conversation.id,\n channelId: mapping.channelId,\n messageThreadId: input.threadId,\n previousAssignedUserId,\n assignedUserId: input.assignedUserId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n } catch (emitErr) {\n // Best-effort lifecycle/workflow-trigger signal \u2014 a bus failure must not\n // abort the reassignment (the rows are already committed above).\n console.warn(\n '[communication_channels:reassign-conversation] reassigned event emit failed:',\n emitErr instanceof Error ? emitErr.message : emitErr,\n )\n }\n\n const undo: ReassignConversationUndoSnapshot = {\n threadMappingId: mapping.id,\n conversationId: conversation.id,\n tenantId: mapping.tenantId,\n previousAssignedUserId,\n newAssignedUserId: input.assignedUserId,\n }\n\n return {\n status: 'reassigned',\n threadId: input.threadId,\n previousAssignedUserId,\n nextAssignedUserId: input.assignedUserId,\n conversationId: conversation.id,\n undo,\n }\n },\n // Persist the undo snapshot into the action log. Without this, the command bus\n // mints an undo token (so the UI offers \"Undo\") but the snapshot returned from\n // execute() is never stored, and undo() would silently no-op.\n async buildLog({ input, result }) {\n if (result.status !== 'reassigned') return null\n return {\n resourceKind: 'communication_channels.channel',\n resourceId: result.conversationId,\n tenantId: result.undo.tenantId ?? input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n payload: { undo: result.undo },\n snapshotBefore: result.undo,\n }\n },\n async undo({ ctx, logEntry }) {\n const snapshot = extractSnapshotFromLog(logEntry)\n if (!snapshot) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n // Never resolve by bare id (cross-tenant). New snapshots always carry\n // tenantId; refuse the undo if a legacy snapshot lacks it.\n if (!snapshot.tenantId) return\n const dscope = { tenantId: snapshot.tenantId, organizationId: null }\n\n const mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n { id: snapshot.threadMappingId, tenantId: snapshot.tenantId },\n undefined,\n dscope,\n )\n const conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n { id: snapshot.conversationId, tenantId: snapshot.tenantId },\n undefined,\n dscope,\n )\n if (mapping) mapping.assignedUserId = snapshot.previousAssignedUserId\n if (conversation) conversation.assignedUserId = snapshot.previousAssignedUserId\n if (mapping || conversation) await em.flush()\n },\n}\n\n/**\n * Read the undo payload defensively \u2014 wraps the shared\n * `@open-mercato/shared/lib/commands/undo.ts` helper with a narrow-by-shape\n * validation so callers get a strongly-typed snapshot or `null`.\n *\n * Kept as a separate export for test ergonomics (tests can mock the snapshot\n * shape directly without round-tripping through a CommandLogEntry).\n */\nexport function extractUndoPayload(value: unknown): ReassignConversationUndoSnapshot | null {\n if (!value || typeof value !== 'object') return null\n const candidate = (value as { undo?: unknown }).undo ?? value\n if (!candidate || typeof candidate !== 'object') return null\n const obj = candidate as Record<string, unknown>\n if (typeof obj.threadMappingId !== 'string' || typeof obj.conversationId !== 'string') return null\n return {\n threadMappingId: obj.threadMappingId,\n conversationId: obj.conversationId,\n tenantId: typeof obj.tenantId === 'string' ? obj.tenantId : undefined,\n previousAssignedUserId:\n typeof obj.previousAssignedUserId === 'string' ? obj.previousAssignedUserId : null,\n newAssignedUserId: typeof obj.newAssignedUserId === 'string' ? obj.newAssignedUserId : null,\n }\n}\n\n/**\n * Pulls the reassignment snapshot from a command log entry \u2014 first via the\n * shared `extractUndoPayload` helper, then through the local shape-validator.\n * Always falls back to `null` so the undo handler can no-op safely.\n */\nexport function extractSnapshotFromLog(logEntry: unknown): ReassignConversationUndoSnapshot | null {\n const undo = extractSharedUndoPayload<ReassignConversationUndoSnapshot>(\n (logEntry ?? null) as never,\n )\n if (undo) return extractUndoPayload(undo)\n return extractUndoPayload(logEntry)\n}\n\nregisterCommand(reassignConversationCommand)\n\nexport default reassignConversationCommand\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAGlB,SAAS,uBAAuB;AAChC,SAAS,sBAAsB,gCAAgC;AAC/D,SAAS,6BAA6B;AACtC,SAAS,sBAAsB,4BAA4B;AAC3D,SAAS,sCAAsC;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload as extractSharedUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadMapping, ExternalConversation } from '../data/entities'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'reassign-conversation' })\n\nconst reassignConversationSchema = z.object({\n threadId: z.string().uuid(),\n /** Set to `null` to unassign the conversation. */\n assignedUserId: z.string().uuid().nullable(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type ReassignConversationInput = z.infer<typeof reassignConversationSchema>\n\nexport type ReassignConversationResult =\n | {\n status: 'reassigned'\n threadId: string\n previousAssignedUserId: string | null\n nextAssignedUserId: string | null\n conversationId: string\n undo: ReassignConversationUndoSnapshot\n }\n | { status: 'no_channel_link'; reason: string }\n | { status: 'invalid_assignee'; reason: string }\n | { status: 'noop'; reason: string }\n\nexport interface ReassignConversationUndoSnapshot {\n threadMappingId: string\n conversationId: string\n // Optional for backward compatibility with log entries written before tenant\n // scoping was added to the undo lookup; new snapshots always set it.\n tenantId?: string\n previousAssignedUserId: string | null\n newAssignedUserId: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID =\n 'communication_channels.conversation.reassign'\n\n/**\n * Reassign the owning user of a channel-linked conversation.\n *\n * Updates both `ChannelThreadMapping.assignedUserId` and the linked\n * `ExternalConversation.assignedUserId` so subscribers (notification handlers,\n * future dashboards) see a consistent owner. No external provider call \u2014\n * reassignment is an internal-routing concern.\n *\n * Idempotent: when the new owner matches the existing one, returns `noop`.\n *\n * The command is undoable: the `before` snapshot captures the previous owner on\n * both rows so undo can restore them atomically.\n */\nconst reassignConversationCommand: CommandHandler<\n ReassignConversationInput,\n ReassignConversationResult\n> = {\n id: COMMUNICATION_CHANNELS_REASSIGN_CONVERSATION_COMMAND_ID,\n // Explicitly undoable (the bus also infers this from `undo` below, but\n // declaring it keeps undoability from silently dropping under a refactor).\n isUndoable: true,\n async execute(rawInput, ctx) {\n const input = reassignConversationSchema.parse(rawInput) as ReassignConversationInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n const mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n messageThreadId: input.threadId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n // `message_thread_id` is non-unique (only `(external_conversation_id,\n // tenant_id)` is unique). The 1:1 thread\u2194mapping invariant holds in v1,\n // but order deterministically so a future many-conversations-per-thread\n // merge can never resolve an arbitrary mapping here.\n { orderBy: { createdAt: 'asc' } },\n dscope,\n )\n if (!mapping) {\n return {\n status: 'no_channel_link',\n reason: `no ChannelThreadMapping for thread ${input.threadId}`,\n }\n }\n\n const previousAssignedUserId = mapping.assignedUserId ?? null\n if (previousAssignedUserId === input.assignedUserId) {\n return { status: 'noop', reason: 'assigned user unchanged' }\n }\n\n // Reject an assignee that is not a live user of this tenant \u2014 a UUID-shaped\n // body alone must not create a cross-tenant / dangling owner reference.\n if (input.assignedUserId) {\n // Reference the `auth` user row by string entity name so this command does\n // not import the auth module's entities (module independence); `as never`\n // matches the codebase pattern for cross-module decrypted reads.\n const assignee = await findOneWithDecryption(\n em,\n 'User' as never,\n { id: input.assignedUserId, tenantId: input.scope.tenantId, deletedAt: null } as never,\n undefined,\n dscope,\n )\n if (!assignee) {\n return {\n status: 'invalid_assignee',\n reason: 'assigned user is not a member of this tenant',\n }\n }\n }\n\n const conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n id: mapping.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (!conversation) {\n return {\n status: 'no_channel_link',\n reason: `no ExternalConversation for thread ${input.threadId}`,\n }\n }\n\n // `assignedUserId` is an advisory routing pointer, not a DB foreign key\n // (modules don't share ORM relations). The assignee-existence check above\n // and this write are not atomic, so a user deleted in between leaves a\n // harmless dangling pointer \u2014 consistent with the module's no-FK design.\n mapping.assignedUserId = input.assignedUserId\n conversation.assignedUserId = input.assignedUserId\n await em.flush()\n\n try {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.reassigned',\n {\n conversationId: conversation.id,\n channelId: mapping.channelId,\n messageThreadId: input.threadId,\n previousAssignedUserId,\n assignedUserId: input.assignedUserId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n } catch (emitErr) {\n // Best-effort lifecycle/workflow-trigger signal \u2014 a bus failure must not\n // abort the reassignment (the rows are already committed above).\n logger.warn(\n 'reassigned event emit failed',\n { err: emitErr },\n )\n }\n\n const undo: ReassignConversationUndoSnapshot = {\n threadMappingId: mapping.id,\n conversationId: conversation.id,\n tenantId: mapping.tenantId,\n previousAssignedUserId,\n newAssignedUserId: input.assignedUserId,\n }\n\n return {\n status: 'reassigned',\n threadId: input.threadId,\n previousAssignedUserId,\n nextAssignedUserId: input.assignedUserId,\n conversationId: conversation.id,\n undo,\n }\n },\n // Persist the undo snapshot into the action log. Without this, the command bus\n // mints an undo token (so the UI offers \"Undo\") but the snapshot returned from\n // execute() is never stored, and undo() would silently no-op.\n async buildLog({ input, result }) {\n if (result.status !== 'reassigned') return null\n return {\n resourceKind: 'communication_channels.channel',\n resourceId: result.conversationId,\n tenantId: result.undo.tenantId ?? input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n payload: { undo: result.undo },\n snapshotBefore: result.undo,\n }\n },\n async undo({ ctx, logEntry }) {\n const snapshot = extractSnapshotFromLog(logEntry)\n if (!snapshot) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n // Never resolve by bare id (cross-tenant). New snapshots always carry\n // tenantId; refuse the undo if a legacy snapshot lacks it.\n if (!snapshot.tenantId) return\n const dscope = { tenantId: snapshot.tenantId, organizationId: null }\n\n const mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n { id: snapshot.threadMappingId, tenantId: snapshot.tenantId },\n undefined,\n dscope,\n )\n const conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n { id: snapshot.conversationId, tenantId: snapshot.tenantId },\n undefined,\n dscope,\n )\n if (mapping) mapping.assignedUserId = snapshot.previousAssignedUserId\n if (conversation) conversation.assignedUserId = snapshot.previousAssignedUserId\n if (mapping || conversation) await em.flush()\n },\n}\n\n/**\n * Read the undo payload defensively \u2014 wraps the shared\n * `@open-mercato/shared/lib/commands/undo.ts` helper with a narrow-by-shape\n * validation so callers get a strongly-typed snapshot or `null`.\n *\n * Kept as a separate export for test ergonomics (tests can mock the snapshot\n * shape directly without round-tripping through a CommandLogEntry).\n */\nexport function extractUndoPayload(value: unknown): ReassignConversationUndoSnapshot | null {\n if (!value || typeof value !== 'object') return null\n const candidate = (value as { undo?: unknown }).undo ?? value\n if (!candidate || typeof candidate !== 'object') return null\n const obj = candidate as Record<string, unknown>\n if (typeof obj.threadMappingId !== 'string' || typeof obj.conversationId !== 'string') return null\n return {\n threadMappingId: obj.threadMappingId,\n conversationId: obj.conversationId,\n tenantId: typeof obj.tenantId === 'string' ? obj.tenantId : undefined,\n previousAssignedUserId:\n typeof obj.previousAssignedUserId === 'string' ? obj.previousAssignedUserId : null,\n newAssignedUserId: typeof obj.newAssignedUserId === 'string' ? obj.newAssignedUserId : null,\n }\n}\n\n/**\n * Pulls the reassignment snapshot from a command log entry \u2014 first via the\n * shared `extractUndoPayload` helper, then through the local shape-validator.\n * Always falls back to `null` so the undo handler can no-op safely.\n */\nexport function extractSnapshotFromLog(logEntry: unknown): ReassignConversationUndoSnapshot | null {\n const undo = extractSharedUndoPayload<ReassignConversationUndoSnapshot>(\n (logEntry ?? null) as never,\n )\n if (undo) return extractUndoPayload(undo)\n return extractUndoPayload(logEntry)\n}\n\nregisterCommand(reassignConversationCommand)\n\nexport default reassignConversationCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
2
|
import { ChannelIngestDeadLetter } from "../data/entities.js";
|
|
3
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
|
+
const logger = createLogger("communication_channels").child({ component: "dead-letter" });
|
|
3
5
|
const DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32768;
|
|
4
6
|
function extractExternalUid(message) {
|
|
5
7
|
const meta = message.channelMetadata;
|
|
@@ -49,9 +51,7 @@ async function writeIngestDeadLetter(args) {
|
|
|
49
51
|
em.persist(deadLetter);
|
|
50
52
|
await em.flush();
|
|
51
53
|
} catch (ddlErr) {
|
|
52
|
-
|
|
53
|
-
`[communication_channels:dead-letter] failed to record dead-letter for channel ${channel.id}: ${ddlErr instanceof Error ? ddlErr.message : String(ddlErr)}`
|
|
54
|
-
);
|
|
54
|
+
logger.error("failed to record dead-letter for channel", { channelId: channel.id, err: ddlErr });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/dead-letter.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelIngestDeadLetter } from '../data/entities'\nimport type { NormalizedInboundMessage } from './adapter'\n\nconst DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32_768\n\nexport function extractExternalUid(message: NormalizedInboundMessage): string | null {\n const meta = message.channelMetadata as Record<string, unknown> | undefined\n if (meta && typeof meta.uid === 'string') return meta.uid\n if (meta && typeof meta.uid === 'number') return String(meta.uid)\n return null\n}\n\n/**\n * First N *bytes* of the raw body, for dead-letter forensics. Counts bytes (not\n * UTF-16 code units) so a multi-byte body cannot blow past the intended cap.\n */\nexport function truncateRawBody(message: NormalizedInboundMessage): string | null {\n const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? '', 10)\n const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT\n const body = message.body ?? ''\n if (body.length === 0) return null\n const buf = Buffer.from(body, 'utf-8')\n if (buf.byteLength <= cap) return body\n return `${buf.subarray(0, cap).toString('utf-8')}\u2026[truncated]`\n}\n\nexport interface WriteIngestDeadLetterArgs {\n em: EntityManager\n scope: { tenantId: string; organizationId?: string | null }\n channel: { id: string; providerKey: string }\n message: NormalizedInboundMessage\n err: unknown\n errorMessage: string\n}\n\n/**\n * Persist a `ChannelIngestDeadLetter` row for a permanently-failed inbound\n * message so an operator can replay it later. Best-effort: a failure to write\n * the dead-letter is logged but never thrown, so a bad message cannot block the\n * caller from advancing its cursor.\n *\n * Idempotent on `(channelId, externalMessageId)`: a replayed page that fails the\n * same message again is a no-op, so the dead-letter table never accumulates\n * duplicate rows for the same poison message.\n */\nexport async function writeIngestDeadLetter(args: WriteIngestDeadLetterArgs): Promise<void> {\n const { em, scope, channel, message, err, errorMessage } = args\n const externalMessageId = message.externalMessageId ?? null\n try {\n if (externalMessageId) {\n const existing = await findOneWithDecryption(\n em,\n ChannelIngestDeadLetter,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n externalMessageId,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },\n )\n if (existing) return\n }\n const deadLetter = em.create(ChannelIngestDeadLetter, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n providerKey: channel.providerKey,\n externalUid: extractExternalUid(message),\n externalMessageId,\n errorClass: err instanceof Error ? err.name : 'Error',\n errorMessage,\n rawBody: truncateRawBody(message),\n })\n em.persist(deadLetter)\n await em.flush()\n } catch (ddlErr) {\n
|
|
5
|
-
"mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelIngestDeadLetter } from '../data/entities'\nimport type { NormalizedInboundMessage } from './adapter'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'dead-letter' })\n\nconst DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT = 32_768\n\nexport function extractExternalUid(message: NormalizedInboundMessage): string | null {\n const meta = message.channelMetadata as Record<string, unknown> | undefined\n if (meta && typeof meta.uid === 'string') return meta.uid\n if (meta && typeof meta.uid === 'number') return String(meta.uid)\n return null\n}\n\n/**\n * First N *bytes* of the raw body, for dead-letter forensics. Counts bytes (not\n * UTF-16 code units) so a multi-byte body cannot blow past the intended cap.\n */\nexport function truncateRawBody(message: NormalizedInboundMessage): string | null {\n const envCap = Number.parseInt(process.env.OM_CHANNEL_DEAD_LETTER_RAW_BODY_MAX_BYTES ?? '', 10)\n const cap = Number.isFinite(envCap) && envCap > 0 ? envCap : DEAD_LETTER_RAW_BODY_MAX_BYTES_DEFAULT\n const body = message.body ?? ''\n if (body.length === 0) return null\n const buf = Buffer.from(body, 'utf-8')\n if (buf.byteLength <= cap) return body\n return `${buf.subarray(0, cap).toString('utf-8')}\u2026[truncated]`\n}\n\nexport interface WriteIngestDeadLetterArgs {\n em: EntityManager\n scope: { tenantId: string; organizationId?: string | null }\n channel: { id: string; providerKey: string }\n message: NormalizedInboundMessage\n err: unknown\n errorMessage: string\n}\n\n/**\n * Persist a `ChannelIngestDeadLetter` row for a permanently-failed inbound\n * message so an operator can replay it later. Best-effort: a failure to write\n * the dead-letter is logged but never thrown, so a bad message cannot block the\n * caller from advancing its cursor.\n *\n * Idempotent on `(channelId, externalMessageId)`: a replayed page that fails the\n * same message again is a no-op, so the dead-letter table never accumulates\n * duplicate rows for the same poison message.\n */\nexport async function writeIngestDeadLetter(args: WriteIngestDeadLetterArgs): Promise<void> {\n const { em, scope, channel, message, err, errorMessage } = args\n const externalMessageId = message.externalMessageId ?? null\n try {\n if (externalMessageId) {\n const existing = await findOneWithDecryption(\n em,\n ChannelIngestDeadLetter,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n externalMessageId,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },\n )\n if (existing) return\n }\n const deadLetter = em.create(ChannelIngestDeadLetter, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n channelId: channel.id,\n providerKey: channel.providerKey,\n externalUid: extractExternalUid(message),\n externalMessageId,\n errorClass: err instanceof Error ? err.name : 'Error',\n errorMessage,\n rawBody: truncateRawBody(message),\n })\n em.persist(deadLetter)\n await em.flush()\n } catch (ddlErr) {\n logger.error('failed to record dead-letter for channel', { channelId: channel.id, err: ddlErr })\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AAExC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAExF,MAAM,yCAAyC;AAExC,SAAS,mBAAmB,SAAkD;AACnF,QAAM,OAAO,QAAQ;AACrB,MAAI,QAAQ,OAAO,KAAK,QAAQ,SAAU,QAAO,KAAK;AACtD,MAAI,QAAQ,OAAO,KAAK,QAAQ,SAAU,QAAO,OAAO,KAAK,GAAG;AAChE,SAAO;AACT;AAMO,SAAS,gBAAgB,SAAkD;AAChF,QAAM,SAAS,OAAO,SAAS,QAAQ,IAAI,6CAA6C,IAAI,EAAE;AAC9F,QAAM,MAAM,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC7D,QAAM,OAAO,QAAQ,QAAQ;AAC7B,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,MAAM,OAAO,KAAK,MAAM,OAAO;AACrC,MAAI,IAAI,cAAc,IAAK,QAAO;AAClC,SAAO,GAAG,IAAI,SAAS,GAAG,GAAG,EAAE,SAAS,OAAO,CAAC;AAClD;AAqBA,eAAsB,sBAAsB,MAAgD;AAC1F,QAAM,EAAE,IAAI,OAAO,SAAS,SAAS,KAAK,aAAa,IAAI;AAC3D,QAAM,oBAAoB,QAAQ,qBAAqB;AACvD,MAAI;AACF,QAAI,mBAAmB;AACrB,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,UACE,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM,kBAAkB;AAAA,UACxC,WAAW,QAAQ;AAAA,UACnB;AAAA,QACF;AAAA,QACA;AAAA,QACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,kBAAkB,KAAK;AAAA,MAC3E;AACA,UAAI,SAAU;AAAA,IAChB;AACA,UAAM,aAAa,GAAG,OAAO,yBAAyB;AAAA,MACpD,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,WAAW,QAAQ;AAAA,MACnB,aAAa,QAAQ;AAAA,MACrB,aAAa,mBAAmB,OAAO;AAAA,MACvC;AAAA,MACA,YAAY,eAAe,QAAQ,IAAI,OAAO;AAAA,MAC9C;AAAA,MACA,SAAS,gBAAgB,OAAO;AAAA,IAClC,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAAA,EACjB,SAAS,QAAQ;AACf,WAAO,MAAM,4CAA4C,EAAE,WAAW,QAAQ,IAAI,KAAK,OAAO,CAAC;AAAA,EACjG;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { EMAIL_MAX_ATTACHMENT_BYTES } from "./email-capabilities.js";
|
|
3
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
|
+
const logger = createLogger("communication_channels").child({ component: "email-mime" });
|
|
3
5
|
const TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES = EMAIL_MAX_ATTACHMENT_BYTES * 2;
|
|
4
6
|
function stringOrUndefined(value) {
|
|
5
7
|
if (typeof value !== "string") return void 0;
|
|
@@ -165,15 +167,11 @@ function normalizeAttachments(attachments) {
|
|
|
165
167
|
if (!att.content) continue;
|
|
166
168
|
const byteLength = att.content.byteLength;
|
|
167
169
|
if (byteLength > EMAIL_MAX_ATTACHMENT_BYTES) {
|
|
168
|
-
|
|
169
|
-
`[email-mime] dropping oversized inbound attachment "${att.filename ?? "attachment"}" (${byteLength} bytes > ${EMAIL_MAX_ATTACHMENT_BYTES} cap)`
|
|
170
|
-
);
|
|
170
|
+
logger.warn("dropping oversized inbound attachment", { fileName: att.filename ?? "attachment", bytes: byteLength, maxBytes: EMAIL_MAX_ATTACHMENT_BYTES });
|
|
171
171
|
continue;
|
|
172
172
|
}
|
|
173
173
|
if (totalBytes + byteLength > TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES) {
|
|
174
|
-
|
|
175
|
-
`[email-mime] aggregate inbound attachment size exceeded ${TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES} bytes; dropping remaining attachments`
|
|
176
|
-
);
|
|
174
|
+
logger.warn("aggregate inbound attachment size exceeded; dropping remaining attachments", { maxBytes: TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES });
|
|
177
175
|
break;
|
|
178
176
|
}
|
|
179
177
|
totalBytes += byteLength;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/email-mime.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { NormalizedInboundMessage, NormalizedAttachment } from './adapter'\nimport { EMAIL_MAX_ATTACHMENT_BYTES } from './email-capabilities'\n\n/**\n * Aggregate ceiling for all attachments on a single inbound message. Inbound\n * mail is untrusted, so without a cap a malicious/large message would be fully\n * base64-buffered in memory (~1.33x raw bytes) and persisted. Allow a small\n * multiple of the per-attachment limit for legitimate multi-file emails.\n */\nconst TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES = EMAIL_MAX_ATTACHMENT_BYTES * 2\n\n/**\n * Shared email MIME helpers for the email channel providers (Gmail, IMAP).\n * Outbound assembly, inbound parsing, header/address normalization,\n * and threading-id extraction all live here so every provider shares one\n * correct implementation instead of copy-pasting (which previously let Gmail's\n * `extractHeaders` drift into a Map-handling bug that IMAP had already fixed).\n *\n * Provider-specific transport (Gmail History API, IMAP UID sync)\n * stays in each package \u2014 this module only owns the format-level plumbing.\n */\n\n// \u2500\u2500 Outbound MIME helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function stringOrUndefined(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function toAddressList(value: unknown): string[] {\n if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter((v) => v.length > 0)\n if (typeof value === 'string') {\n return value\n .split(/[,;]\\s*/)\n .map((v) => v.trim())\n .filter((v) => v.length > 0)\n }\n return []\n}\n\nexport function referencesFromMeta(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined\n return value.filter((entry): entry is string => typeof entry === 'string')\n}\n\n/**\n * Strip every `<tag>\u2026</tag>` block (and its contents) for a single tag name.\n * Inbound HTML is untrusted, so the matcher must resist evasion: the close tag\n * allows trailing attributes/whitespace (`</script >`, `</style foo>`), the `i`\n * flag covers mixed case, and the replacement loops until the string is stable\n * so a payload split across nested or reconstructed tags cannot survive a\n * single pass (`<scr<script>ipt>` collapsing back into `<script>`).\n *\n * The opening tag is also matched when it is truncated or never closed \u2014 a bare\n * `<script` or an unterminated `<script>\u2026` running to end-of-input is removed\n * outright \u2014 so no prefix of the element name can leak into the output.\n */\nfunction stripTagBlocks(html: string, tag: string): string {\n const blockPattern = new RegExp(\n `<${tag}\\\\b[^>]*(?:>[\\\\s\\\\S]*?(?:<\\\\/${tag}[^>]*>|$)|$)`,\n 'gi',\n )\n let previous: string\n let current = html\n do {\n previous = current\n current = current.replace(blockPattern, ' ')\n } while (current !== previous)\n return current\n}\n\n/**\n * Drop HTML comments (`<!-- \u2026 -->`), including an unterminated comment running\n * to end-of-input. Comments are stripped first because they can wrap content\n * that would otherwise survive tag removal (`<!--<script-->`), and a naive\n * filter that ignores them leaves a tag fragment behind.\n */\nfunction stripHtmlComments(html: string): string {\n return html.replace(/<!--[\\s\\S]*?(?:-->|$)/g, ' ')\n}\n\nconst BASIC_HTML_ENTITIES: Record<string, string> = {\n ' ': ' ',\n '<': '<',\n '>': '>',\n '"': '\"',\n '&': '&',\n}\n\n/**\n * Decode the handful of HTML entities we surface in plaintext in ONE\n * left-to-right pass. A single pass cannot double-unescape: characters produced\n * by a replacement (e.g. the `&` from `&`) are never re-scanned, so\n * `&lt;` decodes to the literal `<` rather than collapsing into `<`.\n */\nfunction decodeBasicEntities(input: string): string {\n return input.replace(\n /&(?:nbsp|lt|gt|quot|amp);/gi,\n (match) => BASIC_HTML_ENTITIES[match.toLowerCase()] ?? match,\n )\n}\n\n/**\n * Remove every remaining `<\u2026>` tag, looping until the string is stable so a tag\n * reconstructed by a single pass (`<<div>div>`) cannot survive \u2014 the same\n * loop-until-stable shape `stripTagBlocks` uses. A raw `<` that is not part of a\n * tag (`a < b`) has no closing `>`, never matches, and is intentionally kept.\n */\nfunction stripRemainingTags(html: string): string {\n let previous: string\n let current = html\n do {\n previous = current\n current = current.replace(/<[^>]*>/g, '')\n } while (current !== previous)\n return current\n}\n\n/**\n * Convert untrusted inbound HTML to plaintext. Entities are decoded FIRST so any\n * entity-encoded markup (e.g. `<script>`) is normalized into real tag\n * syntax before the strippers run \u2014 otherwise decoding last would reintroduce a\n * `<script` fragment into the final output after sanitization had finished\n * (CodeQL `js/incomplete-multi-character-sanitization`). After decoding, all\n * tag removal (script/style blocks, then remaining tags) happens via\n * loop-until-stable passes, so no `<tag` prefix can leak into the plaintext.\n */\nexport function htmlToText(html: string): string {\n const decoded = decodeBasicEntities(html)\n const stripped = stripTagBlocks(stripTagBlocks(stripHtmlComments(decoded), 'style'), 'script')\n .replace(/<br\\s*\\/?>(?=\\s*)/gi, '\\n')\n .replace(/<\\/p\\s*>/gi, '\\n\\n')\n return stripRemainingTags(stripped)\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport function escapeQuotes(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')\n}\n\n/**\n * Collapse CR/LF/TAB in an email header value to a single space to prevent\n * RFC 5322 header injection \u2014 e.g. a Subject smuggling an extra\n * `Bcc:`/`Content-Type:` header or splitting the message into a body.\n * Collapsing (rather than folding) is safe for the short structured headers\n * we emit (Subject, addresses, Message-ID, References).\n */\nexport function sanitizeHeaderValue(value: string): string {\n return value.replace(/[\\r\\n\\t]+/g, ' ').trim()\n}\n\nexport function ensureBrackets(value: string): string {\n const trimmed = sanitizeHeaderValue(value)\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed\n return `<${trimmed}>`\n}\n\nfunction isPureAscii(value: string): boolean {\n // eslint-disable-next-line no-control-regex\n return /^[\\x00-\\x7F]*$/.test(value)\n}\n\n/**\n * Encode a single header value as an RFC 2047 \"B\" (base64) encoded-word when it\n * contains non-ASCII characters, so 8-bit text like \"Caf\u00E9\" survives strict MTAs\n * that treat header bytes as 7-bit ASCII. Pure-ASCII values are returned\n * unchanged. Apply only AFTER `sanitizeHeaderValue` so the CR/LF injection guard\n * still runs against the raw value.\n */\nexport function encodeHeaderWord(value: string): string {\n if (isPureAscii(value)) return value\n return `=?utf-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`\n}\n\n/**\n * Encode the display-name part of a single address header value, leaving the\n * `<addr@domain>` untouched (per RFC 2047, encoded-words are not permitted\n * inside the addr-spec). Inputs without a bracketed address are treated as a\n * bare display name / address and encoded only when non-ASCII.\n */\nexport function encodeAddressHeaderWord(value: string): string {\n if (isPureAscii(value)) return value\n const match = value.match(/^(.*?)(\\s*<[^>]*>)\\s*$/)\n if (match) {\n const [, displayPart, addrPart] = match\n const displayName = displayPart.replace(/^\"|\"$/g, '').trim()\n if (!displayName) return value\n return `${encodeHeaderWord(displayName)}${addrPart}`\n }\n return encodeHeaderWord(value)\n}\n\n/**\n * Generate an RFC 5322 Message-ID rooted in the sender's domain. Used as a\n * downstream idempotency key, so entropy comes from `crypto.randomUUID()`\n * rather than `Math.random()`.\n */\nexport function generateMessageId(fromAddress: string, fallbackDomain = 'localhost'): string {\n const domain = fromAddress.split('@')[1] ?? fallbackDomain\n return `<${crypto.randomUUID()}@${domain}>`\n}\n\nexport interface AssembleRfc2822Input {\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n subject: string | undefined\n text: string | undefined\n html: string | undefined\n inReplyTo: string | undefined\n references: string[] | undefined\n messageId: string\n}\n\n/**\n * Render one MIME body part's CTE header + body content. Non-ASCII bodies are\n * base64-encoded (CRLF-wrapped at 76 cols) and labelled\n * `Content-Transfer-Encoding: base64` so 8-bit text survives strict MTAs;\n * pure-ASCII bodies stay `7bit` and verbatim.\n */\nfunction encodeBodyPart(content: string): { cte: string; body: string } {\n if (isPureAscii(content)) return { cte: '7bit', body: content }\n const base64 = Buffer.from(content, 'utf-8').toString('base64')\n const wrapped = base64.match(/.{1,76}/g)?.join('\\r\\n') ?? base64\n return { cte: 'base64', body: wrapped }\n}\n\n/**\n * Assemble a raw RFC2822 message (used by transports that send the encoded\n * message directly, e.g. Gmail `users.messages.send`). Emits a\n * `multipart/alternative` body when both html and text are present, otherwise a\n * single-part text or html body.\n */\nexport function assembleRfc2822(input: AssembleRfc2822Input): Buffer {\n const boundary = `omc_${crypto.randomUUID()}`\n const headers: string[] = []\n headers.push(`From: ${encodeAddressHeaderWord(sanitizeHeaderValue(input.from))}`)\n headers.push(`To: ${input.to.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.cc.length) headers.push(`Cc: ${input.cc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.bcc.length) headers.push(`Bcc: ${input.bcc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.subject) headers.push(`Subject: ${encodeHeaderWord(sanitizeHeaderValue(input.subject))}`)\n headers.push(`Message-ID: ${ensureBrackets(input.messageId)}`)\n if (input.inReplyTo) headers.push(`In-Reply-To: ${ensureBrackets(input.inReplyTo)}`)\n if (input.references && input.references.length) {\n headers.push(`References: ${input.references.map(ensureBrackets).join(' ')}`)\n }\n headers.push('MIME-Version: 1.0')\n headers.push(`Date: ${new Date().toUTCString()}`)\n\n if (input.html && input.text) {\n headers.push(`Content-Type: multipart/alternative; boundary=\"${boundary}\"`)\n const textPart = encodeBodyPart(input.text)\n const htmlPart = encodeBodyPart(input.html)\n const body = [\n '',\n `--${boundary}`,\n 'Content-Type: text/plain; charset=utf-8',\n `Content-Transfer-Encoding: ${textPart.cte}`,\n '',\n textPart.body,\n `--${boundary}`,\n 'Content-Type: text/html; charset=utf-8',\n `Content-Transfer-Encoding: ${htmlPart.cte}`,\n '',\n htmlPart.body,\n `--${boundary}--`,\n '',\n ].join('\\r\\n')\n return Buffer.from(headers.join('\\r\\n') + body, 'utf-8')\n }\n\n if (input.html) {\n const htmlPart = encodeBodyPart(input.html)\n headers.push('Content-Type: text/html; charset=utf-8')\n headers.push(`Content-Transfer-Encoding: ${htmlPart.cte}`)\n return Buffer.from(headers.join('\\r\\n') + '\\r\\n\\r\\n' + htmlPart.body, 'utf-8')\n }\n\n const textPart = encodeBodyPart(input.text ?? '')\n headers.push('Content-Type: text/plain; charset=utf-8')\n headers.push(`Content-Transfer-Encoding: ${textPart.cte}`)\n return Buffer.from(headers.join('\\r\\n') + '\\r\\n\\r\\n' + textPart.body, 'utf-8')\n}\n\n// \u2500\u2500 Inbound MIME parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedMail {\n messageId?: string | null\n inReplyTo?: string | null\n references?: string | string[] | null\n from?: { value?: Array<{ address?: string; name?: string }> }\n to?: { value?: Array<{ address?: string; name?: string }> }\n cc?: { value?: Array<{ address?: string; name?: string }> }\n bcc?: { value?: Array<{ address?: string; name?: string }> }\n subject?: string | null\n html?: string | false\n text?: string\n date?: string | Date | null\n attachments?: ParsedAttachment[]\n headers?: Map<string, unknown> | Record<string, unknown>\n}\n\nexport interface ParsedAttachment {\n content?: Buffer | Uint8Array\n contentType?: string\n filename?: string\n size?: number\n contentDisposition?: string\n cid?: string\n}\n\nexport function stripBrackets(value: string | undefined | null): string | undefined {\n if (!value) return undefined\n const trimmed = value.trim()\n if (!trimmed) return undefined\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1)\n return trimmed\n}\n\nexport function parseReferences(value: string | string[] | undefined | null): string[] {\n if (!value) return []\n if (Array.isArray(value)) return value.map((v) => stripBrackets(v)).filter((v): v is string => Boolean(v))\n return value\n .split(/\\s+/)\n .map((segment) => stripBrackets(segment))\n .filter((segment): segment is string => Boolean(segment))\n}\n\nexport function normalizeAttachments(attachments: ParsedAttachment[]): NormalizedAttachment[] {\n const out: NormalizedAttachment[] = []\n let totalBytes = 0\n for (const att of attachments) {\n if (!att.content) continue\n const byteLength = att.content.byteLength\n if (byteLength > EMAIL_MAX_ATTACHMENT_BYTES) {\n console.warn(\n `[email-mime] dropping oversized inbound attachment \"${att.filename ?? 'attachment'}\" (${byteLength} bytes > ${EMAIL_MAX_ATTACHMENT_BYTES} cap)`,\n )\n continue\n }\n if (totalBytes + byteLength > TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES) {\n console.warn(\n `[email-mime] aggregate inbound attachment size exceeded ${TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES} bytes; dropping remaining attachments`,\n )\n break\n }\n totalBytes += byteLength\n const base64 = Buffer.isBuffer(att.content)\n ? att.content.toString('base64')\n : Buffer.from(att.content).toString('base64')\n out.push({\n url: `data:${att.contentType ?? 'application/octet-stream'};base64,${base64}`,\n mimeType: att.contentType ?? 'application/octet-stream',\n fileName: att.filename ?? 'attachment',\n fileSize: att.size,\n inline: Boolean(att.contentDisposition && /inline/i.test(att.contentDisposition)) || Boolean(att.cid),\n })\n }\n return out\n}\n\nfunction stringifyHeaderValue(value: unknown): string | undefined {\n if (typeof value === 'string') return value\n if (Array.isArray(value)) return value.map((v) => String(v)).join(', ')\n if (value instanceof Date) return value.toISOString()\n if (value && typeof value === 'object') {\n try {\n return JSON.stringify(value)\n } catch {\n return undefined\n }\n }\n if (value === undefined || value === null) return undefined\n return String(value)\n}\n\n/**\n * Flatten parsed MIME headers to a `Record<string, string>`.\n *\n * mailparser returns `headers` as a `Map<string, unknown>`. `Object.entries` on\n * a Map yields an empty array (Maps have no enumerable own properties), so we\n * iterate Map entries directly, with a Record fallback for test fakes.\n */\nexport function extractHeaders(headers: ParsedMail['headers']): Record<string, string> {\n if (!headers) return {}\n const out: Record<string, string> = {}\n if (headers instanceof Map) {\n for (const [key, value] of headers.entries()) {\n const stringified = stringifyHeaderValue(value)\n if (stringified !== undefined) out[String(key)] = stringified\n }\n return out\n }\n for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {\n const stringified = stringifyHeaderValue(value)\n if (stringified !== undefined) out[key] = stringified\n }\n return out\n}\n\nexport interface NormalizeMimeInboundOptions {\n /**\n * The parsed MIME message. Providers parse with their own `mailparser`\n * dependency (Gmail/IMAP) and pass the result here, so the hub stays free of a\n * MIME-parser dependency while still owning the normalization logic.\n */\n parsed: ParsedMail\n /** External identifier of the receiving channel (typically the account's email). */\n accountIdentifier: string\n /** Deterministic id used when the MIME message carries no `Message-ID` header. */\n fallbackMessageId: string\n /** Compute the conversation grouping id from the resolved message id + references. */\n resolveConversationId: (context: { messageId: string; references: string[] }) => string\n /** Fallback timestamp when the parsed message has no Date header. */\n fallbackDate?: Date\n /** Provider-specific fields merged into `channelMetadata`. */\n channelMetadata?: (parsed: ParsedMail) => Record<string, unknown>\n /** Provider-specific fields merged into `channelPayload`. */\n channelPayload?: (parsed: ParsedMail) => Record<string, unknown>\n}\n\n/**\n * Build the hub's canonical `NormalizedInboundMessage` from a parsed MIME\n * message. Providers supply the bits that genuinely differ (message-id\n * fallback, conversation grouping, extra metadata) and inherit the shared\n * threading / attachment / header logic.\n */\nexport function normalizeMimeInbound(options: NormalizeMimeInboundOptions): NormalizedInboundMessage {\n const { parsed } = options\n\n const messageId = stripBrackets(parsed.messageId) ?? options.fallbackMessageId\n const inReplyTo = stripBrackets(parsed.inReplyTo)\n const references = parseReferences(parsed.references)\n const conversationId = options.resolveConversationId({ messageId, references })\n\n const from = parsed.from?.value?.[0]\n const subject = parsed.subject?.trim() || undefined\n const bodyHtml = parsed.html && typeof parsed.html === 'string' ? parsed.html : undefined\n const bodyText = typeof parsed.text === 'string' ? parsed.text : undefined\n const body = bodyHtml ?? bodyText ?? ''\n const bodyFormat: 'text' | 'html' = bodyHtml ? 'html' : 'text'\n\n const attachments = normalizeAttachments(parsed.attachments ?? [])\n\n const channelMetadata: Record<string, unknown> = {\n ...(options.channelMetadata?.(parsed) ?? {}),\n messageId,\n inReplyTo: inReplyTo ?? null,\n references,\n headers: extractHeaders(parsed.headers),\n }\n\n const channelPayload: Record<string, unknown> = {\n subject,\n from: from ? { address: from.address, name: from.name } : null,\n to: parsed.to?.value ?? [],\n cc: parsed.cc?.value ?? [],\n bcc: parsed.bcc?.value ?? [],\n html: bodyHtml ?? null,\n text: bodyText ?? null,\n messageId,\n ...(options.channelPayload?.(parsed) ?? {}),\n }\n\n return {\n externalMessageId: messageId,\n externalConversationId: conversationId,\n senderIdentifier: from?.address ?? options.accountIdentifier,\n senderDisplayName: from?.name?.trim() || undefined,\n subject,\n body,\n bodyFormat,\n attachments,\n timestamp: parsed.date ? new Date(parsed.date) : options.fallbackDate ?? new Date(),\n replyToExternalId: inReplyTo ?? undefined,\n channelPayload,\n channelContentType: 'email/mime',\n channelMetadata,\n }\n}\n\n// \u2500\u2500 Provider sync cursor helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Encode a provider channel-state object into an opaque base64 sync cursor. */\nexport function encodeCursor(state: unknown): string {\n return Buffer.from(JSON.stringify(state)).toString('base64')\n}\n\n/** Decode a base64 sync cursor back into its object form, or `null` if malformed. */\nexport function decodeCursor(value: string | null | undefined): unknown {\n if (!value) return null\n try {\n return JSON.parse(Buffer.from(value, 'base64').toString('utf-8'))\n } catch {\n return null\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,kCAAkC;
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport type { NormalizedInboundMessage, NormalizedAttachment } from './adapter'\nimport { EMAIL_MAX_ATTACHMENT_BYTES } from './email-capabilities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'email-mime' })\n\n/**\n * Aggregate ceiling for all attachments on a single inbound message. Inbound\n * mail is untrusted, so without a cap a malicious/large message would be fully\n * base64-buffered in memory (~1.33x raw bytes) and persisted. Allow a small\n * multiple of the per-attachment limit for legitimate multi-file emails.\n */\nconst TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES = EMAIL_MAX_ATTACHMENT_BYTES * 2\n\n/**\n * Shared email MIME helpers for the email channel providers (Gmail, IMAP).\n * Outbound assembly, inbound parsing, header/address normalization,\n * and threading-id extraction all live here so every provider shares one\n * correct implementation instead of copy-pasting (which previously let Gmail's\n * `extractHeaders` drift into a Map-handling bug that IMAP had already fixed).\n *\n * Provider-specific transport (Gmail History API, IMAP UID sync)\n * stays in each package \u2014 this module only owns the format-level plumbing.\n */\n\n// \u2500\u2500 Outbound MIME helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function stringOrUndefined(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function toAddressList(value: unknown): string[] {\n if (Array.isArray(value)) return value.map((v) => String(v).trim()).filter((v) => v.length > 0)\n if (typeof value === 'string') {\n return value\n .split(/[,;]\\s*/)\n .map((v) => v.trim())\n .filter((v) => v.length > 0)\n }\n return []\n}\n\nexport function referencesFromMeta(value: unknown): string[] | undefined {\n if (!Array.isArray(value)) return undefined\n return value.filter((entry): entry is string => typeof entry === 'string')\n}\n\n/**\n * Strip every `<tag>\u2026</tag>` block (and its contents) for a single tag name.\n * Inbound HTML is untrusted, so the matcher must resist evasion: the close tag\n * allows trailing attributes/whitespace (`</script >`, `</style foo>`), the `i`\n * flag covers mixed case, and the replacement loops until the string is stable\n * so a payload split across nested or reconstructed tags cannot survive a\n * single pass (`<scr<script>ipt>` collapsing back into `<script>`).\n *\n * The opening tag is also matched when it is truncated or never closed \u2014 a bare\n * `<script` or an unterminated `<script>\u2026` running to end-of-input is removed\n * outright \u2014 so no prefix of the element name can leak into the output.\n */\nfunction stripTagBlocks(html: string, tag: string): string {\n const blockPattern = new RegExp(\n `<${tag}\\\\b[^>]*(?:>[\\\\s\\\\S]*?(?:<\\\\/${tag}[^>]*>|$)|$)`,\n 'gi',\n )\n let previous: string\n let current = html\n do {\n previous = current\n current = current.replace(blockPattern, ' ')\n } while (current !== previous)\n return current\n}\n\n/**\n * Drop HTML comments (`<!-- \u2026 -->`), including an unterminated comment running\n * to end-of-input. Comments are stripped first because they can wrap content\n * that would otherwise survive tag removal (`<!--<script-->`), and a naive\n * filter that ignores them leaves a tag fragment behind.\n */\nfunction stripHtmlComments(html: string): string {\n return html.replace(/<!--[\\s\\S]*?(?:-->|$)/g, ' ')\n}\n\nconst BASIC_HTML_ENTITIES: Record<string, string> = {\n ' ': ' ',\n '<': '<',\n '>': '>',\n '"': '\"',\n '&': '&',\n}\n\n/**\n * Decode the handful of HTML entities we surface in plaintext in ONE\n * left-to-right pass. A single pass cannot double-unescape: characters produced\n * by a replacement (e.g. the `&` from `&`) are never re-scanned, so\n * `&lt;` decodes to the literal `<` rather than collapsing into `<`.\n */\nfunction decodeBasicEntities(input: string): string {\n return input.replace(\n /&(?:nbsp|lt|gt|quot|amp);/gi,\n (match) => BASIC_HTML_ENTITIES[match.toLowerCase()] ?? match,\n )\n}\n\n/**\n * Remove every remaining `<\u2026>` tag, looping until the string is stable so a tag\n * reconstructed by a single pass (`<<div>div>`) cannot survive \u2014 the same\n * loop-until-stable shape `stripTagBlocks` uses. A raw `<` that is not part of a\n * tag (`a < b`) has no closing `>`, never matches, and is intentionally kept.\n */\nfunction stripRemainingTags(html: string): string {\n let previous: string\n let current = html\n do {\n previous = current\n current = current.replace(/<[^>]*>/g, '')\n } while (current !== previous)\n return current\n}\n\n/**\n * Convert untrusted inbound HTML to plaintext. Entities are decoded FIRST so any\n * entity-encoded markup (e.g. `<script>`) is normalized into real tag\n * syntax before the strippers run \u2014 otherwise decoding last would reintroduce a\n * `<script` fragment into the final output after sanitization had finished\n * (CodeQL `js/incomplete-multi-character-sanitization`). After decoding, all\n * tag removal (script/style blocks, then remaining tags) happens via\n * loop-until-stable passes, so no `<tag` prefix can leak into the plaintext.\n */\nexport function htmlToText(html: string): string {\n const decoded = decodeBasicEntities(html)\n const stripped = stripTagBlocks(stripTagBlocks(stripHtmlComments(decoded), 'style'), 'script')\n .replace(/<br\\s*\\/?>(?=\\s*)/gi, '\\n')\n .replace(/<\\/p\\s*>/gi, '\\n\\n')\n return stripRemainingTags(stripped)\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nexport function escapeQuotes(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')\n}\n\n/**\n * Collapse CR/LF/TAB in an email header value to a single space to prevent\n * RFC 5322 header injection \u2014 e.g. a Subject smuggling an extra\n * `Bcc:`/`Content-Type:` header or splitting the message into a body.\n * Collapsing (rather than folding) is safe for the short structured headers\n * we emit (Subject, addresses, Message-ID, References).\n */\nexport function sanitizeHeaderValue(value: string): string {\n return value.replace(/[\\r\\n\\t]+/g, ' ').trim()\n}\n\nexport function ensureBrackets(value: string): string {\n const trimmed = sanitizeHeaderValue(value)\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed\n return `<${trimmed}>`\n}\n\nfunction isPureAscii(value: string): boolean {\n // eslint-disable-next-line no-control-regex\n return /^[\\x00-\\x7F]*$/.test(value)\n}\n\n/**\n * Encode a single header value as an RFC 2047 \"B\" (base64) encoded-word when it\n * contains non-ASCII characters, so 8-bit text like \"Caf\u00E9\" survives strict MTAs\n * that treat header bytes as 7-bit ASCII. Pure-ASCII values are returned\n * unchanged. Apply only AFTER `sanitizeHeaderValue` so the CR/LF injection guard\n * still runs against the raw value.\n */\nexport function encodeHeaderWord(value: string): string {\n if (isPureAscii(value)) return value\n return `=?utf-8?B?${Buffer.from(value, 'utf-8').toString('base64')}?=`\n}\n\n/**\n * Encode the display-name part of a single address header value, leaving the\n * `<addr@domain>` untouched (per RFC 2047, encoded-words are not permitted\n * inside the addr-spec). Inputs without a bracketed address are treated as a\n * bare display name / address and encoded only when non-ASCII.\n */\nexport function encodeAddressHeaderWord(value: string): string {\n if (isPureAscii(value)) return value\n const match = value.match(/^(.*?)(\\s*<[^>]*>)\\s*$/)\n if (match) {\n const [, displayPart, addrPart] = match\n const displayName = displayPart.replace(/^\"|\"$/g, '').trim()\n if (!displayName) return value\n return `${encodeHeaderWord(displayName)}${addrPart}`\n }\n return encodeHeaderWord(value)\n}\n\n/**\n * Generate an RFC 5322 Message-ID rooted in the sender's domain. Used as a\n * downstream idempotency key, so entropy comes from `crypto.randomUUID()`\n * rather than `Math.random()`.\n */\nexport function generateMessageId(fromAddress: string, fallbackDomain = 'localhost'): string {\n const domain = fromAddress.split('@')[1] ?? fallbackDomain\n return `<${crypto.randomUUID()}@${domain}>`\n}\n\nexport interface AssembleRfc2822Input {\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n subject: string | undefined\n text: string | undefined\n html: string | undefined\n inReplyTo: string | undefined\n references: string[] | undefined\n messageId: string\n}\n\n/**\n * Render one MIME body part's CTE header + body content. Non-ASCII bodies are\n * base64-encoded (CRLF-wrapped at 76 cols) and labelled\n * `Content-Transfer-Encoding: base64` so 8-bit text survives strict MTAs;\n * pure-ASCII bodies stay `7bit` and verbatim.\n */\nfunction encodeBodyPart(content: string): { cte: string; body: string } {\n if (isPureAscii(content)) return { cte: '7bit', body: content }\n const base64 = Buffer.from(content, 'utf-8').toString('base64')\n const wrapped = base64.match(/.{1,76}/g)?.join('\\r\\n') ?? base64\n return { cte: 'base64', body: wrapped }\n}\n\n/**\n * Assemble a raw RFC2822 message (used by transports that send the encoded\n * message directly, e.g. Gmail `users.messages.send`). Emits a\n * `multipart/alternative` body when both html and text are present, otherwise a\n * single-part text or html body.\n */\nexport function assembleRfc2822(input: AssembleRfc2822Input): Buffer {\n const boundary = `omc_${crypto.randomUUID()}`\n const headers: string[] = []\n headers.push(`From: ${encodeAddressHeaderWord(sanitizeHeaderValue(input.from))}`)\n headers.push(`To: ${input.to.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.cc.length) headers.push(`Cc: ${input.cc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.bcc.length) headers.push(`Bcc: ${input.bcc.map((value) => encodeAddressHeaderWord(sanitizeHeaderValue(value))).join(', ')}`)\n if (input.subject) headers.push(`Subject: ${encodeHeaderWord(sanitizeHeaderValue(input.subject))}`)\n headers.push(`Message-ID: ${ensureBrackets(input.messageId)}`)\n if (input.inReplyTo) headers.push(`In-Reply-To: ${ensureBrackets(input.inReplyTo)}`)\n if (input.references && input.references.length) {\n headers.push(`References: ${input.references.map(ensureBrackets).join(' ')}`)\n }\n headers.push('MIME-Version: 1.0')\n headers.push(`Date: ${new Date().toUTCString()}`)\n\n if (input.html && input.text) {\n headers.push(`Content-Type: multipart/alternative; boundary=\"${boundary}\"`)\n const textPart = encodeBodyPart(input.text)\n const htmlPart = encodeBodyPart(input.html)\n const body = [\n '',\n `--${boundary}`,\n 'Content-Type: text/plain; charset=utf-8',\n `Content-Transfer-Encoding: ${textPart.cte}`,\n '',\n textPart.body,\n `--${boundary}`,\n 'Content-Type: text/html; charset=utf-8',\n `Content-Transfer-Encoding: ${htmlPart.cte}`,\n '',\n htmlPart.body,\n `--${boundary}--`,\n '',\n ].join('\\r\\n')\n return Buffer.from(headers.join('\\r\\n') + body, 'utf-8')\n }\n\n if (input.html) {\n const htmlPart = encodeBodyPart(input.html)\n headers.push('Content-Type: text/html; charset=utf-8')\n headers.push(`Content-Transfer-Encoding: ${htmlPart.cte}`)\n return Buffer.from(headers.join('\\r\\n') + '\\r\\n\\r\\n' + htmlPart.body, 'utf-8')\n }\n\n const textPart = encodeBodyPart(input.text ?? '')\n headers.push('Content-Type: text/plain; charset=utf-8')\n headers.push(`Content-Transfer-Encoding: ${textPart.cte}`)\n return Buffer.from(headers.join('\\r\\n') + '\\r\\n\\r\\n' + textPart.body, 'utf-8')\n}\n\n// \u2500\u2500 Inbound MIME parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ParsedMail {\n messageId?: string | null\n inReplyTo?: string | null\n references?: string | string[] | null\n from?: { value?: Array<{ address?: string; name?: string }> }\n to?: { value?: Array<{ address?: string; name?: string }> }\n cc?: { value?: Array<{ address?: string; name?: string }> }\n bcc?: { value?: Array<{ address?: string; name?: string }> }\n subject?: string | null\n html?: string | false\n text?: string\n date?: string | Date | null\n attachments?: ParsedAttachment[]\n headers?: Map<string, unknown> | Record<string, unknown>\n}\n\nexport interface ParsedAttachment {\n content?: Buffer | Uint8Array\n contentType?: string\n filename?: string\n size?: number\n contentDisposition?: string\n cid?: string\n}\n\nexport function stripBrackets(value: string | undefined | null): string | undefined {\n if (!value) return undefined\n const trimmed = value.trim()\n if (!trimmed) return undefined\n if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1)\n return trimmed\n}\n\nexport function parseReferences(value: string | string[] | undefined | null): string[] {\n if (!value) return []\n if (Array.isArray(value)) return value.map((v) => stripBrackets(v)).filter((v): v is string => Boolean(v))\n return value\n .split(/\\s+/)\n .map((segment) => stripBrackets(segment))\n .filter((segment): segment is string => Boolean(segment))\n}\n\nexport function normalizeAttachments(attachments: ParsedAttachment[]): NormalizedAttachment[] {\n const out: NormalizedAttachment[] = []\n let totalBytes = 0\n for (const att of attachments) {\n if (!att.content) continue\n const byteLength = att.content.byteLength\n if (byteLength > EMAIL_MAX_ATTACHMENT_BYTES) {\n logger.warn('dropping oversized inbound attachment', { fileName: att.filename ?? 'attachment', bytes: byteLength, maxBytes: EMAIL_MAX_ATTACHMENT_BYTES })\n continue\n }\n if (totalBytes + byteLength > TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES) {\n logger.warn('aggregate inbound attachment size exceeded; dropping remaining attachments', { maxBytes: TOTAL_INBOUND_ATTACHMENTS_MAX_BYTES })\n break\n }\n totalBytes += byteLength\n const base64 = Buffer.isBuffer(att.content)\n ? att.content.toString('base64')\n : Buffer.from(att.content).toString('base64')\n out.push({\n url: `data:${att.contentType ?? 'application/octet-stream'};base64,${base64}`,\n mimeType: att.contentType ?? 'application/octet-stream',\n fileName: att.filename ?? 'attachment',\n fileSize: att.size,\n inline: Boolean(att.contentDisposition && /inline/i.test(att.contentDisposition)) || Boolean(att.cid),\n })\n }\n return out\n}\n\nfunction stringifyHeaderValue(value: unknown): string | undefined {\n if (typeof value === 'string') return value\n if (Array.isArray(value)) return value.map((v) => String(v)).join(', ')\n if (value instanceof Date) return value.toISOString()\n if (value && typeof value === 'object') {\n try {\n return JSON.stringify(value)\n } catch {\n return undefined\n }\n }\n if (value === undefined || value === null) return undefined\n return String(value)\n}\n\n/**\n * Flatten parsed MIME headers to a `Record<string, string>`.\n *\n * mailparser returns `headers` as a `Map<string, unknown>`. `Object.entries` on\n * a Map yields an empty array (Maps have no enumerable own properties), so we\n * iterate Map entries directly, with a Record fallback for test fakes.\n */\nexport function extractHeaders(headers: ParsedMail['headers']): Record<string, string> {\n if (!headers) return {}\n const out: Record<string, string> = {}\n if (headers instanceof Map) {\n for (const [key, value] of headers.entries()) {\n const stringified = stringifyHeaderValue(value)\n if (stringified !== undefined) out[String(key)] = stringified\n }\n return out\n }\n for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {\n const stringified = stringifyHeaderValue(value)\n if (stringified !== undefined) out[key] = stringified\n }\n return out\n}\n\nexport interface NormalizeMimeInboundOptions {\n /**\n * The parsed MIME message. Providers parse with their own `mailparser`\n * dependency (Gmail/IMAP) and pass the result here, so the hub stays free of a\n * MIME-parser dependency while still owning the normalization logic.\n */\n parsed: ParsedMail\n /** External identifier of the receiving channel (typically the account's email). */\n accountIdentifier: string\n /** Deterministic id used when the MIME message carries no `Message-ID` header. */\n fallbackMessageId: string\n /** Compute the conversation grouping id from the resolved message id + references. */\n resolveConversationId: (context: { messageId: string; references: string[] }) => string\n /** Fallback timestamp when the parsed message has no Date header. */\n fallbackDate?: Date\n /** Provider-specific fields merged into `channelMetadata`. */\n channelMetadata?: (parsed: ParsedMail) => Record<string, unknown>\n /** Provider-specific fields merged into `channelPayload`. */\n channelPayload?: (parsed: ParsedMail) => Record<string, unknown>\n}\n\n/**\n * Build the hub's canonical `NormalizedInboundMessage` from a parsed MIME\n * message. Providers supply the bits that genuinely differ (message-id\n * fallback, conversation grouping, extra metadata) and inherit the shared\n * threading / attachment / header logic.\n */\nexport function normalizeMimeInbound(options: NormalizeMimeInboundOptions): NormalizedInboundMessage {\n const { parsed } = options\n\n const messageId = stripBrackets(parsed.messageId) ?? options.fallbackMessageId\n const inReplyTo = stripBrackets(parsed.inReplyTo)\n const references = parseReferences(parsed.references)\n const conversationId = options.resolveConversationId({ messageId, references })\n\n const from = parsed.from?.value?.[0]\n const subject = parsed.subject?.trim() || undefined\n const bodyHtml = parsed.html && typeof parsed.html === 'string' ? parsed.html : undefined\n const bodyText = typeof parsed.text === 'string' ? parsed.text : undefined\n const body = bodyHtml ?? bodyText ?? ''\n const bodyFormat: 'text' | 'html' = bodyHtml ? 'html' : 'text'\n\n const attachments = normalizeAttachments(parsed.attachments ?? [])\n\n const channelMetadata: Record<string, unknown> = {\n ...(options.channelMetadata?.(parsed) ?? {}),\n messageId,\n inReplyTo: inReplyTo ?? null,\n references,\n headers: extractHeaders(parsed.headers),\n }\n\n const channelPayload: Record<string, unknown> = {\n subject,\n from: from ? { address: from.address, name: from.name } : null,\n to: parsed.to?.value ?? [],\n cc: parsed.cc?.value ?? [],\n bcc: parsed.bcc?.value ?? [],\n html: bodyHtml ?? null,\n text: bodyText ?? null,\n messageId,\n ...(options.channelPayload?.(parsed) ?? {}),\n }\n\n return {\n externalMessageId: messageId,\n externalConversationId: conversationId,\n senderIdentifier: from?.address ?? options.accountIdentifier,\n senderDisplayName: from?.name?.trim() || undefined,\n subject,\n body,\n bodyFormat,\n attachments,\n timestamp: parsed.date ? new Date(parsed.date) : options.fallbackDate ?? new Date(),\n replyToExternalId: inReplyTo ?? undefined,\n channelPayload,\n channelContentType: 'email/mime',\n channelMetadata,\n }\n}\n\n// \u2500\u2500 Provider sync cursor helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Encode a provider channel-state object into an opaque base64 sync cursor. */\nexport function encodeCursor(state: unknown): string {\n return Buffer.from(JSON.stringify(state)).toString('base64')\n}\n\n/** Decode a base64 sync cursor back into its object form, or `null` if malformed. */\nexport function decodeCursor(value: string | null | undefined): unknown {\n if (!value) return null\n try {\n return JSON.parse(Buffer.from(value, 'base64').toString('utf-8'))\n } catch {\n return null\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["textPart"]
|
|
7
7
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
const logger = createLogger("communication_channels").child({ component: "oauth-client-config" });
|
|
1
3
|
async function resolveOAuthClientCredentials(credentialsService, providerKey, scope) {
|
|
2
4
|
if (!credentialsService) return null;
|
|
3
5
|
const integrationId = `channel_${providerKey}`;
|
|
@@ -14,9 +16,9 @@ async function resolveOAuthClientCredentials(credentialsService, providerKey, sc
|
|
|
14
16
|
userId: null
|
|
15
17
|
});
|
|
16
18
|
} catch (resolveErr) {
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
19
|
+
logger.warn(
|
|
20
|
+
"resolveOAuthClientCredentials credential resolve failed for an org scope",
|
|
21
|
+
{ err: resolveErr }
|
|
20
22
|
);
|
|
21
23
|
row = null;
|
|
22
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'oauth-client-config' })\n\n/**\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 logger.warn(\n 'resolveOAuthClientCredentials credential resolve failed for an org scope',\n { err: 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": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,sBAAsB,CAAC;AA4ChG,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,aAAO;AAAA,QACL;AAAA,QACA,EAAE,KAAK,WAAW;AAAA,MACpB;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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
import { ChannelMutationBlockedError, guardOutboundCreate } from "./mutation-guards.js";
|
|
9
9
|
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "./queue.js";
|
|
10
10
|
import { htmlToText } from "./email-mime.js";
|
|
11
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
|
+
const logger = createLogger("communication_channels");
|
|
11
13
|
async function sendAsUser(container, actor, input) {
|
|
12
14
|
if (!input.body.plain && !input.body.html) {
|
|
13
15
|
return { ok: false, status: 422, error: "Either body.plain or body.html is required" };
|
|
@@ -82,7 +84,7 @@ async function sendAsUser(container, actor, input) {
|
|
|
82
84
|
}
|
|
83
85
|
);
|
|
84
86
|
} catch (err) {
|
|
85
|
-
|
|
87
|
+
logger.error("communication_channels send-as-user compose failed", { err });
|
|
86
88
|
return { ok: false, status: 500, error: "[internal] compose failed" };
|
|
87
89
|
}
|
|
88
90
|
const messageId = result.result.id;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/send-as-user.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n MessageChannelLink,\n} from '../data/entities'\nimport { ChannelMutationBlockedError, guardOutboundCreate } from './mutation-guards'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from './queue'\nimport type { OutboundDeliveryPayload } from '../workers/outbound-delivery'\nimport { htmlToText } from './email-mime'\n\nexport type SendAsUserActor = {\n userId: string\n tenantId: string\n organizationId: string | null\n /** Forwarded as the messages compose command `ctx.auth`. */\n auth?: unknown\n}\n\nexport type SendAsUserInput = {\n userChannelId: string\n to: string[]\n cc?: string[]\n bcc?: string[]\n subject: string\n body: { plain?: string; html?: string }\n inReplyTo?: string\n references?: string[]\n /**\n * Open Mercato `messages.message` id of the message being replied to. When\n * set, the composed message joins that message's thread (the messages module\n * derives `threadId` from the parent), so a CRM reply continues the existing\n * conversation instead of starting a new thread. Optional \u2014 omitted for new\n * threads.\n */\n parentMessageId?: string\n /**\n * Free-form metadata persisted on the resulting MessageChannelLink. Used by\n * downstream subscribers (e.g. the customers module's link-channel-message\n * subscriber) to anchor the sent message back to a CRM Person or honor a\n * caller-specified visibility flag. The hub does not interpret these keys.\n */\n channelMetadata?: Record<string, unknown>\n}\n\nexport type SendAsUserResult =\n | {\n ok: true\n messageId: string\n threadId: string\n channelId: string\n providerKey: string\n }\n | { ok: false; status: number; error: string; fieldErrors?: Record<string, string> }\n\n/**\n * In-process send-as-user facade.\n *\n * Validates the actor owns `userChannelId`, composes the Message via the\n * messages module command, persists the outbound MessageChannelLink + thread\n * mapping, and enqueues delivery. Returns a discriminated result instead of an\n * HTTP Response so it can be invoked both by the `/send-as-user` route and by\n * other modules via DI (`container.resolve('communicationChannelsSendAsUser')`)\n * \u2014 no HTTP self-call required.\n */\nexport async function sendAsUser(\n container: AppContainer,\n actor: SendAsUserActor,\n input: SendAsUserInput,\n): Promise<SendAsUserResult> {\n if (!input.body.plain && !input.body.html) {\n return { ok: false, status: 422, error: 'Either body.plain or body.html is required' }\n }\n // Re-validate here (not only in the HTTP route's zod schema) because this\n // facade is also reachable via DI; `input.to[0]` below would otherwise become\n // `undefined` for an empty recipient list.\n if (!Array.isArray(input.to) || input.to.length === 0) {\n return { ok: false, status: 422, error: 'At least one recipient is required' }\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n const { tenantId } = actor\n const organizationId = actor.organizationId\n const dscope = { tenantId, organizationId }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: input.userChannelId, tenantId, organizationId, deletedAt: null },\n undefined,\n dscope,\n )\n if (!channel) return { ok: false, status: 404, error: 'Channel not found' }\n if (channel.userId !== actor.userId) {\n return { ok: false, status: 403, error: 'You can only send through channels you own' }\n }\n\n // Hub mutation guard: map known non-deliverable states (`requires_reauth`,\n // `disconnected`) to a 422 with field-level errors; other transitional states\n // fall through to the 409 below.\n try {\n await guardOutboundCreate(em, { channelId: channel.id, scope: dscope })\n } catch (err) {\n if (err instanceof ChannelMutationBlockedError) {\n return { ok: false, status: 422, error: err.message, fieldErrors: err.errors }\n }\n throw err\n }\n if (!channel.isActive || channel.status !== 'connected') {\n return {\n ok: false,\n status: 409,\n error: `Channel is in status '${channel.status}' (not connected)`,\n }\n }\n\n // Create the Message via the messages module compose command. The outbound\n // subscriber picks it up via `messages.message.sent` and routes through the\n // adapter chain.\n const commandBus = container.resolve('commandBus') as CommandBus\n const messageBody = input.body.plain ?? htmlToText(input.body.html ?? '')\n const composeInput = {\n type: `channel.${channel.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.send_as_user',\n sourceEntityId: channel.id,\n externalEmail: input.to[0],\n externalName: input.subject,\n recipients: [],\n subject: input.subject,\n body: messageBody,\n bodyFormat: 'text' as const,\n priority: 'normal' as const,\n sendViaEmail: false,\n parentMessageId: input.parentMessageId,\n isDraft: false,\n tenantId,\n organizationId,\n userId: actor.userId,\n }\n let result\n try {\n result = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: {\n container,\n auth: actor.auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n },\n )\n } catch (err) {\n console.error('[communication_channels] send-as-user compose failed', err)\n return { ok: false, status: 500, error: '[internal] compose failed' }\n }\n\n const messageId = result.result.id\n const messageThreadId = result.result.threadId ?? messageId\n const externalThreadRef = `outbound:${messageThreadId}`\n\n // Persist the conversation, thread mapping, and channel link as one unit. The\n // conversation's id is DB-generated (`gen_random_uuid()`), so it must be\n // flushed before the mapping/link can reference it \u2014 hence the eager flushes\n // rather than `withAtomicFlush` (single trailing flush). `em.transactional`\n // wraps the whole sequence so a mid-way failure rolls back all three writes\n // instead of leaving an orphaned conversation row.\n await em.transactional(async () => {\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: channel.id,\n externalConversationId: externalThreadRef,\n tenantId,\n organizationId,\n },\n undefined,\n dscope,\n )\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: channel.id,\n externalConversationId: externalThreadRef,\n subject: input.subject,\n assignedUserId: actor.userId,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n } else {\n conversation.subject = conversation.subject ?? input.subject\n conversation.assignedUserId = conversation.assignedUserId ?? actor.userId\n conversation.lastMessageAt = new Date()\n // Flush the scalar updates BEFORE the ChannelThreadMapping lookup below: a\n // find/findOne on the same EntityManager between a scalar mutation and flush\n // can silently drop the pending UPDATE (core AGENTS.md \u2192 Entity Update Safety).\n await em.flush()\n }\n\n const existingMapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId,\n organizationId,\n },\n undefined,\n dscope,\n )\n if (!existingMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId,\n channelId: channel.id,\n providerKey: channel.providerKey,\n externalThreadRef,\n assignedUserId: actor.userId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n }\n\n const channelLink = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n direction: 'outbound',\n deliveryStatus: 'pending',\n channelPayload: {\n text: input.body.plain ?? messageBody,\n ...(input.body.html ? { html: input.body.html } : {}),\n },\n channelContentType: input.body.html ? 'text/html' : 'text/plain',\n channelMetadata: {\n // Caller-supplied pass-through metadata merged FIRST so the validated\n // routing fields below always win \u2014 a caller cannot override the\n // recipients/subject/threading headers via `channelMetadata`.\n ...(input.channelMetadata ?? {}),\n to: input.to,\n cc: input.cc ?? [],\n bcc: input.bcc ?? [],\n subject: input.subject,\n inReplyTo: input.inReplyTo ?? null,\n references: input.references ?? [],\n },\n tenantId,\n organizationId,\n })\n em.persist(channelLink)\n await em.flush()\n })\n\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)\n const deliveryJob: OutboundDeliveryPayload = {\n messageId,\n scope: dscope,\n attempt: 1,\n }\n await queue.enqueue(deliveryJob as unknown as Record<string, unknown>)\n\n return {\n ok: true,\n messageId,\n threadId: messageThreadId,\n channelId: channel.id,\n providerKey: channel.providerKey,\n }\n}\n\n/** DI service type for cross-module callers (resolve `communicationChannelsSendAsUser`). */\nexport type SendAsUserService = typeof sendAsUser\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B,2BAA2B;AACjE,SAAS,+BAA+B,qCAAqC;AAE7E,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n MessageChannelLink,\n} from '../data/entities'\nimport { ChannelMutationBlockedError, guardOutboundCreate } from './mutation-guards'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from './queue'\nimport type { OutboundDeliveryPayload } from '../workers/outbound-delivery'\nimport { htmlToText } from './email-mime'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels')\n\nexport type SendAsUserActor = {\n userId: string\n tenantId: string\n organizationId: string | null\n /** Forwarded as the messages compose command `ctx.auth`. */\n auth?: unknown\n}\n\nexport type SendAsUserInput = {\n userChannelId: string\n to: string[]\n cc?: string[]\n bcc?: string[]\n subject: string\n body: { plain?: string; html?: string }\n inReplyTo?: string\n references?: string[]\n /**\n * Open Mercato `messages.message` id of the message being replied to. When\n * set, the composed message joins that message's thread (the messages module\n * derives `threadId` from the parent), so a CRM reply continues the existing\n * conversation instead of starting a new thread. Optional \u2014 omitted for new\n * threads.\n */\n parentMessageId?: string\n /**\n * Free-form metadata persisted on the resulting MessageChannelLink. Used by\n * downstream subscribers (e.g. the customers module's link-channel-message\n * subscriber) to anchor the sent message back to a CRM Person or honor a\n * caller-specified visibility flag. The hub does not interpret these keys.\n */\n channelMetadata?: Record<string, unknown>\n}\n\nexport type SendAsUserResult =\n | {\n ok: true\n messageId: string\n threadId: string\n channelId: string\n providerKey: string\n }\n | { ok: false; status: number; error: string; fieldErrors?: Record<string, string> }\n\n/**\n * In-process send-as-user facade.\n *\n * Validates the actor owns `userChannelId`, composes the Message via the\n * messages module command, persists the outbound MessageChannelLink + thread\n * mapping, and enqueues delivery. Returns a discriminated result instead of an\n * HTTP Response so it can be invoked both by the `/send-as-user` route and by\n * other modules via DI (`container.resolve('communicationChannelsSendAsUser')`)\n * \u2014 no HTTP self-call required.\n */\nexport async function sendAsUser(\n container: AppContainer,\n actor: SendAsUserActor,\n input: SendAsUserInput,\n): Promise<SendAsUserResult> {\n if (!input.body.plain && !input.body.html) {\n return { ok: false, status: 422, error: 'Either body.plain or body.html is required' }\n }\n // Re-validate here (not only in the HTTP route's zod schema) because this\n // facade is also reachable via DI; `input.to[0]` below would otherwise become\n // `undefined` for an empty recipient list.\n if (!Array.isArray(input.to) || input.to.length === 0) {\n return { ok: false, status: 422, error: 'At least one recipient is required' }\n }\n\n const em = (container.resolve('em') as EntityManager).fork()\n const { tenantId } = actor\n const organizationId = actor.organizationId\n const dscope = { tenantId, organizationId }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: input.userChannelId, tenantId, organizationId, deletedAt: null },\n undefined,\n dscope,\n )\n if (!channel) return { ok: false, status: 404, error: 'Channel not found' }\n if (channel.userId !== actor.userId) {\n return { ok: false, status: 403, error: 'You can only send through channels you own' }\n }\n\n // Hub mutation guard: map known non-deliverable states (`requires_reauth`,\n // `disconnected`) to a 422 with field-level errors; other transitional states\n // fall through to the 409 below.\n try {\n await guardOutboundCreate(em, { channelId: channel.id, scope: dscope })\n } catch (err) {\n if (err instanceof ChannelMutationBlockedError) {\n return { ok: false, status: 422, error: err.message, fieldErrors: err.errors }\n }\n throw err\n }\n if (!channel.isActive || channel.status !== 'connected') {\n return {\n ok: false,\n status: 409,\n error: `Channel is in status '${channel.status}' (not connected)`,\n }\n }\n\n // Create the Message via the messages module compose command. The outbound\n // subscriber picks it up via `messages.message.sent` and routes through the\n // adapter chain.\n const commandBus = container.resolve('commandBus') as CommandBus\n const messageBody = input.body.plain ?? htmlToText(input.body.html ?? '')\n const composeInput = {\n type: `channel.${channel.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.send_as_user',\n sourceEntityId: channel.id,\n externalEmail: input.to[0],\n externalName: input.subject,\n recipients: [],\n subject: input.subject,\n body: messageBody,\n bodyFormat: 'text' as const,\n priority: 'normal' as const,\n sendViaEmail: false,\n parentMessageId: input.parentMessageId,\n isDraft: false,\n tenantId,\n organizationId,\n userId: actor.userId,\n }\n let result\n try {\n result = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: {\n container,\n auth: actor.auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n },\n },\n )\n } catch (err) {\n logger.error('communication_channels send-as-user compose failed', { err })\n return { ok: false, status: 500, error: '[internal] compose failed' }\n }\n\n const messageId = result.result.id\n const messageThreadId = result.result.threadId ?? messageId\n const externalThreadRef = `outbound:${messageThreadId}`\n\n // Persist the conversation, thread mapping, and channel link as one unit. The\n // conversation's id is DB-generated (`gen_random_uuid()`), so it must be\n // flushed before the mapping/link can reference it \u2014 hence the eager flushes\n // rather than `withAtomicFlush` (single trailing flush). `em.transactional`\n // wraps the whole sequence so a mid-way failure rolls back all three writes\n // instead of leaving an orphaned conversation row.\n await em.transactional(async () => {\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: channel.id,\n externalConversationId: externalThreadRef,\n tenantId,\n organizationId,\n },\n undefined,\n dscope,\n )\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: channel.id,\n externalConversationId: externalThreadRef,\n subject: input.subject,\n assignedUserId: actor.userId,\n tenantId,\n organizationId,\n lastMessageAt: new Date(),\n })\n em.persist(conversation)\n await em.flush()\n } else {\n conversation.subject = conversation.subject ?? input.subject\n conversation.assignedUserId = conversation.assignedUserId ?? actor.userId\n conversation.lastMessageAt = new Date()\n // Flush the scalar updates BEFORE the ChannelThreadMapping lookup below: a\n // find/findOne on the same EntityManager between a scalar mutation and flush\n // can silently drop the pending UPDATE (core AGENTS.md \u2192 Entity Update Safety).\n await em.flush()\n }\n\n const existingMapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId,\n organizationId,\n },\n undefined,\n dscope,\n )\n if (!existingMapping) {\n const mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId,\n channelId: channel.id,\n providerKey: channel.providerKey,\n externalThreadRef,\n assignedUserId: actor.userId,\n tenantId,\n organizationId,\n })\n em.persist(mapping)\n }\n\n const channelLink = em.create(MessageChannelLink, {\n messageId,\n externalConversationId: conversation.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n direction: 'outbound',\n deliveryStatus: 'pending',\n channelPayload: {\n text: input.body.plain ?? messageBody,\n ...(input.body.html ? { html: input.body.html } : {}),\n },\n channelContentType: input.body.html ? 'text/html' : 'text/plain',\n channelMetadata: {\n // Caller-supplied pass-through metadata merged FIRST so the validated\n // routing fields below always win \u2014 a caller cannot override the\n // recipients/subject/threading headers via `channelMetadata`.\n ...(input.channelMetadata ?? {}),\n to: input.to,\n cc: input.cc ?? [],\n bcc: input.bcc ?? [],\n subject: input.subject,\n inReplyTo: input.inReplyTo ?? null,\n references: input.references ?? [],\n },\n tenantId,\n organizationId,\n })\n em.persist(channelLink)\n await em.flush()\n })\n\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.outbound)\n const deliveryJob: OutboundDeliveryPayload = {\n messageId,\n scope: dscope,\n attempt: 1,\n }\n await queue.enqueue(deliveryJob as unknown as Record<string, unknown>)\n\n return {\n ok: true,\n messageId,\n threadId: messageThreadId,\n channelId: channel.id,\n providerKey: channel.providerKey,\n }\n}\n\n/** DI service type for cross-module callers (resolve `communicationChannelsSendAsUser`). */\nexport type SendAsUserService = typeof sendAsUser\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,8 @@ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
|
2
2
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
3
|
import { ChannelThreadToken } from "../data/entities.js";
|
|
4
4
|
import { isUniqueViolation } from "./pg-errors.js";
|
|
5
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
|
+
const logger = createLogger("communication_channels").child({ component: "thread-token" });
|
|
5
7
|
const TOKEN_PREFIX = "om_";
|
|
6
8
|
const RANDOM_BYTES = 16;
|
|
7
9
|
const HMAC_BYTES = 8;
|
|
@@ -27,8 +29,8 @@ function getKey() {
|
|
|
27
29
|
`[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014 refusing to sign thread tokens with a static dev key in production.`
|
|
28
30
|
);
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
`
|
|
32
|
+
logger.warn(
|
|
33
|
+
`No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured. Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.`
|
|
32
34
|
);
|
|
33
35
|
cachedKey = createHash("sha256").update("open-mercato-thread-token-dev").digest();
|
|
34
36
|
return cachedKey;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/thread-token.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadToken } from '../data/entities'\nimport { isUniqueViolation } from './pg-errors'\n\n/**\n * Per-thread crypto token used by the layered thread-matcher to attach\n * inbound replies to the originating Open Mercato message thread, even\n * when the recipient's mail client strips RFC 5322 headers.\n *\n * Token format: `om_<22b64url>_<11b64url>` \u2014 16 random bytes followed by\n * 8 bytes of HMAC-SHA256(random, key), each base64url-encoded without\n * padding. Approximately 37 characters total.\n *\n * Tokens are stored on the `channel_thread_tokens` table keyed by\n * `(tenantId, token)` so that even if the HMAC key leaked, tenant\n * isolation still holds at the DB layer.\n *\n * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.\n */\n\nconst TOKEN_PREFIX = 'om_'\nconst RANDOM_BYTES = 16\nconst HMAC_BYTES = 8\nconst HMAC_KEY_ENV = 'OM_THREAD_TOKEN_SECRET'\nconst HMAC_FALLBACK_KEY_ENV = 'KMS_MASTER_KEY'\nconst HMAC_KEY_INFO = 'thread-token'\n\n/**\n * Pre-validated regex for parsing token candidates extracted from headers\n * or body content. Matches our exact format and rejects anything else\n * before HMAC verification \u2014 defense in depth.\n */\nconst TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/\n\nlet cachedKey: Buffer | null = null\n\n/** Resolve the HMAC key. Falls back through env vars per the spec. */\nfunction getKey(): Buffer {\n if (cachedKey) return cachedKey\n const primary = process.env[HMAC_KEY_ENV]\n if (primary && primary.length > 0) {\n cachedKey = Buffer.from(primary, 'utf8')\n return cachedKey\n }\n const fallback = process.env[HMAC_FALLBACK_KEY_ENV]\n if (fallback && fallback.length > 0) {\n // HKDF-style: derive a per-purpose subkey by HMAC-ing the fallback secret\n // with a constant info label so different purposes don't share a key.\n cachedKey = createHmac('sha256', fallback).update(HMAC_KEY_INFO).digest()\n return cachedKey\n }\n // No secret configured. Fail closed in production rather than signing thread\n // tokens with a public static key (which would let anyone forge a thread\n // token). In non-production we fall back to a dev-only static key and warn.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014` +\n ' refusing to sign thread tokens with a static dev key in production.',\n )\n }\n console.warn(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured.` +\n ' Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.',\n )\n cachedKey = createHash('sha256').update('open-mercato-thread-token-dev').digest()\n return cachedKey\n}\n\n/** Reset the cached key \u2014 for tests that mutate env vars. */\nexport function _resetThreadTokenKeyCache(): void {\n cachedKey = null\n}\n\nfunction base64urlEncode(buf: Buffer): string {\n return buf\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n}\n\nfunction base64urlDecode(value: string): Buffer | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) return null\n const padded = value + '='.repeat((4 - (value.length % 4)) % 4)\n try {\n return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')\n } catch {\n return null\n }\n}\n\nfunction computeHmacBytes(random: Buffer): Buffer {\n return createHmac('sha256', getKey()).update(random).digest().subarray(0, HMAC_BYTES)\n}\n\n/**\n * Generate a new HMAC-signed thread token. The 16 random bytes make a token\n * collision astronomically unlikely; the `(tenantId, token)` unique constraint\n * is the backstop. Per-thread deduplication (one token per thread) is handled\n * separately by `getOrCreateThreadToken` via the `(tenantId, messageThreadId)`\n * unique constraint \u2014 not here.\n */\nexport function generateToken(): string {\n const random = randomBytes(RANDOM_BYTES)\n const hmac = computeHmacBytes(random)\n return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`\n}\n\n// Fixed lengths of the base64url-encoded components, without padding.\n// Computed once: 16 bytes -> 22 chars, 8 bytes -> 11 chars.\nconst RANDOM_B64_LEN = Math.ceil((RANDOM_BYTES * 4) / 3)\nconst HMAC_B64_LEN = Math.ceil((HMAC_BYTES * 4) / 3)\nconst TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN\n\n/**\n * Verify the HMAC signature on a token. Returns `true` only when the\n * structural form is correct AND the HMAC matches under the current key.\n *\n * Does NOT verify the token exists in the DB \u2014 that lookup is the\n * matcher's responsibility (see `thread-matcher.ts`). Verifying here\n * lets us drop forged tokens before any DB I/O.\n *\n * Parsing note: base64url-encoded random/HMAC portions may themselves\n * contain `_` characters, so `split('_')` is unsafe. We parse positionally\n * using the fixed lengths declared above.\n */\nexport function verifyToken(token: string): boolean {\n if (typeof token !== 'string') return false\n if (token.length !== TOKEN_TOTAL_LEN) return false\n if (!token.startsWith(TOKEN_PREFIX)) return false\n const randomStart = TOKEN_PREFIX.length\n const randomEnd = randomStart + RANDOM_B64_LEN\n const separator = token[randomEnd]\n if (separator !== '_') return false\n const hmacStart = randomEnd + 1\n const randomPart = token.slice(randomStart, randomEnd)\n const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN)\n const random = base64urlDecode(randomPart)\n if (!random || random.length !== RANDOM_BYTES) return false\n const provided = base64urlDecode(hmacPart)\n if (!provided || provided.length !== HMAC_BYTES) return false\n const expected = computeHmacBytes(random)\n try {\n return timingSafeEqual(provided, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Build the synthetic RFC 5322 Message-ID we inject into outbound\n * `References:` headers. Uses the IANA-reserved `.invalid` TLD (RFC 6761\n * \u00A7 3) so RFC-compliant MTAs MUST accept it as syntactically valid.\n */\nexport function buildReferencesId(token: string): string {\n return `<${token}@open-mercato.invalid>`\n}\n\n/**\n * Build the hidden HTML body span + plain-text trailer used as the\n * token's secondary attachment point (in case `References` is stripped\n * by the recipient's MUA).\n */\nexport function buildBodyFooter(token: string): { html: string; plain: string } {\n return {\n html: `<span style=\"display:none\">[OM:${token}]</span>`,\n plain: `\\n\\n[OM:${token}]`,\n }\n}\n\n/**\n * Apply the thread token to an outbound MIME-like payload. Mutates the\n * input shape minimally and idempotently:\n * - `headers.references`: appends the synthetic `<om_TOKEN@\u2026>` id if not\n * already present (deduped).\n * - `bodyHtml`: injects a hidden `<span>` before the last `</body>` tag,\n * or appends if no `</body>` is present.\n * - `bodyText`: appends the plain-text trailer.\n *\n * Returns a NEW object \u2014 does not mutate the input. Callers that maintain\n * their own MIME structure can call the building blocks directly.\n */\nexport function applyOutboundThreadingToken<\n T extends {\n headers?: Record<string, string | string[] | undefined>\n bodyHtml?: string\n bodyText?: string\n },\n>(payload: T, token: string): T {\n if (!verifyToken(token)) {\n throw new Error('applyOutboundThreadingToken: invalid token format/signature')\n }\n const refId = buildReferencesId(token)\n const footer = buildBodyFooter(token)\n\n const headers = { ...(payload.headers ?? {}) } as Record<string, string | string[] | undefined>\n const existingRefs = headers['references'] ?? headers['References']\n let nextRefs: string\n if (Array.isArray(existingRefs)) {\n nextRefs = existingRefs.includes(refId) ? existingRefs.join(' ') : [...existingRefs, refId].join(' ')\n } else if (typeof existingRefs === 'string' && existingRefs.length > 0) {\n nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`\n } else {\n nextRefs = refId\n }\n // Normalise to the canonical RFC 5322 header name and drop any duplicate\n // lowercase entry so the MTA sees a single `References` header.\n delete headers['references']\n headers['References'] = nextRefs\n\n let bodyHtml = payload.bodyHtml\n if (typeof bodyHtml === 'string') {\n if (!bodyHtml.includes(`[OM:${token}]`)) {\n const closing = bodyHtml.lastIndexOf('</body>')\n if (closing >= 0) {\n bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`\n } else {\n bodyHtml = `${bodyHtml}${footer.html}`\n }\n }\n }\n\n let bodyText = payload.bodyText\n if (typeof bodyText === 'string') {\n if (!bodyText.includes(`[OM:${token}]`)) {\n bodyText = `${bodyText}${footer.plain}`\n }\n }\n\n return {\n ...payload,\n headers,\n ...(bodyHtml !== undefined ? { bodyHtml } : {}),\n ...(bodyText !== undefined ? { bodyText } : {}),\n }\n}\n\n/**\n * Extract token candidates from a `References` / `In-Reply-To` header\n * value (string or string[]) and return the FIRST one that HMAC-verifies.\n * Returns `null` if no valid token is present.\n */\nexport function extractTokenFromHeaders(\n inReplyTo: string | null | undefined,\n references: string[] | string | null | undefined,\n): string | null {\n const haystack: string[] = []\n if (typeof inReplyTo === 'string' && inReplyTo.length > 0) haystack.push(inReplyTo)\n if (Array.isArray(references)) haystack.push(...references)\n else if (typeof references === 'string' && references.length > 0) haystack.push(references)\n for (const candidate of haystack) {\n const matches = candidate.match(new RegExp(TOKEN_REGEX, 'g'))\n if (!matches) continue\n for (const match of matches) {\n if (verifyToken(match)) return match\n }\n }\n return null\n}\n\n/**\n * Idempotent get-or-create: return the existing `ChannelThreadToken` for the\n * given thread, or create + return a new one. Idempotency is enforced by the\n * `channel_thread_tokens_thread_uq` unique constraint on\n * `(tenant_id, message_thread_id)`: a concurrent double-create loses the race\n * with a unique violation, which we catch and resolve by re-selecting the\n * winner \u2014 so callers always converge on exactly one token per thread.\n *\n * Reads via the standard EntityManager (no encryption needed \u2014 the token\n * column itself is the HMAC-signed value, not encrypted at rest).\n *\n * Use cases:\n * - Outbound subscriber: get or create a token before injecting it\n * into the outbound MIME (`applyOutboundThreadingToken`).\n * - Future \"reset\" UI for tenant admins: explicit rotation by deleting\n * the row + calling this helper again.\n */\nexport async function getOrCreateThreadToken(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n messageThreadId: string\n },\n): Promise<{ token: string; created: boolean }> {\n const dscope = { tenantId: args.tenantId, organizationId: args.organizationId }\n const existing = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (existing) {\n return { token: existing.token, created: false }\n }\n const row = em.create(ChannelThreadToken, {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n token: generateToken(),\n })\n // MikroORM v7 removed `persistAndFlush` \u2014 split into persist + flush.\n em.persist(row)\n try {\n await em.flush()\n return { token: row.token, created: true }\n } catch (err) {\n // A concurrent create for the same (tenant, thread) won the race; the\n // unique constraint rejected ours. Re-select the winner on a clean fork so\n // we never return a half-persisted row or surface a spurious error.\n if (!isUniqueViolation(err)) throw err\n const winner = await findOneWithDecryption(\n em.fork(),\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (winner) return { token: winner.token, created: false }\n throw err\n }\n}\n\n/**\n * Extract a token candidate from an inbound body (HTML or plain text).\n * Scans for `[OM:om_\u2026]` markers and returns the first that HMAC-verifies.\n */\nexport function extractTokenFromBody(\n bodyHtml: string | null | undefined,\n bodyText: string | null | undefined,\n): string | null {\n const haystacks = [bodyHtml, bodyText].filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n )\n const pattern = new RegExp(`\\\\[OM:(${TOKEN_REGEX.source})\\\\]`, 'g')\n for (const haystack of haystacks) {\n let match: RegExpExecArray | null\n pattern.lastIndex = 0\n while ((match = pattern.exec(haystack)) !== null) {\n if (verifyToken(match[1])) return match[1]\n }\n }\n return null\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,YAAY,aAAa,uBAAuB;AAErE,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { createHash, createHmac, randomBytes, timingSafeEqual } from 'crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { ChannelThreadToken } from '../data/entities'\nimport { isUniqueViolation } from './pg-errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'thread-token' })\n\n/**\n * Per-thread crypto token used by the layered thread-matcher to attach\n * inbound replies to the originating Open Mercato message thread, even\n * when the recipient's mail client strips RFC 5322 headers.\n *\n * Token format: `om_<22b64url>_<11b64url>` \u2014 16 random bytes followed by\n * 8 bytes of HMAC-SHA256(random, key), each base64url-encoded without\n * padding. Approximately 37 characters total.\n *\n * Tokens are stored on the `channel_thread_tokens` table keyed by\n * `(tenantId, token)` so that even if the HMAC key leaked, tenant\n * isolation still holds at the DB layer.\n *\n * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.\n */\n\nconst TOKEN_PREFIX = 'om_'\nconst RANDOM_BYTES = 16\nconst HMAC_BYTES = 8\nconst HMAC_KEY_ENV = 'OM_THREAD_TOKEN_SECRET'\nconst HMAC_FALLBACK_KEY_ENV = 'KMS_MASTER_KEY'\nconst HMAC_KEY_INFO = 'thread-token'\n\n/**\n * Pre-validated regex for parsing token candidates extracted from headers\n * or body content. Matches our exact format and rejects anything else\n * before HMAC verification \u2014 defense in depth.\n */\nconst TOKEN_REGEX = /om_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{11}/\n\nlet cachedKey: Buffer | null = null\n\n/** Resolve the HMAC key. Falls back through env vars per the spec. */\nfunction getKey(): Buffer {\n if (cachedKey) return cachedKey\n const primary = process.env[HMAC_KEY_ENV]\n if (primary && primary.length > 0) {\n cachedKey = Buffer.from(primary, 'utf8')\n return cachedKey\n }\n const fallback = process.env[HMAC_FALLBACK_KEY_ENV]\n if (fallback && fallback.length > 0) {\n // HKDF-style: derive a per-purpose subkey by HMAC-ing the fallback secret\n // with a constant info label so different purposes don't share a key.\n cachedKey = createHmac('sha256', fallback).update(HMAC_KEY_INFO).digest()\n return cachedKey\n }\n // No secret configured. Fail closed in production rather than signing thread\n // tokens with a public static key (which would let anyone forge a thread\n // token). In non-production we fall back to a dev-only static key and warn.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n `[communication_channels] No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured \u2014` +\n ' refusing to sign thread tokens with a static dev key in production.',\n )\n }\n logger.warn(\n `No ${HMAC_KEY_ENV} or ${HMAC_FALLBACK_KEY_ENV} configured.` +\n ' Thread tokens will use a dev-only static key \u2014 DO NOT USE IN PRODUCTION.',\n )\n cachedKey = createHash('sha256').update('open-mercato-thread-token-dev').digest()\n return cachedKey\n}\n\n/** Reset the cached key \u2014 for tests that mutate env vars. */\nexport function _resetThreadTokenKeyCache(): void {\n cachedKey = null\n}\n\nfunction base64urlEncode(buf: Buffer): string {\n return buf\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '')\n}\n\nfunction base64urlDecode(value: string): Buffer | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value)) return null\n const padded = value + '='.repeat((4 - (value.length % 4)) % 4)\n try {\n return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64')\n } catch {\n return null\n }\n}\n\nfunction computeHmacBytes(random: Buffer): Buffer {\n return createHmac('sha256', getKey()).update(random).digest().subarray(0, HMAC_BYTES)\n}\n\n/**\n * Generate a new HMAC-signed thread token. The 16 random bytes make a token\n * collision astronomically unlikely; the `(tenantId, token)` unique constraint\n * is the backstop. Per-thread deduplication (one token per thread) is handled\n * separately by `getOrCreateThreadToken` via the `(tenantId, messageThreadId)`\n * unique constraint \u2014 not here.\n */\nexport function generateToken(): string {\n const random = randomBytes(RANDOM_BYTES)\n const hmac = computeHmacBytes(random)\n return `${TOKEN_PREFIX}${base64urlEncode(random)}_${base64urlEncode(hmac)}`\n}\n\n// Fixed lengths of the base64url-encoded components, without padding.\n// Computed once: 16 bytes -> 22 chars, 8 bytes -> 11 chars.\nconst RANDOM_B64_LEN = Math.ceil((RANDOM_BYTES * 4) / 3)\nconst HMAC_B64_LEN = Math.ceil((HMAC_BYTES * 4) / 3)\nconst TOKEN_TOTAL_LEN = TOKEN_PREFIX.length + RANDOM_B64_LEN + 1 + HMAC_B64_LEN\n\n/**\n * Verify the HMAC signature on a token. Returns `true` only when the\n * structural form is correct AND the HMAC matches under the current key.\n *\n * Does NOT verify the token exists in the DB \u2014 that lookup is the\n * matcher's responsibility (see `thread-matcher.ts`). Verifying here\n * lets us drop forged tokens before any DB I/O.\n *\n * Parsing note: base64url-encoded random/HMAC portions may themselves\n * contain `_` characters, so `split('_')` is unsafe. We parse positionally\n * using the fixed lengths declared above.\n */\nexport function verifyToken(token: string): boolean {\n if (typeof token !== 'string') return false\n if (token.length !== TOKEN_TOTAL_LEN) return false\n if (!token.startsWith(TOKEN_PREFIX)) return false\n const randomStart = TOKEN_PREFIX.length\n const randomEnd = randomStart + RANDOM_B64_LEN\n const separator = token[randomEnd]\n if (separator !== '_') return false\n const hmacStart = randomEnd + 1\n const randomPart = token.slice(randomStart, randomEnd)\n const hmacPart = token.slice(hmacStart, hmacStart + HMAC_B64_LEN)\n const random = base64urlDecode(randomPart)\n if (!random || random.length !== RANDOM_BYTES) return false\n const provided = base64urlDecode(hmacPart)\n if (!provided || provided.length !== HMAC_BYTES) return false\n const expected = computeHmacBytes(random)\n try {\n return timingSafeEqual(provided, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Build the synthetic RFC 5322 Message-ID we inject into outbound\n * `References:` headers. Uses the IANA-reserved `.invalid` TLD (RFC 6761\n * \u00A7 3) so RFC-compliant MTAs MUST accept it as syntactically valid.\n */\nexport function buildReferencesId(token: string): string {\n return `<${token}@open-mercato.invalid>`\n}\n\n/**\n * Build the hidden HTML body span + plain-text trailer used as the\n * token's secondary attachment point (in case `References` is stripped\n * by the recipient's MUA).\n */\nexport function buildBodyFooter(token: string): { html: string; plain: string } {\n return {\n html: `<span style=\"display:none\">[OM:${token}]</span>`,\n plain: `\\n\\n[OM:${token}]`,\n }\n}\n\n/**\n * Apply the thread token to an outbound MIME-like payload. Mutates the\n * input shape minimally and idempotently:\n * - `headers.references`: appends the synthetic `<om_TOKEN@\u2026>` id if not\n * already present (deduped).\n * - `bodyHtml`: injects a hidden `<span>` before the last `</body>` tag,\n * or appends if no `</body>` is present.\n * - `bodyText`: appends the plain-text trailer.\n *\n * Returns a NEW object \u2014 does not mutate the input. Callers that maintain\n * their own MIME structure can call the building blocks directly.\n */\nexport function applyOutboundThreadingToken<\n T extends {\n headers?: Record<string, string | string[] | undefined>\n bodyHtml?: string\n bodyText?: string\n },\n>(payload: T, token: string): T {\n if (!verifyToken(token)) {\n throw new Error('applyOutboundThreadingToken: invalid token format/signature')\n }\n const refId = buildReferencesId(token)\n const footer = buildBodyFooter(token)\n\n const headers = { ...(payload.headers ?? {}) } as Record<string, string | string[] | undefined>\n const existingRefs = headers['references'] ?? headers['References']\n let nextRefs: string\n if (Array.isArray(existingRefs)) {\n nextRefs = existingRefs.includes(refId) ? existingRefs.join(' ') : [...existingRefs, refId].join(' ')\n } else if (typeof existingRefs === 'string' && existingRefs.length > 0) {\n nextRefs = existingRefs.includes(refId) ? existingRefs : `${existingRefs} ${refId}`\n } else {\n nextRefs = refId\n }\n // Normalise to the canonical RFC 5322 header name and drop any duplicate\n // lowercase entry so the MTA sees a single `References` header.\n delete headers['references']\n headers['References'] = nextRefs\n\n let bodyHtml = payload.bodyHtml\n if (typeof bodyHtml === 'string') {\n if (!bodyHtml.includes(`[OM:${token}]`)) {\n const closing = bodyHtml.lastIndexOf('</body>')\n if (closing >= 0) {\n bodyHtml = `${bodyHtml.slice(0, closing)}${footer.html}${bodyHtml.slice(closing)}`\n } else {\n bodyHtml = `${bodyHtml}${footer.html}`\n }\n }\n }\n\n let bodyText = payload.bodyText\n if (typeof bodyText === 'string') {\n if (!bodyText.includes(`[OM:${token}]`)) {\n bodyText = `${bodyText}${footer.plain}`\n }\n }\n\n return {\n ...payload,\n headers,\n ...(bodyHtml !== undefined ? { bodyHtml } : {}),\n ...(bodyText !== undefined ? { bodyText } : {}),\n }\n}\n\n/**\n * Extract token candidates from a `References` / `In-Reply-To` header\n * value (string or string[]) and return the FIRST one that HMAC-verifies.\n * Returns `null` if no valid token is present.\n */\nexport function extractTokenFromHeaders(\n inReplyTo: string | null | undefined,\n references: string[] | string | null | undefined,\n): string | null {\n const haystack: string[] = []\n if (typeof inReplyTo === 'string' && inReplyTo.length > 0) haystack.push(inReplyTo)\n if (Array.isArray(references)) haystack.push(...references)\n else if (typeof references === 'string' && references.length > 0) haystack.push(references)\n for (const candidate of haystack) {\n const matches = candidate.match(new RegExp(TOKEN_REGEX, 'g'))\n if (!matches) continue\n for (const match of matches) {\n if (verifyToken(match)) return match\n }\n }\n return null\n}\n\n/**\n * Idempotent get-or-create: return the existing `ChannelThreadToken` for the\n * given thread, or create + return a new one. Idempotency is enforced by the\n * `channel_thread_tokens_thread_uq` unique constraint on\n * `(tenant_id, message_thread_id)`: a concurrent double-create loses the race\n * with a unique violation, which we catch and resolve by re-selecting the\n * winner \u2014 so callers always converge on exactly one token per thread.\n *\n * Reads via the standard EntityManager (no encryption needed \u2014 the token\n * column itself is the HMAC-signed value, not encrypted at rest).\n *\n * Use cases:\n * - Outbound subscriber: get or create a token before injecting it\n * into the outbound MIME (`applyOutboundThreadingToken`).\n * - Future \"reset\" UI for tenant admins: explicit rotation by deleting\n * the row + calling this helper again.\n */\nexport async function getOrCreateThreadToken(\n em: EntityManager,\n args: {\n tenantId: string\n organizationId: string | null\n messageThreadId: string\n },\n): Promise<{ token: string; created: boolean }> {\n const dscope = { tenantId: args.tenantId, organizationId: args.organizationId }\n const existing = await findOneWithDecryption(\n em,\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (existing) {\n return { token: existing.token, created: false }\n }\n const row = em.create(ChannelThreadToken, {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n token: generateToken(),\n })\n // MikroORM v7 removed `persistAndFlush` \u2014 split into persist + flush.\n em.persist(row)\n try {\n await em.flush()\n return { token: row.token, created: true }\n } catch (err) {\n // A concurrent create for the same (tenant, thread) won the race; the\n // unique constraint rejected ours. Re-select the winner on a clean fork so\n // we never return a half-persisted row or surface a spurious error.\n if (!isUniqueViolation(err)) throw err\n const winner = await findOneWithDecryption(\n em.fork(),\n ChannelThreadToken,\n {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n messageThreadId: args.messageThreadId,\n },\n undefined,\n dscope,\n )\n if (winner) return { token: winner.token, created: false }\n throw err\n }\n}\n\n/**\n * Extract a token candidate from an inbound body (HTML or plain text).\n * Scans for `[OM:om_\u2026]` markers and returns the first that HMAC-verifies.\n */\nexport function extractTokenFromBody(\n bodyHtml: string | null | undefined,\n bodyText: string | null | undefined,\n): string | null {\n const haystacks = [bodyHtml, bodyText].filter(\n (value): value is string => typeof value === 'string' && value.length > 0,\n )\n const pattern = new RegExp(`\\\\[OM:(${TOKEN_REGEX.source})\\\\]`, 'g')\n for (const haystack of haystacks) {\n let match: RegExpExecArray | null\n pattern.lastIndex = 0\n while ((match = pattern.exec(haystack)) !== null) {\n if (verifyToken(match[1])) return match[1]\n }\n }\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { COMMUNICATION_CHANNELS_QUEUES } from "./lib/queue.js";
|
|
3
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
|
+
const logger = createLogger("communication_channels");
|
|
3
5
|
const POLL_TICK_INTERVAL_SECONDS = Math.max(
|
|
4
6
|
10,
|
|
5
7
|
Number.parseInt(process.env.OM_HUB_POLL_SCHEDULER_TICK_SECONDS ?? "60", 10) || 60
|
|
@@ -90,10 +92,7 @@ const setup = {
|
|
|
90
92
|
isEnabled: true
|
|
91
93
|
});
|
|
92
94
|
} catch (error) {
|
|
93
|
-
|
|
94
|
-
"[communication_channels] Failed to register module schedules:",
|
|
95
|
-
error instanceof Error ? error.message : error
|
|
96
|
-
);
|
|
95
|
+
logger.warn("Failed to register module schedules", { err: error });
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
};
|