@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240

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 (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +7 -2
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/audit_logs/services/accessLogService.js +127 -8
  7. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  8. package/dist/modules/auth/backend/auth/profile/page.js +1 -1
  9. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  10. package/dist/modules/auth/backend/profile/change-password/page.js +1 -1
  11. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  12. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  14. package/dist/modules/auth/backend/users/create/page.js +6 -1
  15. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  16. package/dist/modules/auth/di.js +17 -3
  17. package/dist/modules/auth/di.js.map +2 -2
  18. package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
  19. package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
  20. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +8 -1
  21. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  23. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +3 -2
  25. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  26. package/dist/modules/configs/cli.js +27 -14
  27. package/dist/modules/configs/cli.js.map +2 -2
  28. package/dist/modules/currencies/api/currencies/route.js +3 -4
  29. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  30. package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
  31. package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
  32. package/dist/modules/customers/api/people/route.js +26 -24
  33. package/dist/modules/customers/api/people/route.js.map +2 -2
  34. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
  35. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
  36. package/dist/modules/directory/utils/organizationScope.js +85 -0
  37. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  38. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  39. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  40. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
  41. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  42. package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
  43. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  44. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
  45. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  46. package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  48. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  50. package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
  51. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  52. package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
  53. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  54. package/dist/modules/workflows/components/StepsEditor.js +31 -0
  55. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
  57. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  58. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
  59. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
  60. package/dist/modules/workflows/components/nodes/index.js +3 -1
  61. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  62. package/dist/modules/workflows/data/validators.js +117 -0
  63. package/dist/modules/workflows/data/validators.js.map +2 -2
  64. package/dist/modules/workflows/di.js +5 -1
  65. package/dist/modules/workflows/di.js.map +2 -2
  66. package/dist/modules/workflows/lib/activity-executor.js +42 -1
  67. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  68. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  69. package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
  70. package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
  71. package/dist/modules/workflows/lib/duration.js +32 -0
  72. package/dist/modules/workflows/lib/duration.js.map +7 -0
  73. package/dist/modules/workflows/lib/event-logger.js +1 -0
  74. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  75. package/dist/modules/workflows/lib/format-validation-error.js +12 -0
  76. package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
  77. package/dist/modules/workflows/lib/graph-utils.js +6 -3
  78. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  79. package/dist/modules/workflows/lib/node-type-icons.js +9 -5
  80. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  81. package/dist/modules/workflows/lib/signal-handler.js +55 -23
  82. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  83. package/dist/modules/workflows/lib/step-handler.js +79 -29
  84. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/timer-handler.js +159 -0
  86. package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
  87. package/dist/modules/workflows/lib/workflow-executor.js +1 -1
  88. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  89. package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
  90. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  91. package/package.json +7 -7
  92. package/src/modules/attachments/api/file/[id]/route.ts +7 -2
  93. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
  94. package/src/modules/audit_logs/services/accessLogService.ts +179 -15
  95. package/src/modules/auth/backend/auth/profile/page.tsx +1 -1
  96. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -1
  97. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  98. package/src/modules/auth/backend/users/create/page.tsx +6 -1
  99. package/src/modules/auth/di.ts +26 -3
  100. package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
  101. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +8 -1
  102. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +3 -2
  103. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +3 -2
  104. package/src/modules/configs/cli.ts +34 -13
  105. package/src/modules/currencies/api/currencies/route.ts +3 -4
  106. package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
  107. package/src/modules/customers/api/people/route.ts +27 -25
  108. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
  109. package/src/modules/directory/utils/organizationScope.ts +121 -0
  110. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  111. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
  112. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
  113. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
  114. package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
  115. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
  116. package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
  117. package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
  118. package/src/modules/workflows/components/StepsEditor.tsx +36 -0
  119. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
  120. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
  121. package/src/modules/workflows/components/nodes/index.ts +3 -0
  122. package/src/modules/workflows/data/validators.ts +121 -0
  123. package/src/modules/workflows/di.ts +4 -0
  124. package/src/modules/workflows/i18n/de.json +10 -1
  125. package/src/modules/workflows/i18n/en.json +10 -1
  126. package/src/modules/workflows/i18n/es.json +10 -1
  127. package/src/modules/workflows/i18n/pl.json +10 -1
  128. package/src/modules/workflows/lib/activity-executor.ts +86 -2
  129. package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
  130. package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
  131. package/src/modules/workflows/lib/duration.ts +51 -0
  132. package/src/modules/workflows/lib/event-logger.ts +1 -0
  133. package/src/modules/workflows/lib/format-validation-error.ts +30 -0
  134. package/src/modules/workflows/lib/graph-utils.ts +3 -0
  135. package/src/modules/workflows/lib/node-type-icons.ts +6 -2
  136. package/src/modules/workflows/lib/signal-handler.ts +62 -24
  137. package/src/modules/workflows/lib/step-handler.ts +107 -50
  138. package/src/modules/workflows/lib/timer-handler.ts +213 -0
  139. package/src/modules/workflows/lib/workflow-executor.ts +1 -1
  140. package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
@@ -1,4 +1,3 @@
1
- import { WORKFLOW_ACTIVITIES_QUEUE_NAME } from "../lib/activity-queue-types.js";
2
1
  import { WorkflowInstance } from "../data/entities.js";
3
2
  import { logWorkflowEvent } from "../lib/event-logger.js";
4
3
  import {
@@ -9,21 +8,36 @@ import {
9
8
  executeCallWebhook,
10
9
  executeFunction
11
10
  } from "../lib/activity-executor.js";
11
+ const WORKFLOW_ACTIVITIES_QUEUE = "workflow-activities";
12
12
  const DEFAULT_CONCURRENCY = 1;
13
13
  const envConcurrency = process.env.WORKERS_WORKFLOW_ACTIVITIES_CONCURRENCY;
14
14
  const metadata = {
15
- queue: WORKFLOW_ACTIVITIES_QUEUE_NAME,
15
+ queue: WORKFLOW_ACTIVITIES_QUEUE,
16
16
  id: "workflows:workflow-activities",
17
17
  concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY
18
18
  };
19
19
  async function handle(job, ctx) {
20
20
  const { payload } = job;
21
21
  const startTime = Date.now();
22
+ const em = ctx.resolve("em");
23
+ const container = ctx;
24
+ if (payload.kind === "timer") {
25
+ console.log(
26
+ `[workflows:activity-worker] Firing timer for instance ${payload.workflowInstanceId} (job ${ctx.jobId})`
27
+ );
28
+ const { fireTimer } = await import("../lib/timer-handler.js");
29
+ await fireTimer(em, container, {
30
+ instanceId: payload.workflowInstanceId,
31
+ stepInstanceId: payload.stepInstanceId,
32
+ tenantId: payload.tenantId,
33
+ organizationId: payload.organizationId,
34
+ userId: payload.userId
35
+ });
36
+ return;
37
+ }
22
38
  console.log(
23
39
  `[workflows:activity-worker] Processing activity ${payload.activityId} (${payload.activityType}) for workflow instance ${payload.workflowInstanceId} (job ${ctx.jobId}, attempt ${ctx.attemptNumber})`
24
40
  );
25
- const em = ctx.resolve("em");
26
- const container = ctx;
27
41
  try {
28
42
  const instance = await em.findOne(WorkflowInstance, {
29
43
  id: payload.workflowInstanceId,
@@ -67,6 +81,8 @@ async function handle(job, ctx) {
67
81
  return await executeCallWebhook(payload.activityConfig, activityContext, { signal });
68
82
  case "EXECUTE_FUNCTION":
69
83
  return await executeFunction(payload.activityConfig, activityContext, container);
84
+ case "WAIT":
85
+ return { waited: true };
70
86
  default:
71
87
  throw new Error(`Unsupported activity type: ${payload.activityType}`);
72
88
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/workflows/workers/workflow-activities.worker.ts"],
4
- "sourcesContent": ["/**\n * Workflow Activity Worker\n *\n * Background worker that processes async activities from the workflow queue.\n * Executes activities with timeout, logs events, and triggers workflow resume.\n *\n * This worker is auto-discovered by the queue system and processes jobs from\n * the 'workflow-activities' queue.\n */\n\nimport type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport { WORKFLOW_ACTIVITIES_QUEUE_NAME, type WorkflowActivityJob } from '../lib/activity-queue-types'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { logWorkflowEvent } from '../lib/event-logger'\nimport {\n executeSendEmail,\n executeCallApi,\n executeEmitEvent,\n executeUpdateEntity,\n executeCallWebhook,\n executeFunction,\n} from '../lib/activity-executor'\n\n// Worker metadata for auto-discovery\nconst DEFAULT_CONCURRENCY = 1\nconst envConcurrency = process.env.WORKERS_WORKFLOW_ACTIVITIES_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: WORKFLOW_ACTIVITIES_QUEUE_NAME,\n id: 'workflows:workflow-activities',\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\n/**\n * Process a workflow activity job.\n *\n * This handler:\n * 1. Fetches the workflow instance\n * 2. Executes the activity by type with timeout support\n * 3. Logs success/failure events to workflow event log\n * 4. Attempts to resume the workflow if all activities complete\n *\n * @param job - The queued job containing activity payload\n * @param ctx - Job context with DI container access\n */\nexport default async function handle(\n job: QueuedJob<WorkflowActivityJob>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n const { payload } = job\n const startTime = Date.now()\n\n console.log(\n `[workflows:activity-worker] Processing activity ${payload.activityId} (${payload.activityType}) for workflow instance ${payload.workflowInstanceId} (job ${ctx.jobId}, attempt ${ctx.attemptNumber})`\n )\n\n // Resolve services from DI container\n const em = ctx.resolve<EntityManager>('em')\n\n // Create a container-like object from ctx.resolve for activity executors\n // The ctx already has the resolve method we need, we just need to cast it\n const container = ctx as unknown as AwilixContainer\n\n try {\n // Fetch workflow instance with tenant/org scoping\n const instance = await em.findOne(WorkflowInstance, {\n id: payload.workflowInstanceId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n if (!instance) {\n throw new Error(\n `Workflow instance ${payload.workflowInstanceId} not found (tenant: ${payload.tenantId}, org: ${payload.organizationId})`\n )\n }\n\n // Build activity execution context\n const activityContext = {\n workflowInstance: instance,\n workflowContext: payload.workflowContext,\n stepContext: payload.stepContext,\n stepInstanceId: payload.stepInstanceId,\n userId: payload.userId,\n }\n\n // Execute activity by type\n const executeActivityByType = async (signal?: AbortSignal) => {\n switch (payload.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(payload.activityConfig, activityContext, container)\n case 'CALL_API':\n return await executeCallApi(\n em,\n payload.activityConfig,\n activityContext,\n container,\n signal\n )\n case 'EMIT_EVENT':\n return await executeEmitEvent(payload.activityConfig, activityContext, container)\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(\n em,\n payload.activityConfig,\n activityContext,\n container\n )\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(payload.activityConfig, activityContext, { signal })\n case 'EXECUTE_FUNCTION':\n return await executeFunction(payload.activityConfig, activityContext, container)\n default:\n throw new Error(`Unsupported activity type: ${payload.activityType}`)\n }\n }\n\n // Execute with optional timeout. AbortController aborts in-flight fetches\n // when the timeout wins the race, preventing phantom executions.\n let result: any\n if (payload.timeoutMs && payload.timeoutMs > 0) {\n const abortController = new AbortController()\n const timeoutId = setTimeout(() => {\n abortController.abort()\n }, payload.timeoutMs)\n\n try {\n result = await Promise.race([\n executeActivityByType(abortController.signal),\n new Promise((_, reject) =>\n setTimeout(\n () => reject(new Error(`Activity timeout after ${payload.timeoutMs}ms`)),\n payload.timeoutMs\n )\n ),\n ])\n } finally {\n clearTimeout(timeoutId)\n }\n } else {\n result = await executeActivityByType()\n }\n\n const executionTimeMs = Date.now() - startTime\n\n // Log success event to workflow event log\n await logWorkflowEvent(em, {\n workflowInstanceId: payload.workflowInstanceId,\n stepInstanceId: payload.stepInstanceId,\n eventType: 'ACTIVITY_COMPLETED',\n eventData: {\n activityId: payload.activityId,\n activityName: payload.activityName,\n activityType: payload.activityType,\n async: true,\n jobId: ctx.jobId,\n attemptNumber: ctx.attemptNumber,\n executionTimeMs,\n output: result,\n },\n userId: payload.userId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n console.log(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) completed successfully for workflow instance ${payload.workflowInstanceId} in ${executionTimeMs}ms`\n )\n\n // Attempt to resume workflow if all activities complete\n await checkAndResumeWorkflow(em, ctx, payload.workflowInstanceId)\n } catch (error: any) {\n const executionTimeMs = Date.now() - startTime\n\n console.error(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) failed for workflow instance ${payload.workflowInstanceId} (attempt ${ctx.attemptNumber}):`,\n error.message\n )\n\n // Log failure event to workflow event log\n await logWorkflowEvent(em, {\n workflowInstanceId: payload.workflowInstanceId,\n stepInstanceId: payload.stepInstanceId,\n eventType: 'ACTIVITY_FAILED',\n eventData: {\n activityId: payload.activityId,\n activityName: payload.activityName,\n activityType: payload.activityType,\n async: true,\n jobId: ctx.jobId,\n attemptNumber: ctx.attemptNumber,\n error: error.message,\n errorStack: error.stack,\n executionTimeMs,\n },\n userId: payload.userId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n // Check if this was final attempt (BullMQ handles retries automatically)\n const maxAttempts = payload.retryPolicy?.maxAttempts || 1\n if (ctx.attemptNumber >= maxAttempts) {\n console.error(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) failed after ${maxAttempts} attempts for workflow instance ${payload.workflowInstanceId} - triggering workflow failure handling`\n )\n // Final failure - attempt to resume workflow (may transition to FAILED state)\n await checkAndResumeWorkflow(em, ctx, payload.workflowInstanceId)\n }\n\n // Re-throw to let BullMQ handle retry logic\n throw error\n }\n}\n\n/**\n * Helper to check if workflow can resume after activities complete/fail.\n *\n * This function is called after each activity completes or fails.\n * It checks if all pending activities are done and resumes workflow execution.\n *\n * @param em - Entity manager\n * @param ctx - Handler context with resolve method for DI\n * @param workflowInstanceId - Workflow instance ID to resume\n */\nasync function checkAndResumeWorkflow(\n em: EntityManager,\n ctx: HandlerContext,\n workflowInstanceId: string\n): Promise<void> {\n // Import here to avoid circular dependency\n const { resumeWorkflowAfterActivities } = await import('../lib/workflow-executor')\n\n // Cast ctx to AwilixContainer for the resume function\n const container = ctx as unknown as AwilixContainer\n\n try {\n await resumeWorkflowAfterActivities(em, container, workflowInstanceId)\n } catch (error: any) {\n // Ignore error if workflow not ready to resume yet (activities still pending)\n if (!error.message?.includes('Activities still pending')) {\n console.error(\n `[workflows:activity-worker] Failed to resume workflow instance ${workflowInstanceId}:`,\n error.message\n )\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Workflow Activity Worker\n *\n * Background worker that processes async activities from the workflow queue.\n * Executes activities with timeout, logs events, and triggers workflow resume.\n *\n * This worker is auto-discovered by the queue system and processes jobs from\n * the 'workflow-activities' queue.\n */\n\nimport type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'\nimport type { WorkflowActivityJob } from '../lib/activity-queue-types'\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { WorkflowInstance } from '../data/entities'\nimport { logWorkflowEvent } from '../lib/event-logger'\nimport {\n executeSendEmail,\n executeCallApi,\n executeEmitEvent,\n executeUpdateEntity,\n executeCallWebhook,\n executeFunction,\n} from '../lib/activity-executor'\n\n// Worker metadata for auto-discovery.\n// NOTE: `queue` MUST be a string literal (or locally-declared const) so the\n// generator's AST-based extractor can resolve it when Node cannot import the\n// .ts source file directly. Importing `WORKFLOW_ACTIVITIES_QUEUE_NAME` from\n// another module breaks auto-discovery and silently drops the worker from\n// `modules.generated.ts`.\nconst WORKFLOW_ACTIVITIES_QUEUE = 'workflow-activities'\nconst DEFAULT_CONCURRENCY = 1\nconst envConcurrency = process.env.WORKERS_WORKFLOW_ACTIVITIES_CONCURRENCY\n\nexport const metadata: WorkerMeta = {\n queue: WORKFLOW_ACTIVITIES_QUEUE,\n id: 'workflows:workflow-activities',\n concurrency: envConcurrency ? parseInt(envConcurrency, 10) : DEFAULT_CONCURRENCY,\n}\n\ntype HandlerContext = { resolve: <T = unknown>(name: string) => T }\n\n/**\n * Process a workflow activity job.\n *\n * This handler:\n * 1. Fetches the workflow instance\n * 2. Executes the activity by type with timeout support\n * 3. Logs success/failure events to workflow event log\n * 4. Attempts to resume the workflow if all activities complete\n *\n * @param job - The queued job containing activity payload\n * @param ctx - Job context with DI container access\n */\nexport default async function handle(\n job: QueuedJob<WorkflowActivityJob>,\n ctx: JobContext & HandlerContext\n): Promise<void> {\n const { payload } = job\n const startTime = Date.now()\n\n // Resolve services from DI container\n const em = ctx.resolve<EntityManager>('em')\n\n // Create a container-like object from ctx.resolve for activity executors\n // The ctx already has the resolve method we need, we just need to cast it\n const container = ctx as unknown as AwilixContainer\n\n // Timer jobs (kind: 'timer') are a distinct flow \u2014 they resume a paused\n // workflow at a WAIT_FOR_TIMER step rather than running an activity.\n if (payload.kind === 'timer') {\n console.log(\n `[workflows:activity-worker] Firing timer for instance ${payload.workflowInstanceId} (job ${ctx.jobId})`\n )\n const { fireTimer } = await import('../lib/timer-handler')\n await fireTimer(em, container, {\n instanceId: payload.workflowInstanceId,\n stepInstanceId: payload.stepInstanceId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n userId: payload.userId,\n })\n return\n }\n\n console.log(\n `[workflows:activity-worker] Processing activity ${payload.activityId} (${payload.activityType}) for workflow instance ${payload.workflowInstanceId} (job ${ctx.jobId}, attempt ${ctx.attemptNumber})`\n )\n\n try {\n // Fetch workflow instance with tenant/org scoping\n const instance = await em.findOne(WorkflowInstance, {\n id: payload.workflowInstanceId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n if (!instance) {\n throw new Error(\n `Workflow instance ${payload.workflowInstanceId} not found (tenant: ${payload.tenantId}, org: ${payload.organizationId})`\n )\n }\n\n // Build activity execution context\n const activityContext = {\n workflowInstance: instance,\n workflowContext: payload.workflowContext,\n stepContext: payload.stepContext,\n stepInstanceId: payload.stepInstanceId,\n userId: payload.userId,\n }\n\n // Execute activity by type\n const executeActivityByType = async (signal?: AbortSignal) => {\n switch (payload.activityType) {\n case 'SEND_EMAIL':\n return await executeSendEmail(payload.activityConfig, activityContext, container)\n case 'CALL_API':\n return await executeCallApi(\n em,\n payload.activityConfig,\n activityContext,\n container,\n signal\n )\n case 'EMIT_EVENT':\n return await executeEmitEvent(payload.activityConfig, activityContext, container)\n case 'UPDATE_ENTITY':\n return await executeUpdateEntity(\n em,\n payload.activityConfig,\n activityContext,\n container\n )\n case 'CALL_WEBHOOK':\n return await executeCallWebhook(payload.activityConfig, activityContext, { signal })\n case 'EXECUTE_FUNCTION':\n return await executeFunction(payload.activityConfig, activityContext, container)\n case 'WAIT':\n // Delay already handled by queue's delayMs \u2014 return success immediately\n return { waited: true }\n default:\n throw new Error(`Unsupported activity type: ${payload.activityType}`)\n }\n }\n\n // Execute with optional timeout. AbortController aborts in-flight fetches\n // when the timeout wins the race, preventing phantom executions.\n let result: any\n if (payload.timeoutMs && payload.timeoutMs > 0) {\n const abortController = new AbortController()\n const timeoutId = setTimeout(() => {\n abortController.abort()\n }, payload.timeoutMs)\n\n try {\n result = await Promise.race([\n executeActivityByType(abortController.signal),\n new Promise((_, reject) =>\n setTimeout(\n () => reject(new Error(`Activity timeout after ${payload.timeoutMs}ms`)),\n payload.timeoutMs\n )\n ),\n ])\n } finally {\n clearTimeout(timeoutId)\n }\n } else {\n result = await executeActivityByType()\n }\n\n const executionTimeMs = Date.now() - startTime\n\n // Log success event to workflow event log\n await logWorkflowEvent(em, {\n workflowInstanceId: payload.workflowInstanceId,\n stepInstanceId: payload.stepInstanceId,\n eventType: 'ACTIVITY_COMPLETED',\n eventData: {\n activityId: payload.activityId,\n activityName: payload.activityName,\n activityType: payload.activityType,\n async: true,\n jobId: ctx.jobId,\n attemptNumber: ctx.attemptNumber,\n executionTimeMs,\n output: result,\n },\n userId: payload.userId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n console.log(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) completed successfully for workflow instance ${payload.workflowInstanceId} in ${executionTimeMs}ms`\n )\n\n // Attempt to resume workflow if all activities complete\n await checkAndResumeWorkflow(em, ctx, payload.workflowInstanceId)\n } catch (error: any) {\n const executionTimeMs = Date.now() - startTime\n\n console.error(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) failed for workflow instance ${payload.workflowInstanceId} (attempt ${ctx.attemptNumber}):`,\n error.message\n )\n\n // Log failure event to workflow event log\n await logWorkflowEvent(em, {\n workflowInstanceId: payload.workflowInstanceId,\n stepInstanceId: payload.stepInstanceId,\n eventType: 'ACTIVITY_FAILED',\n eventData: {\n activityId: payload.activityId,\n activityName: payload.activityName,\n activityType: payload.activityType,\n async: true,\n jobId: ctx.jobId,\n attemptNumber: ctx.attemptNumber,\n error: error.message,\n errorStack: error.stack,\n executionTimeMs,\n },\n userId: payload.userId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n })\n\n // Check if this was final attempt (BullMQ handles retries automatically)\n const maxAttempts = payload.retryPolicy?.maxAttempts || 1\n if (ctx.attemptNumber >= maxAttempts) {\n console.error(\n `[workflows:activity-worker] Activity ${payload.activityId} (${payload.activityType}) failed after ${maxAttempts} attempts for workflow instance ${payload.workflowInstanceId} - triggering workflow failure handling`\n )\n // Final failure - attempt to resume workflow (may transition to FAILED state)\n await checkAndResumeWorkflow(em, ctx, payload.workflowInstanceId)\n }\n\n // Re-throw to let BullMQ handle retry logic\n throw error\n }\n}\n\n/**\n * Helper to check if workflow can resume after activities complete/fail.\n *\n * This function is called after each activity completes or fails.\n * It checks if all pending activities are done and resumes workflow execution.\n *\n * @param em - Entity manager\n * @param ctx - Handler context with resolve method for DI\n * @param workflowInstanceId - Workflow instance ID to resume\n */\nasync function checkAndResumeWorkflow(\n em: EntityManager,\n ctx: HandlerContext,\n workflowInstanceId: string\n): Promise<void> {\n // Import here to avoid circular dependency\n const { resumeWorkflowAfterActivities } = await import('../lib/workflow-executor')\n\n // Cast ctx to AwilixContainer for the resume function\n const container = ctx as unknown as AwilixContainer\n\n try {\n await resumeWorkflowAfterActivities(em, container, workflowInstanceId)\n } catch (error: any) {\n // Ignore error if workflow not ready to resume yet (activities still pending)\n if (!error.message?.includes('Activities still pending')) {\n console.error(\n `[workflows:activity-worker] Failed to resume workflow instance ${workflowInstanceId}:`,\n error.message\n )\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.3-develop.3876.1.d40fe4ec2d",
3
+ "version": "0.6.3-develop.3894.1.352abf4240",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.3-develop.3876.1.d40fe4ec2d",
247
- "@open-mercato/shared": "0.6.3-develop.3876.1.d40fe4ec2d",
248
- "@open-mercato/ui": "0.6.3-develop.3876.1.d40fe4ec2d",
246
+ "@open-mercato/ai-assistant": "0.6.3-develop.3894.1.352abf4240",
247
+ "@open-mercato/shared": "0.6.3-develop.3894.1.352abf4240",
248
+ "@open-mercato/ui": "0.6.3-develop.3894.1.352abf4240",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.3-develop.3876.1.d40fe4ec2d",
254
- "@open-mercato/shared": "0.6.3-develop.3876.1.d40fe4ec2d",
255
- "@open-mercato/ui": "0.6.3-develop.3876.1.d40fe4ec2d",
253
+ "@open-mercato/ai-assistant": "0.6.3-develop.3894.1.352abf4240",
254
+ "@open-mercato/shared": "0.6.3-develop.3894.1.352abf4240",
255
+ "@open-mercato/ui": "0.6.3-develop.3894.1.352abf4240",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -7,7 +7,7 @@ import {
7
7
  AttachmentPartition,
8
8
  } from "@open-mercato/core/modules/attachments/data/entities";
9
9
  import type { EntityManager } from "@mikro-orm/postgresql";
10
- import { checkAttachmentAccess } from "@open-mercato/core/modules/attachments/lib/access";
10
+ import { checkAttachmentAccess, isSuperAdminAuth } from "@open-mercato/core/modules/attachments/lib/access";
11
11
  import { z } from "zod";
12
12
  import { attachmentsTag, attachmentErrorSchema } from "../../openapi";
13
13
  import {
@@ -38,7 +38,12 @@ export async function GET(
38
38
  (resolve("storageDriverFactory") as StorageDriverFactory | null) ??
39
39
  new StorageDriverFactory(em);
40
40
 
41
- const attachment = await em.findOne(Attachment, { id });
41
+ const findFilter: Record<string, unknown> = { id };
42
+ if (auth && !isSuperAdminAuth(auth)) {
43
+ if (auth.tenantId) findFilter.tenantId = auth.tenantId;
44
+ if (auth.orgId) findFilter.organizationId = auth.orgId;
45
+ }
46
+ const attachment = await em.findOne(Attachment, findFilter);
42
47
  if (!attachment) {
43
48
  return NextResponse.json(
44
49
  { error: "Attachment not found" },
@@ -11,7 +11,7 @@ import {
11
11
  writeThumbnailCache,
12
12
  } from '@open-mercato/core/modules/attachments/lib/thumbnailCache'
13
13
  import { canRenderInlineAttachment } from '@open-mercato/core/modules/attachments/lib/security'
14
- import { checkAttachmentAccess } from '@open-mercato/core/modules/attachments/lib/access'
14
+ import { checkAttachmentAccess, isSuperAdminAuth } from '@open-mercato/core/modules/attachments/lib/access'
15
15
  import type { EntityManager } from '@mikro-orm/postgresql'
16
16
  import { attachmentsTag, imageQuerySchema, attachmentErrorSchema } from '../../../openapi'
17
17
  import {
@@ -53,9 +53,12 @@ export async function GET(
53
53
  const storageDriverFactory =
54
54
  (resolve('storageDriverFactory') as StorageDriverFactory | null) ?? new StorageDriverFactory(em)
55
55
 
56
- const attachment = await em.findOne(Attachment, {
57
- id,
58
- })
56
+ const findFilter: Record<string, unknown> = { id }
57
+ if (auth && !isSuperAdminAuth(auth)) {
58
+ if (auth.tenantId) findFilter.tenantId = auth.tenantId
59
+ if (auth.orgId) findFilter.organizationId = auth.orgId
60
+ }
61
+ const attachment = await em.findOne(Attachment, findFilter)
59
62
  if (!attachment) {
60
63
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
61
64
  }
@@ -25,24 +25,191 @@ const NON_CORE_RETENTION_HOURS = toPositiveNumber(process.env.AUDIT_LOGS_NON_COR
25
25
  const CORE_RETENTION_MS = CORE_RETENTION_DAYS * 24 * 60 * 60 * 1000
26
26
  const NON_CORE_RETENTION_MS = NON_CORE_RETENTION_HOURS * 60 * 60 * 1000
27
27
  const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
28
+ // Postgres has a hard limit of 65k bind parameters per statement. Each access
29
+ // log row uses 10 bind values (see INSERT below), so 500 rows × 10 = 5 000
30
+ // parameters — well below the limit while keeping memory bounded.
31
+ const MAX_BATCH_ROWS = 500
28
32
 
29
33
  let validationWarningLogged = false
30
34
  let runtimeValidationAvailable: boolean | null = null
31
35
 
36
+ // Module-level registry of in-flight access-log writes. Both `log` and
37
+ // `logMany` opt every promise they kick off into this set so that
38
+ // `flushAccessLog()` can drain them. This is what makes the new
39
+ // fire-and-forget CRUD path safe for test code that asserts on `access_logs`
40
+ // rows immediately after a response — the integration harness defaults to
41
+ // blocking via `OM_CRUD_ACCESS_LOG_BLOCKING=1`, and direct callers can opt
42
+ // in to draining explicitly via `flushAccessLog()`.
43
+ const pendingAccessLogWrites = new Set<Promise<unknown>>()
44
+
45
+ function trackPendingAccessLogWrite<T>(promise: Promise<T>): Promise<T> {
46
+ pendingAccessLogWrites.add(promise as unknown as Promise<unknown>)
47
+ promise
48
+ .catch(() => undefined)
49
+ .finally(() => {
50
+ pendingAccessLogWrites.delete(promise as unknown as Promise<unknown>)
51
+ })
52
+ return promise
53
+ }
54
+
55
+ export async function flushAccessLog(): Promise<void> {
56
+ while (pendingAccessLogWrites.size > 0) {
57
+ const snapshot = Array.from(pendingAccessLogWrites)
58
+ await Promise.allSettled(snapshot)
59
+ }
60
+ }
61
+
32
62
  const isZodRuntimeMissing = (err: unknown) => err instanceof TypeError && typeof err.message === 'string' && err.message.includes('_zod')
33
63
 
64
+ type RawEncryptedFields = {
65
+ resourceKind?: unknown
66
+ resourceId?: unknown
67
+ accessType?: unknown
68
+ fieldsJson?: unknown
69
+ contextJson?: unknown
70
+ }
71
+
72
+ function serializeJsonColumn(value: unknown): string | null {
73
+ if (value === null || value === undefined) return null
74
+ if (typeof value === 'string') {
75
+ try {
76
+ JSON.parse(value)
77
+ return value
78
+ } catch {
79
+ return JSON.stringify(value)
80
+ }
81
+ }
82
+ try {
83
+ return JSON.stringify(value)
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
34
89
  export class AccessLogService {
35
90
  constructor(private readonly em: EntityManager) {}
36
91
 
37
92
  async log(input: AccessLogCreateInput): Promise<AccessLog | null> {
38
- let data: AccessLogCreateInput
93
+ const promise = this.logInternal(input)
94
+ return trackPendingAccessLogWrite(promise)
95
+ }
96
+
97
+ async logMany(inputs: AccessLogCreateInput[]): Promise<number> {
98
+ if (!Array.isArray(inputs) || inputs.length === 0) return 0
99
+ const promise = this.logManyInternal(inputs)
100
+ return trackPendingAccessLogWrite(promise)
101
+ }
102
+
103
+ flush(): Promise<void> {
104
+ return flushAccessLog()
105
+ }
106
+
107
+ private async logManyInternal(inputs: AccessLogCreateInput[]): Promise<number> {
108
+ // Parsing in parallel matches the legacy fan-out `Promise.all(map(...service.log()))`
109
+ // path's wall-clock; the previous sequential loop made batched writes slower than
110
+ // un-batched on tenants with encryption enabled and pushed UI integration tests
111
+ // over their dialog-stability budget.
112
+ const parsedResults = await Promise.all(inputs.map((input) => this.parseInput(input)))
113
+ const normalized: AccessLogCreateInput[] = []
114
+ for (const parsed of parsedResults) {
115
+ if (parsed) normalized.push(parsed)
116
+ }
117
+ if (!normalized.length) return 0
118
+
119
+ let written = 0
120
+ for (let offset = 0; offset < normalized.length; offset += MAX_BATCH_ROWS) {
121
+ const chunk = normalized.slice(offset, offset + MAX_BATCH_ROWS)
122
+ written += await this.writeChunk(chunk)
123
+ }
124
+ if (written > 0) {
125
+ const fork = this.em.fork({ useContext: true })
126
+ await this.rotate(fork)
127
+ }
128
+ return written
129
+ }
130
+
131
+ private async writeChunk(chunk: AccessLogCreateInput[]): Promise<number> {
132
+ if (!chunk.length) return 0
133
+ const fork = this.em.fork({ useContext: true })
134
+ const encryption = resolveTenantEncryptionService(fork as any)
135
+ const createdAt = new Date()
136
+
137
+ // Encrypt every row in parallel so encryption-enabled tenants do not pay
138
+ // the N-rows × per-row latency penalty that the previous sequential
139
+ // for-of loop introduced. The legacy `service.log()` fan-out resolved
140
+ // its 50 encryption calls concurrently via `Promise.all`; preserve that
141
+ // characteristic here so the batched single-INSERT path is strictly
142
+ // faster than the legacy parallel-INSERTs path.
143
+ type PreparedRow = {
144
+ tenantId: string | null
145
+ organizationId: string | null
146
+ data: AccessLogCreateInput
147
+ fields: unknown[] | null
148
+ context: Record<string, unknown> | null
149
+ encrypted: RawEncryptedFields | null
150
+ }
151
+ const prepared: PreparedRow[] = await Promise.all(
152
+ chunk.map(async (data) => {
153
+ const fields = Array.isArray(data.fields) && data.fields.length ? data.fields : null
154
+ const context = data.context && Object.keys(data.context).length ? data.context : null
155
+ const tenantId = data.tenantId ?? null
156
+ const organizationId = data.organizationId ?? null
157
+ const encrypted = encryption
158
+ ? ((await encryption.encryptEntityPayload(
159
+ E.audit_logs.access_log,
160
+ {
161
+ resourceKind: data.resourceKind,
162
+ resourceId: data.resourceId,
163
+ accessType: data.accessType,
164
+ fieldsJson: fields,
165
+ contextJson: context,
166
+ },
167
+ tenantId,
168
+ organizationId,
169
+ )) as RawEncryptedFields)
170
+ : null
171
+ return { tenantId, organizationId, data, fields, context, encrypted }
172
+ }),
173
+ )
174
+
175
+ const placeholders: string[] = []
176
+ const params: unknown[] = []
177
+ for (const row of prepared) {
178
+ const { tenantId, organizationId, data, fields, context, encrypted } = row
179
+ const resourceKindOut = encrypted?.resourceKind ?? data.resourceKind
180
+ const resourceIdOut = encrypted?.resourceId ?? data.resourceId
181
+ const accessTypeOut = encrypted?.accessType ?? data.accessType
182
+ const fieldsOut = encrypted?.fieldsJson ?? fields
183
+ const contextOut = encrypted?.contextJson ?? context
184
+ placeholders.push('(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
185
+ params.push(
186
+ tenantId,
187
+ organizationId,
188
+ data.actorUserId ?? null,
189
+ resourceKindOut,
190
+ resourceIdOut,
191
+ accessTypeOut,
192
+ serializeJsonColumn(fieldsOut),
193
+ serializeJsonColumn(contextOut),
194
+ createdAt,
195
+ null,
196
+ )
197
+ }
198
+ if (!placeholders.length) return 0
199
+ const sql = `insert into "access_logs" ("tenant_id", "organization_id", "actor_user_id", "resource_kind", "resource_id", "access_type", "fields_json", "context_json", "created_at", "deleted_at") values ${placeholders.join(', ')}`
200
+ await fork.getConnection().execute(sql, params)
201
+ return chunk.length
202
+ }
203
+
204
+ private async parseInput(input: AccessLogCreateInput): Promise<AccessLogCreateInput | null> {
39
205
  const schema = accessLogCreateSchema as typeof accessLogCreateSchema & { _zod?: unknown }
40
206
  const canValidate = Boolean(schema && typeof schema.parse === 'function')
41
207
  const shouldValidate = canValidate && runtimeValidationAvailable !== false
42
208
  if (shouldValidate) {
43
209
  try {
44
- data = schema.parse(input)
210
+ const data = schema.parse(input)
45
211
  runtimeValidationAvailable = true
212
+ return data
46
213
  } catch (err) {
47
214
  if (!isZodRuntimeMissing(err) && !validationWarningLogged) {
48
215
  validationWarningLogged = true
@@ -50,11 +217,15 @@ export class AccessLogService {
50
217
  console.warn('[audit_logs] falling back to permissive access log payload parser', err)
51
218
  }
52
219
  if (isZodRuntimeMissing(err)) runtimeValidationAvailable = false
53
- data = this.normalizeInput(input)
220
+ return this.normalizeInput(input)
54
221
  }
55
- } else {
56
- data = this.normalizeInput(input)
57
222
  }
223
+ return this.normalizeInput(input)
224
+ }
225
+
226
+ private async logInternal(input: AccessLogCreateInput): Promise<AccessLog | null> {
227
+ const data = await this.parseInput(input)
228
+ if (!data) return null
58
229
  const fork = this.em.fork({ useContext: true })
59
230
  const fields = Array.isArray(data.fields) && data.fields.length ? data.fields : null
60
231
  const context = data.context && Object.keys(data.context).length ? data.context : null
@@ -62,13 +233,6 @@ export class AccessLogService {
62
233
  const tenantId = data.tenantId ?? null
63
234
  const organizationId = data.organizationId ?? null
64
235
 
65
- type AccessLogEncryptedFields = {
66
- resourceKind?: unknown
67
- resourceId?: unknown
68
- accessType?: unknown
69
- fieldsJson?: unknown
70
- contextJson?: unknown
71
- }
72
236
  const encryption = resolveTenantEncryptionService(fork as any)
73
237
  const encrypted = encryption
74
238
  ? ((await encryption.encryptEntityPayload(
@@ -82,7 +246,7 @@ export class AccessLogService {
82
246
  },
83
247
  tenantId,
84
248
  organizationId,
85
- )) as AccessLogEncryptedFields)
249
+ )) as RawEncryptedFields)
86
250
  : null
87
251
 
88
252
  const payload = {
@@ -102,8 +266,8 @@ export class AccessLogService {
102
266
  payload.resourceKind,
103
267
  payload.resourceId,
104
268
  payload.accessType,
105
- payload.fieldsJson !== null && payload.fieldsJson !== undefined ? JSON.stringify(payload.fieldsJson) : null,
106
- payload.contextJson !== null && payload.contextJson !== undefined ? JSON.stringify(payload.contextJson) : null,
269
+ serializeJsonColumn(payload.fieldsJson),
270
+ serializeJsonColumn(payload.contextJson),
107
271
  createdAt,
108
272
  null,
109
273
  ],
@@ -55,7 +55,7 @@ export default function AuthProfilePage() {
55
55
  setError(null)
56
56
  try {
57
57
  const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')
58
- if (!ok) throw new Error('load_failed')
58
+ if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))
59
59
  const resolvedEmail = typeof result?.email === 'string' ? result.email : ''
60
60
  if (!cancelled) setEmail(resolvedEmail)
61
61
  } catch (err) {
@@ -54,7 +54,7 @@ export default function ProfileChangePasswordPage() {
54
54
  setError(null)
55
55
  try {
56
56
  const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')
57
- if (!ok) throw new Error('load_failed')
57
+ if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))
58
58
  const resolvedEmail = typeof result?.email === 'string' ? result.email : ''
59
59
  if (!cancelled) setEmail(resolvedEmail)
60
60
  } catch (err) {
@@ -176,7 +176,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
176
176
  const { ok, result } = await apiCall<UserListResponse>(
177
177
  `/api/auth/users?id=${encodeURIComponent(String(id))}&page=1&pageSize=1`,
178
178
  )
179
- if (!ok) throw new Error('load_failed')
179
+ if (!ok) throw new Error(tRef.current('auth.users.form.errors.load', 'Failed to load user data'))
180
180
  const item = Array.isArray(result?.items) ? result?.items?.[0] : undefined
181
181
  if (!cancelled) {
182
182
  setActorIsSuperAdmin(Boolean(result?.isSuperAdmin))
@@ -109,7 +109,12 @@ export default function CreateUserPage() {
109
109
  setWidgetError(null)
110
110
  try {
111
111
  const { ok, result } = await apiCall<WidgetCatalogResponse>('/api/dashboards/widgets/catalog')
112
- if (!ok) throw new Error('request_failed')
112
+ if (!ok) {
113
+ throw new Error(t(
114
+ 'auth.users.widgets.errors.load',
115
+ 'Unable to load dashboard widgets. You can configure them later from the user page.',
116
+ ))
117
+ }
113
118
  if (!cancelled) {
114
119
  const rawItems: unknown[] = Array.isArray(result?.items) ? result?.items ?? [] : []
115
120
  const normalized = rawItems
@@ -1,11 +1,34 @@
1
- import { asClass } from 'awilix'
1
+ import { asClass, asFunction } from 'awilix'
2
2
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import type { CacheStrategy } from '@open-mercato/cache'
3
5
  import { AuthService } from '@open-mercato/core/modules/auth/services/authService'
4
6
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
+ import {
8
+ createRbacFallbackCache,
9
+ isRbacDefaultCacheEnabled,
10
+ resetRbacFallbackCache,
11
+ } from '@open-mercato/core/modules/auth/services/rbacDefaultCache'
12
+
13
+ export { resetRbacFallbackCache }
5
14
 
6
15
  export function register(container: AppContainer) {
7
16
  // Register or override core auth service
8
17
  container.register({ authService: asClass(AuthService).scoped() })
9
- // RBAC service
10
- container.register({ rbacService: asClass(RbacService).scoped() })
18
+ // RBAC service. The bare `asClass(...).scoped()` registration matches
19
+ // develop and is the default. Setting `OM_RBAC_DEFAULT_CACHE=on` opts
20
+ // into the in-process LRU fallback for deployments that don't wire a
21
+ // shared CacheStrategy (CLI scripts, lean test bootstraps). Production
22
+ // bootstraps that already register `cache` via `@open-mercato/cache`
23
+ // preempt this fallback because the container's existing `cache`
24
+ // registration wins when `RbacService` reaches for it.
25
+ if (isRbacDefaultCacheEnabled()) {
26
+ container.register({
27
+ rbacService: asFunction((cradle: { em: EntityManager; cache?: CacheStrategy }) => {
28
+ return new RbacService(cradle.em, cradle.cache ?? createRbacFallbackCache())
29
+ }).scoped(),
30
+ })
31
+ } else {
32
+ container.register({ rbacService: asClass(RbacService).scoped() })
33
+ }
11
34
  }