@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6814.1.0627c7e9f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +12 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +12 -2
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/business_rules/lib/rule-engine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport type { EventBus } from '@open-mercato/events'\nimport { BusinessRule, RuleExecutionLog, type RuleType } from '../data/entities'\nimport * as ruleEvaluator from './rule-evaluator'\nimport * as actionExecutor from './action-executor'\nimport type { RuleEvaluationContext } from './rule-evaluator'\nimport type { ActionContext, ActionExecutionOutcome } from './action-executor'\nimport { ruleEngineContextSchema, ruleDiscoveryOptionsSchema, directRuleExecutionContextSchema, ruleIdExecutionContextSchema } from '../data/validators'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'rule-engine' })\n\n/**\n * Constants\n */\nconst DEFAULT_ENTITY_ID = 'unknown'\nconst RULE_TYPE_GUARD = 'GUARD'\nconst EXECUTION_RESULT_ERROR = 'ERROR'\nconst EXECUTION_RESULT_SUCCESS = 'SUCCESS'\nconst EXECUTION_RESULT_FAILURE = 'FAILURE'\n\n/**\n * Execution limits\n */\nconst MAX_RULES_PER_EXECUTION = 100\nconst MAX_SINGLE_RULE_TIMEOUT_MS = 30000 // 30 seconds\nconst MAX_TOTAL_EXECUTION_TIMEOUT_MS = 60000 // 60 seconds\nconst RULE_DISCOVERY_CACHE_TTL = 5 * 60 * 1000\n\n/**\n * Rule execution context\n */\nexport interface RuleEngineContext {\n entityType: string\n entityId?: string\n eventType?: string\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenant?: {\n id?: string\n [key: string]: any\n }\n organization?: {\n id?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n [key: string]: any\n}\n\n/**\n * Single rule execution result\n */\nexport interface RuleExecutionResult {\n rule: BusinessRule\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n logId?: string // Database log ID (if logged)\n}\n\n/**\n * Overall rule engine result\n */\nexport interface RuleEngineResult {\n allowed: boolean\n executedRules: RuleExecutionResult[]\n totalExecutionTime: number\n errors?: string[]\n logIds?: string[]\n}\n\n/**\n * Rule discovery options\n */\nexport interface RuleDiscoveryOptions {\n entityType: string\n eventType?: string\n tenantId: string\n organizationId: string\n ruleType?: RuleType\n}\n\ntype CachedRuleDiscovery = {\n ruleIds: string[]\n}\n\nexport type RuleDiscoveryCache = Pick<CacheStrategy, 'get' | 'set' | 'deleteByTags'>\n\nexport type RuleDiscoveryCacheOptions = {\n cache?: RuleDiscoveryCache | null\n}\n\nfunction normalizeCachePart(value: string | null | undefined): string {\n return encodeURIComponent(value?.trim() || '*')\n}\n\nfunction getRuleDiscoveryCacheKey(options: RuleDiscoveryOptions): string {\n return [\n normalizeCachePart(options.tenantId),\n normalizeCachePart(options.organizationId),\n normalizeCachePart(options.entityType),\n normalizeCachePart(options.eventType),\n normalizeCachePart(options.ruleType),\n ].join(':')\n}\n\nfunction isCachedRuleDiscovery(value: unknown): value is CachedRuleDiscovery {\n if (!value || typeof value !== 'object') return false\n const ruleIds = (value as CachedRuleDiscovery).ruleIds\n return Array.isArray(ruleIds) && ruleIds.every((entry) => typeof entry === 'string')\n}\n\nexport function isRuleDiscoveryCache(value: unknown): value is RuleDiscoveryCache {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<RuleDiscoveryCache>\n return (\n typeof candidate.get === 'function'\n && typeof candidate.set === 'function'\n && typeof candidate.deleteByTags === 'function'\n )\n}\n\nexport function resolveBusinessRuleDiscoveryCache(resolve: (token: string) => unknown): RuleDiscoveryCache | null {\n try {\n const cache = resolve('cache')\n return isRuleDiscoveryCache(cache) ? cache : null\n } catch {\n return null\n }\n}\n\nfunction getRuleDiscoveryCacheTags(options: Pick<RuleDiscoveryOptions, 'organizationId'>): string[] {\n return [\n 'business_rules:discovery',\n `business_rules:discovery:organization:${options.organizationId}`,\n ]\n}\n\nasync function getCachedRuleDiscovery(\n cache: RuleDiscoveryCache | null | undefined,\n options: RuleDiscoveryOptions,\n): Promise<CachedRuleDiscovery | null> {\n if (!cache) return null\n\n let value: unknown\n try {\n value = await runWithCacheTenant(options.tenantId, () =>\n cache.get(getRuleDiscoveryCacheKey(options))\n )\n } catch (error) {\n logger.warn('Failed to read rule discovery cache', { err: error })\n return null\n }\n\n return isCachedRuleDiscovery(value) ? value : null\n}\n\nasync function cacheRuleDiscovery(\n cache: RuleDiscoveryCache | null | undefined,\n options: RuleDiscoveryOptions,\n rules: BusinessRule[],\n): Promise<void> {\n if (!cache) return\n\n const ruleIds = rules\n .map((rule) => rule.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n\n if (ruleIds.length !== rules.length) return\n\n try {\n await runWithCacheTenant(options.tenantId, () =>\n cache.set(\n getRuleDiscoveryCacheKey(options),\n { ruleIds },\n {\n ttl: RULE_DISCOVERY_CACHE_TTL,\n tags: getRuleDiscoveryCacheTags(options),\n },\n )\n )\n } catch (error) {\n logger.warn('Failed to write rule discovery cache', { err: error })\n }\n}\n\nexport async function invalidateBusinessRuleDiscoveryCache(\n cache: RuleDiscoveryCache | null | undefined,\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<void> {\n if (!cache) return\n\n const normalizedTenantId = tenantId?.trim()\n const normalizedOrganizationId = organizationId?.trim()\n\n if (!normalizedTenantId) {\n return\n }\n\n const tags = normalizedOrganizationId\n ? [`business_rules:discovery:organization:${normalizedOrganizationId}`]\n : ['business_rules:discovery']\n\n try {\n await runWithCacheTenant(normalizedTenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n logger.warn('Failed to invalidate rule discovery cache', { err: error })\n }\n}\n\nexport type RuleEngineExecutionOptions = {\n eventBus?: Pick<EventBus, 'emitEvent'> | null\n cache?: RuleDiscoveryCache | null\n}\n\ntype RuleExecutionFailedPayload = {\n ruleId: string\n ruleName: string\n entityType?: string | null\n errorMessage?: string | null\n tenantId: string\n organizationId?: string | null\n}\n\n/**\n * Direct rule execution context (for executing a specific rule by ID)\n */\nexport interface DirectRuleExecutionContext {\n ruleId: string // Database UUID of the rule\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n // Optional for logging (falls back to rule's entityType)\n entityType?: string\n entityId?: string\n eventType?: string\n}\n\n/**\n * Direct rule execution result\n */\nexport interface DirectRuleExecutionResult {\n success: boolean\n ruleId: string\n ruleName: string\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n logId?: string\n}\n\n/**\n * Context for executing a rule by its string rule_id identifier\n * Unlike DirectRuleExecutionContext which uses database UUID,\n * this uses the string identifier (e.g., \"workflow_checkout_inventory_available\")\n */\nexport interface RuleIdExecutionContext {\n ruleId: string // String identifier (e.g., \"workflow_checkout_inventory_available\")\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n entityType?: string\n entityId?: string\n eventType?: string\n}\n\n/**\n * Execute a function with a timeout\n */\nasync function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage: string\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([promise, timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n\n/**\n * Execute all applicable rules for the given context\n */\nexport async function executeRules(\n em: EntityManager,\n context: RuleEngineContext,\n options: RuleEngineExecutionOptions = {}\n): Promise<RuleEngineResult> {\n // Validate input\n const validation = ruleEngineContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: validationErrors,\n }\n }\n\n const startTime = Date.now()\n const executedRules: RuleExecutionResult[] = []\n const errors: string[] = []\n const logIds: string[] = []\n\n try {\n // Discover applicable rules\n const rules = await findApplicableRules(em, {\n entityType: context.entityType,\n eventType: context.eventType,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n }, { cache: options.cache })\n\n // Check rule count limit\n if (rules.length > MAX_RULES_PER_EXECUTION) {\n errors.push(\n `Rule count limit exceeded: ${rules.length} rules found, maximum is ${MAX_RULES_PER_EXECUTION}`\n )\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: Date.now() - startTime,\n errors,\n }\n }\n\n // Rules already sorted by database query (priority DESC, ruleId ASC)\n // Execute each rule with total timeout\n const executionPromise = (async () => {\n for (const rule of rules) {\n try {\n const ruleResult = await executeSingleRule(em, rule, context, options)\n executedRules.push(ruleResult)\n\n if (ruleResult.logId) {\n logIds.push(ruleResult.logId)\n }\n\n if (ruleResult.error) {\n errors.push(\n `Rule execution failed [ruleId=${rule.ruleId}, type=${rule.ruleType}, entityType=${context.entityType}]: ${ruleResult.error}`\n )\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(\n `Unexpected error in rule execution [ruleId=${rule.ruleId}, type=${rule.ruleType}]: ${errorMessage}`\n )\n\n if (!context.dryRun) {\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n executedRules.push({\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: 0,\n error: errorMessage,\n })\n }\n }\n })()\n\n // Execute with timeout\n await withTimeout(\n executionPromise,\n MAX_TOTAL_EXECUTION_TIMEOUT_MS,\n `Total rule execution timeout [entityType=${context.entityType}]`\n )\n\n // Determine overall allowed status\n // For GUARD rules: all must pass for operation to be allowed\n const guardRules = executedRules.filter((r) => r.rule.ruleType === RULE_TYPE_GUARD)\n const allowed = guardRules.length === 0 || guardRules.every((r) => r.conditionResult)\n\n const totalExecutionTime = Date.now() - startTime\n\n return {\n allowed,\n executedRules,\n totalExecutionTime,\n errors: errors.length > 0 ? errors : undefined,\n logIds: logIds.length > 0 ? logIds : undefined,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n errors.push(\n `Critical rule engine error [entityType=${context.entityType}, entityId=${context.entityId || 'unknown'}]: ${errorMessage}${stack ? `\\nStack: ${stack}` : ''}`\n )\n\n const totalExecutionTime = Date.now() - startTime\n\n return {\n allowed: false,\n executedRules,\n totalExecutionTime,\n errors,\n }\n }\n}\n\n/**\n * Execute a single rule\n */\nexport async function executeSingleRule(\n em: EntityManager,\n rule: BusinessRule,\n context: RuleEngineContext,\n options: RuleEngineExecutionOptions = {}\n): Promise<RuleExecutionResult> {\n const startTime = Date.now()\n\n try {\n // Wrap execution in timeout\n const executeWithTimeout = async () => {\n // Build evaluation context\n const evalContext: RuleEvaluationContext = {\n entityType: context.entityType,\n entityId: context.entityId,\n eventType: context.eventType,\n user: context.user,\n tenant: context.tenant,\n organization: context.organization,\n }\n\n // Evaluate rule conditions\n const result = await ruleEvaluator.evaluateSingleRule(rule, context.data, evalContext)\n\n // Check if evaluation completed (not just if conditions passed)\n if (!result.evaluationCompleted) {\n const executionTime = Date.now() - startTime\n\n let logId: string | undefined\n\n // Log failure if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: result.error,\n })\n\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage: result.error ?? null,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n return {\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: result.error,\n logId,\n }\n }\n\n // Evaluation completed successfully - determine which actions to execute\n const actions = result.conditionsPassed ? rule.successActions : rule.failureActions\n\n let actionsExecuted: ActionExecutionOutcome | null = null\n\n if (actions && Array.isArray(actions) && actions.length > 0) {\n // Build action context\n const actionContext: ActionContext = {\n ...evalContext,\n data: context.data,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n }\n\n // Execute actions\n actionsExecuted = await actionExecutor.executeActions(actions, actionContext)\n }\n\n const executionTime = Date.now() - startTime\n\n let logId: string | undefined\n\n // Log execution if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: result.conditionsPassed,\n actionsExecuted,\n executionTime,\n })\n }\n\n return {\n rule,\n conditionResult: result.conditionsPassed,\n actionsExecuted,\n executionTime,\n logId,\n }\n }\n\n // Execute with single rule timeout\n return await withTimeout(\n executeWithTimeout(),\n MAX_SINGLE_RULE_TIMEOUT_MS,\n `Single rule execution timeout [ruleId=${rule.ruleId}]`\n )\n } catch (error) {\n const executionTime = Date.now() - startTime\n const errorMessage = error instanceof Error ? error.message : String(error)\n const enhancedError = `Failed to execute rule [ruleId=${rule.ruleId}, entityType=${context.entityType}]: ${errorMessage}`\n\n let logId: string | undefined\n\n // Log error if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: enhancedError,\n })\n\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage: enhancedError,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n return {\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: enhancedError,\n logId,\n }\n }\n}\n\n/**\n * Find all applicable rules for the given criteria\n */\nexport async function findApplicableRules(\n em: EntityManager,\n options: RuleDiscoveryOptions,\n cacheOptions: RuleDiscoveryCacheOptions = {},\n): Promise<BusinessRule[]> {\n // Validate input\n ruleDiscoveryOptionsSchema.parse(options)\n\n const { entityType, eventType, tenantId, organizationId, ruleType } = options\n const baseWhere: Record<string, unknown> = {\n entityType,\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }\n\n if (eventType) {\n baseWhere.eventType = eventType\n }\n\n if (ruleType) {\n baseWhere.ruleType = ruleType\n }\n\n const cached = await getCachedRuleDiscovery(cacheOptions.cache, options)\n let rules: BusinessRule[]\n\n if (cached) {\n if (cached.ruleIds.length === 0) {\n rules = []\n } else {\n const cachedRules = await em.find(BusinessRule, {\n ...baseWhere,\n id: { $in: cached.ruleIds },\n }, {\n orderBy: { priority: 'DESC', ruleId: 'ASC' },\n } as any)\n const byId = new Map(cachedRules.map((rule) => [rule.id, rule]))\n rules = cached.ruleIds\n .map((id) => byId.get(id))\n .filter((rule): rule is BusinessRule => Boolean(rule))\n }\n } else {\n rules = await em.find(BusinessRule, baseWhere as Partial<BusinessRule>, {\n orderBy: { priority: 'DESC', ruleId: 'ASC' },\n })\n\n await cacheRuleDiscovery(cacheOptions.cache, options, rules)\n }\n\n // Filter by effective date range\n const now = new Date()\n return rules.filter((rule) => {\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return false\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return false\n }\n return true\n })\n}\n\n/**\n * Execute a specific rule by its database UUID\n * This bypasses the entityType/eventType discovery mechanism and directly executes the rule\n */\nexport async function executeRuleById(\n em: EntityManager,\n context: DirectRuleExecutionContext\n): Promise<DirectRuleExecutionResult> {\n const startTime = Date.now()\n\n // Validate input\n const validation = directRuleExecutionContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Validation failed: ${validationErrors.join(', ')}`,\n }\n }\n\n // Fetch rule by ID with tenant/org validation\n const rule = await em.findOne(BusinessRule, {\n id: context.ruleId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (!rule) {\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule not found',\n }\n }\n\n if (!rule.enabled) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n const now = new Date()\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,\n }\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,\n }\n }\n\n // Build RuleEngineContext (use provided entityType or fall back to rule's)\n const engineContext: RuleEngineContext = {\n entityType: context.entityType || rule.entityType,\n entityId: context.entityId,\n eventType: context.eventType || rule.eventType || undefined,\n data: context.data,\n user: context.user,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy,\n dryRun: context.dryRun,\n }\n\n // Execute via existing executeSingleRule\n const result = await executeSingleRule(em, rule, engineContext)\n\n return {\n success: !result.error,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: result.conditionResult,\n actionsExecuted: result.actionsExecuted,\n executionTime: result.executionTime,\n error: result.error,\n logId: result.logId,\n }\n}\n\n/**\n * Execute a rule by its string rule_id identifier\n * Looks up rule by rule_id (string column) + tenant_id (unique constraint)\n * This is useful for workflow conditions that reference rules by their string identifiers\n */\nexport async function executeRuleByRuleId(\n em: EntityManager,\n context: RuleIdExecutionContext\n): Promise<DirectRuleExecutionResult> {\n const startTime = Date.now()\n\n // Validate input\n const validation = ruleIdExecutionContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n success: false,\n ruleId: context.ruleId || 'unknown',\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Validation failed: ${validationErrors.join(', ')}`,\n }\n }\n\n // Fetch rule by rule_id (string identifier) + tenant/org\n const rule = await em.findOne(BusinessRule, {\n ruleId: context.ruleId, // String identifier column\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (!rule) {\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule not found',\n }\n }\n\n if (!rule.enabled) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n const now = new Date()\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,\n }\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,\n }\n }\n\n // Build RuleEngineContext (use provided entityType or fall back to rule's)\n const engineContext: RuleEngineContext = {\n entityType: context.entityType || rule.entityType,\n entityId: context.entityId,\n eventType: context.eventType || rule.eventType || undefined,\n data: context.data,\n user: context.user,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy,\n dryRun: context.dryRun,\n }\n\n // Execute via existing executeSingleRule\n const result = await executeSingleRule(em, rule, engineContext)\n\n return {\n success: !result.error,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: result.conditionResult,\n actionsExecuted: result.actionsExecuted,\n executionTime: result.executionTime,\n error: result.error,\n logId: result.logId,\n }\n}\n\n/**\n * Sensitive field patterns to exclude from logs\n */\nconst SENSITIVE_FIELD_PATTERNS = [\n /password/i,\n /passwd/i,\n /pwd/i,\n /secret/i,\n /token/i,\n /api[_-]?key/i,\n /auth/i,\n /credit[_-]?card/i,\n /card[_-]?number/i,\n /cvv/i,\n /ssn/i,\n /social[_-]?security/i,\n /tax[_-]?id/i,\n /driver[_-]?license/i,\n /passport/i,\n]\n\n/**\n * Maximum depth for nested object sanitization\n */\nconst MAX_SANITIZATION_DEPTH = 5\n\n/**\n * Sanitize data for logging by removing sensitive fields\n */\nfunction sanitizeForLogging(data: any, depth: number = 0): any {\n // Prevent infinite recursion\n if (depth > MAX_SANITIZATION_DEPTH) {\n return '[Max depth exceeded]'\n }\n\n // Handle null/undefined\n if (data === null || data === undefined) {\n return data\n }\n\n // Handle primitives\n if (typeof data !== 'object') {\n return data\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n return data.map(item => sanitizeForLogging(item, depth + 1))\n }\n\n // Handle objects\n const sanitized: Record<string, any> = {}\n\n for (const [key, value] of Object.entries(data)) {\n // Check if field name matches sensitive patterns\n const isSensitive = SENSITIVE_FIELD_PATTERNS.some(pattern => pattern.test(key))\n\n if (isSensitive) {\n sanitized[key] = '[REDACTED]'\n } else if (typeof value === 'object' && value !== null) {\n sanitized[key] = sanitizeForLogging(value, depth + 1)\n } else {\n sanitized[key] = value\n }\n }\n\n return sanitized\n}\n\n/**\n * Sanitize user object for logging (keep only safe fields)\n */\nfunction sanitizeUser(user: any): any {\n if (!user) {\n return undefined\n }\n\n // Only log safe user fields\n return {\n id: user.id,\n role: user.role,\n // Don't log: email, name, phone, address, etc.\n }\n}\n\n/**\n * Log rule execution to database\n */\ninterface LogExecutionOptions {\n rule: BusinessRule\n context: RuleEngineContext\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n}\n\nexport async function logRuleExecution(\n em: EntityManager,\n options: LogExecutionOptions\n): Promise<string> {\n const { rule, context, conditionResult, actionsExecuted, executionTime, error } = options\n\n const executionResult: 'SUCCESS' | 'FAILURE' | 'ERROR' = error\n ? EXECUTION_RESULT_ERROR\n : conditionResult\n ? EXECUTION_RESULT_SUCCESS\n : EXECUTION_RESULT_FAILURE\n\n const log = em.create(RuleExecutionLog, {\n rule,\n entityId: context.entityId || DEFAULT_ENTITY_ID,\n entityType: context.entityType,\n executionResult,\n inputContext: {\n data: sanitizeForLogging(context.data),\n eventType: context.eventType,\n user: sanitizeUser(context.user),\n },\n outputContext: actionsExecuted\n ? {\n conditionResult,\n actionsExecuted: actionsExecuted.results.map((r) => ({\n type: r.action.type,\n success: r.success,\n error: r.error,\n })),\n }\n : null,\n errorMessage: error || null,\n executionTimeMs: executionTime,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy || null,\n })\n\n await em.persist(log).flush()\n\n return String(log.id)\n}\n\nasync function emitRuleExecutionFailed(\n eventBus: Pick<EventBus, 'emitEvent'> | null | undefined,\n payload: RuleExecutionFailedPayload\n): Promise<void> {\n if (!eventBus?.emitEvent) return\n\n await eventBus.emitEvent('business_rules.rule.execution_failed', payload).catch(() => undefined)\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/core'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport type { EventBus } from '@open-mercato/events'\nimport { BusinessRule, RuleExecutionLog, type RuleType } from '../data/entities'\nimport * as ruleEvaluator from './rule-evaluator'\nimport * as actionExecutor from './action-executor'\nimport type { RuleEvaluationContext } from './rule-evaluator'\nimport type { ActionContext, ActionExecutionOutcome } from './action-executor'\nimport { ruleEngineContextSchema, ruleDiscoveryOptionsSchema, directRuleExecutionContextSchema, ruleIdExecutionContextSchema } from '../data/validators'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('business_rules').child({ component: 'rule-engine' })\n\n/**\n * Constants\n */\nconst DEFAULT_ENTITY_ID = 'unknown'\nconst RULE_TYPE_GUARD = 'GUARD'\nconst EXECUTION_RESULT_ERROR = 'ERROR'\nconst EXECUTION_RESULT_SUCCESS = 'SUCCESS'\nconst EXECUTION_RESULT_FAILURE = 'FAILURE'\n\n/**\n * Execution limits\n */\nconst MAX_RULES_PER_EXECUTION = 100\nconst MAX_SINGLE_RULE_TIMEOUT_MS = 30000 // 30 seconds\nconst MAX_TOTAL_EXECUTION_TIMEOUT_MS = 60000 // 60 seconds\nconst RULE_DISCOVERY_CACHE_TTL = 5 * 60 * 1000\n\n/**\n * Rule execution context\n */\nexport interface RuleEngineContext {\n entityType: string\n entityId?: string\n eventType?: string\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenant?: {\n id?: string\n [key: string]: any\n }\n organization?: {\n id?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n [key: string]: any\n}\n\n/**\n * Single rule execution result\n */\nexport interface RuleExecutionResult {\n rule: BusinessRule\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n logId?: string // Database log ID (if logged)\n}\n\n/**\n * Overall rule engine result\n */\nexport interface RuleEngineResult {\n allowed: boolean\n executedRules: RuleExecutionResult[]\n totalExecutionTime: number\n errors?: string[]\n logIds?: string[]\n}\n\n/**\n * Rule discovery options\n */\nexport interface RuleDiscoveryOptions {\n entityType: string\n eventType?: string\n tenantId: string\n organizationId: string\n ruleType?: RuleType\n}\n\ntype CachedRuleDiscovery = {\n ruleIds: string[]\n}\n\nexport type RuleDiscoveryCache = Pick<CacheStrategy, 'get' | 'set' | 'deleteByTags'>\n\nexport type RuleDiscoveryCacheOptions = {\n cache?: RuleDiscoveryCache | null\n}\n\nfunction normalizeCachePart(value: string | null | undefined): string {\n return encodeURIComponent(value?.trim() || '*')\n}\n\nfunction getRuleDiscoveryCacheKey(options: RuleDiscoveryOptions): string {\n return [\n normalizeCachePart(options.tenantId),\n normalizeCachePart(options.organizationId),\n normalizeCachePart(options.entityType),\n normalizeCachePart(options.eventType),\n normalizeCachePart(options.ruleType),\n ].join(':')\n}\n\nfunction isCachedRuleDiscovery(value: unknown): value is CachedRuleDiscovery {\n if (!value || typeof value !== 'object') return false\n const ruleIds = (value as CachedRuleDiscovery).ruleIds\n return Array.isArray(ruleIds) && ruleIds.every((entry) => typeof entry === 'string')\n}\n\nexport function isRuleDiscoveryCache(value: unknown): value is RuleDiscoveryCache {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<RuleDiscoveryCache>\n return (\n typeof candidate.get === 'function'\n && typeof candidate.set === 'function'\n && typeof candidate.deleteByTags === 'function'\n )\n}\n\nexport function resolveBusinessRuleDiscoveryCache(resolve: (token: string) => unknown): RuleDiscoveryCache | null {\n try {\n const cache = resolve('cache')\n return isRuleDiscoveryCache(cache) ? cache : null\n } catch {\n return null\n }\n}\n\nfunction getRuleDiscoveryCacheTags(options: Pick<RuleDiscoveryOptions, 'organizationId'>): string[] {\n return [\n 'business_rules:discovery',\n `business_rules:discovery:organization:${options.organizationId}`,\n ]\n}\n\nasync function getCachedRuleDiscovery(\n cache: RuleDiscoveryCache | null | undefined,\n options: RuleDiscoveryOptions,\n): Promise<CachedRuleDiscovery | null> {\n if (!cache) return null\n\n let value: unknown\n try {\n value = await runWithCacheTenant(options.tenantId, () =>\n cache.get(getRuleDiscoveryCacheKey(options))\n )\n } catch (error) {\n logger.warn('Failed to read rule discovery cache', { err: error })\n return null\n }\n\n return isCachedRuleDiscovery(value) ? value : null\n}\n\nasync function cacheRuleDiscovery(\n cache: RuleDiscoveryCache | null | undefined,\n options: RuleDiscoveryOptions,\n rules: BusinessRule[],\n): Promise<void> {\n if (!cache) return\n\n const ruleIds = rules\n .map((rule) => rule.id)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n\n if (ruleIds.length !== rules.length) return\n\n try {\n await runWithCacheTenant(options.tenantId, () =>\n cache.set(\n getRuleDiscoveryCacheKey(options),\n { ruleIds },\n {\n ttl: RULE_DISCOVERY_CACHE_TTL,\n tags: getRuleDiscoveryCacheTags(options),\n },\n )\n )\n } catch (error) {\n logger.warn('Failed to write rule discovery cache', { err: error })\n }\n}\n\nexport async function invalidateBusinessRuleDiscoveryCache(\n cache: RuleDiscoveryCache | null | undefined,\n tenantId?: string | null,\n organizationId?: string | null,\n): Promise<void> {\n if (!cache) return\n\n const normalizedTenantId = tenantId?.trim()\n const normalizedOrganizationId = organizationId?.trim()\n\n if (!normalizedTenantId) {\n return\n }\n\n const tags = normalizedOrganizationId\n ? [`business_rules:discovery:organization:${normalizedOrganizationId}`]\n : ['business_rules:discovery']\n\n try {\n await runWithCacheTenant(normalizedTenantId, () => cache.deleteByTags(tags))\n } catch (error) {\n logger.warn('Failed to invalidate rule discovery cache', { err: error })\n }\n}\n\nexport type RuleEngineExecutionOptions = {\n eventBus?: Pick<EventBus, 'emitEvent'> | null\n cache?: RuleDiscoveryCache | null\n}\n\ntype RuleExecutionFailedPayload = {\n ruleId: string\n ruleName: string\n entityType?: string | null\n errorMessage?: string | null\n tenantId: string\n organizationId?: string | null\n}\n\n/**\n * Direct rule execution context (for executing a specific rule by ID)\n */\nexport interface DirectRuleExecutionContext {\n ruleId: string // Database UUID of the rule\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n // Optional for logging (falls back to rule's entityType)\n entityType?: string\n entityId?: string\n eventType?: string\n}\n\n/**\n * Direct rule execution result\n */\nexport interface DirectRuleExecutionResult {\n success: boolean\n ruleId: string\n ruleName: string\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n logId?: string\n}\n\n/**\n * Context for executing a rule by its string rule_id identifier\n * Unlike DirectRuleExecutionContext which uses database UUID,\n * this uses the string identifier (e.g., \"workflow_checkout_inventory_available\")\n */\nexport interface RuleIdExecutionContext {\n ruleId: string // String identifier (e.g., \"workflow_checkout_inventory_available\")\n data: any\n user?: {\n id?: string\n email?: string\n role?: string\n [key: string]: any\n }\n tenantId: string\n organizationId: string\n executedBy?: string\n dryRun?: boolean\n entityType?: string\n entityId?: string\n eventType?: string\n}\n\n/**\n * Execute a function with a timeout\n */\nasync function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage: string\n): Promise<T> {\n let timeoutId: NodeJS.Timeout\n\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`))\n }, timeoutMs)\n })\n\n try {\n return await Promise.race([promise, timeoutPromise])\n } finally {\n clearTimeout(timeoutId!)\n }\n}\n\n/**\n * Execute all applicable rules for the given context\n */\nexport async function executeRules(\n em: EntityManager,\n context: RuleEngineContext,\n options: RuleEngineExecutionOptions = {}\n): Promise<RuleEngineResult> {\n // Validate input\n const validation = ruleEngineContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: 0,\n errors: validationErrors,\n }\n }\n\n const startTime = Date.now()\n const executedRules: RuleExecutionResult[] = []\n const errors: string[] = []\n const logIds: string[] = []\n\n try {\n // Discover applicable rules\n const rules = await findApplicableRules(em, {\n entityType: context.entityType,\n eventType: context.eventType,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n }, { cache: options.cache })\n\n // Check rule count limit\n if (rules.length > MAX_RULES_PER_EXECUTION) {\n errors.push(\n `Rule count limit exceeded: ${rules.length} rules found, maximum is ${MAX_RULES_PER_EXECUTION}`\n )\n return {\n allowed: false,\n executedRules: [],\n totalExecutionTime: Date.now() - startTime,\n errors,\n }\n }\n\n // Rules already sorted by database query (priority DESC, ruleId ASC)\n // Execute each rule with total timeout\n const executionPromise = (async () => {\n for (const rule of rules) {\n try {\n const ruleResult = await executeSingleRule(em, rule, context, options)\n executedRules.push(ruleResult)\n\n if (ruleResult.logId) {\n logIds.push(ruleResult.logId)\n }\n\n if (ruleResult.error) {\n errors.push(\n `Rule execution failed [ruleId=${rule.ruleId}, type=${rule.ruleType}, entityType=${context.entityType}]: ${ruleResult.error}`\n )\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(\n `Unexpected error in rule execution [ruleId=${rule.ruleId}, type=${rule.ruleType}]: ${errorMessage}`\n )\n\n if (!context.dryRun) {\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n executedRules.push({\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: 0,\n error: errorMessage,\n })\n }\n }\n })()\n\n // Execute with timeout\n await withTimeout(\n executionPromise,\n MAX_TOTAL_EXECUTION_TIMEOUT_MS,\n `Total rule execution timeout [entityType=${context.entityType}]`\n )\n\n // Determine overall allowed status\n // For GUARD rules: all must pass for operation to be allowed\n const guardRules = executedRules.filter((r) => r.rule.ruleType === RULE_TYPE_GUARD)\n const allowed = guardRules.length === 0 || guardRules.every((r) => r.conditionResult)\n\n const totalExecutionTime = Date.now() - startTime\n\n return {\n allowed,\n executedRules,\n totalExecutionTime,\n errors: errors.length > 0 ? errors : undefined,\n logIds: logIds.length > 0 ? logIds : undefined,\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n errors.push(\n `Critical rule engine error [entityType=${context.entityType}, entityId=${context.entityId || 'unknown'}]: ${errorMessage}${stack ? `\\nStack: ${stack}` : ''}`\n )\n\n const totalExecutionTime = Date.now() - startTime\n\n return {\n allowed: false,\n executedRules,\n totalExecutionTime,\n errors,\n }\n }\n}\n\n/**\n * Execute a single rule\n */\nexport async function executeSingleRule(\n em: EntityManager,\n rule: BusinessRule,\n context: RuleEngineContext,\n options: RuleEngineExecutionOptions = {}\n): Promise<RuleExecutionResult> {\n const startTime = Date.now()\n\n try {\n // Wrap execution in timeout\n const executeWithTimeout = async () => {\n // Build evaluation context\n const evalContext: RuleEvaluationContext = {\n entityType: context.entityType,\n entityId: context.entityId,\n eventType: context.eventType,\n user: context.user,\n tenant: context.tenant,\n organization: context.organization,\n }\n\n // Evaluate rule conditions\n const result = await ruleEvaluator.evaluateSingleRule(rule, context.data, evalContext)\n\n // Check if evaluation completed (not just if conditions passed)\n if (!result.evaluationCompleted) {\n const executionTime = Date.now() - startTime\n\n let logId: string | undefined\n\n // Log failure if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: result.error,\n })\n\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage: result.error ?? null,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n return {\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: result.error,\n logId,\n }\n }\n\n // Evaluation completed successfully - determine which actions to execute\n const actions = result.conditionsPassed ? rule.successActions : rule.failureActions\n\n let actionsExecuted: ActionExecutionOutcome | null = null\n\n if (actions && Array.isArray(actions) && actions.length > 0) {\n // Build action context\n const actionContext: ActionContext = {\n ...evalContext,\n data: context.data,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy ?? context.user?.id ?? null,\n em: em as any,\n }\n\n // Execute actions\n actionsExecuted = await actionExecutor.executeActions(actions, actionContext)\n }\n\n const executionTime = Date.now() - startTime\n\n let logId: string | undefined\n\n // Log execution if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: result.conditionsPassed,\n actionsExecuted,\n executionTime,\n })\n }\n\n return {\n rule,\n conditionResult: result.conditionsPassed,\n actionsExecuted,\n executionTime,\n logId,\n }\n }\n\n // Execute with single rule timeout\n return await withTimeout(\n executeWithTimeout(),\n MAX_SINGLE_RULE_TIMEOUT_MS,\n `Single rule execution timeout [ruleId=${rule.ruleId}]`\n )\n } catch (error) {\n const executionTime = Date.now() - startTime\n const errorMessage = error instanceof Error ? error.message : String(error)\n const enhancedError = `Failed to execute rule [ruleId=${rule.ruleId}, entityType=${context.entityType}]: ${errorMessage}`\n\n let logId: string | undefined\n\n // Log error if not dry run\n if (!context.dryRun) {\n logId = await logRuleExecution(em, {\n rule,\n context,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: enhancedError,\n })\n\n await emitRuleExecutionFailed(options.eventBus, {\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n entityType: context.entityType ?? null,\n errorMessage: enhancedError,\n tenantId: context.tenantId,\n organizationId: context.organizationId ?? null,\n })\n }\n\n return {\n rule,\n conditionResult: false,\n actionsExecuted: null,\n executionTime,\n error: enhancedError,\n logId,\n }\n }\n}\n\n/**\n * Find all applicable rules for the given criteria\n */\nexport async function findApplicableRules(\n em: EntityManager,\n options: RuleDiscoveryOptions,\n cacheOptions: RuleDiscoveryCacheOptions = {},\n): Promise<BusinessRule[]> {\n // Validate input\n ruleDiscoveryOptionsSchema.parse(options)\n\n const { entityType, eventType, tenantId, organizationId, ruleType } = options\n const baseWhere: Record<string, unknown> = {\n entityType,\n tenantId,\n organizationId,\n enabled: true,\n deletedAt: null,\n }\n\n if (eventType) {\n baseWhere.eventType = eventType\n }\n\n if (ruleType) {\n baseWhere.ruleType = ruleType\n }\n\n const cached = await getCachedRuleDiscovery(cacheOptions.cache, options)\n let rules: BusinessRule[]\n\n if (cached) {\n if (cached.ruleIds.length === 0) {\n rules = []\n } else {\n const cachedRules = await em.find(BusinessRule, {\n ...baseWhere,\n id: { $in: cached.ruleIds },\n }, {\n orderBy: { priority: 'DESC', ruleId: 'ASC' },\n } as any)\n const byId = new Map(cachedRules.map((rule) => [rule.id, rule]))\n rules = cached.ruleIds\n .map((id) => byId.get(id))\n .filter((rule): rule is BusinessRule => Boolean(rule))\n }\n } else {\n rules = await em.find(BusinessRule, baseWhere as Partial<BusinessRule>, {\n orderBy: { priority: 'DESC', ruleId: 'ASC' },\n })\n\n await cacheRuleDiscovery(cacheOptions.cache, options, rules)\n }\n\n // Filter by effective date range\n const now = new Date()\n return rules.filter((rule) => {\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return false\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return false\n }\n return true\n })\n}\n\n/**\n * Execute a specific rule by its database UUID\n * This bypasses the entityType/eventType discovery mechanism and directly executes the rule\n */\nexport async function executeRuleById(\n em: EntityManager,\n context: DirectRuleExecutionContext\n): Promise<DirectRuleExecutionResult> {\n const startTime = Date.now()\n\n // Validate input\n const validation = directRuleExecutionContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Validation failed: ${validationErrors.join(', ')}`,\n }\n }\n\n // Fetch rule by ID with tenant/org validation\n const rule = await em.findOne(BusinessRule, {\n id: context.ruleId,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (!rule) {\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule not found',\n }\n }\n\n if (!rule.enabled) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n const now = new Date()\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,\n }\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,\n }\n }\n\n // Build RuleEngineContext (use provided entityType or fall back to rule's)\n const engineContext: RuleEngineContext = {\n entityType: context.entityType || rule.entityType,\n entityId: context.entityId,\n eventType: context.eventType || rule.eventType || undefined,\n data: context.data,\n user: context.user,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy,\n dryRun: context.dryRun,\n }\n\n // Execute via existing executeSingleRule\n const result = await executeSingleRule(em, rule, engineContext)\n\n return {\n success: !result.error,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: result.conditionResult,\n actionsExecuted: result.actionsExecuted,\n executionTime: result.executionTime,\n error: result.error,\n logId: result.logId,\n }\n}\n\n/**\n * Execute a rule by its string rule_id identifier\n * Looks up rule by rule_id (string column) + tenant_id (unique constraint)\n * This is useful for workflow conditions that reference rules by their string identifiers\n */\nexport async function executeRuleByRuleId(\n em: EntityManager,\n context: RuleIdExecutionContext\n): Promise<DirectRuleExecutionResult> {\n const startTime = Date.now()\n\n // Validate input\n const validation = ruleIdExecutionContextSchema.safeParse(context)\n if (!validation.success) {\n const validationErrors = validation.error.issues.map(e => `${e.path.join('.')}: ${e.message}`)\n return {\n success: false,\n ruleId: context.ruleId || 'unknown',\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Validation failed: ${validationErrors.join(', ')}`,\n }\n }\n\n // Fetch rule by rule_id (string identifier) + tenant/org\n const rule = await em.findOne(BusinessRule, {\n ruleId: context.ruleId, // String identifier column\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n deletedAt: null,\n })\n\n if (!rule) {\n return {\n success: false,\n ruleId: context.ruleId,\n ruleName: 'Unknown',\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule not found',\n }\n }\n\n if (!rule.enabled) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: 'Rule is disabled',\n }\n }\n\n // Check effective date range\n const now = new Date()\n if (rule.effectiveFrom && rule.effectiveFrom > now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule is not yet effective (starts ${rule.effectiveFrom.toISOString()})`,\n }\n }\n if (rule.effectiveTo && rule.effectiveTo < now) {\n return {\n success: false,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: false,\n actionsExecuted: null,\n executionTime: Date.now() - startTime,\n error: `Rule has expired (ended ${rule.effectiveTo.toISOString()})`,\n }\n }\n\n // Build RuleEngineContext (use provided entityType or fall back to rule's)\n const engineContext: RuleEngineContext = {\n entityType: context.entityType || rule.entityType,\n entityId: context.entityId,\n eventType: context.eventType || rule.eventType || undefined,\n data: context.data,\n user: context.user,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy,\n dryRun: context.dryRun,\n }\n\n // Execute via existing executeSingleRule\n const result = await executeSingleRule(em, rule, engineContext)\n\n return {\n success: !result.error,\n ruleId: rule.ruleId,\n ruleName: rule.ruleName,\n conditionResult: result.conditionResult,\n actionsExecuted: result.actionsExecuted,\n executionTime: result.executionTime,\n error: result.error,\n logId: result.logId,\n }\n}\n\n/**\n * Sensitive field patterns to exclude from logs\n */\nconst SENSITIVE_FIELD_PATTERNS = [\n /password/i,\n /passwd/i,\n /pwd/i,\n /secret/i,\n /token/i,\n /api[_-]?key/i,\n /auth/i,\n /credit[_-]?card/i,\n /card[_-]?number/i,\n /cvv/i,\n /ssn/i,\n /social[_-]?security/i,\n /tax[_-]?id/i,\n /driver[_-]?license/i,\n /passport/i,\n]\n\n/**\n * Maximum depth for nested object sanitization\n */\nconst MAX_SANITIZATION_DEPTH = 5\n\n/**\n * Sanitize data for logging by removing sensitive fields\n */\nfunction sanitizeForLogging(data: any, depth: number = 0): any {\n // Prevent infinite recursion\n if (depth > MAX_SANITIZATION_DEPTH) {\n return '[Max depth exceeded]'\n }\n\n // Handle null/undefined\n if (data === null || data === undefined) {\n return data\n }\n\n // Handle primitives\n if (typeof data !== 'object') {\n return data\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n return data.map(item => sanitizeForLogging(item, depth + 1))\n }\n\n // Handle objects\n const sanitized: Record<string, any> = {}\n\n for (const [key, value] of Object.entries(data)) {\n // Check if field name matches sensitive patterns\n const isSensitive = SENSITIVE_FIELD_PATTERNS.some(pattern => pattern.test(key))\n\n if (isSensitive) {\n sanitized[key] = '[REDACTED]'\n } else if (typeof value === 'object' && value !== null) {\n sanitized[key] = sanitizeForLogging(value, depth + 1)\n } else {\n sanitized[key] = value\n }\n }\n\n return sanitized\n}\n\n/**\n * Sanitize user object for logging (keep only safe fields)\n */\nfunction sanitizeUser(user: any): any {\n if (!user) {\n return undefined\n }\n\n // Only log safe user fields\n return {\n id: user.id,\n role: user.role,\n // Don't log: email, name, phone, address, etc.\n }\n}\n\n/**\n * Log rule execution to database\n */\ninterface LogExecutionOptions {\n rule: BusinessRule\n context: RuleEngineContext\n conditionResult: boolean\n actionsExecuted: ActionExecutionOutcome | null\n executionTime: number\n error?: string\n}\n\nexport async function logRuleExecution(\n em: EntityManager,\n options: LogExecutionOptions\n): Promise<string> {\n const { rule, context, conditionResult, actionsExecuted, executionTime, error } = options\n\n const executionResult: 'SUCCESS' | 'FAILURE' | 'ERROR' = error\n ? EXECUTION_RESULT_ERROR\n : conditionResult\n ? EXECUTION_RESULT_SUCCESS\n : EXECUTION_RESULT_FAILURE\n\n const log = em.create(RuleExecutionLog, {\n rule,\n entityId: context.entityId || DEFAULT_ENTITY_ID,\n entityType: context.entityType,\n executionResult,\n inputContext: {\n data: sanitizeForLogging(context.data),\n eventType: context.eventType,\n user: sanitizeUser(context.user),\n },\n outputContext: actionsExecuted\n ? {\n conditionResult,\n actionsExecuted: actionsExecuted.results.map((r) => ({\n type: r.action.type,\n success: r.success,\n error: r.error,\n })),\n }\n : null,\n errorMessage: error || null,\n executionTimeMs: executionTime,\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n executedBy: context.executedBy || null,\n })\n\n await em.persist(log).flush()\n\n return String(log.id)\n}\n\nasync function emitRuleExecutionFailed(\n eventBus: Pick<EventBus, 'emitEvent'> | null | undefined,\n payload: RuleExecutionFailedPayload\n): Promise<void> {\n if (!eventBus?.emitEvent) return\n\n await eventBus.emitEvent('business_rules.rule.execution_failed', payload).catch(() => undefined)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["executionTime", "logId"]
|
|
7
7
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getCommunicationChannelsQueue
|
|
9
9
|
} from "../../../../lib/queue.js";
|
|
10
10
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
11
|
+
import { readBoundedRequestBody, WebhookBodyTooLargeError } from "@open-mercato/shared/lib/webhooks";
|
|
11
12
|
const logger = createLogger("communication_channels").child({ component: "inbound-webhook" });
|
|
12
13
|
const metadata = {
|
|
13
14
|
path: "/communication_channels/webhook/[provider]",
|
|
@@ -31,7 +32,15 @@ async function POST(req, { params }) {
|
|
|
31
32
|
{ status: 404 }
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
let rawBody;
|
|
36
|
+
try {
|
|
37
|
+
rawBody = await readBoundedRequestBody(req);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof WebhookBodyTooLargeError) {
|
|
40
|
+
return NextResponse.json({ error: "Webhook payload too large" }, { status: 413 });
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
35
44
|
const headers = {};
|
|
36
45
|
req.headers.forEach((value, key) => {
|
|
37
46
|
headers[key] = value;
|
|
@@ -162,6 +171,7 @@ const openApi = {
|
|
|
162
171
|
responses: [
|
|
163
172
|
{ status: 202, description: "Webhook accepted for async processing" },
|
|
164
173
|
{ status: 401, description: "Signature verification failed against every candidate channel" },
|
|
174
|
+
{ status: 413, description: "Webhook payload too large" },
|
|
165
175
|
{ status: 404, description: "Unknown provider" }
|
|
166
176
|
]
|
|
167
177
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/communication_channels/api/post/webhook/%5Bprovider%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CommunicationChannel } from '../../../../data/entities'\nimport { getChannelAdapterRegistry } from '../../../../lib/adapter-registry-singleton'\nimport {\n COMMUNICATION_CHANNELS_QUEUES,\n getCommunicationChannelsQueue,\n} from '../../../../lib/queue'\nimport type { InboundMessage } from '../../../../lib/adapter'\nimport type { InboundProcessorPayload } from '../../../../workers/inbound-processor'\nimport type { ReactionInboundJob } from '../../../../lib/reaction-processor-types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'inbound-webhook' })\n\n/**\n * Inbound webhook endpoint for the communication_channels hub.\n *\n * One endpoint per provider \u2014 provider key in the path. The hub iterates all\n * candidate `CommunicationChannel` rows for `(provider_key, is_active=true,\n * deleted_at IS NULL)` across tenants and asks each adapter to verify the\n * signature with that channel's credentials. The first successful verification\n * pins the request to that channel's tenant scope; we then enqueue an inbound\n * processor job and return 202.\n *\n * Fail-closed: if no candidate verifies, we return 401 \u2014 never 200. This mirrors\n * the per-tenant authentication model used by `shipping_carriers/api/webhook/[provider]`\n * and the security note in SPEC-045d \u00A713.\n *\n * No auth required at the route level \u2014 signature verification IS the auth.\n */\nexport const metadata = {\n path: '/communication_channels/webhook/[provider]',\n POST: {\n requireAuth: false,\n // Unauthenticated by design (signature verification IS the auth), but the\n // handler fans out an O(N) cross-tenant candidate scan, so bound per-IP\n // request volume to limit abuse. Generous enough for real provider traffic;\n // the dedicated gmail route carries its own matching limits.\n rateLimit: { points: 120, duration: 60, keyPrefix: 'cc_webhook_inbound' },\n },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\nexport async function POST(req: Request, { params }: RouteContext): Promise<Response> {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n\n const registry = getChannelAdapterRegistry()\n const adapter = registry?.get(providerKey)\n if (!adapter) {\n return NextResponse.json(\n { error: `No ChannelAdapter for provider: ${providerKey}` },\n { status: 404 },\n )\n }\n\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAEnC,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAIP,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CommunicationChannel } from '../../../../data/entities'\nimport { getChannelAdapterRegistry } from '../../../../lib/adapter-registry-singleton'\nimport {\n COMMUNICATION_CHANNELS_QUEUES,\n getCommunicationChannelsQueue,\n} from '../../../../lib/queue'\nimport type { InboundMessage } from '../../../../lib/adapter'\nimport type { InboundProcessorPayload } from '../../../../workers/inbound-processor'\nimport type { ReactionInboundJob } from '../../../../lib/reaction-processor-types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('communication_channels').child({ component: 'inbound-webhook' })\n\n/**\n * Inbound webhook endpoint for the communication_channels hub.\n *\n * One endpoint per provider \u2014 provider key in the path. The hub iterates all\n * candidate `CommunicationChannel` rows for `(provider_key, is_active=true,\n * deleted_at IS NULL)` across tenants and asks each adapter to verify the\n * signature with that channel's credentials. The first successful verification\n * pins the request to that channel's tenant scope; we then enqueue an inbound\n * processor job and return 202.\n *\n * Fail-closed: if no candidate verifies, we return 401 \u2014 never 200. This mirrors\n * the per-tenant authentication model used by `shipping_carriers/api/webhook/[provider]`\n * and the security note in SPEC-045d \u00A713.\n *\n * No auth required at the route level \u2014 signature verification IS the auth.\n */\nexport const metadata = {\n path: '/communication_channels/webhook/[provider]',\n POST: {\n requireAuth: false,\n // Unauthenticated by design (signature verification IS the auth), but the\n // handler fans out an O(N) cross-tenant candidate scan, so bound per-IP\n // request volume to limit abuse. Generous enough for real provider traffic;\n // the dedicated gmail route carries its own matching limits.\n rateLimit: { points: 120, duration: 60, keyPrefix: 'cc_webhook_inbound' },\n },\n}\n\ntype RouteContext = {\n params: Promise<{ provider: string }> | { provider: string }\n}\n\nexport async function POST(req: Request, { params }: RouteContext): Promise<Response> {\n const resolvedParams = await params\n const providerKey = resolvedParams.provider\n\n const registry = getChannelAdapterRegistry()\n const adapter = registry?.get(providerKey)\n if (!adapter) {\n return NextResponse.json(\n { error: `No ChannelAdapter for provider: ${providerKey}` },\n { status: 404 },\n )\n }\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req)\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })\n }\n throw error\n }\n const headers: Record<string, string> = {}\n req.headers.forEach((value, key) => {\n headers[key] = value\n })\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n type CredentialsServiceLike = {\n resolve: (\n providerOrIntegrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n }\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = container.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n\n try {\n // The webhook is unauthenticated. The TENANT MUST be derived from a channel whose\n // per-tenant credentials successfully verify the inbound signature \u2014 NEVER from\n // attacker-controlled payload headers or unsigned retries.\n const candidates = await findWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey,\n isActive: true,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'desc' } },\n )\n\n let matchedChannel: CommunicationChannel | null = null\n let matchedScope: { tenantId: string; organizationId: string } | null = null\n let event: InboundMessage | null = null\n let lastVerificationError: unknown = null\n\n for (const candidate of candidates as CommunicationChannel[]) {\n const candidateScope = {\n tenantId: candidate.tenantId,\n organizationId: candidate.organizationId ?? candidate.tenantId,\n }\n // Per-user credential lookup: each candidate channel has its own user\n // and therefore its own credentials row. See review R2-C1 / N1.\n const credentialsLookupScope = {\n ...candidateScope,\n userId: candidate.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (candidate.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${providerKey}`, credentialsLookupScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n try {\n event = await adapter.verifyWebhook({\n rawBody,\n headers,\n credentials,\n scope: candidateScope,\n })\n matchedChannel = candidate\n matchedScope = candidateScope\n break\n } catch (error: unknown) {\n lastVerificationError = error\n }\n }\n\n if (!event || !matchedChannel || !matchedScope) {\n throw (\n lastVerificationError ?? new Error('Webhook verification failed: no matching channel')\n )\n }\n\n // Dispatch by event type:\n // - 'message' (default) \u2192 inbound-processor (slice 2b)\n // - 'reaction' \u2192 reaction-processor (slice 2d)\n // - 'status_update' / 'other' \u2192 202 not handled (future slice)\n if (event.eventType === 'reaction') {\n if (typeof adapter.normalizeInboundReaction !== 'function') {\n return NextResponse.json(\n {\n received: true,\n queued: false,\n reason: `adapter '${providerKey}' does not implement normalizeInboundReaction`,\n },\n { status: 202 },\n )\n }\n const reactionEvent = await adapter.normalizeInboundReaction(event)\n const reactionJob: ReactionInboundJob = {\n kind: 'inbound',\n providerKey,\n channelId: matchedChannel.id,\n channelType: matchedChannel.channelType,\n event: reactionEvent,\n // Use the channel's REAL org (null when null), matching the poll and\n // dedicated gmail webhook path \u2014 `candidateScope` falls org\n // back to tenantId for credential/verify lookups, which must not leak\n // into the ingest scope (it would diverge dedup for null-org channels).\n scope: { tenantId: matchedScope.tenantId, organizationId: matchedChannel.organizationId ?? null },\n attempt: 1,\n }\n const reactionsQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.reactions)\n await reactionsQueue.enqueue(reactionJob as unknown as Record<string, unknown>)\n return NextResponse.json({ received: true, queued: true, kind: 'reaction' }, { status: 202 })\n }\n\n if (event.eventType && event.eventType !== 'message') {\n return NextResponse.json(\n {\n received: true,\n queued: false,\n reason: `event ${event.eventType} not yet handled`,\n },\n { status: 202 },\n )\n }\n\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.inbound)\n const jobPayload: InboundProcessorPayload = {\n providerKey,\n channelId: matchedChannel.id,\n channelType: matchedChannel.channelType,\n raw: event,\n // Ingest scope uses the channel's REAL org (null when null) so dedup matches\n // the poll + dedicated webhook paths; see the reaction branch above.\n scope: { tenantId: matchedScope.tenantId, organizationId: matchedChannel.organizationId ?? null },\n }\n await queue.enqueue(jobPayload as unknown as Record<string, unknown>)\n\n return NextResponse.json({ received: true, queued: true, kind: 'message' }, { status: 202 })\n } catch (error: unknown) {\n // Do not echo adapter/verification internals to an unauthenticated caller;\n // log the detail server-side and return a fixed, minimal message.\n logger.warn(\n 'inbound webhook verification failed',\n { err: error },\n )\n return NextResponse.json({ error: 'verification_failed' }, { status: 401 })\n }\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n summary: 'Receive a communication channel webhook',\n methods: {\n POST: {\n summary: 'Process an inbound channel webhook (Slack, WhatsApp, Email, ...)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 202, description: 'Webhook accepted for async processing' },\n { status: 401, description: 'Signature verification failed against every candidate channel' },\n { status: 413, description: 'Webhook payload too large' },\n { status: 404, description: 'Unknown provider' },\n ],\n },\n },\n}\nexport default POST\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAEnC,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAIP,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,gCAAgC;AAEjE,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AAkBrF,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,IAKb,WAAW,EAAE,QAAQ,KAAK,UAAU,IAAI,WAAW,qBAAqB;AAAA,EAC1E;AACF;AAMA,eAAsB,KAAK,KAAc,EAAE,OAAO,GAAoC;AACpF,QAAM,iBAAiB,MAAM;AAC7B,QAAM,cAAc,eAAe;AAEnC,QAAM,WAAW,0BAA0B;AAC3C,QAAM,UAAU,UAAU,IAAI,WAAW;AACzC,MAAI,CAAC,SAAS;AACZ,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,mCAAmC,WAAW,GAAG;AAAA,MAC1D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,uBAAuB,GAAG;AAAA,EAC5C,SAAS,OAAO;AACd,QAAI,iBAAiB,0BAA0B;AAC7C,aAAO,aAAa,KAAK,EAAE,OAAO,4BAA4B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClF;AACA,UAAM;AAAA,EACR;AACA,QAAM,UAAkC,CAAC;AACzC,MAAI,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AAClC,YAAQ,GAAG,IAAI;AAAA,EACjB,CAAC;AAED,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAQ3D,MAAI,qBAAoD;AACxD,MAAI;AACF,yBAAqB,UAAU,QAAgC,+BAA+B;AAAA,EAChG,QAAQ;AACN,yBAAqB;AAAA,EACvB;AAEA,MAAI;AAIF,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,EAAE,SAAS,EAAE,WAAW,OAAO,EAAE;AAAA,IACnC;AAEA,QAAI,iBAA8C;AAClD,QAAI,eAAoE;AACxE,QAAI,QAA+B;AACnC,QAAI,wBAAiC;AAErC,eAAW,aAAa,YAAsC;AAC5D,YAAM,iBAAiB;AAAA,QACrB,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU,kBAAkB,UAAU;AAAA,MACxD;AAGA,YAAM,yBAAyB;AAAA,QAC7B,GAAG;AAAA,QACH,QAAQ,UAAU,UAAU;AAAA,MAC9B;AACA,UAAI,cAAuC,CAAC;AAC5C,UAAI,UAAU,kBAAkB,oBAAoB;AAClD,YAAI;AACF,wBACG,MAAM,mBAAmB,QAAQ,WAAW,WAAW,IAAI,sBAAsB,KAAM,CAAC;AAAA,QAC7F,QAAQ;AACN,wBAAc,CAAC;AAAA,QACjB;AAAA,MACF;AACA,UAAI;AACF,gBAAQ,MAAM,QAAQ,cAAc;AAAA,UAClC;AAAA,UACA;AAAA,UACA;AAAA,UACA,OAAO;AAAA,QACT,CAAC;AACD,yBAAiB;AACjB,uBAAe;AACf;AAAA,MACF,SAAS,OAAgB;AACvB,gCAAwB;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,cAAc;AAC9C,YACE,yBAAyB,IAAI,MAAM,kDAAkD;AAAA,IAEzF;AAMA,QAAI,MAAM,cAAc,YAAY;AAClC,UAAI,OAAO,QAAQ,6BAA6B,YAAY;AAC1D,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,QAAQ,YAAY,WAAW;AAAA,UACjC;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AACA,YAAM,gBAAgB,MAAM,QAAQ,yBAAyB,KAAK;AAClE,YAAM,cAAkC;AAAA,QACtC,MAAM;AAAA,QACN;AAAA,QACA,WAAW,eAAe;AAAA,QAC1B,aAAa,eAAe;AAAA,QAC5B,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,QAKP,OAAO,EAAE,UAAU,aAAa,UAAU,gBAAgB,eAAe,kBAAkB,KAAK;AAAA,QAChG,SAAS;AAAA,MACX;AACA,YAAM,iBAAiB,8BAA8B,8BAA8B,SAAS;AAC5F,YAAM,eAAe,QAAQ,WAAiD;AAC9E,aAAO,aAAa,KAAK,EAAE,UAAU,MAAM,QAAQ,MAAM,MAAM,WAAW,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC9F;AAEA,QAAI,MAAM,aAAa,MAAM,cAAc,WAAW;AACpD,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,QAAQ,SAAS,MAAM,SAAS;AAAA,QAClC;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,QAAQ,8BAA8B,8BAA8B,OAAO;AACjF,UAAM,aAAsC;AAAA,MAC1C;AAAA,MACA,WAAW,eAAe;AAAA,MAC1B,aAAa,eAAe;AAAA,MAC5B,KAAK;AAAA;AAAA;AAAA,MAGL,OAAO,EAAE,UAAU,aAAa,UAAU,gBAAgB,eAAe,kBAAkB,KAAK;AAAA,IAClG;AACA,UAAM,MAAM,QAAQ,UAAgD;AAEpE,WAAO,aAAa,KAAK,EAAE,UAAU,MAAM,QAAQ,MAAM,MAAM,UAAU,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC7F,SAAS,OAAgB;AAGvB,WAAO;AAAA,MACL;AAAA,MACA,EAAE,KAAK,MAAM;AAAA,IACf;AACA,WAAO,aAAa,KAAK,EAAE,OAAO,sBAAsB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5E;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,uBAAuB;AAAA,EAC9B,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,uBAAuB;AAAA,MAC9B,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,wCAAwC;AAAA,QACpE,EAAE,QAAQ,KAAK,aAAa,gEAAgE;AAAA,QAC5F,EAAE,QAAQ,KAAK,aAAa,4BAA4B;AAAA,QACxD,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AACF;AACA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
GmailPubSubJwtError
|
|
13
13
|
} from "../../../../lib/gmail-pubsub-jwt.js";
|
|
14
14
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
|
+
import { readBoundedRequestBody, WebhookBodyTooLargeError } from "@open-mercato/shared/lib/webhooks";
|
|
15
16
|
const logger = createLogger("communication_channels").child({ component: "gmail-webhook" });
|
|
16
17
|
const metadata = {
|
|
17
18
|
path: "/communication_channels/webhooks/gmail",
|
|
@@ -27,6 +28,15 @@ async function POST(req) {
|
|
|
27
28
|
logger.error("OM_GMAIL_PUBSUB_AUDIENCE / OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL not set");
|
|
28
29
|
return NextResponse.json({ error: "webhook not configured" }, { status: 503 });
|
|
29
30
|
}
|
|
31
|
+
let rawBody;
|
|
32
|
+
try {
|
|
33
|
+
rawBody = await readBoundedRequestBody(req);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof WebhookBodyTooLargeError) {
|
|
36
|
+
return NextResponse.json({ error: "Webhook payload too large" }, { status: 413 });
|
|
37
|
+
}
|
|
38
|
+
return NextResponse.json({ error: "unreadable_body" }, { status: 400 });
|
|
39
|
+
}
|
|
30
40
|
const verifier = getGmailPubSubVerifier();
|
|
31
41
|
try {
|
|
32
42
|
await verifier.verify({
|
|
@@ -41,12 +51,6 @@ async function POST(req) {
|
|
|
41
51
|
}
|
|
42
52
|
throw err;
|
|
43
53
|
}
|
|
44
|
-
let rawBody;
|
|
45
|
-
try {
|
|
46
|
-
rawBody = await req.text();
|
|
47
|
-
} catch {
|
|
48
|
-
return NextResponse.json({ error: "unreadable_body" }, { status: 400 });
|
|
49
|
-
}
|
|
50
54
|
let payload;
|
|
51
55
|
try {
|
|
52
56
|
payload = decodeGmailPubSubBody(rawBody);
|
|
@@ -108,6 +112,7 @@ const openApi = {
|
|
|
108
112
|
{ status: 400, description: "Body not a valid Pub/Sub envelope" },
|
|
109
113
|
{ status: 401, description: "Invalid JWT or email claim" },
|
|
110
114
|
{ status: 403, description: "Wrong audience" },
|
|
115
|
+
{ status: 413, description: "Webhook payload too large" },
|
|
111
116
|
{ status: 503, description: "Webhook not configured / Google certs unreachable" }
|
|
112
117
|
]
|
|
113
118
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/communication_channels/api/post/webhooks/gmail/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CommunicationChannel } from '../../../../data/entities'\nimport {\n COMMUNICATION_CHANNELS_QUEUES,\n getCommunicationChannelsQueue,\n} from '../../../../lib/queue'\nimport {\n decodeGmailPubSubBody,\n getGmailPubSubVerifier,\n GmailPubSubJwtError,\n} from '../../../../lib/gmail-pubsub-jwt'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'gmail-webhook' })\n\n/**\n * Spec C \u00A7 Phase C2 \u2014 Gmail Pub/Sub push webhook.\n *\n * Auth model: NOT authenticated via the platform's session cookie. Pub/Sub\n * authenticates with a Google-signed JWT in the `Authorization: Bearer \u2026`\n * header, which we verify against Google's public certs.\n *\n * Validation pipeline:\n * 1. Verify JWT signature, audience (`OM_GMAIL_PUBSUB_AUDIENCE`), and\n * email claim (`OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL`).\n *
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAEnC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CommunicationChannel } from '../../../../data/entities'\nimport {\n COMMUNICATION_CHANNELS_QUEUES,\n getCommunicationChannelsQueue,\n} from '../../../../lib/queue'\nimport {\n decodeGmailPubSubBody,\n getGmailPubSubVerifier,\n GmailPubSubJwtError,\n} from '../../../../lib/gmail-pubsub-jwt'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { readBoundedRequestBody, WebhookBodyTooLargeError } from '@open-mercato/shared/lib/webhooks'\n\nconst logger = createLogger('communication_channels').child({ component: 'gmail-webhook' })\n\n/**\n * Spec C \u00A7 Phase C2 \u2014 Gmail Pub/Sub push webhook.\n *\n * Auth model: NOT authenticated via the platform's session cookie. Pub/Sub\n * authenticates with a Google-signed JWT in the `Authorization: Bearer \u2026`\n * header, which we verify against Google's public certs.\n *\n * Validation pipeline:\n * 1. Bound the raw request body before cryptographic work.\n * 2. Verify JWT signature, audience (`OM_GMAIL_PUBSUB_AUDIENCE`), and\n * email claim (`OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL`).\n * 3. Decode the Pub/Sub envelope \u2192 `{ emailAddress, historyId }`.\n * 4. Look up every active Gmail channel matching `emailAddress`. Multiple\n * tenants may have the same mailbox connected \u2014 we enqueue one sync\n * job per matching channel (each tenant sees their own data).\n * 5. Return `204 No Content` even when no channels match \u2014 returning 4xx\n * would cause Pub/Sub to retry forever on a permanently-orphaned\n * registration.\n */\nexport const metadata = {\n path: '/communication_channels/webhooks/gmail',\n // Unauthenticated at the platform layer (a Google-signed JWT is the auth).\n // Rate-limited so a caller can't drive unbounded JWT-verification +\n // cert-fetch work before the signature gate rejects them.\n POST: { requireAuth: false, rateLimit: { points: 120, duration: 60, keyPrefix: 'cc_webhook_gmail' } },\n}\n\ntype GmailHistorySyncJobPayload = {\n channelId: string\n scope: { tenantId: string; organizationId: string | null }\n notification: { emailAddress: string; historyId: string }\n}\n\nexport async function POST(req: Request): Promise<Response> {\n const expectedAudience = process.env.OM_GMAIL_PUBSUB_AUDIENCE\n const expectedEmail = process.env.OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL\n if (!expectedAudience || !expectedEmail) {\n // Misconfiguration is operator-facing; log and 503 so Pub/Sub retries\n // briefly and the operator notices.\n logger.error('OM_GMAIL_PUBSUB_AUDIENCE / OM_GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL not set')\n return NextResponse.json({ error: 'webhook not configured' }, { status: 503 })\n }\n\n let rawBody: string\n try {\n rawBody = await readBoundedRequestBody(req)\n } catch (error) {\n if (error instanceof WebhookBodyTooLargeError) {\n return NextResponse.json({ error: 'Webhook payload too large' }, { status: 413 })\n }\n return NextResponse.json({ error: 'unreadable_body' }, { status: 400 })\n }\n\n const verifier = getGmailPubSubVerifier()\n try {\n await verifier.verify({\n authorizationHeader: req.headers.get('authorization'),\n expectedAudience,\n expectedEmail,\n })\n } catch (err) {\n if (err instanceof GmailPubSubJwtError) {\n const status =\n err.code === 'wrong_audience' ? 403 :\n err.code === 'fetch_certs_failed' ? 503 :\n 401\n return NextResponse.json({ error: err.code, message: err.message }, { status })\n }\n throw err\n }\n\n let payload: { emailAddress: string; historyId: string | number }\n try {\n payload = decodeGmailPubSubBody(rawBody)\n } catch (err) {\n return NextResponse.json(\n { error: err instanceof Error ? err.message : 'invalid_payload' },\n { status: 400 },\n )\n }\n\n // Channel lookup is intentionally NOT tenant-scoped here \u2014 we don't have a\n // tenant signal beyond the email address. We enumerate matching channels\n // across all tenants and enqueue one sync job per match. Each job carries\n // its own tenant scope, so downstream ingest stays tenant-isolated.\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n const channels = await findWithDecryption(\n em,\n CommunicationChannel,\n {\n providerKey: 'gmail',\n externalIdentifier: payload.emailAddress,\n isActive: true,\n deletedAt: null,\n },\n )\n if (channels.length === 0) {\n // Return 204 anyway \u2014 see route header comment.\n return new NextResponse(null, { status: 204 })\n }\n\n const queue = getCommunicationChannelsQueue(\n COMMUNICATION_CHANNELS_QUEUES.gmailHistorySync,\n )\n for (const channel of channels) {\n const job: GmailHistorySyncJobPayload = {\n channelId: channel.id,\n scope: {\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n },\n notification: {\n emailAddress: payload.emailAddress,\n historyId: String(payload.historyId),\n },\n }\n try {\n await queue.enqueue(job as unknown as Record<string, unknown>)\n } catch (err) {\n logger.error(\n 'failed to enqueue history-sync for channel',\n { channelId: channel.id, err },\n )\n }\n }\n\n return new NextResponse(null, { status: 204 })\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n POST: {\n summary: 'Gmail Pub/Sub push notification webhook (Spec C \u00A7 Phase C2)',\n tags: ['CommunicationChannels'],\n responses: [\n { status: 204, description: 'Notification verified + history-sync job enqueued' },\n { status: 400, description: 'Body not a valid Pub/Sub envelope' },\n { status: 401, description: 'Invalid JWT or email claim' },\n { status: 403, description: 'Wrong audience' },\n { status: 413, description: 'Webhook payload too large' },\n { status: 503, description: 'Webhook not configured / Google certs unreachable' },\n ],\n },\n },\n}\n\nexport default POST\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -61,6 +61,18 @@ const upgradeActions = [
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "payment_gateways.register-session-initialization-prune",
|
|
67
|
+
version: "0.6.6",
|
|
68
|
+
messageKey: "payment_gateways.upgradeActions.sessionInitializationPrune.message",
|
|
69
|
+
ctaKey: "payment_gateways.upgradeActions.sessionInitializationPrune.cta",
|
|
70
|
+
successKey: "payment_gateways.upgradeActions.sessionInitializationPrune.success",
|
|
71
|
+
loadingKey: "payment_gateways.upgradeActions.sessionInitializationPrune.loading",
|
|
72
|
+
run: async ({ container, tenantId, organizationId }) => {
|
|
73
|
+
const { registerSessionInitializationPruneSchedule } = await import("@open-mercato/core/modules/payment_gateways/setup");
|
|
74
|
+
await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId });
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
];
|
|
66
78
|
function actionsUpToVersion(version) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/configs/lib/upgrade-actions.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
|
|
5
|
-
"mappings": "AACA,YAAY,YAAY;AAExB,SAAS,oBAAoB;AAE7B,SAAS,wCAAwC;AAEjD,MAAM,SAAS,aAAa,SAAS,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AAyBtE,SAAS,gBAAgB,GAAW,GAAmB;AAC5D,QAAM,SAAS,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC;AAC5C,QAAM,SAAS,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC;AAC5C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,4BAA4B,CAAC,oDAAoD;AAAA,EACnG;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,4BAA4B,CAAC,oDAAoD;AAAA,EACnG;AACA,SAAO,OAAO,QAAQ,QAAQ,MAAM;AACtC;AAEO,MAAM,iBAA4C;AAAA,EACvD;AAAA,IACE,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM,IAAI,EAAE,WAAW,IAAI,SAAS,GAAG;AACrC,YAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,YAAM,SAAS,MAAM,iCAAiC,EAAE,IAAI,aAAa,SAAS,CAAC;AACnF,aAAO,KAAK,gDAAgD;AAAA,QAC1D;AAAA,QACA,SAAS,OAAO;AAAA,QAChB,SAAS,OAAO;AAAA,QAChB,YAAY,OAAO;AAAA,QACnB,gBAAgB,OAAO;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAKZ,KAAK,OAAO,EAAE,IAAI,UAAU,eAAe,MAAM;AAC/C,YAAM,CAAC,EAAE,4BAA4B,GAAG,EAAE,sBAAsB,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACrF,OAAO,0CAA0C;AAAA,QACjD,OAAO,sDAAsD;AAAA,MAC/D,CAAC;AACD,iBAAW,SAAS,6BAA6B;AAC/C,cAAM,sBAAsB,IAAI;AAAA,UAC9B;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,OAAO,MAAM;AAAA,UACb,OAAO,MAAM;AAAA,UACb,OAAO,MAAM;AAAA,UACb,MAAM,MAAM;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,SAA4C;AAC7E,SAAO,eACJ,OAAO,CAAC,WAAW,gBAAgB,OAAO,SAAS,OAAO,KAAK,CAAC,EAChE,KAAK,CAAC,GAAG,MAAM,gBAAgB,EAAE,SAAS,EAAE,OAAO,KAAK,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC;AACrF;AAEO,SAAS,kBAAkB,UAAkB,YAAyD;AAC3G,QAAM,UAAU,mBAAmB,UAAU,EAAE,OAAO,CAAC,WAAW,OAAO,OAAO,QAAQ;AACxF,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,QAAQ,QAAQ,SAAS,CAAC;AACnC;",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport * as semver from 'semver'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'\n\nconst logger = createLogger('configs').child({ component: 'upgrade-actions' })\n\nexport type UpgradeActionContext = {\n tenantId: string\n organizationId: string\n container: AppContainer\n em: EntityManager\n}\n\nexport type UpgradeActionDefinition = {\n id: string\n version: string\n messageKey: string\n ctaKey: string\n successKey: string\n loadingKey?: string\n run: (ctx: UpgradeActionContext) => Promise<void>\n}\n\n/**\n * Compare two semantic version strings.\n * Uses the semver library for robust version comparison.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n * Throws an error if either version string is invalid.\n */\nexport function compareVersions(a: string, b: string): number {\n const cleanA = semver.valid(semver.coerce(a))\n const cleanB = semver.valid(semver.coerce(b))\n if (!cleanA) {\n throw new Error(`Invalid version string: \"${a}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n if (!cleanB) {\n throw new Error(`Invalid version string: \"${b}\". Expected a valid semver format (e.g., \"1.2.3\").`)\n }\n return semver.compare(cleanA, cleanB)\n}\n\nexport const upgradeActions: UpgradeActionDefinition[] = [\n {\n id: 'attachments.reconcile-organization',\n version: '0.6.6',\n messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',\n ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',\n successKey: 'configs.upgrades.attachmentsOrgReconcile.success',\n loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',\n async run({ container, em, tenantId }) {\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })\n logger.info('attachments organization reconcile completed', {\n tenantId,\n scanned: report.scanned,\n updated: report.updated,\n unresolved: report.unresolved,\n skippedVirtual: report.skippedVirtual,\n })\n },\n },\n {\n id: 'customers.seed-interaction-statuses',\n version: '0.6.5',\n messageKey: 'customers.config.upgradeActions.interactionStatuses.message',\n ctaKey: 'customers.config.upgradeActions.interactionStatuses.cta',\n successKey: 'customers.config.upgradeActions.interactionStatuses.success',\n loadingKey: 'customers.config.upgradeActions.interactionStatuses.loading',\n // Existing tenants predate the `interaction_status` dictionary, so their status\n // dropdown is empty until seeded. New tenants get it via customers `seedDefaults`.\n // Lazy-imported so configs stays decoupled from customers (the catalog of actions\n // lives here, but customers code only loads when the action actually runs).\n run: async ({ em, tenantId, organizationId }) => {\n const [{ INTERACTION_STATUS_DEFAULTS }, { ensureDictionaryEntry }] = await Promise.all([\n import('@open-mercato/core/modules/customers/cli'),\n import('@open-mercato/core/modules/customers/commands/shared'),\n ])\n for (const entry of INTERACTION_STATUS_DEFAULTS) {\n await ensureDictionaryEntry(em, {\n tenantId,\n organizationId,\n kind: 'interaction_status',\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n })\n }\n },\n },\n {\n id: 'payment_gateways.register-session-initialization-prune',\n version: '0.6.6',\n messageKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.message',\n ctaKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.cta',\n successKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.success',\n loadingKey: 'payment_gateways.upgradeActions.sessionInitializationPrune.loading',\n run: async ({ container, tenantId, organizationId }) => {\n const { registerSessionInitializationPruneSchedule } = await import(\n '@open-mercato/core/modules/payment_gateways/setup'\n )\n await registerSessionInitializationPruneSchedule(container, { tenantId, organizationId })\n },\n },\n]\n\nexport function actionsUpToVersion(version: string): UpgradeActionDefinition[] {\n return upgradeActions // NOSONAR \u2014 upgradeActions is populated at boot time by modules\n .filter((action) => compareVersions(action.version, version) <= 0)\n .sort((a, b) => compareVersions(a.version, b.version) || a.id.localeCompare(b.id))\n}\n\nexport function findUpgradeAction(actionId: string, maxVersion: string): UpgradeActionDefinition | undefined {\n const matches = actionsUpToVersion(maxVersion).filter((action) => action.id === actionId)\n if (!matches.length) return undefined\n return matches[matches.length - 1]\n}\n"],
|
|
5
|
+
"mappings": "AACA,YAAY,YAAY;AAExB,SAAS,oBAAoB;AAE7B,SAAS,wCAAwC;AAEjD,MAAM,SAAS,aAAa,SAAS,EAAE,MAAM,EAAE,WAAW,kBAAkB,CAAC;AAyBtE,SAAS,gBAAgB,GAAW,GAAmB;AAC5D,QAAM,SAAS,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC;AAC5C,QAAM,SAAS,OAAO,MAAM,OAAO,OAAO,CAAC,CAAC;AAC5C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,4BAA4B,CAAC,oDAAoD;AAAA,EACnG;AACA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,4BAA4B,CAAC,oDAAoD;AAAA,EACnG;AACA,SAAO,OAAO,QAAQ,QAAQ,MAAM;AACtC;AAEO,MAAM,iBAA4C;AAAA,EACvD;AAAA,IACE,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,MAAM,IAAI,EAAE,WAAW,IAAI,SAAS,GAAG;AACrC,YAAM,cAAc,UAAU,QAAQ,aAAa;AACnD,YAAM,SAAS,MAAM,iCAAiC,EAAE,IAAI,aAAa,SAAS,CAAC;AACnF,aAAO,KAAK,gDAAgD;AAAA,QAC1D;AAAA,QACA,SAAS,OAAO;AAAA,QAChB,SAAS,OAAO;AAAA,QAChB,YAAY,OAAO;AAAA,QACnB,gBAAgB,OAAO;AAAA,MACzB,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAKZ,KAAK,OAAO,EAAE,IAAI,UAAU,eAAe,MAAM;AAC/C,YAAM,CAAC,EAAE,4BAA4B,GAAG,EAAE,sBAAsB,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,QACrF,OAAO,0CAA0C;AAAA,QACjD,OAAO,sDAAsD;AAAA,MAC/D,CAAC;AACD,iBAAW,SAAS,6BAA6B;AAC/C,cAAM,sBAAsB,IAAI;AAAA,UAC9B;AAAA,UACA;AAAA,UACA,MAAM;AAAA,UACN,OAAO,MAAM;AAAA,UACb,OAAO,MAAM;AAAA,UACb,OAAO,MAAM;AAAA,UACb,MAAM,MAAM;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,KAAK,OAAO,EAAE,WAAW,UAAU,eAAe,MAAM;AACtD,YAAM,EAAE,2CAA2C,IAAI,MAAM,OAC3D,mDACF;AACA,YAAM,2CAA2C,WAAW,EAAE,UAAU,eAAe,CAAC;AAAA,IAC1F;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,SAA4C;AAC7E,SAAO,eACJ,OAAO,CAAC,WAAW,gBAAgB,OAAO,SAAS,OAAO,KAAK,CAAC,EAChE,KAAK,CAAC,GAAG,MAAM,gBAAgB,EAAE,SAAS,EAAE,OAAO,KAAK,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC;AACrF;AAEO,SAAS,kBAAkB,UAAkB,YAAyD;AAC3G,QAAM,UAAU,mBAAmB,UAAU,EAAE,OAAO,CAAC,WAAW,OAAO,OAAO,QAAQ;AACxF,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,SAAO,QAAQ,QAAQ,SAAS,CAAC;AACnC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,7 +6,7 @@ import { CustomerUser, CustomerUserRole, CustomerRole, CustomerUserSession } fro
|
|
|
6
6
|
import { adminUpdateUserSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
7
7
|
import { emitCustomerAccountsEvent } from "@open-mercato/core/modules/customer_accounts/events";
|
|
8
8
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
9
|
-
import { isOwnedCompanyEntity } from "@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership";
|
|
9
|
+
import { isOwnedCompanyEntity, isOwnedPersonEntity } from "@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership";
|
|
10
10
|
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
11
11
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
12
12
|
const metadata = {};
|
|
@@ -136,6 +136,15 @@ async function PUT(req, { params }) {
|
|
|
136
136
|
return NextResponse.json({ ok: false, error: "Company not found" }, { status: 400 });
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
if (parsed.data.personEntityId) {
|
|
140
|
+
const owned = await isOwnedPersonEntity(em, parsed.data.personEntityId, {
|
|
141
|
+
tenantId: auth.tenantId,
|
|
142
|
+
organizationId: auth.orgId
|
|
143
|
+
});
|
|
144
|
+
if (!owned) {
|
|
145
|
+
return NextResponse.json({ ok: false, error: "Person not found" }, { status: 400 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
139
148
|
const nextUpdatedAt = /* @__PURE__ */ new Date();
|
|
140
149
|
const updates = { updatedAt: nextUpdatedAt };
|
|
141
150
|
if (parsed.data.displayName !== void 0) updates.displayName = parsed.data.displayName;
|