@open-mercato/core 0.6.7-develop.6851.1.c1680b00d8 → 0.6.7-develop.6860.1.f796b22e80

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.
Files changed (82) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/bootstrap.js +16 -1
  3. package/dist/bootstrap.js.map +2 -2
  4. package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
  5. package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
  6. package/dist/generated/entities.ids.generated.js +2 -1
  7. package/dist/generated/entities.ids.generated.js.map +2 -2
  8. package/dist/generated/entity-fields-registry.js +17 -0
  9. package/dist/generated/entity-fields-registry.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +104 -8
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/attachments/data/entities.js +67 -1
  13. package/dist/modules/attachments/data/entities.js.map +2 -2
  14. package/dist/modules/attachments/di.js +7 -1
  15. package/dist/modules/attachments/di.js.map +2 -2
  16. package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
  17. package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
  18. package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
  19. package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
  20. package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
  21. package/dist/modules/attachments/lib/quota-service.js +224 -0
  22. package/dist/modules/attachments/lib/quota-service.js.map +7 -0
  23. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
  24. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
  25. package/dist/modules/attachments/workers/quota-recovery.js +45 -0
  26. package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
  27. package/dist/modules/auth/lib/setup-app.js +16 -2
  28. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  29. package/dist/modules/dashboards/services/widgetDataService.js +3 -4
  30. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  31. package/dist/modules/entities/cli.js +193 -19
  32. package/dist/modules/entities/cli.js.map +2 -2
  33. package/dist/modules/messages/api/[id]/route.js +15 -0
  34. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  35. package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
  36. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  37. package/dist/modules/sales/commands/documents.js +110 -22
  38. package/dist/modules/sales/commands/documents.js.map +2 -2
  39. package/dist/modules/sales/workflows.js +10 -7
  40. package/dist/modules/sales/workflows.js.map +2 -2
  41. package/dist/modules/workflows/lib/activity-executor.js +27 -0
  42. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  43. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
  44. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
  45. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
  46. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  47. package/generated/entities/attachment_quota_reservation/index.ts +15 -0
  48. package/generated/entities.ids.generated.ts +2 -1
  49. package/generated/entity-fields-registry.ts +17 -0
  50. package/package.json +7 -7
  51. package/src/bootstrap.ts +17 -2
  52. package/src/modules/attachments/api/route.ts +114 -7
  53. package/src/modules/attachments/data/entities.ts +59 -0
  54. package/src/modules/attachments/di.ts +9 -1
  55. package/src/modules/attachments/i18n/de.json +3 -0
  56. package/src/modules/attachments/i18n/en.json +3 -0
  57. package/src/modules/attachments/i18n/es.json +3 -0
  58. package/src/modules/attachments/i18n/ko.json +3 -0
  59. package/src/modules/attachments/i18n/pl.json +3 -0
  60. package/src/modules/attachments/lib/drivers/index.ts +1 -1
  61. package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
  62. package/src/modules/attachments/lib/drivers/types.ts +5 -0
  63. package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
  64. package/src/modules/attachments/lib/quota-service.ts +442 -0
  65. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
  66. package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
  67. package/src/modules/attachments/workers/quota-recovery.ts +68 -0
  68. package/src/modules/auth/lib/setup-app.ts +20 -2
  69. package/src/modules/dashboards/services/widgetDataService.ts +3 -6
  70. package/src/modules/entities/cli.ts +238 -21
  71. package/src/modules/messages/api/[id]/route.ts +21 -0
  72. package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
  73. package/src/modules/sales/commands/documents.ts +188 -29
  74. package/src/modules/sales/workflows.ts +10 -7
  75. package/src/modules/workflows/i18n/de.json +1 -0
  76. package/src/modules/workflows/i18n/en.json +1 -0
  77. package/src/modules/workflows/i18n/es.json +1 -0
  78. package/src/modules/workflows/i18n/ko.json +1 -0
  79. package/src/modules/workflows/i18n/pl.json +1 -0
  80. package/src/modules/workflows/lib/activity-executor.ts +40 -1
  81. package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
  82. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +15 -6
