@open-mercato/core 0.6.6-develop.6551.1.1511bfa890 → 0.6.6
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.ids.generated.js +0 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +0 -12
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/api/keys/route.js +13 -30
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/api_keys/cli.js +1 -2
- package/dist/modules/api_keys/cli.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +7 -9
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +17 -21
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentContentPreview.js +35 -6
- package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
- package/dist/modules/audit_logs/backend/audit-logs/page.js +24 -14
- package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +2 -6
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/cli.js +2 -3
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +0 -22
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +0 -1
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -17
- package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +1 -6
- package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +1 -6
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js +0 -1
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerRbacService.js +1 -6
- package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +2 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -7
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +43 -37
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +35 -23
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DetailTabsLayout.js +21 -6
- package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
- package/dist/modules/customers/components/detail/MobilePersonDetail.js +32 -26
- package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +39 -29
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +3 -22
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/cli.js +3 -4
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js +1 -9
- package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
- package/dist/modules/dictionaries/commands/entries.js +14 -7
- package/dist/modules/dictionaries/commands/entries.js.map +2 -2
- package/dist/modules/dictionaries/commands/entry-operations.js +16 -7
- package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +3 -9
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +41 -56
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +3 -10
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/records.js +4 -5
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +0 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
- package/dist/modules/entities/cli.js +1 -2
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/feature_toggles/api/check/boolean/route.js +14 -0
- package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
- package/dist/modules/feature_toggles/defaults.json +0 -8
- package/dist/modules/integrations/backend/integrations/[id]/page.js +80 -9
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +0 -45
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +38 -173
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
- package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +14 -16
- package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +30 -18
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/status.js +8 -2
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/create/page.js +0 -6
- package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/create/page.meta.js +0 -2
- package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +0 -6
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +0 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +0 -6
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.meta.js +0 -2
- package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +4 -6
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +0 -44
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +27 -2
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +2 -6
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +0 -1
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +0 -9
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +3 -51
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +8 -10
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +38 -59
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/sales/lib/shipments/snapshots.js +1 -27
- package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +25 -22
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +14 -12
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +29 -10
- package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +20 -19
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/sync_excel/lib/parser.js +0 -185
- package/dist/modules/sync_excel/lib/parser.js.map +2 -2
- package/dist/modules/sync_excel/lib/upload-storage.js +1 -20
- package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +8 -1
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +1 -1
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/find-definition.js +18 -2
- package/dist/modules/workflows/lib/find-definition.js.map +2 -2
- package/dist/modules/workflows/lib/signal-handler.js +3 -2
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +4 -8
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +3 -5
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/timer-handler.js +2 -1
- package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +5 -13
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +11 -9
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260715120000.js +68 -0
- package/dist/modules/workflows/migrations/Migration20260715120000.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js +24 -0
- package/dist/modules/workflows/migrations/Migration20260716120000.js.map +7 -0
- package/dist/modules/workflows/workflows.js +2 -0
- package/dist/modules/workflows/workflows.js.map +2 -2
- package/generated/entities.ids.generated.ts +0 -1
- package/generated/entity-fields-registry.ts +0 -12
- package/package.json +8 -9
- package/src/modules/api_keys/api/keys/route.ts +13 -36
- package/src/modules/api_keys/cli.ts +3 -2
- package/src/modules/attachments/api/file/[id]/route.ts +7 -11
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -11
- package/src/modules/attachments/api/route.ts +21 -26
- package/src/modules/attachments/components/AttachmentContentPreview.tsx +44 -9
- package/src/modules/audit_logs/backend/audit-logs/page.tsx +22 -12
- package/src/modules/auth/api/users/route.ts +2 -11
- package/src/modules/auth/cli.ts +2 -3
- package/src/modules/configs/i18n/de.json +1 -5
- package/src/modules/configs/i18n/en.json +1 -5
- package/src/modules/configs/i18n/es.json +1 -5
- package/src/modules/configs/i18n/pl.json +1 -5
- package/src/modules/configs/lib/upgrade-actions.ts +0 -24
- package/src/modules/customer_accounts/AGENTS.md +6 -8
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +0 -1
- package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -17
- package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +1 -6
- package/src/modules/customer_accounts/api/portal/users-invite.ts +1 -6
- package/src/modules/customer_accounts/services/customerInvitationService.ts +0 -1
- package/src/modules/customer_accounts/services/customerRbacService.ts +2 -9
- package/src/modules/customer_accounts/widgets/injection-table.ts +2 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -11
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +2 -6
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +55 -39
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +49 -24
- package/src/modules/customers/components/detail/DetailTabsLayout.tsx +24 -13
- package/src/modules/customers/components/detail/MobilePersonDetail.tsx +31 -25
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +50 -29
- package/src/modules/dashboards/api/roles/widgets/route.ts +3 -22
- package/src/modules/dashboards/api/users/widgets/route.ts +3 -22
- package/src/modules/dashboards/cli.ts +9 -4
- package/src/modules/dashboards/lib/widgetAssignmentScope.ts +0 -18
- package/src/modules/dictionaries/commands/entries.ts +16 -7
- package/src/modules/dictionaries/commands/entry-operations.ts +18 -8
- package/src/modules/dictionaries/commands/factory.ts +2 -8
- package/src/modules/dictionaries/fields/dictionary.tsx +32 -55
- package/src/modules/dictionaries/i18n/de.json +0 -2
- package/src/modules/dictionaries/i18n/en.json +0 -2
- package/src/modules/dictionaries/i18n/es.json +0 -2
- package/src/modules/dictionaries/i18n/pl.json +0 -2
- package/src/modules/directory/utils/organizationScope.ts +2 -36
- package/src/modules/entities/api/records.ts +7 -5
- package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +0 -1
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +49 -51
- package/src/modules/entities/cli.ts +1 -2
- package/src/modules/entities/i18n/de.json +0 -34
- package/src/modules/entities/i18n/en.json +0 -34
- package/src/modules/entities/i18n/es.json +0 -34
- package/src/modules/entities/i18n/pl.json +0 -34
- package/src/modules/feature_toggles/api/check/boolean/route.ts +15 -0
- package/src/modules/feature_toggles/defaults.json +0 -8
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +59 -16
- package/src/modules/payment_gateways/data/entities.ts +0 -39
- package/src/modules/payment_gateways/lib/gateway-service.ts +40 -211
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +0 -236
- package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +20 -14
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +34 -16
- package/src/modules/query_index/api/status.ts +8 -2
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +37 -27
- package/src/modules/sales/backend/sales/channels/create/page.meta.ts +0 -3
- package/src/modules/sales/backend/sales/channels/create/page.tsx +0 -7
- package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +0 -3
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +0 -7
- package/src/modules/sales/backend/sales/channels/page.meta.ts +0 -4
- package/src/modules/sales/backend/sales/channels/page.tsx +0 -7
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +5 -7
- package/src/modules/sales/commands/documents.ts +0 -65
- package/src/modules/sales/commands/returns.ts +32 -2
- package/src/modules/sales/components/documents/AddressesSection.tsx +2 -6
- package/src/modules/sales/components/documents/ItemsSection.tsx +0 -5
- package/src/modules/sales/components/documents/LineItemDialog.tsx +0 -10
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +3 -53
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +8 -10
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +40 -65
- package/src/modules/sales/i18n/de.json +0 -5
- package/src/modules/sales/i18n/en.json +0 -5
- package/src/modules/sales/i18n/es.json +0 -5
- package/src/modules/sales/i18n/pl.json +0 -5
- package/src/modules/sales/lib/shipments/snapshots.ts +0 -30
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +34 -25
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -11
- package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +28 -11
- package/src/modules/sync_excel/lib/adapters/customers.ts +22 -20
- package/src/modules/sync_excel/lib/parser.ts +0 -245
- package/src/modules/sync_excel/lib/upload-storage.ts +1 -23
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +12 -1
- package/src/modules/workflows/lib/activity-executor.ts +22 -2
- package/src/modules/workflows/lib/find-definition.ts +53 -1
- package/src/modules/workflows/lib/signal-handler.ts +5 -4
- package/src/modules/workflows/lib/step-handler.ts +4 -8
- package/src/modules/workflows/lib/task-handler.ts +2 -4
- package/src/modules/workflows/lib/timer-handler.ts +3 -2
- package/src/modules/workflows/lib/transition-handler.ts +5 -13
- package/src/modules/workflows/lib/workflow-executor.ts +11 -8
- package/src/modules/workflows/migrations/Migration20260715120000.ts +114 -0
- package/src/modules/workflows/migrations/Migration20260716120000.ts +57 -0
- package/src/modules/workflows/workflows.ts +2 -0
- package/dist/generated/entities/gateway_session_initialization/index.js +0 -23
- package/dist/generated/entities/gateway_session_initialization/index.js.map +0 -7
- package/dist/modules/attachments/lib/reconcileOrganization.js +0 -110
- package/dist/modules/attachments/lib/reconcileOrganization.js.map +0 -7
- package/dist/modules/attachments/lib/requestScope.js +0 -10
- package/dist/modules/attachments/lib/requestScope.js.map +0 -7
- package/dist/modules/auth/api/autologin.js +0 -85
- package/dist/modules/auth/api/autologin.js.map +0 -7
- package/dist/modules/auth/lib/autologin.js +0 -19
- package/dist/modules/auth/lib/autologin.js.map +0 -7
- package/dist/modules/customers/components/detail/useDealsAccess.js +0 -16
- package/dist/modules/customers/components/detail/useDealsAccess.js.map +0 -7
- package/dist/modules/payment_gateways/lib/session-idempotency.js +0 -104
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +0 -7
- package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +0 -11
- package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +0 -7
- package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +0 -15
- package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +0 -7
- package/dist/modules/sales/components/useSalesChannelsEnabled.js +0 -44
- package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +0 -7
- package/dist/modules/sales/lib/salesChannelsToggle.js +0 -18
- package/dist/modules/sales/lib/salesChannelsToggle.js.map +0 -7
- package/dist/modules/sales/lib/salesChannelsToggleId.js +0 -5
- package/dist/modules/sales/lib/salesChannelsToggleId.js.map +0 -7
- package/generated/entities/gateway_session_initialization/index.ts +0 -10
- package/src/modules/attachments/lib/reconcileOrganization.ts +0 -188
- package/src/modules/attachments/lib/requestScope.ts +0 -27
- package/src/modules/auth/api/autologin.ts +0 -100
- package/src/modules/auth/lib/autologin.ts +0 -36
- package/src/modules/customers/components/detail/useDealsAccess.ts +0 -19
- package/src/modules/payment_gateways/lib/session-idempotency.ts +0 -142
- package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +0 -10
- package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +0 -21
- package/src/modules/sales/components/useSalesChannelsEnabled.ts +0 -59
- package/src/modules/sales/lib/salesChannelsToggle.ts +0 -26
- package/src/modules/sales/lib/salesChannelsToggleId.ts +0 -1
|
@@ -500,7 +500,7 @@ async function executeCallApi(em, config, context, container, signal) {
|
|
|
500
500
|
}
|
|
501
501
|
const fullUrl = buildApiUrl(endpoint);
|
|
502
502
|
const { withOnetimeApiKey } = await import("../../api_keys/services/apiKeyService.js");
|
|
503
|
-
const apiKeyEm = container.resolve("em");
|
|
503
|
+
const apiKeyEm = container.resolve("em").fork({ clear: true, freshEventManager: true, useContext: false });
|
|
504
504
|
const resolvedRoleIds = await resolveCallApiRoleIds(apiKeyEm, context.workflowInstance);
|
|
505
505
|
if (resolvedRoleIds.length === 0) {
|
|
506
506
|
throw new Error(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/activity-executor.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Workflows Module - Activity Executor Service\n *\n * Executes workflow activities (send email, call API, emit events, etc.)\n * - Supports multiple activity types\n * - Implements retry logic with exponential backoff\n * - Handles timeouts\n * - Variable interpolation from workflow context\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { createModuleQueue, Queue } from '@open-mercato/queue'\nimport { getRedisUrl } from '@open-mercato/shared/lib/redis/connection'\nimport {\n safeOutboundFetch,\n UnsafeOutboundUrlError,\n type HostLookup,\n} from '@open-mercato/shared/lib/url-safety'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { callWebhookConfigSchema } from '../data/validators'\nimport { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'\nimport { logWorkflowEvent } from './event-logger'\nimport { parseDuration } from './duration'\n\nexport { isPrivateUrl } from '@open-mercato/shared/lib/network'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nfunction isAllowPrivateWorkflowWebhookUrlsEnabled(): boolean {\n if (parseBooleanWithDefault(process.env.OM_WORKFLOWS_ALLOW_PRIVATE_URLS, false)) {\n if (process.env.NODE_ENV === 'production') {\n logger.warn('OM_WORKFLOWS_ALLOW_PRIVATE_URLS is set but ignored in production. SSRF protection remains enabled.', { component: 'CALL_WEBHOOK' })\n return false\n }\n\n logger.warn('OM_WORKFLOWS_ALLOW_PRIVATE_URLS is enabled. SSRF protection is bypassed for workflow webhooks; use only in development.', { component: 'CALL_WEBHOOK' })\n return true\n }\n\n if (parseBooleanWithDefault(process.env.WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS, false)) {\n if (process.env.NODE_ENV === 'production') {\n logger.warn('WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated and ignored in production. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS for development only. SSRF protection remains enabled.', { component: 'CALL_WEBHOOK' })\n return false\n }\n\n logger.warn('WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS instead. SSRF protection is bypassed.', { component: 'CALL_WEBHOOK' })\n return true\n }\n\n return false\n}\n\nconst DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST = new Set(['APP_URL'])\nconst WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY = 'OM_WORKFLOWS_ENV_INTERPOLATION_ALLOWLIST'\n\nfunction getWorkflowEnvInterpolationAllowlist(): Set<string> {\n const allowlist = new Set(DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST)\n const configuredKeys = process.env[WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY]\n if (!configuredKeys) {\n return allowlist\n }\n\n for (const key of configuredKeys.split(',')) {\n const trimmedKey = key.trim()\n if (trimmedKey) {\n allowlist.add(trimmedKey)\n }\n }\n\n return allowlist\n}\n\nfunction resolveWorkflowEnvInterpolation(envKey: string): string {\n if (!getWorkflowEnvInterpolationAllowlist().has(envKey)) {\n return ''\n }\n\n return process.env[envKey] ?? ''\n}\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport type ActivityType =\n | 'SEND_EMAIL'\n | 'CALL_API'\n | 'EMIT_EVENT'\n | 'UPDATE_ENTITY'\n | 'CALL_WEBHOOK'\n | 'EXECUTE_FUNCTION'\n | 'WAIT'\n\nexport interface ActivityDefinition {\n activityId: string // Unique identifier for activity\n activityName?: string // Optional, for debugging/logging\n activityType: ActivityType\n config: any\n async?: boolean // Flag to execute activity asynchronously via queue\n retryPolicy?: RetryPolicy\n timeoutMs?: number\n compensate?: boolean // Flag to execute compensation on failure\n}\n\nexport interface RetryPolicy {\n maxAttempts: number\n initialIntervalMs: number\n backoffCoefficient: number\n maxIntervalMs: number\n}\n\nexport interface ActivityContext {\n workflowInstance: WorkflowInstance\n workflowContext: Record<string, any>\n stepContext?: Record<string, any>\n stepInstanceId?: string\n // Set when the activity runs inside a parallel branch; carried on the queue\n // payload so async resume targets the branch rather than the instance.\n branchInstanceId?: string | null\n transitionId?: string\n userId?: string\n}\n\nexport interface ActivityExecutionResult {\n activityId: string\n activityName?: string\n activityType: ActivityType\n success: boolean\n output?: any\n error?: string\n retryCount: number\n executionTimeMs: number\n async?: boolean // Marks activity as async (queued)\n jobId?: string // Queue job ID for async activities\n}\n\nexport class ActivityExecutionError extends Error {\n constructor(\n message: string,\n public activityType: ActivityType,\n public activityName?: string,\n public details?: any\n ) {\n super(message)\n this.name = 'ActivityExecutionError'\n }\n}\n\n// ============================================================================\n// Queue Integration for Async Activities\n// ============================================================================\n\nlet activityQueue: Queue<WorkflowActivityJob> | null = null\n\n/**\n * Get or create the activity queue (lazy initialization)\n */\nfunction getActivityQueue(): Queue<WorkflowActivityJob> {\n if (!activityQueue) {\n activityQueue = createModuleQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n { concurrency: parseInt(process.env.WORKFLOW_WORKER_CONCURRENCY || '5') },\n )\n }\n\n return activityQueue\n}\n\n/**\n * Enqueue an activity for background execution\n *\n * @param em - Entity manager\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Job ID\n */\nexport async function enqueueActivity(\n em: EntityManager,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<string> {\n const { workflowInstance, workflowContext, stepContext, transitionId, stepInstanceId, branchInstanceId } =\n context\n\n // Interpolate config variables NOW (before queuing)\n const interpolatedConfig = interpolateVariables(activity.config, workflowContext, workflowInstance)\n\n // Create job payload\n const job: WorkflowActivityJob = {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n branchInstanceId: branchInstanceId ?? undefined,\n transitionId,\n activityId: activity.activityId,\n activityName: activity.activityName || activity.activityType,\n activityType: activity.activityType,\n activityConfig: interpolatedConfig,\n workflowContext,\n stepContext,\n retryPolicy: activity.retryPolicy,\n timeoutMs: activity.timeoutMs,\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n userId: context.userId,\n }\n\n // Enqueue to queue (WAIT activities use delayMs for the actual delay)\n const queue = getActivityQueue()\n const enqueueOptions = activity.activityType === 'WAIT' && (interpolatedConfig.duration || interpolatedConfig.until)\n ? { delayMs: calculateWaitDelayMs(interpolatedConfig) }\n : undefined\n const jobId = await queue.enqueue(job, enqueueOptions)\n\n // Log event\n await logWorkflowEvent(em, {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n eventType: 'ACTIVITY_QUEUED',\n eventData: {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n async: true,\n jobId,\n },\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n })\n\n return jobId\n}\n\n/**\n * Enqueue a delayed timer job for a WAIT_FOR_TIMER step.\n *\n * The activity worker handles `kind: 'timer'` jobs by calling\n * `timerHandler.fireTimer`, which resumes the paused workflow instance.\n */\nexport async function enqueueTimerJob(params: {\n workflowInstanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n tenantId: string\n organizationId: string\n userId?: string\n fireAt: string\n delayMs: number\n}): Promise<string> {\n const { workflowInstanceId, stepInstanceId, branchInstanceId, tenantId, organizationId, userId, fireAt, delayMs } =\n params\n\n const queue = getActivityQueue()\n const jobId = await queue.enqueue(\n {\n kind: 'timer',\n workflowInstanceId,\n stepInstanceId,\n branchInstanceId: branchInstanceId ?? undefined,\n tenantId,\n organizationId,\n userId,\n fireAt,\n },\n { delayMs: delayMs > 0 ? delayMs : undefined }\n )\n\n return jobId\n}\n\n// ============================================================================\n// Main Activity Execution Functions\n// ============================================================================\n\n/**\n * Execute a single activity with retry logic and timeout\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeActivity(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<ActivityExecutionResult> {\n const retryPolicy = activity.retryPolicy || {\n maxAttempts: 1,\n initialIntervalMs: 0,\n backoffCoefficient: 1,\n maxIntervalMs: 0,\n }\n\n let lastError: any\n let retryCount = 0\n\n for (let attempt = 0; attempt < retryPolicy.maxAttempts; attempt++) {\n try {\n const startTime = Date.now()\n\n // Execute with timeout if specified\n const result = activity.timeoutMs\n ? await executeWithTimeout(\n () => executeActivityByType(em, container, activity, context),\n activity.timeoutMs\n )\n : await executeActivityByType(em, container, activity, context)\n\n const executionTimeMs = Date.now() - startTime\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true,\n output: result,\n retryCount: attempt,\n executionTimeMs,\n async: activity.async || false,\n }\n } catch (error) {\n lastError = error\n retryCount = attempt + 1\n\n // Log activity retry attempt with context\n if (attempt < retryPolicy.maxAttempts - 1) {\n logger.error('Activity failed; will retry', {\n activityId: activity.activityId,\n activityType: activity.activityType,\n attempt: attempt + 1,\n maxAttempts: retryPolicy.maxAttempts,\n instanceId: context.workflowInstance.id,\n err: error,\n })\n }\n\n // If not the last attempt, apply backoff and retry\n if (attempt < retryPolicy.maxAttempts - 1) {\n const backoff = calculateBackoff(\n retryPolicy.initialIntervalMs,\n retryPolicy.backoffCoefficient,\n attempt,\n retryPolicy.maxIntervalMs\n )\n\n await sleep(backoff)\n }\n }\n }\n\n // All retries exhausted\n const errorMessage = lastError instanceof Error ? lastError.message : String(lastError)\n logger.error('Activity failed after all attempts', {\n activityId: activity.activityId,\n activityType: activity.activityType,\n attempts: retryCount,\n instanceId: context.workflowInstance.id,\n err: lastError,\n })\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: false,\n error: `Activity failed after ${retryCount} attempts: ${errorMessage}`,\n retryCount,\n executionTimeMs: 0,\n async: activity.async || false,\n }\n}\n\n/**\n * Execute multiple activities in sequence\n * Supports both synchronous and asynchronous (queued) execution\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activities - Array of activity definitions\n * @param context - Execution context\n * @returns Array of execution results\n */\nexport async function executeActivities(\n em: EntityManager,\n container: AwilixContainer,\n activities: ActivityDefinition[],\n context: ActivityContext\n): Promise<ActivityExecutionResult[]> {\n const results: ActivityExecutionResult[] = []\n\n for (let i = 0; i < activities.length; i++) {\n const activity = activities[i]\n\n // Check if activity should run async\n if (activity.async) {\n // Enqueue for background execution\n const jobId = await enqueueActivity(em, activity, context)\n\n results.push({\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true, // Queued successfully\n async: true,\n jobId,\n retryCount: 0,\n executionTimeMs: 0,\n })\n } else {\n // Execute synchronously (existing logic)\n const result = await executeActivity(em, container, activity, context)\n results.push(result)\n\n // Stop execution if activity fails (fail-fast)\n if (!result.success) {\n break\n }\n\n // Update workflow context with activity output\n if (result.output && typeof result.output === 'object') {\n const key = activity.activityName || activity.activityType\n context.workflowContext = {\n ...context.workflowContext,\n [key]: result.output,\n }\n }\n }\n }\n\n return results\n}\n\n// ============================================================================\n// Activity Type Handlers\n// ============================================================================\n\n/**\n * Execute activity based on its type\n */\nasync function executeActivityByType(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<any> {\n // Interpolate config variables from context (including workflow metadata)\n const interpolatedConfig = interpolateVariables(activity.config, context.workflowContext, context.workflowInstance)\n\n switch (activity.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(interpolatedConfig, context, container)\n\n case 'CALL_API':\n return await executeCallApi(em, interpolatedConfig, context, container)\n\n case 'EMIT_EVENT':\n return await executeEmitEvent(interpolatedConfig, context, container)\n\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(em, interpolatedConfig, context, container)\n\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(interpolatedConfig, context)\n\n case 'EXECUTE_FUNCTION':\n return await executeFunction(interpolatedConfig, context, container)\n\n case 'WAIT':\n return await executeWait(interpolatedConfig)\n\n default:\n throw new ActivityExecutionError(\n `Unknown activity type: ${activity.activityType}`,\n activity.activityType,\n activity.activityName\n )\n }\n}\n\n/**\n * SEND_EMAIL activity handler\n *\n * For MVP, this logs the email (actual email sending can be added later)\n */\nexport async function executeSendEmail(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { to, subject, template, templateData, body } = config\n\n if (!to || !subject) {\n throw new Error('SEND_EMAIL requires \"to\" and \"subject\" fields')\n }\n\n // For MVP: Log the email (actual email service integration can be added later)\n logger.info('Send email activity invoked', { component: 'SEND_EMAIL', subject })\n\n // Check if email service is available in container\n try {\n const emailService = container.resolve<{ send: (input: unknown) => Promise<unknown> | unknown }>('emailService')\n if (emailService && typeof emailService.send === 'function') {\n await emailService.send({\n to,\n subject,\n template,\n templateData,\n body,\n })\n return { sent: true, to, subject, via: 'emailService' }\n }\n } catch (error) {\n // Email service not available, just log\n }\n\n return { sent: true, to, subject, via: 'console' }\n}\n\n/**\n * EMIT_EVENT activity handler\n *\n * Publishes a domain event to the event bus\n */\nexport async function executeEmitEvent(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { eventName, payload } = config\n\n if (!eventName) {\n throw new Error('EMIT_EVENT requires \"eventName\" field')\n }\n\n // Get event bus from container\n const eventBus = container.resolve<{ emitEvent: (event: string, payload: unknown, options?: unknown) => Promise<unknown> | unknown }>('eventBus')\n\n if (!eventBus || typeof eventBus.emitEvent !== 'function') {\n throw new Error('Event bus not available in container')\n }\n\n // Publish event with workflow metadata\n const enrichedPayload = {\n ...payload,\n _workflow: {\n workflowInstanceId: context.workflowInstance.id,\n workflowId: context.workflowInstance.workflowId,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n },\n }\n\n await eventBus.emitEvent(eventName, enrichedPayload, {\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n })\n\n return { emitted: true, eventName, payload: enrichedPayload }\n}\n\n/**\n * UPDATE_ENTITY activity handler\n *\n * Updates an entity via CommandBus for proper audit logging, undo support, and side effects.\n *\n * Config format:\n * ```json\n * {\n * \"commandId\": \"sales.documents.update\",\n * \"input\": {\n * \"id\": \"{{context.orderId}}\",\n * \"statusEntryId\": \"{{context.approvedStatusId}}\"\n * }\n * }\n * ```\n *\n * Alternative format with statusValue (auto-resolves to statusEntryId):\n * ```json\n * {\n * \"commandId\": \"sales.orders.update\",\n * \"statusDictionary\": \"sales.order_status\",\n * \"input\": {\n * \"id\": \"{{context.id}}\",\n * \"statusValue\": \"pending_approval\"\n * }\n * }\n * ```\n */\nexport async function executeUpdateEntity(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { commandId, input, statusDictionary } = config\n\n if (!commandId) {\n throw new Error('UPDATE_ENTITY requires \"commandId\" field (e.g., \"sales.documents.update\")')\n }\n\n if (!input || typeof input !== 'object') {\n throw new Error('UPDATE_ENTITY requires \"input\" object with entity data')\n }\n\n // Resolve CommandBus from container\n const commandBus = container.resolve('commandBus') as any\n\n if (!commandBus || typeof commandBus.execute !== 'function') {\n throw new Error('CommandBus not available in container')\n }\n\n // Prepare final input, resolving statusValue if provided\n let finalInput = { ...input }\n\n // If statusValue is provided with a statusDictionary, resolve it to statusEntryId\n if (finalInput.statusValue && statusDictionary) {\n const statusEntryId = await resolveDictionaryEntryId(\n em,\n statusDictionary,\n finalInput.statusValue,\n context.workflowInstance.tenantId,\n context.workflowInstance.organizationId\n )\n if (statusEntryId) {\n finalInput.statusEntryId = statusEntryId\n }\n delete finalInput.statusValue\n }\n\n // Build synthetic CommandRuntimeContext for workflow execution\n // Use nil UUID for system actions when no user context is available\n const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'\n const ctx = {\n container,\n auth: {\n sub: context.userId || SYSTEM_USER_ID,\n tenantId: context.workflowInstance.tenantId,\n orgId: context.workflowInstance.organizationId,\n isSuperAdmin: false,\n },\n organizationScope: null,\n selectedOrganizationId: context.workflowInstance.organizationId,\n organizationIds: context.workflowInstance.organizationId\n ? [context.workflowInstance.organizationId]\n : null,\n }\n\n // Execute the command\n const { result, logEntry } = await commandBus.execute(commandId, {\n input: finalInput,\n ctx,\n })\n\n return {\n executed: true,\n commandId,\n result,\n logEntryId: logEntry?.id,\n }\n}\n\n/**\n * Helper to resolve dictionary entry ID by value\n */\nasync function resolveDictionaryEntryId(\n em: EntityManager,\n dictionaryKey: string,\n value: string,\n tenantId: string,\n organizationId: string\n): Promise<string | null> {\n try {\n // Import here to avoid circular dependencies\n const { Dictionary, DictionaryEntry } = await import('@open-mercato/core/modules/dictionaries/data/entities')\n\n // Find the dictionary\n const dictionary = await em.findOne(Dictionary, {\n key: dictionaryKey,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!dictionary) {\n logger.warn('Dictionary not found', { component: 'UPDATE_ENTITY', dictionaryKey })\n return null\n }\n\n // Find the entry by normalized value\n const normalizedValue = value.toLowerCase().trim()\n const entry = await em.findOne(DictionaryEntry, {\n dictionary: dictionary.id,\n tenantId,\n organizationId,\n normalizedValue,\n })\n\n if (!entry) {\n logger.warn('Dictionary entry not found', { component: 'UPDATE_ENTITY', dictionaryKey, value })\n return null\n }\n\n return entry.id\n } catch (error) {\n logger.error('Error resolving dictionary entry', { component: 'UPDATE_ENTITY', err: error })\n return null\n }\n}\n\n/**\n * CALL_WEBHOOK activity handler\n *\n * Makes HTTP request to an external URL. Applies shared SSRF guard\n * (protocol / credentials / blocked host / private IP literal / DNS rebinding)\n * before issuing the request and rejects any 3xx redirect rather than following.\n */\nexport type CallWebhookDeps = {\n lookupHost?: HostLookup\n allowPrivate?: boolean\n fetchImpl?: typeof fetch\n signal?: AbortSignal\n}\n\nexport async function executeCallWebhook(\n config: unknown,\n context: ActivityContext,\n deps: CallWebhookDeps = {}\n): Promise<any> {\n const parsed = callWebhookConfigSchema.safeParse(config)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => `${issue.path.join('.') || 'config'}: ${issue.message}`)\n .join('; ')\n throw new Error(`CALL_WEBHOOK config invalid: ${issues}`)\n }\n const { url, method, headers: rawHeaders, body } = parsed.data\n const headers = rawHeaders ?? {}\n\n const allowPrivate = deps.allowPrivate ?? isAllowPrivateWorkflowWebhookUrlsEnabled()\n\n let response: Response\n try {\n response = await safeOutboundFetch(\n url,\n {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,\n redirect: 'manual',\n signal: deps.signal,\n },\n {\n subject: 'Workflow webhook URL',\n allowPrivate,\n lookupHost: deps.lookupHost,\n fetchImpl: deps.fetchImpl,\n },\n )\n } catch (error) {\n if (error instanceof UnsafeOutboundUrlError) {\n throw new Error(\n `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`\n )\n }\n throw error\n }\n\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location')\n throw new Error(\n `CALL_WEBHOOK refused to follow redirect ${response.status} to ${\n location ?? '(no Location header)'\n }`\n )\n }\n\n // Parse response\n let result: any\n const contentType = response.headers.get('content-type')\n\n if (contentType && contentType.includes('application/json')) {\n result = await response.json()\n } else {\n result = await response.text()\n }\n\n // Check for HTTP errors\n if (!response.ok) {\n throw new Error(\n `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`\n )\n }\n\n return {\n status: response.status,\n statusText: response.statusText,\n result,\n }\n}\n\n/**\n * EXECUTE_FUNCTION activity handler\n *\n * Calls a registered function from DI container\n */\nexport async function executeFunction(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { functionName, args = {} } = config\n\n if (!functionName) {\n throw new Error('EXECUTE_FUNCTION requires \"functionName\" field')\n }\n\n // Look up function in container\n const fnKey = `workflowFunction:${functionName}`\n\n try {\n const fn = container.resolve(fnKey)\n\n if (typeof fn !== 'function') {\n throw new Error(`Registered workflow function \"${functionName}\" is not a function`)\n }\n\n // Call function with args and context\n const result = await fn(args, context)\n\n return { executed: true, functionName, result }\n } catch (error) {\n if (error instanceof Error && error.message.includes('not registered')) {\n throw new Error(\n `Workflow function \"${functionName}\" not registered in DI container (key: ${fnKey})`\n )\n }\n throw error\n }\n}\n\n/**\n * Calculate delay in milliseconds from a WAIT activity config.\n * Supports either `duration` (relative, e.g. \"PT5M\") or `until` (absolute ISO 8601 datetime).\n */\nfunction calculateWaitDelayMs(config: { duration?: string; until?: string }): number {\n if (config.until) {\n const targetDate = new Date(config.until)\n if (isNaN(targetDate.getTime())) {\n throw new Error(`WAIT activity: invalid \"until\" datetime: ${config.until}`)\n }\n const delayMs = targetDate.getTime() - Date.now()\n return Math.max(0, delayMs)\n }\n\n if (config.duration) {\n return parseDuration(config.duration)\n }\n\n throw new Error('WAIT activity requires \"duration\" (e.g., \"PT5M\", \"1h\") or \"until\" (ISO 8601 datetime)')\n}\n\n/**\n * WAIT activity handler\n *\n * Delays workflow execution for a configured duration or until a specific datetime.\n * - `duration`: relative delay (e.g. \"PT5M\", \"1h\", \"30s\")\n * - `until`: absolute datetime (e.g. \"2026-04-15T10:00:00Z\")\n * - Sync mode: blocks via sleep (suitable for short delays)\n * - Async mode: delay is handled by the queue's delayMs option;\n * this handler returns immediately when called from the worker\n */\nasync function executeWait(config: any): Promise<any> {\n const durationMs = calculateWaitDelayMs(config)\n\n // In sync mode, actually sleep for the duration\n // In async mode (called from worker), the delay already happened via queue scheduling\n await sleep(durationMs)\n\n return { waited: true, durationMs }\n}\n\n/**\n * CALL_API activity handler\n *\n * Makes authenticated HTTP request to internal Open Mercato APIs\n * - Automatically creates one-time API key for authentication\n * - Injects tenant/organization context headers\n * - Validates URL security (SSRF prevention)\n * - Classifies errors (retriable vs non-retriable)\n * - Deletes API key after request (no stored credentials!)\n */\nexport async function executeCallApi(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer,\n signal?: AbortSignal\n): Promise<any> {\n // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, allowlisted {{env.*}}, {{now}})\n const interpolatedConfig = interpolateVariables(config, context.workflowContext, context.workflowInstance)\n\n const {\n endpoint,\n method = 'GET',\n headers = {},\n body,\n validateTenantMatch = true,\n } = interpolatedConfig\n\n\n if (!endpoint) {\n throw new Error('CALL_API requires \"endpoint\" field')\n }\n\n // 2. Build full URL (prepend APP_URL for relative paths)\n const fullUrl = buildApiUrl(endpoint)\n\n // 3. Import the one-time API key helper\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n\n // 4. Get EntityManager from container (for correct type)\n const apiKeyEm = container.resolve<PostgreSqlEntityManager>('em')\n\n // 5. Resolve the roles that the one-time API key will inherit.\n //\n // SECURITY: The key must never exceed the permissions of the human who\n // triggered (or authored) this workflow. Previously this code looked up\n // a role named \"admin\"/\"superadmin\" for the tenant and assigned it to\n // the key \u2014 which allowed any non-admin workflow author with\n // `workflows.definitions.edit` + `workflows.instances.create` to issue\n // arbitrary administrative API calls via a CALL_API activity. See the\n // SECURITY.md changelog entry for this fix.\n //\n // The resolution strategy is:\n // 1. Use the workflow instance's `metadata.initiatedBy` user (whoever\n // manually started the instance), when available. Only this user's\n // current active roles are used \u2014 we never fall back to the author\n // when the initiator is known, because that would escalate the\n // initiator's privileges.\n // 2. Fall back to the workflow definition's `createdBy` (author) only\n // when the instance was started by an event trigger with no user.\n // 3. If no traceable principal exists, the activity refuses to run \u2014\n // there is no \"system\" fallback that bypasses RBAC.\n const resolvedRoleIds = await resolveCallApiRoleIds(apiKeyEm, context.workflowInstance)\n\n if (resolvedRoleIds.length === 0) {\n throw new Error(\n `[CALL_API] Refusing to execute CALL_API for workflow instance ${context.workflowInstance.id}: ` +\n `no traceable user roles could be resolved from the workflow instance or definition. ` +\n `CALL_API activities must run under the identity of the user who triggered them.`\n )\n }\n\n // 6. Execute request with one-time API key scoped to the resolved user's roles\n return await withOnetimeApiKey(\n apiKeyEm,\n {\n name: `__workflow_${context.workflowInstance.id}__`,\n description: `One-time key for workflow ${context.workflowInstance.workflowId} instance ${context.workflowInstance.id}`,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n roles: resolvedRoleIds,\n expiresAt: null,\n },\n async (apiKeySecret) => {\n // Build request headers (auth + context + custom)\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': context.workflowInstance.tenantId,\n 'X-Organization-Id': context.workflowInstance.organizationId,\n 'X-Workflow-Instance-Id': context.workflowInstance.id,\n ...headers,\n }\n\n // Make HTTP request\n const response = await fetch(fullUrl, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n signal,\n })\n\n // Parse response body (JSON-safe)\n let responseBody: any\n const contentType = response.headers.get('content-type')\n\n try {\n if (contentType && contentType.includes('application/json')) {\n responseBody = await response.json()\n } else {\n responseBody = await response.text()\n }\n } catch (error) {\n responseBody = null\n }\n\n // Check for HTTP errors and classify\n if (!response.ok) {\n classifyAndThrowError(response.status, responseBody, fullUrl)\n }\n\n // Validate tenant match (security check)\n if (validateTenantMatch && responseBody && typeof responseBody === 'object') {\n if (responseBody.tenantId && responseBody.tenantId !== context.workflowInstance.tenantId) {\n throw new Error(\n `Tenant ID mismatch: workflow expects ${context.workflowInstance.tenantId} but API returned ${responseBody.tenantId}`\n )\n }\n }\n\n // Return structured result\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: responseBody,\n authenticated: true,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n }\n }\n )\n}\n\n// ============================================================================\n// CALL_API Helper Functions\n// ============================================================================\n\nexport type CallApiInstanceLike = {\n id: string\n tenantId: string\n organizationId: string\n definitionId: string\n metadata?: { initiatedBy?: string | null } | null\n}\n\nasync function resolveActiveRoleIdsForUser(\n em: any,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<string[]> {\n const { findOneWithDecryption, findWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const { User, UserRole, Role } = await import('../../auth/data/entities')\n\n const user = await findOneWithDecryption(em, User, {\n id: userId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }, {}, scope)\n if (!user) return []\n\n const userRoles = await findWithDecryption(\n em,\n UserRole,\n { user: user.id, deletedAt: null },\n { populate: ['role'] },\n scope,\n )\n const roleIds = userRoles\n .map((ur: any) => (typeof ur.role === 'string' ? ur.role : ur.role?.id))\n .filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)\n\n if (roleIds.length === 0) return []\n\n const scopedRoles = await findWithDecryption(em, Role, {\n id: { $in: roleIds },\n tenantId: scope.tenantId,\n deletedAt: null,\n }, {}, scope)\n return scopedRoles.map((r: any) => r.id as string)\n}\n\nexport async function resolveCallApiRoleIds(\n em: any,\n instance: CallApiInstanceLike\n): Promise<string[]> {\n if (!instance.definitionId) return []\n\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const { WorkflowDefinition } = await import('../data/entities')\n\n const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }\n\n // 1. Prefer the triggering user (whoever manually started this instance).\n // WorkflowInstance.metadata.initiatedBy is the canonical record of that\n // principal for user-started instances; use their current role set so\n // CALL_API never exceeds the initiator's permissions. Refuse if the\n // initiator has no active scoped roles \u2014 do not fall back to the\n // definition author, which would escalate the initiator's privileges.\n const initiatorUserId = instance.metadata?.initiatedBy ?? null\n if (initiatorUserId) {\n return resolveActiveRoleIdsForUser(em, initiatorUserId, scope)\n }\n\n // 2. Event-triggered instance with no human initiator: fall back to the\n // definition author. Soft-deleted definitions must not mint keys.\n const definition = await findOneWithDecryption(em, WorkflowDefinition, {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n deletedAt: null,\n }, {}, scope)\n const authorUserId = definition?.createdBy\n if (!authorUserId) return []\n\n return resolveActiveRoleIdsForUser(em, authorUserId, scope)\n}\n\n/**\n * Build full API URL from endpoint\n * - Relative paths (/api/...) \u2192 prepend APP_URL\n * - Absolute URLs \u2192 validate domain matches APP_URL (SSRF prevention)\n */\nfunction buildApiUrl(endpoint: string): string {\n const appUrl = process.env.APP_URL || 'http://localhost:3000'\n\n // Relative path - prepend APP_URL\n if (endpoint.startsWith('/')) {\n // Security: Only allow /api/* paths\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_API only supports /api/* paths, got: ${endpoint}`)\n }\n return `${appUrl}${endpoint}`\n }\n\n // Absolute URL - validate domain matches APP_URL (SSRF prevention)\n try {\n const endpointUrl = new URL(endpoint)\n const appUrlObj = new URL(appUrl)\n\n if (endpointUrl.host !== appUrlObj.host) {\n throw new Error(\n `SSRF Prevention: CALL_API endpoint domain (${endpointUrl.host}) does not match APP_URL (${appUrlObj.host})`\n )\n }\n\n return endpoint\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error(`Invalid endpoint URL: ${endpoint}`)\n }\n throw error\n }\n}\n\n/**\n * Classify HTTP error and throw appropriate error\n * - 400-499: Non-retriable (client error - validation/auth)\n * - 500-599: Retriable (server error)\n */\nfunction classifyAndThrowError(status: number, body: any, url: string): never {\n const bodyStr = typeof body === 'string' ? body : JSON.stringify(body)\n\n if (status >= 400 && status < 500) {\n // Client errors - non-retriable\n throw new Error(\n `CALL_API request failed with status ${status} (non-retriable): ${bodyStr}`\n )\n }\n\n if (status >= 500) {\n // Server errors - retriable\n const error: any = new Error(\n `CALL_API request failed with status ${status} (retriable): ${bodyStr}`\n )\n error.retriable = true\n throw error\n }\n\n // Other errors\n throw new Error(`CALL_API request failed with status ${status}: ${bodyStr}`)\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Interpolate variables in config from workflow context\n *\n * Supports syntax:\n * - {{context.field}} or {{context.nested.field}} - from workflow context\n * - {{workflow.instanceId}} - workflow instance ID\n * - {{workflow.tenantId}} - tenant ID\n * - {{workflow.organizationId}} - organization ID\n * - {{workflow.currentStepId}} - current step ID\n * - {{env.VAR_NAME}} - server-allowlisted environment variables\n * - {{now}} - current ISO timestamp\n */\nfunction interpolateVariables(\n config: any,\n context: Record<string, any>,\n workflowInstance?: WorkflowInstance\n): any {\n if (typeof config === 'string') {\n // Check if this is a single variable reference (e.g., \"{{context.cart.items}}\")\n // This preserves the original type (array, object, number, boolean)\n const singleVarMatch = config.match(/^\\{\\{([^}]+)\\}\\}$/)\n\n if (singleVarMatch) {\n const trimmedPath = singleVarMatch[1].trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return workflowInstance.version // Return as number\n default:\n return config // Return original if unknown\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return resolveWorkflowEnvInterpolation(envKey)\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? value : config // Return raw value to preserve type\n }\n\n // Multiple interpolations or mixed text - return string\n return config.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return String(workflowInstance.version)\n default:\n return match // Unknown workflow key\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return resolveWorkflowEnvInterpolation(envKey)\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? String(value) : match\n })\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => interpolateVariables(item, context, workflowInstance))\n }\n\n if (config && typeof config === 'object') {\n const result: Record<string, any> = {}\n for (const [key, value] of Object.entries(config)) {\n result[key] = interpolateVariables(value, context, workflowInstance)\n }\n return result\n }\n\n return config\n}\n\n/**\n * Get nested value from object by path (e.g., \"user.email\")\n */\nfunction getNestedValue(obj: any, path: string): any {\n const parts = path.split('.')\n let value = obj\n\n for (const part of parts) {\n if (value && typeof value === 'object' && part in value) {\n value = value[part]\n } else {\n return undefined\n }\n }\n\n return value\n}\n\n/**\n * Calculate exponential backoff delay\n */\nfunction calculateBackoff(\n initialIntervalMs: number,\n backoffCoefficient: number,\n attempt: number,\n maxIntervalMs: number\n): number {\n const backoff = initialIntervalMs * Math.pow(backoffCoefficient, attempt)\n return Math.min(backoff, maxIntervalMs || Infinity)\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Execute a promise with timeout\n */\nasync function executeWithTimeout<T>(\n executor: () => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(), timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * Workflows Module - Activity Executor Service\n *\n * Executes workflow activities (send email, call API, emit events, etc.)\n * - Supports multiple activity types\n * - Implements retry logic with exponential backoff\n * - Handles timeouts\n * - Variable interpolation from workflow context\n *\n * Functional API (no classes) following Open Mercato conventions.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { createModuleQueue, Queue } from '@open-mercato/queue'\nimport { getRedisUrl } from '@open-mercato/shared/lib/redis/connection'\nimport {\n safeOutboundFetch,\n UnsafeOutboundUrlError,\n type HostLookup,\n} from '@open-mercato/shared/lib/url-safety'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { callWebhookConfigSchema } from '../data/validators'\nimport { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'\nimport { logWorkflowEvent } from './event-logger'\nimport { parseDuration } from './duration'\n\nexport { isPrivateUrl } from '@open-mercato/shared/lib/network'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nfunction isAllowPrivateWorkflowWebhookUrlsEnabled(): boolean {\n if (parseBooleanWithDefault(process.env.OM_WORKFLOWS_ALLOW_PRIVATE_URLS, false)) {\n if (process.env.NODE_ENV === 'production') {\n logger.warn('OM_WORKFLOWS_ALLOW_PRIVATE_URLS is set but ignored in production. SSRF protection remains enabled.', { component: 'CALL_WEBHOOK' })\n return false\n }\n\n logger.warn('OM_WORKFLOWS_ALLOW_PRIVATE_URLS is enabled. SSRF protection is bypassed for workflow webhooks; use only in development.', { component: 'CALL_WEBHOOK' })\n return true\n }\n\n if (parseBooleanWithDefault(process.env.WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS, false)) {\n if (process.env.NODE_ENV === 'production') {\n logger.warn('WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated and ignored in production. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS for development only. SSRF protection remains enabled.', { component: 'CALL_WEBHOOK' })\n return false\n }\n\n logger.warn('WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS instead. SSRF protection is bypassed.', { component: 'CALL_WEBHOOK' })\n return true\n }\n\n return false\n}\n\nconst DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST = new Set(['APP_URL'])\nconst WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY = 'OM_WORKFLOWS_ENV_INTERPOLATION_ALLOWLIST'\n\nfunction getWorkflowEnvInterpolationAllowlist(): Set<string> {\n const allowlist = new Set(DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST)\n const configuredKeys = process.env[WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY]\n if (!configuredKeys) {\n return allowlist\n }\n\n for (const key of configuredKeys.split(',')) {\n const trimmedKey = key.trim()\n if (trimmedKey) {\n allowlist.add(trimmedKey)\n }\n }\n\n return allowlist\n}\n\nfunction resolveWorkflowEnvInterpolation(envKey: string): string {\n if (!getWorkflowEnvInterpolationAllowlist().has(envKey)) {\n return ''\n }\n\n return process.env[envKey] ?? ''\n}\n\n// ============================================================================\n// Types and Interfaces\n// ============================================================================\n\nexport type ActivityType =\n | 'SEND_EMAIL'\n | 'CALL_API'\n | 'EMIT_EVENT'\n | 'UPDATE_ENTITY'\n | 'CALL_WEBHOOK'\n | 'EXECUTE_FUNCTION'\n | 'WAIT'\n\nexport interface ActivityDefinition {\n activityId: string // Unique identifier for activity\n activityName?: string // Optional, for debugging/logging\n activityType: ActivityType\n config: any\n async?: boolean // Flag to execute activity asynchronously via queue\n retryPolicy?: RetryPolicy\n timeoutMs?: number\n compensate?: boolean // Flag to execute compensation on failure\n}\n\nexport interface RetryPolicy {\n maxAttempts: number\n initialIntervalMs: number\n backoffCoefficient: number\n maxIntervalMs: number\n}\n\nexport interface ActivityContext {\n workflowInstance: WorkflowInstance\n workflowContext: Record<string, any>\n stepContext?: Record<string, any>\n stepInstanceId?: string\n // Set when the activity runs inside a parallel branch; carried on the queue\n // payload so async resume targets the branch rather than the instance.\n branchInstanceId?: string | null\n transitionId?: string\n userId?: string\n}\n\nexport interface ActivityExecutionResult {\n activityId: string\n activityName?: string\n activityType: ActivityType\n success: boolean\n output?: any\n error?: string\n retryCount: number\n executionTimeMs: number\n async?: boolean // Marks activity as async (queued)\n jobId?: string // Queue job ID for async activities\n}\n\nexport class ActivityExecutionError extends Error {\n constructor(\n message: string,\n public activityType: ActivityType,\n public activityName?: string,\n public details?: any\n ) {\n super(message)\n this.name = 'ActivityExecutionError'\n }\n}\n\n// ============================================================================\n// Queue Integration for Async Activities\n// ============================================================================\n\nlet activityQueue: Queue<WorkflowActivityJob> | null = null\n\n/**\n * Get or create the activity queue (lazy initialization)\n */\nfunction getActivityQueue(): Queue<WorkflowActivityJob> {\n if (!activityQueue) {\n activityQueue = createModuleQueue<WorkflowActivityJob>(\n WORKFLOW_ACTIVITIES_QUEUE_NAME,\n { concurrency: parseInt(process.env.WORKFLOW_WORKER_CONCURRENCY || '5') },\n )\n }\n\n return activityQueue\n}\n\n/**\n * Enqueue an activity for background execution\n *\n * @param em - Entity manager\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Job ID\n */\nexport async function enqueueActivity(\n em: EntityManager,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<string> {\n const { workflowInstance, workflowContext, stepContext, transitionId, stepInstanceId, branchInstanceId } =\n context\n\n // Interpolate config variables NOW (before queuing)\n const interpolatedConfig = interpolateVariables(activity.config, workflowContext, workflowInstance)\n\n // Create job payload\n const job: WorkflowActivityJob = {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n branchInstanceId: branchInstanceId ?? undefined,\n transitionId,\n activityId: activity.activityId,\n activityName: activity.activityName || activity.activityType,\n activityType: activity.activityType,\n activityConfig: interpolatedConfig,\n workflowContext,\n stepContext,\n retryPolicy: activity.retryPolicy,\n timeoutMs: activity.timeoutMs,\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n userId: context.userId,\n }\n\n // Enqueue to queue (WAIT activities use delayMs for the actual delay)\n const queue = getActivityQueue()\n const enqueueOptions = activity.activityType === 'WAIT' && (interpolatedConfig.duration || interpolatedConfig.until)\n ? { delayMs: calculateWaitDelayMs(interpolatedConfig) }\n : undefined\n const jobId = await queue.enqueue(job, enqueueOptions)\n\n // Log event\n await logWorkflowEvent(em, {\n workflowInstanceId: workflowInstance.id,\n stepInstanceId,\n eventType: 'ACTIVITY_QUEUED',\n eventData: {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n async: true,\n jobId,\n },\n tenantId: workflowInstance.tenantId,\n organizationId: workflowInstance.organizationId,\n })\n\n return jobId\n}\n\n/**\n * Enqueue a delayed timer job for a WAIT_FOR_TIMER step.\n *\n * The activity worker handles `kind: 'timer'` jobs by calling\n * `timerHandler.fireTimer`, which resumes the paused workflow instance.\n */\nexport async function enqueueTimerJob(params: {\n workflowInstanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n tenantId: string\n organizationId: string\n userId?: string\n fireAt: string\n delayMs: number\n}): Promise<string> {\n const { workflowInstanceId, stepInstanceId, branchInstanceId, tenantId, organizationId, userId, fireAt, delayMs } =\n params\n\n const queue = getActivityQueue()\n const jobId = await queue.enqueue(\n {\n kind: 'timer',\n workflowInstanceId,\n stepInstanceId,\n branchInstanceId: branchInstanceId ?? undefined,\n tenantId,\n organizationId,\n userId,\n fireAt,\n },\n { delayMs: delayMs > 0 ? delayMs : undefined }\n )\n\n return jobId\n}\n\n// ============================================================================\n// Main Activity Execution Functions\n// ============================================================================\n\n/**\n * Execute a single activity with retry logic and timeout\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activity - Activity definition\n * @param context - Execution context\n * @returns Execution result\n */\nexport async function executeActivity(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<ActivityExecutionResult> {\n const retryPolicy = activity.retryPolicy || {\n maxAttempts: 1,\n initialIntervalMs: 0,\n backoffCoefficient: 1,\n maxIntervalMs: 0,\n }\n\n let lastError: any\n let retryCount = 0\n\n for (let attempt = 0; attempt < retryPolicy.maxAttempts; attempt++) {\n try {\n const startTime = Date.now()\n\n // Execute with timeout if specified\n const result = activity.timeoutMs\n ? await executeWithTimeout(\n () => executeActivityByType(em, container, activity, context),\n activity.timeoutMs\n )\n : await executeActivityByType(em, container, activity, context)\n\n const executionTimeMs = Date.now() - startTime\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true,\n output: result,\n retryCount: attempt,\n executionTimeMs,\n async: activity.async || false,\n }\n } catch (error) {\n lastError = error\n retryCount = attempt + 1\n\n // Log activity retry attempt with context\n if (attempt < retryPolicy.maxAttempts - 1) {\n logger.error('Activity failed; will retry', {\n activityId: activity.activityId,\n activityType: activity.activityType,\n attempt: attempt + 1,\n maxAttempts: retryPolicy.maxAttempts,\n instanceId: context.workflowInstance.id,\n err: error,\n })\n }\n\n // If not the last attempt, apply backoff and retry\n if (attempt < retryPolicy.maxAttempts - 1) {\n const backoff = calculateBackoff(\n retryPolicy.initialIntervalMs,\n retryPolicy.backoffCoefficient,\n attempt,\n retryPolicy.maxIntervalMs\n )\n\n await sleep(backoff)\n }\n }\n }\n\n // All retries exhausted\n const errorMessage = lastError instanceof Error ? lastError.message : String(lastError)\n logger.error('Activity failed after all attempts', {\n activityId: activity.activityId,\n activityType: activity.activityType,\n attempts: retryCount,\n instanceId: context.workflowInstance.id,\n err: lastError,\n })\n\n return {\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: false,\n error: `Activity failed after ${retryCount} attempts: ${errorMessage}`,\n retryCount,\n executionTimeMs: 0,\n async: activity.async || false,\n }\n}\n\n/**\n * Execute multiple activities in sequence\n * Supports both synchronous and asynchronous (queued) execution\n *\n * @param em - Entity manager\n * @param container - DI container\n * @param activities - Array of activity definitions\n * @param context - Execution context\n * @returns Array of execution results\n */\nexport async function executeActivities(\n em: EntityManager,\n container: AwilixContainer,\n activities: ActivityDefinition[],\n context: ActivityContext\n): Promise<ActivityExecutionResult[]> {\n const results: ActivityExecutionResult[] = []\n\n for (let i = 0; i < activities.length; i++) {\n const activity = activities[i]\n\n // Check if activity should run async\n if (activity.async) {\n // Enqueue for background execution\n const jobId = await enqueueActivity(em, activity, context)\n\n results.push({\n activityId: activity.activityId,\n activityName: activity.activityName,\n activityType: activity.activityType,\n success: true, // Queued successfully\n async: true,\n jobId,\n retryCount: 0,\n executionTimeMs: 0,\n })\n } else {\n // Execute synchronously (existing logic)\n const result = await executeActivity(em, container, activity, context)\n results.push(result)\n\n // Stop execution if activity fails (fail-fast)\n if (!result.success) {\n break\n }\n\n // Update workflow context with activity output\n if (result.output && typeof result.output === 'object') {\n const key = activity.activityName || activity.activityType\n context.workflowContext = {\n ...context.workflowContext,\n [key]: result.output,\n }\n }\n }\n }\n\n return results\n}\n\n// ============================================================================\n// Activity Type Handlers\n// ============================================================================\n\n/**\n * Execute activity based on its type\n */\nasync function executeActivityByType(\n em: EntityManager,\n container: AwilixContainer,\n activity: ActivityDefinition,\n context: ActivityContext\n): Promise<any> {\n // Interpolate config variables from context (including workflow metadata)\n const interpolatedConfig = interpolateVariables(activity.config, context.workflowContext, context.workflowInstance)\n\n switch (activity.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(interpolatedConfig, context, container)\n\n case 'CALL_API':\n return await executeCallApi(em, interpolatedConfig, context, container)\n\n case 'EMIT_EVENT':\n return await executeEmitEvent(interpolatedConfig, context, container)\n\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(em, interpolatedConfig, context, container)\n\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(interpolatedConfig, context)\n\n case 'EXECUTE_FUNCTION':\n return await executeFunction(interpolatedConfig, context, container)\n\n case 'WAIT':\n return await executeWait(interpolatedConfig)\n\n default:\n throw new ActivityExecutionError(\n `Unknown activity type: ${activity.activityType}`,\n activity.activityType,\n activity.activityName\n )\n }\n}\n\n/**\n * SEND_EMAIL activity handler\n *\n * For MVP, this logs the email (actual email sending can be added later)\n */\nexport async function executeSendEmail(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { to, subject, template, templateData, body } = config\n\n if (!to || !subject) {\n throw new Error('SEND_EMAIL requires \"to\" and \"subject\" fields')\n }\n\n // For MVP: Log the email (actual email service integration can be added later)\n logger.info('Send email activity invoked', { component: 'SEND_EMAIL', subject })\n\n // Check if email service is available in container\n try {\n const emailService = container.resolve<{ send: (input: unknown) => Promise<unknown> | unknown }>('emailService')\n if (emailService && typeof emailService.send === 'function') {\n await emailService.send({\n to,\n subject,\n template,\n templateData,\n body,\n })\n return { sent: true, to, subject, via: 'emailService' }\n }\n } catch (error) {\n // Email service not available, just log\n }\n\n return { sent: true, to, subject, via: 'console' }\n}\n\n/**\n * EMIT_EVENT activity handler\n *\n * Publishes a domain event to the event bus\n */\nexport async function executeEmitEvent(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { eventName, payload } = config\n\n if (!eventName) {\n throw new Error('EMIT_EVENT requires \"eventName\" field')\n }\n\n // Get event bus from container\n const eventBus = container.resolve<{ emitEvent: (event: string, payload: unknown, options?: unknown) => Promise<unknown> | unknown }>('eventBus')\n\n if (!eventBus || typeof eventBus.emitEvent !== 'function') {\n throw new Error('Event bus not available in container')\n }\n\n // Publish event with workflow metadata\n const enrichedPayload = {\n ...payload,\n _workflow: {\n workflowInstanceId: context.workflowInstance.id,\n workflowId: context.workflowInstance.workflowId,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n },\n }\n\n await eventBus.emitEvent(eventName, enrichedPayload, {\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n })\n\n return { emitted: true, eventName, payload: enrichedPayload }\n}\n\n/**\n * UPDATE_ENTITY activity handler\n *\n * Updates an entity via CommandBus for proper audit logging, undo support, and side effects.\n *\n * Config format:\n * ```json\n * {\n * \"commandId\": \"sales.documents.update\",\n * \"input\": {\n * \"id\": \"{{context.orderId}}\",\n * \"statusEntryId\": \"{{context.approvedStatusId}}\"\n * }\n * }\n * ```\n *\n * Alternative format with statusValue (auto-resolves to statusEntryId):\n * ```json\n * {\n * \"commandId\": \"sales.orders.update\",\n * \"statusDictionary\": \"sales.order_status\",\n * \"input\": {\n * \"id\": \"{{context.id}}\",\n * \"statusValue\": \"pending_approval\"\n * }\n * }\n * ```\n */\nexport async function executeUpdateEntity(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { commandId, input, statusDictionary } = config\n\n if (!commandId) {\n throw new Error('UPDATE_ENTITY requires \"commandId\" field (e.g., \"sales.documents.update\")')\n }\n\n if (!input || typeof input !== 'object') {\n throw new Error('UPDATE_ENTITY requires \"input\" object with entity data')\n }\n\n // Resolve CommandBus from container\n const commandBus = container.resolve('commandBus') as any\n\n if (!commandBus || typeof commandBus.execute !== 'function') {\n throw new Error('CommandBus not available in container')\n }\n\n // Prepare final input, resolving statusValue if provided\n let finalInput = { ...input }\n\n // If statusValue is provided with a statusDictionary, resolve it to statusEntryId\n if (finalInput.statusValue && statusDictionary) {\n const statusEntryId = await resolveDictionaryEntryId(\n em,\n statusDictionary,\n finalInput.statusValue,\n context.workflowInstance.tenantId,\n context.workflowInstance.organizationId\n )\n if (statusEntryId) {\n finalInput.statusEntryId = statusEntryId\n }\n delete finalInput.statusValue\n }\n\n // Build synthetic CommandRuntimeContext for workflow execution\n // Use nil UUID for system actions when no user context is available\n const SYSTEM_USER_ID = '00000000-0000-0000-0000-000000000000'\n const ctx = {\n container,\n auth: {\n sub: context.userId || SYSTEM_USER_ID,\n tenantId: context.workflowInstance.tenantId,\n orgId: context.workflowInstance.organizationId,\n isSuperAdmin: false,\n },\n organizationScope: null,\n selectedOrganizationId: context.workflowInstance.organizationId,\n organizationIds: context.workflowInstance.organizationId\n ? [context.workflowInstance.organizationId]\n : null,\n }\n\n // Execute the command\n const { result, logEntry } = await commandBus.execute(commandId, {\n input: finalInput,\n ctx,\n })\n\n return {\n executed: true,\n commandId,\n result,\n logEntryId: logEntry?.id,\n }\n}\n\n/**\n * Helper to resolve dictionary entry ID by value\n */\nasync function resolveDictionaryEntryId(\n em: EntityManager,\n dictionaryKey: string,\n value: string,\n tenantId: string,\n organizationId: string\n): Promise<string | null> {\n try {\n // Import here to avoid circular dependencies\n const { Dictionary, DictionaryEntry } = await import('@open-mercato/core/modules/dictionaries/data/entities')\n\n // Find the dictionary\n const dictionary = await em.findOne(Dictionary, {\n key: dictionaryKey,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n\n if (!dictionary) {\n logger.warn('Dictionary not found', { component: 'UPDATE_ENTITY', dictionaryKey })\n return null\n }\n\n // Find the entry by normalized value\n const normalizedValue = value.toLowerCase().trim()\n const entry = await em.findOne(DictionaryEntry, {\n dictionary: dictionary.id,\n tenantId,\n organizationId,\n normalizedValue,\n })\n\n if (!entry) {\n logger.warn('Dictionary entry not found', { component: 'UPDATE_ENTITY', dictionaryKey, value })\n return null\n }\n\n return entry.id\n } catch (error) {\n logger.error('Error resolving dictionary entry', { component: 'UPDATE_ENTITY', err: error })\n return null\n }\n}\n\n/**\n * CALL_WEBHOOK activity handler\n *\n * Makes HTTP request to an external URL. Applies shared SSRF guard\n * (protocol / credentials / blocked host / private IP literal / DNS rebinding)\n * before issuing the request and rejects any 3xx redirect rather than following.\n */\nexport type CallWebhookDeps = {\n lookupHost?: HostLookup\n allowPrivate?: boolean\n fetchImpl?: typeof fetch\n signal?: AbortSignal\n}\n\nexport async function executeCallWebhook(\n config: unknown,\n context: ActivityContext,\n deps: CallWebhookDeps = {}\n): Promise<any> {\n const parsed = callWebhookConfigSchema.safeParse(config)\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map((issue) => `${issue.path.join('.') || 'config'}: ${issue.message}`)\n .join('; ')\n throw new Error(`CALL_WEBHOOK config invalid: ${issues}`)\n }\n const { url, method, headers: rawHeaders, body } = parsed.data\n const headers = rawHeaders ?? {}\n\n const allowPrivate = deps.allowPrivate ?? isAllowPrivateWorkflowWebhookUrlsEnabled()\n\n let response: Response\n try {\n response = await safeOutboundFetch(\n url,\n {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,\n redirect: 'manual',\n signal: deps.signal,\n },\n {\n subject: 'Workflow webhook URL',\n allowPrivate,\n lookupHost: deps.lookupHost,\n fetchImpl: deps.fetchImpl,\n },\n )\n } catch (error) {\n if (error instanceof UnsafeOutboundUrlError) {\n throw new Error(\n `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`\n )\n }\n throw error\n }\n\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get('location')\n throw new Error(\n `CALL_WEBHOOK refused to follow redirect ${response.status} to ${\n location ?? '(no Location header)'\n }`\n )\n }\n\n // Parse response\n let result: any\n const contentType = response.headers.get('content-type')\n\n if (contentType && contentType.includes('application/json')) {\n result = await response.json()\n } else {\n result = await response.text()\n }\n\n // Check for HTTP errors\n if (!response.ok) {\n throw new Error(\n `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`\n )\n }\n\n return {\n status: response.status,\n statusText: response.statusText,\n result,\n }\n}\n\n/**\n * EXECUTE_FUNCTION activity handler\n *\n * Calls a registered function from DI container\n */\nexport async function executeFunction(\n config: any,\n context: ActivityContext,\n container: AwilixContainer\n): Promise<any> {\n const { functionName, args = {} } = config\n\n if (!functionName) {\n throw new Error('EXECUTE_FUNCTION requires \"functionName\" field')\n }\n\n // Look up function in container\n const fnKey = `workflowFunction:${functionName}`\n\n try {\n const fn = container.resolve(fnKey)\n\n if (typeof fn !== 'function') {\n throw new Error(`Registered workflow function \"${functionName}\" is not a function`)\n }\n\n // Call function with args and context\n const result = await fn(args, context)\n\n return { executed: true, functionName, result }\n } catch (error) {\n if (error instanceof Error && error.message.includes('not registered')) {\n throw new Error(\n `Workflow function \"${functionName}\" not registered in DI container (key: ${fnKey})`\n )\n }\n throw error\n }\n}\n\n/**\n * Calculate delay in milliseconds from a WAIT activity config.\n * Supports either `duration` (relative, e.g. \"PT5M\") or `until` (absolute ISO 8601 datetime).\n */\nfunction calculateWaitDelayMs(config: { duration?: string; until?: string }): number {\n if (config.until) {\n const targetDate = new Date(config.until)\n if (isNaN(targetDate.getTime())) {\n throw new Error(`WAIT activity: invalid \"until\" datetime: ${config.until}`)\n }\n const delayMs = targetDate.getTime() - Date.now()\n return Math.max(0, delayMs)\n }\n\n if (config.duration) {\n return parseDuration(config.duration)\n }\n\n throw new Error('WAIT activity requires \"duration\" (e.g., \"PT5M\", \"1h\") or \"until\" (ISO 8601 datetime)')\n}\n\n/**\n * WAIT activity handler\n *\n * Delays workflow execution for a configured duration or until a specific datetime.\n * - `duration`: relative delay (e.g. \"PT5M\", \"1h\", \"30s\")\n * - `until`: absolute datetime (e.g. \"2026-04-15T10:00:00Z\")\n * - Sync mode: blocks via sleep (suitable for short delays)\n * - Async mode: delay is handled by the queue's delayMs option;\n * this handler returns immediately when called from the worker\n */\nasync function executeWait(config: any): Promise<any> {\n const durationMs = calculateWaitDelayMs(config)\n\n // In sync mode, actually sleep for the duration\n // In async mode (called from worker), the delay already happened via queue scheduling\n await sleep(durationMs)\n\n return { waited: true, durationMs }\n}\n\n/**\n * CALL_API activity handler\n *\n * Makes authenticated HTTP request to internal Open Mercato APIs\n * - Automatically creates one-time API key for authentication\n * - Injects tenant/organization context headers\n * - Validates URL security (SSRF prevention)\n * - Classifies errors (retriable vs non-retriable)\n * - Deletes API key after request (no stored credentials!)\n */\nexport async function executeCallApi(\n em: EntityManager,\n config: any,\n context: ActivityContext,\n container: AwilixContainer,\n signal?: AbortSignal\n): Promise<any> {\n // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, allowlisted {{env.*}}, {{now}})\n const interpolatedConfig = interpolateVariables(config, context.workflowContext, context.workflowInstance)\n\n const {\n endpoint,\n method = 'GET',\n headers = {},\n body,\n validateTenantMatch = true,\n } = interpolatedConfig\n\n\n if (!endpoint) {\n throw new Error('CALL_API requires \"endpoint\" field')\n }\n\n // 2. Build full URL (prepend APP_URL for relative paths)\n const fullUrl = buildApiUrl(endpoint)\n\n // 3. Import the one-time API key helper\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n\n // 4. Create the one-time API key on an EntityManager that is fully detached\n // from the surrounding request/transaction context.\n //\n // CALL_API runs inside `workflowExecutor.executeWorkflow()`, which wraps\n // the whole execution in `em.transactional(...)`. The request EM is forked\n // with `useContext: true`, so while that transaction is open, EVERY\n // operation on the container's `em` \u2014 including this API key's\n // persist/flush \u2014 is transparently redirected to the uncommitted\n // transaction fork (MikroORM `getContext()` \u2192 `TransactionContext`). The\n // key would therefore stay invisible until the transaction commits, but\n // the commit cannot happen until this activity returns. The outbound\n // self-authenticated `fetch` below opens a SEPARATE DB connection that\n // cannot see the uncommitted row, so the internal API responds `401` and\n // the activity fails (issue #4202).\n //\n // Forking with `useContext: false` (matching the query_index/webhooks\n // isolated-EM convention) gives the key its own pooled connection with\n // autocommit, so it is committed and visible to the internal request\n // immediately.\n const apiKeyEm = container\n .resolve<PostgreSqlEntityManager>('em')\n .fork({ clear: true, freshEventManager: true, useContext: false }) as PostgreSqlEntityManager\n\n // 5. Resolve the roles that the one-time API key will inherit.\n //\n // SECURITY: The key must never exceed the permissions of the human who\n // triggered (or authored) this workflow. Previously this code looked up\n // a role named \"admin\"/\"superadmin\" for the tenant and assigned it to\n // the key \u2014 which allowed any non-admin workflow author with\n // `workflows.definitions.edit` + `workflows.instances.create` to issue\n // arbitrary administrative API calls via a CALL_API activity. See the\n // SECURITY.md changelog entry for this fix.\n //\n // The resolution strategy is:\n // 1. Use the workflow instance's `metadata.initiatedBy` user (whoever\n // manually started the instance), when available. Only this user's\n // current active roles are used \u2014 we never fall back to the author\n // when the initiator is known, because that would escalate the\n // initiator's privileges.\n // 2. Fall back to the workflow definition's `createdBy` (author) only\n // when the instance was started by an event trigger with no user.\n // 3. If no traceable principal exists, the activity refuses to run \u2014\n // there is no \"system\" fallback that bypasses RBAC.\n const resolvedRoleIds = await resolveCallApiRoleIds(apiKeyEm, context.workflowInstance)\n\n if (resolvedRoleIds.length === 0) {\n throw new Error(\n `[CALL_API] Refusing to execute CALL_API for workflow instance ${context.workflowInstance.id}: ` +\n `no traceable user roles could be resolved from the workflow instance or definition. ` +\n `CALL_API activities must run under the identity of the user who triggered them.`\n )\n }\n\n // 6. Execute request with one-time API key scoped to the resolved user's roles\n return await withOnetimeApiKey(\n apiKeyEm,\n {\n name: `__workflow_${context.workflowInstance.id}__`,\n description: `One-time key for workflow ${context.workflowInstance.workflowId} instance ${context.workflowInstance.id}`,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n roles: resolvedRoleIds,\n expiresAt: null,\n },\n async (apiKeySecret) => {\n // Build request headers (auth + context + custom)\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': context.workflowInstance.tenantId,\n 'X-Organization-Id': context.workflowInstance.organizationId,\n 'X-Workflow-Instance-Id': context.workflowInstance.id,\n ...headers,\n }\n\n // Make HTTP request\n const response = await fetch(fullUrl, {\n method,\n headers: requestHeaders,\n body: body ? JSON.stringify(body) : undefined,\n signal,\n })\n\n // Parse response body (JSON-safe)\n let responseBody: any\n const contentType = response.headers.get('content-type')\n\n try {\n if (contentType && contentType.includes('application/json')) {\n responseBody = await response.json()\n } else {\n responseBody = await response.text()\n }\n } catch (error) {\n responseBody = null\n }\n\n // Check for HTTP errors and classify\n if (!response.ok) {\n classifyAndThrowError(response.status, responseBody, fullUrl)\n }\n\n // Validate tenant match (security check)\n if (validateTenantMatch && responseBody && typeof responseBody === 'object') {\n if (responseBody.tenantId && responseBody.tenantId !== context.workflowInstance.tenantId) {\n throw new Error(\n `Tenant ID mismatch: workflow expects ${context.workflowInstance.tenantId} but API returned ${responseBody.tenantId}`\n )\n }\n }\n\n // Return structured result\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: responseBody,\n authenticated: true,\n tenantId: context.workflowInstance.tenantId,\n organizationId: context.workflowInstance.organizationId,\n }\n }\n )\n}\n\n// ============================================================================\n// CALL_API Helper Functions\n// ============================================================================\n\nexport type CallApiInstanceLike = {\n id: string\n tenantId: string\n organizationId: string\n definitionId: string\n metadata?: { initiatedBy?: string | null } | null\n}\n\nasync function resolveActiveRoleIdsForUser(\n em: any,\n userId: string,\n scope: { tenantId: string; organizationId: string },\n): Promise<string[]> {\n const { findOneWithDecryption, findWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const { User, UserRole, Role } = await import('../../auth/data/entities')\n\n const user = await findOneWithDecryption(em, User, {\n id: userId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }, {}, scope)\n if (!user) return []\n\n const userRoles = await findWithDecryption(\n em,\n UserRole,\n { user: user.id, deletedAt: null },\n { populate: ['role'] },\n scope,\n )\n const roleIds = userRoles\n .map((ur: any) => (typeof ur.role === 'string' ? ur.role : ur.role?.id))\n .filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)\n\n if (roleIds.length === 0) return []\n\n const scopedRoles = await findWithDecryption(em, Role, {\n id: { $in: roleIds },\n tenantId: scope.tenantId,\n deletedAt: null,\n }, {}, scope)\n return scopedRoles.map((r: any) => r.id as string)\n}\n\nexport async function resolveCallApiRoleIds(\n em: any,\n instance: CallApiInstanceLike\n): Promise<string[]> {\n if (!instance.definitionId) return []\n\n const { findOneWithDecryption } = await import('@open-mercato/shared/lib/encryption/find')\n const { WorkflowDefinition } = await import('../data/entities')\n\n const scope = { tenantId: instance.tenantId, organizationId: instance.organizationId }\n\n // 1. Prefer the triggering user (whoever manually started this instance).\n // WorkflowInstance.metadata.initiatedBy is the canonical record of that\n // principal for user-started instances; use their current role set so\n // CALL_API never exceeds the initiator's permissions. Refuse if the\n // initiator has no active scoped roles \u2014 do not fall back to the\n // definition author, which would escalate the initiator's privileges.\n const initiatorUserId = instance.metadata?.initiatedBy ?? null\n if (initiatorUserId) {\n return resolveActiveRoleIdsForUser(em, initiatorUserId, scope)\n }\n\n // 2. Event-triggered instance with no human initiator: fall back to the\n // definition author. Soft-deleted definitions must not mint keys.\n const definition = await findOneWithDecryption(em, WorkflowDefinition, {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n deletedAt: null,\n }, {}, scope)\n const authorUserId = definition?.createdBy\n if (!authorUserId) return []\n\n return resolveActiveRoleIdsForUser(em, authorUserId, scope)\n}\n\n/**\n * Build full API URL from endpoint\n * - Relative paths (/api/...) \u2192 prepend APP_URL\n * - Absolute URLs \u2192 validate domain matches APP_URL (SSRF prevention)\n */\nfunction buildApiUrl(endpoint: string): string {\n const appUrl = process.env.APP_URL || 'http://localhost:3000'\n\n // Relative path - prepend APP_URL\n if (endpoint.startsWith('/')) {\n // Security: Only allow /api/* paths\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_API only supports /api/* paths, got: ${endpoint}`)\n }\n return `${appUrl}${endpoint}`\n }\n\n // Absolute URL - validate domain matches APP_URL (SSRF prevention)\n try {\n const endpointUrl = new URL(endpoint)\n const appUrlObj = new URL(appUrl)\n\n if (endpointUrl.host !== appUrlObj.host) {\n throw new Error(\n `SSRF Prevention: CALL_API endpoint domain (${endpointUrl.host}) does not match APP_URL (${appUrlObj.host})`\n )\n }\n\n return endpoint\n } catch (error) {\n if (error instanceof TypeError) {\n throw new Error(`Invalid endpoint URL: ${endpoint}`)\n }\n throw error\n }\n}\n\n/**\n * Classify HTTP error and throw appropriate error\n * - 400-499: Non-retriable (client error - validation/auth)\n * - 500-599: Retriable (server error)\n */\nfunction classifyAndThrowError(status: number, body: any, url: string): never {\n const bodyStr = typeof body === 'string' ? body : JSON.stringify(body)\n\n if (status >= 400 && status < 500) {\n // Client errors - non-retriable\n throw new Error(\n `CALL_API request failed with status ${status} (non-retriable): ${bodyStr}`\n )\n }\n\n if (status >= 500) {\n // Server errors - retriable\n const error: any = new Error(\n `CALL_API request failed with status ${status} (retriable): ${bodyStr}`\n )\n error.retriable = true\n throw error\n }\n\n // Other errors\n throw new Error(`CALL_API request failed with status ${status}: ${bodyStr}`)\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Interpolate variables in config from workflow context\n *\n * Supports syntax:\n * - {{context.field}} or {{context.nested.field}} - from workflow context\n * - {{workflow.instanceId}} - workflow instance ID\n * - {{workflow.tenantId}} - tenant ID\n * - {{workflow.organizationId}} - organization ID\n * - {{workflow.currentStepId}} - current step ID\n * - {{env.VAR_NAME}} - server-allowlisted environment variables\n * - {{now}} - current ISO timestamp\n */\nfunction interpolateVariables(\n config: any,\n context: Record<string, any>,\n workflowInstance?: WorkflowInstance\n): any {\n if (typeof config === 'string') {\n // Check if this is a single variable reference (e.g., \"{{context.cart.items}}\")\n // This preserves the original type (array, object, number, boolean)\n const singleVarMatch = config.match(/^\\{\\{([^}]+)\\}\\}$/)\n\n if (singleVarMatch) {\n const trimmedPath = singleVarMatch[1].trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return workflowInstance.version // Return as number\n default:\n return config // Return original if unknown\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return resolveWorkflowEnvInterpolation(envKey)\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? value : config // Return raw value to preserve type\n }\n\n // Multiple interpolations or mixed text - return string\n return config.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n\n // Handle {{workflow.*}} variables\n if (trimmedPath.startsWith('workflow.') && workflowInstance) {\n const workflowKey = trimmedPath.substring('workflow.'.length)\n switch (workflowKey) {\n case 'instanceId':\n return workflowInstance.id\n case 'tenantId':\n return workflowInstance.tenantId\n case 'organizationId':\n return workflowInstance.organizationId\n case 'currentStepId':\n return workflowInstance.currentStepId\n case 'workflowId':\n return workflowInstance.workflowId\n case 'version':\n return String(workflowInstance.version)\n default:\n return match // Unknown workflow key\n }\n }\n\n // Handle {{env.*}} variables\n if (trimmedPath.startsWith('env.')) {\n const envKey = trimmedPath.substring('env.'.length)\n return resolveWorkflowEnvInterpolation(envKey)\n }\n\n // Handle {{now}} - current timestamp\n if (trimmedPath === 'now') {\n return new Date().toISOString()\n }\n\n // Handle {{context.*}} variables (default behavior)\n const contextPath = trimmedPath.startsWith('context.')\n ? trimmedPath.substring('context.'.length)\n : trimmedPath\n\n const value = getNestedValue(context, contextPath)\n return value !== undefined ? String(value) : match\n })\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => interpolateVariables(item, context, workflowInstance))\n }\n\n if (config && typeof config === 'object') {\n const result: Record<string, any> = {}\n for (const [key, value] of Object.entries(config)) {\n result[key] = interpolateVariables(value, context, workflowInstance)\n }\n return result\n }\n\n return config\n}\n\n/**\n * Get nested value from object by path (e.g., \"user.email\")\n */\nfunction getNestedValue(obj: any, path: string): any {\n const parts = path.split('.')\n let value = obj\n\n for (const part of parts) {\n if (value && typeof value === 'object' && part in value) {\n value = value[part]\n } else {\n return undefined\n }\n }\n\n return value\n}\n\n/**\n * Calculate exponential backoff delay\n */\nfunction calculateBackoff(\n initialIntervalMs: number,\n backoffCoefficient: number,\n attempt: number,\n maxIntervalMs: number\n): number {\n const backoff = initialIntervalMs * Math.pow(backoffCoefficient, attempt)\n return Math.min(backoff, maxIntervalMs || Infinity)\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Execute a promise with timeout\n */\nasync function executeWithTimeout<T>(\n executor: () => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(), timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,8 +35,11 @@ async function findWorkflowDefinition(em, options) {
|
|
|
35
35
|
const codeDef = getCodeWorkflow(workflowId);
|
|
36
36
|
if (!codeDef) return null;
|
|
37
37
|
if (version === void 0 && !codeDef.enabled) return null;
|
|
38
|
+
return buildVirtualCodeDefinition(codeDef, tenantId, organizationId);
|
|
39
|
+
}
|
|
40
|
+
function buildVirtualCodeDefinition(codeDef, tenantId, organizationId) {
|
|
38
41
|
const virtual = new WorkflowDefinition();
|
|
39
|
-
virtual.id = codeWorkflowUuid(workflowId);
|
|
42
|
+
virtual.id = codeWorkflowUuid(codeDef.workflowId);
|
|
40
43
|
virtual.workflowId = codeDef.workflowId;
|
|
41
44
|
virtual.workflowName = codeDef.workflowName;
|
|
42
45
|
virtual.description = codeDef.description ?? null;
|
|
@@ -51,8 +54,21 @@ async function findWorkflowDefinition(em, options) {
|
|
|
51
54
|
virtual.updatedAt = /* @__PURE__ */ new Date(0);
|
|
52
55
|
return virtual;
|
|
53
56
|
}
|
|
57
|
+
function resolveCodeDefinitionForInstance(instance) {
|
|
58
|
+
const codeDef = getCodeWorkflow(instance.workflowId);
|
|
59
|
+
if (!codeDef) return null;
|
|
60
|
+
if (codeWorkflowUuid(instance.workflowId) !== instance.definitionId) return null;
|
|
61
|
+
return buildVirtualCodeDefinition(codeDef, instance.tenantId, instance.organizationId);
|
|
62
|
+
}
|
|
63
|
+
async function findDefinitionForInstance(em, instance) {
|
|
64
|
+
const dbDef = await em.findOne(WorkflowDefinition, { id: instance.definitionId });
|
|
65
|
+
if (dbDef) return dbDef;
|
|
66
|
+
return resolveCodeDefinitionForInstance(instance);
|
|
67
|
+
}
|
|
54
68
|
export {
|
|
55
69
|
codeWorkflowUuid,
|
|
56
|
-
|
|
70
|
+
findDefinitionForInstance,
|
|
71
|
+
findWorkflowDefinition,
|
|
72
|
+
resolveCodeDefinitionForInstance
|
|
57
73
|
};
|
|
58
74
|
//# sourceMappingURL=find-definition.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/find-definition.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Unified Workflow Definition Lookup\n *\n * Finds workflow definitions from the database first, falling back to the\n * in-memory code workflow registry. This enables code-based definitions to\n * be used seamlessly by the executor and validators.\n */\n\nimport { createHash } from 'node:crypto'\nimport { EntityManager } from '@mikro-orm/core'\nimport { WorkflowDefinition, type WorkflowDefinitionData } from '../data/entities'\nimport { getCodeWorkflow } from './code-registry'\n\n/**\n * Generate a deterministic UUID for a code workflow definition.\n * Uses SHA-256 truncated to UUID v4 format so the same workflowId always\n * produces the same UUID across restarts and horizontal replicas.\n */\nexport function codeWorkflowUuid(workflowId: string): string {\n const hash = createHash('sha256').update(`code-workflow:${workflowId}`).digest('hex')\n // Format as UUID v4 (set version nibble to 4, variant bits to 10xx)\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${(parseInt(hash[16], 16) & 0x3 | 0x8).toString(16)}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-')\n}\n\n/**\n * Find a workflow definition by workflowId, checking the database first\n * and falling back to the code registry.\n *\n * When a DB row exists with `codeWorkflowId` set, it means the user has\n * customized the code definition \u2014 the DB version takes precedence.\n *\n * When no DB row exists but a code definition is registered, a virtual\n * WorkflowDefinition-like object is constructed with a deterministic UUID.\n */\nexport async function findWorkflowDefinition(\n em: EntityManager,\n options: {\n workflowId: string\n version?: number\n tenantId: string\n organizationId: string\n },\n): Promise<WorkflowDefinition | null> {\n const { workflowId, version, tenantId, organizationId } = options\n\n // 1. Try database first\n const where: Record<string, unknown> = {\n workflowId,\n tenantId,\n organizationId,\n deletedAt: null,\n }\n\n if (version !== undefined) {\n where.version = version\n }\n\n if (version === undefined) {\n where.enabled = true\n const dbDef = await em.findOne(WorkflowDefinition, where, {\n orderBy: { version: 'DESC' },\n })\n if (dbDef) return dbDef\n } else {\n const dbDef = await em.findOne(WorkflowDefinition, where)\n if (dbDef) return dbDef\n }\n\n // 2. Fall back to code registry (version filter not applicable to code defs)\n const codeDef = getCodeWorkflow(workflowId)\n if (!codeDef) return null\n\n // When no version was requested, mirror the DB branch's `enabled = true` filter\n // so disabled code workflows aren't silently executable.\n if (version === undefined && !codeDef.enabled) return null\n\n // Construct a virtual WorkflowDefinition object (not persisted)\n const virtual = new WorkflowDefinition()\n virtual.id = codeWorkflowUuid(workflowId)\n virtual.workflowId = codeDef.workflowId\n virtual.workflowName = codeDef.workflowName\n virtual.description = codeDef.description ?? null\n virtual.version = codeDef.version\n virtual.definition = codeDef.definition as WorkflowDefinitionData\n virtual.metadata = codeDef.metadata ?? null\n virtual.enabled = codeDef.enabled\n virtual.codeWorkflowId = codeDef.workflowId\n virtual.tenantId = tenantId\n virtual.organizationId = organizationId\n virtual.createdAt = new Date(0) // code defs have no creation date\n virtual.updatedAt = new Date(0)\n\n return virtual\n}\n"],
|
|
5
|
-
"mappings": "AAQA,SAAS,kBAAkB;AAE3B,SAAS,0BAAuD;AAChE,SAAS,uBAAuB;AAOzB,SAAS,iBAAiB,YAA4B;AAC3D,QAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,iBAAiB,UAAU,EAAE,EAAE,OAAO,KAAK;AAEpF,SAAO;AAAA,IACL,KAAK,MAAM,GAAG,CAAC;AAAA,IACf,KAAK,MAAM,GAAG,EAAE;AAAA,IAChB,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC;AAAA,IACtB,IAAI,SAAS,KAAK,EAAE,GAAG,EAAE,IAAI,IAAM,GAAK,SAAS,EAAE,CAAC,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAAA,IACzE,KAAK,MAAM,IAAI,EAAE;AAAA,EACnB,EAAE,KAAK,GAAG;AACZ;AAYA,eAAsB,uBACpB,IACA,SAMoC;AACpC,QAAM,EAAE,YAAY,SAAS,UAAU,eAAe,IAAI;AAG1D,QAAM,QAAiC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AAEA,MAAI,YAAY,QAAW;AACzB,UAAM,UAAU;AAAA,EAClB;AAEA,MAAI,YAAY,QAAW;AACzB,UAAM,UAAU;AAChB,UAAM,QAAQ,MAAM,GAAG,QAAQ,oBAAoB,OAAO;AAAA,MACxD,SAAS,EAAE,SAAS,OAAO;AAAA,IAC7B,CAAC;AACD,QAAI,MAAO,QAAO;AAAA,EACpB,OAAO;AACL,UAAM,QAAQ,MAAM,GAAG,QAAQ,oBAAoB,KAAK;AACxD,QAAI,MAAO,QAAO;AAAA,EACpB;AAGA,QAAM,UAAU,gBAAgB,UAAU;AAC1C,MAAI,CAAC,QAAS,QAAO;AAIrB,MAAI,YAAY,UAAa,CAAC,QAAQ,QAAS,QAAO;
|
|
4
|
+
"sourcesContent": ["/**\n * Unified Workflow Definition Lookup\n *\n * Finds workflow definitions from the database first, falling back to the\n * in-memory code workflow registry. This enables code-based definitions to\n * be used seamlessly by the executor and validators.\n */\n\nimport { createHash } from 'node:crypto'\nimport { EntityManager } from '@mikro-orm/core'\nimport { WorkflowDefinition, type WorkflowDefinitionData } from '../data/entities'\nimport { getCodeWorkflow } from './code-registry'\n\n/**\n * Generate a deterministic UUID for a code workflow definition.\n * Uses SHA-256 truncated to UUID v4 format so the same workflowId always\n * produces the same UUID across restarts and horizontal replicas.\n */\nexport function codeWorkflowUuid(workflowId: string): string {\n const hash = createHash('sha256').update(`code-workflow:${workflowId}`).digest('hex')\n // Format as UUID v4 (set version nibble to 4, variant bits to 10xx)\n return [\n hash.slice(0, 8),\n hash.slice(8, 12),\n `4${hash.slice(13, 16)}`,\n `${(parseInt(hash[16], 16) & 0x3 | 0x8).toString(16)}${hash.slice(17, 20)}`,\n hash.slice(20, 32),\n ].join('-')\n}\n\n/**\n * Find a workflow definition by workflowId, checking the database first\n * and falling back to the code registry.\n *\n * When a DB row exists with `codeWorkflowId` set, it means the user has\n * customized the code definition \u2014 the DB version takes precedence.\n *\n * When no DB row exists but a code definition is registered, a virtual\n * WorkflowDefinition-like object is constructed with a deterministic UUID.\n */\nexport async function findWorkflowDefinition(\n em: EntityManager,\n options: {\n workflowId: string\n version?: number\n tenantId: string\n organizationId: string\n },\n): Promise<WorkflowDefinition | null> {\n const { workflowId, version, tenantId, organizationId } = options\n\n // 1. Try database first\n const where: Record<string, unknown> = {\n workflowId,\n tenantId,\n organizationId,\n deletedAt: null,\n }\n\n if (version !== undefined) {\n where.version = version\n }\n\n if (version === undefined) {\n where.enabled = true\n const dbDef = await em.findOne(WorkflowDefinition, where, {\n orderBy: { version: 'DESC' },\n })\n if (dbDef) return dbDef\n } else {\n const dbDef = await em.findOne(WorkflowDefinition, where)\n if (dbDef) return dbDef\n }\n\n // 2. Fall back to code registry (version filter not applicable to code defs)\n const codeDef = getCodeWorkflow(workflowId)\n if (!codeDef) return null\n\n // When no version was requested, mirror the DB branch's `enabled = true` filter\n // so disabled code workflows aren't silently executable.\n if (version === undefined && !codeDef.enabled) return null\n\n return buildVirtualCodeDefinition(codeDef, tenantId, organizationId)\n}\n\nfunction buildVirtualCodeDefinition(\n codeDef: NonNullable<ReturnType<typeof getCodeWorkflow>>,\n tenantId: string,\n organizationId: string,\n): WorkflowDefinition {\n // Construct a virtual WorkflowDefinition object (not persisted)\n const virtual = new WorkflowDefinition()\n virtual.id = codeWorkflowUuid(codeDef.workflowId)\n virtual.workflowId = codeDef.workflowId\n virtual.workflowName = codeDef.workflowName\n virtual.description = codeDef.description ?? null\n virtual.version = codeDef.version\n virtual.definition = codeDef.definition as WorkflowDefinitionData\n virtual.metadata = codeDef.metadata ?? null\n virtual.enabled = codeDef.enabled\n virtual.codeWorkflowId = codeDef.workflowId\n virtual.tenantId = tenantId\n virtual.organizationId = organizationId\n virtual.createdAt = new Date(0) // code defs have no creation date\n virtual.updatedAt = new Date(0)\n\n return virtual\n}\n\n/**\n * Resolve a virtual code definition for an already-started instance whose\n * `definitionId` does not exist in the database.\n *\n * Instances started from a virtual code definition persist the deterministic\n * `codeWorkflowUuid(workflowId)` as their `definitionId`. Runtime handlers\n * (step/transition/signal/timer/task) load definitions by that id, so without\n * this fallback an unpersisted code workflow starts but can never advance.\n *\n * The UUID equality gate ensures the fallback never substitutes the code\n * definition for a hard-deleted persisted row: those rows have their own\n * random UUIDs that can never match the deterministic code UUID.\n */\nexport function resolveCodeDefinitionForInstance(instance: {\n definitionId: string\n workflowId: string\n tenantId: string\n organizationId: string\n}): WorkflowDefinition | null {\n const codeDef = getCodeWorkflow(instance.workflowId)\n if (!codeDef) return null\n if (codeWorkflowUuid(instance.workflowId) !== instance.definitionId) return null\n return buildVirtualCodeDefinition(codeDef, instance.tenantId, instance.organizationId)\n}\n\n/**\n * Find the definition backing a workflow instance: database row by id first,\n * falling back to the in-memory code registry for instances started from an\n * unpersisted code definition.\n */\nexport async function findDefinitionForInstance(\n em: EntityManager,\n instance: {\n definitionId: string\n workflowId: string\n tenantId: string\n organizationId: string\n },\n): Promise<WorkflowDefinition | null> {\n const dbDef = await em.findOne(WorkflowDefinition, { id: instance.definitionId })\n if (dbDef) return dbDef\n return resolveCodeDefinitionForInstance(instance)\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,kBAAkB;AAE3B,SAAS,0BAAuD;AAChE,SAAS,uBAAuB;AAOzB,SAAS,iBAAiB,YAA4B;AAC3D,QAAM,OAAO,WAAW,QAAQ,EAAE,OAAO,iBAAiB,UAAU,EAAE,EAAE,OAAO,KAAK;AAEpF,SAAO;AAAA,IACL,KAAK,MAAM,GAAG,CAAC;AAAA,IACf,KAAK,MAAM,GAAG,EAAE;AAAA,IAChB,IAAI,KAAK,MAAM,IAAI,EAAE,CAAC;AAAA,IACtB,IAAI,SAAS,KAAK,EAAE,GAAG,EAAE,IAAI,IAAM,GAAK,SAAS,EAAE,CAAC,GAAG,KAAK,MAAM,IAAI,EAAE,CAAC;AAAA,IACzE,KAAK,MAAM,IAAI,EAAE;AAAA,EACnB,EAAE,KAAK,GAAG;AACZ;AAYA,eAAsB,uBACpB,IACA,SAMoC;AACpC,QAAM,EAAE,YAAY,SAAS,UAAU,eAAe,IAAI;AAG1D,QAAM,QAAiC;AAAA,IACrC;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AAEA,MAAI,YAAY,QAAW;AACzB,UAAM,UAAU;AAAA,EAClB;AAEA,MAAI,YAAY,QAAW;AACzB,UAAM,UAAU;AAChB,UAAM,QAAQ,MAAM,GAAG,QAAQ,oBAAoB,OAAO;AAAA,MACxD,SAAS,EAAE,SAAS,OAAO;AAAA,IAC7B,CAAC;AACD,QAAI,MAAO,QAAO;AAAA,EACpB,OAAO;AACL,UAAM,QAAQ,MAAM,GAAG,QAAQ,oBAAoB,KAAK;AACxD,QAAI,MAAO,QAAO;AAAA,EACpB;AAGA,QAAM,UAAU,gBAAgB,UAAU;AAC1C,MAAI,CAAC,QAAS,QAAO;AAIrB,MAAI,YAAY,UAAa,CAAC,QAAQ,QAAS,QAAO;AAEtD,SAAO,2BAA2B,SAAS,UAAU,cAAc;AACrE;AAEA,SAAS,2BACP,SACA,UACA,gBACoB;AAEpB,QAAM,UAAU,IAAI,mBAAmB;AACvC,UAAQ,KAAK,iBAAiB,QAAQ,UAAU;AAChD,UAAQ,aAAa,QAAQ;AAC7B,UAAQ,eAAe,QAAQ;AAC/B,UAAQ,cAAc,QAAQ,eAAe;AAC7C,UAAQ,UAAU,QAAQ;AAC1B,UAAQ,aAAa,QAAQ;AAC7B,UAAQ,WAAW,QAAQ,YAAY;AACvC,UAAQ,UAAU,QAAQ;AAC1B,UAAQ,iBAAiB,QAAQ;AACjC,UAAQ,WAAW;AACnB,UAAQ,iBAAiB;AACzB,UAAQ,YAAY,oBAAI,KAAK,CAAC;AAC9B,UAAQ,YAAY,oBAAI,KAAK,CAAC;AAE9B,SAAO;AACT;AAeO,SAAS,iCAAiC,UAKnB;AAC5B,QAAM,UAAU,gBAAgB,SAAS,UAAU;AACnD,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,iBAAiB,SAAS,UAAU,MAAM,SAAS,aAAc,QAAO;AAC5E,SAAO,2BAA2B,SAAS,SAAS,UAAU,SAAS,cAAc;AACvF;AAOA,eAAsB,0BACpB,IACA,UAMoC;AACpC,QAAM,QAAQ,MAAM,GAAG,QAAQ,oBAAoB,EAAE,IAAI,SAAS,aAAa,CAAC;AAChF,MAAI,MAAO,QAAO;AAClB,SAAO,iCAAiC,QAAQ;AAClD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
2
|
import { WorkflowInstance, WorkflowBranchInstance, WorkflowDefinition, StepInstance } from "../data/entities.js";
|
|
3
|
+
import { resolveCodeDefinitionForInstance } from "./find-definition.js";
|
|
3
4
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
4
5
|
const logger = createLogger("workflows");
|
|
5
6
|
class SignalError extends Error {
|
|
@@ -41,7 +42,7 @@ async function sendSignal(em, container, options) {
|
|
|
41
42
|
{ id: instance.definitionId, tenantId: instance.tenantId, organizationId: instance.organizationId, deletedAt: null },
|
|
42
43
|
void 0,
|
|
43
44
|
{ tenantId: instance.tenantId, organizationId: instance.organizationId }
|
|
44
|
-
);
|
|
45
|
+
) ?? resolveCodeDefinitionForInstance(instance);
|
|
45
46
|
if (!branchDefinition) {
|
|
46
47
|
throw new SignalError("Workflow definition not found", "DEFINITION_NOT_FOUND", { definitionId: instance.definitionId });
|
|
47
48
|
}
|
|
@@ -114,7 +115,7 @@ async function sendSignal(em, container, options) {
|
|
|
114
115
|
},
|
|
115
116
|
void 0,
|
|
116
117
|
{ tenantId: instance.tenantId, organizationId: instance.organizationId }
|
|
117
|
-
);
|
|
118
|
+
) ?? resolveCodeDefinitionForInstance(instance);
|
|
118
119
|
if (!definition) {
|
|
119
120
|
throw new SignalError(
|
|
120
121
|
"Workflow definition not found",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/signal-handler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Signal Handler Service\n *\n * Receives external signals and resumes workflows waiting for them.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WorkflowInstance, WorkflowBranchInstance, WorkflowDefinition, StepInstance } from '../data/entities'\nimport type * as eventLoggerModule from './event-logger'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport interface SendSignalOptions {\n /**\n * Workflow instance ID\n */\n instanceId: string\n\n /**\n * Signal name to match against WAIT_FOR_SIGNAL step config\n */\n signalName: string\n\n /**\n * Optional payload to merge into workflow context\n */\n payload?: Record<string, any>\n\n /**\n * User ID sending the signal\n */\n userId?: string\n\n /**\n * Tenant/org scope\n */\n tenantId: string\n organizationId: string\n}\n\nexport class SignalError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'SignalError'\n }\n}\n\n/**\n * Send signal to workflow instance and resume execution\n */\nexport async function sendSignal(\n em: EntityManager,\n container: AwilixContainer,\n options: SendSignalOptions\n): Promise<void> {\n const { instanceId, signalName, payload, userId, tenantId, organizationId } = options\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n // Fetch workflow instance\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n {\n id: instanceId,\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId },\n )\n\n if (!instance) {\n throw new SignalError(\n 'Workflow instance not found',\n 'INSTANCE_NOT_FOUND',\n { instanceId }\n )\n }\n\n // Branch-scoped signal: a FORKED instance routes the signal to the branch\n // paused at a matching WAIT_FOR_SIGNAL step.\n if (instance.status === 'FORKED') {\n const branchDefinition = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n { id: instance.definitionId, tenantId: instance.tenantId, organizationId: instance.organizationId, deletedAt: null },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )\n if (!branchDefinition) {\n throw new SignalError('Workflow definition not found', 'DEFINITION_NOT_FOUND', { definitionId: instance.definitionId })\n }\n\n const pausedBranches = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instanceId,\n status: 'PAUSED',\n tenantId,\n organizationId,\n })\n\n let targetBranch: WorkflowBranchInstance | null = null\n for (const candidate of pausedBranches) {\n const step = branchDefinition.definition.steps.find((s: any) => s.stepId === candidate.currentStepId)\n if (step?.stepType === 'WAIT_FOR_SIGNAL') {\n const candidateSignal = step.signalConfig?.signalName || step.stepId\n if (candidateSignal === signalName) {\n targetBranch = candidate\n break\n }\n }\n }\n\n if (!targetBranch) {\n throw new SignalError('No parallel branch awaiting this signal', 'NO_BRANCH_AWAITING_SIGNAL', { instanceId, signalName })\n }\n\n const branchStepInstance = await em.findOne(StepInstance, {\n workflowInstanceId: instanceId,\n branchInstanceId: targetBranch.id,\n stepId: targetBranch.currentStepId,\n status: 'ACTIVE',\n })\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instanceId,\n stepInstanceId: branchStepInstance?.id,\n branchInstanceId: targetBranch.id,\n eventType: 'SIGNAL_RECEIVED',\n eventData: { signalName, branch: true },\n userId,\n tenantId,\n organizationId,\n })\n\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId,\n branchInstanceId: targetBranch.id,\n tenantId,\n organizationId,\n contextMerge: payload,\n exitStepInstanceId: branchStepInstance?.id ?? null,\n exitOutput: { signalName, payload },\n })\n if (resumed) {\n await workflowExecutor.executeWorkflow(em, container, instanceId, { userId })\n }\n return\n }\n\n // Verify workflow is paused\n if (instance.status !== 'PAUSED') {\n throw new SignalError(\n 'Workflow is not paused',\n 'WORKFLOW_NOT_PAUSED',\n { instanceId, status: instance.status }\n )\n }\n\n // Load workflow definition with tenant/org scope to check current step\n const definition = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )\n if (!definition) {\n throw new SignalError(\n 'Workflow definition not found',\n 'DEFINITION_NOT_FOUND',\n { definitionId: instance.definitionId }\n )\n }\n\n // Find current step\n const currentStep = definition.definition.steps.find(\n (s: any) => s.stepId === instance.currentStepId\n )\n\n if (!currentStep || currentStep.stepType !== 'WAIT_FOR_SIGNAL') {\n throw new SignalError(\n 'Workflow is not waiting for signal',\n 'NOT_WAITING_FOR_SIGNAL',\n { instanceId, currentStepId: instance.currentStepId }\n )\n }\n\n // Check signal name matches\n const expectedSignalName = currentStep.signalConfig?.signalName || currentStep.stepId\n if (expectedSignalName !== signalName) {\n throw new SignalError(\n 'Signal name mismatch',\n 'SIGNAL_NAME_MISMATCH',\n { expected: expectedSignalName, received: signalName }\n )\n }\n\n const now = new Date()\n\n // Merge signal payload into workflow context\n if (payload) {\n instance.context = {\n ...instance.context,\n ...payload,\n [`signal_${signalName}_payload`]: payload,\n [`signal_${signalName}_receivedAt`]: now.toISOString(),\n }\n }\n\n instance.updatedAt = now\n\n // Log signal received event\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'SIGNAL_RECEIVED',\n eventData: {\n signalName,\n payload,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Find active step instance and exit it\n const stepInstance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n {\n workflowInstanceId: instance.id,\n stepId: instance.currentStepId,\n status: 'ACTIVE',\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )\n\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, {\n signalName,\n payload,\n })\n }\n\n // Find automatic transitions from current step\n const autoTransitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === instance.currentStepId && t.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n // No automatic transitions, mark as RUNNING but stay at current step\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n // Find valid transitions\n const transitionContext = {\n workflowContext: instance.context,\n userId,\n }\n\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n instance.currentStepId,\n transitionContext\n )\n\n const firstValidTransition = validTransitions.find((t) => t.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n // No valid transitions, mark as RUNNING anyway\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n // Execute transition to next step\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n instance.currentStepId,\n firstValidTransition.transition.toStepId,\n transitionContext\n )\n\n if (!transitionResult.success) {\n throw new SignalError(\n 'Transition failed after signal',\n 'TRANSITION_FAILED',\n { error: transitionResult.error }\n )\n }\n\n // Resume workflow execution\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n}\n\n/**\n * Send signal by correlation key (finds all waiting instances)\n */\nexport async function sendSignalByCorrelationKey(\n em: EntityManager,\n container: AwilixContainer,\n options: Omit<SendSignalOptions, 'instanceId'> & { correlationKey: string }\n): Promise<number> {\n const { correlationKey, signalName, payload, userId, tenantId, organizationId } = options\n\n // Find all paused instances with this correlation key\n const instances = await findWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n {\n correlationKey,\n status: 'PAUSED',\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId },\n )\n\n let signalsProcessed = 0\n\n for (const instance of instances) {\n try {\n await sendSignal(em, container, {\n instanceId: instance.id,\n signalName,\n payload,\n userId,\n tenantId,\n organizationId,\n })\n signalsProcessed++\n } catch (error) {\n // Log error but continue processing other instances\n logger.error('Failed to send signal to instance', { instanceId: instance.id, err: error })\n }\n }\n\n return signalsProcessed\n}\n"],
|
|
5
|
-
"mappings": "AASA,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,kBAAkB,wBAAwB,oBAAoB,oBAAoB;AAK3F,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AA8BhC,MAAM,oBAAoB,MAAM;AAAA,EACrC,YACE,SACO,MACA,SACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAKA,eAAsB,WACpB,IACA,WACA,SACe;AACf,QAAM,EAAE,YAAY,YAAY,SAAS,QAAQ,UAAU,eAAe,IAAI;AAE9E,QAAM,cAAc,UAAU,QAAkC,aAAa;AAC7E,QAAM,cAAc,UAAU,QAAkC,aAAa;AAC7E,QAAM,oBAAoB,UAAU,QAAwC,mBAAmB;AAC/F,QAAM,mBAAmB,UAAU,QAAuC,kBAAkB;AAG5F,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,EAC7B;AAEA,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,EAAE,WAAW;AAAA,IACf;AAAA,EACF;AAIA,MAAI,SAAS,WAAW,UAAU;AAChC,UAAM,
|
|
4
|
+
"sourcesContent": ["/**\n * Signal Handler Service\n *\n * Receives external signals and resumes workflows waiting for them.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { WorkflowInstance, WorkflowBranchInstance, WorkflowDefinition, StepInstance } from '../data/entities'\nimport type * as eventLoggerModule from './event-logger'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\nimport { resolveCodeDefinitionForInstance } from './find-definition'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport interface SendSignalOptions {\n /**\n * Workflow instance ID\n */\n instanceId: string\n\n /**\n * Signal name to match against WAIT_FOR_SIGNAL step config\n */\n signalName: string\n\n /**\n * Optional payload to merge into workflow context\n */\n payload?: Record<string, any>\n\n /**\n * User ID sending the signal\n */\n userId?: string\n\n /**\n * Tenant/org scope\n */\n tenantId: string\n organizationId: string\n}\n\nexport class SignalError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: any\n ) {\n super(message)\n this.name = 'SignalError'\n }\n}\n\n/**\n * Send signal to workflow instance and resume execution\n */\nexport async function sendSignal(\n em: EntityManager,\n container: AwilixContainer,\n options: SendSignalOptions\n): Promise<void> {\n const { instanceId, signalName, payload, userId, tenantId, organizationId } = options\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n // Fetch workflow instance\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n {\n id: instanceId,\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId },\n )\n\n if (!instance) {\n throw new SignalError(\n 'Workflow instance not found',\n 'INSTANCE_NOT_FOUND',\n { instanceId }\n )\n }\n\n // Branch-scoped signal: a FORKED instance routes the signal to the branch\n // paused at a matching WAIT_FOR_SIGNAL step.\n if (instance.status === 'FORKED') {\n const branchDefinition = (await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n { id: instance.definitionId, tenantId: instance.tenantId, organizationId: instance.organizationId, deletedAt: null },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )) ?? resolveCodeDefinitionForInstance(instance)\n if (!branchDefinition) {\n throw new SignalError('Workflow definition not found', 'DEFINITION_NOT_FOUND', { definitionId: instance.definitionId })\n }\n\n const pausedBranches = await em.find(WorkflowBranchInstance, {\n workflowInstanceId: instanceId,\n status: 'PAUSED',\n tenantId,\n organizationId,\n })\n\n let targetBranch: WorkflowBranchInstance | null = null\n for (const candidate of pausedBranches) {\n const step = branchDefinition.definition.steps.find((s: any) => s.stepId === candidate.currentStepId)\n if (step?.stepType === 'WAIT_FOR_SIGNAL') {\n const candidateSignal = step.signalConfig?.signalName || step.stepId\n if (candidateSignal === signalName) {\n targetBranch = candidate\n break\n }\n }\n }\n\n if (!targetBranch) {\n throw new SignalError('No parallel branch awaiting this signal', 'NO_BRANCH_AWAITING_SIGNAL', { instanceId, signalName })\n }\n\n const branchStepInstance = await em.findOne(StepInstance, {\n workflowInstanceId: instanceId,\n branchInstanceId: targetBranch.id,\n stepId: targetBranch.currentStepId,\n status: 'ACTIVE',\n })\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instanceId,\n stepInstanceId: branchStepInstance?.id,\n branchInstanceId: targetBranch.id,\n eventType: 'SIGNAL_RECEIVED',\n eventData: { signalName, branch: true },\n userId,\n tenantId,\n organizationId,\n })\n\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId,\n branchInstanceId: targetBranch.id,\n tenantId,\n organizationId,\n contextMerge: payload,\n exitStepInstanceId: branchStepInstance?.id ?? null,\n exitOutput: { signalName, payload },\n })\n if (resumed) {\n await workflowExecutor.executeWorkflow(em, container, instanceId, { userId })\n }\n return\n }\n\n // Verify workflow is paused\n if (instance.status !== 'PAUSED') {\n throw new SignalError(\n 'Workflow is not paused',\n 'WORKFLOW_NOT_PAUSED',\n { instanceId, status: instance.status }\n )\n }\n\n // Load workflow definition with tenant/org scope to check current step\n const definition = (await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )) ?? resolveCodeDefinitionForInstance(instance)\n if (!definition) {\n throw new SignalError(\n 'Workflow definition not found',\n 'DEFINITION_NOT_FOUND',\n { definitionId: instance.definitionId }\n )\n }\n\n // Find current step\n const currentStep = definition.definition.steps.find(\n (s: any) => s.stepId === instance.currentStepId\n )\n\n if (!currentStep || currentStep.stepType !== 'WAIT_FOR_SIGNAL') {\n throw new SignalError(\n 'Workflow is not waiting for signal',\n 'NOT_WAITING_FOR_SIGNAL',\n { instanceId, currentStepId: instance.currentStepId }\n )\n }\n\n // Check signal name matches\n const expectedSignalName = currentStep.signalConfig?.signalName || currentStep.stepId\n if (expectedSignalName !== signalName) {\n throw new SignalError(\n 'Signal name mismatch',\n 'SIGNAL_NAME_MISMATCH',\n { expected: expectedSignalName, received: signalName }\n )\n }\n\n const now = new Date()\n\n // Merge signal payload into workflow context\n if (payload) {\n instance.context = {\n ...instance.context,\n ...payload,\n [`signal_${signalName}_payload`]: payload,\n [`signal_${signalName}_receivedAt`]: now.toISOString(),\n }\n }\n\n instance.updatedAt = now\n\n // Log signal received event\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n eventType: 'SIGNAL_RECEIVED',\n eventData: {\n signalName,\n payload,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n // Find active step instance and exit it\n const stepInstance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n {\n workflowInstanceId: instance.id,\n stepId: instance.currentStepId,\n status: 'ACTIVE',\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId },\n )\n\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, {\n signalName,\n payload,\n })\n }\n\n // Find automatic transitions from current step\n const autoTransitions = (definition.definition.transitions || []).filter(\n (t: any) => t.fromStepId === instance.currentStepId && t.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n // No automatic transitions, mark as RUNNING but stay at current step\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n // Find valid transitions\n const transitionContext = {\n workflowContext: instance.context,\n userId,\n }\n\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n instance.currentStepId,\n transitionContext\n )\n\n const firstValidTransition = validTransitions.find((t) => t.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n // No valid transitions, mark as RUNNING anyway\n instance.status = 'RUNNING'\n await em.flush()\n return\n }\n\n // Execute transition to next step\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n instance.currentStepId,\n firstValidTransition.transition.toStepId,\n transitionContext\n )\n\n if (!transitionResult.success) {\n throw new SignalError(\n 'Transition failed after signal',\n 'TRANSITION_FAILED',\n { error: transitionResult.error }\n )\n }\n\n // Resume workflow execution\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n}\n\n/**\n * Send signal by correlation key (finds all waiting instances)\n */\nexport async function sendSignalByCorrelationKey(\n em: EntityManager,\n container: AwilixContainer,\n options: Omit<SendSignalOptions, 'instanceId'> & { correlationKey: string }\n): Promise<number> {\n const { correlationKey, signalName, payload, userId, tenantId, organizationId } = options\n\n // Find all paused instances with this correlation key\n const instances = await findWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n {\n correlationKey,\n status: 'PAUSED',\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId },\n )\n\n let signalsProcessed = 0\n\n for (const instance of instances) {\n try {\n await sendSignal(em, container, {\n instanceId: instance.id,\n signalName,\n payload,\n userId,\n tenantId,\n organizationId,\n })\n signalsProcessed++\n } catch (error) {\n // Log error but continue processing other instances\n logger.error('Failed to send signal to instance', { instanceId: instance.id, err: error })\n }\n }\n\n return signalsProcessed\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
WorkflowDefinition,
|
|
3
2
|
StepInstance,
|
|
4
3
|
UserTask,
|
|
5
4
|
WorkflowEvent
|
|
6
5
|
} from "../data/entities.js";
|
|
7
6
|
import { parseDuration } from "./duration.js";
|
|
8
7
|
import { logWorkflowEvent } from "./event-logger.js";
|
|
8
|
+
import { findDefinitionForInstance } from "./find-definition.js";
|
|
9
9
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
10
10
|
const logger = createLogger("workflows");
|
|
11
11
|
class StepExecutionError extends Error {
|
|
@@ -17,9 +17,7 @@ class StepExecutionError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
async function enterStep(em, instance, stepId, context, branch) {
|
|
20
|
-
const definition = await em
|
|
21
|
-
id: instance.definitionId
|
|
22
|
-
});
|
|
20
|
+
const definition = await findDefinitionForInstance(em, instance);
|
|
23
21
|
if (!definition) {
|
|
24
22
|
throw new StepExecutionError(
|
|
25
23
|
`Workflow definition not found: ${instance.definitionId}`,
|
|
@@ -97,9 +95,7 @@ async function exitStep(em, stepInstance, outputData) {
|
|
|
97
95
|
async function executeStep(em, instance, stepId, context, container, branch) {
|
|
98
96
|
try {
|
|
99
97
|
const stepInstance = await enterStep(em, instance, stepId, context, branch);
|
|
100
|
-
const definition = await em
|
|
101
|
-
id: instance.definitionId
|
|
102
|
-
});
|
|
98
|
+
const definition = await findDefinitionForInstance(em, instance);
|
|
103
99
|
if (!definition) {
|
|
104
100
|
throw new StepExecutionError(
|
|
105
101
|
`Workflow definition not found: ${instance.definitionId}`,
|
|
@@ -195,7 +191,7 @@ async function executeStepByType(em, instance, stepInstance, stepDef, context, c
|
|
|
195
191
|
{ stepType, stepId: stepDef.stepId }
|
|
196
192
|
);
|
|
197
193
|
}
|
|
198
|
-
const definition = await em
|
|
194
|
+
const definition = await findDefinitionForInstance(em, instance);
|
|
199
195
|
if (!definition) {
|
|
200
196
|
throw new StepExecutionError(
|
|
201
197
|
`Workflow definition not found: ${instance.definitionId}`,
|