@open-mercato/core 0.10.1-develop.7749.1.ea3edf75e3 → 0.10.1-develop.7751.1.a11f57298b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/services/principalService.js +3 -2
  3. package/dist/modules/api_keys/services/principalService.js.map +2 -2
  4. package/dist/modules/api_keys/services/sessionKeyRoles.js +29 -0
  5. package/dist/modules/api_keys/services/sessionKeyRoles.js.map +7 -0
  6. package/dist/modules/auth/services/rbacService.js +18 -10
  7. package/dist/modules/auth/services/rbacService.js.map +2 -2
  8. package/dist/modules/business_rules/lib/action-executor.js +2 -1
  9. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  10. package/dist/modules/directory/utils/organizationScope.js +11 -4
  11. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  12. package/dist/modules/warranty_claims/commands/claim-lines.js +25 -1
  13. package/dist/modules/warranty_claims/commands/claim-lines.js.map +3 -3
  14. package/dist/modules/warranty_claims/commands/claims.js +19 -1
  15. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  16. package/dist/modules/warranty_claims/commands/shared.js +30 -0
  17. package/dist/modules/warranty_claims/commands/shared.js.map +2 -2
  18. package/package.json +7 -7
  19. package/src/modules/api_keys/services/principalService.ts +3 -2
  20. package/src/modules/api_keys/services/sessionKeyRoles.ts +35 -0
  21. package/src/modules/auth/services/rbacService.ts +22 -9
  22. package/src/modules/business_rules/lib/action-executor.ts +2 -3
  23. package/src/modules/directory/utils/organizationScope.ts +11 -4
  24. package/src/modules/warranty_claims/commands/claim-lines.ts +26 -1
  25. package/src/modules/warranty_claims/commands/claims.ts +19 -1
  26. package/src/modules/warranty_claims/commands/shared.ts +36 -1
  27. package/src/modules/warranty_claims/i18n/de.json +1 -0
  28. package/src/modules/warranty_claims/i18n/en.json +1 -0
  29. package/src/modules/warranty_claims/i18n/es.json +1 -0
  30. package/src/modules/warranty_claims/i18n/ko.json +1 -0
  31. package/src/modules/warranty_claims/i18n/pl.json +1 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/business_rules/lib/action-executor.ts"],
