@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5-develop.5382.1.f542de69af

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 (119) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/modules/attachments/api/library/route.js +2 -2
  4. package/dist/modules/attachments/api/library/route.js.map +2 -2
  5. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  6. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  7. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  8. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  9. package/dist/modules/auth/commands/users.js +20 -14
  10. package/dist/modules/auth/commands/users.js.map +2 -2
  11. package/dist/modules/auth/data/entities.js +1 -1
  12. package/dist/modules/auth/data/entities.js.map +2 -2
  13. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  14. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  16. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  17. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  18. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  19. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  20. package/dist/modules/communication_channels/encryption.js.map +1 -1
  21. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  22. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  23. package/dist/modules/currencies/api/currencies/route.js +4 -3
  24. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  25. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  26. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  27. package/dist/modules/customer_accounts/events.js +1 -1
  28. package/dist/modules/customer_accounts/events.js.map +1 -1
  29. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  30. package/dist/modules/customers/acl.js +1 -1
  31. package/dist/modules/customers/acl.js.map +1 -1
  32. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  33. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  34. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  35. package/dist/modules/customers/api/companies/route.js +4 -4
  36. package/dist/modules/customers/api/companies/route.js.map +2 -2
  37. package/dist/modules/customers/api/people/route.js +4 -4
  38. package/dist/modules/customers/api/people/route.js.map +2 -2
  39. package/dist/modules/customers/commands/addresses.js +5 -5
  40. package/dist/modules/customers/commands/addresses.js.map +2 -2
  41. package/dist/modules/customers/commands/comments.js +5 -5
  42. package/dist/modules/customers/commands/comments.js.map +2 -2
  43. package/dist/modules/customers/commands/deals.js +2 -2
  44. package/dist/modules/customers/commands/deals.js.map +2 -2
  45. package/dist/modules/customers/commands/entity-roles.js +2 -1
  46. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  47. package/dist/modules/customers/commands/interactions.js +8 -5
  48. package/dist/modules/customers/commands/interactions.js.map +2 -2
  49. package/dist/modules/customers/commands/shared.js +21 -6
  50. package/dist/modules/customers/commands/shared.js.map +2 -2
  51. package/dist/modules/customers/commands/tags.js +3 -3
  52. package/dist/modules/customers/commands/tags.js.map +2 -2
  53. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  54. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  55. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  56. package/dist/modules/data_sync/api/run.js +1 -1
  57. package/dist/modules/data_sync/api/run.js.map +2 -2
  58. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  59. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  60. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  61. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  62. package/dist/modules/progress/api/jobs/route.js +1 -1
  63. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  64. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  65. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  66. package/dist/modules/resources/api/resources.js +2 -3
  67. package/dist/modules/resources/api/resources.js.map +2 -2
  68. package/dist/modules/sales/api/documents/factory.js +2 -2
  69. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  70. package/dist/modules/sync_excel/api/import/route.js +1 -1
  71. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  72. package/dist/modules/workflows/api/definitions/route.js +3 -2
  73. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  74. package/package.json +7 -7
  75. package/src/modules/attachments/api/library/route.ts +2 -2
  76. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  77. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  78. package/src/modules/auth/commands/users.ts +32 -15
  79. package/src/modules/auth/data/entities.ts +11 -1
  80. package/src/modules/auth/migrations/.snapshot-open-mercato.json +0 -10
  81. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  82. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  83. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  84. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  85. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  86. package/src/modules/communication_channels/data/entities.ts +2 -2
  87. package/src/modules/communication_channels/encryption.ts +1 -1
  88. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  89. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  90. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  91. package/src/modules/currencies/api/currencies/route.ts +4 -3
  92. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  93. package/src/modules/customer_accounts/events.ts +1 -1
  94. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  95. package/src/modules/customers/acl.ts +1 -1
  96. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  97. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  98. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  99. package/src/modules/customers/api/companies/route.ts +4 -4
  100. package/src/modules/customers/api/people/route.ts +4 -4
  101. package/src/modules/customers/commands/addresses.ts +5 -5
  102. package/src/modules/customers/commands/comments.ts +5 -5
  103. package/src/modules/customers/commands/deals.ts +2 -2
  104. package/src/modules/customers/commands/entity-roles.ts +2 -1
  105. package/src/modules/customers/commands/interactions.ts +8 -5
  106. package/src/modules/customers/commands/shared.ts +26 -4
  107. package/src/modules/customers/commands/tags.ts +3 -3
  108. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  109. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  110. package/src/modules/data_sync/api/run.ts +1 -1
  111. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  112. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  113. package/src/modules/progress/api/jobs/route.ts +1 -1
  114. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  115. package/src/modules/resources/api/resources.ts +2 -3
  116. package/src/modules/sales/api/documents/factory.ts +2 -2
  117. package/src/modules/staff/AGENTS.md +1 -1
  118. package/src/modules/sync_excel/api/import/route.ts +1 -1
  119. package/src/modules/workflows/api/definitions/route.ts +3 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/workflows/api/definitions/route.ts"],
