@open-mercato/core 0.6.3-develop.3562.1.71049275ff → 0.6.3-develop.3574.1.f3c5dcf345

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 (35) hide show
  1. package/dist/helpers/integration/queue-runner.js +15 -3
  2. package/dist/helpers/integration/queue-runner.js.map +2 -2
  3. package/dist/helpers/integration/queue.js +1 -1
  4. package/dist/helpers/integration/queue.js.map +2 -2
  5. package/dist/modules/auth/api/roles/acl/route.js +37 -1
  6. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  7. package/dist/modules/auth/api/roles/route.js +36 -2
  8. package/dist/modules/auth/api/roles/route.js.map +2 -2
  9. package/dist/modules/auth/api/users/acl/route.js +36 -1
  10. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +40 -2
  12. package/dist/modules/auth/api/users/route.js.map +2 -2
  13. package/dist/modules/auth/commands/users.js +3 -0
  14. package/dist/modules/auth/commands/users.js.map +2 -2
  15. package/dist/modules/auth/lib/grantChecks.js +97 -2
  16. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  17. package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
  18. package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
  19. package/dist/modules/translations/components/TranslationManager.js +9 -4
  20. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  21. package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
  22. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  23. package/jest.config.cjs +1 -0
  24. package/package.json +7 -7
  25. package/src/helpers/integration/queue-runner.ts +21 -2
  26. package/src/helpers/integration/queue.ts +1 -1
  27. package/src/modules/auth/api/roles/acl/route.ts +39 -1
  28. package/src/modules/auth/api/roles/route.ts +38 -2
  29. package/src/modules/auth/api/users/acl/route.ts +41 -1
  30. package/src/modules/auth/api/users/route.ts +41 -2
  31. package/src/modules/auth/commands/users.ts +3 -0
  32. package/src/modules/auth/lib/grantChecks.ts +116 -2
  33. package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
  34. package/src/modules/translations/components/TranslationManager.tsx +9 -4
  35. package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
@@ -1,4 +1,6 @@
1
1
  import { matchEventPattern } from "@open-mercato/shared/lib/events/patterns";
