@open-mercato/core 0.7.0 → 0.7.1-develop.7102.1.b41f7e3e51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/api.js +13 -1
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/communicationChannelsFixtures.js +19 -0
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/modules/api_keys/api/keys/route.js +1 -1
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/backend/api-keys/page.js +3 -1
- package/dist/modules/api_keys/backend/api-keys/page.js.map +2 -2
- package/dist/modules/api_keys/di.js +9 -0
- package/dist/modules/api_keys/di.js.map +7 -0
- package/dist/modules/api_keys/services/principalService.js +31 -0
- package/dist/modules/api_keys/services/principalService.js.map +7 -0
- package/dist/modules/attachments/api/route.js +2 -2
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentLibrary.js +2 -0
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/attachments/di.js +11 -1
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/index.js.map +1 -1
- package/dist/modules/attachments/lib/attachment-service.js +313 -0
- package/dist/modules/attachments/lib/attachment-service.js.map +7 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js +15 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/upload-limits.js +6 -2
- package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +6 -0
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/audit_logs/lib/display-helpers.js +19 -11
- package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
- package/dist/modules/audit_logs/services/actionLogService.js +57 -12
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/auth/acl.js +30 -5
- package/dist/modules/auth/acl.js.map +2 -2
- package/dist/modules/auth/api/feature-check.js +8 -2
- package/dist/modules/auth/api/feature-check.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -1
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/api/reset/validate.js +55 -0
- package/dist/modules/auth/api/reset/validate.js.map +7 -0
- package/dist/modules/auth/api/roles/acl/route.js +28 -33
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/session/refresh.js +25 -1
- package/dist/modules/auth/api/session/refresh.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +96 -51
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +13 -5
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/roles/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/backend/roles/page.js +3 -1
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js +5 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.meta.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js +2 -1
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/auth/commands/acl.js +254 -0
- package/dist/modules/auth/commands/acl.js.map +7 -0
- package/dist/modules/auth/components/AclEditor.js +39 -30
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/data/validators.js +5 -1
- package/dist/modules/auth/data/validators.js.map +2 -2
- package/dist/modules/auth/di.js +35 -9
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/frontend/reset/[token]/page.js +46 -0
- package/dist/modules/auth/frontend/reset/[token]/page.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/auth/lib/userIdFilter.js +16 -0
- package/dist/modules/auth/lib/userIdFilter.js.map +7 -0
- package/dist/modules/auth/services/authService.js +16 -2
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/auth/services/principalService.js +200 -0
- package/dist/modules/auth/services/principalService.js.map +7 -0
- package/dist/modules/auth/services/rbacService.js +67 -24
- package/dist/modules/auth/services/rbacService.js.map +3 -3
- package/dist/modules/auth/services/roleOrganizationScope.js +41 -0
- package/dist/modules/auth/services/roleOrganizationScope.js.map +7 -0
- package/dist/modules/business_rules/backend/logs/page.js +3 -1
- package/dist/modules/business_rules/backend/logs/page.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/page.js +3 -1
- package/dist/modules/business_rules/backend/rules/page.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/page.js +3 -1
- package/dist/modules/business_rules/backend/sets/page.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -9
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-evaluator.js +10 -9
- package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
- package/dist/modules/catalog/ai-agents.js +1 -1
- package/dist/modules/catalog/ai-agents.js.map +1 -1
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +2 -2
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +5 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js +2 -0
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductsDataTable.js +7 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +2 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +9 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +95 -10
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +3 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +14 -1
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/di.js +7 -1
- package/dist/modules/communication_channels/di.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-capabilities.js +4 -1
- package/dist/modules/communication_channels/lib/email-capabilities.js.map +2 -2
- package/dist/modules/communication_channels/lib/outbound-recipient.js +33 -0
- package/dist/modules/communication_channels/lib/outbound-recipient.js.map +7 -0
- package/dist/modules/communication_channels/lib/resolve-channel-type.js +51 -0
- package/dist/modules/communication_channels/lib/resolve-channel-type.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +41 -2
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/configs/lib/system-status.js +9 -0
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +3 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +3 -1
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/services/rateFetchingService.js +6 -1
- package/dist/modules/currencies/services/rateFetchingService.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js +9 -0
- package/dist/modules/customer_accounts/api/admin/domain-mappings.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/services/domainMappingService.js +10 -0
- package/dist/modules/customer_accounts/services/domainMappingService.js.map +2 -2
- package/dist/modules/customers/ai-tools/companies-pack.js +4 -1
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js +13 -2
- package/dist/modules/customers/ai-tools/deal-analyzer-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/deals-pack.js +52 -6
- package/dist/modules/customers/ai-tools/deals-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +4 -1
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +48 -6
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +20 -64
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +17 -94
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js +8 -5
- package/dist/modules/customers/api/dashboard/widgets/customer-todos/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +1 -1
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +8 -25
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/stats/route.js +3 -22
- package/dist/modules/customers/api/deals/[id]/stats/route.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +11 -4
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +2 -1
- package/dist/modules/customers/api/deals/map/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +38 -1
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +4 -4
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +27 -4
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +4 -3
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/context.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/context.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +1 -1
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +11 -10
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/check-phone/route.js +51 -9
- package/dist/modules/customers/api/people/check-phone/route.js.map +2 -2
- package/dist/modules/customers/api/todos/route.js +30 -5
- package/dist/modules/customers/api/todos/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +4 -2
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +6 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +5 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +4 -13
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +85 -14
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js +18 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/toneClasses.js.map +7 -0
- package/dist/modules/customers/backend/customers/people/page.js +4 -2
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js +9 -3
- package/dist/modules/customers/backend/hooks/useEmailDuplicateCheck.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +21 -34
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js.map +1 -1
- package/dist/modules/customers/commands/personCompanyLinks.js +131 -2
- package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js +1 -0
- package/dist/modules/customers/components/CustomerTodosTable.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +7 -6
- package/dist/modules/customers/components/DealsKpiStrip.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +1 -1
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +3 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +40 -5
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +2 -1
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/types.js +1 -1
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +5 -4
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +3 -1
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +19 -7
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +6 -5
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/EntityTagsDialog.js +35 -11
- package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js +3 -1
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js +19 -13
- package/dist/modules/customers/components/detail/hooks/usePersonTasks.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -4
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +3 -3
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +5 -5
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/customers/data/entities.js.map +1 -1
- package/dist/modules/customers/data/validators.js +32 -3
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/events.js +5 -0
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +8 -2
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +3 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/mapItem.js +8 -3
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/participantIdentity.js +9 -0
- package/dist/modules/customers/lib/calendar/participantIdentity.js.map +7 -0
- package/dist/modules/customers/lib/closureStage.js +45 -0
- package/dist/modules/customers/lib/closureStage.js.map +7 -0
- package/dist/modules/customers/lib/dealStatus.js +30 -0
- package/dist/modules/customers/lib/dealStatus.js.map +2 -2
- package/dist/modules/customers/lib/detailReadAccess.js +12 -0
- package/dist/modules/customers/lib/detailReadAccess.js.map +7 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js +1 -0
- package/dist/modules/customers/lib/findPeopleByAddresses.js.map +2 -2
- package/dist/modules/customers/lib/personCompanies.js +100 -2
- package/dist/modules/customers/lib/personCompanies.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +12 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetScope.js +6 -1
- package/dist/modules/dashboards/lib/widgetScope.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/lib/abandoned-run.js +19 -0
- package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +72 -6
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +5 -1
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +5 -1
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/devices/acl.js +9 -1
- package/dist/modules/devices/acl.js.map +2 -2
- package/dist/modules/devices/api/admin/devices/[id]/route.js +5 -32
- package/dist/modules/devices/api/admin/devices/[id]/route.js.map +2 -2
- package/dist/modules/devices/api/admin/devices/route.js +14 -1
- package/dist/modules/devices/api/admin/devices/route.js.map +2 -2
- package/dist/modules/devices/api/deviceList.js +99 -12
- package/dist/modules/devices/api/deviceList.js.map +2 -2
- package/dist/modules/devices/api/deviceSerialization.js +46 -0
- package/dist/modules/devices/api/deviceSerialization.js.map +7 -0
- package/dist/modules/devices/api/openapi.js +3 -1
- package/dist/modules/devices/api/openapi.js.map +2 -2
- package/dist/modules/devices/api/route.js +9 -1
- package/dist/modules/devices/api/route.js.map +2 -2
- package/dist/modules/devices/backend/devices/[id]/page.js +10 -26
- package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/create/page.js +17 -1
- package/dist/modules/devices/backend/devices/create/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/page.js +28 -34
- package/dist/modules/devices/backend/devices/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js +34 -0
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js.map +7 -0
- package/dist/modules/devices/backend/devices/userOptions.js +52 -0
- package/dist/modules/devices/backend/devices/userOptions.js.map +7 -0
- package/dist/modules/devices/setup.js +6 -2
- package/dist/modules/devices/setup.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +2 -1
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +2 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/directory/di.js +9 -0
- package/dist/modules/directory/di.js.map +2 -2
- package/dist/modules/directory/services/organizationHierarchyService.js +29 -0
- package/dist/modules/directory/services/organizationHierarchyService.js.map +7 -0
- package/dist/modules/directory/services/organizationScopeService.js +56 -0
- package/dist/modules/directory/services/organizationScopeService.js.map +7 -0
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +3 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/eudr/api/plots/route.js +5 -1
- package/dist/modules/eudr/api/plots/route.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/plots/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.js +3 -0
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
- package/dist/modules/eudr/commands/statements.js +0 -3
- package/dist/modules/eudr/commands/statements.js.map +2 -2
- package/dist/modules/eudr/data/validators.js +4 -1
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js +1 -0
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/OverridesTable.js +1 -0
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +2 -2
- package/dist/modules/feature_toggles/defaults.json +0 -8
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js +3 -0
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +2 -2
- package/dist/modules/inbox_ops/lib/htmlToPlainText.js +1 -12
- package/dist/modules/inbox_ops/lib/htmlToPlainText.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +6 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +38 -29
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +36 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js +55 -0
- package/dist/modules/integrations/backend/integrations/credential-secret-fields.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +11 -3
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-masking.js +6 -2
- package/dist/modules/integrations/lib/credentials-masking.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +29 -1
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +17 -2
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/api/route.js +17 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +7 -14
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +15 -0
- package/dist/modules/messages/components/inboxFilters.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js +11 -1
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +3 -1
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js +2 -1
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
- package/dist/modules/messages/components/messageListLabels.js +14 -2
- package/dist/modules/messages/components/messageListLabels.js.map +2 -2
- package/dist/modules/messages/data/validators.js +18 -4
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/messages/lib/channel-sender-identity.js +26 -0
- package/dist/modules/messages/lib/channel-sender-identity.js.map +7 -0
- package/dist/modules/messages/lib/composeSourceChannelType.js +41 -0
- package/dist/modules/messages/lib/composeSourceChannelType.js.map +7 -0
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +4 -6
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +3 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +7 -0
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +3 -0
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/page.js +3 -1
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +2 -2
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js +1 -1
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +150 -51
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-entity-policy.js +14 -0
- package/dist/modules/query_index/lib/search-entity-policy.js.map +7 -0
- package/dist/modules/query_index/lib/search-tokens.js +79 -3
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +3 -1
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js +3 -1
- package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +3 -0
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +10 -8
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +79 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/PriceWithCurrency.js +7 -5
- package/dist/modules/sales/components/PriceWithCurrency.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +3 -0
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +51 -22
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +83 -18
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +23 -9
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +8 -6
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +68 -14
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +6 -5
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +5 -4
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +8 -6
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js +54 -0
- package/dist/modules/sales/components/documents/lineItemShipmentLock.js.map +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/sales/data/entities.js +8 -2
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/extension-points.js +1 -0
- package/dist/modules/sales/extension-points.js.map +2 -2
- package/dist/modules/sales/lib/salesChannelsToggleSeed.js +33 -0
- package/dist/modules/sales/lib/salesChannelsToggleSeed.js.map +7 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js +32 -0
- package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js.map +7 -0
- package/dist/modules/sales/setup.js +2 -0
- package/dist/modules/sales/setup.js.map +2 -2
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js +10 -2
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js.map +2 -2
- package/dist/modules/sales/widgets/injection-table.js +4 -7
- package/dist/modules/sales/widgets/injection-table.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js +3 -0
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js +3 -0
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js +3 -0
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/page.js +29 -18
- package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js +3 -0
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/warranty_claims/api/assignees/route.js +3 -3
- package/dist/modules/warranty_claims/api/assignees/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +12 -9
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +3 -0
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +3 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/wms/api/warehouseSearch.js +21 -0
- package/dist/modules/wms/api/warehouseSearch.js.map +7 -0
- package/dist/modules/wms/api/warehouses/route.js +13 -9
- package/dist/modules/wms/api/warehouses/route.js.map +2 -2
- package/dist/modules/wms/api/zones/route.js +15 -2
- package/dist/modules/wms/api/zones/route.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +121 -9
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +3 -0
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/wms/commands/shared.js +9 -1
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js +158 -0
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +76 -111
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +3 -3
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +1 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotsListPage.js +1 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +2 -2
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +27 -20
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +2 -2
- package/dist/modules/wms/components/backend/warehouseFormOptions.js +66 -0
- package/dist/modules/wms/components/backend/warehouseFormOptions.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/page.js +3 -1
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/events/page.js +3 -1
- package/dist/modules/workflows/backend/events/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/page.js +3 -1
- package/dist/modules/workflows/backend/instances/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.js +3 -1
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +4 -0
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/package.json +9 -10
- package/src/helpers/integration/api.ts +33 -1
- package/src/helpers/integration/communicationChannelsFixtures.ts +69 -1
- package/src/modules/api_keys/api/keys/route.ts +3 -1
- package/src/modules/api_keys/backend/api-keys/page.tsx +4 -1
- package/src/modules/api_keys/di.ts +7 -0
- package/src/modules/api_keys/services/principalService.ts +33 -0
- package/src/modules/attachments/AGENTS.md +15 -0
- package/src/modules/attachments/api/route.ts +2 -2
- package/src/modules/attachments/components/AttachmentLibrary.tsx +3 -0
- package/src/modules/attachments/di.ts +16 -0
- package/src/modules/attachments/i18n/de.json +5 -0
- package/src/modules/attachments/i18n/en.json +5 -0
- package/src/modules/attachments/i18n/es.json +5 -0
- package/src/modules/attachments/i18n/ko.json +5 -0
- package/src/modules/attachments/i18n/pl.json +5 -0
- package/src/modules/attachments/index.ts +10 -0
- package/src/modules/attachments/lib/attachment-service.ts +468 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +46 -0
- package/src/modules/attachments/lib/upload-limits.ts +6 -2
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +8 -0
- package/src/modules/audit_logs/lib/display-helpers.tsx +42 -32
- package/src/modules/audit_logs/services/actionLogService.ts +74 -11
- package/src/modules/auth/acl.ts +30 -6
- package/src/modules/auth/api/feature-check.ts +10 -3
- package/src/modules/auth/api/login.ts +9 -1
- package/src/modules/auth/api/reset/validate.ts +57 -0
- package/src/modules/auth/api/roles/acl/route.ts +42 -39
- package/src/modules/auth/api/session/refresh.ts +32 -1
- package/src/modules/auth/api/users/acl/route.ts +177 -53
- package/src/modules/auth/api/users/route.ts +18 -5
- package/src/modules/auth/backend/roles/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/backend/roles/page.tsx +4 -2
- package/src/modules/auth/backend/users/[id]/edit/page.meta.ts +5 -2
- package/src/modules/auth/backend/users/page.tsx +3 -2
- package/src/modules/auth/commands/acl.ts +495 -0
- package/src/modules/auth/components/AclEditor.tsx +63 -33
- package/src/modules/auth/data/entities.ts +4 -1
- package/src/modules/auth/data/validators.ts +5 -0
- package/src/modules/auth/di.ts +46 -16
- package/src/modules/auth/frontend/reset/[token]/page.tsx +74 -0
- package/src/modules/auth/i18n/de.json +389 -0
- package/src/modules/auth/i18n/en.json +389 -0
- package/src/modules/auth/i18n/es.json +389 -0
- package/src/modules/auth/i18n/ko.json +389 -0
- package/src/modules/auth/i18n/pl.json +391 -2
- package/src/modules/auth/lib/grantChecks.ts +6 -0
- package/src/modules/auth/lib/userIdFilter.ts +31 -0
- package/src/modules/auth/services/authService.ts +18 -2
- package/src/modules/auth/services/principalService.ts +266 -0
- package/src/modules/auth/services/rbacService.ts +110 -24
- package/src/modules/auth/services/roleOrganizationScope.ts +70 -0
- package/src/modules/business_rules/backend/logs/page.tsx +4 -1
- package/src/modules/business_rules/backend/rules/page.tsx +4 -1
- package/src/modules/business_rules/backend/sets/page.tsx +4 -1
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -9
- package/src/modules/business_rules/lib/rule-evaluator.ts +10 -9
- package/src/modules/catalog/ai-agents.ts +1 -1
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +2 -2
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +12 -5
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +3 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +7 -0
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +4 -0
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +17 -1
- package/src/modules/communication_channels/api/post/test-seed/route.ts +155 -16
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +4 -0
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +38 -1
- package/src/modules/communication_channels/di.ts +7 -0
- package/src/modules/communication_channels/lib/adapter.ts +14 -0
- package/src/modules/communication_channels/lib/email-capabilities.ts +4 -0
- package/src/modules/communication_channels/lib/outbound-recipient.ts +71 -0
- package/src/modules/communication_channels/lib/resolve-channel-type.ts +99 -0
- package/src/modules/communication_channels/lib/test-seed.ts +64 -4
- package/src/modules/configs/i18n/de.json +2 -0
- package/src/modules/configs/i18n/en.json +2 -0
- package/src/modules/configs/i18n/es.json +2 -0
- package/src/modules/configs/i18n/ko.json +2 -0
- package/src/modules/configs/i18n/pl.json +2 -0
- package/src/modules/configs/lib/system-status.ts +9 -0
- package/src/modules/currencies/AGENTS.md +3 -0
- package/src/modules/currencies/backend/currencies/page.tsx +4 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +4 -1
- package/src/modules/currencies/services/README.md +45 -0
- package/src/modules/currencies/services/rateFetchingService.ts +6 -1
- package/src/modules/customer_accounts/api/admin/domain-mappings.ts +7 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +4 -1
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +4 -1
- package/src/modules/customer_accounts/services/domainMappingService.ts +10 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +4 -1
- package/src/modules/customers/ai-tools/deal-analyzer-pack.ts +17 -2
- package/src/modules/customers/ai-tools/deals-pack.ts +66 -1
- package/src/modules/customers/ai-tools/people-pack.ts +4 -1
- package/src/modules/customers/api/activities/route.ts +45 -0
- package/src/modules/customers/api/companies/[id]/people/route.ts +24 -73
- package/src/modules/customers/api/companies/[id]/route.ts +24 -107
- package/src/modules/customers/api/dashboard/widgets/customer-todos/route.ts +16 -8
- 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/[id]/route.ts +17 -28
- package/src/modules/customers/api/deals/[id]/stats/route.ts +10 -25
- package/src/modules/customers/api/deals/aggregate/route.ts +14 -4
- package/src/modules/customers/api/deals/map/route.ts +1 -0
- package/src/modules/customers/api/deals/route.ts +50 -1
- package/src/modules/customers/api/entity-roles-factory.ts +8 -4
- package/src/modules/customers/api/interactions/route.ts +8 -2
- package/src/modules/customers/api/interactions/tasks/route.ts +24 -1
- package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +12 -4
- package/src/modules/customers/api/people/[id]/companies/context.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 +16 -11
- package/src/modules/customers/api/people/check-phone/route.ts +75 -10
- package/src/modules/customers/api/todos/route.ts +27 -2
- package/src/modules/customers/backend/customers/companies/page.tsx +5 -2
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +7 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +8 -2
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +5 -14
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +109 -27
- package/src/modules/customers/backend/customers/deals/pipeline/components/toneClasses.ts +21 -0
- package/src/modules/customers/backend/customers/people/page.tsx +5 -2
- package/src/modules/customers/backend/hooks/useEmailDuplicateCheck.ts +12 -4
- package/src/modules/customers/commands/deals.ts +34 -59
- package/src/modules/customers/commands/interactions.ts +1 -1
- package/src/modules/customers/commands/personCompanyLinks.ts +224 -8
- package/src/modules/customers/components/CustomerTodosTable.tsx +2 -0
- package/src/modules/customers/components/DealsKpiStrip.tsx +7 -7
- package/src/modules/customers/components/calendar/AgendaList.tsx +3 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +1 -1
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -1
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +1 -1
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -2
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +4 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +61 -5
- package/src/modules/customers/components/calendar/editor/hooks.ts +7 -1
- package/src/modules/customers/components/calendar/types.ts +2 -2
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +5 -4
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +8 -1
- package/src/modules/customers/components/detail/DealForm.tsx +22 -2
- package/src/modules/customers/components/detail/DealsSection.tsx +11 -5
- package/src/modules/customers/components/detail/EntityTagsDialog.tsx +58 -12
- package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +8 -1
- package/src/modules/customers/components/detail/hooks/usePersonTasks.ts +23 -14
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +5 -5
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +5 -5
- package/src/modules/customers/components/detail/types.ts +1 -1
- package/src/modules/customers/components/detail/utils.ts +15 -5
- package/src/modules/customers/data/entities.ts +1 -1
- package/src/modules/customers/data/validators.ts +50 -9
- package/src/modules/customers/events.ts +5 -0
- package/src/modules/customers/i18n/de.json +9 -1
- package/src/modules/customers/i18n/en.json +9 -1
- package/src/modules/customers/i18n/es.json +9 -1
- package/src/modules/customers/i18n/ko.json +9 -1
- package/src/modules/customers/i18n/pl.json +9 -1
- package/src/modules/customers/lib/calendar/conflicts.ts +10 -2
- package/src/modules/customers/lib/calendar/editorPayload.ts +4 -3
- package/src/modules/customers/lib/calendar/mapItem.ts +8 -3
- package/src/modules/customers/lib/calendar/participantIdentity.ts +17 -0
- package/src/modules/customers/lib/closureStage.ts +76 -0
- package/src/modules/customers/lib/dealStatus.ts +42 -0
- package/src/modules/customers/lib/detailReadAccess.ts +35 -0
- package/src/modules/customers/lib/findPeopleByAddresses.ts +8 -6
- package/src/modules/customers/lib/personCompanies.ts +136 -1
- package/src/modules/customers/lib/todoCompatibility.ts +19 -1
- package/src/modules/dashboards/i18n/es.json +4 -4
- package/src/modules/dashboards/i18n/pl.json +7 -7
- package/src/modules/dashboards/lib/widgetScope.ts +6 -0
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +4 -1
- package/src/modules/data_sync/lib/abandoned-run.ts +56 -0
- package/src/modules/data_sync/lib/adapter.ts +36 -0
- package/src/modules/data_sync/lib/sync-engine.ts +113 -5
- package/src/modules/data_sync/workers/sync-export.ts +4 -0
- package/src/modules/data_sync/workers/sync-import.ts +4 -0
- package/src/modules/devices/AGENTS.md +20 -1
- package/src/modules/devices/acl.ts +9 -1
- package/src/modules/devices/api/admin/devices/[id]/route.ts +5 -34
- package/src/modules/devices/api/admin/devices/route.ts +14 -1
- package/src/modules/devices/api/deviceList.ts +135 -11
- package/src/modules/devices/api/deviceSerialization.ts +50 -0
- package/src/modules/devices/api/openapi.ts +9 -1
- package/src/modules/devices/api/route.ts +9 -1
- package/src/modules/devices/backend/devices/[id]/page.tsx +18 -32
- package/src/modules/devices/backend/devices/create/page.tsx +17 -1
- package/src/modules/devices/backend/devices/page.tsx +44 -45
- package/src/modules/devices/backend/devices/useDeviceUserLabels.ts +45 -0
- package/src/modules/devices/backend/devices/userOptions.ts +99 -0
- package/src/modules/devices/i18n/de.json +3 -2
- package/src/modules/devices/i18n/en.json +3 -2
- package/src/modules/devices/i18n/es.json +3 -2
- package/src/modules/devices/i18n/ko.json +3 -2
- package/src/modules/devices/i18n/pl.json +3 -2
- package/src/modules/devices/setup.ts +6 -2
- package/src/modules/directory/backend/directory/organizations/page.tsx +3 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +3 -1
- package/src/modules/directory/di.ts +14 -2
- package/src/modules/directory/services/organizationHierarchyService.ts +34 -0
- package/src/modules/directory/services/organizationScopeService.ts +85 -0
- package/src/modules/directory/utils/organizationScope.ts +4 -15
- package/src/modules/entities/api/records.ts +2 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +4 -1
- package/src/modules/eudr/api/plots/route.ts +4 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/plots/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.tsx +4 -0
- package/src/modules/eudr/backend/eudr/statements/page.tsx +4 -0
- package/src/modules/eudr/commands/statements.ts +0 -3
- package/src/modules/eudr/data/validators.ts +16 -1
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +2 -1
- package/src/modules/feature_toggles/defaults.json +0 -8
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +4 -0
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +4 -0
- package/src/modules/inbox_ops/lib/htmlToPlainText.ts +1 -17
- package/src/modules/integrations/api/[id]/credentials/route.ts +6 -4
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +61 -31
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +44 -9
- package/src/modules/integrations/backend/integrations/credential-secret-fields.ts +84 -0
- package/src/modules/integrations/data/validators.ts +12 -2
- package/src/modules/integrations/i18n/de.json +1 -0
- package/src/modules/integrations/i18n/en.json +1 -0
- package/src/modules/integrations/i18n/es.json +1 -0
- package/src/modules/integrations/i18n/ko.json +1 -0
- package/src/modules/integrations/i18n/pl.json +1 -0
- package/src/modules/integrations/lib/credentials-masking.ts +10 -6
- package/src/modules/messages/api/[id]/route.ts +55 -1
- package/src/modules/messages/api/openapi.ts +20 -0
- package/src/modules/messages/api/route.ts +31 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +9 -16
- package/src/modules/messages/components/inboxFilters.ts +36 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsConversation.ts +14 -1
- package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
- package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +2 -1
- package/src/modules/messages/components/message-detail/types.ts +3 -0
- package/src/modules/messages/components/messageListLabels.ts +52 -3
- package/src/modules/messages/data/validators.ts +43 -4
- package/src/modules/messages/lib/channel-sender-identity.ts +55 -0
- package/src/modules/messages/lib/composeSourceChannelType.ts +95 -0
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +4 -6
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +4 -1
- package/src/modules/payment_gateways/lib/webhook-processor.ts +11 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +4 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +4 -1
- package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +4 -4
- package/src/modules/query_index/lib/engine.ts +249 -61
- package/src/modules/query_index/lib/search-entity-policy.ts +46 -0
- package/src/modules/query_index/lib/search-tokens.ts +135 -3
- package/src/modules/resources/backend/resources/resource-types/page.tsx +4 -1
- package/src/modules/resources/backend/resources/resources/page.tsx +4 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +4 -0
- package/src/modules/sales/backend/sales/channels/page.tsx +4 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +14 -8
- package/src/modules/sales/commands/documents.ts +93 -0
- package/src/modules/sales/components/PriceWithCurrency.tsx +17 -5
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +4 -0
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/ItemsSection.tsx +56 -18
- package/src/modules/sales/components/documents/LineItemDialog.tsx +137 -20
- package/src/modules/sales/components/documents/PaymentsSection.tsx +20 -9
- package/src/modules/sales/components/documents/ReturnsSection.tsx +6 -5
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +106 -14
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +6 -5
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +5 -4
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +6 -5
- package/src/modules/sales/components/documents/lineItemShipmentLock.ts +74 -0
- package/src/modules/sales/components/documents/lineItemUtils.ts +7 -2
- package/src/modules/sales/data/entities.ts +6 -0
- package/src/modules/sales/extension-points.ts +1 -0
- package/src/modules/sales/i18n/de.json +8 -0
- package/src/modules/sales/i18n/en.json +8 -0
- package/src/modules/sales/i18n/es.json +8 -0
- package/src/modules/sales/i18n/ko.json +8 -0
- package/src/modules/sales/i18n/pl.json +8 -0
- package/src/modules/sales/lib/salesChannelsToggleSeed.ts +42 -0
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +60 -0
- package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts +87 -0
- package/src/modules/sales/setup.ts +2 -0
- package/src/modules/sales/widgets/injection/payment-gateway-status-column/widget.ts +9 -1
- package/src/modules/sales/widgets/injection-table.ts +4 -7
- package/src/modules/staff/backend/staff/leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/my-leave-requests/page.tsx +4 -0
- package/src/modules/staff/backend/staff/team-members/page.tsx +6 -0
- package/src/modules/staff/backend/staff/team-roles/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +4 -0
- package/src/modules/staff/backend/staff/teams/page.tsx +4 -0
- package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +4 -0
- package/src/modules/sync_excel/lib/adapters/customers.ts +5 -0
- package/src/modules/warranty_claims/api/assignees/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +12 -9
- package/src/modules/warranty_claims/backend/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +4 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +4 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +4 -0
- package/src/modules/wms/api/warehouseSearch.ts +23 -0
- package/src/modules/wms/api/warehouses/route.ts +13 -9
- package/src/modules/wms/api/zones/route.ts +16 -0
- package/src/modules/wms/commands/configuration.ts +159 -10
- package/src/modules/wms/commands/inventory-actions.ts +3 -0
- package/src/modules/wms/commands/shared.ts +17 -0
- package/src/modules/wms/components/backend/WarehouseEditDialog.tsx +216 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +95 -141
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +2 -0
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +2 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +36 -20
- package/src/modules/wms/components/backend/warehouseFormOptions.ts +72 -0
- package/src/modules/wms/i18n/de.json +2 -0
- package/src/modules/wms/i18n/en.json +2 -0
- package/src/modules/wms/i18n/es.json +2 -0
- package/src/modules/wms/i18n/ko.json +2 -0
- package/src/modules/wms/i18n/pl.json +2 -0
- package/src/modules/workflows/backend/definitions/page.tsx +4 -1
- package/src/modules/workflows/backend/events/page.tsx +4 -1
- package/src/modules/workflows/backend/instances/page.tsx +4 -1
- package/src/modules/workflows/backend/tasks/page.tsx +4 -1
- package/src/modules/workflows/lib/activity-executor.ts +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/commands/ingest-inbound-message.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { resolveContact } from '../lib/contact-resolver'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { matchThread, type ThreadMatch } from '../lib/thread-matcher'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\nimport { normalizedInboundMessageSchema } from '../data/validators'\nimport { resolveCommunicationChannelsSystemUserId } from '../lib/system-user'\nimport { isUniqueViolation } from '../lib/pg-errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'ingest-inbound-message' })\n\nconst ingestInputSchema = z.object({\n channelId: z.string().uuid(),\n providerKey: z.string().min(1),\n channelType: z.string().min(1),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n message: normalizedInboundMessageSchema,\n})\n\nexport type IngestInboundMessageInput = z.infer<typeof ingestInputSchema>\n\nexport type IngestInboundMessageResult = {\n status: 'created' | 'duplicate'\n messageId?: string\n externalConversationId?: string\n externalMessageId?: string\n channelLinkId?: string\n threadMappingId?: string\n contactPersonId?: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = 'communication_channels.message.ingest_inbound'\n\n/**\n * Idempotently ingest a normalized inbound channel message.\n *\n * Steps (per SPEC-045d \u00A76):\n * 1. Dedup by `(channel_id, external_message_id)` \u2014 if a MessageChannelLink already\n * exists for that pair, return `{ status: 'duplicate' }` without side effects.\n * 2. Create or load `ExternalConversation` by `(channel_id, external_conversation_id)`.\n * 3. Create or load `ChannelThreadMapping` (1:1 with ExternalConversation).\n * 4. Resolve CRM contact via adapter + QueryEngine (best-effort).\n * 5. Compose the platform `Message` via `messages.messages.compose` (separate transaction).\n * 6. Create `ExternalMessage` + `MessageChannelLink`.\n * 7. Emit `communication_channels.message.received` (and `.conversation.created` / `.contact.resolved` when applicable).\n *\n * The two-transaction model (compose-message-then-record-link) is acceptable for v1;\n * the link's unique-on-message-id constraint is the safety net against orphans. See\n * the pre-implementation analysis for a discussion of single-transaction alternatives.\n */\nconst ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, IngestInboundMessageResult> = {\n id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = ingestInputSchema.parse(rawInput) as IngestInboundMessageInput\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n // (1) Dedup: short-circuit if we've already processed this provider message.\n // The unique constraint is on `messageId`, not (channel, externalMessageId).\n // We must dedup by joining against ExternalMessage which IS uniquely indexed by\n // (channel_id, external_message_id). Hub-side dedup is the authoritative gate.\n const existingExternal = await findOneWithDecryption(\n em,\n ExternalMessage,\n {\n channelId: input.channelId,\n externalMessageId: input.message.externalMessageId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (existingExternal) {\n return {\n status: 'duplicate',\n externalConversationId: existingExternal.conversationId,\n externalMessageId: existingExternal.id,\n }\n }\n\n // (1b) Spec B \u00A7 Sent-folder dedup.\n //\n // When an outbound message lands in the user's IMAP Sent folder (or\n // when Gmail's \"send and archive\" deposits it in All Mail), the next\n // poll will re-fetch it from INBOX as if it were inbound. Skip it\n // here using the RFC 5322 `Message-ID` header \u2014 we recorded it on the\n // outbound `MessageChannelLink.channelMetadata.messageId` at send time.\n //\n // We dedup ONLY on outbound links (direction='outbound') for the same\n // channel \u2014 that way an inbound copy of someone ELSE's email that\n // happens to share a Message-ID is still ingested normally.\n const incomingMessageId = (() => {\n const fromMeta = (input.message.channelMetadata as Record<string, unknown> | undefined)?.messageId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n return null\n })()\n if (incomingMessageId) {\n // MikroORM v7 dropped the Knex builder in favour of Kysely/raw SQL.\n // We use a positional-placeholder raw query for the JSONB\n // `channel_metadata->>messageId` comparison.\n try {\n const sentFolderHit = await em.getConnection().execute<Array<{ id: string }>>(\n `SELECT link.id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.direction = 'outbound'\n AND link.channel_metadata->>'messageId' = ?\n LIMIT 1`,\n [\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.channelId,\n incomingMessageId,\n ],\n )\n if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {\n return {\n status: 'duplicate',\n externalConversationId: input.message.externalConversationId,\n externalMessageId: input.message.externalMessageId,\n }\n }\n } catch (dedupErr) {\n // Sent-folder dedup is best-effort \u2014 a failure here must not abort\n // ingest (better a possible duplicate than a lost inbound message).\n logger.warn('sent-folder dedup query failed, continuing', { err: dedupErr })\n }\n }\n\n // Channel + adapter lookup (the channel must exist + be active).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw new Error(\n `[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`,\n )\n }\n if (!channel.isActive) {\n throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`)\n }\n\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. ` +\n 'Check that the provider package is enabled in modules.ts.',\n )\n }\n\n // (2) ExternalConversation upsert by (channel_id, externalConversationId).\n const m = input.message\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n let conversationCreated = false\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n subject: m.subject ?? null,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n lastMessageAt: m.timestamp ?? new Date(),\n })\n em.persist(conversation)\n conversationCreated = true\n }\n\n // (3) ChannelThreadMapping upsert (1:1 with ExternalConversation per tenant).\n let mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n\n // Last-activity bump on an existing conversation. Applied AFTER the mapping\n // lookup, immediately before the flush, so the scalar mutation and its flush\n // stay adjacent with no query in between (core flush-ordering rule \u2014 a query\n // between a scalar mutation and `em.flush()` can drop the change under some\n // flush modes / subscriber configurations).\n if (\n !conversationCreated &&\n m.timestamp &&\n (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)\n ) {\n conversation.lastMessageAt = m.timestamp\n }\n // We'll fill `messageThreadId` after composing the platform Message (since the\n // first inbound message becomes the thread root in the messages module).\n await em.flush()\n\n // (3b) Spec B \u2014 layered thread match.\n //\n // Resolve the inbound message to an existing platform thread using\n // (in priority order):\n // 1. Crypto token in References / In-Reply-To header (high confidence)\n // 2. Crypto token in body hidden span or plain-text marker (high)\n // 3. JWZ on Message-Id \u2194 stored `MessageChannelLink.channelMetadata.messageId` (medium)\n // 4. Subject + participants in last 30 days, same channel (low)\n //\n // The matcher returns `null` when nothing hits \u2014 in that case we fall\n // back to the existing `ChannelThreadMapping`-by-conversation-id lookup\n // (which also returns null on first-ever inbound, in which case the\n // compose command opens a new thread).\n const metaForMatcher = (m.channelMetadata ?? {}) as Record<string, unknown>\n let threadMatch: ThreadMatch | null = null\n try {\n threadMatch = await matchThread(\n {\n channelId: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n messageId: extractStringFromMeta(metaForMatcher, 'messageId') ?? m.externalMessageId,\n inReplyTo:\n m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, 'inReplyTo'),\n references: extractStringArrayFromMeta(metaForMatcher, 'references'),\n subject: m.subject ?? '',\n fromAddress:\n extractStringFromMeta(metaForMatcher, 'from') ?? m.senderIdentifier,\n toAddresses: extractStringArrayFromMeta(metaForMatcher, 'to'),\n ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),\n bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,\n bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,\n receivedAt: m.timestamp ?? new Date(),\n },\n { em },\n )\n } catch (matcherErr) {\n // Matcher failure must not block ingest \u2014 fall back to the existing\n // conversation-based thread mapping so the message still lands.\n logger.warn('thread matcher failed, falling back to conversation mapping', { err: matcherErr })\n }\n\n // (4) Contact resolution (best-effort, advisory).\n let contactHint: {\n matchedPersonId?: string | null\n email?: string\n displayName?: string\n } | null = null\n try {\n contactHint = await resolveContact(\n {\n adapter,\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName,\n channelMetadata: m.channelMetadata,\n credentials: {}, // credentials decrypted at the webhook route; resolver doesn't re-fetch\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n },\n { container: ctx.container },\n )\n } catch (contactErr) {\n // Best-effort: contact resolution is advisory and must not abort ingest.\n // Log like the sibling dedup/matcher catches so a misbehaving resolver is\n // visible in operator logs instead of failing silently.\n logger.warn('contact resolution failed, continuing without a CRM match', { err: contactErr })\n contactHint = null\n }\n const matchedPersonId = contactHint?.matchedPersonId ?? null\n if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {\n conversation.contactPersonId = matchedPersonId\n // Flush this scalar mutation before the system-user lookup below queries the\n // same EntityManager. SPEC-018: a query between a scalar mutation and its\n // flush can silently discard the pending UPDATE (mirrors the lastMessageAt\n // bump above).\n await em.flush()\n }\n\n // (5) Compose the platform Message via the messages module command.\n //\n // Sanitize against the `messages` module's validators (max 50_000 char body\n // + non-empty subject) so real-world emails don't get rejected mid-ingest:\n // - HTML emails routinely exceed 50k (Gmail signatures, marketing\n // templates, RFC 5322 multipart). Truncate with a marker rather than\n // drop the whole message \u2014 the full raw body is still in\n // ExternalMessage.rawPayload if needed for forensic / forward use.\n // - Some legitimate messages have no subject (notifications, bounce\n // digests). Substitute a placeholder instead of failing ingest.\n const MAX_COMPOSE_BODY = 50_000\n const TRUNCATE_MARKER =\n '\\n\\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]'\n const rawBody = m.body ?? ''\n const truncatedBody =\n rawBody.length > MAX_COMPOSE_BODY\n ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER\n : rawBody\n const safeSubject = (m.subject ?? '').trim() || '(no subject)'\n\n const composeInput = {\n type: `channel.${input.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.external_conversation',\n sourceEntityId: conversation.id,\n externalEmail: contactHint?.email ?? undefined,\n externalName: contactHint?.displayName ?? m.senderDisplayName,\n recipients: mapping?.assignedUserId\n ? [{ userId: mapping.assignedUserId, type: 'to' as const }]\n : [],\n subject: safeSubject,\n body: truncatedBody,\n bodyFormat: (m.bodyFormat === 'html' ? 'text' : m.bodyFormat) as 'text' | 'markdown',\n priority: 'normal' as const,\n sendViaEmail: false,\n // Spec B: matcher-resolved thread id takes priority over the existing\n // conversation-based mapping. Falls through to `mapping?.messageThreadId`\n // when the matcher returned null (no token / JWZ / subject hit).\n parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,\n isDraft: false,\n // Stable dedup key so a retried ingest (after a transient failure between\n // compose and the ExternalMessage anchor insert) reuses the message\n // composed by the first attempt instead of duplicating it. Mirrors the\n // (channel, externalMessageId) ExternalMessage anchor's natural key.\n idempotencyKey: m.externalMessageId\n ? `cc:${input.channelId}:${m.externalMessageId}`\n : undefined,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: await resolveCommunicationChannelsSystemUserId(\n em,\n input.scope.tenantId,\n mapping?.assignedUserId ?? null,\n ),\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const composeResult = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: passthroughCommandCtx(ctx, input.scope),\n },\n )\n const message = composeResult.result\n if (!message?.id) {\n throw new Error('messages.messages.compose did not return a message id')\n }\n\n // (3 continued) Create or update ChannelThreadMapping now that we have a threadId.\n if (!mapping) {\n mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: message.threadId ?? message.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n externalThreadRef: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(mapping)\n }\n\n // (6) Create ExternalMessage + MessageChannelLink (hub-side records).\n //\n // The PrimaryKey for both uses `defaultRaw: 'gen_random_uuid()'` \u2014 a\n // Postgres-side default that doesn't populate the entity's `id` field\n // until after the INSERT returns. If we let MikroORM generate both, then\n // `em.create(MessageChannelLink, { externalMessageId: externalMessage.id })`\n // reads `undefined` for `externalMessage.id` (it hasn't been flushed yet)\n // and writes NULL to `message_channel_links.external_message_id`,\n // breaking the FK and causing downstream joins to silently return 0 rows.\n //\n // Pre-generating both UUIDs client-side fixes the cross-row reference\n // problem and keeps the single-transaction flush semantics intact.\n const externalMessageRowId = randomUUID()\n const channelLinkRowId = randomUUID()\n const externalMessage = em.create(ExternalMessage, {\n id: externalMessageRowId,\n channelId: input.channelId,\n conversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n direction: 'inbound',\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName ?? null,\n providerTimestamp: m.timestamp,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(externalMessage)\n\n // Spec B: annotate the link with which thread-matcher strategy resolved\n // this message (or `'new-thread'` when matcher returned null and we\n // opened a fresh thread). Surfaced to observability + future UI (\"this\n // thread match is low-confidence \u2014 confirm or move\").\n const matcherAnnotatedMetadata: Record<string, unknown> = {\n ...((m.channelMetadata as Record<string, unknown> | undefined) ?? {}),\n threadMatchStrategy: threadMatch?.matchedBy ?? 'new-thread',\n threadMatchConfidence: threadMatch?.confidence ?? 'low',\n }\n\n const channelLink = em.create(MessageChannelLink, {\n id: channelLinkRowId,\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessageRowId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n deliveryStatus: 'received',\n channelPayload: m.channelPayload,\n channelContentType: m.channelContentType,\n channelMetadata: matcherAnnotatedMetadata,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(channelLink)\n\n try {\n await em.flush()\n } catch (flushErr) {\n // Concurrency guard: the pre-check at (1) is not atomic with this insert,\n // so a poll re-fetch racing a push notification (or two push deliveries)\n // can both pass the check and reach here. The `(channel_id,\n // external_message_id)` unique index rejects the loser with a 23505. Treat\n // that as a duplicate \u2014 returning here (instead of throwing) prevents the\n // message from being dead-lettered and retried forever. The winning job\n // already recorded the message + link.\n if (isUniqueViolation(flushErr)) {\n return {\n status: 'duplicate',\n externalConversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n }\n }\n throw flushErr\n }\n\n // (7) Emit events \u2014 order matters for downstream subscribers.\n if (conversationCreated) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.created',\n {\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n if (matchedPersonId) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.contact.resolved',\n {\n conversationId: conversation.id,\n contactPersonId: matchedPersonId,\n providerKey: input.providerKey,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n messageId: message.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'created',\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n threadMappingId: mapping.id,\n contactPersonId: matchedPersonId,\n }\n },\n}\n\n\n/**\n * Build a runtime context for the nested `messages.messages.compose` call.\n *\n * The compose command expects a `CommandRuntimeContext`. For inbound webhook\n * processing there is no platform user; we pass `auth: null` and use the tenant\n * scope from our input.\n */\nfunction passthroughCommandCtx(\n parent: CommandRuntimeContext,\n scope: IngestInboundMessageInput['scope'],\n): CommandRuntimeContext {\n return {\n container: parent.container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n}\n\n/**\n * Pull a string value from the provider's `channelMetadata` map. Returns\n * `null` (not `undefined`) when the key is absent or the value isn't a\n * string \u2014 keeps the matcher's input shape predictable.\n */\nfunction extractStringFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string | null {\n const value = meta[key]\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * Pull a string[] value from the provider's `channelMetadata` map.\n * Filters out non-string entries defensively. Returns an empty array\n * when the key is absent or the value isn't an array.\n */\nfunction extractStringArrayFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string[] {\n const value = meta[key]\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === 'string')\n}\n\nregisterCommand(ingestInboundMessageCommand)\n\nexport default ingestInboundMessageCommand\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandBus, CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { htmlToPlainText } from '@open-mercato/shared/lib/html/htmlToPlainText'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport { resolveContact } from '../lib/contact-resolver'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { matchThread, type ThreadMatch } from '../lib/thread-matcher'\nimport {\n ChannelThreadMapping,\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n} from '../data/entities'\nimport { normalizedInboundMessageSchema } from '../data/validators'\nimport { resolveCommunicationChannelsSystemUserId } from '../lib/system-user'\nimport { isUniqueViolation } from '../lib/pg-errors'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'ingest-inbound-message' })\n\nconst ingestInputSchema = z.object({\n channelId: z.string().uuid(),\n providerKey: z.string().min(1),\n channelType: z.string().min(1),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n message: normalizedInboundMessageSchema,\n})\n\nexport type IngestInboundMessageInput = z.infer<typeof ingestInputSchema>\n\nexport type IngestInboundMessageResult = {\n status: 'created' | 'duplicate'\n messageId?: string\n externalConversationId?: string\n externalMessageId?: string\n channelLinkId?: string\n threadMappingId?: string\n contactPersonId?: string | null\n}\n\nexport const COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID = 'communication_channels.message.ingest_inbound'\n\n/**\n * Idempotently ingest a normalized inbound channel message.\n *\n * Steps (per SPEC-045d \u00A76):\n * 1. Dedup by `(channel_id, external_message_id)` \u2014 if a MessageChannelLink already\n * exists for that pair, return `{ status: 'duplicate' }` without side effects.\n * 2. Create or load `ExternalConversation` by `(channel_id, external_conversation_id)`.\n * 3. Create or load `ChannelThreadMapping` (1:1 with ExternalConversation).\n * 4. Resolve CRM contact via adapter + QueryEngine (best-effort).\n * 5. Compose the platform `Message` via `messages.messages.compose` (separate transaction).\n * 6. Create `ExternalMessage` + `MessageChannelLink`.\n * 7. Emit `communication_channels.message.received` (and `.conversation.created` / `.contact.resolved` when applicable).\n *\n * The two-transaction model (compose-message-then-record-link) is acceptable for v1;\n * the link's unique-on-message-id constraint is the safety net against orphans. See\n * the pre-implementation analysis for a discussion of single-transaction alternatives.\n */\nconst ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, IngestInboundMessageResult> = {\n id: COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = ingestInputSchema.parse(rawInput) as IngestInboundMessageInput\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n // (1) Dedup: short-circuit if we've already processed this provider message.\n // The unique constraint is on `messageId`, not (channel, externalMessageId).\n // We must dedup by joining against ExternalMessage which IS uniquely indexed by\n // (channel_id, external_message_id). Hub-side dedup is the authoritative gate.\n const existingExternal = await findOneWithDecryption(\n em,\n ExternalMessage,\n {\n channelId: input.channelId,\n externalMessageId: input.message.externalMessageId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n if (existingExternal) {\n return {\n status: 'duplicate',\n externalConversationId: existingExternal.conversationId,\n externalMessageId: existingExternal.id,\n }\n }\n\n // (1b) Spec B \u00A7 Sent-folder dedup.\n //\n // When an outbound message lands in the user's IMAP Sent folder (or\n // when Gmail's \"send and archive\" deposits it in All Mail), the next\n // poll will re-fetch it from INBOX as if it were inbound. Skip it\n // here using the RFC 5322 `Message-ID` header \u2014 we recorded it on the\n // outbound `MessageChannelLink.channelMetadata.messageId` at send time.\n //\n // We dedup ONLY on outbound links (direction='outbound') for the same\n // channel \u2014 that way an inbound copy of someone ELSE's email that\n // happens to share a Message-ID is still ingested normally.\n const incomingMessageId = (() => {\n const fromMeta = (input.message.channelMetadata as Record<string, unknown> | undefined)?.messageId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n return null\n })()\n if (incomingMessageId) {\n // MikroORM v7 dropped the Knex builder in favour of Kysely/raw SQL.\n // We use a positional-placeholder raw query for the JSONB\n // `channel_metadata->>messageId` comparison.\n try {\n const sentFolderHit = await em.getConnection().execute<Array<{ id: string }>>(\n `SELECT link.id FROM message_channel_links AS link\n INNER JOIN external_conversations AS conv\n ON conv.id = link.external_conversation_id\n WHERE link.tenant_id = ?\n AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)\n AND conv.tenant_id = ?\n AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)\n AND conv.channel_id = ?\n AND link.direction = 'outbound'\n AND link.channel_metadata->>'messageId' = ?\n LIMIT 1`,\n [\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.scope.tenantId,\n input.scope.organizationId ?? null,\n input.scope.organizationId ?? null,\n input.channelId,\n incomingMessageId,\n ],\n )\n if (Array.isArray(sentFolderHit) && sentFolderHit.length > 0) {\n return {\n status: 'duplicate',\n externalConversationId: input.message.externalConversationId,\n externalMessageId: input.message.externalMessageId,\n }\n }\n } catch (dedupErr) {\n // Sent-folder dedup is best-effort \u2014 a failure here must not abort\n // ingest (better a possible duplicate than a lost inbound message).\n logger.warn('sent-folder dedup query failed, continuing', { err: dedupErr })\n }\n }\n\n // Channel + adapter lookup (the channel must exist + be active).\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n throw new Error(\n `[internal] Channel ${input.channelId} not found for tenant ${input.scope.tenantId} (or has been deleted)`,\n )\n }\n if (!channel.isActive) {\n throw new Error(`[internal] Channel ${input.channelId} is inactive; refusing to ingest`)\n }\n\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n throw new Error(\n `[internal] No ChannelAdapter registered for providerKey '${input.providerKey}'. ` +\n 'Check that the provider package is enabled in modules.ts.',\n )\n }\n\n // (2) ExternalConversation upsert by (channel_id, externalConversationId).\n const m = input.message\n let conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n let conversationCreated = false\n if (!conversation) {\n conversation = em.create(ExternalConversation, {\n channelId: input.channelId,\n externalConversationId: m.externalConversationId,\n subject: m.subject ?? null,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n lastMessageAt: m.timestamp ?? new Date(),\n })\n em.persist(conversation)\n conversationCreated = true\n }\n\n // (3) ChannelThreadMapping upsert (1:1 with ExternalConversation per tenant).\n let mapping = await findOneWithDecryption(\n em,\n ChannelThreadMapping,\n {\n externalConversationId: conversation.id,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n undefined,\n dscope,\n )\n\n // Last-activity bump on an existing conversation. Applied AFTER the mapping\n // lookup, immediately before the flush, so the scalar mutation and its flush\n // stay adjacent with no query in between (core flush-ordering rule \u2014 a query\n // between a scalar mutation and `em.flush()` can drop the change under some\n // flush modes / subscriber configurations).\n if (\n !conversationCreated &&\n m.timestamp &&\n (!conversation.lastMessageAt || m.timestamp > conversation.lastMessageAt)\n ) {\n conversation.lastMessageAt = m.timestamp\n }\n // We'll fill `messageThreadId` after composing the platform Message (since the\n // first inbound message becomes the thread root in the messages module).\n await em.flush()\n\n // (3b) Spec B \u2014 layered thread match.\n //\n // Resolve the inbound message to an existing platform thread using\n // (in priority order):\n // 1. Crypto token in References / In-Reply-To header (high confidence)\n // 2. Crypto token in body hidden span or plain-text marker (high)\n // 3. JWZ on Message-Id \u2194 stored `MessageChannelLink.channelMetadata.messageId` (medium)\n // 4. Subject + participants in last 30 days, same channel (low)\n //\n // The matcher returns `null` when nothing hits \u2014 in that case we fall\n // back to the existing `ChannelThreadMapping`-by-conversation-id lookup\n // (which also returns null on first-ever inbound, in which case the\n // compose command opens a new thread).\n const metaForMatcher = (m.channelMetadata ?? {}) as Record<string, unknown>\n let threadMatch: ThreadMatch | null = null\n try {\n threadMatch = await matchThread(\n {\n channelId: input.channelId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n messageId: extractStringFromMeta(metaForMatcher, 'messageId') ?? m.externalMessageId,\n inReplyTo:\n m.replyToExternalId ?? extractStringFromMeta(metaForMatcher, 'inReplyTo'),\n references: extractStringArrayFromMeta(metaForMatcher, 'references'),\n subject: m.subject ?? '',\n fromAddress:\n extractStringFromMeta(metaForMatcher, 'from') ?? m.senderIdentifier,\n toAddresses: extractStringArrayFromMeta(metaForMatcher, 'to'),\n ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),\n bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,\n bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,\n receivedAt: m.timestamp ?? new Date(),\n },\n { em },\n )\n } catch (matcherErr) {\n // Matcher failure must not block ingest \u2014 fall back to the existing\n // conversation-based thread mapping so the message still lands.\n logger.warn('thread matcher failed, falling back to conversation mapping', { err: matcherErr })\n }\n\n // (4) Contact resolution (best-effort, advisory).\n let contactHint: {\n matchedPersonId?: string | null\n email?: string\n displayName?: string\n } | null = null\n try {\n contactHint = await resolveContact(\n {\n adapter,\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName,\n channelMetadata: m.channelMetadata,\n credentials: {}, // credentials decrypted at the webhook route; resolver doesn't re-fetch\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n },\n { container: ctx.container },\n )\n } catch (contactErr) {\n // Best-effort: contact resolution is advisory and must not abort ingest.\n // Log like the sibling dedup/matcher catches so a misbehaving resolver is\n // visible in operator logs instead of failing silently.\n logger.warn('contact resolution failed, continuing without a CRM match', { err: contactErr })\n contactHint = null\n }\n const matchedPersonId = contactHint?.matchedPersonId ?? null\n if (matchedPersonId && conversation.contactPersonId !== matchedPersonId) {\n conversation.contactPersonId = matchedPersonId\n // Flush this scalar mutation before the system-user lookup below queries the\n // same EntityManager. SPEC-018: a query between a scalar mutation and its\n // flush can silently discard the pending UPDATE (mirrors the lastMessageAt\n // bump above).\n await em.flush()\n }\n\n // (5) Compose the platform Message via the messages module command.\n //\n // Sanitize against the `messages` module's validators (max 50_000 char body\n // + non-empty subject) so real-world emails don't get rejected mid-ingest:\n // - HTML emails routinely exceed 50k (Gmail signatures, marketing\n // templates, RFC 5322 multipart). Truncate with a marker rather than\n // drop the whole message \u2014 the full raw body is still in\n // ExternalMessage.rawPayload if needed for forensic / forward use.\n // - Some legitimate messages have no subject (notifications, bounce\n // digests). Substitute a placeholder instead of failing ingest.\n // - HTML bodies land in the platform body as plain text rather than being\n // merely relabelled as `text`: `MarkdownContent` renders a `text` body\n // verbatim, so unconverted markup shows up as literal doctype, <style>\n // blocks and tags in the inbox preview and the detail body. The source\n // markup is untouched in `channelPayload.html`, which `data/enrichers.ts`\n // sanitizes into `_channelPayload.sanitizedHtml` for the channel-payload\n // renderer widget mounted at `detail:messages:message:body:after`.\n // - The plain-text alternative wins over conversion when the sender's\n // client supplied one (`multipart/alternative`), because that part is\n // what a human actually wrote; `html-to-text` output \u2014 inlined link\n // URLs, flattened tables, synthesized list markers \u2014 is the fallback for\n // HTML-only senders. `lib/email-mime.ts` preferred `bodyHtml` when\n // picking `body`, but preserved the text part at `channelPayload.text`.\n const MAX_COMPOSE_BODY = 50_000\n // Bound the synchronous parse: inbound mail is untrusted and the 5MB body\n // ceiling from `lib/email-capabilities.ts` would otherwise block the ingest\n // worker on a single oversized document. This cap counts markup bytes while\n // MAX_COMPOSE_BODY counts text characters, so it can bite first \u2014 an\n // ordinary marketing template or a long quoted thread is easily over 512KB\n // of markup and still well under 50k characters of text. It therefore\n // carries the same truncation marker, rather than ending mid-sentence with\n // nothing to tell the reader the rest exists.\n const MAX_HTML_PARSE_INPUT = 512 * 1024\n const TRUNCATE_MARKER =\n '\\n\\n[\u2026message truncated by Open Mercato \u2014 full body preserved in ExternalMessage.rawPayload]'\n const sourceBody = m.body ?? ''\n const plainAlternative =\n typeof m.channelPayload?.text === 'string' ? m.channelPayload.text.trim() : ''\n const convertHtmlBody = () => {\n const converted = htmlToPlainText(sourceBody.slice(0, MAX_HTML_PARSE_INPUT))\n return sourceBody.length > MAX_HTML_PARSE_INPUT ? converted + TRUNCATE_MARKER : converted\n }\n const rawBody = m.bodyFormat === 'html'\n ? (plainAlternative || convertHtmlBody())\n : sourceBody\n const truncatedBody =\n rawBody.length > MAX_COMPOSE_BODY\n ? rawBody.slice(0, MAX_COMPOSE_BODY - TRUNCATE_MARKER.length) + TRUNCATE_MARKER\n : rawBody\n const safeSubject = (m.subject ?? '').trim() || '(no subject)'\n\n const composeInput = {\n type: `channel.${input.providerKey}`,\n visibility: 'public' as const,\n sourceEntityType: 'communication_channels.external_conversation',\n sourceEntityId: conversation.id,\n externalEmail: contactHint?.email ?? undefined,\n externalName: contactHint?.displayName ?? m.senderDisplayName,\n // #4975: tells the hub which identity contract applies to this sender.\n // Email-typed channels keep the mandatory `externalEmail`; providers whose\n // senders have no address (Discord, Slack, SMS\u2026) are validated without it.\n // The messages validator fails closed on any type it does not recognize.\n sourceChannelType: input.channelType,\n recipients: mapping?.assignedUserId\n ? [{ userId: mapping.assignedUserId, type: 'to' as const }]\n : [],\n subject: safeSubject,\n body: truncatedBody,\n bodyFormat: (m.bodyFormat === 'html' ? 'text' : m.bodyFormat) as 'text' | 'markdown',\n priority: 'normal' as const,\n sendViaEmail: false,\n // Spec B: matcher-resolved thread id takes priority over the existing\n // conversation-based mapping. Falls through to `mapping?.messageThreadId`\n // when the matcher returned null (no token / JWZ / subject hit).\n parentMessageId: threadMatch?.messageThreadId ?? mapping?.messageThreadId,\n isDraft: false,\n // Stable dedup key so a retried ingest (after a transient failure between\n // compose and the ExternalMessage anchor insert) reuses the message\n // composed by the first attempt instead of duplicating it. Mirrors the\n // (channel, externalMessageId) ExternalMessage anchor's natural key.\n idempotencyKey: m.externalMessageId\n ? `cc:${input.channelId}:${m.externalMessageId}`\n : undefined,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId,\n userId: await resolveCommunicationChannelsSystemUserId(\n em,\n input.scope.tenantId,\n mapping?.assignedUserId ?? null,\n ),\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const composeResult = await commandBus.execute<typeof composeInput, { id: string; threadId: string | null }>(\n 'messages.messages.compose',\n {\n input: composeInput,\n ctx: passthroughCommandCtx(ctx, input.scope),\n },\n )\n const message = composeResult.result\n if (!message?.id) {\n throw new Error('messages.messages.compose did not return a message id')\n }\n\n // (3 continued) Create or update ChannelThreadMapping now that we have a threadId.\n if (!mapping) {\n mapping = em.create(ChannelThreadMapping, {\n externalConversationId: conversation.id,\n messageThreadId: message.threadId ?? message.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n externalThreadRef: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(mapping)\n }\n\n // (6) Create ExternalMessage + MessageChannelLink (hub-side records).\n //\n // The PrimaryKey for both uses `defaultRaw: 'gen_random_uuid()'` \u2014 a\n // Postgres-side default that doesn't populate the entity's `id` field\n // until after the INSERT returns. If we let MikroORM generate both, then\n // `em.create(MessageChannelLink, { externalMessageId: externalMessage.id })`\n // reads `undefined` for `externalMessage.id` (it hasn't been flushed yet)\n // and writes NULL to `message_channel_links.external_message_id`,\n // breaking the FK and causing downstream joins to silently return 0 rows.\n //\n // Pre-generating both UUIDs client-side fixes the cross-row reference\n // problem and keeps the single-transaction flush semantics intact.\n const externalMessageRowId = randomUUID()\n const channelLinkRowId = randomUUID()\n const externalMessage = em.create(ExternalMessage, {\n id: externalMessageRowId,\n channelId: input.channelId,\n conversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n direction: 'inbound',\n senderIdentifier: m.senderIdentifier,\n senderDisplayName: m.senderDisplayName ?? null,\n providerTimestamp: m.timestamp,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(externalMessage)\n\n // Spec B: annotate the link with which thread-matcher strategy resolved\n // this message (or `'new-thread'` when matcher returned null and we\n // opened a fresh thread). Surfaced to observability + future UI (\"this\n // thread match is low-confidence \u2014 confirm or move\").\n const matcherAnnotatedMetadata: Record<string, unknown> = {\n ...((m.channelMetadata as Record<string, unknown> | undefined) ?? {}),\n threadMatchStrategy: threadMatch?.matchedBy ?? 'new-thread',\n threadMatchConfidence: threadMatch?.confidence ?? 'low',\n }\n\n const channelLink = em.create(MessageChannelLink, {\n id: channelLinkRowId,\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessageRowId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n deliveryStatus: 'received',\n channelPayload: m.channelPayload,\n channelContentType: m.channelContentType,\n channelMetadata: matcherAnnotatedMetadata,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n })\n em.persist(channelLink)\n\n try {\n await em.flush()\n } catch (flushErr) {\n // Concurrency guard: the pre-check at (1) is not atomic with this insert,\n // so a poll re-fetch racing a push notification (or two push deliveries)\n // can both pass the check and reach here. The `(channel_id,\n // external_message_id)` unique index rejects the loser with a 23505. Treat\n // that as a duplicate \u2014 returning here (instead of throwing) prevents the\n // message from being dead-lettered and retried forever. The winning job\n // already recorded the message + link.\n if (isUniqueViolation(flushErr)) {\n return {\n status: 'duplicate',\n externalConversationId: conversation.id,\n externalMessageId: m.externalMessageId,\n }\n }\n throw flushErr\n }\n\n // (7) Emit events \u2014 order matters for downstream subscribers.\n if (conversationCreated) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.conversation.created',\n {\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n externalConversationId: m.externalConversationId,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n if (matchedPersonId) {\n await emitCommunicationChannelsEvent(\n 'communication_channels.contact.resolved',\n {\n conversationId: conversation.id,\n contactPersonId: matchedPersonId,\n providerKey: input.providerKey,\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n }\n await emitCommunicationChannelsEvent(\n 'communication_channels.message.received',\n {\n messageId: message.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n conversationId: conversation.id,\n channelId: input.channelId,\n providerKey: input.providerKey,\n channelType: input.channelType,\n direction: 'inbound',\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'created',\n messageId: message.id,\n externalConversationId: conversation.id,\n externalMessageId: externalMessage.id,\n channelLinkId: channelLink.id,\n threadMappingId: mapping.id,\n contactPersonId: matchedPersonId,\n }\n },\n}\n\n\n/**\n * Build a runtime context for the nested `messages.messages.compose` call.\n *\n * The compose command expects a `CommandRuntimeContext`. For inbound webhook\n * processing there is no platform user; we pass `auth: null` and use the tenant\n * scope from our input.\n */\nfunction passthroughCommandCtx(\n parent: CommandRuntimeContext,\n scope: IngestInboundMessageInput['scope'],\n): CommandRuntimeContext {\n return {\n container: parent.container,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n}\n\n/**\n * Pull a string value from the provider's `channelMetadata` map. Returns\n * `null` (not `undefined`) when the key is absent or the value isn't a\n * string \u2014 keeps the matcher's input shape predictable.\n */\nfunction extractStringFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string | null {\n const value = meta[key]\n if (typeof value === 'string' && value.length > 0) return value\n return null\n}\n\n/**\n * Pull a string[] value from the provider's `channelMetadata` map.\n * Filters out non-string entries defensively. Returns an empty array\n * when the key is absent or the value isn't an array.\n */\nfunction extractStringArrayFromMeta(\n meta: Record<string, unknown>,\n key: string,\n): string[] {\n const value = meta[key]\n if (!Array.isArray(value)) return []\n return value.filter((item): item is string => typeof item === 'string')\n}\n\nregisterCommand(ingestInboundMessageCommand)\n\nexport default ingestInboundMessageCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { getChannelAdapterRegistry } from "./lib/adapter-registry-singleton.js";
|
|
11
11
|
import { ensureTestSeedAdapterRegistered } from "./lib/test-seed.js";
|
|
12
12
|
import { sendAsUser } from "./lib/send-as-user.js";
|
|
13
|
+
import { resolveChannelTypeSafely } from "./lib/resolve-channel-type.js";
|
|
13
14
|
function register(container) {
|
|
14
15
|
ensureTestSeedAdapterRegistered();
|
|
15
16
|
container.register({
|
|
@@ -26,7 +27,12 @@ function register(container) {
|
|
|
26
27
|
channelAdapterRegistry: asValue(getChannelAdapterRegistry()),
|
|
27
28
|
// In-process send-as-user facade. Cross-module callers (e.g. the customers
|
|
28
29
|
// compose route) resolve this instead of making an HTTP self-call.
|
|
29
|
-
communicationChannelsSendAsUser: asValue(sendAsUser)
|
|
30
|
+
communicationChannelsSendAsUser: asValue(sendAsUser),
|
|
31
|
+
// Cross-module channel-type lookup. The messages compose route resolves this
|
|
32
|
+
// to decide whether an external correspondent must carry an email address
|
|
33
|
+
// (#4975); absent module or failed lookup reads as "unknown", which the
|
|
34
|
+
// messages validator handles fail-closed.
|
|
35
|
+
communicationChannelsResolveChannelType: asValue(resolveChannelTypeSafely)
|
|
30
36
|
});
|
|
31
37
|
}
|
|
32
38
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/communication_channels/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n ChannelThreadMapping,\n MessageReaction,\n} from './data/entities'\nimport { getChannelAdapterRegistry } from './lib/adapter-registry-singleton'\nimport { ensureTestSeedAdapterRegistered } from './lib/test-seed'\nimport { sendAsUser } from './lib/send-as-user'\n\nexport function register(container: AppContainer) {\n // Test-only: register the network-free stub channel adapter when\n // `OM_ENABLE_TEST_CHANNEL_SEEDING` is set (no-op in production). Lets the\n // integration harness connect a channel + complete the outbound send chain.\n // See lib/test-seed.ts.\n ensureTestSeedAdapterRegistered()\n\n container.register({\n // Entity class registrations (for EntityManager lookups by string)\n CommunicationChannel: asValue(CommunicationChannel),\n ExternalConversation: asValue(ExternalConversation),\n ExternalMessage: asValue(ExternalMessage),\n MessageChannelLink: asValue(MessageChannelLink),\n ChannelThreadMapping: asValue(ChannelThreadMapping),\n MessageReaction: asValue(MessageReaction),\n\n // Channel adapter registry \u2014 process-wide singleton backed by globalThis so\n // the auth-less webhook route resolves the same registry as DI consumers.\n // See lib/adapter-registry-singleton.ts.\n channelAdapterRegistry: asValue(getChannelAdapterRegistry()),\n\n // In-process send-as-user facade. Cross-module callers (e.g. the customers\n // compose route) resolve this instead of making an HTTP self-call.\n communicationChannelsSendAsUser: asValue(sendAsUser),\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uCAAuC;AAChD,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n CommunicationChannel,\n ExternalConversation,\n ExternalMessage,\n MessageChannelLink,\n ChannelThreadMapping,\n MessageReaction,\n} from './data/entities'\nimport { getChannelAdapterRegistry } from './lib/adapter-registry-singleton'\nimport { ensureTestSeedAdapterRegistered } from './lib/test-seed'\nimport { sendAsUser } from './lib/send-as-user'\nimport { resolveChannelTypeSafely } from './lib/resolve-channel-type'\n\nexport function register(container: AppContainer) {\n // Test-only: register the network-free stub channel adapter when\n // `OM_ENABLE_TEST_CHANNEL_SEEDING` is set (no-op in production). Lets the\n // integration harness connect a channel + complete the outbound send chain.\n // See lib/test-seed.ts.\n ensureTestSeedAdapterRegistered()\n\n container.register({\n // Entity class registrations (for EntityManager lookups by string)\n CommunicationChannel: asValue(CommunicationChannel),\n ExternalConversation: asValue(ExternalConversation),\n ExternalMessage: asValue(ExternalMessage),\n MessageChannelLink: asValue(MessageChannelLink),\n ChannelThreadMapping: asValue(ChannelThreadMapping),\n MessageReaction: asValue(MessageReaction),\n\n // Channel adapter registry \u2014 process-wide singleton backed by globalThis so\n // the auth-less webhook route resolves the same registry as DI consumers.\n // See lib/adapter-registry-singleton.ts.\n channelAdapterRegistry: asValue(getChannelAdapterRegistry()),\n\n // In-process send-as-user facade. Cross-module callers (e.g. the customers\n // compose route) resolve this instead of making an HTTP self-call.\n communicationChannelsSendAsUser: asValue(sendAsUser),\n\n // Cross-module channel-type lookup. The messages compose route resolves this\n // to decide whether an external correspondent must carry an email address\n // (#4975); absent module or failed lookup reads as \"unknown\", which the\n // messages validator handles fail-closed.\n communicationChannelsResolveChannelType: asValue(resolveChannelTypeSafely),\n })\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iCAAiC;AAC1C,SAAS,uCAAuC;AAChD,SAAS,kBAAkB;AAC3B,SAAS,gCAAgC;AAElC,SAAS,SAAS,WAAyB;AAKhD,kCAAgC;AAEhC,YAAU,SAAS;AAAA;AAAA,IAEjB,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,iBAAiB,QAAQ,eAAe;AAAA,IACxC,oBAAoB,QAAQ,kBAAkB;AAAA,IAC9C,sBAAsB,QAAQ,oBAAoB;AAAA,IAClD,iBAAiB,QAAQ,eAAe;AAAA;AAAA;AAAA;AAAA,IAKxC,wBAAwB,QAAQ,0BAA0B,CAAC;AAAA;AAAA;AAAA,IAI3D,iCAAiC,QAAQ,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,IAMnD,yCAAyC,QAAQ,wBAAwB;AAAA,EAC3E,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -38,7 +38,10 @@ const baseEmailCapabilities = {
|
|
|
38
38
|
supportedBodyFormats: ["text", "html"],
|
|
39
39
|
maxBodyLength: 5e6,
|
|
40
40
|
// Polling (real-time push deferred to v2 for all email providers)
|
|
41
|
-
realtimePush: false
|
|
41
|
+
realtimePush: false,
|
|
42
|
+
// Stated rather than left to the default so the email baseline reads as an
|
|
43
|
+
// explicit choice: outbound recipients here are addresses, not provider ids.
|
|
44
|
+
recipientFormat: "email"
|
|
42
45
|
};
|
|
43
46
|
export {
|
|
44
47
|
EMAIL_MAX_ATTACHMENT_BYTES,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/email-capabilities.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ChannelCapabilities } from './adapter'\n\n/**\n * Shared default attachment ceiling for email providers (Gmail allows\n * more, but larger uploads need resumable/upload-session APIs we don't use, so\n * all providers cap at the same conservative value).\n */\nexport const EMAIL_MAX_ATTACHMENT_BYTES = 25_000_000\n\n/**\n * Baseline capability profile shared by every email channel provider. Providers\n * spread this and override only what genuinely differs (e.g. `deleteMessage`).\n *\n * `fileSharing: false` until a provider's `convertOutbound` stitches attachment\n * bytes into the sent message \u2014 advertising `true` would silently drop bytes.\n */\nexport const baseEmailCapabilities: ChannelCapabilities = {\n // Core\n threading: true,\n richText: true,\n fileSharing: false,\n maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,\n supportedMimeTypes: [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'application/zip',\n 'application/octet-stream',\n 'text/plain',\n 'text/html',\n 'text/csv',\n ],\n readReceipts: false,\n deliveryReceipts: false,\n typingIndicators: false,\n\n // Extended\n reactions: false,\n multiReactionPerUser: false,\n editMessage: false,\n deleteMessage: false,\n presence: false,\n richBlocks: false,\n interactiveComponents: false,\n inlineImages: true,\n conversationHistory: true,\n contactCards: false,\n locationSharing: false,\n voiceNotes: false,\n stickers: false,\n\n // Body formats\n supportedBodyFormats: ['text', 'html'],\n maxBodyLength: 5_000_000,\n\n // Polling (real-time push deferred to v2 for all email providers)\n realtimePush: false,\n}\n"],
|
|
5
|
-
"mappings": "AAOO,MAAM,6BAA6B;AASnC,MAAM,wBAA6C;AAAA;AAAA,EAExD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,aAAa;AAAA,EACb,aAAa;AAAA,EACb,oBAAoB;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,kBAAkB;AAAA;AAAA,EAGlB,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,uBAAuB;AAAA,EACvB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,sBAAsB,CAAC,QAAQ,MAAM;AAAA,EACrC,eAAe;AAAA;AAAA,EAGf,cAAc;
|
|
4
|
+
"sourcesContent": ["import type { ChannelCapabilities } from './adapter'\n\n/**\n * Shared default attachment ceiling for email providers (Gmail allows\n * more, but larger uploads need resumable/upload-session APIs we don't use, so\n * all providers cap at the same conservative value).\n */\nexport const EMAIL_MAX_ATTACHMENT_BYTES = 25_000_000\n\n/**\n * Baseline capability profile shared by every email channel provider. Providers\n * spread this and override only what genuinely differs (e.g. `deleteMessage`).\n *\n * `fileSharing: false` until a provider's `convertOutbound` stitches attachment\n * bytes into the sent message \u2014 advertising `true` would silently drop bytes.\n */\nexport const baseEmailCapabilities: ChannelCapabilities = {\n // Core\n threading: true,\n richText: true,\n fileSharing: false,\n maxFileSize: EMAIL_MAX_ATTACHMENT_BYTES,\n supportedMimeTypes: [\n 'image/png',\n 'image/jpeg',\n 'image/gif',\n 'image/webp',\n 'application/pdf',\n 'application/zip',\n 'application/octet-stream',\n 'text/plain',\n 'text/html',\n 'text/csv',\n ],\n readReceipts: false,\n deliveryReceipts: false,\n typingIndicators: false,\n\n // Extended\n reactions: false,\n multiReactionPerUser: false,\n editMessage: false,\n deleteMessage: false,\n presence: false,\n richBlocks: false,\n interactiveComponents: false,\n inlineImages: true,\n conversationHistory: true,\n contactCards: false,\n locationSharing: false,\n voiceNotes: false,\n stickers: false,\n\n // Body formats\n supportedBodyFormats: ['text', 'html'],\n maxBodyLength: 5_000_000,\n\n // Polling (real-time push deferred to v2 for all email providers)\n realtimePush: false,\n\n // Stated rather than left to the default so the email baseline reads as an\n // explicit choice: outbound recipients here are addresses, not provider ids.\n recipientFormat: 'email',\n}\n"],
|
|
5
|
+
"mappings": "AAOO,MAAM,6BAA6B;AASnC,MAAM,wBAA6C;AAAA;AAAA,EAExD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,aAAa;AAAA,EACb,aAAa;AAAA,EACb,oBAAoB;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,kBAAkB;AAAA;AAAA,EAGlB,WAAW;AAAA,EACX,sBAAsB;AAAA,EACtB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,uBAAuB;AAAA,EACvB,cAAc;AAAA,EACd,qBAAqB;AAAA,EACrB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,UAAU;AAAA;AAAA,EAGV,sBAAsB,CAAC,QAAQ,MAAM;AAAA,EACrC,eAAe;AAAA;AAAA,EAGf,cAAc;AAAA;AAAA;AAAA,EAId,iBAAiB;AACnB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const MAX_OUTBOUND_RECIPIENT_LENGTH = 320;
|
|
3
|
+
const emailRecipientSchema = z.string().email();
|
|
4
|
+
const PROVIDER_NATIVE_ALLOWED = /^[A-Za-z0-9._:@+-]+$/;
|
|
5
|
+
function validateOutboundRecipient(recipient, capabilities) {
|
|
6
|
+
if (typeof recipient !== "string" || recipient.length === 0) {
|
|
7
|
+
return { ok: false, error: "Recipient is required" };
|
|
8
|
+
}
|
|
9
|
+
if (recipient.length > MAX_OUTBOUND_RECIPIENT_LENGTH) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error: `Recipient must be at most ${MAX_OUTBOUND_RECIPIENT_LENGTH} characters`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
if (capabilities?.recipientFormat !== "provider-native") {
|
|
16
|
+
return emailRecipientSchema.safeParse(recipient).success ? { ok: true } : { ok: false, error: "Recipient must be a valid email address" };
|
|
17
|
+
}
|
|
18
|
+
if (!PROVIDER_NATIVE_ALLOWED.test(recipient)) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
error: "Recipient may only contain letters, digits, and the characters . _ : @ + -"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (recipient.includes("..")) {
|
|
25
|
+
return { ok: false, error: 'Recipient must not contain ".."' };
|
|
26
|
+
}
|
|
27
|
+
return { ok: true };
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
MAX_OUTBOUND_RECIPIENT_LENGTH,
|
|
31
|
+
validateOutboundRecipient
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=outbound-recipient.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/outbound-recipient.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { ChannelCapabilities } from './adapter'\n\n/**\n * Upper bound shared by both recipient shapes. 320 is the RFC 5321 maximum for\n * an email address (64 local + `@` + 255 domain); provider-native identifiers\n * are far shorter, so one ceiling covers both.\n */\nexport const MAX_OUTBOUND_RECIPIENT_LENGTH = 320\n\nconst emailRecipientSchema = z.string().email()\n\n/**\n * Characters a provider-native recipient may contain. This is an allowlist, not a\n * denylist, because the recipient reaches adapters that interpolate it into a REST\n * path (Discord posts to `/channels/{recipient}/messages`), and a denylist fails\n * open on every character nobody thought of \u2014 percent-encoded separators\n * (`%2F`, `%2e%2e%2f`) and control bytes (NUL, DEL) all walked through the\n * previous `[\\r\\n\\s/\\\\?#]` class. An allowlist fails closed instead: the worst\n * case is a provider whose identifier alphabet needs widening, which is a\n * reviewable one-line change rather than a silent path-steering primitive.\n *\n * The set covers every provider-native identifier shape the hub has to carry\n * today \u2014 Discord snowflakes (digits), Slack-style ids (alphanumerics), and email\n * addresses, since `'provider-native'` must never narrow what `'email'` accepts.\n */\nconst PROVIDER_NATIVE_ALLOWED = /^[A-Za-z0-9._:@+-]+$/\n\nexport type OutboundRecipientCheck = { ok: true } | { ok: false; error: string }\n\n/**\n * Validate an outbound recipient against the provider's declared recipient\n * shape.\n *\n * The hub used to hard-wire `z.string().email()` on every outbound endpoint,\n * which left providers whose recipients are not email addresses with no product\n * path to send at all (#4976). Validation now follows the adapter's\n * `capabilities.recipientFormat`, defaulting to `'email'` so every existing\n * provider keeps byte-identical behavior.\n */\nexport function validateOutboundRecipient(\n recipient: unknown,\n capabilities: Pick<ChannelCapabilities, 'recipientFormat'> | null | undefined,\n): OutboundRecipientCheck {\n if (typeof recipient !== 'string' || recipient.length === 0) {\n return { ok: false, error: 'Recipient is required' }\n }\n if (recipient.length > MAX_OUTBOUND_RECIPIENT_LENGTH) {\n return {\n ok: false,\n error: `Recipient must be at most ${MAX_OUTBOUND_RECIPIENT_LENGTH} characters`,\n }\n }\n if (capabilities?.recipientFormat !== 'provider-native') {\n return emailRecipientSchema.safeParse(recipient).success\n ? { ok: true }\n : { ok: false, error: 'Recipient must be a valid email address' }\n }\n if (!PROVIDER_NATIVE_ALLOWED.test(recipient)) {\n return {\n ok: false,\n error: 'Recipient may only contain letters, digits, and the characters . _ : @ + -',\n }\n }\n // `.` is allowed (email addresses need it), so traversal still has to be\n // rejected explicitly even though the separators it would need are not.\n if (recipient.includes('..')) {\n return { ok: false, error: 'Recipient must not contain \"..\"' }\n }\n return { ok: true }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAQX,MAAM,gCAAgC;AAE7C,MAAM,uBAAuB,EAAE,OAAO,EAAE,MAAM;AAgB9C,MAAM,0BAA0B;AAczB,SAAS,0BACd,WACA,cACwB;AACxB,MAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GAAG;AAC3D,WAAO,EAAE,IAAI,OAAO,OAAO,wBAAwB;AAAA,EACrD;AACA,MAAI,UAAU,SAAS,+BAA+B;AACpD,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,6BAA6B,6BAA6B;AAAA,IACnE;AAAA,EACF;AACA,MAAI,cAAc,oBAAoB,mBAAmB;AACvD,WAAO,qBAAqB,UAAU,SAAS,EAAE,UAC7C,EAAE,IAAI,KAAK,IACX,EAAE,IAAI,OAAO,OAAO,0CAA0C;AAAA,EACpE;AACA,MAAI,CAAC,wBAAwB,KAAK,SAAS,GAAG;AAC5C,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,IACT;AAAA,EACF;AAGA,MAAI,UAAU,SAAS,IAAI,GAAG;AAC5B,WAAO,EAAE,IAAI,OAAO,OAAO,kCAAkC;AAAA,EAC/D;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
|
+
import { CommunicationChannel, ExternalConversation, MessageChannelLink } from "../data/entities.js";
|
|
4
|
+
const logger = createLogger("communication_channels").child({ component: "resolve-channel-type" });
|
|
5
|
+
async function resolveChannelType(container, scope, reference) {
|
|
6
|
+
const em = container.resolve("em").fork();
|
|
7
|
+
const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null };
|
|
8
|
+
const baseFilter = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null };
|
|
9
|
+
let externalConversationId = reference.externalConversationId ?? null;
|
|
10
|
+
if (!externalConversationId && reference.messageId) {
|
|
11
|
+
const link = await findOneWithDecryption(
|
|
12
|
+
em,
|
|
13
|
+
MessageChannelLink,
|
|
14
|
+
{ messageId: reference.messageId, ...baseFilter },
|
|
15
|
+
void 0,
|
|
16
|
+
dscope
|
|
17
|
+
);
|
|
18
|
+
if (link?.channelType) return link.channelType;
|
|
19
|
+
externalConversationId = link?.externalConversationId ?? null;
|
|
20
|
+
}
|
|
21
|
+
if (!externalConversationId) return null;
|
|
22
|
+
const conversation = await findOneWithDecryption(
|
|
23
|
+
em,
|
|
24
|
+
ExternalConversation,
|
|
25
|
+
{ id: externalConversationId, ...baseFilter },
|
|
26
|
+
void 0,
|
|
27
|
+
dscope
|
|
28
|
+
);
|
|
29
|
+
if (!conversation?.channelId) return null;
|
|
30
|
+
const channel = await findOneWithDecryption(
|
|
31
|
+
em,
|
|
32
|
+
CommunicationChannel,
|
|
33
|
+
{ id: conversation.channelId, ...baseFilter, deletedAt: null },
|
|
34
|
+
void 0,
|
|
35
|
+
dscope
|
|
36
|
+
);
|
|
37
|
+
return channel?.channelType ?? null;
|
|
38
|
+
}
|
|
39
|
+
async function resolveChannelTypeSafely(container, scope, reference) {
|
|
40
|
+
try {
|
|
41
|
+
return await resolveChannelType(container, scope, reference);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.warn("channel type resolution failed, treating the channel as unknown", { err });
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
resolveChannelType,
|
|
49
|
+
resolveChannelTypeSafely
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=resolve-channel-type.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/lib/resolve-channel-type.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CommunicationChannel, ExternalConversation, MessageChannelLink } from '../data/entities'\n\nconst logger = createLogger('communication_channels').child({ component: 'resolve-channel-type' })\n\nexport type ChannelTypeScope = {\n tenantId: string\n organizationId: string | null\n}\n\nexport type ChannelTypeReference = {\n /** `ExternalConversation.id`, as carried by a message's `sourceEntityId`. */\n externalConversationId?: string | null\n /** A platform `messages.message` id that may be linked to a channel. */\n messageId?: string | null\n}\n\n/**\n * Resolve the channel type behind a platform message or conversation (#4975).\n *\n * Cross-module callers (the `messages` compose route) need to know whether an\n * external correspondent is reachable by email before validating a compose\n * payload, but `messages` may not reach into this module's entities. They\n * resolve this facade from DI instead \u2014 mirroring `communicationChannelsSendAsUser`\n * \u2014 and treat a `null` result as \"unknown\", which the messages validator\n * handles fail-closed.\n *\n * Always tenant/organization scoped: a reference that does not resolve inside\n * the caller's scope is reported as unknown rather than looked up globally.\n */\nexport async function resolveChannelType(\n container: AppContainer,\n scope: ChannelTypeScope,\n reference: ChannelTypeReference,\n): Promise<string | null> {\n const em = (container.resolve('em') as EntityManager).fork()\n const dscope = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }\n const baseFilter = { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }\n\n let externalConversationId = reference.externalConversationId ?? null\n\n if (!externalConversationId && reference.messageId) {\n // The message is linked to a channel directly \u2014 `MessageChannelLink` carries\n // a denormalized channel type, so this is the cheapest hop when it exists.\n const link = await findOneWithDecryption(\n em,\n MessageChannelLink,\n { messageId: reference.messageId, ...baseFilter },\n undefined,\n dscope,\n )\n if (link?.channelType) return link.channelType\n externalConversationId = link?.externalConversationId ?? null\n }\n\n if (!externalConversationId) return null\n\n const conversation = await findOneWithDecryption(\n em,\n ExternalConversation,\n { id: externalConversationId, ...baseFilter },\n undefined,\n dscope,\n )\n if (!conversation?.channelId) return null\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: conversation.channelId, ...baseFilter, deletedAt: null },\n undefined,\n dscope,\n )\n return channel?.channelType ?? null\n}\n\n/**\n * Same contract, but never throws: a lookup failure degrades to \"unknown\" so a\n * transient database error cannot turn into a 500 on an otherwise valid compose.\n * Unknown is the safe answer \u2014 the messages validator fails closed on it.\n */\nexport async function resolveChannelTypeSafely(\n container: AppContainer,\n scope: ChannelTypeScope,\n reference: ChannelTypeReference,\n): Promise<string | null> {\n try {\n return await resolveChannelType(container, scope, reference)\n } catch (err) {\n logger.warn('channel type resolution failed, treating the channel as unknown', { err })\n return null\n }\n}\n\n/** DI service type for cross-module callers (resolve `communicationChannelsResolveChannelType`). */\nexport type ResolveChannelTypeService = typeof resolveChannelTypeSafely\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,6BAA6B;AACtC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB,sBAAsB,0BAA0B;AAE/E,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,uBAAuB,CAAC;AA2BjG,eAAsB,mBACpB,WACA,OACA,WACwB;AACxB,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,QAAM,SAAS,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,kBAAkB,KAAK;AACxF,QAAM,aAAa,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,kBAAkB,KAAK;AAE5F,MAAI,yBAAyB,UAAU,0BAA0B;AAEjE,MAAI,CAAC,0BAA0B,UAAU,WAAW;AAGlD,UAAM,OAAO,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,MACA,EAAE,WAAW,UAAU,WAAW,GAAG,WAAW;AAAA,MAChD;AAAA,MACA;AAAA,IACF;AACA,QAAI,MAAM,YAAa,QAAO,KAAK;AACnC,6BAAyB,MAAM,0BAA0B;AAAA,EAC3D;AAEA,MAAI,CAAC,uBAAwB,QAAO;AAEpC,QAAM,eAAe,MAAM;AAAA,IACzB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,wBAAwB,GAAG,WAAW;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,cAAc,UAAW,QAAO;AAErC,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA,EAAE,IAAI,aAAa,WAAW,GAAG,YAAY,WAAW,KAAK;AAAA,IAC7D;AAAA,IACA;AAAA,EACF;AACA,SAAO,SAAS,eAAe;AACjC;AAOA,eAAsB,yBACpB,WACA,OACA,WACwB;AACxB,MAAI;AACF,WAAO,MAAM,mBAAmB,WAAW,OAAO,SAAS;AAAA,EAC7D,SAAS,KAAK;AACZ,WAAO,KAAK,mEAAmE,EAAE,IAAI,CAAC;AACtF,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { baseEmailCapabilities } from "./email-capabilities.js";
|
|
2
2
|
import { hasChannelAdapter, registerChannelAdapter } from "./adapter-registry-singleton.js";
|
|
3
3
|
const TEST_SEED_PROVIDER_KEY = "__test_seed__";
|
|
4
|
+
const TEST_SEED_CHAT_PROVIDER_KEY = "__test_seed_chat__";
|
|
4
5
|
const TEST_CHANNEL_SEEDING_ENV = "OM_ENABLE_TEST_CHANNEL_SEEDING";
|
|
5
6
|
function isTestChannelSeedingEnabled() {
|
|
6
7
|
const raw = process.env[TEST_CHANNEL_SEEDING_ENV];
|
|
@@ -14,6 +15,8 @@ const testSeedCapabilities = {
|
|
|
14
15
|
};
|
|
15
16
|
class TestSeedChannelAdapter {
|
|
16
17
|
constructor() {
|
|
18
|
+
// Widened to `string` rather than inferred as a literal so the chat-flavoured
|
|
19
|
+
// subclass below can override both with its own provider key / channel type.
|
|
17
20
|
this.providerKey = TEST_SEED_PROVIDER_KEY;
|
|
18
21
|
this.channelType = "email";
|
|
19
22
|
this.capabilities = testSeedCapabilities;
|
|
@@ -49,18 +52,54 @@ class TestSeedChannelAdapter {
|
|
|
49
52
|
return { ok: true };
|
|
50
53
|
}
|
|
51
54
|
}
|
|
55
|
+
class TestSeedChatChannelAdapter extends TestSeedChannelAdapter {
|
|
56
|
+
constructor() {
|
|
57
|
+
super(...arguments);
|
|
58
|
+
this.providerKey = TEST_SEED_CHAT_PROVIDER_KEY;
|
|
59
|
+
this.channelType = "discord";
|
|
60
|
+
}
|
|
61
|
+
async normalizeInbound(raw) {
|
|
62
|
+
const frame = raw.raw ?? {};
|
|
63
|
+
const senderIdentifier = String(frame.senderIdentifier ?? "");
|
|
64
|
+
if (!senderIdentifier) {
|
|
65
|
+
throw new Error("[internal] TestSeedChatChannelAdapter requires a senderIdentifier");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
externalMessageId: String(frame.externalMessageId ?? ""),
|
|
69
|
+
externalConversationId: String(frame.externalConversationId ?? ""),
|
|
70
|
+
senderIdentifier,
|
|
71
|
+
senderDisplayName: typeof frame.senderDisplayName === "string" ? frame.senderDisplayName : void 0,
|
|
72
|
+
body: typeof frame.body === "string" ? frame.body : "",
|
|
73
|
+
bodyFormat: "text",
|
|
74
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
75
|
+
channelPayload: {},
|
|
76
|
+
channelContentType: "text/plain",
|
|
77
|
+
channelMetadata: {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
52
81
|
let cachedTestSeedAdapter = null;
|
|
82
|
+
let cachedTestSeedChatAdapter = null;
|
|
53
83
|
function getTestSeedChannelAdapter() {
|
|
54
84
|
if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter();
|
|
55
85
|
return cachedTestSeedAdapter;
|
|
56
86
|
}
|
|
87
|
+
function getTestSeedChatChannelAdapter() {
|
|
88
|
+
if (!cachedTestSeedChatAdapter) cachedTestSeedChatAdapter = new TestSeedChatChannelAdapter();
|
|
89
|
+
return cachedTestSeedChatAdapter;
|
|
90
|
+
}
|
|
57
91
|
function ensureTestSeedAdapterRegistered() {
|
|
58
92
|
if (!isTestChannelSeedingEnabled()) return;
|
|
59
|
-
if (hasChannelAdapter(TEST_SEED_PROVIDER_KEY))
|
|
60
|
-
|
|
93
|
+
if (!hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) {
|
|
94
|
+
registerChannelAdapter(getTestSeedChannelAdapter());
|
|
95
|
+
}
|
|
96
|
+
if (!hasChannelAdapter(TEST_SEED_CHAT_PROVIDER_KEY)) {
|
|
97
|
+
registerChannelAdapter(getTestSeedChatChannelAdapter());
|
|
98
|
+
}
|
|
61
99
|
}
|
|
62
100
|
export {
|
|
63
101
|
TEST_CHANNEL_SEEDING_ENV,
|
|
102
|
+
TEST_SEED_CHAT_PROVIDER_KEY,
|
|
64
103
|
TEST_SEED_PROVIDER_KEY,
|
|
65
104
|
ensureTestSeedAdapterRegistered,
|
|
66
105
|
isTestChannelSeedingEnabled
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/lib/test-seed.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n ChannelAdapter,\n ChannelCapabilities,\n ChannelNativeContent,\n ConvertOutboundInput,\n GetMessageStatusInput,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n SendMessageInput,\n SendMessageResult,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from './adapter'\nimport { baseEmailCapabilities } from './email-capabilities'\nimport { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'\n\n/**\n * Test-only channel seeding support.\n *\n * The ephemeral integration harness cannot connect a REAL email channel:\n * - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server\n * (none exists in CI), so `POST /channels/connect/credentials` returns 422.\n * - Even with a connected channel, the outbound delivery worker calls the real\n * SMTP adapter, which fails with no server \u2014 so `communication_channels.message.sent`\n * never fires and the customers link subscriber never runs.\n *\n * To make the compose \u2192 deliver \u2192 `.sent` \u2192 CRM-link \u2192 cross-user-visibility chain\n * (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable\n * end-to-end against real Postgres, this module provides a network-free stub adapter\n * that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.\n *\n * Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};\n * when the env flag is unset (the production default) the adapter is never registered\n * and the `__test_seed__` provider key resolves to no adapter \u2014 so the connect route\n * returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated\n * test-seed API route enforces the same gate independently (fail-closed 404 in prod).\n */\n\n/** Provider key for the network-free test stub adapter. */\nexport const TEST_SEED_PROVIDER_KEY = '__test_seed__'\n\n/** Env flag that unlocks test-only channel seeding. Off in production. */\nexport const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'\n\n/**\n * True only when the test-seeding env flag is explicitly enabled. Accepts the\n * usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a\n * plain `=true`. Any other value (including unset) is treated as disabled.\n */\nexport function isTestChannelSeedingEnabled(): boolean {\n const raw = process.env[TEST_CHANNEL_SEEDING_ENV]\n if (typeof raw !== 'string') return false\n return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())\n}\n\n/**\n * Capabilities for the stub: an email channel that supports neither reactions,\n * edit/delete, nor conversation history \u2014 so the strict registry validator\n * (`validateAdapterCapabilities`) requires only the core method surface.\n */\nconst testSeedCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n conversationHistory: false,\n realtimePush: false,\n}\n\n/**\n * A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any\n * network I/O. Used exclusively by the integration harness to let the outbound\n * delivery worker reach its success path and emit `communication_channels.message.sent`.\n */\nclass TestSeedChannelAdapter implements ChannelAdapter {\n readonly providerKey = TEST_SEED_PROVIDER_KEY\n readonly channelType = 'email'\n readonly capabilities = testSeedCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n // Synthesize a deterministic-looking RFC2822-style message id; never touches\n // the network. The delivery worker persists this as the external message id.\n const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'sent',\n metadata: { testSeed: true },\n }\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n // No real webhook \u2014 return the inert event so the generic webhook route 202s\n // without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).\n return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return {\n content: {\n text: input.body,\n bodyFormat: input.bodyFormat,\n },\n metadata: input.channelMetadata ?? {},\n }\n }\n\n async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // The test-seed inbound path seeds MessageChannelLink rows directly and emits\n // the hub event, so this adapter never normalizes a raw inbound payload.\n throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')\n }\n\n async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n // No real server to authenticate against \u2014 accept any credentials so the\n // connect command persists a connected channel.\n return { ok: true }\n }\n}\n\nlet cachedTestSeedAdapter: TestSeedChannelAdapter | null = null\n\nfunction getTestSeedChannelAdapter(): TestSeedChannelAdapter {\n if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()\n return cachedTestSeedAdapter\n}\n\n/**\n * Register the test-seed adapter exactly once, but ONLY when the env flag is set.\n * Idempotent and safe to call from every container creation (`di.register`) \u2014 a\n * no-op when seeding is disabled or the adapter is already registered.\n */\nexport function ensureTestSeedAdapterRegistered(): void {\n if (!isTestChannelSeedingEnabled()) return\n if (hasChannelAdapter(TEST_SEED_PROVIDER_KEY))
|
|
5
|
-
"mappings": "AAeA,SAAS,6BAA6B;AACtC,SAAS,mBAAmB,8BAA8B;AAyBnD,MAAM,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type {\n ChannelAdapter,\n ChannelCapabilities,\n ChannelNativeContent,\n ConvertOutboundInput,\n GetMessageStatusInput,\n InboundMessage,\n MessageStatus,\n NormalizedInboundMessage,\n SendMessageInput,\n SendMessageResult,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from './adapter'\nimport { baseEmailCapabilities } from './email-capabilities'\nimport { hasChannelAdapter, registerChannelAdapter } from './adapter-registry-singleton'\n\n/**\n * Test-only channel seeding support.\n *\n * The ephemeral integration harness cannot connect a REAL email channel:\n * - IMAP/SMTP `validateCredentials` performs a live LOGIN against a mail server\n * (none exists in CI), so `POST /channels/connect/credentials` returns 422.\n * - Even with a connected channel, the outbound delivery worker calls the real\n * SMTP adapter, which fails with no server \u2014 so `communication_channels.message.sent`\n * never fires and the customers link subscriber never runs.\n *\n * To make the compose \u2192 deliver \u2192 `.sent` \u2192 CRM-link \u2192 cross-user-visibility chain\n * (TC-CRM-EMAIL-001) and the inbound auto-link chain (TC-CRM-EMAIL-002..005) runnable\n * end-to-end against real Postgres, this module provides a network-free stub adapter\n * that is registered ONLY when `OM_ENABLE_TEST_CHANNEL_SEEDING` is set.\n *\n * Production safety: the registration is gated by {@link isTestChannelSeedingEnabled};\n * when the env flag is unset (the production default) the adapter is never registered\n * and the `__test_seed__` provider key resolves to no adapter \u2014 so the connect route\n * returns 404 `no_adapter` exactly as it would for any unknown provider. The dedicated\n * test-seed API route enforces the same gate independently (fail-closed 404 in prod).\n */\n\n/** Provider key for the network-free test stub adapter. */\nexport const TEST_SEED_PROVIDER_KEY = '__test_seed__'\n\n/**\n * Provider key for the network-free stub adapter that stands in for a CHAT\n * provider \u2014 one whose senders are identified by an opaque handle and have no\n * email address at all (Discord, Slack, Telegram\u2026).\n *\n * It exists because the email-shaped stub above can only ever prove the hub\n * accepts email-shaped data. That is precisely how CI stayed green while every\n * real inbound Discord message was rejected (#4975): the fixture invented an\n * address the provider can never produce. Tests that need to prove the hub's\n * non-email identity contract MUST drive this provider instead.\n */\nexport const TEST_SEED_CHAT_PROVIDER_KEY = '__test_seed_chat__'\n\n/** Env flag that unlocks test-only channel seeding. Off in production. */\nexport const TEST_CHANNEL_SEEDING_ENV = 'OM_ENABLE_TEST_CHANNEL_SEEDING'\n\n/**\n * True only when the test-seeding env flag is explicitly enabled. Accepts the\n * usual truthy tokens (`1`, `true`, `yes`, `on`) so the harness can opt in via a\n * plain `=true`. Any other value (including unset) is treated as disabled.\n */\nexport function isTestChannelSeedingEnabled(): boolean {\n const raw = process.env[TEST_CHANNEL_SEEDING_ENV]\n if (typeof raw !== 'string') return false\n return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase())\n}\n\n/**\n * Capabilities for the stub: an email channel that supports neither reactions,\n * edit/delete, nor conversation history \u2014 so the strict registry validator\n * (`validateAdapterCapabilities`) requires only the core method surface.\n */\nconst testSeedCapabilities: ChannelCapabilities = {\n ...baseEmailCapabilities,\n conversationHistory: false,\n realtimePush: false,\n}\n\n/**\n * A `ChannelAdapter` whose `sendMessage` reports a successful send WITHOUT any\n * network I/O. Used exclusively by the integration harness to let the outbound\n * delivery worker reach its success path and emit `communication_channels.message.sent`.\n */\nclass TestSeedChannelAdapter implements ChannelAdapter {\n // Widened to `string` rather than inferred as a literal so the chat-flavoured\n // subclass below can override both with its own provider key / channel type.\n readonly providerKey: string = TEST_SEED_PROVIDER_KEY\n readonly channelType: string = 'email'\n readonly capabilities = testSeedCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n // Synthesize a deterministic-looking RFC2822-style message id; never touches\n // the network. The delivery worker persists this as the external message id.\n const externalMessageId = `test-seed-${Date.now()}-${Math.random().toString(16).slice(2, 10)}@test-seed.local`\n return {\n externalMessageId,\n conversationId: input.conversationId,\n status: 'sent',\n metadata: { testSeed: true },\n }\n }\n\n async verifyWebhook(_input: VerifyWebhookInput): Promise<InboundMessage> {\n // No real webhook \u2014 return the inert event so the generic webhook route 202s\n // without enqueuing tenant-scoped work (mirrors the IMAP adapter contract).\n return { raw: {}, eventType: 'other', metadata: { reason: 'test-seed-no-webhook' } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return {\n content: {\n text: input.body,\n bodyFormat: input.bodyFormat,\n },\n metadata: input.channelMetadata ?? {},\n }\n }\n\n async normalizeInbound(_raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // The test-seed inbound path seeds MessageChannelLink rows directly and emits\n // the hub event, so this adapter never normalizes a raw inbound payload.\n throw new Error('[internal] TestSeedChannelAdapter.normalizeInbound is not used by the seed harness')\n }\n\n async validateCredentials(_input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n // No real server to authenticate against \u2014 accept any credentials so the\n // connect command persists a connected channel.\n return { ok: true }\n }\n}\n\n/**\n * Chat-flavoured twin of {@link TestSeedChannelAdapter}: same network-free\n * behaviour, but it declares a non-email `channelType`, so a channel connected\n * through it is shaped like a real chat channel \u2014 including an\n * `externalIdentifier` of NULL when no email-ish credential key is supplied.\n */\nclass TestSeedChatChannelAdapter extends TestSeedChannelAdapter {\n readonly providerKey: string = TEST_SEED_CHAT_PROVIDER_KEY\n readonly channelType: string = 'discord'\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n // Unlike the email stub, this one is reachable: the test-seed ingest action\n // feeds it a chat-shaped frame so the message travels the real ingest path\n // (and therefore the real compose validation) rather than a SQL shortcut.\n const frame = (raw.raw ?? {}) as Record<string, unknown>\n const senderIdentifier = String(frame.senderIdentifier ?? '')\n if (!senderIdentifier) {\n throw new Error('[internal] TestSeedChatChannelAdapter requires a senderIdentifier')\n }\n return {\n externalMessageId: String(frame.externalMessageId ?? ''),\n externalConversationId: String(frame.externalConversationId ?? ''),\n senderIdentifier,\n senderDisplayName:\n typeof frame.senderDisplayName === 'string' ? frame.senderDisplayName : undefined,\n body: typeof frame.body === 'string' ? frame.body : '',\n bodyFormat: 'text',\n timestamp: new Date(),\n channelPayload: {},\n channelContentType: 'text/plain',\n channelMetadata: {},\n }\n }\n}\n\nlet cachedTestSeedAdapter: TestSeedChannelAdapter | null = null\nlet cachedTestSeedChatAdapter: TestSeedChatChannelAdapter | null = null\n\nfunction getTestSeedChannelAdapter(): TestSeedChannelAdapter {\n if (!cachedTestSeedAdapter) cachedTestSeedAdapter = new TestSeedChannelAdapter()\n return cachedTestSeedAdapter\n}\n\nfunction getTestSeedChatChannelAdapter(): TestSeedChatChannelAdapter {\n if (!cachedTestSeedChatAdapter) cachedTestSeedChatAdapter = new TestSeedChatChannelAdapter()\n return cachedTestSeedChatAdapter\n}\n\n/**\n * Register the test-seed adapter exactly once, but ONLY when the env flag is set.\n * Idempotent and safe to call from every container creation (`di.register`) \u2014 a\n * no-op when seeding is disabled or the adapter is already registered.\n */\nexport function ensureTestSeedAdapterRegistered(): void {\n if (!isTestChannelSeedingEnabled()) return\n if (!hasChannelAdapter(TEST_SEED_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChannelAdapter())\n }\n if (!hasChannelAdapter(TEST_SEED_CHAT_PROVIDER_KEY)) {\n registerChannelAdapter(getTestSeedChatChannelAdapter())\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -227,6 +227,15 @@ const SYSTEM_STATUS_VARIABLES = [
|
|
|
227
227
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#optimize_index_coverage_stats`,
|
|
228
228
|
defaultValue: "false"
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
key: "OM_LIST_COUNT_CAP",
|
|
232
|
+
category: "query_index",
|
|
233
|
+
kind: "string",
|
|
234
|
+
labelKey: "configs.systemStatus.variables.listCountCap.label",
|
|
235
|
+
descriptionKey: "configs.systemStatus.variables.listCountCap.description",
|
|
236
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_list_count_cap`,
|
|
237
|
+
defaultValue: "10000"
|
|
238
|
+
},
|
|
230
239
|
{
|
|
231
240
|
key: "FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES",
|
|
232
241
|
category: "query_index",
|