4
- "sourcesContent": ["/**\n * Workflow Definitions API\n *\n * Endpoints:\n * - GET /api/workflows/definitions - List workflow definitions\n * - POST /api/workflows/definitions - Create workflow definition\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { WorkflowDefinition } from '../../data/entities'\nimport {\n createWorkflowDefinitionInputSchema,\n type CreateWorkflowDefinitionApiInput,\n} from '../../data/validators'\nimport { serializeWorkflowDefinition, serializeCodeWorkflowDefinition } from './serialize'\nimport { invalidateTriggerCache } from '../../lib/event-trigger-service'\nimport { getAllCodeWorkflows } from '../../lib/code-registry'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.definitions.view'],\n}\n\nconst WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT = 'workflow_definitions_workflow_id_tenant_id_unique'\n\nfunction isWorkflowIdUniqueConstraintError(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false\n }\n\n const value = error as Record<string, unknown>\n const constraint = value.constraint\n const code = value.code\n const message = typeof value.message === 'string' ? value.message : ''\n const detail = typeof value.detail === 'string' ? value.detail : ''\n\n if (constraint === WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT) {\n return true\n }\n\n if (code === '23505' && detail.includes('(workflow_id, tenant_id)')) {\n return true\n }\n\n return message.includes(WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT)\n}\n\n/**\n * GET /api/workflows/definitions\n *\n * List workflow definitions with optional filters\n */\nexport async function GET(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n\n const { searchParams } = new URL(request.url)\n const enabled = searchParams.get('enabled')\n const workflowId = searchParams.get('workflowId')\n const search = searchParams.get('search')\n const limit = parseInt(searchParams.get('limit') || '50')\n const offset = parseInt(searchParams.get('offset') || '0')\n\n // Build where clause with tenant scoping\n const where: any = {\n tenantId,\n ...orgFilter.where,\n deletedAt: null,\n }\n\n if (enabled !== null) {\n where.enabled = enabled === 'true'\n }\n\n if (workflowId) {\n where.workflowId = workflowId\n }\n\n if (search) {\n where.$or = [\n { workflowId: { $ilike: `%${search}%` } },\n { workflowName: { $ilike: `%${search}%` } },\n ]\n }\n\n // Determine which code workflows are shadowed by a DB row (so we can\n // exclude them from the code-only list) without loading all DB rows.\n const enabledFilter = enabled !== null ? enabled === 'true' : null\n const searchLower = search ? search.toLowerCase() : null\n const allCodeIds = getAllCodeWorkflows().map((cw) => cw.workflowId)\n const shadowed = allCodeIds.length > 0\n ? new Set(\n (\n await em.find(\n WorkflowDefinition,\n { ...where, workflowId: { $in: allCodeIds } },\n { fields: ['workflowId'] as const },\n )\n ).map((d: WorkflowDefinition) => d.workflowId),\n )\n : new Set<string>()\n\n const codeOnly = getAllCodeWorkflows()\n .filter((cw) => !shadowed.has(cw.workflowId))\n .filter((cw) => {\n if (searchLower) {\n const matches =\n cw.workflowId.toLowerCase().includes(searchLower) ||\n cw.workflowName.toLowerCase().includes(searchLower)\n if (!matches) return false\n }\n if (enabledFilter !== null && cw.enabled !== enabledFilter) return false\n if (workflowId && cw.workflowId !== workflowId) return false\n return true\n })\n .map((cw) => serializeCodeWorkflowDefinition(cw, `code:${cw.workflowId}`))\n\n const dbCount = await em.count(WorkflowDefinition, where)\n const total = dbCount + codeOnly.length\n\n // Fetch only the prefix of DB rows we might need to fill the requested\n // page after merging with the (already-filtered) code-only list.\n const dbWindowLimit = offset + limit\n const dbWindow = dbWindowLimit > 0\n ? await em.find(WorkflowDefinition, where, {\n orderBy: { workflowName: 'ASC' },\n limit: dbWindowLimit,\n })\n : []\n\n const serializedDb = dbWindow.map(serializeWorkflowDefinition)\n\n const merged = [...serializedDb, ...codeOnly].sort((a, b) =>\n a.workflowName.localeCompare(b.workflowName),\n )\n\n const paginated = merged.slice(offset, offset + limit)\n\n return NextResponse.json({\n data: paginated,\n pagination: {\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n },\n })\n } catch (error) {\n console.error('Error listing workflow definitions:', error)\n return NextResponse.json(\n { error: 'Failed to list workflow definitions' },\n { status: 500 }\n )\n }\n}\n\n/**\n * POST /api/workflows/definitions\n *\n * Create a new workflow definition\n */\nexport async function POST(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n // Check create permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.definitions.create'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n const body = await request.json()\n\n // Validate input\n const validation = createWorkflowDefinitionInputSchema.safeParse(body)\n if (!validation.success) {\n return NextResponse.json(\n {\n error: 'Validation failed',\n details: validation.error.issues,\n },\n { status: 400 }\n )\n }\n\n const input: CreateWorkflowDefinitionApiInput = validation.data\n\n // workflow_id is unique per tenant; check upfront to return 409 instead of DB error.\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId: input.workflowId,\n tenantId,\n })\n\n if (existing) {\n return NextResponse.json(\n {\n error: `Workflow definition with ID \"${input.workflowId}\" already exists`,\n },\n { status: 409 }\n )\n }\n\n // Create workflow definition\n const definition = em.create(WorkflowDefinition, {\n workflowId: input.workflowId,\n workflowName: input.workflowName,\n description: input.description,\n version: input.version,\n definition: input.definition,\n metadata: input.metadata,\n enabled: input.enabled ?? true,\n tenantId,\n organizationId,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n\n await em.persist(definition).flush()\n\n // Newly-created embedded triggers must be visible to the wildcard event\n // subscriber immediately; invalidate the in-memory trigger cache so the\n // next event reload picks up this definition.\n if (tenantId) invalidateTriggerCache(tenantId, organizationId ?? undefined)\n\n return NextResponse.json(\n {\n data: serializeWorkflowDefinition(definition),\n message: 'Workflow definition created successfully',\n },\n { status: 201 }\n )\n } catch (error) {\n if (isWorkflowIdUniqueConstraintError(error)) {\n return NextResponse.json(\n { error: 'Workflow definition with this ID already exists' },\n { status: 409 }\n )\n }\n\n console.error('Error creating workflow definition:', error)\n return NextResponse.json(\n { error: 'Failed to create workflow definition' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n summary: 'List workflow definitions',\n description: 'Get a list of workflow definitions with optional filters. Supports pagination and search.',\n tags: ['Workflows'],\n query: createWorkflowDefinitionInputSchema.pick({ workflowId: true }).extend({\n enabled: z.boolean().optional(),\n search: z.string().optional(),\n limit: z.number().int().positive().default(50).optional(),\n offset: z.number().int().min(0).default(0).optional(),\n }),\n responses: [\n {\n status: 200,\n description: 'List of workflow definitions with pagination',\n example: {\n data: [\n {\n id: '123e4567-e89b-12d3-a456-426614174000',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n {\n stepId: 'start',\n stepName: 'Start',\n stepType: 'START',\n },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n },\n {\n stepId: 'end',\n stepName: 'End',\n stepType: 'END',\n },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-end',\n fromStepId: 'validate-cart',\n toStepId: 'end',\n trigger: 'auto',\n },\n ],\n },\n enabled: true,\n tenantId: '123e4567-e89b-12d3-a456-426614174001',\n organizationId: '123e4567-e89b-12d3-a456-426614174002',\n createdAt: '2025-12-08T10:00:00.000Z',\n updatedAt: '2025-12-08T10:00:00.000Z',\n },\n ],\n pagination: {\n total: 1,\n limit: 50,\n offset: 0,\n hasMore: false,\n },\n },\n },\n ],\n },\n POST: {\n summary: 'Create workflow definition',\n description: 'Create a new workflow definition. The definition must include at least START and END steps with at least one transition connecting them.',\n tags: ['Workflows'],\n requestBody: {\n schema: createWorkflowDefinitionInputSchema,\n example: {\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n {\n stepId: 'start',\n stepName: 'Start',\n stepType: 'START',\n },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n description: 'Validate cart items and check inventory',\n },\n {\n stepId: 'payment',\n stepName: 'Process Payment',\n stepType: 'AUTOMATED',\n description: 'Charge payment method',\n retryPolicy: {\n maxAttempts: 3,\n backoffMs: 1000,\n },\n },\n {\n stepId: 'end',\n stepName: 'End',\n stepType: 'END',\n },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-payment',\n fromStepId: 'validate-cart',\n toStepId: 'payment',\n trigger: 'auto',\n },\n {\n transitionId: 'payment-to-end',\n fromStepId: 'payment',\n toStepId: 'end',\n trigger: 'auto',\n activities: [\n {\n activityName: 'Send Order Confirmation',\n activityType: 'SEND_EMAIL',\n config: {\n to: '{{context.customerEmail}}',\n subject: 'Order Confirmation #{{context.orderId}}',\n template: 'order_confirmation',\n },\n },\n ],\n },\n ],\n },\n enabled: true,\n },\n },\n responses: [\n {\n status: 201,\n description: 'Workflow definition created successfully',\n example: {\n data: {\n id: '123e4567-e89b-12d3-a456-426614174000',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n { stepId: 'start', stepName: 'Start', stepType: 'START' },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n },\n {\n stepId: 'payment',\n stepName: 'Process Payment',\n stepType: 'AUTOMATED',\n },\n { stepId: 'end', stepName: 'End', stepType: 'END' },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-payment',\n fromStepId: 'validate-cart',\n toStepId: 'payment',\n trigger: 'auto',\n },\n {\n transitionId: 'payment-to-end',\n fromStepId: 'payment',\n toStepId: 'end',\n trigger: 'auto',\n },\n ],\n },\n enabled: true,\n tenantId: '123e4567-e89b-12d3-a456-426614174001',\n organizationId: '123e4567-e89b-12d3-a456-426614174002',\n createdAt: '2025-12-08T10:00:00.000Z',\n updatedAt: '2025-12-08T10:00:00.000Z',\n },\n message: 'Workflow definition created successfully',\n },\n },\n {\n status: 400,\n description: 'Validation error - invalid workflow structure',\n example: {\n error: 'Validation failed',\n details: [\n {\n code: 'invalid_type',\n message: 'Workflow must have at least START and END steps',\n path: ['definition', 'steps'],\n },\n ],\n },\n },\n {\n status: 409,\n description: 'Conflict - workflow with same ID and version already exists',\n example: {\n error: 'Workflow definition with ID \"checkout-flow\" and version 1 already exists',\n },\n },\n ],\n },\n },\n}\n\n// Full OpenAPI documentation (kept for reference but not used by type system)\nexport const _openApiDetailedDocs = {\n get: {\n summary: 'List workflow definitions',\n description: 'Get a list of workflow definitions with optional filters',\n tags: ['Workflows'],\n parameters: [\n {\n name: 'enabled',\n in: 'query',\n description: 'Filter by enabled status',\n schema: { type: 'boolean' },\n },\n {\n name: 'workflowId',\n in: 'query',\n description: 'Filter by workflow ID',\n schema: { type: 'string' },\n },\n {\n name: 'search',\n in: 'query',\n description: 'Search in workflow ID and name',\n schema: { type: 'string' },\n },\n {\n name: 'limit',\n in: 'query',\n description: 'Number of results to return',\n schema: { type: 'integer', default: 50 },\n },\n {\n name: 'offset',\n in: 'query',\n description: 'Offset for pagination',\n schema: { type: 'integer', default: 0 },\n },\n ],\n responses: {\n 200: {\n description: 'List of workflow definitions',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: { $ref: '#/components/schemas/WorkflowDefinition' },\n },\n pagination: {\n type: 'object',\n properties: {\n total: { type: 'integer' },\n limit: { type: 'integer' },\n offset: { type: 'integer' },\n hasMore: { type: 'boolean' },\n },\n },\n },\n },\n },\n },\n },\n },\n },\n post: {\n summary: 'Create workflow definition',\n description: 'Create a new workflow definition',\n tags: ['Workflows'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/CreateWorkflowDefinition' },\n },\n },\n },\n responses: {\n 201: {\n description: 'Workflow definition created',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n data: { $ref: '#/components/schemas/WorkflowDefinition' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n 400: {\n description: 'Validation error',\n },\n 409: {\n description: 'Workflow definition already exists',\n },\n },\n },\n}\n"],
5
- "mappings": "AAQA,SAAsB,oBAAoB;AAC1C,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,sCAAsC;AAC/C,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,OAEK;AACP,SAAS,6BAA6B,uCAAuC;AAC7E,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AAE7B,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,4BAA4B;AAChD;AAEA,MAAM,uCAAuC;AAE7C,SAAS,kCAAkC,OAAyB;AAClE,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ;AACd,QAAM,aAAa,MAAM;AACzB,QAAM,OAAO,MAAM;AACnB,QAAM,UAAU,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;AACpE,QAAM,SAAS,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;AAEjE,MAAI,eAAe,sCAAsC;AACvD,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,WAAW,OAAO,SAAS,0BAA0B,GAAG;AACnE,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,SAAS,oCAAoC;AAC9D;AAOA,eAAsB,IAAI,SAAsB;AAC9C,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,OAAO,MAAM,mBAAmB,OAAO;AAE7C,QAAI,CAAC,MAAM;AACT,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AAEA,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAC;AACnF,UAAM,WAAW,KAAK;AACtB,UAAM,YAAY,+BAA+B,OAAO,IAAI;AAE5D,UAAM,EAAE,aAAa,IAAI,IAAI,IAAI,QAAQ,GAAG;AAC5C,UAAM,UAAU,aAAa,IAAI,SAAS;AAC1C,UAAM,aAAa,aAAa,IAAI,YAAY;AAChD,UAAM,SAAS,aAAa,IAAI,QAAQ;AACxC,UAAM,QAAQ,SAAS,aAAa,IAAI,OAAO,KAAK,IAAI;AACxD,UAAM,SAAS,SAAS,aAAa,IAAI,QAAQ,KAAK,GAAG;AAGzD,UAAM,QAAa;AAAA,MACjB;AAAA,MACA,GAAG,UAAU;AAAA,MACb,WAAW;AAAA,IACb;AAEA,QAAI,YAAY,MAAM;AACpB,YAAM,UAAU,YAAY;AAAA,IAC9B;AAEA,QAAI,YAAY;AACd,YAAM,aAAa;AAAA,IACrB;AAEA,QAAI,QAAQ;AACV,YAAM,MAAM;AAAA,QACV,EAAE,YAAY,EAAE,QAAQ,IAAI,MAAM,IAAI,EAAE;AAAA,QACxC,EAAE,cAAc,EAAE,QAAQ,IAAI,MAAM,IAAI,EAAE;AAAA,MAC5C;AAAA,IACF;AAIA,UAAM,gBAAgB,YAAY,OAAO,YAAY,SAAS;AAC9D,UAAM,cAAc,SAAS,OAAO,YAAY,IAAI;AACpD,UAAM,aAAa,oBAAoB,EAAE,IAAI,CAAC,OAAO,GAAG,UAAU;AAClE,UAAM,WAAW,WAAW,SAAS,IACjC,IAAI;AAAA,OAEA,MAAM,GAAG;AAAA,QACP;AAAA,QACA,EAAE,GAAG,OAAO,YAAY,EAAE,KAAK,WAAW,EAAE;AAAA,QAC5C,EAAE,QAAQ,CAAC,YAAY,EAAW;AAAA,MACpC,GACA,IAAI,CAAC,MAA0B,EAAE,UAAU;AAAA,IAC/C,IACA,oBAAI,IAAY;AAEpB,UAAM,WAAW,oBAAoB,EAClC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,GAAG,UAAU,CAAC,EAC3C,OAAO,CAAC,OAAO;AACd,UAAI,aAAa;AACf,cAAM,UACJ,GAAG,WAAW,YAAY,EAAE,SAAS,WAAW,KAChD,GAAG,aAAa,YAAY,EAAE,SAAS,WAAW;AACpD,YAAI,CAAC,QAAS,QAAO;AAAA,MACvB;AACA,UAAI,kBAAkB,QAAQ,GAAG,YAAY,cAAe,QAAO;AACnE,UAAI,cAAc,GAAG,eAAe,WAAY,QAAO;AACvD,aAAO;AAAA,IACT,CAAC,EACA,IAAI,CAAC,OAAO,gCAAgC,IAAI,QAAQ,GAAG,UAAU,EAAE,CAAC;AAE3E,UAAM,UAAU,MAAM,GAAG,MAAM,oBAAoB,KAAK;AACxD,UAAM,QAAQ,UAAU,SAAS;AAIjC,UAAM,gBAAgB,SAAS;AAC/B,UAAM,WAAW,gBAAgB,IAC7B,MAAM,GAAG,KAAK,oBAAoB,OAAO;AAAA,MACvC,SAAS,EAAE,cAAc,MAAM;AAAA,MAC/B,OAAO;AAAA,IACT,CAAC,IACD,CAAC;AAEL,UAAM,eAAe,SAAS,IAAI,2BAA2B;AAE7D,UAAM,SAAS,CAAC,GAAG,cAAc,GAAG,QAAQ,EAAE;AAAA,MAAK,CAAC,GAAG,MACrD,EAAE,aAAa,cAAc,EAAE,YAAY;AAAA,IAC7C;AAEA,UAAM,YAAY,OAAO,MAAM,QAAQ,SAAS,KAAK;AAErD,WAAO,aAAa,KAAK;AAAA,MACvB,MAAM;AAAA,MACN,YAAY;AAAA,QACV;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,SAAS,QAAQ;AAAA,MAC5B;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,uCAAuC,KAAK;AAC1D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,sCAAsC;AAAA,MAC/C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAOA,eAAsB,KAAK,SAAsB;AAC/C,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,UAAM,OAAO,MAAM,mBAAmB,OAAO;AAE7C,QAAI,CAAC,MAAM;AACT,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AAEA,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,QAAQ,CAAC;AACnF,UAAM,WAAW,KAAK;AACtB,UAAM,iBAAiB,OAAO,cAAc,KAAK;AAGjD,UAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,UAAM,gBAAgB,MAAM,YAAY;AAAA,MACtC,KAAK;AAAA,MACL,CAAC,8BAA8B;AAAA,MAC/B;AAAA,QACE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,eAAe;AAClB,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,2BAA2B;AAAA,QACpC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,QAAQ,KAAK;AAGhC,UAAM,aAAa,oCAAoC,UAAU,IAAI;AACrE,QAAI,CAAC,WAAW,SAAS;AACvB,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO;AAAA,UACP,SAAS,WAAW,MAAM;AAAA,QAC5B;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,QAA0C,WAAW;AAG3D,UAAM,WAAW,MAAM,GAAG,QAAQ,oBAAoB;AAAA,MACpD,YAAY,MAAM;AAAA,MAClB;AAAA,IACF,CAAC;AAED,QAAI,UAAU;AACZ,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO,gCAAgC,MAAM,UAAU;AAAA,QACzD;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,UAAM,aAAa,GAAG,OAAO,oBAAoB;AAAA,MAC/C,YAAY,MAAM;AAAA,MAClB,cAAc,MAAM;AAAA,MACpB,aAAa,MAAM;AAAA,MACnB,SAAS,MAAM;AAAA,MACf,YAAY,MAAM;AAAA,MAClB,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM,WAAW;AAAA,MAC1B;AAAA,MACA;AAAA,MACA,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AAED,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAKnC,QAAI,SAAU,wBAAuB,UAAU,kBAAkB,MAAS;AAE1E,WAAO,aAAa;AAAA,MAClB;AAAA,QACE,MAAM,4BAA4B,UAAU;AAAA,QAC5C,SAAS;AAAA,MACX;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF,SAAS,OAAO;AACd,QAAI,kCAAkC,KAAK,GAAG;AAC5C,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,kDAAkD;AAAA,QAC3D,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,YAAQ,MAAM,uCAAuC,KAAK;AAC1D,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,uCAAuC;AAAA,MAChD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,WAAW;AAAA,MAClB,OAAO,oCAAoC,KAAK,EAAE,YAAY,KAAK,CAAC,EAAE,OAAO;AAAA,QAC3E,SAAS,EAAE,QAAQ,EAAE,SAAS;AAAA,QAC9B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,QAC5B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,SAAS;AAAA,QACxD,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,MACtD,CAAC;AAAA,MACD,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,MAAM;AAAA,cACJ;AAAA,gBACE,IAAI;AAAA,gBACJ,YAAY;AAAA,gBACZ,cAAc;AAAA,gBACd,aAAa;AAAA,gBACb,SAAS;AAAA,gBACT,YAAY;AAAA,kBACV,OAAO;AAAA,oBACL;AAAA,sBACE,QAAQ;AAAA,sBACR,UAAU;AAAA,sBACV,UAAU;AAAA,oBACZ;AAAA,oBACA;AAAA,sBACE,QAAQ;AAAA,sBACR,UAAU;AAAA,sBACV,UAAU;AAAA,oBACZ;AAAA,oBACA;AAAA,sBACE,QAAQ;AAAA,sBACR,UAAU;AAAA,sBACV,UAAU;AAAA,oBACZ;AAAA,kBACF;AAAA,kBACA,aAAa;AAAA,oBACX;AAAA,sBACE,cAAc;AAAA,sBACd,YAAY;AAAA,sBACZ,UAAU;AAAA,sBACV,SAAS;AAAA,oBACX;AAAA,oBACA;AAAA,sBACE,cAAc;AAAA,sBACd,YAAY;AAAA,sBACZ,UAAU;AAAA,sBACV,SAAS;AAAA,oBACX;AAAA,kBACF;AAAA,gBACF;AAAA,gBACA,SAAS;AAAA,gBACT,UAAU;AAAA,gBACV,gBAAgB;AAAA,gBAChB,WAAW;AAAA,gBACX,WAAW;AAAA,cACb;AAAA,YACF;AAAA,YACA,YAAY;AAAA,cACV,OAAO;AAAA,cACP,OAAO;AAAA,cACP,QAAQ;AAAA,cACR,SAAS;AAAA,YACX;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,MACb,MAAM,CAAC,WAAW;AAAA,MAClB,aAAa;AAAA,QACX,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,YAAY;AAAA,UACZ,cAAc;AAAA,UACd,aAAa;AAAA,UACb,SAAS;AAAA,UACT,YAAY;AAAA,YACV,OAAO;AAAA,cACL;AAAA,gBACE,QAAQ;AAAA,gBACR,UAAU;AAAA,gBACV,UAAU;AAAA,cACZ;AAAA,cACA;AAAA,gBACE,QAAQ;AAAA,gBACR,UAAU;AAAA,gBACV,UAAU;AAAA,gBACV,aAAa;AAAA,cACf;AAAA,cACA;AAAA,gBACE,QAAQ;AAAA,gBACR,UAAU;AAAA,gBACV,UAAU;AAAA,gBACV,aAAa;AAAA,gBACb,aAAa;AAAA,kBACX,aAAa;AAAA,kBACb,WAAW;AAAA,gBACb;AAAA,cACF;AAAA,cACA;AAAA,gBACE,QAAQ;AAAA,gBACR,UAAU;AAAA,gBACV,UAAU;AAAA,cACZ;AAAA,YACF;AAAA,YACA,aAAa;AAAA,cACX;AAAA,gBACE,cAAc;AAAA,gBACd,YAAY;AAAA,gBACZ,UAAU;AAAA,gBACV,SAAS;AAAA,cACX;AAAA,cACA;AAAA,gBACE,cAAc;AAAA,gBACd,YAAY;AAAA,gBACZ,UAAU;AAAA,gBACV,SAAS;AAAA,cACX;AAAA,cACA;AAAA,gBACE,cAAc;AAAA,gBACd,YAAY;AAAA,gBACZ,UAAU;AAAA,gBACV,SAAS;AAAA,gBACT,YAAY;AAAA,kBACV;AAAA,oBACE,cAAc;AAAA,oBACd,cAAc;AAAA,oBACd,QAAQ;AAAA,sBACN,IAAI;AAAA,sBACJ,SAAS;AAAA,sBACT,UAAU;AAAA,oBACZ;AAAA,kBACF;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,MAAM;AAAA,cACJ,IAAI;AAAA,cACJ,YAAY;AAAA,cACZ,cAAc;AAAA,cACd,aAAa;AAAA,cACb,SAAS;AAAA,cACT,YAAY;AAAA,gBACV,OAAO;AAAA,kBACL,EAAE,QAAQ,SAAS,UAAU,SAAS,UAAU,QAAQ;AAAA,kBACxD;AAAA,oBACE,QAAQ;AAAA,oBACR,UAAU;AAAA,oBACV,UAAU;AAAA,kBACZ;AAAA,kBACA;AAAA,oBACE,QAAQ;AAAA,oBACR,UAAU;AAAA,oBACV,UAAU;AAAA,kBACZ;AAAA,kBACA,EAAE,QAAQ,OAAO,UAAU,OAAO,UAAU,MAAM;AAAA,gBACpD;AAAA,gBACA,aAAa;AAAA,kBACX;AAAA,oBACE,cAAc;AAAA,oBACd,YAAY;AAAA,oBACZ,UAAU;AAAA,oBACV,SAAS;AAAA,kBACX;AAAA,kBACA;AAAA,oBACE,cAAc;AAAA,oBACd,YAAY;AAAA,oBACZ,UAAU;AAAA,oBACV,SAAS;AAAA,kBACX;AAAA,kBACA;AAAA,oBACE,cAAc;AAAA,oBACd,YAAY;AAAA,oBACZ,UAAU;AAAA,oBACV,SAAS;AAAA,kBACX;AAAA,gBACF;AAAA,cACF;AAAA,cACA,SAAS;AAAA,cACT,UAAU;AAAA,cACV,gBAAgB;AAAA,cAChB,WAAW;AAAA,cACX,WAAW;AAAA,YACb;AAAA,YACA,SAAS;AAAA,UACX;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,OAAO;AAAA,YACP,SAAS;AAAA,cACP;AAAA,gBACE,MAAM;AAAA,gBACN,SAAS;AAAA,gBACT,MAAM,CAAC,cAAc,OAAO;AAAA,cAC9B;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS;AAAA,YACP,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAGO,MAAM,uBAAuB;AAAA,EAClC,KAAK;AAAA,IACH,SAAS;AAAA,IACT,aAAa;AAAA,IACb,MAAM,CAAC,WAAW;AAAA,IAClB,YAAY;AAAA,MACV;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,QAAQ,EAAE,MAAM,UAAU;AAAA,MAC5B;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,QAAQ,EAAE,MAAM,SAAS;AAAA,MAC3B;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,QAAQ,EAAE,MAAM,SAAS;AAAA,MAC3B;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,QAAQ,EAAE,MAAM,WAAW,SAAS,GAAG;AAAA,MACzC;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,QAAQ,EAAE,MAAM,WAAW,SAAS,EAAE;AAAA,MACxC;AAAA,IACF;AAAA,IACA,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,cACN,MAAM;AAAA,cACN,YAAY;AAAA,gBACV,MAAM;AAAA,kBACJ,MAAM;AAAA,kBACN,OAAO,EAAE,MAAM,0CAA0C;AAAA,gBAC3D;AAAA,gBACA,YAAY;AAAA,kBACV,MAAM;AAAA,kBACN,YAAY;AAAA,oBACV,OAAO,EAAE,MAAM,UAAU;AAAA,oBACzB,OAAO,EAAE,MAAM,UAAU;AAAA,oBACzB,QAAQ,EAAE,MAAM,UAAU;AAAA,oBAC1B,SAAS,EAAE,MAAM,UAAU;AAAA,kBAC7B;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,aAAa;AAAA,IACb,MAAM,CAAC,WAAW;AAAA,IAClB,aAAa;AAAA,MACX,UAAU;AAAA,MACV,SAAS;AAAA,QACP,oBAAoB;AAAA,UAClB,QAAQ,EAAE,MAAM,gDAAgD;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,IACA,WAAW;AAAA,MACT,KAAK;AAAA,QACH,aAAa;AAAA,QACb,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB,QAAQ;AAAA,cACN,MAAM;AAAA,cACN,YAAY;AAAA,gBACV,MAAM,EAAE,MAAM,0CAA0C;AAAA,gBACxD,SAAS,EAAE,MAAM,SAAS;AAAA,cAC5B;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,KAAK;AAAA,QACH,aAAa;AAAA,MACf;AAAA,MACA,KAAK;AAAA,QACH,aAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["/**\n * Workflow Definitions API\n *\n * Endpoints:\n * - GET /api/workflows/definitions - List workflow definitions\n * - POST /api/workflows/definitions - Create workflow definition\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveOrganizationScopeFilter } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { WorkflowDefinition } from '../../data/entities'\nimport {\n createWorkflowDefinitionInputSchema,\n type CreateWorkflowDefinitionApiInput,\n} from '../../data/validators'\nimport { serializeWorkflowDefinition, serializeCodeWorkflowDefinition } from './serialize'\nimport { invalidateTriggerCache } from '../../lib/event-trigger-service'\nimport { getAllCodeWorkflows } from '../../lib/code-registry'\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['workflows.definitions.view'],\n}\n\nconst WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT = 'workflow_definitions_workflow_id_tenant_id_unique'\n\nfunction isWorkflowIdUniqueConstraintError(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false\n }\n\n const value = error as Record<string, unknown>\n const constraint = value.constraint\n const code = value.code\n const message = typeof value.message === 'string' ? value.message : ''\n const detail = typeof value.detail === 'string' ? value.detail : ''\n\n if (constraint === WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT) {\n return true\n }\n\n if (code === '23505' && detail.includes('(workflow_id, tenant_id)')) {\n return true\n }\n\n return message.includes(WORKFLOW_ID_TENANT_UNIQUE_CONSTRAINT)\n}\n\n/**\n * GET /api/workflows/definitions\n *\n * List workflow definitions with optional filters\n */\nexport async function GET(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const orgFilter = resolveOrganizationScopeFilter(scope, auth)\n\n const { searchParams } = new URL(request.url)\n const enabled = searchParams.get('enabled')\n const workflowId = searchParams.get('workflowId')\n const search = searchParams.get('search')\n const limit = parseInt(searchParams.get('limit') || '50')\n const offset = parseInt(searchParams.get('offset') || '0')\n\n // Build where clause with tenant scoping\n const where: any = {\n tenantId,\n ...orgFilter.where,\n deletedAt: null,\n }\n\n if (enabled !== null) {\n where.enabled = enabled === 'true'\n }\n\n if (workflowId) {\n where.workflowId = workflowId\n }\n\n if (search) {\n where.$or = [\n { workflowId: { $ilike: `%${escapeLikePattern(search)}%` } },\n { workflowName: { $ilike: `%${escapeLikePattern(search)}%` } },\n ]\n }\n\n // Determine which code workflows are shadowed by a DB row (so we can\n // exclude them from the code-only list) without loading all DB rows.\n const enabledFilter = enabled !== null ? enabled === 'true' : null\n const searchLower = search ? search.toLowerCase() : null\n const allCodeIds = getAllCodeWorkflows().map((cw) => cw.workflowId)\n const shadowed = allCodeIds.length > 0\n ? new Set(\n (\n await em.find(\n WorkflowDefinition,\n { ...where, workflowId: { $in: allCodeIds } },\n { fields: ['workflowId'] as const },\n )\n ).map((d: WorkflowDefinition) => d.workflowId),\n )\n : new Set<string>()\n\n const codeOnly = getAllCodeWorkflows()\n .filter((cw) => !shadowed.has(cw.workflowId))\n .filter((cw) => {\n if (searchLower) {\n const matches =\n cw.workflowId.toLowerCase().includes(searchLower) ||\n cw.workflowName.toLowerCase().includes(searchLower)\n if (!matches) return false\n }\n if (enabledFilter !== null && cw.enabled !== enabledFilter) return false\n if (workflowId && cw.workflowId !== workflowId) return false\n return true\n })\n .map((cw) => serializeCodeWorkflowDefinition(cw, `code:${cw.workflowId}`))\n\n const dbCount = await em.count(WorkflowDefinition, where)\n const total = dbCount + codeOnly.length\n\n // Fetch only the prefix of DB rows we might need to fill the requested\n // page after merging with the (already-filtered) code-only list.\n const dbWindowLimit = offset + limit\n const dbWindow = dbWindowLimit > 0\n ? await em.find(WorkflowDefinition, where, {\n orderBy: { workflowName: 'ASC' },\n limit: dbWindowLimit,\n })\n : []\n\n const serializedDb = dbWindow.map(serializeWorkflowDefinition)\n\n const merged = [...serializedDb, ...codeOnly].sort((a, b) =>\n a.workflowName.localeCompare(b.workflowName),\n )\n\n const paginated = merged.slice(offset, offset + limit)\n\n return NextResponse.json({\n data: paginated,\n pagination: {\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n },\n })\n } catch (error) {\n console.error('Error listing workflow definitions:', error)\n return NextResponse.json(\n { error: 'Failed to list workflow definitions' },\n { status: 500 }\n )\n }\n}\n\n/**\n * POST /api/workflows/definitions\n *\n * Create a new workflow definition\n */\nexport async function POST(request: NextRequest) {\n try {\n const container = await createRequestContainer()\n const em = container.resolve('em')\n const auth = await getAuthFromRequest(request)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request })\n const tenantId = auth.tenantId\n const organizationId = scope?.selectedId ?? auth.orgId\n\n // Check create permission\n const rbacService = container.resolve('rbacService')\n const hasPermission = await rbacService.userHasAllFeatures(\n auth.sub,\n ['workflows.definitions.create'],\n {\n tenantId,\n organizationId,\n }\n )\n\n if (!hasPermission) {\n return NextResponse.json(\n { error: 'Insufficient permissions' },\n { status: 403 }\n )\n }\n\n const body = await request.json()\n\n // Validate input\n const validation = createWorkflowDefinitionInputSchema.safeParse(body)\n if (!validation.success) {\n return NextResponse.json(\n {\n error: 'Validation failed',\n details: validation.error.issues,\n },\n { status: 400 }\n )\n }\n\n const input: CreateWorkflowDefinitionApiInput = validation.data\n\n // workflow_id is unique per tenant; check upfront to return 409 instead of DB error.\n const existing = await em.findOne(WorkflowDefinition, {\n workflowId: input.workflowId,\n tenantId,\n })\n\n if (existing) {\n return NextResponse.json(\n {\n error: `Workflow definition with ID \"${input.workflowId}\" already exists`,\n },\n { status: 409 }\n )\n }\n\n // Create workflow definition\n const definition = em.create(WorkflowDefinition, {\n workflowId: input.workflowId,\n workflowName: input.workflowName,\n description: input.description,\n version: input.version,\n definition: input.definition,\n metadata: input.metadata,\n enabled: input.enabled ?? true,\n tenantId,\n organizationId,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n\n await em.persist(definition).flush()\n\n // Newly-created embedded triggers must be visible to the wildcard event\n // subscriber immediately; invalidate the in-memory trigger cache so the\n // next event reload picks up this definition.\n if (tenantId) invalidateTriggerCache(tenantId, organizationId ?? undefined)\n\n return NextResponse.json(\n {\n data: serializeWorkflowDefinition(definition),\n message: 'Workflow definition created successfully',\n },\n { status: 201 }\n )\n } catch (error) {\n if (isWorkflowIdUniqueConstraintError(error)) {\n return NextResponse.json(\n { error: 'Workflow definition with this ID already exists' },\n { status: 409 }\n )\n }\n\n console.error('Error creating workflow definition:', error)\n return NextResponse.json(\n { error: 'Failed to create workflow definition' },\n { status: 500 }\n )\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n summary: 'List workflow definitions',\n description: 'Get a list of workflow definitions with optional filters. Supports pagination and search.',\n tags: ['Workflows'],\n query: createWorkflowDefinitionInputSchema.pick({ workflowId: true }).extend({\n enabled: z.boolean().optional(),\n search: z.string().optional(),\n limit: z.number().int().positive().default(50).optional(),\n offset: z.number().int().min(0).default(0).optional(),\n }),\n responses: [\n {\n status: 200,\n description: 'List of workflow definitions with pagination',\n example: {\n data: [\n {\n id: '123e4567-e89b-12d3-a456-426614174000',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n {\n stepId: 'start',\n stepName: 'Start',\n stepType: 'START',\n },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n },\n {\n stepId: 'end',\n stepName: 'End',\n stepType: 'END',\n },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-end',\n fromStepId: 'validate-cart',\n toStepId: 'end',\n trigger: 'auto',\n },\n ],\n },\n enabled: true,\n tenantId: '123e4567-e89b-12d3-a456-426614174001',\n organizationId: '123e4567-e89b-12d3-a456-426614174002',\n createdAt: '2025-12-08T10:00:00.000Z',\n updatedAt: '2025-12-08T10:00:00.000Z',\n },\n ],\n pagination: {\n total: 1,\n limit: 50,\n offset: 0,\n hasMore: false,\n },\n },\n },\n ],\n },\n POST: {\n summary: 'Create workflow definition',\n description: 'Create a new workflow definition. The definition must include at least START and END steps with at least one transition connecting them.',\n tags: ['Workflows'],\n requestBody: {\n schema: createWorkflowDefinitionInputSchema,\n example: {\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n {\n stepId: 'start',\n stepName: 'Start',\n stepType: 'START',\n },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n description: 'Validate cart items and check inventory',\n },\n {\n stepId: 'payment',\n stepName: 'Process Payment',\n stepType: 'AUTOMATED',\n description: 'Charge payment method',\n retryPolicy: {\n maxAttempts: 3,\n backoffMs: 1000,\n },\n },\n {\n stepId: 'end',\n stepName: 'End',\n stepType: 'END',\n },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-payment',\n fromStepId: 'validate-cart',\n toStepId: 'payment',\n trigger: 'auto',\n },\n {\n transitionId: 'payment-to-end',\n fromStepId: 'payment',\n toStepId: 'end',\n trigger: 'auto',\n activities: [\n {\n activityName: 'Send Order Confirmation',\n activityType: 'SEND_EMAIL',\n config: {\n to: '{{context.customerEmail}}',\n subject: 'Order Confirmation #{{context.orderId}}',\n template: 'order_confirmation',\n },\n },\n ],\n },\n ],\n },\n enabled: true,\n },\n },\n responses: [\n {\n status: 201,\n description: 'Workflow definition created successfully',\n example: {\n data: {\n id: '123e4567-e89b-12d3-a456-426614174000',\n workflowId: 'checkout-flow',\n workflowName: 'Checkout Flow',\n description: 'Complete checkout workflow for processing orders',\n version: 1,\n definition: {\n steps: [\n { stepId: 'start', stepName: 'Start', stepType: 'START' },\n {\n stepId: 'validate-cart',\n stepName: 'Validate Cart',\n stepType: 'AUTOMATED',\n },\n {\n stepId: 'payment',\n stepName: 'Process Payment',\n stepType: 'AUTOMATED',\n },\n { stepId: 'end', stepName: 'End', stepType: 'END' },\n ],\n transitions: [\n {\n transitionId: 'start-to-validate',\n fromStepId: 'start',\n toStepId: 'validate-cart',\n trigger: 'auto',\n },\n {\n transitionId: 'validate-to-payment',\n fromStepId: 'validate-cart',\n toStepId: 'payment',\n trigger: 'auto',\n },\n {\n transitionId: 'payment-to-end',\n fromStepId: 'payment',\n toStepId: 'end',\n trigger: 'auto',\n },\n ],\n },\n enabled: true,\n tenantId: '123e4567-e89b-12d3-a456-426614174001',\n organizationId: '123e4567-e89b-12d3-a456-426614174002',\n createdAt: '2025-12-08T10:00:00.000Z',\n updatedAt: '2025-12-08T10:00:00.000Z',\n },\n message: 'Workflow definition created successfully',\n },\n },\n {\n status: 400,\n description: 'Validation error - invalid workflow structure',\n example: {\n error: 'Validation failed',\n details: [\n {\n code: 'invalid_type',\n message: 'Workflow must have at least START and END steps',\n path: ['definition', 'steps'],\n },\n ],\n },\n },\n {\n status: 409,\n description: 'Conflict - workflow with same ID and version already exists',\n example: {\n error: 'Workflow definition with ID \"checkout-flow\" and version 1 already exists',\n },\n },\n ],\n },\n },\n}\n\n// Full OpenAPI documentation (kept for reference but not used by type system)\nexport const _openApiDetailedDocs = {\n get: {\n summary: 'List workflow definitions',\n description: 'Get a list of workflow definitions with optional filters',\n tags: ['Workflows'],\n parameters: [\n {\n name: 'enabled',\n in: 'query',\n description: 'Filter by enabled status',\n schema: { type: 'boolean' },\n },\n {\n name: 'workflowId',\n in: 'query',\n description: 'Filter by workflow ID',\n schema: { type: 'string' },\n },\n {\n name: 'search',\n in: 'query',\n description: 'Search in workflow ID and name',\n schema: { type: 'string' },\n },\n {\n name: 'limit',\n in: 'query',\n description: 'Number of results to return',\n schema: { type: 'integer', default: 50 },\n },\n {\n name: 'offset',\n in: 'query',\n description: 'Offset for pagination',\n schema: { type: 'integer', default: 0 },\n },\n ],\n responses: {\n 200: {\n description: 'List of workflow definitions',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n data: {\n type: 'array',\n items: { $ref: '#/components/schemas/WorkflowDefinition' },\n },\n pagination: {\n type: 'object',\n properties: {\n total: { type: 'integer' },\n limit: { type: 'integer' },\n offset: { type: 'integer' },\n hasMore: { type: 'boolean' },\n },\n },\n },\n },\n },\n },\n },\n },\n },\n post: {\n summary: 'Create workflow definition',\n description: 'Create a new workflow definition',\n tags: ['Workflows'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/CreateWorkflowDefinition' },\n },\n },\n },\n responses: {\n 201: {\n description: 'Workflow definition created',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n data: { $ref: '#/components/schemas/WorkflowDefinition' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n 400: {\n description: 'Validation error',\n },\n 409: {\n description: 'Workflow definition already exists',\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.5-develop.5337.1.534b781eac",
3
+ "version": "0.6.5-develop.5382.1.f542de69af",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -245,16 +245,16 @@
245
245
  "zod": "^4.4.3"
246
246
  },