@@ -49,7 +49,7 @@ const orderApproval = defineWorkflow({
49
49
  config: {
50
50
  commandId: "sales.orders.update",
51
51
  statusDictionary: "sales.order_status",
52
- input: { id: "{{context.id}}", statusValue: "pending_approval" }
52
+ input: { id: "{{context.orderId}}", statusValue: "pending_approval" }
53
53
  },
54
54
  retryPolicy: { maxAttempts: 3, initialIntervalMs: 1e3, backoffCoefficient: 2, maxIntervalMs: 1e4 }
55
55
  },
@@ -60,7 +60,7 @@ const orderApproval = defineWorkflow({
60
60
  async: true,
61
61
  config: {
62
62
  eventName: "sales.order.approval.requested",
63
- payload: { orderId: "{{context.id}}", workflowInstanceId: "{{workflow.instanceId}}" }
63
+ payload: { orderId: "{{context.orderId}}", workflowInstanceId: "{{workflow.instanceId}}" }
64
64
  }
65
65
  }
66
66
  ]
@@ -83,7 +83,7 @@ const orderApproval = defineWorkflow({
83
83
  config: {
84
84
  commandId: "sales.orders.update",
85
85
  statusDictionary: "sales.order_status",
86
- input: { id: "{{context.id}}", statusValue: "approved" }
86
+ input: { id: "{{context.orderId}}", statusValue: "approved" }
87
87
  },
88
88
  retryPolicy: { maxAttempts: 3, initialIntervalMs: 1e3, backoffCoefficient: 2, maxIntervalMs: 1e4 }
89
89
  },
@@ -95,7 +95,7 @@ const orderApproval = defineWorkflow({
95
95
  config: {
96
96
  eventName: "sales.order.approval.approved",
97
97
  payload: {
98
- orderId: "{{context.id}}",
98
+ orderId: "{{context.orderId}}",
99
99
  workflowInstanceId: "{{workflow.instanceId}}",
100
100
  approvedBy: "{{context.completedBy}}",
101
101
  comments: "{{context.comments}}"
@@ -122,7 +122,7 @@ const orderApproval = defineWorkflow({
122
122
  config: {
123
123
  commandId: "sales.orders.update",
124
124
  statusDictionary: "sales.order_status",
125
- input: { id: "{{context.id}}", statusValue: "rejected" }
125
+ input: { id: "{{context.orderId}}", statusValue: "rejected" }
126
126
  },
127
127
  retryPolicy: { maxAttempts: 3, initialIntervalMs: 1e3, backoffCoefficient: 2, maxIntervalMs: 1e4 }
128
128
  },
@@ -134,7 +134,7 @@ const orderApproval = defineWorkflow({
134
134
  config: {
135
135
  eventName: "sales.order.approval.rejected",
136
136
  payload: {
137
- orderId: "{{context.id}}",
137
+ orderId: "{{context.orderId}}",
138
138
  workflowInstanceId: "{{workflow.instanceId}}",
139
139
  rejectedBy: "{{context.completedBy}}",
140
140
  comments: "{{context.comments}}"
@@ -167,7 +167,10 @@ const orderApproval = defineWorkflow({
167
167
  name: "Order Approval Trigger",
168
168
  description: "Triggers when a new sales order is created",
169
169
  eventPattern: "sales.order.created",
170
- config: { entityType: "SalesOrder" },
170
+ config: {
171
+ entityType: "SalesOrder",
172
+ contextMapping: [{ targetKey: "orderId", sourceExpression: "id" }]
173
+ },
171
174
  enabled: true,
172
175
  priority: 0
173
176
  }]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/sales/workflows.ts"],
4
- "sourcesContent": ["/**\n * Sales Module \u2014 Code-Based Workflow Definitions\n *\n * Order approval workflow shipped with the sales module.\n */\n\nimport { defineWorkflow, createWorkflowsModuleConfig } from '@open-mercato/shared/modules/workflows'\nimport { registerWorkflowSafeCommands } from '@open-mercato/core/modules/workflows/lib/workflow-safe-commands'\n\nregisterWorkflowSafeCommands([\n { commandId: 'sales.orders.update', requiredFeatures: ['sales.orders.manage'] },\n])\n\nconst orderApproval = defineWorkflow({\n workflowId: 'sales.order-approval',\n workflowName: 'Sales Order Approval Workflow',\n description: 'Approval workflow for sales orders requiring authorization before processing',\n metadata: { category: 'Sales', tags: ['sales', 'approval', 'order'], icon: 'check-circle' },\n steps: [\n { stepId: 'start', stepName: 'Start', stepType: 'START', description: 'Initialize order approval workflow' },\n {\n stepId: 'pending_approval',\n stepName: 'Pending Approval',\n stepType: 'USER_TASK',\n description: 'Order awaiting approval decision',\n userTaskConfig: {\n formSchema: {\n type: 'object',\n required: ['decision'],\n properties: {\n comments: { type: 'string', title: 'Comments', description: 'Optional comments for the decision' },\n decision: { enum: ['approve', 'reject'], type: 'string', title: 'Decision', description: 'Approve or reject the order' },\n },\n },\n slaDuration: 'PT24H',\n },\n },\n { stepId: 'approved', stepName: 'Approved', stepType: 'AUTOMATED', description: 'Order has been approved' },\n { stepId: 'rejected', stepName: 'Rejected', stepType: 'AUTOMATED', description: 'Order has been rejected' },\n { stepId: 'end', stepName: 'Complete', stepType: 'END', description: 'Workflow complete' },\n ] as const,\n transitions: [\n {\n transitionId: 'start_to_pending',\n transitionName: 'Submit for Approval',\n fromStepId: 'start',\n toStepId: 'pending_approval',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_pending_status',\n activityName: 'Set Order to Pending Approval',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.id}}', statusValue: 'pending_approval' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_approval_requested',\n activityName: 'Emit Approval Requested Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.requested',\n payload: { orderId: '{{context.id}}', workflowInstanceId: '{{workflow.instanceId}}' },\n },\n },\n ],\n },\n {\n transitionId: 'pending_approval_to_approved',\n transitionName: 'Approve Order',\n fromStepId: 'pending_approval',\n toStepId: 'approved',\n trigger: 'auto',\n priority: 100,\n preConditions: [{ ruleId: 'workflow_order_approval_check_approved', required: true }],\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_approved_status',\n activityName: 'Set Order to Approved',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.id}}', statusValue: 'approved' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_order_approved',\n activityName: 'Emit Order Approved Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.approved',\n payload: {\n orderId: '{{context.id}}',\n workflowInstanceId: '{{workflow.instanceId}}',\n approvedBy: '{{context.completedBy}}',\n comments: '{{context.comments}}',\n },\n },\n },\n ],\n },\n {\n transitionId: 'pending_approval_to_rejected',\n transitionName: 'Reject Order',\n fromStepId: 'pending_approval',\n toStepId: 'rejected',\n trigger: 'auto',\n priority: 90,\n preConditions: [{ ruleId: 'workflow_order_approval_check_rejected', required: true }],\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_rejected_status',\n activityName: 'Set Order to Rejected',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.id}}', statusValue: 'rejected' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_order_rejected',\n activityName: 'Emit Order Rejected Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.rejected',\n payload: {\n orderId: '{{context.id}}',\n workflowInstanceId: '{{workflow.instanceId}}',\n rejectedBy: '{{context.completedBy}}',\n comments: '{{context.comments}}',\n },\n },\n },\n ],\n },\n {\n transitionId: 'approved_to_end',\n transitionName: 'Complete After Approval',\n fromStepId: 'approved',\n toStepId: 'end',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n },\n {\n transitionId: 'rejected_to_end',\n transitionName: 'Complete After Rejection',\n fromStepId: 'rejected',\n toStepId: 'end',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n },\n ],\n triggers: [{\n triggerId: 'order_approval_trigger',\n name: 'Order Approval Trigger',\n description: 'Triggers when a new sales order is created',\n eventPattern: 'sales.order.created',\n config: { entityType: 'SalesOrder' },\n enabled: true,\n priority: 0,\n }],\n})\n\nexport const workflowsConfig = createWorkflowsModuleConfig({\n moduleId: 'sales',\n workflows: [orderApproval],\n})\n\nexport default workflowsConfig\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Sales Module \u2014 Code-Based Workflow Definitions\n *\n * Order approval workflow shipped with the sales module.\n */\n\nimport { defineWorkflow, createWorkflowsModuleConfig } from '@open-mercato/shared/modules/workflows'\nimport { registerWorkflowSafeCommands } from '@open-mercato/core/modules/workflows/lib/workflow-safe-commands'\n\nregisterWorkflowSafeCommands([\n { commandId: 'sales.orders.update', requiredFeatures: ['sales.orders.manage'] },\n])\n\nconst orderApproval = defineWorkflow({\n workflowId: 'sales.order-approval',\n workflowName: 'Sales Order Approval Workflow',\n description: 'Approval workflow for sales orders requiring authorization before processing',\n metadata: { category: 'Sales', tags: ['sales', 'approval', 'order'], icon: 'check-circle' },\n steps: [\n { stepId: 'start', stepName: 'Start', stepType: 'START', description: 'Initialize order approval workflow' },\n {\n stepId: 'pending_approval',\n stepName: 'Pending Approval',\n stepType: 'USER_TASK',\n description: 'Order awaiting approval decision',\n userTaskConfig: {\n formSchema: {\n type: 'object',\n required: ['decision'],\n properties: {\n comments: { type: 'string', title: 'Comments', description: 'Optional comments for the decision' },\n decision: { enum: ['approve', 'reject'], type: 'string', title: 'Decision', description: 'Approve or reject the order' },\n },\n },\n slaDuration: 'PT24H',\n },\n },\n { stepId: 'approved', stepName: 'Approved', stepType: 'AUTOMATED', description: 'Order has been approved' },\n { stepId: 'rejected', stepName: 'Rejected', stepType: 'AUTOMATED', description: 'Order has been rejected' },\n { stepId: 'end', stepName: 'Complete', stepType: 'END', description: 'Workflow complete' },\n ] as const,\n transitions: [\n {\n transitionId: 'start_to_pending',\n transitionName: 'Submit for Approval',\n fromStepId: 'start',\n toStepId: 'pending_approval',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_pending_status',\n activityName: 'Set Order to Pending Approval',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.orderId}}', statusValue: 'pending_approval' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_approval_requested',\n activityName: 'Emit Approval Requested Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.requested',\n payload: { orderId: '{{context.orderId}}', workflowInstanceId: '{{workflow.instanceId}}' },\n },\n },\n ],\n },\n {\n transitionId: 'pending_approval_to_approved',\n transitionName: 'Approve Order',\n fromStepId: 'pending_approval',\n toStepId: 'approved',\n trigger: 'auto',\n priority: 100,\n preConditions: [{ ruleId: 'workflow_order_approval_check_approved', required: true }],\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_approved_status',\n activityName: 'Set Order to Approved',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.orderId}}', statusValue: 'approved' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_order_approved',\n activityName: 'Emit Order Approved Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.approved',\n payload: {\n orderId: '{{context.orderId}}',\n workflowInstanceId: '{{workflow.instanceId}}',\n approvedBy: '{{context.completedBy}}',\n comments: '{{context.comments}}',\n },\n },\n },\n ],\n },\n {\n transitionId: 'pending_approval_to_rejected',\n transitionName: 'Reject Order',\n fromStepId: 'pending_approval',\n toStepId: 'rejected',\n trigger: 'auto',\n priority: 90,\n preConditions: [{ ruleId: 'workflow_order_approval_check_rejected', required: true }],\n continueOnActivityFailure: true,\n activities: [\n {\n activityId: 'set_rejected_status',\n activityName: 'Set Order to Rejected',\n activityType: 'UPDATE_ENTITY',\n async: false,\n config: {\n commandId: 'sales.orders.update',\n statusDictionary: 'sales.order_status',\n input: { id: '{{context.orderId}}', statusValue: 'rejected' },\n },\n retryPolicy: { maxAttempts: 3, initialIntervalMs: 1000, backoffCoefficient: 2, maxIntervalMs: 10000 },\n },\n {\n activityId: 'emit_order_rejected',\n activityName: 'Emit Order Rejected Event',\n activityType: 'EMIT_EVENT',\n async: true,\n config: {\n eventName: 'sales.order.approval.rejected',\n payload: {\n orderId: '{{context.orderId}}',\n workflowInstanceId: '{{workflow.instanceId}}',\n rejectedBy: '{{context.completedBy}}',\n comments: '{{context.comments}}',\n },\n },\n },\n ],\n },\n {\n transitionId: 'approved_to_end',\n transitionName: 'Complete After Approval',\n fromStepId: 'approved',\n toStepId: 'end',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n },\n {\n transitionId: 'rejected_to_end',\n transitionName: 'Complete After Rejection',\n fromStepId: 'rejected',\n toStepId: 'end',\n trigger: 'auto',\n priority: 100,\n continueOnActivityFailure: true,\n },\n ],\n triggers: [{\n triggerId: 'order_approval_trigger',\n name: 'Order Approval Trigger',\n description: 'Triggers when a new sales order is created',\n eventPattern: 'sales.order.created',\n config: {\n entityType: 'SalesOrder',\n contextMapping: [{ targetKey: 'orderId', sourceExpression: 'id' }],\n },\n enabled: true,\n priority: 0,\n }],\n})\n\nexport const workflowsConfig = createWorkflowsModuleConfig({\n moduleId: 'sales',\n workflows: [orderApproval],\n})\n\nexport default workflowsConfig\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -310,6 +310,12 @@ async function executeEmitEvent(config, context, container) {
310
310
  if (!eventName) {
311
311
  throw new Error('EMIT_EVENT requires "eventName" field');
312
312
  }
313
+ const unresolvedPayloadKeys = findUnresolvedTemplateKeys(payload);
314
+ if (unresolvedPayloadKeys.length > 0) {
315
+ throw new Error(
316
+ `EMIT_EVENT payload contains unresolved template variables for: ${unresolvedPayloadKeys.join(", ")}. Check that the workflow context provides these keys.`
317
+ );
318
+ }
313
319
  const eventBus = container.resolve("eventBus");
314
320
  if (!eventBus || typeof eventBus.emitEvent !== "function") {
315
321
  throw new Error("Event bus not available in container");
@@ -329,6 +335,21 @@ async function executeEmitEvent(config, context, container) {
329
335
  });
330
336
  return { emitted: true, eventName, payload: enrichedPayload };
331
337
  }
338
+ const UNRESOLVED_TEMPLATE_PATTERN = /\{\{[^}]+\}\}/;
339
+ function findUnresolvedTemplateKeys(value, path = "") {
340
+ if (typeof value === "string") {
341
+ return UNRESOLVED_TEMPLATE_PATTERN.test(value) ? [path] : [];
342
+ }
343
+ if (Array.isArray(value)) {
344
+ return value.flatMap((item, index) => findUnresolvedTemplateKeys(item, `${path}[${index}]`));
345
+ }
346
+ if (value && typeof value === "object") {
347
+ return Object.entries(value).flatMap(
348
+ ([key, nested]) => findUnresolvedTemplateKeys(nested, path ? `${path}.${key}` : key)
349
+ );
350
+ }
351
+ return [];
352
+ }
332
353
  async function executeUpdateEntity(em, config, context, container) {
333
354
  const { commandId, input, statusDictionary } = config;
334
355
  if (!commandId) {
@@ -359,6 +380,12 @@ async function executeUpdateEntity(em, config, context, container) {
359
380
  if (!commandBus || typeof commandBus.execute !== "function") {
360
381
  throw new Error("CommandBus not available in container");
361
382
  }
383
+ const unresolvedKeys = findUnresolvedTemplateKeys(input);
384
+ if (unresolvedKeys.length > 0) {
385
+ throw new Error(
386
+ `UPDATE_ENTITY input contains unresolved template variables for: ${unresolvedKeys.join(", ")}. Check that the workflow context provides these keys.`
387
+ );
388
+ }
362
389
  let finalInput = { ...input };
363
390
  if (finalInput.statusValue && statusDictionary) {
364
391
  const statusEntryId = await resolveDictionaryEntryId(
@@ -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'\nimport { getWorkflowSafeCommand } from './workflow-safe-commands'\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 /**\n * @deprecated Use `timeoutMs`. Legacy ISO 8601 duration string accepted by\n * the definition schema before #4424; normalized by `resolveActivityTimeoutMs`.\n */\n timeout?: string\n compensate?: boolean // Flag to execute compensation on failure\n}\n\n/**\n * Effective timeout for an activity, in milliseconds.\n *\n * The editor and this executor both speak `timeoutMs`, but the definition\n * schema historically accepted only an ISO 8601 `timeout` string \u2014 so stored\n * definitions can carry either. Prefer `timeoutMs`; fall back to parsing\n * `timeout`, ignoring a malformed value rather than throwing mid-execution\n * (an unparseable timeout must not fail an activity that would otherwise\n * succeed). Returns undefined when no usable timeout is configured (#4424).\n */\nexport function resolveActivityTimeoutMs(activity: {\n timeoutMs?: number\n timeout?: string\n}): number | undefined {\n if (typeof activity.timeoutMs === 'number' && activity.timeoutMs > 0) {\n return activity.timeoutMs\n }\n if (typeof activity.timeout === 'string' && activity.timeout.trim().length > 0) {\n try {\n const parsed = parseDuration(activity.timeout.trim())\n if (Number.isFinite(parsed) && parsed > 0) return parsed\n } catch {\n return undefined\n }\n }\n return undefined\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\ntype RbacFeatureResolver = {\n userHasAllFeatures: (\n userId: string,\n required: string[],\n opts: { tenantId: string | null; organizationId: string | null }\n ) => Promise<boolean>\n}\n\nasync function workflowUserHasAllFeatures(\n container: AwilixContainer,\n userId: string,\n required: readonly string[],\n tenantId: string | null,\n organizationId: string | null\n): Promise<boolean> {\n try {\n const rbac = container.resolve('rbacService') as RbacFeatureResolver | undefined\n if (rbac?.userHasAllFeatures) {\n return await rbac.userHasAllFeatures(userId, [...required], { tenantId, organizationId })\n }\n } catch {\n // Fail closed below when the workflow executor cannot prove the actor's grants.\n }\n return false\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 (timeoutMs, or a legacy ISO 8601\n // `timeout` string normalized to ms \u2014 see resolveActivityTimeoutMs).\n const timeoutMs = resolveActivityTimeoutMs(activity)\n const result = timeoutMs\n ? await executeWithTimeout(\n (signal) => executeActivityByType(em, container, activity, context, signal),\n 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 signal?: AbortSignal\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, signal)\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, { signal })\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 const workflowSafeCommand = getWorkflowSafeCommand(commandId)\n if (!workflowSafeCommand) {\n throw new Error('UPDATE_ENTITY command is not allowed')\n }\n\n if (!input || typeof input !== 'object') {\n throw new Error('UPDATE_ENTITY requires \"input\" object with entity data')\n }\n\n const actorUserId = typeof context.userId === 'string' ? context.userId.trim() : ''\n if (!actorUserId) {\n throw new Error('UPDATE_ENTITY requires an authenticated workflow user')\n }\n\n const authorized = await workflowUserHasAllFeatures(\n container,\n actorUserId,\n workflowSafeCommand.requiredFeatures,\n context.workflowInstance.tenantId,\n context.workflowInstance.organizationId\n )\n if (!authorized) {\n throw new Error('UPDATE_ENTITY command is not authorized')\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 const ctx = {\n container,\n auth: {\n sub: actorUserId,\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: (signal: AbortSignal) => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n const abortController = new AbortController()\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n abortController.abort()\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(abortController.signal), 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'\nimport { getWorkflowSafeCommand } from './workflow-safe-commands'\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 /**\n * @deprecated Use `timeoutMs`. Legacy ISO 8601 duration string accepted by\n * the definition schema before #4424; normalized by `resolveActivityTimeoutMs`.\n */\n timeout?: string\n compensate?: boolean // Flag to execute compensation on failure\n}\n\n/**\n * Effective timeout for an activity, in milliseconds.\n *\n * The editor and this executor both speak `timeoutMs`, but the definition\n * schema historically accepted only an ISO 8601 `timeout` string \u2014 so stored\n * definitions can carry either. Prefer `timeoutMs`; fall back to parsing\n * `timeout`, ignoring a malformed value rather than throwing mid-execution\n * (an unparseable timeout must not fail an activity that would otherwise\n * succeed). Returns undefined when no usable timeout is configured (#4424).\n */\nexport function resolveActivityTimeoutMs(activity: {\n timeoutMs?: number\n timeout?: string\n}): number | undefined {\n if (typeof activity.timeoutMs === 'number' && activity.timeoutMs > 0) {\n return activity.timeoutMs\n }\n if (typeof activity.timeout === 'string' && activity.timeout.trim().length > 0) {\n try {\n const parsed = parseDuration(activity.timeout.trim())\n if (Number.isFinite(parsed) && parsed > 0) return parsed\n } catch {\n return undefined\n }\n }\n return undefined\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\ntype RbacFeatureResolver = {\n userHasAllFeatures: (\n userId: string,\n required: string[],\n opts: { tenantId: string | null; organizationId: string | null }\n ) => Promise<boolean>\n}\n\nasync function workflowUserHasAllFeatures(\n container: AwilixContainer,\n userId: string,\n required: readonly string[],\n tenantId: string | null,\n organizationId: string | null\n): Promise<boolean> {\n try {\n const rbac = container.resolve('rbacService') as RbacFeatureResolver | undefined\n if (rbac?.userHasAllFeatures) {\n return await rbac.userHasAllFeatures(userId, [...required], { tenantId, organizationId })\n }\n } catch {\n // Fail closed below when the workflow executor cannot prove the actor's grants.\n }\n return false\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 (timeoutMs, or a legacy ISO 8601\n // `timeout` string normalized to ms \u2014 see resolveActivityTimeoutMs).\n const timeoutMs = resolveActivityTimeoutMs(activity)\n const result = timeoutMs\n ? await executeWithTimeout(\n (signal) => executeActivityByType(em, container, activity, context, signal),\n 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 signal?: AbortSignal\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, signal)\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, { signal })\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 // Emissions are fire-and-forget and no subscriber validates the payload shape,\n // so an unresolved `{{context.x}}` here is even quieter than on the command\n // path \u2014 it ships the literal template to every consumer. Fail loudly instead.\n const unresolvedPayloadKeys = findUnresolvedTemplateKeys(payload)\n if (unresolvedPayloadKeys.length > 0) {\n throw new Error(\n `EMIT_EVENT payload contains unresolved template variables for: ${unresolvedPayloadKeys.join(', ')}. ` +\n `Check that the workflow context provides these keys.`\n )\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\nconst UNRESOLVED_TEMPLATE_PATTERN = /\\{\\{[^}]+\\}\\}/\n\nfunction findUnresolvedTemplateKeys(value: unknown, path = ''): string[] {\n if (typeof value === 'string') {\n return UNRESOLVED_TEMPLATE_PATTERN.test(value) ? [path] : []\n }\n if (Array.isArray(value)) {\n return value.flatMap((item, index) => findUnresolvedTemplateKeys(item, `${path}[${index}]`))\n }\n if (value && typeof value === 'object') {\n return Object.entries(value).flatMap(([key, nested]) =>\n findUnresolvedTemplateKeys(nested, path ? `${path}.${key}` : key)\n )\n }\n return []\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.orderId}}\",\n * \"statusValue\": \"pending_approval\"\n * }\n * }\n * ```\n *\n * Every `{{...}}` reference in `input` must resolve against the workflow context \u2014\n * an unresolved reference is rejected rather than forwarded to the command bus.\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 const workflowSafeCommand = getWorkflowSafeCommand(commandId)\n if (!workflowSafeCommand) {\n throw new Error('UPDATE_ENTITY command is not allowed')\n }\n\n if (!input || typeof input !== 'object') {\n throw new Error('UPDATE_ENTITY requires \"input\" object with entity data')\n }\n\n const actorUserId = typeof context.userId === 'string' ? context.userId.trim() : ''\n if (!actorUserId) {\n throw new Error('UPDATE_ENTITY requires an authenticated workflow user')\n }\n\n const authorized = await workflowUserHasAllFeatures(\n container,\n actorUserId,\n workflowSafeCommand.requiredFeatures,\n context.workflowInstance.tenantId,\n context.workflowInstance.organizationId\n )\n if (!authorized) {\n throw new Error('UPDATE_ENTITY command is not authorized')\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 const unresolvedKeys = findUnresolvedTemplateKeys(input)\n if (unresolvedKeys.length > 0) {\n throw new Error(\n `UPDATE_ENTITY input contains unresolved template variables for: ${unresolvedKeys.join(', ')}. ` +\n `Check that the workflow context provides these keys.`\n )\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 const ctx = {\n container,\n auth: {\n sub: actorUserId,\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: (signal: AbortSignal) => Promise<T>,\n timeoutMs: number\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n const abortController = new AbortController()\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n abortController.abort()\n reject(new Error(`Activity execution timeout after ${timeoutMs}ms`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([executor(abortController.signal), timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,10 @@
1
+ const ORDER_APPROVAL_CONTEXT_KEYS = [
2
+ "orderId",
3
+ "pendingApprovalStatusId",
4
+ "approvedStatusId",
5
+ "rejectedStatusId"
6
+ ];
7
+ export {
8
+ ORDER_APPROVAL_CONTEXT_KEYS
9
+ };
10
+ //# sourceMappingURL=context-keys.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/workflows/widgets/injection/order-approval/context-keys.ts"],
4
+ "sourcesContent": ["/**\n * The context keys the order-approval widget seeds when it starts the\n * `sales.order-approval` workflow. The workflow definition interpolates\n * `{{context.<key>}}` against exactly this set, and `interpolateVariables()`\n * silently returns the original string when a key is missing \u2014 so the two must\n * not drift. Both the widget and the contract test import this list, which is\n * what makes the drift a type error rather than a runtime surprise (issue #4334).\n */\nexport const ORDER_APPROVAL_CONTEXT_KEYS = [\n 'orderId',\n 'pendingApprovalStatusId',\n 'approvedStatusId',\n 'rejectedStatusId',\n] as const\n\nexport type OrderApprovalContextKey = (typeof ORDER_APPROVAL_CONTEXT_KEYS)[number]\n\nexport type OrderApprovalInitialContext = Record<OrderApprovalContextKey, string>\n"],
5
+ "mappings": "AAQO,MAAM,8BAA8B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
6
+ "names": []
7
+ }
@@ -129,19 +129,23 @@ function OrderApprovalWidget({ data }) {
129
129
  const pendingApprovalStatusId = findStatusId("pending_approval");
130
130
  const approvedStatusId = findStatusId("approved");
131
131
  const rejectedStatusId = findStatusId("rejected");
132
+ if (!orderId) {
133
+ throw new Error(t("workflows.orderApproval.missingOrderId", "Cannot start the approval workflow before the order has loaded."));
134
+ }
132
135
  if (!pendingApprovalStatusId || !approvedStatusId || !rejectedStatusId) {
133
136
  throw new Error(t("workflows.orderApproval.missingStatuses", "Missing order status entries. Please ensure pending_approval, approved, and rejected statuses exist in the sales.order_status dictionary."));
134
137
  }
138
+ const initialContext = {
139
+ orderId,
140
+ pendingApprovalStatusId,
141
+ approvedStatusId,
142
+ rejectedStatusId
143
+ };
135
144
  const result = await apiCall("/api/workflows/instances", {
136
145
  method: "POST",
137
146
  body: JSON.stringify({
138
147
  workflowId: WORKFLOW_ID,
139
- initialContext: {
140
- orderId,
141
- pendingApprovalStatusId,
142
- approvedStatusId,
143
- rejectedStatusId
144
- },
148
+ initialContext,
145
149
  metadata: {
146
150
  entityType: "SalesOrder",
147
151
  entityId: orderId