4
- "sourcesContent": ["import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { EvaluationContext } from './expression-evaluator'\nimport { getNestedValue, resolveSpecialValue } from './value-resolver'\nimport {\n canActorUseOpenMercatoApiKeyProfile,\n findOpenMercatoEndpointOption,\n getCurrentOpenMercatoEndpointOptions,\n resolveOpenMercatoApiKeyProfile,\n} from './openmercato-call-options'\nimport type { RbacService } from '../../auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenMercatoEndpointOption } from './openmercato-call-options-types'\n\n/**\n * Action definition\n */\nexport interface Action {\n type: string\n config?: Record<string, any>\n}\n\n/**\n * Action execution context\n */\nexport interface ActionContext extends EvaluationContext {\n entityType?: string\n entityId?: string\n eventType?: string\n data?: any\n ruleId?: string\n ruleName?: string\n tenantId?: string\n organizationId?: string\n executedBy?: string | null\n em?: PostgreSqlEntityManager\n openMercatoEndpointOptions?: OpenMercatoEndpointOption[]\n ruleAuthorId?: string | null\n rbacService?: RbacService\n [key: string]: any\n}\n\n/**\n * Specific action result types\n */\nexport interface AllowTransitionResult {\n type: 'ALLOW_TRANSITION'\n allowed: true\n message: string\n}\n\nexport interface BlockTransitionResult {\n type: 'BLOCK_TRANSITION'\n allowed: false\n message: string\n}\n\nexport interface LogResult {\n type: 'LOG'\n level: string\n message: string\n timestamp: string\n}\n\nexport interface ShowErrorResult {\n type: 'SHOW_ERROR'\n severity: 'error'\n message: string\n}\n\nexport interface ShowWarningResult {\n type: 'SHOW_WARNING'\n severity: 'warning'\n message: string\n}\n\nexport interface ShowInfoResult {\n type: 'SHOW_INFO'\n severity: 'info'\n message: string\n}\n\nexport interface NotifyResult {\n type: 'NOTIFY'\n recipients: string[]\n subject: string\n message: string\n template?: string\n}\n\nexport interface SetFieldResult {\n type: 'SET_FIELD'\n field: string\n value: any\n}\n\nexport interface CallWebhookResult {\n type: 'CALL_WEBHOOK'\n url: string\n method: string\n headers: Record<string, any>\n body?: any\n status: 'pending'\n}\n\nexport interface CallOpenMercatoResult {\n type: 'CALL_OPEN_MERCATO'\n endpoint: string\n method: string\n apiKeyId: string\n status: number\n statusText: string\n body: any\n authenticated: true\n tenantId: string\n organizationId: string\n}\n\nexport interface EmitEventResult {\n type: 'EMIT_EVENT'\n event: string\n payload: Record<string, any>\n}\n\n/**\n * Union type of all action results\n */\nexport type ActionHandlerResult =\n | AllowTransitionResult\n | BlockTransitionResult\n | LogResult\n | ShowErrorResult\n | ShowWarningResult\n | ShowInfoResult\n | NotifyResult\n | SetFieldResult\n | CallWebhookResult\n | CallOpenMercatoResult\n | EmitEventResult\n\n/**\n * Action execution result\n */\nexport interface ActionResult {\n action: Action\n success: boolean\n result?: ActionHandlerResult\n error?: string\n executionTime: number\n}\n\n/**\n * Action execution outcome (aggregated results)\n */\nexport interface ActionExecutionOutcome {\n success: boolean\n results: ActionResult[]\n totalTime: number\n errors?: string[]\n}\n\n/**\n * Execute multiple actions in sequence\n */\nexport async function executeActions(\n actions: Action[],\n context: ActionContext\n): Promise<ActionExecutionOutcome> {\n const startTime = Date.now()\n const results: ActionResult[] = []\n const errors: string[] = []\n\n for (const action of actions) {\n try {\n const result = await executeAction(action, context)\n results.push(result)\n\n if (result.error) {\n errors.push(`Action ${action.type}: ${result.error}`)\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(`Action ${action.type}: ${errorMessage}`)\n\n results.push({\n action,\n success: false,\n error: errorMessage,\n executionTime: 0,\n })\n }\n }\n\n const totalTime = Date.now() - startTime\n\n return {\n success: results.every((r) => r.success),\n results,\n totalTime,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Execute a single action\n */\nexport async function executeAction(action: Action, context: ActionContext): Promise<ActionResult> {\n const startTime = Date.now()\n\n try {\n const handler = getActionHandler(action.type)\n const result = await handler(action, context)\n\n const executionTime = Date.now() - startTime\n\n return {\n action,\n success: true,\n result,\n executionTime,\n }\n } catch (error) {\n const executionTime = Date.now() - startTime\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n return {\n action,\n success: false,\n error: errorMessage,\n executionTime,\n }\n }\n}\n\n/**\n * Action handler function type\n */\ntype ActionHandler = (action: Action, context: ActionContext) => Promise<ActionHandlerResult>\n\n/**\n * Get action handler by type\n */\nfunction getActionHandler(actionType: string): ActionHandler {\n const handlers: Record<string, ActionHandler> = {\n ALLOW_TRANSITION: handleAllowTransition,\n BLOCK_TRANSITION: handleBlockTransition,\n LOG: handleLog,\n SHOW_ERROR: handleShowError,\n SHOW_WARNING: handleShowWarning,\n SHOW_INFO: handleShowInfo,\n NOTIFY: handleNotify,\n SET_FIELD: handleSetField,\n CALL_WEBHOOK: handleCallWebhook,\n CALL_OPEN_MERCATO: handleCallOpenMercato,\n EMIT_EVENT: handleEmitEvent,\n }\n\n const handler = handlers[actionType]\n\n if (!handler) {\n throw new Error(`Unknown action type: ${actionType}`)\n }\n\n return handler\n}\n\n/**\n * ALLOW_TRANSITION action handler\n */\nasync function handleAllowTransition(\n action: Action,\n context: ActionContext\n): Promise<AllowTransitionResult> {\n return {\n type: 'ALLOW_TRANSITION',\n allowed: true,\n message: interpolateMessage(action.config?.message || 'Transition allowed', context),\n }\n}\n\n/**\n * BLOCK_TRANSITION action handler\n */\nasync function handleBlockTransition(\n action: Action,\n context: ActionContext\n): Promise<BlockTransitionResult> {\n return {\n type: 'BLOCK_TRANSITION',\n allowed: false,\n message: interpolateMessage(action.config?.message || 'Transition blocked', context),\n }\n}\n\n/**\n * LOG action handler\n */\nasync function handleLog(action: Action, context: ActionContext): Promise<LogResult> {\n const level = action.config?.level || 'info'\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'LOG',\n level,\n message,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * SHOW_ERROR action handler\n */\nasync function handleShowError(action: Action, context: ActionContext): Promise<ShowErrorResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_ERROR',\n severity: 'error',\n message,\n }\n}\n\n/**\n * SHOW_WARNING action handler\n */\nasync function handleShowWarning(\n action: Action,\n context: ActionContext\n): Promise<ShowWarningResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_WARNING',\n severity: 'warning',\n message,\n }\n}\n\n/**\n * SHOW_INFO action handler\n */\nasync function handleShowInfo(action: Action, context: ActionContext): Promise<ShowInfoResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_INFO',\n severity: 'info',\n message,\n }\n}\n\n/**\n * NOTIFY action handler\n */\nasync function handleNotify(action: Action, context: ActionContext): Promise<NotifyResult> {\n const recipients = action.config?.recipients || []\n const message = interpolateMessage(action.config?.message || '', context)\n const subject = interpolateMessage(action.config?.subject || '', context)\n const template = action.config?.template\n\n if (!Array.isArray(recipients)) {\n throw new Error('NOTIFY action requires recipients to be an array')\n }\n\n if (recipients.length === 0) {\n throw new Error('NOTIFY action requires at least one recipient')\n }\n\n return {\n type: 'NOTIFY',\n recipients,\n subject,\n message,\n template,\n }\n}\n\n/**\n * SET_FIELD action handler\n */\nasync function handleSetField(action: Action, context: ActionContext): Promise<SetFieldResult> {\n const field = action.config?.field\n const value = resolveValue(action.config?.value, context)\n\n if (!field) {\n throw new Error('SET_FIELD action requires a field name')\n }\n\n if (typeof field !== 'string' || field.trim() === '') {\n throw new Error('SET_FIELD action requires a non-empty field name')\n }\n\n return {\n type: 'SET_FIELD',\n field,\n value,\n }\n}\n\n/**\n * CALL_WEBHOOK action handler\n */\nasync function handleCallWebhook(\n action: Action,\n context: ActionContext\n): Promise<CallWebhookResult> {\n const url = interpolateMessage(action.config?.url || '', context)\n const method = action.config?.method || 'POST'\n const headers = action.config?.headers || {}\n const body = action.config?.body\n\n if (!url || url.trim() === '') {\n throw new Error('CALL_WEBHOOK action requires a non-empty URL')\n }\n\n const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n if (!validMethods.includes(method.toUpperCase())) {\n throw new Error(\n `CALL_WEBHOOK action requires a valid HTTP method (${validMethods.join(', ')})`\n )\n }\n\n return {\n type: 'CALL_WEBHOOK',\n url,\n method: method.toUpperCase(),\n headers,\n body,\n status: 'pending',\n }\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\nconst API_KEY_ACTOR_PREFIX = 'api_key:'\n\nasync function resolveRbacService(): Promise<RbacService> {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n return container.resolve('rbacService') as RbacService\n}\n\nasync function translatedOpenMercatoCallError(key: string, fallback: string): Promise<Error> {\n const { resolveTranslations } = await import('@open-mercato/shared/lib/i18n/server')\n const { t } = await resolveTranslations()\n return new Error(t(key, fallback))\n}\n\nasync function resolveActiveRuleAuthorId(\n em: PostgreSqlEntityManager,\n authorId: string | null | undefined,\n tenantId: string,\n): Promise<string | null> {\n const candidate = typeof authorId === 'string' ? authorId.trim() : ''\n if (!candidate) return null\n if (candidate.startsWith(API_KEY_ACTOR_PREFIX)) {\n const apiKeyId = candidate.slice(API_KEY_ACTOR_PREFIX.length)\n if (!UUID_PATTERN.test(apiKeyId)) return null\n const { ApiKey } = await import('../../api_keys/data/entities')\n const key = await findOneWithDecryption(em, ApiKey, { id: apiKeyId, tenantId, deletedAt: null }, {}, { tenantId, organizationId: null })\n if (!key) return null\n if (key.expiresAt && key.expiresAt.getTime() <= Date.now()) return null\n return candidate\n }\n if (!UUID_PATTERN.test(candidate)) return null\n const { User } = await import('../../auth/data/entities')\n const user = await findOneWithDecryption(em, User, { id: candidate, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n if (!user) return null\n if (user.tenantId && String(user.tenantId) === tenantId) return candidate\n const { isUserEffectivelySuperAdmin } = await import('../../auth/lib/grantChecks')\n return await isUserEffectivelySuperAdmin(em, candidate) ? candidate : null\n}\n\n/**\n * CALL_OPEN_MERCATO action handler\n */\nasync function handleCallOpenMercato(\n action: Action,\n context: ActionContext\n): Promise<CallOpenMercatoResult> {\n const endpoint = String(action.config?.endpoint ?? '').trim()\n const method = String(action.config?.method ?? '').trim().toUpperCase()\n const apiKeyId = String(action.config?.apiKeyId ?? '').trim()\n\n if (!endpoint) {\n throw new Error('CALL_OPEN_MERCATO action requires an endpoint')\n }\n if (!method) {\n throw new Error('CALL_OPEN_MERCATO action requires an HTTP method')\n }\n if (!apiKeyId) {\n throw new Error('CALL_OPEN_MERCATO action requires an API key profile')\n }\n\n const endpointOptions = context.openMercatoEndpointOptions ?? await getCurrentOpenMercatoEndpointOptions()\n const endpointOption = findOpenMercatoEndpointOption(\n endpoint,\n method,\n endpointOptions,\n )\n if (!endpointOption) {\n throw new Error('CALL_OPEN_MERCATO action requires a currently available /api/* endpoint')\n }\n\n const em = context.em\n if (!em) {\n throw new Error('CALL_OPEN_MERCATO action requires an EntityManager in action context')\n }\n\n const tenantId = context.tenantId ?? context.tenant?.id\n const organizationId = context.organizationId ?? context.organization?.id\n if (!tenantId || !organizationId) {\n throw new Error('CALL_OPEN_MERCATO action requires tenant and organization context')\n }\n\n const apiKeyProfile = await resolveOpenMercatoApiKeyProfile(em, apiKeyId, {\n tenantId,\n organizationId,\n })\n if (!apiKeyProfile) {\n throw new Error('CALL_OPEN_MERCATO action requires an active API key profile in scope')\n }\n\n const roleIds = Array.isArray(apiKeyProfile.rolesJson)\n ? apiKeyProfile.rolesJson.filter((roleId): roleId is string => typeof roleId === 'string' && roleId.length > 0)\n : []\n if (roleIds.length === 0) {\n throw new Error('CALL_OPEN_MERCATO action requires an API key profile with at least one role')\n }\n\n const ruleAuthorId = await resolveActiveRuleAuthorId(em, context.ruleAuthorId, tenantId)\n if (!ruleAuthorId) {\n throw await translatedOpenMercatoCallError(\n 'business_rules.errors.openMercatoCall.executionAuthorInactive',\n 'CALL_OPEN_MERCATO action refused: the rule has no active author',\n )\n }\n const rbacService = context.rbacService ?? await resolveRbacService()\n const authorCanUseProfile = await canActorUseOpenMercatoApiKeyProfile({\n em,\n rbacService,\n actorId: ruleAuthorId,\n tenantId,\n organizationId,\n apiKey: apiKeyProfile,\n })\n if (!authorCanUseProfile) {\n throw await translatedOpenMercatoCallError(\n 'business_rules.errors.openMercatoCall.executionAuthorCannotGrant',\n 'CALL_OPEN_MERCATO action refused: the rule author cannot grant every role of the API key profile',\n )\n }\n\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n const fullUrl = buildOpenMercatoApiUrl(endpointOption.path)\n const requestBody = method === 'GET'\n ? undefined\n : normalizeOpenMercatoRequestBody(action.config?.body, context)\n\n return await withOnetimeApiKey(\n em,\n {\n name: `__business_rule_${context.ruleId || 'unknown'}__`,\n description: `One-time key for business rule ${context.ruleId || 'unknown'}`,\n tenantId,\n organizationId,\n roles: roleIds,\n createdBy: null,\n expiresAt: null,\n },\n async (apiKeySecret) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': tenantId,\n 'X-Organization-Id': organizationId,\n }\n if (context.ruleId) headers['X-Business-Rule-Id'] = context.ruleId\n if (context.ruleName) headers['X-Business-Rule-Name'] = context.ruleName\n if (context.entityType) headers['X-Business-Rule-Entity-Type'] = context.entityType\n if (context.entityId) headers['X-Business-Rule-Entity-Id'] = context.entityId\n\n const response = await fetch(fullUrl, {\n method,\n headers,\n body: requestBody === undefined ? undefined : JSON.stringify(requestBody),\n })\n\n const responseBody = await parseOpenMercatoResponseBody(response)\n if (!response.ok) {\n throwOpenMercatoResponseError(response.status)\n }\n\n return {\n type: 'CALL_OPEN_MERCATO',\n endpoint: endpointOption.path,\n method,\n apiKeyId,\n status: response.status,\n statusText: response.statusText,\n body: responseBody,\n authenticated: true,\n tenantId,\n organizationId,\n }\n },\n )\n}\n\n/**\n * EMIT_EVENT action handler\n */\nasync function handleEmitEvent(action: Action, context: ActionContext): Promise<EmitEventResult> {\n const eventName = action.config?.event\n const payload = action.config?.payload || {}\n\n if (!eventName) {\n throw new Error('EMIT_EVENT action requires an event name')\n }\n\n if (typeof eventName !== 'string' || eventName.trim() === '') {\n throw new Error('EMIT_EVENT action requires a non-empty event name')\n }\n\n return {\n type: 'EMIT_EVENT',\n event: eventName,\n payload,\n }\n}\n\n/**\n * Interpolate message template with context values\n * Supports {{field}} syntax for variable substitution\n */\nexport function interpolateMessage(template: string, context: ActionContext): string {\n if (!template) {\n return ''\n }\n\n return template.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n const value = resolveValue(`{{${trimmedPath}}}`, context)\n\n if (value === undefined || value === null) {\n return match\n }\n\n return String(value)\n })\n}\n\n/**\n * Resolve a value from context (supports special values like {{today}}, {{user.id}}, etc.)\n */\nfunction resolveValue(value: any, context: ActionContext): any {\n if (typeof value !== 'string') {\n return value\n }\n\n if (value.startsWith('{{') && value.endsWith('}}')) {\n return resolveSpecialValue(value, context)\n }\n\n return value\n}\n\nfunction buildOpenMercatoApiUrl(endpoint: string): string {\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_OPEN_MERCATO only supports /api/* paths, got: ${endpoint}`)\n }\n const appUrl = (process.env.APP_URL || 'http://localhost:3000').replace(/\\/$/, '')\n return `${appUrl}${endpoint}`\n}\n\nfunction normalizeOpenMercatoRequestBody(value: any, context: ActionContext): any {\n if (value === undefined || value === null || value === '') return undefined\n const interpolated = interpolateActionValue(value, context)\n if (typeof interpolated !== 'string') return interpolated\n\n const trimmed = interpolated.trim()\n if (!trimmed) return undefined\n\n try {\n return JSON.parse(trimmed)\n } catch {\n return interpolated\n }\n}\n\nfunction interpolateActionValue(value: any, context: ActionContext): any {\n if (typeof value === 'string') {\n return interpolateMessage(value, context)\n }\n if (Array.isArray(value)) {\n return value.map((item) => interpolateActionValue(item, context))\n }\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [key, interpolateActionValue(entry, context)]),\n )\n }\n return value\n}\n\nasync function parseOpenMercatoResponseBody(response: Response): Promise<any> {\n const contentType = response.headers.get('content-type')\n try {\n if (contentType && contentType.includes('application/json')) {\n return await response.json()\n }\n return await response.text()\n } catch {\n return null\n }\n}\n\nfunction throwOpenMercatoResponseError(status: number): never {\n const error: Error & { retriable?: boolean } = new Error(`CALL_OPEN_MERCATO request failed with status ${status}`)\n if (status >= 500) {\n error.retriable = true\n }\n throw error\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { EvaluationContext } from './expression-evaluator'\nimport { getNestedValue, resolveSpecialValue } from './value-resolver'\nimport {\n canActorUseOpenMercatoApiKeyProfile,\n findOpenMercatoEndpointOption,\n getCurrentOpenMercatoEndpointOptions,\n resolveOpenMercatoApiKeyProfile,\n} from './openmercato-call-options'\nimport type { RbacService } from '../../auth/services/rbacService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { OpenMercatoEndpointOption } from './openmercato-call-options-types'\nimport { resolveApiKeyRoleIds } from '@open-mercato/core/modules/api_keys/services/sessionKeyRoles'\n\n/**\n * Action definition\n */\nexport interface Action {\n type: string\n config?: Record<string, any>\n}\n\n/**\n * Action execution context\n */\nexport interface ActionContext extends EvaluationContext {\n entityType?: string\n entityId?: string\n eventType?: string\n data?: any\n ruleId?: string\n ruleName?: string\n tenantId?: string\n organizationId?: string\n executedBy?: string | null\n em?: PostgreSqlEntityManager\n openMercatoEndpointOptions?: OpenMercatoEndpointOption[]\n ruleAuthorId?: string | null\n rbacService?: RbacService\n [key: string]: any\n}\n\n/**\n * Specific action result types\n */\nexport interface AllowTransitionResult {\n type: 'ALLOW_TRANSITION'\n allowed: true\n message: string\n}\n\nexport interface BlockTransitionResult {\n type: 'BLOCK_TRANSITION'\n allowed: false\n message: string\n}\n\nexport interface LogResult {\n type: 'LOG'\n level: string\n message: string\n timestamp: string\n}\n\nexport interface ShowErrorResult {\n type: 'SHOW_ERROR'\n severity: 'error'\n message: string\n}\n\nexport interface ShowWarningResult {\n type: 'SHOW_WARNING'\n severity: 'warning'\n message: string\n}\n\nexport interface ShowInfoResult {\n type: 'SHOW_INFO'\n severity: 'info'\n message: string\n}\n\nexport interface NotifyResult {\n type: 'NOTIFY'\n recipients: string[]\n subject: string\n message: string\n template?: string\n}\n\nexport interface SetFieldResult {\n type: 'SET_FIELD'\n field: string\n value: any\n}\n\nexport interface CallWebhookResult {\n type: 'CALL_WEBHOOK'\n url: string\n method: string\n headers: Record<string, any>\n body?: any\n status: 'pending'\n}\n\nexport interface CallOpenMercatoResult {\n type: 'CALL_OPEN_MERCATO'\n endpoint: string\n method: string\n apiKeyId: string\n status: number\n statusText: string\n body: any\n authenticated: true\n tenantId: string\n organizationId: string\n}\n\nexport interface EmitEventResult {\n type: 'EMIT_EVENT'\n event: string\n payload: Record<string, any>\n}\n\n/**\n * Union type of all action results\n */\nexport type ActionHandlerResult =\n | AllowTransitionResult\n | BlockTransitionResult\n | LogResult\n | ShowErrorResult\n | ShowWarningResult\n | ShowInfoResult\n | NotifyResult\n | SetFieldResult\n | CallWebhookResult\n | CallOpenMercatoResult\n | EmitEventResult\n\n/**\n * Action execution result\n */\nexport interface ActionResult {\n action: Action\n success: boolean\n result?: ActionHandlerResult\n error?: string\n executionTime: number\n}\n\n/**\n * Action execution outcome (aggregated results)\n */\nexport interface ActionExecutionOutcome {\n success: boolean\n results: ActionResult[]\n totalTime: number\n errors?: string[]\n}\n\n/**\n * Execute multiple actions in sequence\n */\nexport async function executeActions(\n actions: Action[],\n context: ActionContext\n): Promise<ActionExecutionOutcome> {\n const startTime = Date.now()\n const results: ActionResult[] = []\n const errors: string[] = []\n\n for (const action of actions) {\n try {\n const result = await executeAction(action, context)\n results.push(result)\n\n if (result.error) {\n errors.push(`Action ${action.type}: ${result.error}`)\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n errors.push(`Action ${action.type}: ${errorMessage}`)\n\n results.push({\n action,\n success: false,\n error: errorMessage,\n executionTime: 0,\n })\n }\n }\n\n const totalTime = Date.now() - startTime\n\n return {\n success: results.every((r) => r.success),\n results,\n totalTime,\n errors: errors.length > 0 ? errors : undefined,\n }\n}\n\n/**\n * Execute a single action\n */\nexport async function executeAction(action: Action, context: ActionContext): Promise<ActionResult> {\n const startTime = Date.now()\n\n try {\n const handler = getActionHandler(action.type)\n const result = await handler(action, context)\n\n const executionTime = Date.now() - startTime\n\n return {\n action,\n success: true,\n result,\n executionTime,\n }\n } catch (error) {\n const executionTime = Date.now() - startTime\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n return {\n action,\n success: false,\n error: errorMessage,\n executionTime,\n }\n }\n}\n\n/**\n * Action handler function type\n */\ntype ActionHandler = (action: Action, context: ActionContext) => Promise<ActionHandlerResult>\n\n/**\n * Get action handler by type\n */\nfunction getActionHandler(actionType: string): ActionHandler {\n const handlers: Record<string, ActionHandler> = {\n ALLOW_TRANSITION: handleAllowTransition,\n BLOCK_TRANSITION: handleBlockTransition,\n LOG: handleLog,\n SHOW_ERROR: handleShowError,\n SHOW_WARNING: handleShowWarning,\n SHOW_INFO: handleShowInfo,\n NOTIFY: handleNotify,\n SET_FIELD: handleSetField,\n CALL_WEBHOOK: handleCallWebhook,\n CALL_OPEN_MERCATO: handleCallOpenMercato,\n EMIT_EVENT: handleEmitEvent,\n }\n\n const handler = handlers[actionType]\n\n if (!handler) {\n throw new Error(`Unknown action type: ${actionType}`)\n }\n\n return handler\n}\n\n/**\n * ALLOW_TRANSITION action handler\n */\nasync function handleAllowTransition(\n action: Action,\n context: ActionContext\n): Promise<AllowTransitionResult> {\n return {\n type: 'ALLOW_TRANSITION',\n allowed: true,\n message: interpolateMessage(action.config?.message || 'Transition allowed', context),\n }\n}\n\n/**\n * BLOCK_TRANSITION action handler\n */\nasync function handleBlockTransition(\n action: Action,\n context: ActionContext\n): Promise<BlockTransitionResult> {\n return {\n type: 'BLOCK_TRANSITION',\n allowed: false,\n message: interpolateMessage(action.config?.message || 'Transition blocked', context),\n }\n}\n\n/**\n * LOG action handler\n */\nasync function handleLog(action: Action, context: ActionContext): Promise<LogResult> {\n const level = action.config?.level || 'info'\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'LOG',\n level,\n message,\n timestamp: new Date().toISOString(),\n }\n}\n\n/**\n * SHOW_ERROR action handler\n */\nasync function handleShowError(action: Action, context: ActionContext): Promise<ShowErrorResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_ERROR',\n severity: 'error',\n message,\n }\n}\n\n/**\n * SHOW_WARNING action handler\n */\nasync function handleShowWarning(\n action: Action,\n context: ActionContext\n): Promise<ShowWarningResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_WARNING',\n severity: 'warning',\n message,\n }\n}\n\n/**\n * SHOW_INFO action handler\n */\nasync function handleShowInfo(action: Action, context: ActionContext): Promise<ShowInfoResult> {\n const message = interpolateMessage(action.config?.message || '', context)\n\n return {\n type: 'SHOW_INFO',\n severity: 'info',\n message,\n }\n}\n\n/**\n * NOTIFY action handler\n */\nasync function handleNotify(action: Action, context: ActionContext): Promise<NotifyResult> {\n const recipients = action.config?.recipients || []\n const message = interpolateMessage(action.config?.message || '', context)\n const subject = interpolateMessage(action.config?.subject || '', context)\n const template = action.config?.template\n\n if (!Array.isArray(recipients)) {\n throw new Error('NOTIFY action requires recipients to be an array')\n }\n\n if (recipients.length === 0) {\n throw new Error('NOTIFY action requires at least one recipient')\n }\n\n return {\n type: 'NOTIFY',\n recipients,\n subject,\n message,\n template,\n }\n}\n\n/**\n * SET_FIELD action handler\n */\nasync function handleSetField(action: Action, context: ActionContext): Promise<SetFieldResult> {\n const field = action.config?.field\n const value = resolveValue(action.config?.value, context)\n\n if (!field) {\n throw new Error('SET_FIELD action requires a field name')\n }\n\n if (typeof field !== 'string' || field.trim() === '') {\n throw new Error('SET_FIELD action requires a non-empty field name')\n }\n\n return {\n type: 'SET_FIELD',\n field,\n value,\n }\n}\n\n/**\n * CALL_WEBHOOK action handler\n */\nasync function handleCallWebhook(\n action: Action,\n context: ActionContext\n): Promise<CallWebhookResult> {\n const url = interpolateMessage(action.config?.url || '', context)\n const method = action.config?.method || 'POST'\n const headers = action.config?.headers || {}\n const body = action.config?.body\n\n if (!url || url.trim() === '') {\n throw new Error('CALL_WEBHOOK action requires a non-empty URL')\n }\n\n const validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']\n if (!validMethods.includes(method.toUpperCase())) {\n throw new Error(\n `CALL_WEBHOOK action requires a valid HTTP method (${validMethods.join(', ')})`\n )\n }\n\n return {\n type: 'CALL_WEBHOOK',\n url,\n method: method.toUpperCase(),\n headers,\n body,\n status: 'pending',\n }\n}\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\nconst API_KEY_ACTOR_PREFIX = 'api_key:'\n\nasync function resolveRbacService(): Promise<RbacService> {\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n return container.resolve('rbacService') as RbacService\n}\n\nasync function translatedOpenMercatoCallError(key: string, fallback: string): Promise<Error> {\n const { resolveTranslations } = await import('@open-mercato/shared/lib/i18n/server')\n const { t } = await resolveTranslations()\n return new Error(t(key, fallback))\n}\n\nasync function resolveActiveRuleAuthorId(\n em: PostgreSqlEntityManager,\n authorId: string | null | undefined,\n tenantId: string,\n): Promise<string | null> {\n const candidate = typeof authorId === 'string' ? authorId.trim() : ''\n if (!candidate) return null\n if (candidate.startsWith(API_KEY_ACTOR_PREFIX)) {\n const apiKeyId = candidate.slice(API_KEY_ACTOR_PREFIX.length)\n if (!UUID_PATTERN.test(apiKeyId)) return null\n const { ApiKey } = await import('../../api_keys/data/entities')\n const key = await findOneWithDecryption(em, ApiKey, { id: apiKeyId, tenantId, deletedAt: null }, {}, { tenantId, organizationId: null })\n if (!key) return null\n if (key.expiresAt && key.expiresAt.getTime() <= Date.now()) return null\n return candidate\n }\n if (!UUID_PATTERN.test(candidate)) return null\n const { User } = await import('../../auth/data/entities')\n const user = await findOneWithDecryption(em, User, { id: candidate, deletedAt: null }, {}, { tenantId: null, organizationId: null })\n if (!user) return null\n if (user.tenantId && String(user.tenantId) === tenantId) return candidate\n const { isUserEffectivelySuperAdmin } = await import('../../auth/lib/grantChecks')\n return await isUserEffectivelySuperAdmin(em, candidate) ? candidate : null\n}\n\n/**\n * CALL_OPEN_MERCATO action handler\n */\nasync function handleCallOpenMercato(\n action: Action,\n context: ActionContext\n): Promise<CallOpenMercatoResult> {\n const endpoint = String(action.config?.endpoint ?? '').trim()\n const method = String(action.config?.method ?? '').trim().toUpperCase()\n const apiKeyId = String(action.config?.apiKeyId ?? '').trim()\n\n if (!endpoint) {\n throw new Error('CALL_OPEN_MERCATO action requires an endpoint')\n }\n if (!method) {\n throw new Error('CALL_OPEN_MERCATO action requires an HTTP method')\n }\n if (!apiKeyId) {\n throw new Error('CALL_OPEN_MERCATO action requires an API key profile')\n }\n\n const endpointOptions = context.openMercatoEndpointOptions ?? await getCurrentOpenMercatoEndpointOptions()\n const endpointOption = findOpenMercatoEndpointOption(\n endpoint,\n method,\n endpointOptions,\n )\n if (!endpointOption) {\n throw new Error('CALL_OPEN_MERCATO action requires a currently available /api/* endpoint')\n }\n\n const em = context.em\n if (!em) {\n throw new Error('CALL_OPEN_MERCATO action requires an EntityManager in action context')\n }\n\n const tenantId = context.tenantId ?? context.tenant?.id\n const organizationId = context.organizationId ?? context.organization?.id\n if (!tenantId || !organizationId) {\n throw new Error('CALL_OPEN_MERCATO action requires tenant and organization context')\n }\n\n const apiKeyProfile = await resolveOpenMercatoApiKeyProfile(em, apiKeyId, {\n tenantId,\n organizationId,\n })\n if (!apiKeyProfile) {\n throw new Error('CALL_OPEN_MERCATO action requires an active API key profile in scope')\n }\n\n const roleIds = await resolveApiKeyRoleIds(em, apiKeyProfile, tenantId)\n if (roleIds.length === 0) {\n throw new Error('CALL_OPEN_MERCATO action requires an API key profile with at least one role')\n }\n\n const ruleAuthorId = await resolveActiveRuleAuthorId(em, context.ruleAuthorId, tenantId)\n if (!ruleAuthorId) {\n throw await translatedOpenMercatoCallError(\n 'business_rules.errors.openMercatoCall.executionAuthorInactive',\n 'CALL_OPEN_MERCATO action refused: the rule has no active author',\n )\n }\n const rbacService = context.rbacService ?? await resolveRbacService()\n const authorCanUseProfile = await canActorUseOpenMercatoApiKeyProfile({\n em,\n rbacService,\n actorId: ruleAuthorId,\n tenantId,\n organizationId,\n apiKey: apiKeyProfile,\n })\n if (!authorCanUseProfile) {\n throw await translatedOpenMercatoCallError(\n 'business_rules.errors.openMercatoCall.executionAuthorCannotGrant',\n 'CALL_OPEN_MERCATO action refused: the rule author cannot grant every role of the API key profile',\n )\n }\n\n const { withOnetimeApiKey } = await import('../../api_keys/services/apiKeyService')\n const fullUrl = buildOpenMercatoApiUrl(endpointOption.path)\n const requestBody = method === 'GET'\n ? undefined\n : normalizeOpenMercatoRequestBody(action.config?.body, context)\n\n return await withOnetimeApiKey(\n em,\n {\n name: `__business_rule_${context.ruleId || 'unknown'}__`,\n description: `One-time key for business rule ${context.ruleId || 'unknown'}`,\n tenantId,\n organizationId,\n roles: roleIds,\n createdBy: null,\n expiresAt: null,\n },\n async (apiKeySecret) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `apikey ${apiKeySecret}`,\n 'X-Tenant-Id': tenantId,\n 'X-Organization-Id': organizationId,\n }\n if (context.ruleId) headers['X-Business-Rule-Id'] = context.ruleId\n if (context.ruleName) headers['X-Business-Rule-Name'] = context.ruleName\n if (context.entityType) headers['X-Business-Rule-Entity-Type'] = context.entityType\n if (context.entityId) headers['X-Business-Rule-Entity-Id'] = context.entityId\n\n const response = await fetch(fullUrl, {\n method,\n headers,\n body: requestBody === undefined ? undefined : JSON.stringify(requestBody),\n })\n\n const responseBody = await parseOpenMercatoResponseBody(response)\n if (!response.ok) {\n throwOpenMercatoResponseError(response.status)\n }\n\n return {\n type: 'CALL_OPEN_MERCATO',\n endpoint: endpointOption.path,\n method,\n apiKeyId,\n status: response.status,\n statusText: response.statusText,\n body: responseBody,\n authenticated: true,\n tenantId,\n organizationId,\n }\n },\n )\n}\n\n/**\n * EMIT_EVENT action handler\n */\nasync function handleEmitEvent(action: Action, context: ActionContext): Promise<EmitEventResult> {\n const eventName = action.config?.event\n const payload = action.config?.payload || {}\n\n if (!eventName) {\n throw new Error('EMIT_EVENT action requires an event name')\n }\n\n if (typeof eventName !== 'string' || eventName.trim() === '') {\n throw new Error('EMIT_EVENT action requires a non-empty event name')\n }\n\n return {\n type: 'EMIT_EVENT',\n event: eventName,\n payload,\n }\n}\n\n/**\n * Interpolate message template with context values\n * Supports {{field}} syntax for variable substitution\n */\nexport function interpolateMessage(template: string, context: ActionContext): string {\n if (!template) {\n return ''\n }\n\n return template.replace(/\\{\\{([^}]+)\\}\\}/g, (match, path) => {\n const trimmedPath = path.trim()\n const value = resolveValue(`{{${trimmedPath}}}`, context)\n\n if (value === undefined || value === null) {\n return match\n }\n\n return String(value)\n })\n}\n\n/**\n * Resolve a value from context (supports special values like {{today}}, {{user.id}}, etc.)\n */\nfunction resolveValue(value: any, context: ActionContext): any {\n if (typeof value !== 'string') {\n return value\n }\n\n if (value.startsWith('{{') && value.endsWith('}}')) {\n return resolveSpecialValue(value, context)\n }\n\n return value\n}\n\nfunction buildOpenMercatoApiUrl(endpoint: string): string {\n if (!endpoint.startsWith('/api/')) {\n throw new Error(`CALL_OPEN_MERCATO only supports /api/* paths, got: ${endpoint}`)\n }\n const appUrl = (process.env.APP_URL || 'http://localhost:3000').replace(/\\/$/, '')\n return `${appUrl}${endpoint}`\n}\n\nfunction normalizeOpenMercatoRequestBody(value: any, context: ActionContext): any {\n if (value === undefined || value === null || value === '') return undefined\n const interpolated = interpolateActionValue(value, context)\n if (typeof interpolated !== 'string') return interpolated\n\n const trimmed = interpolated.trim()\n if (!trimmed) return undefined\n\n try {\n return JSON.parse(trimmed)\n } catch {\n return interpolated\n }\n}\n\nfunction interpolateActionValue(value: any, context: ActionContext): any {\n if (typeof value === 'string') {\n return interpolateMessage(value, context)\n }\n if (Array.isArray(value)) {\n return value.map((item) => interpolateActionValue(item, context))\n }\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [key, interpolateActionValue(entry, context)]),\n )\n }\n return value\n}\n\nasync function parseOpenMercatoResponseBody(response: Response): Promise<any> {\n const contentType = response.headers.get('content-type')\n try {\n if (contentType && contentType.includes('application/json')) {\n return await response.json()\n }\n return await response.text()\n } catch {\n return null\n }\n}\n\nfunction throwOpenMercatoResponseError(status: number): never {\n const error: Error & { retriable?: boolean } = new Error(`CALL_OPEN_MERCATO request failed with status ${status}`)\n if (status >= 500) {\n error.retriable = true\n }\n throw error\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,6 @@
1
1
  import { Organization } from "@open-mercato/core/modules/directory/data/entities";
2
+ import { ApiKey } from "@open-mercato/core/modules/api_keys/data/entities";
3
+ import { isSessionBoundApiKey } from "@open-mercato/core/modules/api_keys/services/sessionKeyRoles";
2
4
  import { isAllOrganizationsSelection } from "@open-mercato/core/modules/directory/constants";
3
5
  import { getCurrentCacheTenant, runWithCacheTenant } from "@open-mercato/cache";
4
6
  import { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from "./scopeCookies.js";
@@ -327,9 +329,14 @@ async function resolveOrganizationScopeForRequest({
327
329
  if (memoized) return memoized;
328
330
  }
329
331
  const resolveScope = async () => {
330
- if (cache && cacheKey && typeof cache.get === "function") {
332
+ let crossRequestCache = cache;
333
+ if (crossRequestCache && userId?.startsWith("api_key:")) {
334
+ const key = await em.findOne(ApiKey, { id: userId.slice("api_key:".length), deletedAt: null });
335
+ if (!key || isSessionBoundApiKey(key)) crossRequestCache = null;
336
+ }
337
+ if (crossRequestCache && cacheKey && typeof crossRequestCache.get === "function") {
331
338
  try {
332
- const cached = await cache.get(cacheKey);
339
+ const cached = await crossRequestCache.get(cacheKey);
333
340
  if (isValidCachedScope(cached)) return cached;
334
341
  } catch (err) {
335
342
  logger.warn("Cache read failed", { err });
@@ -342,9 +349,9 @@ async function resolveOrganizationScopeForRequest({
342
349
  selectedId: rawSelected,
343
350
  tenantId: effectiveTenantId
344
351
  });
345
- if (cache && cacheKey && userId && typeof cache.set === "function") {
352
+ if (crossRequestCache && cacheKey && userId && typeof crossRequestCache.set === "function") {
346
353
  try {
347
- await cache.set(cacheKey, baseScope, {
354
+ await crossRequestCache.set(cacheKey, baseScope, {
348
355
  ttl: ttlMs,
349
356
  tags: buildOrgScopeCacheTags({ userId, effectiveTenantId })
350
357
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/directory/utils/organizationScope.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { OrganizationScope } from '@open-mercato/shared/lib/auth/principal-service'\nimport { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type { OrganizationScope }\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope:v2'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n tenantId?: string | null,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId\n await runWithCacheTenant(cacheTenantId, () =>\n cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\ntype BoundFeatureScope = {\n userId: string\n scope: OrganizationScope\n organizationId: string | null\n}\n\nconst featureScopeRequestMemo = new WeakMap<object, BoundFeatureScope>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction bindFeatureScopeToRequest(request: unknown, binding: BoundFeatureScope): void {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return\n featureScopeRequestMemo.set(request as object, binding)\n}\n\nfunction getFeatureScopeForRequest(\n request: unknown,\n userId: string,\n tenantOverride: string | null | undefined,\n): BoundFeatureScope | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const entry = featureScopeRequestMemo.get(request as object)\n if (!entry || entry.userId !== userId) return null\n if (tenantOverride === undefined) return entry\n const requestedTenantId = typeof tenantOverride === 'string' ? tenantOverride.trim() : ''\n return requestedTenantId.length > 0 && requestedTenantId === entry.scope.tenantId ? entry : null\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: Pick<RbacService, 'loadAcl'>\n auth: AuthContext | null | undefined\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n // A selection is only honored when it resolves to a real, non-deleted org for\n // this tenant. `orgDescendants` holds exactly the existing orgs among the\n // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org\n // that no longer resolves after a DB reset) has no entry and is dropped here.\n // Without the existence guard an unrestricted (all-orgs) principal \u2014 whose\n // `allowedSet` is null \u2014 would accept the dead id as `effectiveSelected`, and\n // writes derived from `selectedId` would land under an org the read scope\n // (`filterIds`) never filters to, silently orphaning the record.\n const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n // Keep the write target (`selectedId`) aligned with the read scope\n // (`filterIds`): when an unrestricted principal's requested selection was\n // dropped above, fall the selection back to the account org too so a record\n // created here is always readable back by the same caller.\n if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {\n effectiveSelected = fallbackOrgId\n }\n }\n\n if (allowedSet === null && (!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n // A concrete organization was explicitly requested (`normalizedSelectedId` is\n // null for both \"no selection\" and the \"all organizations\" token) but the\n // resolver could not honor it \u2014 it was dropped as non-existent/inaccessible\n // and the effective selection fell back to something else. Surface this so the\n // write layer can reject the request instead of silently creating the record\n // under the fallback org. Only set the field when true to keep the scope shape\n // unchanged for the common (honored) case.\n const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n ...(selectionRejected ? { selectionRejected: true } : {}),\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n if (selectedId === undefined) {\n const boundFeatureScope = getFeatureScopeForRequest(request, auth.sub, tenantOverride)\n if (boundFeatureScope) return boundFeatureScope.scope\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n if (cache && cacheKey && typeof cache.get === 'function') {\n try {\n const cached = await cache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (cache && cacheKey && userId && typeof cache.set === 'function') {\n try {\n await cache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n requiredFeatures,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | {\n cookies?: { get: (name: string) => { value: string } | undefined }\n headers?: { get(name: string): string | null }\n }\n selectedId?: string | null\n tenantId?: string | null\n requiredFeatures?: readonly string[]\n}): Promise<FeatureCheckContext> {\n const boundFeatureScope = auth?.sub && selectedId === undefined\n ? getFeatureScopeForRequest(request, auth.sub, tenantId)\n : null\n let scope = boundFeatureScope\n ? boundFeatureScope.scope\n : await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const organizationId = boundFeatureScope\n ? boundFeatureScope.organizationId\n : resolveFeatureCheckOrganizationId(scope, auth)\n if (\n auth?.sub\n && request\n && scope.selectedId === null\n && scope.tenantId\n && Array.isArray(requiredFeatures)\n && requiredFeatures.length > 0\n ) {\n const em = container.resolve<EntityManager>('em')\n const rbac = container.resolve<RbacService>('rbacService')\n const featureAccess = await rbac.resolveFeatureOrganizationAccess(\n auth.sub,\n requiredFeatures,\n { tenantId: scope.tenantId },\n )\n if (!featureAccess.unrestricted) {\n const organizationFilter: FilterQuery<Organization> = {\n tenant: scope.tenantId,\n deletedAt: null,\n ...(Array.isArray(scope.filterIds) ? { id: { $in: scope.filterIds } } : {}),\n }\n const organizations = await em.find(Organization, organizationFilter, {\n fields: ['id', 'ancestorIds'],\n })\n const candidateOrganizationIds = new Set(\n organizations.map((organization) => String(organization.id)),\n )\n const featureOrganizationIds = featureAccess.filterOrganizationIds(\n organizations.map((organization) => ({\n id: String(organization.id),\n ancestorIds: Array.isArray(organization.ancestorIds) ? organization.ancestorIds : [],\n })),\n )\n const approvedOrganizationIds = Array.from(new Set(\n featureOrganizationIds.filter((featureOrganizationId) => candidateOrganizationIds.has(featureOrganizationId)),\n ))\n scope = {\n selectedId: null,\n filterIds: approvedOrganizationIds,\n allowedIds: approvedOrganizationIds,\n tenantId: scope.tenantId,\n }\n }\n bindFeatureScopeToRequest(request, { userId: auth.sub, scope, organizationId })\n }\n\n return { organizationId, scope, allowedOrganizationIds: scope.allowedIds ?? null }\n}\n\nfunction resolveFeatureCheckOrganizationId(\n scope: OrganizationScope,\n auth: AuthContext | null | undefined,\n): string | null {\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n return scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'\nimport { isSessionBoundApiKey } from '@open-mercato/core/modules/api_keys/services/sessionKeyRoles'\nimport { isAllOrganizationsSelection } from '@open-mercato/core/modules/directory/constants'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport type { OrganizationScope } from '@open-mercato/shared/lib/auth/principal-service'\nimport { getCurrentCacheTenant, runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { parseSelectedOrganizationCookie, parseSelectedTenantCookie } from './scopeCookies'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('directory').child({ component: 'org-scope-cache' })\n\nexport { parseSelectedOrganizationCookie, parseSelectedTenantCookie }\n\nexport type { OrganizationScope }\n\n// Phase 4 \u2014 short-TTL cache for resolveOrganizationScopeForRequest.\n// OrganizationScope is a pure function of (userId, tenantId, selectedOrgId,\n// requestedTenant) between membership changes; caching it bypasses 1\n// SELECT on `organizations` per CRUD request. TTL is short (60s default)\n// to keep staleness bounded as a backstop. Tag-based invalidation also fires\n// eagerly: per-user entries are dropped by RbacService.invalidateUserCache\n// (every ACL/role grant change goes through it \u2014 see buildOrgScopeUserCacheTag)\n// and per-tenant entries by the directory.organization.* subscriber plus\n// RbacService.invalidateTenantCache (role-ACL changes).\nconst ORG_SCOPE_CACHE_KEY_PREFIX = 'org-scope:v2'\n// Phase 4 default-off until the same readiness probe (`GET /api/customers/people`)\n// stays green with the cache layer engaged. Set `OM_ORG_SCOPE_CACHE_TTL_MS=60000`\n// (or any positive integer) to opt in once cross-request safety is re-verified.\nconst ORG_SCOPE_DEFAULT_TTL_MS = 0\n\nfunction resolveOrgScopeTtlMs(): number {\n const raw = process.env.OM_ORG_SCOPE_CACHE_TTL_MS\n if (raw === undefined) return ORG_SCOPE_DEFAULT_TTL_MS\n const parsed = Number(raw)\n if (!Number.isFinite(parsed) || parsed < 0) return ORG_SCOPE_DEFAULT_TTL_MS\n return parsed\n}\n\nfunction buildOrgScopeCacheKey(parts: {\n userId: string\n effectiveTenantId: string\n selectedOrgId: string | null\n requestedTenantId: string | null\n}): string {\n const selected = parts.selectedOrgId ?? 'none'\n const requested = parts.requestedTenantId ?? 'none'\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:${parts.userId}:${parts.effectiveTenantId}:${selected}:${requested}`\n}\n\n// Tag builders are exported so the modules that own the \"this user's scope\n// changed\" / \"this tenant's org tree changed\" signals (auth RBAC invalidation,\n// the directory.organization.* subscriber) can drop the matching cross-request\n// cache entries without re-deriving the tag format. Keeping the format in one\n// place is what lets the TTL be enabled safely (issue #2259).\nexport function buildOrgScopeUserCacheTag(userId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:user:${userId}`\n}\n\nexport function buildOrgScopeTenantCacheTag(tenantId: string): string {\n return `${ORG_SCOPE_CACHE_KEY_PREFIX}:tenant:${tenantId}`\n}\n\nfunction buildOrgScopeCacheTags(parts: { userId: string; effectiveTenantId: string }): string[] {\n return [\n buildOrgScopeUserCacheTag(parts.userId),\n buildOrgScopeTenantCacheTag(parts.effectiveTenantId),\n ]\n}\n\nfunction isValidCachedScope(value: unknown): value is OrganizationScope {\n if (typeof value !== 'object' || value === null) return false\n const record = value as Partial<OrganizationScope>\n const idOk = (v: unknown) => v === null || typeof v === 'string'\n const arrOk = (v: unknown) => v === null || (Array.isArray(v) && v.every((entry) => typeof entry === 'string'))\n const flagOk = record.selectionRejected === undefined || typeof record.selectionRejected === 'boolean'\n return idOk(record.selectedId) && idOk(record.tenantId) && arrOk(record.filterIds) && arrOk(record.allowedIds) && flagOk\n}\n\nfunction resolveCacheFromContainer(container: AwilixContainer | null | undefined): CacheStrategy | null {\n if (!container) return null\n try {\n const c = container.resolve('cache') as CacheStrategy | undefined\n if (c && typeof c.get === 'function' && typeof c.set === 'function') return c\n } catch {\n return null\n }\n return null\n}\n\nexport async function invalidateOrganizationScopeCacheForUser(\n container: AwilixContainer,\n userId: string,\n tenantId?: string | null,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n const cacheTenantId = tenantId === undefined ? getCurrentCacheTenant() : tenantId\n await runWithCacheTenant(cacheTenantId, () =>\n cache.deleteByTags([buildOrgScopeUserCacheTag(userId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate user failed', { err })\n }\n}\n\nexport async function invalidateOrganizationScopeCacheForTenant(\n container: AwilixContainer,\n tenantId: string,\n): Promise<void> {\n const cache = resolveCacheFromContainer(container)\n if (!cache?.deleteByTags) return\n try {\n await runWithCacheTenant(tenantId, () =>\n cache.deleteByTags([buildOrgScopeTenantCacheTag(tenantId)]),\n )\n } catch (err) {\n logger.warn('Cache invalidate tenant failed', { err })\n }\n}\n\n// Issue #2259 \u2014 per-request memoization. resolveOrganizationScopeForRequest\n// runs at least twice per CRUD request: once for the route-level feature check\n// (resolveFeatureCheckContext) and once inside the shared factory's withCtx.\n// Those two call sites use different request-scoped DI containers but are handed\n// the SAME Request instance, so memoizing the resolved scope on a WeakMap keyed\n// by that request collapses the duplicate work \u2014 and the duplicate\n// `organizations` SELECT \u2014 into a single resolution. The inner map is keyed by\n// the same identity tuple as the cross-request cache key, so distinct explicit\n// selectedId/tenant overrides on one request stay independent. There is no\n// staleness risk: the memo lives only for the lifetime of one request and is\n// dropped with the request object by the GC.\nconst orgScopeRequestMemo = new WeakMap<object, Map<string, Promise<OrganizationScope>>>()\n\ntype BoundFeatureScope = {\n userId: string\n scope: OrganizationScope\n organizationId: string | null\n}\n\nconst featureScopeRequestMemo = new WeakMap<object, BoundFeatureScope>()\n\nfunction getRequestScopeMemo(request: unknown): Map<string, Promise<OrganizationScope>> | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const key = request as object\n let memo = orgScopeRequestMemo.get(key)\n if (!memo) {\n memo = new Map<string, Promise<OrganizationScope>>()\n orgScopeRequestMemo.set(key, memo)\n }\n return memo\n}\n\nfunction bindFeatureScopeToRequest(request: unknown, binding: BoundFeatureScope): void {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return\n featureScopeRequestMemo.set(request as object, binding)\n}\n\nfunction getFeatureScopeForRequest(\n request: unknown,\n userId: string,\n tenantOverride: string | null | undefined,\n): BoundFeatureScope | null {\n if (!request || (typeof request !== 'object' && typeof request !== 'function')) return null\n const entry = featureScopeRequestMemo.get(request as object)\n if (!entry || entry.userId !== userId) return null\n if (tenantOverride === undefined) return entry\n const requestedTenantId = typeof tenantOverride === 'string' ? tenantOverride.trim() : ''\n return requestedTenantId.length > 0 && requestedTenantId === entry.scope.tenantId ? entry : null\n}\n\nfunction normalizeOrganizationId(value: unknown): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nexport function getSelectedOrganizationFromRequest(req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_org')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedOrganizationCookie(header)\n}\n\nexport function getSelectedTenantFromRequest(\n req: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } },\n): string | null {\n const cookieContainer = (req as { cookies?: { get: (name: string) => { value: string } | undefined } }).cookies\n if (cookieContainer && typeof cookieContainer.get === 'function') {\n const val = cookieContainer.get('om_selected_tenant')?.value\n return val ?? null\n }\n const headerContainer = (req as { headers?: { get(name: string): string | null } }).headers\n const header = typeof headerContainer?.get === 'function' ? headerContainer.get('cookie') : null\n return parseSelectedTenantCookie(header)\n}\n\nfunction normalizeOrganizationIds(ids: string[]): string[] {\n return Array.from(new Set(\n ids.map((value) => normalizeOrganizationId(value)).filter((value): value is string => {\n if (!value) return false\n if (isAllOrganizationsSelection(value)) return false\n return true\n })\n ))\n}\n\n// Map each organization id to itself plus its persisted descendant ids. Only\n// orgs that exist for the tenant and are not soft-deleted are included, so an\n// unknown/inaccessible id simply has no entry (matching the per-id query that\n// returned an empty set for it).\ntype OrgDescendantMap = Map<string, string[]>\n\n// Issue #2228 \u2014 single round-trip for org-scope resolution. Instead of issuing\n// one `organizations` SELECT per `collectWithDescendants` call (up to 3-4\n// sequential queries per request: accessible set, fallback set, selected set),\n// gather every candidate id up front and fetch their descendant expansions in\n// one `em.find(Organization, { id: $in })`. Expansion then happens in-memory.\nasync function loadOrgDescendantMap(em: EntityManager, tenantId: string, ids: string[]): Promise<OrgDescendantMap> {\n const unique = normalizeOrganizationIds(ids)\n if (!unique.length) return new Map()\n const filter: FilterQuery<Organization> = {\n tenant: tenantId,\n id: { $in: unique },\n deletedAt: null,\n }\n const orgs = await em.find(Organization, filter)\n const map: OrgDescendantMap = new Map()\n for (const org of orgs) {\n const id = String(org.id)\n const expansion = [id]\n if (Array.isArray(org.descendantIds)) {\n for (const desc of org.descendantIds) expansion.push(String(desc))\n }\n map.set(id, expansion)\n }\n return map\n}\n\nfunction expandWithDescendants(map: OrgDescendantMap, ids: string[]): Set<string> {\n const set = new Set<string>()\n for (const value of ids) {\n const id = normalizeOrganizationId(value)\n if (!id || isAllOrganizationsSelection(id)) continue\n const expansion = map.get(id)\n if (!expansion) continue\n for (const entry of expansion) set.add(entry)\n }\n return set\n}\n\nexport async function resolveOrganizationScope({\n em,\n rbac,\n auth,\n selectedId,\n tenantId: tenantIdOverride,\n}: {\n em: EntityManager\n rbac: Pick<RbacService, 'loadAcl'>\n auth: AuthContext | null | undefined\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const actorTenantId = typeof auth.tenantId === 'string' && auth.tenantId.trim().length > 0 ? auth.tenantId.trim() : null\n const candidateTenantId = typeof tenantIdOverride === 'string' && tenantIdOverride.trim().length > 0\n ? tenantIdOverride.trim()\n : tenantIdOverride === null\n ? null\n : actorTenantId\n if (!candidateTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const usingOverride = candidateTenantId !== actorTenantId\n const isSuperAdminActor = auth.isSuperAdmin === true\n const tenantId = usingOverride && actorTenantId && !isSuperAdminActor ? actorTenantId : candidateTenantId\n if (!tenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n const normalizedRequestedSelection = normalizeOrganizationId(selectedId)\n const explicitAllOrgsChoice =\n normalizedRequestedSelection !== null && isAllOrganizationsSelection(normalizedRequestedSelection)\n const normalizedSelectedId = explicitAllOrgsChoice\n ? null\n : normalizedRequestedSelection\n const contextOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const acl = await rbac.loadAcl(auth.sub, { tenantId, organizationId: contextOrgId })\n const aclIsSuperAdmin = acl?.isSuperAdmin === true\n const effectiveSuperAdmin = aclIsSuperAdmin || isSuperAdminActor\n const normalizedAccessible = effectiveSuperAdmin\n ? null\n : Array.isArray(acl?.organizations)\n ? acl.organizations\n .map((value) => normalizeOrganizationId(value))\n .filter((value): value is string => value !== null)\n : null\n const accessibleList = effectiveSuperAdmin\n ? null\n : normalizedAccessible && normalizedAccessible.some((value) => isAllOrganizationsSelection(value))\n ? null\n : normalizedAccessible?.filter((value) => !isAllOrganizationsSelection(value)) ?? null\n\n const accountOrgId = actorTenantId && actorTenantId === tenantId ? normalizeOrganizationId(auth.orgId) : null\n const fallbackOrgId = accountOrgId ?? null\n\n // Every id that could be expanded below \u2014 accessible set, fallback (account)\n // org, and the requested selection \u2014 is known up front, so fetch them all in\n // a single `organizations` query and expand from the in-memory map.\n const candidateIds = [\n ...(accessibleList ?? []),\n ...(fallbackOrgId ? [fallbackOrgId] : []),\n ...(normalizedSelectedId ? [normalizedSelectedId] : []),\n ]\n const orgDescendants = await loadOrgDescendantMap(em, tenantId, candidateIds)\n const loadFallbackSet = (): Set<string> | null =>\n fallbackOrgId ? expandWithDescendants(orgDescendants, [fallbackOrgId]) : null\n\n let allowedSet: Set<string> | null = null\n if (accessibleList === null) {\n allowedSet = null\n } else if (accessibleList.length === 0) {\n allowedSet = new Set()\n } else {\n allowedSet = expandWithDescendants(orgDescendants, accessibleList)\n }\n\n const hasUnrestrictedAccess = effectiveSuperAdmin || (accessibleList === null)\n const noOrgSelection = normalizedSelectedId === null && !explicitAllOrgsChoice\n const widenToAllOrgs =\n (explicitAllOrgsChoice && hasUnrestrictedAccess)\n || (effectiveSuperAdmin && noOrgSelection)\n const initialSelected =\n normalizedSelectedId\n ?? (widenToAllOrgs ? null : accountOrgId ?? null)\n // A selection is only honored when it resolves to a real, non-deleted org for\n // this tenant. `orgDescendants` holds exactly the existing orgs among the\n // candidate ids, so a stale/dead id (e.g. a selected-org cookie or JWT org\n // that no longer resolves after a DB reset) has no entry and is dropped here.\n // Without the existence guard an unrestricted (all-orgs) principal \u2014 whose\n // `allowedSet` is null \u2014 would accept the dead id as `effectiveSelected`, and\n // writes derived from `selectedId` would land under an org the read scope\n // (`filterIds`) never filters to, silently orphaning the record.\n const selectionResolvesToOrg = (id: string): boolean => orgDescendants.has(id)\n let effectiveSelected: string | null = null\n if (initialSelected) {\n if ((allowedSet === null || allowedSet.has(initialSelected)) && selectionResolvesToOrg(initialSelected)) {\n effectiveSelected = initialSelected\n }\n }\n\n let filterSet: Set<string> | null = null\n if (effectiveSelected) {\n filterSet = expandWithDescendants(orgDescendants, [effectiveSelected])\n } else if (allowedSet !== null) {\n filterSet = allowedSet\n } else if (widenToAllOrgs) {\n filterSet = null\n } else if (auth.orgId) {\n filterSet = loadFallbackSet()\n // Keep the write target (`selectedId`) aligned with the read scope\n // (`filterIds`): when an unrestricted principal's requested selection was\n // dropped above, fall the selection back to the account org too so a record\n // created here is always readable back by the same caller.\n if (!effectiveSelected && fallbackOrgId && filterSet && filterSet.size > 0) {\n effectiveSelected = fallbackOrgId\n }\n }\n\n if (allowedSet === null && (!filterSet || filterSet.size === 0) && fallbackOrgId && !widenToAllOrgs) {\n const computed = loadFallbackSet()\n if (computed && computed.size > 0) {\n filterSet = computed\n if (!effectiveSelected) {\n effectiveSelected = fallbackOrgId\n }\n }\n }\n\n // A concrete organization was explicitly requested (`normalizedSelectedId` is\n // null for both \"no selection\" and the \"all organizations\" token) but the\n // resolver could not honor it \u2014 it was dropped as non-existent/inaccessible\n // and the effective selection fell back to something else. Surface this so the\n // write layer can reject the request instead of silently creating the record\n // under the fallback org. Only set the field when true to keep the scope shape\n // unchanged for the common (honored) case.\n const selectionRejected = normalizedSelectedId !== null && effectiveSelected !== normalizedSelectedId\n\n return {\n selectedId: effectiveSelected,\n filterIds: filterSet ? Array.from(filterSet) : null,\n allowedIds: allowedSet ? Array.from(allowedSet) : null,\n tenantId,\n ...(selectionRejected ? { selectionRejected: true } : {}),\n }\n}\n\nexport async function resolveOrganizationScopeForRequest({\n container,\n auth,\n request,\n selectedId,\n tenantId: tenantOverride,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | { cookies?: { get: (name: string) => { value: string } | undefined }; headers?: { get(name: string): string | null } }\n selectedId?: string | null\n tenantId?: string | null\n}): Promise<OrganizationScope> {\n if (!auth || !auth.sub) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n if (selectedId === undefined) {\n const boundFeatureScope = getFeatureScopeForRequest(request, auth.sub, tenantOverride)\n if (boundFeatureScope) return boundFeatureScope.scope\n }\n\n let em: EntityManager | null = null\n let rbac: RbacService | null = null\n try { em = container.resolve<EntityManager>('em') } catch { em = null }\n try { rbac = container.resolve<RbacService>('rbacService') } catch { rbac = null }\n const normalizeString = (value: unknown): string | null => {\n if (typeof value === 'string' && value.trim().length > 0) return value.trim()\n return null\n }\n if (!em || !rbac) {\n const fallbackSelected = normalizeOrganizationId(selectedId ?? auth.orgId ?? null)\n return {\n selectedId: fallbackSelected,\n filterIds: fallbackSelected ? [fallbackSelected] : null,\n allowedIds: fallbackSelected ? [fallbackSelected] : null,\n tenantId: normalizeString(auth.tenantId),\n }\n }\n\n const actorTenantField = (auth as { actorTenantId?: string | null }).actorTenantId\n const actorTenant = actorTenantField === undefined\n ? normalizeString(auth.tenantId)\n : actorTenantField === null\n ? null\n : normalizeString(actorTenantField)\n const actorOrgField = (auth as { actorOrgId?: string | null }).actorOrgId\n const actorOrgId = actorOrgField === undefined\n ? normalizeString(auth.orgId)\n : actorOrgField === null\n ? null\n : normalizeString(actorOrgField)\n\n const cookieTenant = request ? getSelectedTenantFromRequest(request) : null\n const requestedTenant =\n tenantOverride !== undefined\n ? tenantOverride\n : cookieTenant !== undefined\n ? cookieTenant\n : undefined\n const requestedTenantId = typeof requestedTenant === 'string' && requestedTenant.trim().length > 0 ? requestedTenant.trim() : null\n const isSuperAdminActor = auth.isSuperAdmin === true\n let effectiveTenantId = requestedTenantId ?? actorTenant ?? null\n if (actorTenant && effectiveTenantId && effectiveTenantId !== actorTenant && !isSuperAdminActor) {\n effectiveTenantId = actorTenant\n }\n if (!effectiveTenantId) {\n return { selectedId: null, filterIds: null, allowedIds: null, tenantId: null }\n }\n\n const scopedAuth = {\n ...auth,\n tenantId: effectiveTenantId,\n orgId: actorTenant && actorTenant === effectiveTenantId ? actorOrgId ?? null : null,\n }\n\n const rawSelected = selectedId !== undefined ? selectedId : (request ? getSelectedOrganizationFromRequest(request) : null)\n const normalizedSelectedId = typeof rawSelected === 'string' && rawSelected.trim().length > 0\n ? rawSelected.trim()\n : null\n\n const userId = typeof auth.sub === 'string' && auth.sub.length > 0 ? auth.sub : null\n const ttlMs = resolveOrgScopeTtlMs()\n const cache = ttlMs > 0 ? resolveCacheFromContainer(container) : null\n const cacheKey = userId\n ? buildOrgScopeCacheKey({\n userId,\n effectiveTenantId,\n selectedOrgId: normalizedSelectedId,\n requestedTenantId: requestedTenantId ?? null,\n })\n : null\n\n const requestMemo = getRequestScopeMemo(request)\n if (requestMemo && cacheKey) {\n const memoized = requestMemo.get(cacheKey)\n if (memoized) return memoized\n }\n\n const resolveScope = async (): Promise<OrganizationScope> => {\n let crossRequestCache = cache\n if (crossRequestCache && userId?.startsWith('api_key:')) {\n const key = await em.findOne(ApiKey, { id: userId.slice('api_key:'.length), deletedAt: null })\n if (!key || isSessionBoundApiKey(key)) crossRequestCache = null\n }\n if (crossRequestCache && cacheKey && typeof crossRequestCache.get === 'function') {\n try {\n const cached = await crossRequestCache.get(cacheKey)\n if (isValidCachedScope(cached)) return cached\n } catch (err) {\n logger.warn('Cache read failed', { err })\n }\n }\n\n const baseScope = await resolveOrganizationScope({\n em,\n rbac,\n auth: scopedAuth,\n selectedId: rawSelected,\n tenantId: effectiveTenantId,\n })\n\n if (crossRequestCache && cacheKey && userId && typeof crossRequestCache.set === 'function') {\n try {\n await crossRequestCache.set(cacheKey, baseScope, {\n ttl: ttlMs,\n tags: buildOrgScopeCacheTags({ userId, effectiveTenantId }),\n })\n } catch (err) {\n logger.warn('Cache write failed', { err })\n }\n }\n\n return baseScope\n }\n\n if (requestMemo && cacheKey) {\n const pending = resolveScope()\n requestMemo.set(cacheKey, pending)\n return pending\n }\n\n return resolveScope()\n}\n\nexport type FeatureCheckContext = {\n organizationId: string | null\n scope: OrganizationScope\n allowedOrganizationIds: string[] | null\n}\n\nexport async function resolveFeatureCheckContext({\n container,\n auth,\n request,\n selectedId,\n tenantId,\n requiredFeatures,\n}: {\n container: AwilixContainer\n auth: AuthContext | null | undefined\n request?: Request | {\n cookies?: { get: (name: string) => { value: string } | undefined }\n headers?: { get(name: string): string | null }\n }\n selectedId?: string | null\n tenantId?: string | null\n requiredFeatures?: readonly string[]\n}): Promise<FeatureCheckContext> {\n const boundFeatureScope = auth?.sub && selectedId === undefined\n ? getFeatureScopeForRequest(request, auth.sub, tenantId)\n : null\n let scope = boundFeatureScope\n ? boundFeatureScope.scope\n : await resolveOrganizationScopeForRequest({ container, auth, request, selectedId, tenantId })\n const organizationId = boundFeatureScope\n ? boundFeatureScope.organizationId\n : resolveFeatureCheckOrganizationId(scope, auth)\n if (\n auth?.sub\n && request\n && scope.selectedId === null\n && scope.tenantId\n && Array.isArray(requiredFeatures)\n && requiredFeatures.length > 0\n ) {\n const em = container.resolve<EntityManager>('em')\n const rbac = container.resolve<RbacService>('rbacService')\n const featureAccess = await rbac.resolveFeatureOrganizationAccess(\n auth.sub,\n requiredFeatures,\n { tenantId: scope.tenantId },\n )\n if (!featureAccess.unrestricted) {\n const organizationFilter: FilterQuery<Organization> = {\n tenant: scope.tenantId,\n deletedAt: null,\n ...(Array.isArray(scope.filterIds) ? { id: { $in: scope.filterIds } } : {}),\n }\n const organizations = await em.find(Organization, organizationFilter, {\n fields: ['id', 'ancestorIds'],\n })\n const candidateOrganizationIds = new Set(\n organizations.map((organization) => String(organization.id)),\n )\n const featureOrganizationIds = featureAccess.filterOrganizationIds(\n organizations.map((organization) => ({\n id: String(organization.id),\n ancestorIds: Array.isArray(organization.ancestorIds) ? organization.ancestorIds : [],\n })),\n )\n const approvedOrganizationIds = Array.from(new Set(\n featureOrganizationIds.filter((featureOrganizationId) => candidateOrganizationIds.has(featureOrganizationId)),\n ))\n scope = {\n selectedId: null,\n filterIds: approvedOrganizationIds,\n allowedIds: approvedOrganizationIds,\n tenantId: scope.tenantId,\n }\n }\n bindFeatureScopeToRequest(request, { userId: auth.sub, scope, organizationId })\n }\n\n return { organizationId, scope, allowedOrganizationIds: scope.allowedIds ?? null }\n}\n\nfunction resolveFeatureCheckOrganizationId(\n scope: OrganizationScope,\n auth: AuthContext | null | undefined,\n): string | null {\n const allowedOrganizationIds = scope.allowedIds ?? null\n const authOrgId = auth?.orgId ?? null\n return scope.selectedId\n ?? (authOrgId && (!Array.isArray(allowedOrganizationIds) || allowedOrganizationIds.includes(authOrgId)) ? authOrgId : null)\n ?? (Array.isArray(allowedOrganizationIds) && allowedOrganizationIds.length ? allowedOrganizationIds[0] : null)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -28,6 +28,8 @@ import {
28
28
  WARRANTY_CLAIM_LINE_RESOURCE_KIND,
29
29
  WARRANTY_CLAIM_RESOURCE_KIND,
30
30
  appendClaimEvent,
31
+ assertClaimLineNumberAvailable,
32
+ assertUniqueClaimLineNumbers,
31
33
  enforceWarrantyClaimOptimisticLock,
32
34
  ensureOrganizationScope,
33
35
  ensureTenantScope,
@@ -321,6 +323,10 @@ async function loadLineSnapshot(em, lineId, scope) {
321
323
  );
322
324
  return line ? snapshotLine(line) : null;
323
325
  }
326
+ async function assertLineSnapshotNumberAvailable(em, claim, snapshot) {
327
+ if (!snapshot.deletedAt) await assertClaimLineNumberAvailable(em, claim, snapshot.lineNo, snapshot.id, true);
328
+ else await requireScopedClaim(em, claim.id, { tenantId: claim.tenantId, organizationId: claim.organizationId }, { lockMode: LockMode.PESSIMISTIC_WRITE });
329
+ }
324
330
  function restoreLineFromSnapshot(line, snapshot) {
325
331
  line.lineNo = snapshot.lineNo;
326
332
  line.productId = snapshot.productId;
@@ -519,6 +525,7 @@ const createClaimLineCommand = {
519
525
  }]]]), { currentClaimId: claim.id });
520
526
  }
521
527
  nextLineNo = input.lineNo ?? existingLines.reduce((max, entry) => Math.max(max, entry.lineNo), 0) + 1;
528
+ await assertUniqueClaimLineNumbers([nextLineNo, ...existingLines.filter((line2) => line2.lineNo === nextLineNo).map((line2) => line2.lineNo)]);
522
529
  },
523
530
  () => {
524
531
  line = em.create(WarrantyClaimLine, buildLineCreateData(claim, input, nextLineNo));
@@ -584,9 +591,9 @@ const updateClaimLineCommand = {
584
591
  const guardsClaimedQty = hasOwn(input, "qtyClaimed") || hasOwn(input, "orderLineId");
585
592
  await withAtomicFlush(em, [
586
593
  async () => {
587
- if (!guardsClaimedQty && !effectiveOrderLineId) return;
588
594
  await requireScopedClaim(em, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE, refresh: true });
589
595
  assertParentMutable(claim);
596
+ if (input.lineNo !== void 0) await assertClaimLineNumberAvailable(em, claim, input.lineNo, line.id);
590
597
  if (effectiveOrderLineId && (claim.orderId || effectiveOrderLineId !== (line.orderLineId ?? null))) {
591
598
  await validateClaimReferences(ctx, scope, {
592
599
  lineOrderRefs: [{
@@ -637,6 +644,7 @@ const updateClaimLineCommand = {
637
644
  await withAtomicFlush(em, [
638
645
  async () => {
639
646
  state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule);
647
+ await assertLineSnapshotNumberAvailable(em, state.claim, before);
640
648
  },
641
649
  () => restoreLineFromSnapshot(state.line, before),
642
650
  () => recomputeClaimRollups(em, state.claim)
@@ -666,6 +674,9 @@ const deleteClaimLineCommand = {
666
674
  assertParentMutable(claim);
667
675
  await enforceWarrantyClaimOptimisticLock(ctx, line, WARRANTY_CLAIM_LINE_RESOURCE_KIND);
668
676
  await withAtomicFlush(em, [
677
+ async () => {
678
+ await requireScopedClaim(em, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
679
+ },
669
680
  () => {
670
681
  line.deletedAt = /* @__PURE__ */ new Date();
671
682
  line.updatedAt = /* @__PURE__ */ new Date();
@@ -692,6 +703,7 @@ const deleteClaimLineCommand = {
692
703
  await withAtomicFlush(em, [
693
704
  async () => {
694
705
  state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule);
706
+ await assertLineSnapshotNumberAvailable(em, state.claim, before);
695
707
  },
696
708
  () => {
697
709
  restoreLineFromSnapshot(state.line, before);
@@ -725,6 +737,9 @@ const receiveClaimLineCommand = {
725
737
  const effectiveSettings = await resolveEffectiveWarrantyClaimSettings(em, scope);
726
738
  const shouldQuarantine = Boolean(effectiveSettings.quarantineGrades?.includes(grade));
727
739
  await withAtomicFlush(em, [
740
+ async () => {
741
+ await requireScopedClaim(em, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
742
+ },
728
743
  () => {
729
744
  line.conditionGrade = grade;
730
745
  if (input.inspectionNotes !== void 0) line.inspectionNotes = input.inspectionNotes ?? null;
@@ -788,6 +803,7 @@ const receiveClaimLineCommand = {
788
803
  await withAtomicFlush(em, [
789
804
  async () => {
790
805
  state = await requireCurrentLineUndoState(em, payload?.after, receivingParentUndoRule);
806
+ await assertLineSnapshotNumberAvailable(em, state.claim, before);
791
807
  },
792
808
  () => restoreLineFromSnapshot(state.line, before),
793
809
  () => recomputeClaimRollups(em, state.claim)
@@ -815,6 +831,9 @@ const releaseClaimLineQuarantineCommand = {
815
831
  assertReceivingStatus(claim);
816
832
  await enforceWarrantyClaimOptimisticLock(ctx, line, WARRANTY_CLAIM_LINE_RESOURCE_KIND, input.updatedAt ?? void 0);
817
833
  await withAtomicFlush(em, [
834
+ async () => {
835
+ await requireScopedClaim(em, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
836
+ },
818
837
  () => {
819
838
  line.quarantineStatus = "released";
820
839
  line.updatedAt = /* @__PURE__ */ new Date();
@@ -850,6 +869,7 @@ const releaseClaimLineQuarantineCommand = {
850
869
  await withAtomicFlush(em, [
851
870
  async () => {
852
871
  state = await requireCurrentLineUndoState(em, payload?.after, receivingParentUndoRule);
872
+ await assertLineSnapshotNumberAvailable(em, state.claim, before);
853
873
  },
854
874
  () => restoreLineFromSnapshot(state.line, before),
855
875
  () => recomputeClaimRollups(em, state.claim)
@@ -877,6 +897,9 @@ const setClaimLineAssessmentCommand = {
877
897
  assertParentMutable(claim);
878
898
  await enforceWarrantyClaimOptimisticLock(ctx, line, WARRANTY_CLAIM_LINE_RESOURCE_KIND, input.updatedAt ?? void 0);
879
899
  await withAtomicFlush(em, [
900
+ async () => {
901
+ await requireScopedClaim(em, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
902
+ },
880
903
  () => {
881
904
  line.assessmentPayload = input.assessmentPayload;
882
905
  line.updatedAt = /* @__PURE__ */ new Date();
@@ -911,6 +934,7 @@ const setClaimLineAssessmentCommand = {
911
934
  await withAtomicFlush(em, [
912
935
  async () => {
913
936
  state = await requireCurrentLineUndoState(em, payload?.after, mutableParentUndoRule);
937
+ await assertLineSnapshotNumberAvailable(em, state.claim, before);
914
938
  },
915
939
  () => restoreLineFromSnapshot(state.line, before),
916
940
  () => recomputeClaimRollups(em, state.claim)