247
247
  "peerDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.5-develop.5337.1.534b781eac",
249
- "@open-mercato/shared": "0.6.5-develop.5337.1.534b781eac",
250
- "@open-mercato/ui": "0.6.5-develop.5337.1.534b781eac",
248
+ "@open-mercato/ai-assistant": "0.6.5-develop.5382.1.f542de69af",
249
+ "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
250
+ "@open-mercato/ui": "0.6.5-develop.5382.1.f542de69af",
251
251
  "react": "^19.0.0",
252
252
  "react-dom": "^19.0.0"
253
253
  },
254
254
  "devDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.5-develop.5337.1.534b781eac",
256
- "@open-mercato/shared": "0.6.5-develop.5337.1.534b781eac",
257
- "@open-mercato/ui": "0.6.5-develop.5337.1.534b781eac",
255
+ "@open-mercato/ai-assistant": "0.6.5-develop.5382.1.f542de69af",
256
+ "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
257
+ "@open-mercato/ui": "0.6.5-develop.5382.1.f542de69af",
258
258
  "@testing-library/dom": "^10.4.1",
259
259
  "@testing-library/jest-dom": "^6.9.1",
260
260
  "@testing-library/react": "^16.3.1",
@@ -10,7 +10,7 @@ import { buildAttachmentImageUrl, slugifyAttachmentFileName } from '../../lib/im
10
10
  import { readAttachmentMetadata } from '../../lib/metadata'