2
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { testLinearRegex } from "@open-mercato/shared/lib/regex/linear";
2
4
  import {
3
5
  WorkflowEventTrigger,
4
6
  WorkflowDefinition,
@@ -140,11 +142,12 @@ function evaluateCondition(condition, payload) {
140
142
  if (typeof value !== "string" || typeof expected !== "string") return false;
141
143
  if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false;
142
144
  if (!isSafeWorkflowRegexPattern(expected)) return false;
143
- try {
144
- const regex = new RegExp(expected);
145
- return regex.test(value);
146
- } catch {
147
- return false;
145
+ {
146
+ const result = testLinearRegex(expected, value, {
147
+ maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,
148
+ maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH
149
+ });
150
+ return result.ok ? result.matched : false;
148
151
  }
149
152
  default:
150
153
  return false;
@@ -168,7 +171,9 @@ function getCacheKey(tenantId, organizationId) {
168
171
  return `${tenantId}:${organizationId}`;
169
172
  }
170
173
  async function loadLegacyTriggers(em, tenantId, organizationId) {
171
- const legacyTriggers = await em.find(
174
+ const postgresEm = em;
175
+ const legacyTriggers = await findWithDecryption(
176
+ postgresEm,
172
177
  WorkflowEventTrigger,
173
178
  {
174
179
  tenantId,
@@ -178,14 +183,17 @@ async function loadLegacyTriggers(em, tenantId, organizationId) {
178
183
  },
179
184
  {
180
185
  orderBy: { priority: "DESC", createdAt: "ASC" }
181
- }
186
+ },
187
+ { tenantId, organizationId }
182
188
  );
183
189
  const definitionIds = [...new Set(legacyTriggers.map((t) => t.workflowDefinitionId))];
184
- const definitions = definitionIds.length > 0 ? await em.find(WorkflowDefinition, {
190
+ const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {
185
191
  id: { $in: definitionIds },
192
+ tenantId,
193
+ organizationId,
186
194
  enabled: true,
187
195
  deletedAt: null
188
- }) : [];
196
+ }, {}, { tenantId, organizationId }) : [];
189
197
  const definitionMap = new Map(definitions.map((d) => [d.id, d]));
190
198
  return legacyTriggers.filter((t) => definitionMap.has(t.workflowDefinitionId)).map((t) => {
191
199
  const def = definitionMap.get(t.workflowDefinitionId);
@@ -208,14 +216,18 @@ async function loadLegacyTriggers(em, tenantId, organizationId) {
208
216
  });
209
217
  }
210
218
  async function loadEmbeddedTriggers(em, tenantId, organizationId) {
211
- const definitions = await em.find(
219
+ const postgresEm = em;
220
+ const definitions = await findWithDecryption(
221
+ postgresEm,
212
222
  WorkflowDefinition,
213
223
  {
214
224
  tenantId,
215
225
  organizationId,
216
226
  enabled: true,
217
227
  deletedAt: null
218
- }
228
+ },
229
+ {},
230
+ { tenantId, organizationId }
219
231
  );
220
232
  const triggers = [];
221
233
  for (const def of definitions) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/workflows/lib/event-trigger-service.ts"],
4
- "sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n try {\n const regex = new RegExp(expected)\n return regex.test(value)\n } catch {\n return false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org)\nconst triggerCache = new Map<string, CachedTriggers>()\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const legacyTriggers = await em.find(\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n }\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await em.find(WorkflowDefinition, {\n id: { $in: definitionIds },\n enabled: true,\n deletedAt: null,\n }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n // Load all enabled definitions that may have triggers\n const definitions = await em.find(\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n\n // Check in-memory cache\n const cached = triggerCache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n triggerCache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n triggerCache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of triggerCache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n triggerCache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Workflows Module - Event Trigger Service\n *\n * Core service for evaluating and processing workflow event triggers.\n * Handles pattern matching, filter evaluation, context mapping, and workflow starting.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport type { CacheService } from '@open-mercato/cache'\nimport { matchEventPattern } from '@open-mercato/shared/lib/events/patterns'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'\nimport {\n WorkflowEventTrigger,\n WorkflowDefinition,\n WorkflowInstance,\n type TriggerFilterCondition,\n type TriggerContextMapping,\n type WorkflowEventTriggerConfig,\n type WorkflowDefinitionTrigger,\n} from '../data/entities'\nimport { startWorkflow, executeWorkflow } from './workflow-executor'\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EventTriggerContext {\n eventName: string\n payload: Record<string, unknown>\n tenantId: string\n organizationId: string\n}\n\nexport interface ProcessTriggersResult {\n triggered: number\n skipped: number\n errors: Array<{ triggerId: string; error: string }>\n instances: Array<{ triggerId: string; instanceId: string }>\n}\n\n/**\n * Unified trigger interface for both legacy (entity) and embedded (definition) triggers\n */\nexport interface UnifiedTrigger {\n id: string // For legacy: entity ID, for embedded: `${definitionId}:${triggerId}`\n triggerId: string\n name: string\n description?: string | null\n eventPattern: string\n config: WorkflowEventTriggerConfig | null\n enabled: boolean\n priority: number\n workflowDefinitionId: string\n workflowId: string\n workflowVersion: number\n source: 'legacy' | 'embedded'\n tenantId: string\n organizationId: string\n}\n\ninterface CachedTriggers {\n triggers: UnifiedTrigger[]\n cachedAt: number\n}\n\n// Cache TTL: 5 minutes\nconst TRIGGER_CACHE_TTL = 5 * 60 * 1000\n\n// ============================================================================\n// Pattern Matching\n// ============================================================================\n\n// ============================================================================\n// Filter Evaluation\n// ============================================================================\n\nconst MAX_WORKFLOW_REGEX_PATTERN_LENGTH = 200\nconst MAX_WORKFLOW_REGEX_INPUT_LENGTH = 10_000\n\n/**\n * Get a nested value from an object using dot notation.\n */\nfunction getNestedValue(obj: unknown, path: string): unknown {\n if (obj === null || obj === undefined) return undefined\n\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) return undefined\n if (typeof current !== 'object') return undefined\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\nfunction getQuantifierEnd(pattern: string, index: number): number | null {\n const char = pattern[index]\n if (char === '*' || char === '+' || char === '?') return index\n if (char !== '{') return null\n\n const closeIndex = pattern.indexOf('}', index + 1)\n if (closeIndex === -1) return null\n\n const body = pattern.slice(index + 1, closeIndex)\n return /^[0-9]+(?:,[0-9]*)?$/.test(body) ? closeIndex : null\n}\n\ninterface RegexGroupFrame {\n hasAlternation: boolean\n hasQuantifier: boolean\n}\n\nfunction isSafeWorkflowRegexPattern(pattern: string): boolean {\n if (pattern.length > MAX_WORKFLOW_REGEX_PATTERN_LENGTH) return false\n\n const groupStack: RegexGroupFrame[] = [{ hasAlternation: false, hasQuantifier: false }]\n let inCharClass = false\n let lastClosedGroup: RegexGroupFrame | null = null\n let lastAtomWasQuantified = false\n\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index]\n\n if (char === '\\\\') {\n const next = pattern[index + 1]\n if (!inCharClass && (/[1-9]/.test(next ?? '') || (next === 'k' && pattern[index + 2] === '<'))) {\n return false\n }\n index += 1\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (inCharClass) {\n if (char === ']') {\n inCharClass = false\n lastAtomWasQuantified = false\n }\n lastClosedGroup = null\n continue\n }\n\n if (char === '[') {\n inCharClass = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '(') {\n if (pattern[index + 1] === '?') {\n if (pattern[index + 2] !== ':') return false\n index += 2\n }\n\n groupStack.push({ hasAlternation: false, hasQuantifier: false })\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === ')') {\n if (groupStack.length === 1) return false\n lastClosedGroup = groupStack.pop()!\n lastAtomWasQuantified = false\n continue\n }\n\n if (char === '|') {\n groupStack[groupStack.length - 1].hasAlternation = true\n lastClosedGroup = null\n lastAtomWasQuantified = false\n continue\n }\n\n const quantifierEnd = getQuantifierEnd(pattern, index)\n if (quantifierEnd !== null) {\n if (lastAtomWasQuantified) return false\n if (lastClosedGroup?.hasAlternation || lastClosedGroup?.hasQuantifier) return false\n\n groupStack[groupStack.length - 1].hasQuantifier = true\n lastClosedGroup = null\n lastAtomWasQuantified = true\n index = quantifierEnd\n\n if (pattern[index + 1] === '?') {\n index += 1\n }\n\n continue\n }\n\n lastClosedGroup = null\n lastAtomWasQuantified = false\n }\n\n return groupStack.length === 1 && !inCharClass\n}\n\n/**\n * Evaluate a single filter condition against the event payload.\n */\nfunction evaluateCondition(condition: TriggerFilterCondition, payload: Record<string, unknown>): boolean {\n const value = getNestedValue(payload, condition.field)\n const expected = condition.value\n\n switch (condition.operator) {\n case 'eq':\n return value === expected\n\n case 'neq':\n return value !== expected\n\n case 'gt':\n return typeof value === 'number' && typeof expected === 'number' && value > expected\n\n case 'gte':\n return typeof value === 'number' && typeof expected === 'number' && value >= expected\n\n case 'lt':\n return typeof value === 'number' && typeof expected === 'number' && value < expected\n\n case 'lte':\n return typeof value === 'number' && typeof expected === 'number' && value <= expected\n\n case 'contains':\n if (typeof value === 'string' && typeof expected === 'string') {\n return value.includes(expected)\n }\n if (Array.isArray(value)) {\n return value.includes(expected)\n }\n return false\n\n case 'startsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.startsWith(expected)\n\n case 'endsWith':\n return typeof value === 'string' && typeof expected === 'string' && value.endsWith(expected)\n\n case 'in':\n return Array.isArray(expected) && expected.includes(value)\n\n case 'notIn':\n return Array.isArray(expected) && !expected.includes(value)\n\n case 'exists':\n return value !== undefined && value !== null\n\n case 'notExists':\n return value === undefined || value === null\n\n case 'regex':\n if (typeof value !== 'string' || typeof expected !== 'string') return false\n if (value.length > MAX_WORKFLOW_REGEX_INPUT_LENGTH) return false\n if (!isSafeWorkflowRegexPattern(expected)) return false\n {\n const result = testLinearRegex(expected, value, {\n maxPatternLength: MAX_WORKFLOW_REGEX_PATTERN_LENGTH,\n maxInputLength: MAX_WORKFLOW_REGEX_INPUT_LENGTH,\n })\n return result.ok ? result.matched : false\n }\n\n default:\n return false\n }\n}\n\n/**\n * Evaluate all filter conditions against the event payload.\n * All conditions must pass (AND logic).\n */\nexport function evaluateFilterConditions(\n conditions: TriggerFilterCondition[] | undefined,\n payload: Record<string, unknown>\n): boolean {\n if (!conditions || conditions.length === 0) return true\n\n return conditions.every(condition => evaluateCondition(condition, payload))\n}\n\n// ============================================================================\n// Context Mapping\n// ============================================================================\n\n/**\n * Map event payload to workflow initial context.\n */\nexport function mapEventToContext(\n mapping: TriggerContextMapping[] | undefined,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n const context: Record<string, unknown> = {}\n\n if (!mapping || mapping.length === 0) return context\n\n for (const item of mapping) {\n const value = getNestedValue(payload, item.sourceExpression)\n context[item.targetKey] = value !== undefined ? value : item.defaultValue\n }\n\n return context\n}\n\n// ============================================================================\n// Trigger Loading with Caching\n// ============================================================================\n\n// In-memory cache for triggers (per tenant/org)\nconst triggerCache = new Map<string, CachedTriggers>()\n\nfunction getCacheKey(tenantId: string, organizationId: string): string {\n return `${tenantId}:${organizationId}`\n}\n\n/**\n * Load legacy triggers from WorkflowEventTrigger entity table.\n * For backward compatibility with existing triggers.\n */\nasync function loadLegacyTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n const legacyTriggers = await findWithDecryption(\n postgresEm,\n WorkflowEventTrigger,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {\n orderBy: { priority: 'DESC', createdAt: 'ASC' },\n },\n { tenantId, organizationId },\n )\n\n // Get definitions for these triggers to get workflowId\n const definitionIds = [...new Set(legacyTriggers.map(t => t.workflowDefinitionId))]\n const definitions = definitionIds.length > 0 ? await findWithDecryption(postgresEm, WorkflowDefinition, {\n id: { $in: definitionIds },\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }, {}, { tenantId, organizationId }) : []\n const definitionMap = new Map(definitions.map(d => [d.id, d]))\n\n return legacyTriggers\n .filter(t => definitionMap.has(t.workflowDefinitionId))\n .map(t => {\n const def = definitionMap.get(t.workflowDefinitionId)!\n return {\n id: t.id,\n triggerId: t.id,\n name: t.name,\n description: t.description,\n eventPattern: t.eventPattern,\n config: t.config ?? null,\n enabled: t.enabled,\n priority: t.priority,\n workflowDefinitionId: t.workflowDefinitionId,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'legacy' as const,\n tenantId: t.tenantId,\n organizationId: t.organizationId,\n }\n })\n}\n\n/**\n * Load embedded triggers from workflow definitions.\n * New triggers are embedded directly in the definition JSONB.\n */\nasync function loadEmbeddedTriggers(\n em: EntityManager,\n tenantId: string,\n organizationId: string\n): Promise<UnifiedTrigger[]> {\n const postgresEm = em as unknown as PostgreSqlEntityManager\n // Load all enabled definitions that may have triggers\n const definitions = await findWithDecryption(\n postgresEm,\n WorkflowDefinition,\n {\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n },\n {},\n { tenantId, organizationId },\n )\n\n const triggers: UnifiedTrigger[] = []\n\n for (const def of definitions) {\n const embeddedTriggers = def.definition?.triggers as WorkflowDefinitionTrigger[] | undefined\n if (!embeddedTriggers || embeddedTriggers.length === 0) continue\n\n for (const trigger of embeddedTriggers) {\n if (!trigger.enabled) continue\n\n triggers.push({\n id: `${def.id}:${trigger.triggerId}`,\n triggerId: trigger.triggerId,\n name: trigger.name,\n description: trigger.description ?? null,\n eventPattern: trigger.eventPattern,\n config: trigger.config ?? null,\n enabled: trigger.enabled,\n priority: trigger.priority,\n workflowDefinitionId: def.id,\n workflowId: def.workflowId,\n workflowVersion: def.version,\n source: 'embedded' as const,\n tenantId,\n organizationId,\n })\n }\n }\n\n return triggers\n}\n\n/**\n * Load all enabled triggers for a tenant/organization with caching.\n * Merges both legacy (entity) triggers and embedded (definition) triggers.\n */\nexport async function loadTriggersForTenant(\n em: EntityManager,\n tenantId: string,\n organizationId: string,\n cacheService?: CacheService\n): Promise<UnifiedTrigger[]> {\n const cacheKey = getCacheKey(tenantId, organizationId)\n\n // Check in-memory cache\n const cached = triggerCache.get(cacheKey)\n if (cached && Date.now() - cached.cachedAt < TRIGGER_CACHE_TTL) {\n return cached.triggers\n }\n\n // Load from both sources\n const [legacyTriggers, embeddedTriggers] = await Promise.all([\n loadLegacyTriggers(em, tenantId, organizationId),\n loadEmbeddedTriggers(em, tenantId, organizationId),\n ])\n\n // Merge and sort by priority (higher first)\n const allTriggers = [...legacyTriggers, ...embeddedTriggers]\n .sort((a, b) => b.priority - a.priority)\n\n // Update cache\n triggerCache.set(cacheKey, {\n triggers: allTriggers,\n cachedAt: Date.now(),\n })\n\n return allTriggers\n}\n\n/**\n * Invalidate trigger cache for a tenant/organization.\n * Call this when:\n * - Legacy triggers are created/updated/deleted\n * - Workflow definitions with embedded triggers are created/updated/deleted\n */\nexport function invalidateTriggerCache(tenantId: string, organizationId?: string): void {\n if (organizationId) {\n // Invalidate specific org\n const cacheKey = getCacheKey(tenantId, organizationId)\n triggerCache.delete(cacheKey)\n } else {\n // Invalidate all orgs for tenant\n for (const key of triggerCache.keys()) {\n if (key.startsWith(`${tenantId}:`)) {\n triggerCache.delete(key)\n }\n }\n }\n}\n\n// ============================================================================\n// Trigger Matching\n// ============================================================================\n\n/**\n * Find all triggers that match an event.\n */\nexport async function findMatchingTriggers(\n em: EntityManager,\n context: EventTriggerContext\n): Promise<UnifiedTrigger[]> {\n const triggers = await loadTriggersForTenant(\n em,\n context.tenantId,\n context.organizationId\n )\n\n return triggers.filter(trigger => {\n // Check event pattern\n if (!matchEventPattern(context.eventName, trigger.eventPattern)) {\n return false\n }\n\n // Check filter conditions\n if (!evaluateFilterConditions(trigger.config?.filterConditions, context.payload)) {\n return false\n }\n\n return true\n })\n}\n\n// ============================================================================\n// Trigger Processing\n// ============================================================================\n\n/**\n * Check if max concurrent instances limit is reached.\n */\nasync function checkConcurrencyLimit(\n em: EntityManager,\n trigger: UnifiedTrigger\n): Promise<boolean> {\n const maxInstances = trigger.config?.maxConcurrentInstances\n\n if (!maxInstances) return true // No limit\n\n // Count running instances for this trigger's workflow definition\n const runningCount = await em.count(WorkflowInstance, {\n definitionId: trigger.workflowDefinitionId,\n status: { $in: ['RUNNING', 'WAITING_FOR_ACTIVITIES'] },\n tenantId: trigger.tenantId,\n organizationId: trigger.organizationId,\n deletedAt: null,\n })\n\n return runningCount < maxInstances\n}\n\n/**\n * Process all matching triggers for an event and start workflows.\n */\nexport async function processEventTriggers(\n em: EntityManager,\n container: AwilixContainer,\n context: EventTriggerContext\n): Promise<ProcessTriggersResult> {\n const result: ProcessTriggersResult = {\n triggered: 0,\n skipped: 0,\n errors: [],\n instances: [],\n }\n\n // Find matching triggers\n const triggers = await findMatchingTriggers(em, context)\n\n if (triggers.length === 0) {\n return result\n }\n\n // Process each trigger (definitions already validated during loading)\n for (const trigger of triggers) {\n try {\n // Check concurrency limit\n const canStart = await checkConcurrencyLimit(em, trigger)\n if (!canStart) {\n console.log(`[workflow-trigger] Skipping trigger \"${trigger.name}\": max concurrent instances reached`)\n result.skipped++\n continue\n }\n\n // Map event payload to workflow context\n const mappedContext = mapEventToContext(trigger.config?.contextMapping, context.payload)\n\n // Extract entity info from payload for metadata\n const payloadId = context.payload?.id as string | undefined\n const payloadEntityType = context.payload?.entityType as string | undefined\n\n // Include event metadata and payload in context\n const initialContext = {\n // Include raw event payload fields (e.g., id, organizationId, tenantId)\n ...context.payload,\n // Override with explicit mappings if provided\n ...mappedContext,\n __trigger: {\n triggerId: trigger.id,\n triggerName: trigger.name,\n eventName: context.eventName,\n eventPayload: context.payload,\n triggeredAt: new Date().toISOString(),\n source: trigger.source,\n },\n }\n\n // Start workflow\n const instance = await startWorkflow(em, {\n workflowId: trigger.workflowId,\n version: trigger.workflowVersion,\n initialContext,\n metadata: {\n initiatedBy: `trigger:${trigger.id}`,\n // Include entityId and entityType for widget discovery\n entityId: payloadId,\n entityType: payloadEntityType || trigger.config?.entityType,\n labels: {\n trigger_id: trigger.id,\n trigger_name: trigger.name,\n event_name: context.eventName,\n trigger_source: trigger.source,\n },\n },\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n })\n\n result.triggered++\n result.instances.push({\n triggerId: trigger.id,\n instanceId: instance.id,\n })\n\n // Execute workflow asynchronously (don't wait)\n executeWorkflow(em.fork(), container, instance.id).catch(err => {\n console.error(`[workflow-trigger] Error executing workflow ${instance.id}:`, err)\n })\n\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n console.error(`[workflow-trigger] Error processing trigger \"${trigger.name}\":`, error)\n result.errors.push({\n triggerId: trigger.id,\n error: errorMessage,\n })\n }\n }\n\n return result\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/jest.config.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  /** @type {import('jest').Config} */
2
2
  module.exports = {
3
3
  testEnvironment: 'node',
4
+ testTimeout: 30000,
4
5
  watchman: false,
5
6
  rootDir: '.',
6
7
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.3-develop.3562.1.71049275ff",
3
+ "version": "0.6.3-develop.3574.1.f3c5dcf345",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -242,16 +242,16 @@
242
242
  "zod": "^4.4.3"
243
243
  },
244
244
  "peerDependencies": {
245
- "@open-mercato/ai-assistant": "0.6.3-develop.3562.1.71049275ff",
246
- "@open-mercato/shared": "0.6.3-develop.3562.1.71049275ff",
247
- "@open-mercato/ui": "0.6.3-develop.3562.1.71049275ff",
245
+ "@open-mercato/ai-assistant": "0.6.3-develop.3574.1.f3c5dcf345",
246
+ "@open-mercato/shared": "0.6.3-develop.3574.1.f3c5dcf345",
247
+ "@open-mercato/ui": "0.6.3-develop.3574.1.f3c5dcf345",
248
248
  "react": "^19.0.0",
249
249
  "react-dom": "^19.0.0"
250
250
  },
251
251
  "devDependencies": {
252
- "@open-mercato/ai-assistant": "0.6.3-develop.3562.1.71049275ff",
253
- "@open-mercato/shared": "0.6.3-develop.3562.1.71049275ff",
254
- "@open-mercato/ui": "0.6.3-develop.3562.1.71049275ff",
252
+ "@open-mercato/ai-assistant": "0.6.3-develop.3574.1.f3c5dcf345",
253
+ "@open-mercato/shared": "0.6.3-develop.3574.1.f3c5dcf345",
254
+ "@open-mercato/ui": "0.6.3-develop.3574.1.f3c5dcf345",
255
255
  "@testing-library/dom": "^10.4.1",
256
256
  "@testing-library/jest-dom": "^6.9.1",
257
257
  "@testing-library/react": "^16.3.1",
@@ -7,13 +7,27 @@ import { createQueue } from '@open-mercato/queue'
7
7
  export type QueueDrainRunnerOptions = {
8
8
  appRoot: string
9
9
  jobLimit: number
10
+ queueBaseDir?: string
11
+ }
12
+
13
+ type BootstrapData = Awaited<ReturnType<typeof bootstrapFromAppRoot>>
14
+
15
+ const bootstrapCache = new Map<string, Promise<BootstrapData>>()
16
+
17
+ function getBootstrapData(appRoot: string): Promise<BootstrapData> {
18
+ const resolvedAppRoot = path.resolve(appRoot)
19
+ const cached = bootstrapCache.get(resolvedAppRoot)
20
+ if (cached) return cached
21
+ const promise = bootstrapFromAppRoot(resolvedAppRoot)
22
+ bootstrapCache.set(resolvedAppRoot, promise)
23
+ return promise
10
24
  }
11
25
 
12
26
  export async function drainQueueFromAppRoot(
13
27
  queueName: string,
14
28
  options: QueueDrainRunnerOptions,
15
29
  ): Promise<number> {
16
- const data = await bootstrapFromAppRoot(options.appRoot)
30
+ const data = await getBootstrapData(options.appRoot)
17
31
  const worker = data.modules
18
32
  .flatMap((module) => module.workers ?? [])
19
33
  .find((entry) => entry.queue === queueName)
@@ -21,7 +35,11 @@ export async function drainQueueFromAppRoot(
21
35
 
22
36
  const container = await createRequestContainer()
23
37
  const queue = createQueue(queueName, 'local', {
24
- baseDir: path.resolve(options.appRoot, '.mercato/queue'),
38
+ baseDir: path.resolve(
39
+ options.queueBaseDir?.trim()
40
+ || process.env.QUEUE_BASE_DIR?.trim()
41
+ || path.resolve(options.appRoot, '.mercato/queue'),
42
+ ),
25
43
  concurrency: 1,
26
44
  })
27
45
  const resolve = <T = unknown>(name: string): T => container.resolve(name) as T
@@ -53,6 +71,7 @@ export async function runQueueDrainFromEnv(): Promise<void> {
53
71
  const processed = await drainQueueFromAppRoot(queueName, {
54
72
  appRoot: path.resolve(appRoot),
55
73
  jobLimit,
74
+ queueBaseDir: process.env.QUEUE_BASE_DIR,
56
75
  })
57
76
  console.log(JSON.stringify({ processed }))
58
77
  }
@@ -32,7 +32,7 @@ function drainQueueInAppProcess(queueName: string, options: Required<DrainQueueO
32
32
  OM_INTEGRATION_APP_ROOT: options.appRoot,
33
33
  OM_INTEGRATION_QUEUE_NAME: queueName,
34
34
  OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),
35
- QUEUE_BASE_DIR: path.resolve(options.appRoot, '.mercato/queue'),
35
+ QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),
36
36
  },
37
37
  stdio: ['ignore', 'pipe', 'pipe'],
38
38
  })
@@ -9,7 +9,11 @@ import { RoleAcl, Role } from '@open-mercato/core/modules/auth/data/entities'
9
9
  import type { EntityManager } from '@mikro-orm/postgresql'
10
10
  import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
11
11
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
12
- import { assertActorCanGrantAcl, normalizeGrantFeatureList } from '@open-mercato/core/modules/auth/lib/grantChecks'
12
+ import {
13
+ assertActorCanGrantAcl,
14
+ assertActorCanModifySuperAdminRoleTarget,
15
+ normalizeGrantFeatureList,
16
+ } from '@open-mercato/core/modules/auth/lib/grantChecks'
13
17
 
14
18
  type TaggableCache = { deleteByTags?: (tags: string[]) => Promise<void> | void }
15
19
 
@@ -71,6 +75,23 @@ export async function GET(req: Request) {
71
75
  }
72
76
  if (!tenantScope && !isSuperAdmin) tenantScope = authTenantId ?? null
73
77
 
78
+ if (!isSuperAdmin && auth.sub) {
79
+ try {
80
+ await assertActorCanModifySuperAdminRoleTarget({
81
+ em,
82
+ rbacService: container.resolve('rbacService') as RbacService,
83
+ actorUserId: auth.sub,
84
+ tenantId: tenantScope,
85
+ organizationId: auth.orgId ?? null,
86
+ targetRoleId: parsed.data.roleId,
87
+ actorIsSuperAdmin: false,
88
+ })
89
+ } catch (err) {
90
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
91
+ throw err
92
+ }
93
+ }
94
+
74
95
  const acl = tenantScope
75
96
  ? await em.findOne(RoleAcl, { role, tenantId: tenantScope })
76
97
  : null
@@ -133,6 +154,23 @@ export async function PUT(req: Request) {
133
154
  return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
134
155
  }
135
156
 
157
+ if (!isSuperAdmin && auth.sub) {
158
+ try {
159
+ await assertActorCanModifySuperAdminRoleTarget({
160
+ em,
161
+ rbacService,
162
+ actorUserId: auth.sub,
163
+ tenantId: targetTenantId,
164
+ organizationId: auth.orgId ?? null,
165
+ targetRoleId: parsed.data.roleId,
166
+ actorIsSuperAdmin: false,
167
+ })
168
+ } catch (err) {
169
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
170
+ throw err
171
+ }
172
+ }
173
+
136
174
  let acl = await em.findOne(RoleAcl, { role, tenantId: targetTenantId })
137
175
  if (!acl) {
138
176
  acl = em.create(RoleAcl, {
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
3
3
  import { z } from 'zod'
4
4
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
5
  import { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
6
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
7
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
8
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
9
  import { Role, RoleAcl, UserRole } from '@open-mercato/core/modules/auth/data/entities'
@@ -13,6 +14,8 @@ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
13
14
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
14
15
  import { roleCrudEvents, roleCrudIndexer } from '@open-mercato/core/modules/auth/commands/roles'
15
16
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
17
+ import { assertActorCanModifySuperAdminRoleTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
18
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
16
19
 
17
20
  const querySchema = z.object({
18
21
  id: z.string().uuid().optional(),
@@ -87,7 +90,12 @@ const crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({
87
90
  update: {
88
91
  commandId: 'auth.roles.update',
89
92
  schema: rawBodySchema,
90
- mapInput: ({ parsed }) => parsed,
93
+ mapInput: async ({ parsed, ctx }) => {
94
+ if (ctx.request && typeof parsed.id === 'string' && parsed.id.length) {
95
+ await assertCanModifySuperAdminRole(ctx.request, parsed.id)
96
+ }
97
+ return parsed
98
+ },
91
99
  response: () => ({ ok: true }),
92
100
  },
93
101
  delete: {
@@ -236,7 +244,35 @@ export async function GET(req: Request) {
236
244
 
237
245
  export const POST = crud.POST
238
246
  export const PUT = crud.PUT
239
- export const DELETE = crud.DELETE
247
+ export const DELETE = async (req: Request) => {
248
+ const targetId = new URL(req.url).searchParams.get('id')
249
+ if (targetId) {
250
+ try {
251
+ await assertCanModifySuperAdminRole(req, targetId)
252
+ } catch (err) {
253
+ if (err instanceof CrudHttpError) {
254
+ return NextResponse.json(err.body, { status: err.status })
255
+ }
256
+ throw err
257
+ }
258
+ }
259
+ return crud.DELETE(req)
260
+ }
261
+
262
+ async function assertCanModifySuperAdminRole(req: Request, targetRoleId: string) {
263
+ const auth = await getAuthFromRequest(req)
264
+ if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })
265
+ const container = await createRequestContainer()
266
+ const em = container.resolve('em') as EntityManager
267
+ await assertActorCanModifySuperAdminRoleTarget({
268
+ em,
269
+ rbacService: container.resolve('rbacService') as RbacService,
270
+ actorUserId: auth.sub,
271
+ tenantId: auth.tenantId ?? null,
272
+ organizationId: auth.orgId ?? null,
273
+ targetRoleId,
274
+ })
275
+ }
240
276
 
241
277
  export const openApi: OpenApiRouteDoc = {
242
278
  tag: 'Authentication & Accounts',
@@ -4,8 +4,11 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
4
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
6
6
  import { logCrudAccess } from '@open-mercato/shared/lib/crud/factory'
7
- import { forbidden } from '@open-mercato/shared/lib/crud/errors'
7
+ import { forbidden, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
8
  import { UserAcl } from '@open-mercato/core/modules/auth/data/entities'
9
+ import { assertActorCanModifySuperAdminUserTarget } from '@open-mercato/core/modules/auth/lib/grantChecks'
10
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
11
+ import type { EntityManager } from '@mikro-orm/postgresql'
9
12
 
10
13
  const getSchema = z.object({ userId: z.string().uuid() })
11
14
  const putSchema = z.object({
@@ -42,6 +45,26 @@ export async function GET(req: Request) {
42
45
  if (!parsed.success) return NextResponse.json({ error: 'Invalid input' }, { status: 400 })
43
46
  const container = await createRequestContainer()
44
47
  const em = container.resolve('em') as any
48
+ const rbacService = container.resolve('rbacService') as any
49
+ const actorAcl = auth.sub
50
+ ? await rbacService.loadAcl(auth.sub, { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null })
51
+ : null
52
+ if (!actorAcl?.isSuperAdmin && auth.sub) {
53
+ try {
54
+ await assertActorCanModifySuperAdminUserTarget({
55
+ em: em as EntityManager,
56
+ rbacService: rbacService as RbacService,
57
+ actorUserId: auth.sub,
58
+ tenantId: auth.tenantId ?? null,
59
+ organizationId: auth.orgId ?? null,
60
+ targetUserId: parsed.data.userId,
61
+ actorIsSuperAdmin: false,
62
+ })
63
+ } catch (err) {
64
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
65
+ throw err
66
+ }
67
+ }
45
68
  const acl = await em.findOne(UserAcl, { user: parsed.data.userId as any, tenantId: auth.tenantId as any })
46
69
  const response = acl
47
70
  ? {
@@ -83,6 +106,23 @@ export async function PUT(req: Request) {
83
106
  : null
84
107
  const actorIsSuperAdmin = !!actorAcl?.isSuperAdmin
85
108
 
109
+ if (!actorIsSuperAdmin && auth.sub) {
110
+ try {
111
+ await assertActorCanModifySuperAdminUserTarget({
112
+ em: em as EntityManager,
113
+ rbacService: rbacService as RbacService,
114
+ actorUserId: auth.sub,
115
+ tenantId: auth.tenantId ?? null,
116
+ organizationId: auth.orgId ?? null,
117
+ targetUserId: parsed.data.userId,
118
+ actorIsSuperAdmin: false,
119
+ })
120
+ } catch (err) {
121
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
122
+ throw err
123
+ }
124
+ }
125
+
86
126
  const requestedFeatures = normalizeFeatureList(parsed.data.features)
87
127
  const organizations = Array.isArray(parsed.data.organizations) ? parsed.data.organizations : null
88
128
 
@@ -13,7 +13,11 @@ import { E } from '#generated/entities.ids.generated'
13
13
  import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
14
14
  import type { EntityManager } from '@mikro-orm/postgresql'
15
15
  import { userCrudEvents, userCrudIndexer } from '@open-mercato/core/modules/auth/commands/users'
16
- import { assertActorCanGrantRoleTokens } from '@open-mercato/core/modules/auth/lib/grantChecks'
16
+ import {
17
+ assertActorCanGrantRoleTokens,
18
+ assertActorCanModifySuperAdminUserTarget,
19
+ listSuperAdminUserIds,
20
+ } from '@open-mercato/core/modules/auth/lib/grantChecks'
17
21
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
18
22
  import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
19
23
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
@@ -132,6 +136,9 @@ const crud = makeCrudRoute<CrudInput, CrudInput, Record<string, unknown>>({
132
136
  schema: rawBodySchema,
133
137
  mapInput: async ({ parsed, ctx }) => {
134
138
  if (ctx.request) {
139
+ if (typeof parsed.id === 'string' && parsed.id.length) {
140
+ await assertCanModifySuperAdminTarget(ctx.request, parsed.id)
141
+ }
135
142
  await assertCanAssignRoles(ctx.request, parsed.roles, parsed)
136
143
  }
137
144
  return parsed
@@ -184,6 +191,10 @@ export async function GET(req: Request) {
184
191
  return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })
185
192
  }
186
193
  effectiveTenantId = actorTenantId
194
+ const superAdminUserIds = await listSuperAdminUserIds(em, actorTenantId)
195
+ if (superAdminUserIds.size) {
196
+ filters.push({ id: { $nin: Array.from(superAdminUserIds) as any } })
197
+ }
187
198
  } else {
188
199
  const selectedTenantId = getSelectedTenantFromRequest(req)
189
200
  if (typeof selectedTenantId === 'string' && selectedTenantId.trim().length > 0) {
@@ -437,7 +448,20 @@ export const PUT = async (req: Request) => {
437
448
  return crud.PUT(req)
438
449
  }
439
450
 
440
- export const DELETE = crud.DELETE
451
+ export const DELETE = async (req: Request) => {
452
+ const targetId = new URL(req.url).searchParams.get('id')
453
+ if (targetId) {
454
+ try {
455
+ await assertCanModifySuperAdminTarget(req, targetId)
456
+ } catch (err) {
457
+ if (err instanceof CrudHttpError) {
458
+ return NextResponse.json(err.body, { status: err.status })
459
+ }
460
+ throw err
461
+ }
462
+ }
463
+ return crud.DELETE(req)
464
+ }
441
465
 
442
466
  async function findUserIdsBySearchTokens(
443
467
  em: EntityManager,
@@ -469,6 +493,21 @@ async function findUserIdsBySearchTokens(
469
493
  .filter((id): id is string => typeof id === 'string' && id.length > 0)
470
494
  }
471
495
 
496
+ async function assertCanModifySuperAdminTarget(req: Request, targetUserId: string) {
497
+ const auth = await getAuthFromRequest(req)
498
+ if (!auth?.sub) throw new CrudHttpError(401, { error: 'Unauthorized' })
499
+ const container = await createRequestContainer()
500
+ const em = container.resolve('em') as EntityManager
501
+ await assertActorCanModifySuperAdminUserTarget({
502
+ em,
503
+ rbacService: container.resolve('rbacService') as RbacService,
504
+ actorUserId: auth.sub,
505
+ tenantId: auth.tenantId ?? null,
506
+ organizationId: auth.orgId ?? null,
507
+ targetUserId,
508
+ })
509
+ }
510
+
472
511
  async function assertCanAssignRoles(req: Request, roles: unknown, payload: Record<string, unknown>) {
473
512
  if (!Array.isArray(roles)) return
474
513
  const auth = await getAuthFromRequest(req)
@@ -286,6 +286,7 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
286
286
  resourceKind: 'auth.user',
287
287
  resourceId: String(user.id),
288
288
  tenantId: user.tenantId ? String(user.tenantId) : null,
289
+ organizationId: user.organizationId ? String(user.organizationId) : null,
289
290
  snapshotAfter: snapshot.view,
290
291
  payload: {
291
292
  undo: {
@@ -559,6 +560,7 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
559
560
  resourceKind: 'auth.user',
560
561
  resourceId: String(result.id),
561
562
  tenantId: result.tenantId ? String(result.tenantId) : null,
563
+ organizationId: result.organizationId ? String(result.organizationId) : null,
562
564
  changes,
563
565
  snapshotBefore: before ?? null,
564
566
  snapshotAfter: after,
@@ -689,6 +691,7 @@ const deleteUserCommand: CommandHandler<{ body?: Record<string, unknown>; query?
689
691
  resourceId: id,
690
692
  snapshotBefore: before ?? null,
691
693
  tenantId: before?.tenantId ?? null,
694
+ organizationId: before?.organizationId ?? null,
692
695
  payload: {
693
696
  undo: {
694
697
  before: beforeUndo,