@open-mercato/core 0.6.8-develop.6983.1.e4f794099e → 0.6.8-develop.6985.1.fb93574faa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/notification/index.js +6 -0
- package/dist/generated/entities/notification/index.js.map +2 -2
- package/dist/generated/entities/notification_preference/index.js +19 -0
- package/dist/generated/entities/notification_preference/index.js.map +7 -0
- package/dist/generated/entities/notification_type/index.js +21 -0
- package/dist/generated/entities/notification_type/index.js.map +7 -0
- package/dist/generated/entities/notification_type_override/index.js +17 -0
- package/dist/generated/entities/notification_type_override/index.js.map +7 -0
- package/dist/generated/entities/push_notification_delivery/index.js +41 -0
- package/dist/generated/entities/push_notification_delivery/index.js.map +7 -0
- package/dist/generated/entities/user_device/index.js +35 -0
- package/dist/generated/entities/user_device/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +72 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +2 -1
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/appRoot.js +9 -0
- package/dist/helpers/integration/appRoot.js.map +7 -0
- package/dist/helpers/integration/dbFixtures.js +1 -4
- package/dist/helpers/integration/dbFixtures.js.map +2 -2
- package/dist/helpers/integration/pushFake.js +126 -0
- package/dist/helpers/integration/pushFake.js.map +7 -0
- package/dist/modules/auth/notifications.js +16 -0
- package/dist/modules/auth/notifications.js.map +2 -2
- package/dist/modules/business_rules/notifications.js +2 -0
- package/dist/modules/business_rules/notifications.js.map +2 -2
- package/dist/modules/catalog/notifications.js +2 -0
- package/dist/modules/catalog/notifications.js.map +2 -2
- package/dist/modules/communication_channels/acl.js +9 -0
- package/dist/modules/communication_channels/acl.js.map +2 -2
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js +133 -0
- package/dist/modules/communication_channels/api/delete/admin/channels/[id]/route.js.map +7 -0
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/health/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js +6 -2
- package/dist/modules/communication_channels/api/get/channels/[id]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/get/channels/route.js +2 -1
- package/dist/modules/communication_channels/api/get/channels/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js +10 -5
- package/dist/modules/communication_channels/api/post/channels/[id]/test-send/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +29 -1
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +157 -0
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +7 -0
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js +8 -1
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/admin-delete-channel.js +141 -0
- package/dist/modules/communication_channels/commands/admin-delete-channel.js.map +7 -0
- package/dist/modules/communication_channels/commands/connect-credential-channel.js +25 -8
- package/dist/modules/communication_channels/commands/connect-credential-channel.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-register.js +1 -1
- package/dist/modules/communication_channels/commands/push-register.js.map +2 -2
- package/dist/modules/communication_channels/commands/push-unregister.js +4 -3
- package/dist/modules/communication_channels/commands/push-unregister.js.map +2 -2
- package/dist/modules/communication_channels/data/entities.js +4 -0
- package/dist/modules/communication_channels/data/entities.js.map +2 -2
- package/dist/modules/communication_channels/data/validators.js +2 -1
- package/dist/modules/communication_channels/data/validators.js.map +2 -2
- package/dist/modules/communication_channels/lib/access-control.js +11 -1
- package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
- package/dist/modules/communication_channels/lib/connect-channel.js +20 -12
- package/dist/modules/communication_channels/lib/connect-channel.js.map +2 -2
- package/dist/modules/communication_channels/lib/push-adapter.js +68 -0
- package/dist/modules/communication_channels/lib/push-adapter.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js +31 -0
- package/dist/modules/communication_channels/lib/push-capabilities.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js +20 -0
- package/dist/modules/communication_channels/lib/push-connect-error.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js +41 -0
- package/dist/modules/communication_channels/lib/push-credential-errors.js.map +7 -0
- package/dist/modules/communication_channels/lib/push-envelope.js +46 -0
- package/dist/modules/communication_channels/lib/push-envelope.js.map +7 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js +64 -0
- package/dist/modules/communication_channels/lib/refcounted-client-cache.js.map +7 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js +13 -0
- package/dist/modules/communication_channels/migrations/Migration20260703123630_communication_channels.js.map +7 -0
- package/dist/modules/communication_channels/notifications.js +4 -0
- package/dist/modules/communication_channels/notifications.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +2 -0
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +21 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/notifications.js +12 -0
- package/dist/modules/customer_accounts/notifications.js.map +2 -2
- package/dist/modules/customers/notifications.js +4 -0
- package/dist/modules/customers/notifications.js.map +2 -2
- package/dist/modules/devices/acl.js +11 -0
- package/dist/modules/devices/acl.js.map +7 -0
- package/dist/modules/devices/api/[id]/route.js +167 -0
- package/dist/modules/devices/api/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js +219 -0
- package/dist/modules/devices/api/admin/devices/[id]/route.js.map +7 -0
- package/dist/modules/devices/api/admin/devices/route.js +140 -0
- package/dist/modules/devices/api/admin/devices/route.js.map +7 -0
- package/dist/modules/devices/api/auth.js +9 -0
- package/dist/modules/devices/api/auth.js.map +7 -0
- package/dist/modules/devices/api/deviceList.js +53 -0
- package/dist/modules/devices/api/deviceList.js.map +7 -0
- package/dist/modules/devices/api/deviceOps.js +118 -0
- package/dist/modules/devices/api/deviceOps.js.map +7 -0
- package/dist/modules/devices/api/openapi.js +19 -0
- package/dist/modules/devices/api/openapi.js.map +7 -0
- package/dist/modules/devices/api/route.js +143 -0
- package/dist/modules/devices/api/route.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.js +135 -0
- package/dist/modules/devices/backend/devices/[id]/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/[id]/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.js +68 -0
- package/dist/modules/devices/backend/devices/create/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js +16 -0
- package/dist/modules/devices/backend/devices/create/page.meta.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.js +235 -0
- package/dist/modules/devices/backend/devices/page.js.map +7 -0
- package/dist/modules/devices/backend/devices/page.meta.js +23 -0
- package/dist/modules/devices/backend/devices/page.meta.js.map +7 -0
- package/dist/modules/devices/commands/deactivate.js +107 -0
- package/dist/modules/devices/commands/deactivate.js.map +7 -0
- package/dist/modules/devices/commands/index.js +4 -0
- package/dist/modules/devices/commands/index.js.map +7 -0
- package/dist/modules/devices/commands/register.js +183 -0
- package/dist/modules/devices/commands/register.js.map +7 -0
- package/dist/modules/devices/commands/shared.js +74 -0
- package/dist/modules/devices/commands/shared.js.map +7 -0
- package/dist/modules/devices/commands/update.js +126 -0
- package/dist/modules/devices/commands/update.js.map +7 -0
- package/dist/modules/devices/data/entities.js +83 -0
- package/dist/modules/devices/data/entities.js.map +7 -0
- package/dist/modules/devices/data/validators.js +49 -0
- package/dist/modules/devices/data/validators.js.map +7 -0
- package/dist/modules/devices/di.js +11 -0
- package/dist/modules/devices/di.js.map +7 -0
- package/dist/modules/devices/encryption.js +12 -0
- package/dist/modules/devices/encryption.js.map +7 -0
- package/dist/modules/devices/events.js +19 -0
- package/dist/modules/devices/events.js.map +7 -0
- package/dist/modules/devices/index.js +17 -0
- package/dist/modules/devices/index.js.map +7 -0
- package/dist/modules/devices/lib/operationMetadata.js +20 -0
- package/dist/modules/devices/lib/operationMetadata.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260602120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260630120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js +13 -0
- package/dist/modules/devices/migrations/Migration20260701120000.js.map +7 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js +15 -0
- package/dist/modules/devices/migrations/Migration20260721120000.js.map +7 -0
- package/dist/modules/devices/setup.js +13 -0
- package/dist/modules/devices/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +8 -3
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/migrations/Migration20260722120000.js +28 -0
- package/dist/modules/entities/migrations/Migration20260722120000.js.map +7 -0
- package/dist/modules/inbox_ops/notifications.js +2 -0
- package/dist/modules/inbox_ops/notifications.js.map +2 -2
- package/dist/modules/messages/notifications.js +2 -0
- package/dist/modules/messages/notifications.js.map +2 -2
- package/dist/modules/notifications/acl.js +3 -1
- package/dist/modules/notifications/acl.js.map +2 -2
- package/dist/modules/notifications/api/admin/preferences/route.js +167 -0
- package/dist/modules/notifications/api/admin/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/channels/route.js +54 -0
- package/dist/modules/notifications/api/channels/route.js.map +7 -0
- package/dist/modules/notifications/api/preferences/route.js +126 -0
- package/dist/modules/notifications/api/preferences/route.js.map +7 -0
- package/dist/modules/notifications/api/route.js +8 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js +187 -0
- package/dist/modules/notifications/api/types/[id]/channels/[channel]/route.js.map +7 -0
- package/dist/modules/notifications/api/types/route.js +225 -0
- package/dist/modules/notifications/api/types/route.js.map +7 -0
- package/dist/modules/notifications/api/unread-count/route.js +5 -1
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js +28 -0
- package/dist/modules/notifications/backend/notifications/user-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js +10 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.js.map +7 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +33 -0
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +7 -0
- package/dist/modules/notifications/data/entities.js +128 -1
- package/dist/modules/notifications/data/entities.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +95 -2
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/di.js +7 -0
- package/dist/modules/notifications/di.js.map +2 -2
- package/dist/modules/notifications/events.js +17 -0
- package/dist/modules/notifications/events.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js +190 -0
- package/dist/modules/notifications/frontend/NotificationPreferenceMatrix.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js +124 -0
- package/dist/modules/notifications/frontend/NotificationPreferencesPageClient.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +166 -1
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js +172 -0
- package/dist/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.js.map +7 -0
- package/dist/modules/notifications/generators.js +63 -0
- package/dist/modules/notifications/generators.js.map +7 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js +19 -0
- package/dist/modules/notifications/lib/delivery-strategies-registry.js.map +7 -0
- package/dist/modules/notifications/lib/deliveryStrategies.js.map +2 -2
- package/dist/modules/notifications/lib/derive-category.js +8 -0
- package/dist/modules/notifications/lib/derive-category.js.map +7 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js +35 -0
- package/dist/modules/notifications/lib/notification-channel-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notification-type-registry.js +87 -0
- package/dist/modules/notifications/lib/notification-type-registry.js.map +7 -0
- package/dist/modules/notifications/lib/notificationCopy.js +14 -0
- package/dist/modules/notifications/lib/notificationCopy.js.map +7 -0
- package/dist/modules/notifications/lib/notificationFactory.js +4 -1
- package/dist/modules/notifications/lib/notificationFactory.js.map +2 -2
- package/dist/modules/notifications/lib/notificationMapper.js +2 -0
- package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
- package/dist/modules/notifications/lib/notificationPreferenceService.js +83 -0
- package/dist/modules/notifications/lib/notificationPreferenceService.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +83 -28
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/notificationVisibility.js +24 -0
- package/dist/modules/notifications/lib/notificationVisibility.js.map +7 -0
- package/dist/modules/notifications/lib/routeHelpers.js +11 -1
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/notifications/lib/shouldDeliver.js +46 -0
- package/dist/modules/notifications/lib/shouldDeliver.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +54 -0
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js +12 -0
- package/dist/modules/notifications/lib/strategies/in-app-delivery-strategy.js.map +7 -0
- package/dist/modules/notifications/lib/typeCatalogue.js +71 -0
- package/dist/modules/notifications/lib/typeCatalogue.js.map +7 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js +12 -0
- package/dist/modules/notifications/lib/typeChannelSettings.js.map +7 -0
- package/dist/modules/notifications/lib/typeOverrides.js +19 -0
- package/dist/modules/notifications/lib/typeOverrides.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260625122947_notifications.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js +15 -0
- package/dist/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js +13 -0
- package/dist/modules/notifications/migrations/Migration20260706120000_notifications_channels.js.map +7 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js +14 -0
- package/dist/modules/notifications/migrations/Migration20260716123121_notifications.js.map +7 -0
- package/dist/modules/notifications/notification-channels.js +26 -0
- package/dist/modules/notifications/notification-channels.js.map +7 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js +12 -0
- package/dist/modules/notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/notifications/setup.js +16 -1
- package/dist/modules/notifications/setup.js.map +2 -2
- package/dist/modules/notifications/subscribers/deliver-notification.js +66 -57
- package/dist/modules/notifications/subscribers/deliver-notification.js.map +2 -2
- package/dist/modules/notifications/subscribers/sync-notification-types.js +16 -0
- package/dist/modules/notifications/subscribers/sync-notification-types.js.map +7 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js +23 -0
- package/dist/modules/notifications/widgets/injection/profile-preferences-menu/widget.js.map +7 -0
- package/dist/modules/notifications/widgets/injection-table.js +14 -0
- package/dist/modules/notifications/widgets/injection-table.js.map +7 -0
- package/dist/modules/push_notifications/acl.js +13 -0
- package/dist/modules/push_notifications/acl.js.map +7 -0
- package/dist/modules/push_notifications/api/custom-send/route.js +141 -0
- package/dist/modules/push_notifications/api/custom-send/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js +110 -0
- package/dist/modules/push_notifications/api/deliveries/[id]/route.js.map +7 -0
- package/dist/modules/push_notifications/api/deliveries/route.js +56 -0
- package/dist/modules/push_notifications/api/deliveries/route.js.map +7 -0
- package/dist/modules/push_notifications/api/openapi.js +19 -0
- package/dist/modules/push_notifications/api/openapi.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js +103 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js +13 -0
- package/dist/modules/push_notifications/backend/push_notifications/[id]/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js +213 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js +23 -0
- package/dist/modules/push_notifications/backend/push_notifications/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js +245 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +7 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js +26 -0
- package/dist/modules/push_notifications/backend/push_notifications/send/page.meta.js.map +7 -0
- package/dist/modules/push_notifications/data/entities.js +92 -0
- package/dist/modules/push_notifications/data/entities.js.map +7 -0
- package/dist/modules/push_notifications/data/extensions.js +23 -0
- package/dist/modules/push_notifications/data/extensions.js.map +7 -0
- package/dist/modules/push_notifications/data/validators.js +89 -0
- package/dist/modules/push_notifications/data/validators.js.map +7 -0
- package/dist/modules/push_notifications/di.js +17 -0
- package/dist/modules/push_notifications/di.js.map +7 -0
- package/dist/modules/push_notifications/events.js +24 -0
- package/dist/modules/push_notifications/events.js.map +7 -0
- package/dist/modules/push_notifications/index.js +16 -0
- package/dist/modules/push_notifications/index.js.map +7 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js +80 -0
- package/dist/modules/push_notifications/lib/fake-provider-recorder.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js +54 -0
- package/dist/modules/push_notifications/lib/push-delivery-strategy.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-delivery.js +297 -0
- package/dist/modules/push_notifications/lib/push-delivery.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-fanout.js +138 -0
- package/dist/modules/push_notifications/lib/push-fanout.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-health.js +32 -0
- package/dist/modules/push_notifications/lib/push-health.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-reaper.js +87 -0
- package/dist/modules/push_notifications/lib/push-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js +136 -0
- package/dist/modules/push_notifications/lib/push-receipt-reaper.js.map +7 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js +63 -0
- package/dist/modules/push_notifications/lib/push-stub-adapter.js.map +7 -0
- package/dist/modules/push_notifications/lib/queue.js +53 -0
- package/dist/modules/push_notifications/lib/queue.js.map +7 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js +13 -0
- package/dist/modules/push_notifications/lib/reclaim-window.js.map +7 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js +48 -0
- package/dist/modules/push_notifications/lib/send-custom-push.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js +16 -0
- package/dist/modules/push_notifications/migrations/Migration20260625150049_push_notifications.js.map +7 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js +13 -0
- package/dist/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.js.map +7 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js +8 -0
- package/dist/modules/push_notifications/notifications.delivery-strategies.js.map +7 -0
- package/dist/modules/push_notifications/notifications.js +40 -0
- package/dist/modules/push_notifications/notifications.js.map +7 -0
- package/dist/modules/push_notifications/setup.js +52 -0
- package/dist/modules/push_notifications/setup.js.map +7 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js +57 -0
- package/dist/modules/push_notifications/workers/reclaim-stuck.worker.js.map +7 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js +27 -0
- package/dist/modules/push_notifications/workers/send-push.worker.js.map +7 -0
- package/dist/modules/sales/notifications.js +8 -0
- package/dist/modules/sales/notifications.js.map +2 -2
- package/dist/modules/staff/notifications.js +6 -0
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/notifications.js +2 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/notification/index.ts +3 -0
- package/generated/entities/notification_preference/index.ts +8 -0
- package/generated/entities/notification_type/index.ts +9 -0
- package/generated/entities/notification_type_override/index.ts +7 -0
- package/generated/entities/push_notification_delivery/index.ts +19 -0
- package/generated/entities/user_device/index.ts +16 -0
- package/generated/entities.ids.generated.ts +12 -1
- package/generated/entity-fields-registry.ts +72 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +2 -1
- package/src/helpers/integration/appRoot.ts +13 -0
- package/src/helpers/integration/dbFixtures.ts +1 -5
- package/src/helpers/integration/pushFake.ts +219 -0
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/ko.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/auth/notifications.ts +16 -0
- package/src/modules/business_rules/i18n/de.json +2 -1
- package/src/modules/business_rules/i18n/en.json +2 -1
- package/src/modules/business_rules/i18n/es.json +2 -1
- package/src/modules/business_rules/i18n/ko.json +2 -1
- package/src/modules/business_rules/i18n/pl.json +2 -1
- package/src/modules/business_rules/notifications.ts +2 -0
- package/src/modules/catalog/i18n/de.json +2 -1
- package/src/modules/catalog/i18n/en.json +2 -1
- package/src/modules/catalog/i18n/es.json +2 -1
- package/src/modules/catalog/i18n/ko.json +2 -1
- package/src/modules/catalog/i18n/pl.json +2 -1
- package/src/modules/catalog/notifications.ts +2 -0
- package/src/modules/communication_channels/acl.ts +9 -0
- package/src/modules/communication_channels/api/delete/admin/channels/[id]/route.ts +168 -0
- package/src/modules/communication_channels/api/get/channels/[id]/health/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/[id]/route.ts +6 -2
- package/src/modules/communication_channels/api/get/channels/route.ts +2 -1
- package/src/modules/communication_channels/api/post/channels/[id]/test-send/route.ts +16 -5
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +37 -1
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +175 -0
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +11 -1
- package/src/modules/communication_channels/commands/admin-delete-channel.ts +203 -0
- package/src/modules/communication_channels/commands/connect-credential-channel.ts +63 -15
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +10 -2
- package/src/modules/communication_channels/commands/push-register.ts +7 -1
- package/src/modules/communication_channels/commands/push-unregister.ts +14 -3
- package/src/modules/communication_channels/data/entities.ts +10 -0
- package/src/modules/communication_channels/data/validators.ts +1 -0
- package/src/modules/communication_channels/i18n/de.json +35 -1
- package/src/modules/communication_channels/i18n/en.json +35 -1
- package/src/modules/communication_channels/i18n/es.json +35 -1
- package/src/modules/communication_channels/i18n/ko.json +35 -1
- package/src/modules/communication_channels/i18n/pl.json +35 -1
- package/src/modules/communication_channels/lib/access-control.ts +42 -0
- package/src/modules/communication_channels/lib/adapter.ts +25 -0
- package/src/modules/communication_channels/lib/connect-channel.ts +61 -26
- package/src/modules/communication_channels/lib/push-adapter.ts +139 -0
- package/src/modules/communication_channels/lib/push-capabilities.ts +42 -0
- package/src/modules/communication_channels/lib/push-connect-error.ts +52 -0
- package/src/modules/communication_channels/lib/push-credential-errors.ts +58 -0
- package/src/modules/communication_channels/lib/push-envelope.ts +104 -0
- package/src/modules/communication_channels/lib/refcounted-client-cache.ts +123 -0
- package/src/modules/communication_channels/migrations/.snapshot-open-mercato.json +9 -0
- package/src/modules/communication_channels/migrations/Migration20260703123630_communication_channels.ts +20 -0
- package/src/modules/communication_channels/notifications.ts +4 -0
- package/src/modules/communication_channels/setup.ts +2 -0
- package/src/modules/configs/i18n/de.json +5 -1
- package/src/modules/configs/i18n/en.json +5 -1
- package/src/modules/configs/i18n/es.json +5 -1
- package/src/modules/configs/i18n/ko.json +5 -1
- package/src/modules/configs/i18n/pl.json +5 -1
- package/src/modules/configs/lib/upgrade-actions.ts +21 -0
- package/src/modules/customer_accounts/i18n/de.json +2 -1
- package/src/modules/customer_accounts/i18n/en.json +2 -1
- package/src/modules/customer_accounts/i18n/es.json +2 -1
- package/src/modules/customer_accounts/i18n/ko.json +2 -1
- package/src/modules/customer_accounts/i18n/pl.json +2 -1
- package/src/modules/customer_accounts/notifications.ts +12 -0
- package/src/modules/customers/i18n/de.json +2 -1
- package/src/modules/customers/i18n/en.json +2 -1
- package/src/modules/customers/i18n/es.json +2 -1
- package/src/modules/customers/i18n/ko.json +2 -1
- package/src/modules/customers/i18n/pl.json +2 -1
- package/src/modules/customers/notifications.ts +4 -0
- package/src/modules/devices/AGENTS.md +101 -0
- package/src/modules/devices/acl.ts +7 -0
- package/src/modules/devices/api/[id]/route.ts +180 -0
- package/src/modules/devices/api/admin/devices/[id]/route.ts +235 -0
- package/src/modules/devices/api/admin/devices/route.ts +160 -0
- package/src/modules/devices/api/auth.ts +13 -0
- package/src/modules/devices/api/deviceList.ts +54 -0
- package/src/modules/devices/api/deviceOps.ts +186 -0
- package/src/modules/devices/api/openapi.ts +23 -0
- package/src/modules/devices/api/route.ts +165 -0
- package/src/modules/devices/backend/devices/[id]/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/[id]/page.tsx +164 -0
- package/src/modules/devices/backend/devices/create/page.meta.ts +12 -0
- package/src/modules/devices/backend/devices/create/page.tsx +81 -0
- package/src/modules/devices/backend/devices/page.meta.ts +21 -0
- package/src/modules/devices/backend/devices/page.tsx +259 -0
- package/src/modules/devices/commands/deactivate.ts +126 -0
- package/src/modules/devices/commands/index.ts +3 -0
- package/src/modules/devices/commands/register.ts +199 -0
- package/src/modules/devices/commands/shared.ts +119 -0
- package/src/modules/devices/commands/update.ts +142 -0
- package/src/modules/devices/data/entities.ts +79 -0
- package/src/modules/devices/data/validators.ts +65 -0
- package/src/modules/devices/di.ts +9 -0
- package/src/modules/devices/encryption.ts +15 -0
- package/src/modules/devices/events.ts +16 -0
- package/src/modules/devices/i18n/de.json +45 -0
- package/src/modules/devices/i18n/en.json +45 -0
- package/src/modules/devices/i18n/es.json +45 -0
- package/src/modules/devices/i18n/ko.json +45 -0
- package/src/modules/devices/i18n/pl.json +45 -0
- package/src/modules/devices/index.ts +15 -0
- package/src/modules/devices/lib/operationMetadata.ts +35 -0
- package/src/modules/devices/migrations/.snapshot-open-mercato.json +200 -0
- package/src/modules/devices/migrations/Migration20260602120000.ts +15 -0
- package/src/modules/devices/migrations/Migration20260630120000.ts +20 -0
- package/src/modules/devices/migrations/Migration20260701120000.ts +13 -0
- package/src/modules/devices/migrations/Migration20260721120000.ts +18 -0
- package/src/modules/devices/setup.ts +11 -0
- package/src/modules/entities/cli.ts +16 -2
- package/src/modules/entities/migrations/Migration20260722120000.ts +38 -0
- package/src/modules/inbox_ops/i18n/de.json +2 -1
- package/src/modules/inbox_ops/i18n/en.json +2 -1
- package/src/modules/inbox_ops/i18n/es.json +2 -1
- package/src/modules/inbox_ops/i18n/ko.json +2 -1
- package/src/modules/inbox_ops/i18n/pl.json +2 -1
- package/src/modules/inbox_ops/notifications.ts +2 -0
- package/src/modules/messages/i18n/de.json +2 -1
- package/src/modules/messages/i18n/en.json +2 -1
- package/src/modules/messages/i18n/es.json +2 -1
- package/src/modules/messages/i18n/ko.json +2 -1
- package/src/modules/messages/i18n/pl.json +2 -1
- package/src/modules/messages/notifications.ts +2 -0
- package/src/modules/notifications/AGENTS.md +54 -0
- package/src/modules/notifications/acl.ts +2 -0
- package/src/modules/notifications/api/admin/preferences/route.ts +193 -0
- package/src/modules/notifications/api/channels/route.ts +54 -0
- package/src/modules/notifications/api/preferences/route.ts +131 -0
- package/src/modules/notifications/api/route.ts +8 -1
- package/src/modules/notifications/api/types/[id]/channels/[channel]/route.ts +226 -0
- package/src/modules/notifications/api/types/route.ts +239 -0
- package/src/modules/notifications/api/unread-count/route.ts +5 -1
- package/src/modules/notifications/backend/notifications/user-preferences/page.meta.ts +26 -0
- package/src/modules/notifications/backend/notifications/user-preferences/page.tsx +12 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +31 -0
- package/src/modules/notifications/backend/profile/notification-preferences/page.tsx +12 -0
- package/src/modules/notifications/data/entities.ts +144 -0
- package/src/modules/notifications/data/validators.ts +116 -0
- package/src/modules/notifications/di.ts +7 -0
- package/src/modules/notifications/events.ts +23 -0
- package/src/modules/notifications/frontend/NotificationPreferenceMatrix.tsx +287 -0
- package/src/modules/notifications/frontend/NotificationPreferencesPageClient.tsx +155 -0
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +273 -2
- package/src/modules/notifications/frontend/NotificationUserPreferencesAdminPageClient.tsx +214 -0
- package/src/modules/notifications/generators.ts +82 -0
- package/src/modules/notifications/i18n/de.json +43 -0
- package/src/modules/notifications/i18n/en.json +43 -0
- package/src/modules/notifications/i18n/es.json +43 -0
- package/src/modules/notifications/i18n/ko.json +43 -0
- package/src/modules/notifications/i18n/pl.json +44 -1
- package/src/modules/notifications/lib/delivery-strategies-registry.ts +30 -0
- package/src/modules/notifications/lib/deliveryStrategies.ts +40 -4
- package/src/modules/notifications/lib/derive-category.ts +15 -0
- package/src/modules/notifications/lib/notification-channel-registry.ts +59 -0
- package/src/modules/notifications/lib/notification-type-registry.ts +182 -0
- package/src/modules/notifications/lib/notificationCopy.ts +34 -0
- package/src/modules/notifications/lib/notificationFactory.ts +5 -1
- package/src/modules/notifications/lib/notificationMapper.ts +2 -0
- package/src/modules/notifications/lib/notificationPreferenceService.ts +138 -0
- package/src/modules/notifications/lib/notificationService.ts +129 -25
- package/src/modules/notifications/lib/notificationVisibility.ts +48 -0
- package/src/modules/notifications/lib/routeHelpers.ts +14 -1
- package/src/modules/notifications/lib/shouldDeliver.ts +112 -0
- package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +63 -0
- package/src/modules/notifications/lib/strategies/in-app-delivery-strategy.ts +21 -0
- package/src/modules/notifications/lib/typeCatalogue.ts +141 -0
- package/src/modules/notifications/lib/typeChannelSettings.ts +29 -0
- package/src/modules/notifications/lib/typeOverrides.ts +46 -0
- package/src/modules/notifications/migrations/.snapshot-open-mercato.json +874 -100
- package/src/modules/notifications/migrations/Migration20260625122947_notifications.ts +14 -0
- package/src/modules/notifications/migrations/Migration20260626120500_notifications_push_payload.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260626130000_notifications_non_opt_out.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260626140000_notifications_type_category_silent.ts +15 -0
- package/src/modules/notifications/migrations/Migration20260706120000_notifications_channels.ts +13 -0
- package/src/modules/notifications/migrations/Migration20260716123121_notifications.ts +14 -0
- package/src/modules/notifications/notification-channels.ts +30 -0
- package/src/modules/notifications/notifications.delivery-strategies.ts +15 -0
- package/src/modules/notifications/setup.ts +23 -1
- package/src/modules/notifications/subscribers/deliver-notification.ts +105 -71
- package/src/modules/notifications/subscribers/sync-notification-types.ts +18 -0
- package/src/modules/notifications/widgets/injection/profile-preferences-menu/widget.ts +35 -0
- package/src/modules/notifications/widgets/injection-table.ts +18 -0
- package/src/modules/push_notifications/AGENTS.md +92 -0
- package/src/modules/push_notifications/acl.ts +9 -0
- package/src/modules/push_notifications/api/custom-send/route.ts +175 -0
- package/src/modules/push_notifications/api/deliveries/[id]/route.ts +122 -0
- package/src/modules/push_notifications/api/deliveries/route.ts +58 -0
- package/src/modules/push_notifications/api/openapi.ts +22 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.meta.ts +9 -0
- package/src/modules/push_notifications/backend/push_notifications/[id]/page.tsx +149 -0
- package/src/modules/push_notifications/backend/push_notifications/page.meta.ts +21 -0
- package/src/modules/push_notifications/backend/push_notifications/page.tsx +239 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.meta.ts +24 -0
- package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +327 -0
- package/src/modules/push_notifications/data/entities.ts +111 -0
- package/src/modules/push_notifications/data/extensions.ts +29 -0
- package/src/modules/push_notifications/data/validators.ts +102 -0
- package/src/modules/push_notifications/di.ts +20 -0
- package/src/modules/push_notifications/events.ts +21 -0
- package/src/modules/push_notifications/i18n/de.json +79 -0
- package/src/modules/push_notifications/i18n/en.json +79 -0
- package/src/modules/push_notifications/i18n/es.json +79 -0
- package/src/modules/push_notifications/i18n/ko.json +79 -0
- package/src/modules/push_notifications/i18n/pl.json +79 -0
- package/src/modules/push_notifications/index.ts +15 -0
- package/src/modules/push_notifications/lib/fake-provider-recorder.ts +154 -0
- package/src/modules/push_notifications/lib/push-delivery-strategy.ts +90 -0
- package/src/modules/push_notifications/lib/push-delivery.ts +455 -0
- package/src/modules/push_notifications/lib/push-fanout.ts +237 -0
- package/src/modules/push_notifications/lib/push-health.ts +47 -0
- package/src/modules/push_notifications/lib/push-reaper.ts +140 -0
- package/src/modules/push_notifications/lib/push-receipt-reaper.ts +210 -0
- package/src/modules/push_notifications/lib/push-stub-adapter.ts +80 -0
- package/src/modules/push_notifications/lib/queue.ts +74 -0
- package/src/modules/push_notifications/lib/reclaim-window.ts +16 -0
- package/src/modules/push_notifications/lib/send-custom-push.ts +100 -0
- package/src/modules/push_notifications/migrations/.snapshot-open-mercato.json +386 -0
- package/src/modules/push_notifications/migrations/Migration20260625150049_push_notifications.ts +16 -0
- package/src/modules/push_notifications/migrations/Migration20260626120000_push_notifications_silent.ts +13 -0
- package/src/modules/push_notifications/notifications.delivery-strategies.ts +8 -0
- package/src/modules/push_notifications/notifications.ts +50 -0
- package/src/modules/push_notifications/setup.ts +92 -0
- package/src/modules/push_notifications/workers/reclaim-stuck.worker.ts +78 -0
- package/src/modules/push_notifications/workers/send-push.worker.ts +34 -0
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/sales/notifications.ts +8 -0
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/ko.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/notifications.ts +6 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/notifications.ts +2 -0
package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
6
|
+
import {
|
|
7
|
+
COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID
|
|
8
|
+
} from "../../../../../commands/connect-credential-channel.js";
|
|
9
|
+
import { validateRouteMutationGuard } from "../../../../../lib/route-mutation-guard.js";
|
|
10
|
+
const metadata = {
|
|
11
|
+
path: "/communication_channels/channels/connect/tenant-credentials",
|
|
12
|
+
POST: {
|
|
13
|
+
requireAuth: true,
|
|
14
|
+
requireFeatures: ["communication_channels.connect_tenant_channel"]
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const bodySchema = z.object({
|
|
18
|
+
providerKey: z.string().min(1).max(64),
|
|
19
|
+
displayName: z.string().min(1).max(255),
|
|
20
|
+
credentials: z.record(z.string(), z.unknown()),
|
|
21
|
+
pollIntervalSeconds: z.number().int().positive().max(86400).optional()
|
|
22
|
+
});
|
|
23
|
+
async function POST(req) {
|
|
24
|
+
const auth = await getAuthFromRequest(req);
|
|
25
|
+
if (!auth?.sub || !auth?.tenantId) {
|
|
26
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
27
|
+
}
|
|
28
|
+
let body;
|
|
29
|
+
try {
|
|
30
|
+
const json = await readJsonSafe(req, null);
|
|
31
|
+
body = bodySchema.parse(json);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return NextResponse.json(
|
|
34
|
+
{ error: err instanceof Error ? err.message : "Invalid request body" },
|
|
35
|
+
{ status: 422 }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const container = await createRequestContainer();
|
|
39
|
+
const adapterRegistry = container.resolve("channelAdapterRegistry");
|
|
40
|
+
const adapter = adapterRegistry.get(body.providerKey);
|
|
41
|
+
if (!adapter) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: `No adapter registered for provider '${body.providerKey}'` },
|
|
44
|
+
{ status: 404 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (adapter.channelScope !== "tenant") {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{
|
|
50
|
+
error: "This provider connects per-user channels; use the personal channel connect flow instead.",
|
|
51
|
+
code: "provider_not_tenant_scoped"
|
|
52
|
+
},
|
|
53
|
+
{ status: 400 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const guard = await validateRouteMutationGuard({
|
|
57
|
+
container,
|
|
58
|
+
req,
|
|
59
|
+
auth,
|
|
60
|
+
input: {
|
|
61
|
+
resourceKind: "communication_channels.channel",
|
|
62
|
+
resourceId: `new:${body.providerKey}`,
|
|
63
|
+
operation: "create",
|
|
64
|
+
mutationPayload: {
|
|
65
|
+
providerKey: body.providerKey,
|
|
66
|
+
displayName: body.displayName,
|
|
67
|
+
pollIntervalSeconds: body.pollIntervalSeconds
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if ("response" in guard) return guard.response;
|
|
72
|
+
const commandBus = container.resolve("commandBus");
|
|
73
|
+
const orgId = auth.orgId ?? null;
|
|
74
|
+
const input = {
|
|
75
|
+
providerKey: body.providerKey,
|
|
76
|
+
displayName: body.displayName,
|
|
77
|
+
credentials: body.credentials,
|
|
78
|
+
pollIntervalSeconds: body.pollIntervalSeconds,
|
|
79
|
+
// Tenant-wide: no owning user. The command re-derives the effective scope
|
|
80
|
+
// from the adapter, so the channel + credentials are stored with user_id NULL.
|
|
81
|
+
userId: null,
|
|
82
|
+
scope: {
|
|
83
|
+
tenantId: auth.tenantId,
|
|
84
|
+
organizationId: orgId
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const { result } = await commandBus.execute(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {
|
|
88
|
+
input,
|
|
89
|
+
ctx: {
|
|
90
|
+
container,
|
|
91
|
+
auth,
|
|
92
|
+
organizationScope: null,
|
|
93
|
+
selectedOrganizationId: orgId,
|
|
94
|
+
organizationIds: orgId ? [orgId] : null
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (result.status === "no_adapter") {
|
|
98
|
+
return NextResponse.json({ error: result.reason }, { status: 404 });
|
|
99
|
+
}
|
|
100
|
+
if (result.status === "wrong_scope_for_route") {
|
|
101
|
+
return NextResponse.json(
|
|
102
|
+
{ error: "Unexpected per-user scope on the tenant connect route.", code: "wrong_scope_for_route" },
|
|
103
|
+
{ status: 500 }
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (result.status === "validation_failed") {
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{
|
|
109
|
+
error: "Credential validation failed",
|
|
110
|
+
fieldErrors: result.errors,
|
|
111
|
+
...result.errorCodes ? { fieldErrorCodes: result.errorCodes } : {}
|
|
112
|
+
},
|
|
113
|
+
{ status: 422 }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (result.status === "duplicate_mailbox") {
|
|
117
|
+
return NextResponse.json(
|
|
118
|
+
{
|
|
119
|
+
error: `This mailbox is already connected via "${result.existingProviderKey}". Disconnect it there before connecting it again with a different provider.`,
|
|
120
|
+
code: "mailbox_already_connected"
|
|
121
|
+
},
|
|
122
|
+
{ status: 409 }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
await guard.afterSuccess();
|
|
126
|
+
return NextResponse.json(
|
|
127
|
+
{
|
|
128
|
+
channelId: result.channelId,
|
|
129
|
+
externalIdentifier: result.externalIdentifier
|
|
130
|
+
},
|
|
131
|
+
{ status: 201 }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const openApi = {
|
|
135
|
+
tags: ["CommunicationChannels"],
|
|
136
|
+
methods: {
|
|
137
|
+
POST: {
|
|
138
|
+
summary: "Connect a tenant-wide credential-based channel (push: FCM/APNs/Expo)",
|
|
139
|
+
tags: ["CommunicationChannels"],
|
|
140
|
+
responses: [
|
|
141
|
+
{ status: 201, description: "Channel connected" },
|
|
142
|
+
{ status: 400, description: "Provider is not tenant-scoped" },
|
|
143
|
+
{ status: 401, description: "Unauthorized" },
|
|
144
|
+
{ status: 404, description: "Unknown provider" },
|
|
145
|
+
{ status: 422, description: "Invalid body or credential validation failed" }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var route_default = POST;
|
|
151
|
+
export {
|
|
152
|
+
POST,
|
|
153
|
+
route_default as default,
|
|
154
|
+
metadata,
|
|
155
|
+
openApi
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport {\n COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n type ConnectCredentialChannelInput,\n type ConnectCredentialChannelResult,\n} from '../../../../../commands/connect-credential-channel'\nimport type { ChannelAdapterRegistry } from '../../../../../lib/registry'\nimport { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'\n\nexport const metadata = {\n path: '/communication_channels/channels/connect/tenant-credentials',\n POST: {\n requireAuth: true,\n requireFeatures: ['communication_channels.connect_tenant_channel'],\n },\n}\n\nconst bodySchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n credentials: z.record(z.string(), z.unknown()),\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n})\n\nexport async function POST(req: Request): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: z.infer<typeof bodySchema>\n try {\n const json = await readJsonSafe(req, null)\n body = bodySchema.parse(json)\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'Invalid request body' },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n\n // Reject providers that are NOT tenant-scoped so an admin can't force a\n // per-user provider (Gmail/IMAP) into a shared tenant-wide channel. Push\n // providers (FCM/APNs/Expo) declare `channelScope: 'tenant'` on their adapter.\n const adapterRegistry = container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const adapter = adapterRegistry.get(body.providerKey)\n if (!adapter) {\n return NextResponse.json(\n { error: `No adapter registered for provider '${body.providerKey}'` },\n { status: 404 },\n )\n }\n if (adapter.channelScope !== 'tenant') {\n return NextResponse.json(\n {\n error: 'This provider connects per-user channels; use the personal channel connect flow instead.',\n code: 'provider_not_tenant_scoped',\n },\n { status: 400 },\n )\n }\n\n const guard = await validateRouteMutationGuard({\n container,\n req,\n auth,\n input: {\n resourceKind: 'communication_channels.channel',\n resourceId: `new:${body.providerKey}`,\n operation: 'create',\n mutationPayload: {\n providerKey: body.providerKey,\n displayName: body.displayName,\n pollIntervalSeconds: body.pollIntervalSeconds,\n },\n },\n })\n if ('response' in guard) return guard.response\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const orgId = (auth as { orgId?: string | null }).orgId ?? null\n\n const input: ConnectCredentialChannelInput = {\n providerKey: body.providerKey,\n displayName: body.displayName,\n credentials: body.credentials,\n pollIntervalSeconds: body.pollIntervalSeconds,\n // Tenant-wide: no owning user. The command re-derives the effective scope\n // from the adapter, so the channel + credentials are stored with user_id NULL.\n userId: null,\n scope: {\n tenantId: auth.tenantId as string,\n organizationId: orgId,\n },\n }\n const { result } = await commandBus.execute<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n >(COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID, {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: orgId,\n organizationIds: orgId ? [orgId] : null,\n },\n })\n\n if (result.status === 'no_adapter') {\n return NextResponse.json({ error: result.reason }, { status: 404 })\n }\n if (result.status === 'wrong_scope_for_route') {\n // Unreachable in practice \u2014 this route always dispatches with userId: null \u2014\n // but handle it so the result union narrows and the invariant is explicit.\n return NextResponse.json(\n { error: 'Unexpected per-user scope on the tenant connect route.', code: 'wrong_scope_for_route' },\n { status: 500 },\n )\n }\n if (result.status === 'validation_failed') {\n // `fieldErrors` stays English for API consumers and logs; `fieldErrorCodes`\n // lets the connect widget render a localized message per field.\n return NextResponse.json(\n {\n error: 'Credential validation failed',\n fieldErrors: result.errors,\n ...(result.errorCodes ? { fieldErrorCodes: result.errorCodes } : {}),\n },\n { status: 422 },\n )\n }\n if (result.status === 'duplicate_mailbox') {\n return NextResponse.json(\n {\n error: `This mailbox is already connected via \"${result.existingProviderKey}\". Disconnect it there before connecting it again with a different provider.`,\n code: 'mailbox_already_connected',\n },\n { status: 409 },\n )\n }\n await guard.afterSuccess()\n return NextResponse.json(\n {\n channelId: result.channelId,\n externalIdentifier: result.externalIdentifier,\n },\n { status: 201 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary: 'Connect a tenant-wide credential-based channel (push: FCM/APNs/Expo)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 201, description: 'Channel connected' },\n { status: 400, description: 'Provider is not tenant-scoped' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Unknown provider' },\n { status: 422, description: 'Invalid body or credential validation failed' },\n ],\n },\n },\n}\nexport default POST\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -15,7 +15,13 @@ function ChannelsListPage() {
|
|
|
15
15
|
const [page, setPage] = React.useState(1);
|
|
16
16
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
17
17
|
const [errorMessage, setErrorMessage] = React.useState(null);
|
|
18
|
+
const [reloadToken, setReloadToken] = React.useState(0);
|
|
18
19
|
const pageSize = 50;
|
|
20
|
+
const reload = React.useCallback(() => setReloadToken((token) => token + 1), []);
|
|
21
|
+
const injectionContext = React.useMemo(
|
|
22
|
+
() => ({ tableId: extensionPoints.hosts.channelsTable.tableId, reload }),
|
|
23
|
+
[reload]
|
|
24
|
+
);
|
|
19
25
|
React.useEffect(() => {
|
|
20
26
|
let cancelled = false;
|
|
21
27
|
async function load() {
|
|
@@ -50,7 +56,7 @@ function ChannelsListPage() {
|
|
|
50
56
|
return () => {
|
|
51
57
|
cancelled = true;
|
|
52
58
|
};
|
|
53
|
-
}, [page, t]);
|
|
59
|
+
}, [page, reloadToken, t]);
|
|
54
60
|
const columns = React.useMemo(
|
|
55
61
|
() => [
|
|
56
62
|
{
|
|
@@ -84,6 +90,7 @@ function ChannelsListPage() {
|
|
|
84
90
|
{
|
|
85
91
|
title: t("communication_channels.nav.title", "Communication Channels"),
|
|
86
92
|
extensionTableId: extensionPoints.hosts.channelsTable.tableId,
|
|
93
|
+
injectionContext,
|
|
87
94
|
columns,
|
|
88
95
|
data: rows,
|
|
89
96
|
isLoading,
|
package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/communication_channels/backend/communication_channels/channels/page.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/communication_channels/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype ChannelRow = {\n id: string\n providerKey: string\n channelType: string\n displayName: string\n externalIdentifier: string | null\n isActive: boolean\n capabilities: Record<string, unknown> | null\n createdAt: string | null\n}\n\ntype ChannelListResponse = {\n items?: ChannelRow[]\n total?: number\n totalPages?: number\n}\n\nexport default function ChannelsListPage() {\n const t = useT()\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPage] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const pageSize = 50\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setErrorMessage(null)\n const response = await apiCall<ChannelListResponse>(\n `/api/communication_channels/channels?page=${page}&pageSize=${pageSize}`,\n ).catch((err: unknown) => {\n return {\n ok: false,\n result: { error: err instanceof Error ? err.message : 'Failed to load channels' },\n }\n })\n if (cancelled) return\n if (!response.ok) {\n const errBody = response.result as { error?: string } | undefined\n setErrorMessage(\n errBody?.error ?? t('communication_channels.errors.loadList', 'Failed to load channels'),\n )\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } else {\n const data = (response.result ?? {}) as ChannelListResponse\n setRows(Array.isArray(data.items) ? data.items : [])\n setTotal(typeof data.total === 'number' ? data.total : 0)\n setTotalPages(typeof data.totalPages === 'number' ? data.totalPages : 1)\n }\n setIsLoading(false)\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [page, t])\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(\n () => [\n {\n header: t('communication_channels.columns.displayName', 'Channel'),\n accessorKey: 'displayName',\n },\n {\n header: t('communication_channels.columns.provider', 'Provider'),\n accessorKey: 'providerKey',\n },\n {\n header: t('communication_channels.columns.type', 'Type'),\n accessorKey: 'channelType',\n },\n {\n header: t('communication_channels.columns.identifier', 'External ID'),\n accessorKey: 'externalIdentifier',\n cell: ({ row }) => row.original.externalIdentifier ?? '\u2014',\n meta: { truncate: true, maxWidth: 240 },\n },\n {\n header: t('communication_channels.columns.status', 'Status'),\n accessorKey: 'isActive',\n cell: ({ row }) =>\n row.original.isActive ? (\n <Tag variant=\"success\" dot>\n {t('communication_channels.status.active', 'Active')}\n </Tag>\n ) : (\n <Tag variant=\"neutral\">\n {t('communication_channels.status.inactive', 'Inactive')}\n </Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ChannelRow>\n title={t('communication_channels.nav.title', 'Communication Channels')}\n extensionTableId={extensionPoints.hosts.channelsTable.tableId}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n error={errorMessage}\n emptyState={t(\n 'communication_channels.emptyState',\n 'No shared channels yet. This page lists shared, tenant-wide channels. Your personal email mailbox is private \u2014 connect and manage it under Profile \u2192 Communication Channels.',\n )}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/communication_channels/extension-points'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype ChannelRow = {\n id: string\n providerKey: string\n channelType: string\n displayName: string\n externalIdentifier: string | null\n isActive: boolean\n capabilities: Record<string, unknown> | null\n createdAt: string | null\n}\n\ntype ChannelListResponse = {\n items?: ChannelRow[]\n total?: number\n totalPages?: number\n}\n\nexport default function ChannelsListPage() {\n const t = useT()\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPage] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n // Bumped by injected connect widgets (FCM/APNs/Expo) so a freshly connected\n // tenant-wide channel appears without a manual page reload.\n const [reloadToken, setReloadToken] = React.useState(0)\n const pageSize = 50\n\n const reload = React.useCallback(() => setReloadToken((token) => token + 1), [])\n const injectionContext = React.useMemo(\n () => ({ tableId: extensionPoints.hosts.channelsTable.tableId, reload }),\n [reload],\n )\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setErrorMessage(null)\n const response = await apiCall<ChannelListResponse>(\n `/api/communication_channels/channels?page=${page}&pageSize=${pageSize}`,\n ).catch((err: unknown) => {\n return {\n ok: false,\n result: { error: err instanceof Error ? err.message : 'Failed to load channels' },\n }\n })\n if (cancelled) return\n if (!response.ok) {\n const errBody = response.result as { error?: string } | undefined\n setErrorMessage(\n errBody?.error ?? t('communication_channels.errors.loadList', 'Failed to load channels'),\n )\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } else {\n const data = (response.result ?? {}) as ChannelListResponse\n setRows(Array.isArray(data.items) ? data.items : [])\n setTotal(typeof data.total === 'number' ? data.total : 0)\n setTotalPages(typeof data.totalPages === 'number' ? data.totalPages : 1)\n }\n setIsLoading(false)\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [page, reloadToken, t])\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(\n () => [\n {\n header: t('communication_channels.columns.displayName', 'Channel'),\n accessorKey: 'displayName',\n },\n {\n header: t('communication_channels.columns.provider', 'Provider'),\n accessorKey: 'providerKey',\n },\n {\n header: t('communication_channels.columns.type', 'Type'),\n accessorKey: 'channelType',\n },\n {\n header: t('communication_channels.columns.identifier', 'External ID'),\n accessorKey: 'externalIdentifier',\n cell: ({ row }) => row.original.externalIdentifier ?? '\u2014',\n meta: { truncate: true, maxWidth: 240 },\n },\n {\n header: t('communication_channels.columns.status', 'Status'),\n accessorKey: 'isActive',\n cell: ({ row }) =>\n row.original.isActive ? (\n <Tag variant=\"success\" dot>\n {t('communication_channels.status.active', 'Active')}\n </Tag>\n ) : (\n <Tag variant=\"neutral\">\n {t('communication_channels.status.inactive', 'Inactive')}\n </Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ChannelRow>\n title={t('communication_channels.nav.title', 'Communication Channels')}\n extensionTableId={extensionPoints.hosts.channelsTable.tableId}\n injectionContext={injectionContext}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n error={errorMessage}\n emptyState={t(\n 'communication_channels.emptyState',\n 'No shared channels yet. This page lists shared, tenant-wide channels. Your personal email mailbox is private \u2014 connect and manage it under Profile \u2192 Communication Channels.',\n )}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
3
|
+
import { extractUndoPayload as extractSharedUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
4
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { CommunicationChannel } from "../data/entities.js";
|
|
7
|
+
import { channelOrgScopeWhere } from "../lib/access-control.js";
|
|
8
|
+
import { pushUnregister } from "./push-unregister.js";
|
|
9
|
+
import { emitCommunicationChannelsEvent } from "../events.js";
|
|
10
|
+
const logger = createLogger("communication_channels");
|
|
11
|
+
const adminDeleteChannelSchema = z.object({
|
|
12
|
+
channelId: z.string().uuid(),
|
|
13
|
+
scope: z.object({
|
|
14
|
+
tenantId: z.string().uuid(),
|
|
15
|
+
organizationId: z.string().uuid().nullable()
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
const COMMUNICATION_CHANNELS_ADMIN_DELETE_CHANNEL_COMMAND_ID = "communication_channels.channel.admin_delete";
|
|
19
|
+
const adminDeleteChannelCommand = {
|
|
20
|
+
id: COMMUNICATION_CHANNELS_ADMIN_DELETE_CHANNEL_COMMAND_ID,
|
|
21
|
+
// Explicitly undoable (the bus also infers this from `undo` below, but
|
|
22
|
+
// declaring it keeps undoability from silently dropping under a refactor).
|
|
23
|
+
isUndoable: true,
|
|
24
|
+
async execute(rawInput, ctx) {
|
|
25
|
+
const input = adminDeleteChannelSchema.parse(rawInput);
|
|
26
|
+
const em = ctx.container.resolve("em").fork();
|
|
27
|
+
const dscope = {
|
|
28
|
+
tenantId: input.scope.tenantId,
|
|
29
|
+
organizationId: input.scope.organizationId ?? null
|
|
30
|
+
};
|
|
31
|
+
const channel = await findOneWithDecryption(
|
|
32
|
+
em,
|
|
33
|
+
CommunicationChannel,
|
|
34
|
+
{
|
|
35
|
+
id: input.channelId,
|
|
36
|
+
tenantId: input.scope.tenantId,
|
|
37
|
+
...channelOrgScopeWhere(input.scope.organizationId),
|
|
38
|
+
deletedAt: null
|
|
39
|
+
},
|
|
40
|
+
void 0,
|
|
41
|
+
dscope
|
|
42
|
+
);
|
|
43
|
+
if (!channel) {
|
|
44
|
+
return { status: "noop", reason: "channel not found" };
|
|
45
|
+
}
|
|
46
|
+
if (channel.userId != null) {
|
|
47
|
+
return {
|
|
48
|
+
status: "not_tenant_wide",
|
|
49
|
+
reason: "channel is a per-user channel; use the owner delete path"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (channel.credentialsRef) {
|
|
53
|
+
const channelOrganizationId = channel.organizationId ?? input.scope.tenantId;
|
|
54
|
+
try {
|
|
55
|
+
await pushUnregister({
|
|
56
|
+
container: ctx.container,
|
|
57
|
+
scope: {
|
|
58
|
+
tenantId: input.scope.tenantId,
|
|
59
|
+
organizationId: channelOrganizationId,
|
|
60
|
+
userId: channel.userId ?? null
|
|
61
|
+
},
|
|
62
|
+
input: { channelId: channel.id }
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.warn("admin-delete-channel push unregister failed", { channelId: channel.id, err });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
channel.deletedAt = /* @__PURE__ */ new Date();
|
|
69
|
+
channel.isPrimary = false;
|
|
70
|
+
await em.flush();
|
|
71
|
+
await emitCommunicationChannelsEvent(
|
|
72
|
+
"communication_channels.channel.deleted",
|
|
73
|
+
{
|
|
74
|
+
channelId: channel.id,
|
|
75
|
+
userId: channel.userId ?? null,
|
|
76
|
+
providerKey: channel.providerKey,
|
|
77
|
+
channelType: channel.channelType,
|
|
78
|
+
tenantId: input.scope.tenantId,
|
|
79
|
+
organizationId: channel.organizationId ?? null
|
|
80
|
+
},
|
|
81
|
+
{ persistent: true }
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
status: "deleted",
|
|
85
|
+
channelId: channel.id,
|
|
86
|
+
undo: { channelId: channel.id, tenantId: channel.tenantId }
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
// Persist the undo snapshot into the action log. Without this, the command bus
|
|
90
|
+
// mints an undo token (so the UI offers "Undo") but the snapshot returned from
|
|
91
|
+
// execute() is never stored, and undo() (which clears deleted_at) would no-op.
|
|
92
|
+
async buildLog({ input, result }) {
|
|
93
|
+
if (result.status !== "deleted") return null;
|
|
94
|
+
return {
|
|
95
|
+
resourceKind: "communication_channels.channel",
|
|
96
|
+
resourceId: result.channelId,
|
|
97
|
+
tenantId: result.undo.tenantId ?? input.scope.tenantId,
|
|
98
|
+
organizationId: input.scope.organizationId ?? null,
|
|
99
|
+
payload: { undo: result.undo }
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async undo({ ctx, logEntry }) {
|
|
103
|
+
const snapshot = extractSnapshotFromLog(logEntry);
|
|
104
|
+
if (!snapshot || !snapshot.tenantId) return;
|
|
105
|
+
const em = ctx.container.resolve("em").fork();
|
|
106
|
+
const channel = await findOneWithDecryption(
|
|
107
|
+
em,
|
|
108
|
+
CommunicationChannel,
|
|
109
|
+
{ id: snapshot.channelId, tenantId: snapshot.tenantId },
|
|
110
|
+
void 0,
|
|
111
|
+
{ tenantId: snapshot.tenantId, organizationId: null }
|
|
112
|
+
);
|
|
113
|
+
if (!channel) return;
|
|
114
|
+
channel.deletedAt = null;
|
|
115
|
+
await em.flush();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function extractUndoPayload(value) {
|
|
119
|
+
if (!value || typeof value !== "object") return null;
|
|
120
|
+
const candidate = value.undo ?? value;
|
|
121
|
+
if (!candidate || typeof candidate !== "object") return null;
|
|
122
|
+
const obj = candidate;
|
|
123
|
+
if (typeof obj.channelId !== "string") return null;
|
|
124
|
+
return {
|
|
125
|
+
channelId: obj.channelId,
|
|
126
|
+
tenantId: typeof obj.tenantId === "string" ? obj.tenantId : void 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function extractSnapshotFromLog(logEntry) {
|
|
130
|
+
const undo = extractSharedUndoPayload(logEntry ?? null);
|
|
131
|
+
if (undo) return extractUndoPayload(undo);
|
|
132
|
+
return extractUndoPayload(logEntry);
|
|
133
|
+
}
|
|
134
|
+
registerCommand(adminDeleteChannelCommand);
|
|
135
|
+
var admin_delete_channel_default = adminDeleteChannelCommand;
|
|
136
|
+
export {
|
|
137
|
+
COMMUNICATION_CHANNELS_ADMIN_DELETE_CHANNEL_COMMAND_ID,
|
|
138
|
+
admin_delete_channel_default as default,
|
|
139
|
+
extractUndoPayload
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=admin-delete-channel.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/communication_channels/commands/admin-delete-channel.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload as extractSharedUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport { channelOrgScopeWhere } from '../lib/access-control'\nimport { pushUnregister } from './push-unregister'\nimport { emitCommunicationChannelsEvent } from '../events'\n\nconst logger = createLogger('communication_channels')\n\nconst adminDeleteChannelSchema = z.object({\n channelId: z.string().uuid(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type AdminDeleteChannelInput = z.infer<typeof adminDeleteChannelSchema>\n\nexport type AdminDeleteChannelResult =\n | { status: 'deleted'; channelId: string; undo: AdminDeleteChannelUndoSnapshot }\n | { status: 'noop'; reason: string }\n | { status: 'not_tenant_wide'; reason: string }\n\nexport interface AdminDeleteChannelUndoSnapshot {\n channelId: string\n // Optional only for back-compat with any pre-existing log entries; new\n // snapshots always set it so the undo lookup stays tenant-scoped.\n tenantId?: string\n}\n\nexport const COMMUNICATION_CHANNELS_ADMIN_DELETE_CHANNEL_COMMAND_ID =\n 'communication_channels.channel.admin_delete'\n\n/**\n * Admin soft-delete for a TENANT-WIDE channel (`user_id IS NULL`) \u2014 the\n * companion to `delete-channel.ts` (owner-only, per-user path). Tenant-wide\n * channels (shared inboxes and the push providers FCM/APNs/Expo) have no owner,\n * so the owner path rejects them as `not_owner`; this path deletes them under\n * the `communication_channels.admin` feature.\n *\n * The channel is loaded org-agnostically (tenant-wide push channels store\n * `organization_id IS NULL`, but shared inboxes may pin an org) via\n * `channelOrgScopeWhere`, so it resolves from any session org in the tenant.\n * Per-user channels are refused with `not_tenant_wide` \u2014 they MUST go through\n * the owner path so personal-mailbox privacy holds.\n *\n * Provider-side push delivery is torn down best-effort BEFORE the soft-delete,\n * keyed on the CHANNEL's own org (`channel.organizationId ?? tenantId`) so\n * credentials resolve where they were written (see push-register.ts /\n * connect-credential-channel.ts). Mirrors `delete-channel`'s teardown.\n *\n * Undoable: undo clears `deleted_at` to restore the row. Parity with\n * `delete-channel` \u2014 same event, payload, buildLog, and undo shape.\n */\nconst adminDeleteChannelCommand: CommandHandler<\n AdminDeleteChannelInput,\n AdminDeleteChannelResult\n> = {\n id: COMMUNICATION_CHANNELS_ADMIN_DELETE_CHANNEL_COMMAND_ID,\n // Explicitly undoable (the bus also infers this from `undo` below, but\n // declaring it keeps undoability from silently dropping under a refactor).\n isUndoable: true,\n async execute(rawInput, ctx) {\n const input = adminDeleteChannelSchema.parse(rawInput) as AdminDeleteChannelInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dscope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n ...channelOrgScopeWhere(input.scope.organizationId),\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n if (!channel) {\n return { status: 'noop', reason: 'channel not found' }\n }\n if (channel.userId != null) {\n return {\n status: 'not_tenant_wide',\n reason: 'channel is a per-user channel; use the owner delete path',\n }\n }\n\n // Tear down provider-side push delivery before the soft-delete, while\n // credentials are still resolvable. Keyed on the channel's OWN org (push\n // channels store credentials at `organization_id = tenantId`; shared inboxes\n // at their pinned org). Best-effort \u2014 failures are logged inside\n // pushUnregister and never re-raised.\n if (channel.credentialsRef) {\n const channelOrganizationId = channel.organizationId ?? input.scope.tenantId\n try {\n await pushUnregister({\n container: ctx.container,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: channelOrganizationId,\n userId: channel.userId ?? null,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n logger.warn('admin-delete-channel push unregister failed', { channelId: channel.id, err })\n }\n }\n\n channel.deletedAt = new Date()\n channel.isPrimary = false\n await em.flush()\n\n // Emit AFTER flush so subscribers observe the committed soft-delete. Same\n // event/payload shape as delete-channel so workflows/audit observe the full\n // channel lifecycle. Persistent so the delivery can retry on failure.\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.deleted',\n {\n channelId: channel.id,\n userId: channel.userId ?? null,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n tenantId: input.scope.tenantId,\n organizationId: channel.organizationId ?? null,\n },\n { persistent: true },\n )\n\n return {\n status: 'deleted',\n channelId: channel.id,\n undo: { channelId: channel.id, tenantId: channel.tenantId },\n }\n },\n // Persist the undo snapshot into the action log. Without this, the command bus\n // mints an undo token (so the UI offers \"Undo\") but the snapshot returned from\n // execute() is never stored, and undo() (which clears deleted_at) would no-op.\n async buildLog({ input, result }) {\n if (result.status !== 'deleted') return null\n return {\n resourceKind: 'communication_channels.channel',\n resourceId: result.channelId,\n tenantId: result.undo.tenantId ?? input.scope.tenantId,\n organizationId: input.scope.organizationId ?? null,\n payload: { undo: result.undo },\n }\n },\n async undo({ ctx, logEntry }) {\n const snapshot = extractSnapshotFromLog(logEntry)\n if (!snapshot || !snapshot.tenantId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n // Re-find WITHOUT the `deletedAt: null` filter \u2014 the row we are restoring is\n // soft-deleted. Tenant-scoped to avoid cross-tenant resolution by bare id.\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n { id: snapshot.channelId, tenantId: snapshot.tenantId },\n undefined,\n { tenantId: snapshot.tenantId, organizationId: null },\n )\n if (!channel) return\n channel.deletedAt = null\n await em.flush()\n },\n}\n\n/**\n * Defensive shape-validator for the delete snapshot \u2014 mirrors the helper in\n * `delete-channel.ts` so tests can construct snapshots directly.\n */\nexport function extractUndoPayload(value: unknown): AdminDeleteChannelUndoSnapshot | null {\n if (!value || typeof value !== 'object') return null\n const candidate = (value as { undo?: unknown; channelId?: unknown }).undo ?? value\n if (!candidate || typeof candidate !== 'object') return null\n const obj = candidate as Record<string, unknown>\n if (typeof obj.channelId !== 'string') return null\n return {\n channelId: obj.channelId,\n tenantId: typeof obj.tenantId === 'string' ? obj.tenantId : undefined,\n }\n}\n\nfunction extractSnapshotFromLog(logEntry: unknown): AdminDeleteChannelUndoSnapshot | null {\n const undo = extractSharedUndoPayload<AdminDeleteChannelUndoSnapshot>((logEntry ?? null) as never)\n if (undo) return extractUndoPayload(undo)\n return extractUndoPayload(logEntry)\n}\n\nregisterCommand(adminDeleteChannelCommand)\n\nexport default adminDeleteChannelCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -10,7 +10,12 @@ const connectCredentialChannelSchema = z.object({
|
|
|
10
10
|
credentials: z.record(z.string(), z.unknown()),
|
|
11
11
|
/** Optional polling interval (seconds) — set when adapter is polling-based. */
|
|
12
12
|
pollIntervalSeconds: z.number().int().positive().max(86400).optional(),
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Connecting user. `null` for tenant-wide (admin-connected) channels whose
|
|
15
|
+
* provider declares `channelScope: 'tenant'` (push: FCM/APNs/Expo) — the
|
|
16
|
+
* resulting channel + credentials are stored with `user_id = NULL`.
|
|
17
|
+
*/
|
|
18
|
+
userId: z.string().uuid().nullable(),
|
|
14
19
|
scope: z.object({
|
|
15
20
|
tenantId: z.string().uuid(),
|
|
16
21
|
organizationId: z.string().uuid().nullable()
|
|
@@ -30,6 +35,11 @@ const connectCredentialChannelCommand = {
|
|
|
30
35
|
reason: `No adapter registered for provider '${input.providerKey}'`
|
|
31
36
|
};
|
|
32
37
|
}
|
|
38
|
+
const tenantScoped = adapter.channelScope === "tenant";
|
|
39
|
+
if (tenantScoped && input.userId !== null) {
|
|
40
|
+
return { status: "wrong_scope_for_route", providerKey: input.providerKey };
|
|
41
|
+
}
|
|
42
|
+
const effectiveUserId = tenantScoped ? null : input.userId;
|
|
33
43
|
if (typeof adapter.validateCredentials === "function") {
|
|
34
44
|
const validation = await adapter.validateCredentials({
|
|
35
45
|
providerKey: input.providerKey,
|
|
@@ -42,7 +52,8 @@ const connectCredentialChannelCommand = {
|
|
|
42
52
|
if (!validation.ok) {
|
|
43
53
|
return {
|
|
44
54
|
status: "validation_failed",
|
|
45
|
-
errors: validation.errors ?? { _form: "Credential validation failed" }
|
|
55
|
+
errors: validation.errors ?? { _form: "Credential validation failed" },
|
|
56
|
+
errorCodes: validation.errorCodes
|
|
46
57
|
};
|
|
47
58
|
}
|
|
48
59
|
}
|
|
@@ -54,8 +65,8 @@ const connectCredentialChannelCommand = {
|
|
|
54
65
|
}
|
|
55
66
|
const credentialsScope = {
|
|
56
67
|
tenantId: input.scope.tenantId,
|
|
57
|
-
organizationId: input.scope.organizationId ?? input.scope.tenantId,
|
|
58
|
-
userId:
|
|
68
|
+
organizationId: tenantScoped ? input.scope.tenantId : input.scope.organizationId ?? input.scope.tenantId,
|
|
69
|
+
userId: effectiveUserId
|
|
59
70
|
};
|
|
60
71
|
let credentialsRefId = null;
|
|
61
72
|
let credentialsPersisted = false;
|
|
@@ -63,7 +74,7 @@ const connectCredentialChannelCommand = {
|
|
|
63
74
|
try {
|
|
64
75
|
await credentialsService.save(
|
|
65
76
|
`channel_${input.providerKey}`,
|
|
66
|
-
{ ...input.credentials, userId:
|
|
77
|
+
{ ...input.credentials, ...effectiveUserId ? { userId: effectiveUserId } : {} },
|
|
67
78
|
credentialsScope
|
|
68
79
|
);
|
|
69
80
|
credentialsPersisted = true;
|
|
@@ -106,8 +117,14 @@ const connectCredentialChannelCommand = {
|
|
|
106
117
|
displayName: input.displayName,
|
|
107
118
|
externalIdentifier,
|
|
108
119
|
credentialsRefId,
|
|
109
|
-
userId:
|
|
110
|
-
|
|
120
|
+
userId: effectiveUserId,
|
|
121
|
+
// Tenant-wide channels store `organization_id = NULL` (truly tenant-scoped,
|
|
122
|
+
// matching the entity's `user_id NULL` semantics) so the org-agnostic heal
|
|
123
|
+
// key stays stable across orgs and delivery reads creds at `?? tenantId`.
|
|
124
|
+
scope: {
|
|
125
|
+
tenantId: input.scope.tenantId,
|
|
126
|
+
organizationId: tenantScoped ? null : input.scope.organizationId ?? null
|
|
127
|
+
},
|
|
111
128
|
pollIntervalSeconds: input.pollIntervalSeconds
|
|
112
129
|
});
|
|
113
130
|
} catch (err) {
|
|
@@ -120,7 +137,7 @@ const connectCredentialChannelCommand = {
|
|
|
120
137
|
}
|
|
121
138
|
throw err;
|
|
122
139
|
}
|
|
123
|
-
if (credentialsAvailable && input.providerKey === "gmail") {
|
|
140
|
+
if (credentialsAvailable && input.providerKey === "gmail" && input.userId) {
|
|
124
141
|
const adapterSupportsPush = typeof adapter.registerPush === "function" && typeof adapter.unregisterPush === "function";
|
|
125
142
|
const organizationId = input.scope.organizationId;
|
|
126
143
|
if (adapterSupportsPush && organizationId) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/commands/connect-credential-channel.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../lib/connect-channel'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'connect-credential-channel' })\n\nconst connectCredentialChannelSchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n /** Provider-specific credential fields \u2014 opaque to the hub; the adapter validates. */\n credentials: z.record(z.string(), z.unknown()),\n /** Optional polling interval (seconds) \u2014 set when adapter is polling-based. */\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n userId: z.string().uuid(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type ConnectCredentialChannelInput = z.infer<typeof connectCredentialChannelSchema>\n\nexport type ConnectCredentialChannelResult =\n | { status: 'connected'; channelId: string; externalIdentifier: string | null }\n | { status: 'validation_failed'; errors: Record<string, string> }\n | { status: 'no_adapter'; reason: string }\n | { status: 'duplicate_mailbox'; externalIdentifier: string; existingProviderKey: string }\n\nexport const COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID =\n 'communication_channels.channel.connect_credential'\n\ntype CredentialsServiceLike = {\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<string | void>\n}\n\n/**\n * Connect a per-user credential-based channel (IMAP, and future basic-auth providers).\n *\n * Flow:\n * 1. Resolve the adapter for `providerKey`.\n * 2. Call `adapter.validateCredentials?` \u2014 adapters that don't implement it\n * are accepted optimistically (the hub trusts the adapter to fail on first\n * use). Adapters with the method return field-level errors via Zod-like\n * `{ ok: false, errors: { fieldName: 'message' } }`; we forward those to\n * the caller for `createCrudFormError`.\n * 3. Persist the credentials encrypted via `integrationCredentialsService.save?` if available.\n * 4. Create the `CommunicationChannel` row with `userId = currentUser.id`,\n * `status = 'connected'`, and the adapter's declared capabilities snapshot.\n *\n * The route handler binds the `userId` field from the authenticated session;\n * the command refuses inputs whose `userId` doesn't pass UUID validation.\n */\nconst connectCredentialChannelCommand: CommandHandler<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n> = {\n id: COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = connectCredentialChannelSchema.parse(rawInput) as ConnectCredentialChannelInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n return {\n status: 'no_adapter',\n reason: `No adapter registered for provider '${input.providerKey}'`,\n }\n }\n\n // Optional credential validation.\n if (typeof adapter.validateCredentials === 'function') {\n const validation = await adapter.validateCredentials({\n providerKey: input.providerKey,\n credentials: input.credentials,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n })\n if (!validation.ok) {\n return {\n status: 'validation_failed',\n errors: validation.errors ?? { _form: 'Credential validation failed' },\n }\n }\n }\n\n // Persist credentials best-effort.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n credentialsService = null\n }\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature. The legacy reversed order swapped the scope\n // object into the credentials field; see review C1 (2026-05-26).\n //\n // `scope.userId` is set so the credentials service writes a per-user row.\n // Without this, two users on the same tenant share one credentials row\n // (see review R2-C1 / N1, 2026-05-26).\n const credentialsScope = {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n userId: input.userId,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n await credentialsService.save(\n `channel_${input.providerKey}`,\n { ...input.credentials, userId: input.userId },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn('credentials persist failed for provider', { providerKey: input.providerKey, err })\n }\n // Resolve the saved row id so we can link channel.credentialsRef. Best-effort.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import(\n '@open-mercato/core/modules/integrations/data/entities'\n )\n const { findOneWithDecryption } = await import(\n '@open-mercato/shared/lib/encryption/find'\n )\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${input.providerKey}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n }\n\n // Mailbox identity for this channel: the email address. IMAP/SMTP credentials\n // carry it as `fromAddress`; other credential providers may use `username` or\n // `email`. Normalize emails to lowercase so the cross-provider duplicate guard\n // and the per-(tenant,user,provider,mailbox) heal index match canonically.\n const credBag = input.credentials as Record<string, unknown>\n const rawIdentifier =\n (typeof credBag.username === 'string' && credBag.username.length > 0 ? credBag.username : null) ??\n (typeof credBag.email === 'string' && credBag.email.length > 0 ? credBag.email : null) ??\n (typeof credBag.fromAddress === 'string' && credBag.fromAddress.length > 0\n ? credBag.fromAddress\n : null)\n const externalIdentifier =\n rawIdentifier && rawIdentifier.includes('@') ? rawIdentifier.toLowerCase() : rawIdentifier\n\n // Fail-safe: if credentials persistence failed (no `credentialsRef`), the\n // channel is created in `requires_reauth` + isActive=false so workers\n // don't poll a channel that has no usable credentials. The user can\n // reconnect to recover (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: input.providerKey,\n displayName: input.displayName,\n externalIdentifier,\n credentialsRefId,\n userId: input.userId,\n scope: { tenantId: input.scope.tenantId, organizationId: input.scope.organizationId ?? null },\n pollIntervalSeconds: input.pollIntervalSeconds,\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 reject so we don't\n // create a second channel that double-ingests every message.\n if (err instanceof MailboxAlreadyConnectedError) {\n return {\n status: 'duplicate_mailbox',\n externalIdentifier: err.externalIdentifier,\n existingProviderKey: err.existingProviderKey,\n }\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for providers that\n // support it (Gmail). Failures persist as `pushStatus='failed'`\n // on the channel state but do NOT fail the connect \u2014 polling fallback\n // covers the channel until the operator clicks \"Re-register push\".\n // Imported lazily to avoid a circular module load (push-register reads\n // the channel adapter registry, which is initialised after this module).\n if (credentialsAvailable && input.providerKey === 'gmail') {\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n const organizationId = input.scope.organizationId\n if (adapterSupportsPush && organizationId) {\n try {\n const { pushRegister } = await import('./push-register')\n await pushRegister({\n container: ctx.container,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId,\n userId: input.userId,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n // Never fail the connect on push errors \u2014 operator can manually\n // re-register via the dedicated route. Log + continue.\n logger.warn('best-effort pushRegister failed for channel', { channelId: channel.id, err })\n }\n }\n }\n\n return { status: 'connected', channelId: channel.id, externalIdentifier }\n },\n}\n\nregisterCommand(connectCredentialChannelCommand)\n\nexport default connectCredentialChannelCommand\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAGlB,SAAS,uBAAuB;AAEhC,SAAS,2BAA2B,oCAAoC;AACxE,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,6BAA6B,CAAC;AAEvG,MAAM,iCAAiC,EAAE,OAAO;AAAA,EAC9C,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAAA;AAAA,EAEtC,aAAa,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA;AAAA,EAE7C,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,KAAM,EAAE,SAAS;AAAA,
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport { createConnectedChannelRow, MailboxAlreadyConnectedError } from '../lib/connect-channel'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'connect-credential-channel' })\n\nconst connectCredentialChannelSchema = z.object({\n providerKey: z.string().min(1).max(64),\n displayName: z.string().min(1).max(255),\n /** Provider-specific credential fields \u2014 opaque to the hub; the adapter validates. */\n credentials: z.record(z.string(), z.unknown()),\n /** Optional polling interval (seconds) \u2014 set when adapter is polling-based. */\n pollIntervalSeconds: z.number().int().positive().max(86_400).optional(),\n /**\n * Connecting user. `null` for tenant-wide (admin-connected) channels whose\n * provider declares `channelScope: 'tenant'` (push: FCM/APNs/Expo) \u2014 the\n * resulting channel + credentials are stored with `user_id = NULL`.\n */\n userId: z.string().uuid().nullable(),\n scope: z.object({\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n }),\n})\n\nexport type ConnectCredentialChannelInput = z.infer<typeof connectCredentialChannelSchema>\n\nexport type ConnectCredentialChannelResult =\n | { status: 'connected'; channelId: string; externalIdentifier: string | null }\n | { status: 'validation_failed'; errors: Record<string, string>; errorCodes?: Record<string, string> }\n | { status: 'no_adapter'; reason: string }\n | { status: 'duplicate_mailbox'; externalIdentifier: string; existingProviderKey: string }\n | { status: 'wrong_scope_for_route'; providerKey: string }\n\nexport const COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID =\n 'communication_channels.channel.connect_credential'\n\ntype CredentialsServiceLike = {\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<string | void>\n}\n\n/**\n * Connect a credential-based channel (IMAP, push providers, and future basic-auth\n * providers).\n *\n * Flow:\n * 1. Resolve the adapter for `providerKey`.\n * 2. Decide scope: `adapter.channelScope === 'tenant'` \u2192 the channel + credentials\n * are stored with `user_id = NULL` (one shared channel per tenant, e.g. push\n * FCM/APNs/Expo); otherwise they are stored under the connecting `userId`.\n * 3. Call `adapter.validateCredentials?` \u2014 adapters that don't implement it\n * are accepted optimistically (the hub trusts the adapter to fail on first\n * use). Adapters with the method return field-level errors via Zod-like\n * `{ ok: false, errors: { fieldName: 'message' } }`; we forward those to\n * the caller for `createCrudFormError`.\n * 4. Persist the credentials encrypted via `integrationCredentialsService.save?` if available.\n * 5. Create the `CommunicationChannel` row with the effective `userId`,\n * `status = 'connected'`, and the adapter's declared capabilities snapshot.\n *\n * The per-user route binds `userId` from the authenticated session; the tenant\n * route passes `userId: null`. Either way the command re-derives the effective\n * scope from the adapter, so a tenant provider is never stamped per-user.\n */\nconst connectCredentialChannelCommand: CommandHandler<\n ConnectCredentialChannelInput,\n ConnectCredentialChannelResult\n> = {\n id: COMMUNICATION_CHANNELS_CONNECT_CREDENTIAL_CHANNEL_COMMAND_ID,\n async execute(rawInput, ctx) {\n const input = connectCredentialChannelSchema.parse(rawInput) as ConnectCredentialChannelInput\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry\n const adapter = adapterRegistry.get(input.providerKey)\n if (!adapter) {\n return {\n status: 'no_adapter',\n reason: `No adapter registered for provider '${input.providerKey}'`,\n }\n }\n\n // Tenant-wide providers (push: FCM/APNs/Expo) store one shared channel +\n // credential row per tenant with `user_id = NULL`; per-user providers\n // (Gmail/IMAP) keep the connecting user's id.\n //\n // SECURITY: a tenant-scoped provider must only be connected through the\n // admin-gated tenant route (which passes `userId: null`). If it arrives with\n // a non-null `userId` it came from the per-user route (feature\n // `connect_user_channel`, granted to manager/employee) \u2014 refuse rather than\n // silently minting a privileged tenant-wide channel that bypasses the\n // admin-only `connect_tenant_channel` gate. The tenant route passes\n // `userId: null`, so legitimate tenant connects still pass.\n const tenantScoped = adapter.channelScope === 'tenant'\n if (tenantScoped && input.userId !== null) {\n return { status: 'wrong_scope_for_route', providerKey: input.providerKey }\n }\n const effectiveUserId = tenantScoped ? null : input.userId\n\n // Optional credential validation.\n if (typeof adapter.validateCredentials === 'function') {\n const validation = await adapter.validateCredentials({\n providerKey: input.providerKey,\n credentials: input.credentials,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: input.scope.organizationId ?? input.scope.tenantId,\n },\n })\n if (!validation.ok) {\n return {\n status: 'validation_failed',\n errors: validation.errors ?? { _form: 'Credential validation failed' },\n errorCodes: validation.errorCodes,\n }\n }\n }\n\n // Persist credentials best-effort.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.container.resolve('integrationCredentialsService') as CredentialsServiceLike\n } catch {\n credentialsService = null\n }\n // Arg order MUST be (integrationId, credentials, scope) \u2014 matches the real\n // CredentialsService signature. The legacy reversed order swapped the scope\n // object into the credentials field; see review C1 (2026-05-26).\n //\n // `scope.userId` is set so the credentials service writes a per-user row.\n // Without this, two users on the same tenant share one credentials row\n // (see review R2-C1 / N1, 2026-05-26). Tenant-wide providers pass `null`\n // here so the service writes/reads the shared `user_id = NULL` row (same\n // pattern as Stripe/Akeneo tenant-wide credentials).\n //\n // `organizationId` for tenant scope is pinned to `tenantId` (not the\n // connecting admin's selected org). The channel dedup/heal is org-agnostic\n // (index + heal key ignore org) and the channel row is stored with\n // `organization_id = NULL`, so push-delivery resolves credentials at\n // `channel.organizationId ?? tenantId` = `tenantId`. Pinning the write to the\n // same key keeps a cross-org reconnect (e.g. key rotation from another org)\n // overwriting the one credential row the delivery path actually reads, rather\n // than orphaning it under a per-org key.\n const credentialsScope = {\n tenantId: input.scope.tenantId,\n organizationId: tenantScoped\n ? input.scope.tenantId\n : input.scope.organizationId ?? input.scope.tenantId,\n userId: effectiveUserId,\n }\n let credentialsRefId: string | null = null\n let credentialsPersisted = false\n if (credentialsService?.save) {\n try {\n await credentialsService.save(\n `channel_${input.providerKey}`,\n { ...input.credentials, ...(effectiveUserId ? { userId: effectiveUserId } : {}) },\n credentialsScope,\n )\n credentialsPersisted = true\n } catch (err) {\n logger.warn('credentials persist failed for provider', { providerKey: input.providerKey, err })\n }\n // Resolve the saved row id so we can link channel.credentialsRef. Best-effort.\n if (credentialsPersisted) {\n try {\n const { IntegrationCredentials } = await import(\n '@open-mercato/core/modules/integrations/data/entities'\n )\n const { findOneWithDecryption } = await import(\n '@open-mercato/shared/lib/encryption/find'\n )\n const row = await findOneWithDecryption(\n em,\n IntegrationCredentials,\n {\n integrationId: `channel_${input.providerKey}`,\n tenantId: credentialsScope.tenantId,\n organizationId: credentialsScope.organizationId,\n userId: credentialsScope.userId,\n deletedAt: null,\n },\n undefined,\n credentialsScope,\n )\n credentialsRefId = (row as { id?: string } | null)?.id ?? null\n } catch {\n credentialsRefId = null\n }\n }\n }\n\n // Mailbox identity for this channel: the email address. IMAP/SMTP credentials\n // carry it as `fromAddress`; other credential providers may use `username` or\n // `email`. Normalize emails to lowercase so the cross-provider duplicate guard\n // and the per-(tenant,user,provider,mailbox) heal index match canonically.\n const credBag = input.credentials as Record<string, unknown>\n const rawIdentifier =\n (typeof credBag.username === 'string' && credBag.username.length > 0 ? credBag.username : null) ??\n (typeof credBag.email === 'string' && credBag.email.length > 0 ? credBag.email : null) ??\n (typeof credBag.fromAddress === 'string' && credBag.fromAddress.length > 0\n ? credBag.fromAddress\n : null)\n const externalIdentifier =\n rawIdentifier && rawIdentifier.includes('@') ? rawIdentifier.toLowerCase() : rawIdentifier\n\n // Fail-safe: if credentials persistence failed (no `credentialsRef`), the\n // channel is created in `requires_reauth` + isActive=false so workers\n // don't poll a channel that has no usable credentials. The user can\n // reconnect to recover (see review R2-H4 / F6, 2026-05-26).\n const credentialsAvailable = credentialsRefId !== null\n let channel\n try {\n channel = await createConnectedChannelRow({\n em,\n adapter,\n providerKey: input.providerKey,\n displayName: input.displayName,\n externalIdentifier,\n credentialsRefId,\n userId: effectiveUserId,\n // Tenant-wide channels store `organization_id = NULL` (truly tenant-scoped,\n // matching the entity's `user_id NULL` semantics) so the org-agnostic heal\n // key stays stable across orgs and delivery reads creds at `?? tenantId`.\n scope: {\n tenantId: input.scope.tenantId,\n organizationId: tenantScoped ? null : input.scope.organizationId ?? null,\n },\n pollIntervalSeconds: input.pollIntervalSeconds,\n })\n } catch (err) {\n // Same mailbox already connected via another provider \u2014 reject so we don't\n // create a second channel that double-ingests every message.\n if (err instanceof MailboxAlreadyConnectedError) {\n return {\n status: 'duplicate_mailbox',\n externalIdentifier: err.externalIdentifier,\n existingProviderKey: err.existingProviderKey,\n }\n }\n throw err\n }\n\n // Spec C \u00A7 Phase C5 \u2014 best-effort push registration for providers that\n // support it (Gmail). Failures persist as `pushStatus='failed'`\n // on the channel state but do NOT fail the connect \u2014 polling fallback\n // covers the channel until the operator clicks \"Re-register push\".\n // Imported lazily to avoid a circular module load (push-register reads\n // the channel adapter registry, which is initialised after this module).\n if (credentialsAvailable && input.providerKey === 'gmail' && input.userId) {\n const adapterSupportsPush =\n typeof adapter.registerPush === 'function' && typeof adapter.unregisterPush === 'function'\n const organizationId = input.scope.organizationId\n if (adapterSupportsPush && organizationId) {\n try {\n const { pushRegister } = await import('./push-register')\n await pushRegister({\n container: ctx.container,\n scope: {\n tenantId: input.scope.tenantId,\n organizationId,\n userId: input.userId,\n },\n input: { channelId: channel.id },\n })\n } catch (err) {\n // Never fail the connect on push errors \u2014 operator can manually\n // re-register via the dedicated route. Log + continue.\n logger.warn('best-effort pushRegister failed for channel', { channelId: channel.id, err })\n }\n }\n }\n\n return { status: 'connected', channelId: channel.id, externalIdentifier }\n },\n}\n\nregisterCommand(connectCredentialChannelCommand)\n\nexport default connectCredentialChannelCommand\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -169,7 +169,7 @@ const deliverOutboundMessageCommand = {
|
|
|
169
169
|
}
|
|
170
170
|
const credentialsScope = {
|
|
171
171
|
tenantId: input.scope.tenantId,
|
|
172
|
-
organizationId:
|
|
172
|
+
organizationId: channel.organizationId ?? input.scope.tenantId,
|
|
173
173
|
userId: channel.userId ?? null
|
|
174
174
|
};
|
|
175
175
|
let credentials = {};
|
|
@@ -251,7 +251,7 @@ const deliverOutboundMessageCommand = {
|
|
|
251
251
|
credentials,
|
|
252
252
|
scope: {
|
|
253
253
|
tenantId: input.scope.tenantId,
|
|
254
|
-
organizationId:
|
|
254
|
+
organizationId: channel.organizationId ?? input.scope.tenantId
|
|
255
255
|
},
|
|
256
256
|
metadata: converted.metadata
|
|
257
257
|
});
|