11
11
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
12
12
  import { applyAssignmentEnrichments, resolveAssignmentEnrichments } from '../../lib/assignmentDetails'
13
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
13
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
14
14
  import { ensureDefaultPartitions } from '../../lib/partitions'
15
15
  import {
16
16
  attachmentsTag,
@@ -85,7 +85,7 @@ export async function GET(req: Request) {
85
85
  }
86
86
  qb.where(baseFilter)
87
87
  if (search && search.trim().length > 0) {
88
- qb.andWhere({ fileName: { $ilike: `%${escapeLikePattern(search.trim())}%` } })
88
+ qb.andWhere({ fileName: { $ilike: buildIlikeTerm(search.trim()) } })
89
89
  }
90
90
  if (partition && partition.trim().length > 0) {
91
91
  qb.andWhere({ partitionCode: partition.trim() })
@@ -1,7 +1,5 @@
1
1
  import * as React from 'react'
2
- import ReactMarkdown from 'react-markdown'
3
- import remarkGfm from 'remark-gfm'
4
- import type { PluggableList } from 'unified'
2
+ import { MarkdownContent } from '@open-mercato/ui/backend/markdown'
5
3
  import { Button } from '@open-mercato/ui/primitives/button'
6
4
 
7
5
  type Props = {
@@ -26,7 +24,6 @@ export function AttachmentContentPreview({
26
24
  const [expanded, setExpanded] = React.useState(false)
27
25
  const [tab, setTab] = React.useState<'source' | 'preview'>('source')
28
26
  const text = (content ?? '').trim()
29
- const markdownPlugins = React.useMemo<PluggableList>(() => [remarkGfm], [])
30
27
 
31
28
  // ARIA IDs for accessibility
32
29
  const sourceTabId = 'attachment-content-preview-tab-source'
@@ -96,9 +93,12 @@ export function AttachmentContentPreview({
96
93
  id={previewPanelId}
97
94
  aria-labelledby={previewTabId}
98
95
  data-testid="markdown-preview"
99
- className="text-sm text-muted-foreground [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs"
100
96
  >
101
- <ReactMarkdown remarkPlugins={markdownPlugins}>{text}</ReactMarkdown>
97
+ <MarkdownContent
98
+ body={text}
99
+ format="markdown"
100
+ className="text-sm text-muted-foreground [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs"
101
+ />
102
102
  </div>
103
103
  )}
104
104
 
@@ -70,10 +70,22 @@ export async function POST(req: Request) {
70
70
  if (log.actorUserId && log.actorUserId !== auth.sub && !canRedoTenant) {
71
71
  return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })
72
72
  }
73
- if (log.tenantId && auth.tenantId && log.tenantId !== auth.tenantId) {
73
+ // Fail closed on tenant scope: `audit_logs.redo_tenant` only widens scope WITHIN a
74
+ // tenant, never across tenants, so a tenant-scoped target always requires a caller
75
+ // bound to that same tenant. A caller whose tenantId is null (tenant-less global
76
+ // account or unscoped API key) must never redo a tenant-scoped row. Mirrors the
77
+ // hardened undo route (issue #2685, ported in #2931).
78
+ if (log.tenantId && log.tenantId !== (auth.tenantId ?? null)) {
74
79
  return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })
75
80
  }
76
- if (log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId) {
81
+ // Tenant-level redoers may redo across organizations within the tenant, so an
82
+ // unresolved (null) caller org is allowed and only an explicit mismatch is rejected.
83
+ // Every other caller must resolve to the target's own organization — a null caller
84
+ // org must not bypass an org-scoped target (issue #2685, ported in #2931).
85
+ const orgScopeMismatch = canRedoTenant
86
+ ? Boolean(log.organizationId && scopedOrgId && log.organizationId !== scopedOrgId)
87
+ : Boolean(log.organizationId && log.organizationId !== scopedOrgId)
88
+ if (orgScopeMismatch) {
77
89
  return NextResponse.json({ error: 'Redo target not available' }, { status: 400 })
78
90
  }
79
91
 
@@ -204,9 +204,14 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
204
204
  { tenantId: null, organizationId: parsed.organizationId },
205
205
  )
206
206
  if (!organization) throw new CrudHttpError(400, { error: 'Organization not found' })
207
+ const tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
207
208
 
208
209
  const emailHash = computeEmailHash(parsed.email)
209
- const duplicate = await findOneWithDecryption(em, User, { $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }], deletedAt: null } as any, {}, { tenantId: null, organizationId: null })
210
+ // Email is unique per-tenant, not globally (see Migration20260610120000:
211
+ // users_tenant_email_hash_uniq). Scope the duplicate check to the target tenant so the same
212
+ // email may legitimately exist in other tenants without blocking creation or leaking
213
+ // cross-tenant account existence (#2934).
214
+ const duplicate = await findOneWithDecryption(em, User, { $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }], deletedAt: null, tenantId } as any, {}, { tenantId: null, organizationId: null })
210
215
  if (duplicate) await throwDuplicateEmailError()
211
216
 
212
217
  let passwordHash: string | null = null
@@ -214,7 +219,6 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
214
219
  const { hash } = await import('bcryptjs')
215
220
  passwordHash = await hash(parsed.password, 10)
216
221
  }
217
- const tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
218
222
 
219
223
  const de = (ctx.container.resolve('dataEngine') as DataEngine)
220
224
  let user: User
@@ -518,13 +522,34 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
518
522
  ? await loadUserRoleNames(em, parsed.id)
519
523
  : null
520
524
 
525
+ // Resolve the tenant the user will belong to after this update first, so the email
526
+ // duplicate check below can be scoped to it. Email is unique per-tenant, not globally
527
+ // (see Migration20260610120000: users_tenant_email_hash_uniq) — a matching email in another
528
+ // tenant must not block the update or leak cross-tenant account existence (#2934).
529
+ let tenantId: string | null | undefined
530
+ if (parsed.organizationId !== undefined) {
531
+ const organization = await findOneWithDecryption(
532
+ em,
533
+ Organization,
534
+ { id: parsed.organizationId },
535
+ { populate: ['tenant'] },
536
+ { tenantId: null, organizationId: parsed.organizationId ?? null },
537
+ )
538
+ if (!organization) throw new CrudHttpError(400, { error: 'Organization not found' })
539
+ tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
540
+ }
541
+
521
542
  if (parsed.email !== undefined) {
543
+ const targetTenantId = tenantId !== undefined
544
+ ? tenantId
545
+ : await resolveUserTenantId(em, parsed.id)
522
546
  const duplicate = await findOneWithDecryption(
523
547
  em,
524
548
  User,
525
549
  {
526
550
  $or: [{ email: parsed.email }, { emailHash: { $in: emailHashLookupValues(parsed.email) } }],
527
551
  deletedAt: null,
552
+ tenantId: targetTenantId,
528
553
  id: { $ne: parsed.id } as any,
529
554
  } as FilterQuery<User>,
530
555
  {},
@@ -543,19 +568,6 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
543
568
  emailHash = computeEmailHash(parsed.email)
544
569
  }
545
570
 
546
- let tenantId: string | null | undefined
547
- if (parsed.organizationId !== undefined) {
548
- const organization = await findOneWithDecryption(
549
- em,
550
- Organization,
551
- { id: parsed.organizationId },
552
- { populate: ['tenant'] },
553
- { tenantId: null, organizationId: parsed.organizationId ?? null },
554
- )
555
- if (!organization) throw new CrudHttpError(400, { error: 'Organization not found' })
556
- tenantId = organization.tenant?.id ? String(organization.tenant.id) : null
557
- }
558
-
559
571
  const actorTenantScope = resolveActorTenantScope(ctx)
560
572
  const updateWhere: Record<string, unknown> = { id: parsed.id, deletedAt: null }
561
573
  if (actorTenantScope) updateWhere.tenantId = actorTenantScope
@@ -1069,6 +1081,11 @@ function arrayEquals(left: string[] | undefined, right: string[]): boolean {
1069
1081
  return left.every((value, idx) => value === right[idx])
1070
1082
  }
1071
1083
 
1084
+ async function resolveUserTenantId(em: EntityManager, id: string): Promise<string | null> {
1085
+ const existing = await findOneWithDecryption(em, User, { id, deletedAt: null }, {}, { tenantId: null, organizationId: null })
1086
+ return existing?.tenantId ? String(existing.tenantId) : null
1087
+ }
1088
+
1072
1089
  async function throwDuplicateEmailError(): Promise<never> {
1073
1090
  const { translate } = await resolveTranslations()
1074
1091
  const message = translate('auth.users.errors.emailExists', 'Email already in use')
@@ -1,6 +1,16 @@
1
1
  import { Entity, Index, ManyToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
2
2
 
3
3
  @Entity({ tableName: 'users' })
4
+ // Email uniqueness is per-tenant, enforced by a partial unique index
5
+ // (`users_tenant_email_hash_uniq`) on `(tenant_id, email_hash)` over live rows
6
+ // (`WHERE deleted_at IS NULL AND email_hash IS NOT NULL`), owned by raw SQL in
7
+ // Migration20260610120000. It keys on the deterministic `email_hash`, not `email`, because
8
+ // `email` is encrypted at rest with a per-row IV (see encryption.ts) — its ciphertext is
9
+ // non-deterministic, so a unique index on it would not detect duplicates. A `@Unique`
10
+ // decorator can't express a partial, tenant-scoped index, so the entity omits it — the
11
+ // migration is the source of truth. A global unique constraint contradicts the multi-tenant
12
+ // login flow and leaks cross-tenant account existence (#2934). Mirrors
13
+ // `customer_users_tenant_email_hash_uniq`.
4
14
  export class User {
5
15
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
6
16
  id!: string
@@ -11,7 +21,7 @@ export class User {
11
21
  @Property({ name: 'organization_id', type: 'uuid', nullable: true })
12
22
  organizationId?: string | null
13
23
 
14
- @Property({ type: 'text', unique: true })
24
+ @Property({ type: 'text' })
15
25
  email!: string
16
26
 
17
27
  @Property({ name: 'email_hash', type: 'text', nullable: true })
@@ -668,16 +668,6 @@
668
668
  }
669
669
  },
670
670
  "indexes": [
671
- {
672
- "columnNames": [
673
- "email"
674
- ],
675
- "composite": false,
676
- "keyName": "users_email_unique",
677
- "constraint": true,
678
- "primary": false,
679
- "unique": true
680
- },
681
671
  {
682
672
  "columnNames": [
683
673
  "email_hash"
@@ -0,0 +1,53 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ // #2934: User email uniqueness must be per-tenant, not global. The original
4
+ // `users_email_unique` constraint (unique on `email` across all tenants) contradicts the
5
+ // multi-tenant login flow — which resolves the same email across tenants via
6
+ // `findUsersByEmail` — and leaks cross-tenant account existence / enables registration
7
+ // squatting. Replace it with a partial unique index scoped per-tenant over live rows.
8
+ //
9
+ // The index is keyed on `email_hash`, NOT `email`: `email` is encrypted at rest with a
10
+ // per-row IV (auth/encryption.ts -> shared aes.ts), so its ciphertext is non-deterministic
11
+ // and a unique index on it would never detect duplicates under the default (encryption-on)
12
+ // configuration. `email_hash` is the deterministic lookup hash the application already
13
+ // de-dupes on, so the constraint is effective in both encryption-on and encryption-off
14
+ // modes. This mirrors `customer_users_tenant_email_hash_uniq` in customer_accounts.
15
+ //
16
+ // `WHERE deleted_at IS NULL` lets a soft-deleted user's email be reused (the old non-partial
17
+ // constraint blocked this); `AND email_hash IS NOT NULL` skips the rare legacy/bootstrap rows
18
+ // that predate hash population (encryption-off `setup-app` users) — those remain protected by
19
+ // the tenant-scoped application duplicate check.
20
+ //
21
+ // Before creating the index, soft-delete any pre-existing duplicate live rows per
22
+ // (tenant_id, email_hash), keeping the most-recently-updated one. Under encryption the old
23
+ // `email` constraint never fired, so same-tenant duplicates were only blocked by the
24
+ // application check and a historical race could have slipped one through; the dedupe makes
25
+ // the index creation safe on such data (no-op when there are none).
26
+ export class Migration20260610120000 extends Migration {
27
+
28
+ override up(): void | Promise<void> {
29
+ this.addSql(`
30
+ with ranked as (
31
+ select id,
32
+ row_number() over (
33
+ partition by tenant_id, email_hash
34
+ order by coalesce(updated_at, created_at) desc, created_at desc, id desc
35
+ ) as rn
36
+ from users
37
+ where deleted_at is null and email_hash is not null
38
+ )
39
+ update users
40
+ set deleted_at = now()
41
+ from ranked
42
+ where users.id = ranked.id and ranked.rn > 1;
43
+ `);
44
+ this.addSql(`alter table "users" drop constraint if exists "users_email_unique";`);
45
+ this.addSql(`create unique index if not exists "users_tenant_email_hash_uniq" on "users" ("tenant_id", "email_hash") where "deleted_at" is null and "email_hash" is not null;`);
46
+ }
47
+
48
+ override down(): void | Promise<void> {
49
+ this.addSql(`drop index if exists "users_tenant_email_hash_uniq";`);
50
+ this.addSql(`alter table "users" add constraint "users_email_unique" unique ("email");`);
51
+ }
52
+
53
+ }
@@ -5,7 +5,7 @@
5
5
  * Product-configuration surface: option schemas (variant axes) and unit
6
6
  * conversions (UoM factors).
7
7
  *
8
- * Phase 3c of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
+ * Phase 3c of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
9
9
  * both tools are now API-backed wrappers over the documented CRUD list
10
10
  * routes (`GET /api/catalog/option-schemas` and
11
11
  * `GET /api/catalog/product-unit-conversions`). Tool names, schemas,
@@ -11,7 +11,7 @@
11
11
  * names available so the D18 tool can layer merchandising-specific shape
12
12
  * over the base enumerator.
13
13
  *
14
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
14
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
15
15
  * `catalog.list_prices` and `catalog.list_offers` are now API-backed wrappers
16
16
  * over `GET /api/catalog/prices` and `GET /api/catalog/offers`. Tool names,
17
17
  * schemas, requiredFeatures, and output shapes are unchanged. The offers
@@ -4,7 +4,7 @@
4
4
  * Read-only tools scoped to `ctx.tenantId` + `ctx.organizationId`. Mutation
5
5
  * tools are deferred to Step 5.14 under the pending-action contract.
6
6
  *
7
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
7
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
8
8
  * `catalog.list_products` is now an API-backed wrapper over
9
9
  * `GET /api/catalog/products`. Tool name, schema, requiredFeatures, and
10
10
  * output shape are unchanged.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Enumerate variants for a single product with option values + media refs.
5
5
  *
6
- * Phase 3b of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
6
+ * Phase 3b of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
7
7
  * `catalog.list_variants` is now an API-backed wrapper over
8
8
  * `GET /api/catalog/variants`. Tool name, schema, requiredFeatures, and
9
9
  * output shape are unchanged.
@@ -440,7 +440,7 @@ export class MessageReaction {
440
440
  * forged inbound messages: tokens that don't HMAC-verify never reach the DB
441
441
  * lookup.
442
442
  *
443
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
443
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
444
444
  */
445
445
  @Entity({ tableName: 'channel_thread_tokens' })
446
446
  // One token row per (tenant, thread): the matcher resolves every reply to the
@@ -495,7 +495,7 @@ export class ChannelThreadToken {
495
495
  * `raw_body` is encrypted at rest via the module's `encryption.ts`
496
496
  * `defaultEncryptionMaps` entry (MIME bodies may contain PII).
497
497
  *
498
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
498
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
499
499
  * (§ 3 Data Model).
500
500
  */
501
501
  @Entity({ tableName: 'channel_ingest_dead_letters' })
@@ -31,7 +31,7 @@ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryptio
31
31
  * contact resolution looks addresses up by value (see the address blind-index
32
32
  * follow-up in customers/lib/findPeopleByAddresses.ts).
33
33
  *
34
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
34
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
35
35
  * (§ 3 Encryption posture).
36
36
  */
37
37
  export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
@@ -445,7 +445,7 @@ export interface ExchangeOAuthCodeResult {
445
445
  * `RefreshCredentialsInput`. Adapters without OAuth refresh (IMAP, WhatsApp
446
446
  * Business API) ignore it.
447
447
  *
448
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
448
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
449
449
  */
450
450
  export interface OAuthClientConfig {
451
451
  clientId: string
@@ -19,7 +19,7 @@ import { extractTokenFromBody, extractTokenFromHeaders } from './thread-token'
19
19
  * `UPDATE` that bumps a matched token's `last_seen_at` (a future-GC hint),
20
20
  * which does not touch the caller's pending entities.
21
21
  *
22
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`
22
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`
23
23
  * § 4 Threading Algorithm.
24
24
  */
25
25
 
@@ -17,7 +17,7 @@ import { isUniqueViolation } from './pg-errors'
17
17
  * `(tenantId, token)` so that even if the HMAC key leaked, tenant
18
18
  * isolation still holds at the DB layer.
19
19
  *
20
- * See `.ai/specs/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
20
+ * See `.ai/specs/implemented/2026-05-27-email-integration-inbound-reliability-and-threading.md`.
21
21
  */
22
22
 
23
23
  const TOKEN_PREFIX = 'om_'
@@ -6,6 +6,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import type { FilterQuery } from '@mikro-orm/core'
7
7
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
8
8
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
9
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
9
10
  import { currencyCreateSchema, currencyUpdateSchema } from '../../data/validators'
10
11
  import {
11
12
  createCurrenciesCrudOpenApi,
@@ -148,9 +149,9 @@ export async function GET(req: Request) {
148
149
  if (code) filter.code = code
149
150
  if (search) {
150
151
  filter.$or = [
151
- { code: { $ilike: `%${search}%` } },
152
- { name: { $ilike: `%${search}%` } },
153
- { symbol: { $ilike: `%${search}%` } },
152
+ { code: { $ilike: `%${escapeLikePattern(search)}%` } },
153
+ { name: { $ilike: `%${escapeLikePattern(search)}%` } },
154
+ { symbol: { $ilike: `%${escapeLikePattern(search)}%` } },
154
155
  ]
155
156
  }
156
157
  if (isBase === 'true') filter.isBase = true
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import type { OpenApiRouteDoc, OpenApiMethodDoc } 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
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
6
7
  import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
8
  import { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'
8
9
  import { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
@@ -37,7 +38,7 @@ export async function GET(req: Request) {
37
38
  }
38
39
 
39
40
  if (search) {
40
- const escapedSearch = search.replace(/[%_\\]/g, '\\$&')
41
+ const escapedSearch = escapeLikePattern(search)
41
42
  where.$or = [
42
43
  { name: { $ilike: `%${escapedSearch}%` } },
43
44
  { slug: { $ilike: `%${escapedSearch}%` } },
@@ -18,7 +18,7 @@ const events = [
18
18
  { id: 'customer_accounts.role.deleted', label: 'Customer Role Deleted', entity: 'role', category: 'crud' },
19
19
  { id: 'customer_accounts.invitation.accepted', label: 'Customer Invitation Accepted', category: 'lifecycle', clientBroadcast: true },
20
20
  { id: 'customer_accounts.password_reset.requested', label: 'Customer Password Reset Requested', category: 'lifecycle' },
21
- // Custom domain mapping lifecycle (see .ai/specs/2026-04-08-portal-custom-domain-routing.md)
21
+ // Custom domain mapping lifecycle (see .ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md)
22
22
  { id: 'customer_accounts.domain_mapping.created', label: 'Custom Domain Registered', entity: 'domain_mapping', category: 'crud', clientBroadcast: true },
23
23
  { id: 'customer_accounts.domain_mapping.verified', label: 'Custom Domain DNS Verified', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
24
24
  { id: 'customer_accounts.domain_mapping.activated', label: 'Custom Domain Active', entity: 'domain_mapping', category: 'lifecycle', clientBroadcast: true },
@@ -12,8 +12,8 @@
12
12
  * signup, magic-link, password-reset) so they all behave consistently when
13
13
  * the request arrives on a tenant's branded URL.
14
14
  *
15
- * See `.ai/specs/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and
16
- * `.ai/specs/2026-06-05-tenant-ownership-and-module-acl-authorization.md` § C.
15
+ * See `.ai/specs/implemented/2026-04-08-portal-custom-domain-routing.md` Phase 1.5 and
16
+ * `.ai/specs/implemented/2026-06-05-tenant-ownership-and-module-acl-authorization.md` § C.
17
17
  */
18
18
 
19
19
  import { tryNormalizeHostname } from '@open-mercato/core/modules/customer_accounts/lib/hostname'