@open-mercato/core 0.6.7-develop.6774.1.d836fe3135 → 0.6.7-develop.6784.1.f80b9afce5

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 (165) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +3 -3
  3. package/agentic/standalone-guide.md +13 -0
  4. package/dist/generated/entities/organization/index.js +2 -0
  5. package/dist/generated/entities/organization/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +1 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/cli.js +1 -1
  9. package/dist/modules/auth/cli.js.map +2 -2
  10. package/dist/modules/auth/lib/backendChrome.js +4 -6
  11. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  12. package/dist/modules/auth/lib/setup-app.js +1 -0
  13. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  14. package/dist/modules/auth/services/rbacService.js +29 -44
  15. package/dist/modules/auth/services/rbacService.js.map +2 -2
  16. package/dist/modules/catalog/widgets/injection/product-seo/widget.js +1 -1
  17. package/dist/modules/catalog/widgets/injection/product-seo/widget.js.map +2 -2
  18. package/dist/modules/communication_channels/lib/access-control.js +2 -2
  19. package/dist/modules/communication_channels/lib/access-control.js.map +2 -2
  20. package/dist/modules/customer_accounts/api/invitations/accept.js +1 -2
  21. package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
  22. package/dist/modules/customer_accounts/api/login.js +4 -2
  23. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  24. package/dist/modules/customer_accounts/api/magic-link/verify.js +4 -2
  25. package/dist/modules/customer_accounts/api/magic-link/verify.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/portal/feature-check.js +5 -4
  27. package/dist/modules/customer_accounts/api/portal/feature-check.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/portal/nav.js +6 -4
  29. package/dist/modules/customer_accounts/api/portal/nav.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/portal/profile.js +5 -1
  31. package/dist/modules/customer_accounts/api/portal/profile.js.map +2 -2
  32. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js +3 -3
  33. package/dist/modules/customer_accounts/api/portal/sessions-refresh.js.map +2 -2
  34. package/dist/modules/customer_accounts/lib/customerAuth.js +7 -2
  35. package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
  36. package/dist/modules/customer_accounts/lib/customerAuthServer.js +2 -1
  37. package/dist/modules/customer_accounts/lib/customerAuthServer.js.map +2 -2
  38. package/dist/modules/customer_accounts/services/customerRbacService.js +12 -3
  39. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  40. package/dist/modules/customer_accounts/setup.js +6 -0
  41. package/dist/modules/customer_accounts/setup.js.map +2 -2
  42. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +1 -1
  43. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +2 -2
  44. package/dist/modules/customers/lib/visibilityFilter.js +6 -4
  45. package/dist/modules/customers/lib/visibilityFilter.js.map +2 -2
  46. package/dist/modules/dashboards/api/layout/[itemId]/route.js +6 -3
  47. package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
  48. package/dist/modules/dashboards/api/layout/route.js +22 -9
  49. package/dist/modules/dashboards/api/layout/route.js.map +2 -2
  50. package/dist/modules/dashboards/api/roles/widgets/route.js +9 -3
  51. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  52. package/dist/modules/dashboards/api/users/widgets/route.js +9 -3
  53. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  54. package/dist/modules/dashboards/api/widgets/catalog.js +6 -3
  55. package/dist/modules/dashboards/api/widgets/catalog.js.map +2 -2
  56. package/dist/modules/dashboards/lib/access.js +5 -3
  57. package/dist/modules/dashboards/lib/access.js.map +2 -2
  58. package/dist/modules/directory/api/organization-branding/route.js +10 -3
  59. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  60. package/dist/modules/directory/api/organizations/route.js +7 -0
  61. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  62. package/dist/modules/directory/backend/directory/branding/page.js +31 -8
  63. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  64. package/dist/modules/directory/commands/organizations.js +8 -1
  65. package/dist/modules/directory/commands/organizations.js.map +2 -2
  66. package/dist/modules/directory/data/entities.js +4 -0
  67. package/dist/modules/directory/data/entities.js.map +2 -2
  68. package/dist/modules/directory/data/validators.js +2 -0
  69. package/dist/modules/directory/data/validators.js.map +2 -2
  70. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js +13 -0
  71. package/dist/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.js.map +7 -0
  72. package/dist/modules/entities/lib/entityAcl.js +47 -15
  73. package/dist/modules/entities/lib/entityAcl.js.map +3 -3
  74. package/dist/modules/inbox_ops/ai-tools.js +5 -2
  75. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  76. package/dist/modules/messages/api/route.js +75 -3
  77. package/dist/modules/messages/api/route.js.map +2 -2
  78. package/dist/modules/messages/lib/routeHelpers.js +1 -3
  79. package/dist/modules/messages/lib/routeHelpers.js.map +2 -2
  80. package/dist/modules/notifications/lib/notificationRecipients.js +6 -7
  81. package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
  82. package/dist/modules/query_index/di.js +3 -0
  83. package/dist/modules/query_index/di.js.map +2 -2
  84. package/dist/modules/query_index/lib/coverage.js +2 -2
  85. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  86. package/dist/modules/query_index/lib/indexer.js +83 -17
  87. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  88. package/dist/modules/query_index/lib/search-tokens.js +13 -7
  89. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  90. package/dist/modules/query_index/subscribers/coverage_refresh.js +4 -0
  91. package/dist/modules/query_index/subscribers/coverage_refresh.js.map +2 -2
  92. package/dist/modules/query_index/subscribers/delete_one.js +53 -1
  93. package/dist/modules/query_index/subscribers/delete_one.js.map +3 -3
  94. package/dist/modules/query_index/subscribers/upsert_one.js +60 -0
  95. package/dist/modules/query_index/subscribers/upsert_one.js.map +3 -3
  96. package/dist/modules/sales/commands/documents.js +17 -0
  97. package/dist/modules/sales/commands/documents.js.map +2 -2
  98. package/dist/modules/staff/api/interceptors.js +5 -3
  99. package/dist/modules/staff/api/interceptors.js.map +2 -2
  100. package/dist/modules/sync_excel/api/upload/route.js +15 -2
  101. package/dist/modules/sync_excel/api/upload/route.js.map +2 -2
  102. package/dist/modules/workflows/lib/activity-executor.js +7 -7
  103. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  104. package/generated/entities/organization/index.ts +1 -0
  105. package/generated/entity-fields-registry.ts +1 -0
  106. package/package.json +7 -7
  107. package/src/modules/auth/AGENTS.md +5 -3
  108. package/src/modules/auth/cli.ts +1 -1
  109. package/src/modules/auth/lib/backendChrome.tsx +5 -11
  110. package/src/modules/auth/lib/setup-app.ts +1 -0
  111. package/src/modules/auth/services/rbacService.ts +41 -46
  112. package/src/modules/catalog/widgets/injection/product-seo/widget.ts +1 -1
  113. package/src/modules/communication_channels/lib/access-control.ts +3 -3
  114. package/src/modules/customer_accounts/AGENTS.md +3 -3
  115. package/src/modules/customer_accounts/api/invitations/accept.ts +1 -2
  116. package/src/modules/customer_accounts/api/login.ts +4 -2
  117. package/src/modules/customer_accounts/api/magic-link/verify.ts +4 -2
  118. package/src/modules/customer_accounts/api/portal/feature-check.ts +5 -7
  119. package/src/modules/customer_accounts/api/portal/nav.ts +5 -3
  120. package/src/modules/customer_accounts/api/portal/profile.ts +5 -1
  121. package/src/modules/customer_accounts/api/portal/sessions-refresh.ts +3 -3
  122. package/src/modules/customer_accounts/lib/customerAuth.ts +8 -2
  123. package/src/modules/customer_accounts/lib/customerAuthServer.ts +1 -0
  124. package/src/modules/customer_accounts/services/customerRbacService.ts +16 -3
  125. package/src/modules/customer_accounts/setup.ts +7 -0
  126. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +1 -3
  127. package/src/modules/customers/lib/visibilityFilter.ts +7 -5
  128. package/src/modules/dashboards/api/layout/[itemId]/route.ts +6 -3
  129. package/src/modules/dashboards/api/layout/route.ts +22 -9
  130. package/src/modules/dashboards/api/roles/widgets/route.ts +9 -3
  131. package/src/modules/dashboards/api/users/widgets/route.ts +9 -3
  132. package/src/modules/dashboards/api/widgets/catalog.ts +6 -3
  133. package/src/modules/dashboards/lib/access.ts +5 -3
  134. package/src/modules/directory/api/organization-branding/route.ts +12 -1
  135. package/src/modules/directory/api/organizations/route.ts +7 -0
  136. package/src/modules/directory/backend/directory/branding/page.tsx +34 -9
  137. package/src/modules/directory/commands/organizations.ts +9 -1
  138. package/src/modules/directory/data/entities.ts +3 -0
  139. package/src/modules/directory/data/validators.ts +2 -0
  140. package/src/modules/directory/i18n/de.json +3 -1
  141. package/src/modules/directory/i18n/en.json +3 -1
  142. package/src/modules/directory/i18n/es.json +3 -1
  143. package/src/modules/directory/i18n/pl.json +3 -1
  144. package/src/modules/directory/migrations/.snapshot-open-mercato.json +18 -1
  145. package/src/modules/directory/migrations/Migration20260626145500_directory_logo_preserve_aspect_ratio.ts +13 -0
  146. package/src/modules/entities/lib/entityAcl.ts +47 -18
  147. package/src/modules/inbox_ops/ai-tools.ts +5 -3
  148. package/src/modules/messages/api/route.ts +105 -4
  149. package/src/modules/messages/lib/routeHelpers.ts +4 -6
  150. package/src/modules/notifications/lib/notificationRecipients.ts +6 -8
  151. package/src/modules/query_index/di.ts +6 -0
  152. package/src/modules/query_index/lib/coverage.ts +9 -6
  153. package/src/modules/query_index/lib/indexer.ts +105 -19
  154. package/src/modules/query_index/lib/search-tokens.ts +19 -8
  155. package/src/modules/query_index/subscribers/coverage_refresh.ts +4 -0
  156. package/src/modules/query_index/subscribers/delete_one.ts +65 -1
  157. package/src/modules/query_index/subscribers/upsert_one.ts +68 -1
  158. package/src/modules/sales/commands/documents.ts +27 -0
  159. package/src/modules/sales/i18n/de.json +1 -0
  160. package/src/modules/sales/i18n/en.json +1 -0
  161. package/src/modules/sales/i18n/es.json +1 -0
  162. package/src/modules/sales/i18n/pl.json +1 -0
  163. package/src/modules/staff/api/interceptors.ts +6 -4
  164. package/src/modules/sync_excel/api/upload/route.ts +15 -2
  165. package/src/modules/workflows/lib/activity-executor.ts +12 -10
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/inbox_ops/ai-tools.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { hasFeature } from '@open-mercato/shared/security/features'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'\nimport { inboxProposalCategoryEnum } from './data/validators'\nimport { executeAction } from './lib/executionEngine'\nimport { resolveConfiguredStructuredModel, withTimeout } from './lib/llmProvider'\nimport { resolveOptionalEventBus } from './lib/eventBus'\n\ntype ToolContext = {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n}\n\ninterface AiToolDefinition {\n name: string\n description: string\n inputSchema: z.ZodType\n requiredFeatures?: string[]\n handler: (input: never, ctx: ToolContext) => Promise<unknown>\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {\n if (!ctx.tenantId || !ctx.organizationId) {\n throw new Error('Tenant context is required')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n\nfunction resolveCrossModuleEntities(container: ToolContext['container']) {\n const entities: Record<string, unknown> = {}\n const keys = [\n 'CustomerEntity',\n 'SalesOrder',\n 'SalesShipment',\n 'SalesChannel',\n 'Dictionary',\n 'DictionaryEntry',\n ]\n for (const key of keys) {\n try {\n entities[key] = container.resolve(key)\n } catch {\n /* module not available */\n }\n }\n return entities\n}\n\n// =============================================================================\n// inbox_ops_list_proposals \u2014 Query proposals by status, category, date range\n// =============================================================================\n\nconst listProposalsTool = {\n name: 'inbox_ops_list_proposals',\n description: `List inbox proposals with optional filters by status, category, and date range.\n\nReturns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,\n inputSchema: z.object({\n status: z\n .enum(['pending', 'partial', 'accepted', 'rejected'])\n .optional()\n .describe('Filter by proposal status'),\n category: inboxProposalCategoryEnum\n .optional()\n .describe('Filter by email category'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .default(10)\n .describe('Maximum number of proposals to return (default: 10)'),\n dateFrom: z\n .string()\n .optional()\n .describe('Filter proposals created on or after this date (ISO 8601)'),\n dateTo: z\n .string()\n .optional()\n .describe('Filter proposals created on or before this date (ISO 8601)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const where: Record<string, unknown> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n }\n\n if (input.status) {\n where.status = input.status\n }\n if (input.category) {\n where.category = input.category\n }\n if (input.dateFrom || input.dateTo) {\n const createdAt: Record<string, unknown> = {}\n if (input.dateFrom) {\n createdAt.$gte = new Date(input.dateFrom)\n }\n if (input.dateTo) {\n createdAt.$lte = new Date(input.dateTo)\n }\n where.createdAt = createdAt\n }\n\n const proposals = await findWithDecryption(\n em,\n InboxProposal,\n where,\n { orderBy: { createdAt: 'DESC' }, limit: input.limit },\n scope,\n )\n\n // Count actions per proposal in a single query\n const proposalIds = proposals.map((p) => p.id)\n const actionCountMap = new Map<string, number>()\n\n if (proposalIds.length > 0) {\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: { $in: proposalIds },\n deletedAt: null,\n },\n undefined,\n scope,\n )\n for (const action of actions) {\n actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)\n }\n }\n\n // Get total count for the filter\n const total = await em.count(InboxProposal, where)\n\n return {\n total,\n proposals: proposals.map((p) => ({\n id: p.id,\n summary: p.summary,\n status: p.status,\n category: p.category ?? null,\n confidence: Number(p.confidence),\n actionCount: actionCountMap.get(p.id) ?? 0,\n createdAt: p.createdAt.toISOString(),\n })),\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_get_proposal \u2014 Fetch proposal detail with actions and discrepancies\n// =============================================================================\n\nconst getProposalTool = {\n name: 'inbox_ops_get_proposal',\n description: `Get full details of an inbox proposal including its actions and discrepancies.\n\nReturns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { proposalId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n {\n id: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!proposal) {\n return { error: 'Proposal not found' }\n }\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n scope,\n )\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n return {\n proposal: {\n id: proposal.id,\n summary: proposal.summary,\n status: proposal.status,\n category: proposal.category ?? null,\n confidence: Number(proposal.confidence),\n actions: actions.map((a) => ({\n id: a.id,\n actionType: a.actionType,\n description: a.description,\n status: a.status,\n confidence: Number(a.confidence),\n requiredFeature: a.requiredFeature ?? null,\n sortOrder: a.sortOrder,\n createdEntityId: a.createdEntityId ?? null,\n createdEntityType: a.createdEntityType ?? null,\n })),\n discrepancies: discrepancies.map((d) => ({\n id: d.id,\n type: d.type,\n severity: d.severity,\n description: d.description,\n expectedValue: d.expectedValue ?? null,\n foundValue: d.foundValue ?? null,\n resolved: d.resolved,\n })),\n },\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_accept_action \u2014 Accept and execute a specific action\n// =============================================================================\n\nconst acceptActionTool = {\n name: 'inbox_ops_accept_action',\n description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).\n\nReturns on success: { ok: true, createdEntityId, createdEntityType }\nReturns on error: error message with appropriate detail.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal'),\n actionId: z.string().uuid().describe('The UUID of the action to accept'),\n }),\n requiredFeatures: ['inbox_ops.proposals.manage'],\n // Accepting an inbox action creates downstream entities (orders, contacts,\n // etc.) in target modules \u2014 must surface as a write so any agent that\n // whitelists it routes through the approval card.\n isMutation: true,\n handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n if (!ctx.userId) {\n throw new Error('User context is required')\n }\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n {\n id: input.actionId,\n proposalId: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!action) {\n return { error: 'Action not found' }\n }\n\n // Check if action was already processed\n if (action.status !== 'pending' && action.status !== 'failed') {\n return { error: 'Action already processed', status: action.status }\n }\n\n // Check target module permission\n if (action.requiredFeature) {\n const featureGranted =\n ctx.isSuperAdmin || hasFeature(ctx.userFeatures, action.requiredFeature)\n if (!featureGranted) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature,\n }\n }\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const eventBus = resolveOptionalEventBus(ctx.container)\n\n const result = await executeAction(action, {\n em,\n userId: ctx.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n eventBus,\n container: ctx.container,\n entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,\n })\n\n if (!result.success) {\n if (result.statusCode === 409) {\n return { error: 'Action already processed', status: 'accepted' }\n }\n if (result.statusCode === 403) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature ?? 'unknown',\n }\n }\n return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }\n }\n\n try {\n const { resolveCache, invalidateCountsCache } = await import('./lib/cache')\n const cache = resolveCache(ctx.container)\n if (cache && scope.tenantId) {\n await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))\n }\n } catch { /* cache invalidation is non-critical */ }\n\n return {\n ok: true,\n createdEntityId: result.createdEntityId ?? null,\n createdEntityType: result.createdEntityType ?? null,\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_categorize_email \u2014 Standalone LLM-based text categorization\n// =============================================================================\n\nconst categorizeEmailSchema = z.object({\n category: inboxProposalCategoryEnum,\n confidence: z.number(),\n reasoning: z.string(),\n})\n\nconst categorizeEmailTool = {\n name: 'inbox_ops_categorize_email',\n description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.\n\nReturns: { category, confidence (0-1), reasoning }\nInput text is limited to 10,000 characters for cost control.`,\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(10000)\n .describe('Email or text content to categorize (max 10K chars)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { text: string }, ctx: ToolContext) => {\n requireTenantContext(ctx)\n\n const { model } = await resolveConfiguredStructuredModel({ moduleId: 'inbox_ops' })\n\n const { generateObject } = await import('ai')\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: categorizeEmailSchema,\n system: `You are an email classification agent. Classify the given text into exactly one category:\n- rfq: Request for quotation or pricing inquiry\n- order: New purchase order or order placement\n- order_update: Change or update to an existing order\n- complaint: Customer complaint, dispute, or dissatisfaction\n- shipping_update: Shipment status, tracking, or delivery information\n- inquiry: General question or information request\n- payment: Payment-related (invoice, receipt, payment terms)\n- other: Does not fit any category above\n\nReturn a JSON object with:\n- category: one of the categories above\n- confidence: a number between 0 and 1 indicating how confident you are\n- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,\n prompt: input.text,\n temperature: 0,\n }),\n 15000,\n 'Email categorization timed out after 15s',\n )\n\n return {\n category: result.object.category,\n confidence: Math.round(result.object.confidence * 100) / 100,\n reasoning: result.object.reasoning,\n }\n },\n}\n\n// =============================================================================\n// Export\n// =============================================================================\n\n/**\n * All AI tools exported by the inbox_ops module.\n * Discovered by ai-assistant module's generator.\n */\nexport const aiTools: AiToolDefinition[] = [\n listProposalsTool,\n getProposalTool,\n acceptActionTool,\n categorizeEmailTool,\n]\n\nexport default aiTools\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\nimport { InboxProposal, InboxProposalAction, InboxDiscrepancy } from './data/entities'\nimport { inboxProposalCategoryEnum } from './data/validators'\nimport { executeAction } from './lib/executionEngine'\nimport { resolveConfiguredStructuredModel, withTimeout } from './lib/llmProvider'\nimport { resolveOptionalEventBus } from './lib/eventBus'\n\ntype ToolContext = {\n tenantId: string | null\n organizationId: string | null\n userId: string | null\n container: AwilixContainer\n userFeatures: string[]\n isSuperAdmin: boolean\n}\n\ninterface AiToolDefinition {\n name: string\n description: string\n inputSchema: z.ZodType\n requiredFeatures?: string[]\n handler: (input: never, ctx: ToolContext) => Promise<unknown>\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nfunction requireTenantContext(ctx: ToolContext): { tenantId: string; organizationId: string } {\n if (!ctx.tenantId || !ctx.organizationId) {\n throw new Error('Tenant context is required')\n }\n return { tenantId: ctx.tenantId, organizationId: ctx.organizationId }\n}\n\nfunction resolveCrossModuleEntities(container: ToolContext['container']) {\n const entities: Record<string, unknown> = {}\n const keys = [\n 'CustomerEntity',\n 'SalesOrder',\n 'SalesShipment',\n 'SalesChannel',\n 'Dictionary',\n 'DictionaryEntry',\n ]\n for (const key of keys) {\n try {\n entities[key] = container.resolve(key)\n } catch {\n /* module not available */\n }\n }\n return entities\n}\n\n// =============================================================================\n// inbox_ops_list_proposals \u2014 Query proposals by status, category, date range\n// =============================================================================\n\nconst listProposalsTool = {\n name: 'inbox_ops_list_proposals',\n description: `List inbox proposals with optional filters by status, category, and date range.\n\nReturns: total count and an array of proposals with id, summary, status, category, confidence, actionCount, and createdAt.`,\n inputSchema: z.object({\n status: z\n .enum(['pending', 'partial', 'accepted', 'rejected'])\n .optional()\n .describe('Filter by proposal status'),\n category: inboxProposalCategoryEnum\n .optional()\n .describe('Filter by email category'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .default(10)\n .describe('Maximum number of proposals to return (default: 10)'),\n dateFrom: z\n .string()\n .optional()\n .describe('Filter proposals created on or after this date (ISO 8601)'),\n dateTo: z\n .string()\n .optional()\n .describe('Filter proposals created on or before this date (ISO 8601)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { status?: string; category?: string; limit?: number; dateFrom?: string; dateTo?: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const where: Record<string, unknown> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n }\n\n if (input.status) {\n where.status = input.status\n }\n if (input.category) {\n where.category = input.category\n }\n if (input.dateFrom || input.dateTo) {\n const createdAt: Record<string, unknown> = {}\n if (input.dateFrom) {\n createdAt.$gte = new Date(input.dateFrom)\n }\n if (input.dateTo) {\n createdAt.$lte = new Date(input.dateTo)\n }\n where.createdAt = createdAt\n }\n\n const proposals = await findWithDecryption(\n em,\n InboxProposal,\n where,\n { orderBy: { createdAt: 'DESC' }, limit: input.limit },\n scope,\n )\n\n // Count actions per proposal in a single query\n const proposalIds = proposals.map((p) => p.id)\n const actionCountMap = new Map<string, number>()\n\n if (proposalIds.length > 0) {\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: { $in: proposalIds },\n deletedAt: null,\n },\n undefined,\n scope,\n )\n for (const action of actions) {\n actionCountMap.set(action.proposalId, (actionCountMap.get(action.proposalId) ?? 0) + 1)\n }\n }\n\n // Get total count for the filter\n const total = await em.count(InboxProposal, where)\n\n return {\n total,\n proposals: proposals.map((p) => ({\n id: p.id,\n summary: p.summary,\n status: p.status,\n category: p.category ?? null,\n confidence: Number(p.confidence),\n actionCount: actionCountMap.get(p.id) ?? 0,\n createdAt: p.createdAt.toISOString(),\n })),\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_get_proposal \u2014 Fetch proposal detail with actions and discrepancies\n// =============================================================================\n\nconst getProposalTool = {\n name: 'inbox_ops_get_proposal',\n description: `Get full details of an inbox proposal including its actions and discrepancies.\n\nReturns: proposal with id, summary, status, category, confidence, actions array, and discrepancies array.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal to retrieve'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { proposalId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const proposal = await findOneWithDecryption(\n em,\n InboxProposal,\n {\n id: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n isActive: true,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!proposal) {\n return { error: 'Proposal not found' }\n }\n\n const actions = await findWithDecryption(\n em,\n InboxProposalAction,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { sortOrder: 'ASC' } },\n scope,\n )\n\n const discrepancies = await findWithDecryption(\n em,\n InboxDiscrepancy,\n {\n proposalId: proposal.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n\n return {\n proposal: {\n id: proposal.id,\n summary: proposal.summary,\n status: proposal.status,\n category: proposal.category ?? null,\n confidence: Number(proposal.confidence),\n actions: actions.map((a) => ({\n id: a.id,\n actionType: a.actionType,\n description: a.description,\n status: a.status,\n confidence: Number(a.confidence),\n requiredFeature: a.requiredFeature ?? null,\n sortOrder: a.sortOrder,\n createdEntityId: a.createdEntityId ?? null,\n createdEntityType: a.createdEntityType ?? null,\n })),\n discrepancies: discrepancies.map((d) => ({\n id: d.id,\n type: d.type,\n severity: d.severity,\n description: d.description,\n expectedValue: d.expectedValue ?? null,\n foundValue: d.foundValue ?? null,\n resolved: d.resolved,\n })),\n },\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_accept_action \u2014 Accept and execute a specific action\n// =============================================================================\n\nconst acceptActionTool = {\n name: 'inbox_ops_accept_action',\n description: `Accept and execute a specific action from an inbox proposal. Creates the entity in the target module (e.g., order, contact).\n\nReturns on success: { ok: true, createdEntityId, createdEntityType }\nReturns on error: error message with appropriate detail.`,\n inputSchema: z.object({\n proposalId: z.string().uuid().describe('The UUID of the proposal'),\n actionId: z.string().uuid().describe('The UUID of the action to accept'),\n }),\n requiredFeatures: ['inbox_ops.proposals.manage'],\n // Accepting an inbox action creates downstream entities (orders, contacts,\n // etc.) in target modules \u2014 must surface as a write so any agent that\n // whitelists it routes through the approval card.\n isMutation: true,\n handler: async (input: { proposalId: string; actionId: string }, ctx: ToolContext) => {\n const scope = requireTenantContext(ctx)\n if (!ctx.userId) {\n throw new Error('User context is required')\n }\n\n const em = ctx.container.resolve<EntityManager>('em').fork()\n\n const action = await findOneWithDecryption(\n em,\n InboxProposalAction,\n {\n id: input.actionId,\n proposalId: input.proposalId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n\n if (!action) {\n return { error: 'Action not found' }\n }\n\n // Check if action was already processed\n if (action.status !== 'pending' && action.status !== 'failed') {\n return { error: 'Action already processed', status: action.status }\n }\n\n // Check target module permission\n if (action.requiredFeature) {\n const featureGranted = authorizeFeatures([action.requiredFeature], {\n grantedFeatures: ctx.userFeatures,\n unrestricted: ctx.isSuperAdmin,\n })\n if (!featureGranted) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature,\n }\n }\n }\n\n const entities = resolveCrossModuleEntities(ctx.container)\n const eventBus = resolveOptionalEventBus(ctx.container)\n\n const result = await executeAction(action, {\n em,\n userId: ctx.userId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n eventBus,\n container: ctx.container,\n entities: entities as unknown as import('./lib/executionEngine').CrossModuleEntities,\n })\n\n if (!result.success) {\n if (result.statusCode === 409) {\n return { error: 'Action already processed', status: 'accepted' }\n }\n if (result.statusCode === 403) {\n return {\n error: 'Insufficient permissions',\n requiredFeature: action.requiredFeature ?? 'unknown',\n }\n }\n return { error: 'Execution failed', detail: result.error ?? 'Unknown error' }\n }\n\n try {\n const { resolveCache, invalidateCountsCache } = await import('./lib/cache')\n const cache = resolveCache(ctx.container)\n if (cache && scope.tenantId) {\n await runWithCacheTenant(scope.tenantId, () => invalidateCountsCache(cache, scope.tenantId))\n }\n } catch { /* cache invalidation is non-critical */ }\n\n return {\n ok: true,\n createdEntityId: result.createdEntityId ?? null,\n createdEntityType: result.createdEntityType ?? null,\n }\n },\n}\n\n// =============================================================================\n// inbox_ops_categorize_email \u2014 Standalone LLM-based text categorization\n// =============================================================================\n\nconst categorizeEmailSchema = z.object({\n category: inboxProposalCategoryEnum,\n confidence: z.number(),\n reasoning: z.string(),\n})\n\nconst categorizeEmailTool = {\n name: 'inbox_ops_categorize_email',\n description: `Categorize email or text content using AI. Classifies text into one of: rfq, order, order_update, complaint, shipping_update, inquiry, payment, other.\n\nReturns: { category, confidence (0-1), reasoning }\nInput text is limited to 10,000 characters for cost control.`,\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(10000)\n .describe('Email or text content to categorize (max 10K chars)'),\n }),\n requiredFeatures: ['inbox_ops.proposals.view'],\n handler: async (input: { text: string }, ctx: ToolContext) => {\n requireTenantContext(ctx)\n\n const { model } = await resolveConfiguredStructuredModel({ moduleId: 'inbox_ops' })\n\n const { generateObject } = await import('ai')\n\n const result = await withTimeout(\n generateObject({\n model,\n schema: categorizeEmailSchema,\n system: `You are an email classification agent. Classify the given text into exactly one category:\n- rfq: Request for quotation or pricing inquiry\n- order: New purchase order or order placement\n- order_update: Change or update to an existing order\n- complaint: Customer complaint, dispute, or dissatisfaction\n- shipping_update: Shipment status, tracking, or delivery information\n- inquiry: General question or information request\n- payment: Payment-related (invoice, receipt, payment terms)\n- other: Does not fit any category above\n\nReturn a JSON object with:\n- category: one of the categories above\n- confidence: a number between 0 and 1 indicating how confident you are\n- reasoning: a brief explanation (1-2 sentences) of why this category was chosen`,\n prompt: input.text,\n temperature: 0,\n }),\n 15000,\n 'Email categorization timed out after 15s',\n )\n\n return {\n category: result.object.category,\n confidence: Math.round(result.object.confidence * 100) / 100,\n reasoning: result.object.reasoning,\n }\n },\n}\n\n// =============================================================================\n// Export\n// =============================================================================\n\n/**\n * All AI tools exported by the inbox_ops module.\n * Discovered by ai-assistant module's generator.\n */\nexport const aiTools: AiToolDefinition[] = [\n listProposalsTool,\n getProposalTool,\n acceptActionTool,\n categorizeEmailTool,\n]\n\nexport default aiTools\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,14 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { z } from "zod";
2
3
  import { sql } from "kysely";
4
+ import { runWithCacheTenant } from "@open-mercato/cache";
5
+ import {
6
+ buildCollectionTags,
7
+ debugCrudCache,
8
+ isCrudCacheEnabled,
9
+ normalizeTagSegment,
10
+ resolveCrudCache
11
+ } from "@open-mercato/shared/lib/crud/cache";
3
12
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
13
  import { lookupHashCandidates } from "@open-mercato/shared/lib/encryption/aes";
5
14
  import { User } from "../../auth/data/entities.js";
@@ -20,19 +29,62 @@ import {
20
29
  messageListItemSchema
21
30
  } from "./openapi.js";
22
31
  const NO_MATCH_ID = "00000000-0000-0000-0000-000000000000";
32
+ const MESSAGE_LIST_CACHE_TTL_MS = 3e4;
33
+ const MESSAGE_LIST_RESOURCE = "messages.message";
23
34
  function getDb(em) {
24
35
  return em.getKysely();
25
36
  }
37
+ function normalizeCacheFilterValue(value) {
38
+ if (value === void 0) return null;
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (Array.isArray(value)) return value.map(normalizeCacheFilterValue);
41
+ if (value && typeof value === "object") {
42
+ return Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, entryValue]) => [key, normalizeCacheFilterValue(entryValue)]);
43
+ }
44
+ return value;
45
+ }
46
+ function buildMessageListFilterSignature(input) {
47
+ const canonicalInput = Object.entries(input).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, value]) => [key, normalizeCacheFilterValue(value)]);
48
+ return createHash("sha256").update(JSON.stringify(canonicalInput)).digest("hex").slice(0, 32);
49
+ }
50
+ function buildMessageListCacheKey(scope, input) {
51
+ return [
52
+ "messages:list:v1",
53
+ `tenant:${normalizeTagSegment(scope.tenantId)}`,
54
+ `org:${normalizeTagSegment(scope.organizationId)}`,
55
+ `user:${normalizeTagSegment(scope.userId)}`,
56
+ `filters:${buildMessageListFilterSignature(input)}`
57
+ ].join("|");
58
+ }
59
+ function isMessageListPayload(value) {
60
+ if (!value || typeof value !== "object") return false;
61
+ const payload = value;
62
+ return Array.isArray(payload.items) && typeof payload.page === "number" && typeof payload.pageSize === "number" && typeof payload.total === "number" && typeof payload.totalPages === "number";
63
+ }
26
64
  const metadata = {
27
65
  GET: { requireAuth: true },
28
66
  POST: { requireAuth: true, requireFeatures: ["messages.compose"] }
29
67
  };
30
68
  async function GET(req) {
31
69
  const { ctx, scope } = await resolveMessageContext(req);
32
- const em = ctx.container.resolve("em");
33
70
  const url = new URL(req.url);
34
71
  const params = Object.fromEntries(url.searchParams);
35
72
  const input = listMessagesSchema.parse(params);
73
+ const cache = isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null;
74
+ const cacheKey = cache ? buildMessageListCacheKey(scope, input) : null;
75
+ if (cache && cacheKey) {
76
+ try {
77
+ const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey));
78
+ if (isMessageListPayload(cached)) {
79
+ return Response.json(cached);
80
+ }
81
+ } catch (error) {
82
+ debugCrudCache("messages-list-cache-read-failed", {
83
+ error: error instanceof Error ? error.message : String(error)
84
+ });
85
+ }
86
+ }
87
+ const em = ctx.container.resolve("em");
36
88
  const db = getDb(em);
37
89
  const searchIds = input.search ? await findMessageIdsBySearchTokens({
38
90
  em,
@@ -165,7 +217,7 @@ async function GET(req) {
165
217
  const name = typeof user.name === "string" && user.name.trim().length ? user.name.trim() : null;
166
218
  senderMetaById.set(user.id, { name, email: user.email ?? null });
167
219
  });
168
- return Response.json({
220
+ const payload = {
169
221
  items: typedRows.map((row) => {
170
222
  const message = messagesById.get(row.id);
171
223
  if (!message) return null;
@@ -205,7 +257,27 @@ async function GET(req) {
205
257
  pageSize: input.pageSize,
206
258
  total,
207
259
  totalPages: Math.ceil(total / input.pageSize)
208
- });
260
+ };
261
+ if (cache && cacheKey) {
262
+ try {
263
+ await runWithCacheTenant(
264
+ scope.tenantId,
265
+ () => cache.set(cacheKey, payload, {
266
+ ttl: MESSAGE_LIST_CACHE_TTL_MS,
267
+ tags: buildCollectionTags(
268
+ MESSAGE_LIST_RESOURCE,
269
+ scope.tenantId,
270
+ [scope.organizationId]
271
+ )
272
+ })
273
+ );
274
+ } catch (error) {
275
+ debugCrudCache("messages-list-cache-write-failed", {
276
+ error: error instanceof Error ? error.message : String(error)
277
+ });
278
+ }
279
+ }
280
+ return Response.json(payload);
209
281
  }
210
282
  async function POST(req) {
211
283
  const { ctx, scope } = await resolveMessageContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/api/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { applyMessageParticipantScope } from '../lib/participantScope'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const em = ctx.container.resolve('em') as EntityManager\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n // Sender-OR-recipient participant scope shared with the\n // communication_channels message enricher \u2014 see participantScope.ts (#4133).\n q = applyMessageParticipantScope(q, scope.userId)\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n // Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp \u2014\n // not in the messages:message encryption map whose encrypted fields are:\n // subject, body, external_email, external_name, action_data, action_result).\n // The handler already uses the correct two-phase shape: Kysely SQL\n // ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then\n // findWithDecryption is called only for those IDs \u2014 never for the full\n // result set. The #3278 unbounded-decrypt hazard does not apply here.\n // Covered by __tests__/list.test.ts.\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n return Response.json({\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n })\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands/command-bus'\nimport {\n buildCollectionTags,\n debugCrudCache,\n isCrudCacheEnabled,\n normalizeTagSegment,\n resolveCrudCache,\n} from '@open-mercato/shared/lib/crud/cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageObject } from '../data/entities'\nimport { composeMessageSchema, listMessagesSchema, type ListMessagesInput } from '../data/validators'\nimport { MESSAGE_ATTACHMENT_ENTITY_ID } from '../lib/constants'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { validateMessageObjectsForType } from '../lib/object-validation'\nimport { attachOperationMetadataHeader } from '../lib/operationMetadata'\nimport { canUseMessageEmailFeature, resolveMessageContext } from '../lib/routeHelpers'\nimport { applyMessageParticipantScope } from '../lib/participantScope'\nimport { resolveUserFeatures, runMessageMutationGuardAfterSuccess, runMessageMutationGuards } from './guards'\nimport { findMessageIdsBySearchTokens } from '../lib/searchLookup'\nimport { MessageCommandExecuteResult } from '../commands/shared'\nimport {\n composeMessageSchema as composeSchema,\n composeResponseSchema,\n listMessagesSchema as listSchema,\n messageListItemSchema,\n} from './openapi'\n\ntype MessageCommandExecuteResultWithThreadId = MessageCommandExecuteResult & {\n threadId: string\n}\n\nconst NO_MATCH_ID = '00000000-0000-0000-0000-000000000000'\nconst MESSAGE_LIST_CACHE_TTL_MS = 30_000\nconst MESSAGE_LIST_RESOURCE = 'messages.message'\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\ntype MessageListScopeRow = {\n id: string\n sender_user_id: string\n is_draft: boolean\n recipient_status: string | null\n read_at: string | null\n}\n\ntype AttachmentCountRow = {\n record_id: string\n count: string | number\n}\n\ntype RecipientCountRow = {\n message_id: string\n count: string | number\n}\n\ntype MessageListPayload = {\n items: Array<Record<string, unknown>>\n page: number\n pageSize: number\n total: number\n totalPages: number\n}\n\ntype MessageListScope = Awaited<ReturnType<typeof resolveMessageContext>>['scope']\n\nfunction normalizeCacheFilterValue(value: unknown): unknown {\n if (value === undefined) return null\n if (value instanceof Date) return value.toISOString()\n if (Array.isArray(value)) return value.map(normalizeCacheFilterValue)\n if (value && typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>)\n .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))\n .map(([key, entryValue]) => [key, normalizeCacheFilterValue(entryValue)])\n }\n return value\n}\n\nfunction buildMessageListFilterSignature(input: ListMessagesInput): string {\n const canonicalInput = Object.entries(input)\n .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))\n .map(([key, value]) => [key, normalizeCacheFilterValue(value)])\n\n return createHash('sha256')\n .update(JSON.stringify(canonicalInput))\n .digest('hex')\n .slice(0, 32)\n}\n\nfunction buildMessageListCacheKey(scope: MessageListScope, input: ListMessagesInput): string {\n return [\n 'messages:list:v1',\n `tenant:${normalizeTagSegment(scope.tenantId)}`,\n `org:${normalizeTagSegment(scope.organizationId)}`,\n `user:${normalizeTagSegment(scope.userId)}`,\n `filters:${buildMessageListFilterSignature(input)}`,\n ].join('|')\n}\n\nfunction isMessageListPayload(value: unknown): value is MessageListPayload {\n if (!value || typeof value !== 'object') return false\n const payload = value as Partial<MessageListPayload>\n return Array.isArray(payload.items)\n && typeof payload.page === 'number'\n && typeof payload.pageSize === 'number'\n && typeof payload.total === 'number'\n && typeof payload.totalPages === 'number'\n}\n\nexport const metadata = {\n GET: { requireAuth: true },\n POST: { requireAuth: true, requireFeatures: ['messages.compose'] },\n}\n\nexport async function GET(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const input = listMessagesSchema.parse(params)\n\n const cache = isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null\n const cacheKey = cache ? buildMessageListCacheKey(scope, input) : null\n if (cache && cacheKey) {\n try {\n const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey))\n if (isMessageListPayload(cached)) {\n return Response.json(cached)\n }\n } catch (error) {\n debugCrudCache('messages-list-cache-read-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n const em = ctx.container.resolve('em') as EntityManager\n const db = getDb(em) as any\n\n const searchIds = input.search\n ? await findMessageIdsBySearchTokens({\n em,\n query: input.search,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId,\n })\n : undefined\n\n const buildBaseQuery = () => {\n let q: any = db\n .selectFrom('messages as m')\n .where('m.tenant_id', '=', scope.tenantId)\n .where('m.deleted_at', 'is', null)\n\n if (scope.organizationId) {\n q = q.where('m.organization_id', '=', scope.organizationId)\n } else {\n q = q.where('m.organization_id', 'is', null)\n }\n\n const joinRecipient = () => {\n q = q.leftJoin('message_recipients as r', (jb: any) => jb\n .onRef('m.id', '=', 'r.message_id')\n .on('r.recipient_user_id', '=', scope.userId))\n }\n\n switch (input.folder) {\n case 'inbox':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is', null)\n .where('m.is_draft', '=', false)\n break\n case 'archived':\n joinRecipient()\n q = q\n .where('r.message_id', 'is not', null)\n .where('r.deleted_at', 'is', null)\n .where('r.archived_at', 'is not', null)\n break\n case 'sent':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', false)\n joinRecipient()\n break\n case 'drafts':\n q = q\n .where('m.sender_user_id', '=', scope.userId)\n .where('m.is_draft', '=', true)\n joinRecipient()\n break\n case 'all':\n // Sender-OR-recipient participant scope shared with the\n // communication_channels message enricher \u2014 see participantScope.ts (#4133).\n q = applyMessageParticipantScope(q, scope.userId)\n break\n default: {\n const unsupportedFolder: never = input.folder\n throw new Error(`Unsupported folder: ${String(unsupportedFolder)}`)\n }\n }\n\n if (input.status) q = q.where('r.status', '=', input.status)\n if (input.type) q = q.where('m.type', '=', input.type)\n if (input.visibility) q = q.where('m.visibility', '=', input.visibility)\n if (input.sourceEntityType) q = q.where('m.source_entity_type', '=', input.sourceEntityType)\n if (input.sourceEntityId) q = q.where('m.source_entity_id', '=', input.sourceEntityId)\n if (input.externalEmail) q = q.where('m.external_email_hash', 'in', lookupHashCandidates(input.externalEmail))\n if (input.senderId) q = q.where('m.sender_user_id', '=', input.senderId)\n\n if (input.search) {\n if (!searchIds || searchIds.length === 0) {\n q = q.where('m.id', '=', NO_MATCH_ID)\n } else {\n q = q.where('m.id', 'in', searchIds)\n }\n }\n\n if (input.since) q = q.where('m.sent_at', '>', new Date(input.since))\n\n if (input.hasObjects !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('message_objects')\n .select(sql<number>`1`.as('one'))\n .whereRef('message_objects.message_id', '=', 'm.id')\n ))\n q = input.hasObjects ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasAttachments !== undefined) {\n const existsFn = (eb: any) => eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n )\n const notExistsFn = (eb: any) => eb.not(eb.exists(\n eb.selectFrom('attachments')\n .select(sql<number>`1`.as('one'))\n .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where(sql<boolean>`attachments.record_id = m.id::text`)\n ))\n q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)\n }\n\n if (input.hasActions !== undefined) {\n q = input.hasActions\n ? q.where('m.action_data', 'is not', null)\n : q.where('m.action_data', 'is', null)\n }\n\n return q\n }\n\n // Audited for #3386 rollout (P3): sort is on m.sent_at (a plain timestamp \u2014\n // not in the messages:message encryption map whose encrypted fields are:\n // subject, body, external_email, external_name, action_data, action_result).\n // The handler already uses the correct two-phase shape: Kysely SQL\n // ORDER BY + LIMIT/OFFSET produces a bounded page of IDs, then\n // findWithDecryption is called only for those IDs \u2014 never for the full\n // result set. The #3278 unbounded-decrypt hazard does not apply here.\n // Covered by __tests__/list.test.ts.\n const countResult = await buildBaseQuery()\n .select(sql<number>`count(*)`.as('count'))\n .executeTakeFirst() as { count: string | number } | undefined\n const total = Number(countResult?.count ?? 0)\n\n const offset = (input.page - 1) * input.pageSize\n const scopeRows = await buildBaseQuery()\n .select([\n 'm.id',\n 'm.sender_user_id',\n 'm.is_draft',\n 'r.status as recipient_status',\n 'r.read_at',\n ])\n .orderBy('m.sent_at', 'desc')\n .offset(offset)\n .limit(input.pageSize)\n .execute()\n\n const typedRows = scopeRows as MessageListScopeRow[]\n const messageIds = typedRows.map((row) => row.id)\n\n const messageEntities = messageIds.length > 0\n ? await findWithDecryption(\n em,\n Message,\n { id: { $in: messageIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const messagesById = new Map<string, Message>()\n for (const message of messageEntities) {\n messagesById.set(message.id, message)\n }\n\n const objects = messageIds.length > 0\n ? await em.find(MessageObject, { messageId: { $in: messageIds } })\n : []\n\n const objectsByMessage = objects.reduce((acc, obj) => {\n if (!acc[obj.messageId]) acc[obj.messageId] = []\n acc[obj.messageId].push(obj)\n return acc\n }, {} as Record<string, MessageObject[]>)\n\n const attachmentCounts: AttachmentCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('attachments')\n .select(['record_id', sql<string>`count(*)`.as('count')])\n .where('entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)\n .where('record_id', 'in', messageIds)\n .groupBy('record_id')\n .execute()\n : []\n\n const attachmentCountByMessage = attachmentCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.record_id] = Number(row.count)\n return acc\n }, {})\n\n const recipientCounts: RecipientCountRow[] = messageIds.length > 0\n ? await (getDb(em) as any)\n .selectFrom('message_recipients')\n .select(['message_id', sql<string>`count(*)`.as('count')])\n .where('message_id', 'in', messageIds)\n .where('deleted_at', 'is', null)\n .groupBy('message_id')\n .execute()\n : []\n\n const recipientCountByMessage = recipientCounts.reduce((acc: Record<string, number>, row) => {\n acc[row.message_id] = Number(row.count)\n return acc\n }, {})\n\n const senderUserIds = Array.from(new Set(typedRows.map((row) => row.sender_user_id).filter(Boolean)))\n const senderUsers = senderUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: senderUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId }\n )\n : []\n\n const senderMetaById = new Map<string, { name: string | null; email: string | null }>()\n senderUsers.forEach((user) => {\n const name = typeof user.name === 'string' && user.name.trim().length ? user.name.trim() : null\n senderMetaById.set(user.id, { name, email: user.email ?? null })\n })\n\n const payload: MessageListPayload = {\n items: typedRows\n .map((row) => {\n const message = messagesById.get(row.id)\n if (!message) return null\n const body = typeof message.body === 'string' ? message.body : ''\n const bodyPreview = body.substring(0, 150) + (body.length > 150 ? '...' : '')\n const actionData = message.actionData ?? null\n return {\n ...(senderMetaById.get(row.sender_user_id)\n ? {\n senderName: senderMetaById.get(row.sender_user_id)?.name ?? null,\n senderEmail: senderMetaById.get(row.sender_user_id)?.email ?? null,\n }\n : { senderName: null, senderEmail: null }),\n id: message.id,\n type: message.type,\n visibility: message.visibility ?? null,\n sourceEntityType: message.sourceEntityType ?? null,\n sourceEntityId: message.sourceEntityId ?? null,\n externalEmail: message.externalEmail ?? null,\n externalName: message.externalName ?? null,\n subject: message.subject,\n bodyPreview,\n senderUserId: message.senderUserId,\n priority: message.priority,\n status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),\n hasObjects: (objectsByMessage[message.id] || []).length > 0,\n objectCount: (objectsByMessage[message.id] || []).length,\n hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,\n attachmentCount: attachmentCountByMessage[message.id] || 0,\n recipientCount: recipientCountByMessage[message.id] || 0,\n hasActions:\n Boolean(actionData?.actions?.length)\n || Boolean(getMessageType(message.type)?.defaultActions?.length)\n || (objectsByMessage[message.id] || []).some((item) => item.actionRequired && Boolean(item.actionType)),\n actionTaken: message.actionTaken ?? null,\n sentAt: message.sentAt ? message.sentAt.toISOString() : null,\n readAt: row.read_at,\n threadId: message.threadId ?? null,\n }\n })\n .filter((item): item is NonNullable<typeof item> => item !== null),\n page: input.page,\n pageSize: input.pageSize,\n total,\n totalPages: Math.ceil(total / input.pageSize),\n }\n\n if (cache && cacheKey) {\n try {\n await runWithCacheTenant(scope.tenantId, () =>\n cache.set(cacheKey, payload, {\n ttl: MESSAGE_LIST_CACHE_TTL_MS,\n tags: buildCollectionTags(\n MESSAGE_LIST_RESOURCE,\n scope.tenantId,\n [scope.organizationId],\n ),\n }),\n )\n } catch (error) {\n debugCrudCache('messages-list-cache-write-failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n return Response.json(payload)\n}\n\nexport async function POST(req: Request) {\n const { ctx, scope } = await resolveMessageContext(req)\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const body = await req.json().catch(() => ({}))\n const input = composeMessageSchema.parse(body)\n\n const isPublicVisibility = input.visibility === 'public'\n const sendViaEmail = isPublicVisibility ? true : input.sendViaEmail\n if (sendViaEmail && !(await canUseMessageEmailFeature(ctx, scope))) {\n return Response.json({ error: 'Missing feature: messages.email' }, { status: 403 })\n }\n\n if (input.objects?.length) {\n const objectValidationError = validateMessageObjectsForType(input.type, input.objects)\n if (objectValidationError) {\n return Response.json({ error: objectValidationError }, { status: 400 })\n }\n }\n\n const guardResult = await runMessageMutationGuards(\n ctx.container,\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: null,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: input as Record<string, unknown>,\n },\n resolveUserFeatures(ctx.auth),\n )\n if (!guardResult.ok) {\n return Response.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const { result, logEntry } = await commandBus.execute('messages.messages.compose', {\n input: {\n ...input,\n sendViaEmail,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n request: req,\n },\n })\n const { id: messageId, threadId: responseThreadId } = result as unknown as MessageCommandExecuteResultWithThreadId\n\n const response = Response.json({ id: messageId, threadId: responseThreadId }, { status: 201 })\n attachOperationMetadataHeader(response, logEntry, {\n resourceKind: 'messages.message',\n resourceId: messageId,\n })\n await runMessageMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n resourceKind: 'messages.message',\n resourceId: messageId,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return response\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Messages',\n methods: {\n GET: {\n summary: 'List messages',\n query: listSchema,\n responses: [\n {\n status: 200,\n description: 'Message list',\n schema: z.object({\n items: z.array(messageListItemSchema),\n page: z.number(),\n pageSize: z.number(),\n total: z.number(),\n totalPages: z.number(),\n }),\n },\n ],\n },\n POST: {\n summary: 'Compose a message',\n requestBody: {\n schema: composeSchema,\n },\n responses: [\n {\n status: 201,\n description: 'Message created',\n schema: composeResponseSchema,\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,4 @@
1
1
  import { resolveRequestContext } from "@open-mercato/shared/lib/api/context";
2
- import { hasFeature } from "@open-mercato/shared/security/features";
3
2
  function hasOrganizationAccess(scopeOrganizationId, messageOrganizationId) {
4
3
  if (scopeOrganizationId) {
5
4
  return messageOrganizationId === scopeOrganizationId;
@@ -29,11 +28,10 @@ async function parseRequestBodySafe(req) {
29
28
  async function canUseMessageEmailFeature(ctx, scope) {
30
29
  if (!scope.userId || !scope.tenantId) return false;
31
30
  const rbac = ctx.container.resolve("rbacService");
32
- const acl = await rbac.loadAcl(scope.userId, {
31
+ return rbac.userHasAllFeatures(scope.userId, ["messages.email"], {
33
32
  tenantId: scope.tenantId,
34
33
  organizationId: scope.organizationId
35
34
  });
36
- return Boolean(acl.isSuperAdmin) || hasFeature(acl.features, "messages.email");
37
35
  }
38
36
  export {
39
37
  canUseMessageEmailFeature,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/lib/routeHelpers.ts"],
4
- "sourcesContent": ["import { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\nexport function hasOrganizationAccess(\n scopeOrganizationId: string | null,\n messageOrganizationId: string | null | undefined,\n): boolean {\n if (scopeOrganizationId) {\n return messageOrganizationId === scopeOrganizationId\n }\n return messageOrganizationId == null\n}\n\nexport type MessageScope = {\n tenantId: string\n organizationId: string | null\n userId: string\n}\n\nexport async function resolveMessageContext(req: Request): Promise<{\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n scope: MessageScope\n}> {\n const { ctx } = await resolveRequestContext(req)\n return {\n ctx,\n scope: {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n },\n }\n}\n\ntype RbacService = {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{ features?: string[]; isSuperAdmin?: boolean }>\n}\n\nexport async function parseRequestBodySafe(req: Request): Promise<unknown> {\n try {\n const text = await req.text()\n if (!text) return {}\n return JSON.parse(text)\n } catch {\n return {}\n }\n}\n\nexport async function canUseMessageEmailFeature(\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx'],\n scope: MessageScope,\n): Promise<boolean> {\n if (!scope.userId || !scope.tenantId) return false\n\n const rbac = ctx.container.resolve('rbacService') as RbacService\n const acl = await rbac.loadAcl(scope.userId, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n\n return Boolean(acl.isSuperAdmin) || hasFeature(acl.features, 'messages.email')\n}\n"],
5
- "mappings": "AAAA,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAEpB,SAAS,sBACd,qBACA,uBACS;AACT,MAAI,qBAAqB;AACvB,WAAO,0BAA0B;AAAA,EACnC;AACA,SAAO,yBAAyB;AAClC;AAQA,eAAsB,sBAAsB,KAGzC;AACD,QAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,SAAO;AAAA,IACL;AAAA,IACA,OAAO;AAAA,MACL,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,MAAM,SAAS;AAAA,MACnC,QAAQ,IAAI,MAAM,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;AASA,eAAsB,qBAAqB,KAAgC;AACzE,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,0BACpB,KACA,OACkB;AAClB,MAAI,CAAC,MAAM,UAAU,CAAC,MAAM,SAAU,QAAO;AAE7C,QAAM,OAAO,IAAI,UAAU,QAAQ,aAAa;AAChD,QAAM,MAAM,MAAM,KAAK,QAAQ,MAAM,QAAQ;AAAA,IAC3C,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AAED,SAAO,QAAQ,IAAI,YAAY,KAAK,WAAW,IAAI,UAAU,gBAAgB;AAC/E;",
4
+ "sourcesContent": ["import { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\n\nexport function hasOrganizationAccess(\n scopeOrganizationId: string | null,\n messageOrganizationId: string | null | undefined,\n): boolean {\n if (scopeOrganizationId) {\n return messageOrganizationId === scopeOrganizationId\n }\n return messageOrganizationId == null\n}\n\nexport type MessageScope = {\n tenantId: string\n organizationId: string | null\n userId: string\n}\n\nexport async function resolveMessageContext(req: Request): Promise<{\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n scope: MessageScope\n}> {\n const { ctx } = await resolveRequestContext(req)\n return {\n ctx,\n scope: {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n },\n }\n}\n\ntype RbacService = {\n userHasAllFeatures: (\n userId: string,\n required: string[],\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<boolean>\n}\n\nexport async function parseRequestBodySafe(req: Request): Promise<unknown> {\n try {\n const text = await req.text()\n if (!text) return {}\n return JSON.parse(text)\n } catch {\n return {}\n }\n}\n\nexport async function canUseMessageEmailFeature(\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx'],\n scope: MessageScope,\n): Promise<boolean> {\n if (!scope.userId || !scope.tenantId) return false\n\n const rbac = ctx.container.resolve('rbacService') as RbacService\n return rbac.userHasAllFeatures(scope.userId, ['messages.email'], {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,6BAA6B;AAE/B,SAAS,sBACd,qBACA,uBACS;AACT,MAAI,qBAAqB;AACvB,WAAO,0BAA0B;AAAA,EACnC;AACA,SAAO,yBAAyB;AAClC;AAQA,eAAsB,sBAAsB,KAGzC;AACD,QAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,SAAO;AAAA,IACL;AAAA,IACA,OAAO;AAAA,MACL,UAAU,IAAI,MAAM,YAAY;AAAA,MAChC,gBAAgB,IAAI,MAAM,SAAS;AAAA,MACnC,QAAQ,IAAI,MAAM,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;AAUA,eAAsB,qBAAqB,KAAgC;AACzE,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,CAAC,KAAM,QAAO,CAAC;AACnB,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,eAAsB,0BACpB,KACA,OACkB;AAClB,MAAI,CAAC,MAAM,UAAU,CAAC,MAAM,SAAU,QAAO;AAE7C,QAAM,OAAO,IAAI,UAAU,QAAQ,aAAa;AAChD,SAAO,KAAK,mBAAmB,MAAM,QAAQ,CAAC,gBAAgB,GAAG;AAAA,IAC/D,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { hasFeature } from "@open-mercato/shared/security/features";
1
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
2
2
  function normalizeFeatures(features) {
3
3
  if (!Array.isArray(features)) return void 0;
4
4
  const normalized = features.filter((feature) => typeof feature === "string");
@@ -6,12 +6,11 @@ function normalizeFeatures(features) {
6
6
  }
7
7
  function collectUsersWithFeature(userIdsSet, rows, requiredFeature) {
8
8
  for (const row of rows) {
9
- if (row.is_super_admin) {
10
- userIdsSet.add(row.user_id);
11
- continue;
12
- }
13
- const features = normalizeFeatures(row.features_json);
14
- if (features && hasFeature(features, requiredFeature)) {
9
+ const features = normalizeFeatures(row.features_json) ?? [];
10
+ if (authorizeFeatures([requiredFeature], {
11
+ grantedFeatures: features,
12
+ unrestricted: row.is_super_admin
13
+ })) {
15
14
  userIdsSet.add(row.user_id);
16
15
  }
17
16
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/notifications/lib/notificationRecipients.ts"],
4
- "sourcesContent": ["import type { Kysely } from 'kysely'\nimport { hasFeature } from '@open-mercato/shared/security/features'\n\ninterface AclRow {\n user_id: string\n features_json: unknown\n is_super_admin: boolean\n}\n\nfunction normalizeFeatures(features: unknown): string[] | undefined {\n if (!Array.isArray(features)) return undefined\n const normalized = features.filter((feature): feature is string => typeof feature === 'string')\n return normalized.length ? normalized : undefined\n}\n\n/**\n * Extract user IDs from ACL rows that have the required feature or are super admins.\n */\nfunction collectUsersWithFeature(\n userIdsSet: Set<string>,\n rows: AclRow[],\n requiredFeature: string\n): void {\n for (const row of rows) {\n if (row.is_super_admin) {\n userIdsSet.add(row.user_id)\n continue\n }\n\n const features = normalizeFeatures(row.features_json)\n if (features && hasFeature(features, requiredFeature)) {\n userIdsSet.add(row.user_id)\n }\n }\n}\n\nexport async function getScopedNotificationRecipientUserIds(\n db: Kysely<any>,\n tenantId: string,\n organizationId: string | null,\n recipientUserIds: string[],\n): Promise<string[]> {\n const builder: any = db\n let query = builder\n .selectFrom('users')\n .where('users.id', 'in', recipientUserIds)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n\n if (organizationId) {\n query = query.where('users.organization_id', '=', organizationId)\n }\n\n const users = await query\n .select('users.id as user_id')\n .execute() as Array<{ user_id: string }>\n\n return users.map((user) => user.user_id)\n}\n\nexport async function getRecipientUserIdsForRole(\n db: Kysely<any>,\n tenantId: string,\n roleId: string\n): Promise<string[]> {\n const builder: any = db\n const userRoles = await builder\n .selectFrom('user_roles')\n .innerJoin('users', 'user_roles.user_id', 'users.id')\n .where('user_roles.role_id', '=', roleId)\n .where('user_roles.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select('users.id as user_id')\n .execute() as Array<{ user_id: string }>\n\n return userRoles.map((row) => row.user_id)\n}\n\nexport async function getRecipientUserIdsForFeature(\n db: Kysely<any>,\n tenantId: string,\n requiredFeature: string\n): Promise<string[]> {\n const userIdsSet = new Set<string>()\n const builder: any = db\n\n const userAcls = await builder\n .selectFrom('user_acls')\n .innerJoin('users', 'user_acls.user_id', 'users.id')\n .where('user_acls.tenant_id', '=', tenantId)\n .where('user_acls.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select([\n 'users.id as user_id',\n 'user_acls.features_json',\n 'user_acls.is_super_admin',\n ])\n .execute() as AclRow[]\n\n collectUsersWithFeature(userIdsSet, userAcls, requiredFeature)\n\n const roleAcls = await builder\n .selectFrom('role_acls')\n .innerJoin('user_roles', 'role_acls.role_id', 'user_roles.role_id')\n .innerJoin('users', 'user_roles.user_id', 'users.id')\n .where('role_acls.tenant_id', '=', tenantId)\n .where('role_acls.deleted_at', 'is', null)\n .where('user_roles.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select([\n 'users.id as user_id',\n 'role_acls.features_json',\n 'role_acls.is_super_admin',\n ])\n .execute() as AclRow[]\n\n collectUsersWithFeature(userIdsSet, roleAcls, requiredFeature)\n\n return Array.from(userIdsSet)\n}\n"],
5
- "mappings": "AACA,SAAS,kBAAkB;AAQ3B,SAAS,kBAAkB,UAAyC;AAClE,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACrC,QAAM,aAAa,SAAS,OAAO,CAAC,YAA+B,OAAO,YAAY,QAAQ;AAC9F,SAAO,WAAW,SAAS,aAAa;AAC1C;AAKA,SAAS,wBACP,YACA,MACA,iBACM;AACN,aAAW,OAAO,MAAM;AACtB,QAAI,IAAI,gBAAgB;AACtB,iBAAW,IAAI,IAAI,OAAO;AAC1B;AAAA,IACF;AAEA,UAAM,WAAW,kBAAkB,IAAI,aAAa;AACpD,QAAI,YAAY,WAAW,UAAU,eAAe,GAAG;AACrD,iBAAW,IAAI,IAAI,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAEA,eAAsB,sCACpB,IACA,UACA,gBACA,kBACmB;AACnB,QAAM,UAAe;AACrB,MAAI,QAAQ,QACT,WAAW,OAAO,EAClB,MAAM,YAAY,MAAM,gBAAgB,EACxC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ;AAEzC,MAAI,gBAAgB;AAClB,YAAQ,MAAM,MAAM,yBAAyB,KAAK,cAAc;AAAA,EAClE;AAEA,QAAM,QAAQ,MAAM,MACjB,OAAO,qBAAqB,EAC5B,QAAQ;AAEX,SAAO,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO;AACzC;AAEA,eAAsB,2BACpB,IACA,UACA,QACmB;AACnB,QAAM,UAAe;AACrB,QAAM,YAAY,MAAM,QACrB,WAAW,YAAY,EACvB,UAAU,SAAS,sBAAsB,UAAU,EACnD,MAAM,sBAAsB,KAAK,MAAM,EACvC,MAAM,yBAAyB,MAAM,IAAI,EACzC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO,qBAAqB,EAC5B,QAAQ;AAEX,SAAO,UAAU,IAAI,CAAC,QAAQ,IAAI,OAAO;AAC3C;AAEA,eAAsB,8BACpB,IACA,UACA,iBACmB;AACnB,QAAM,aAAa,oBAAI,IAAY;AACnC,QAAM,UAAe;AAErB,QAAM,WAAW,MAAM,QACpB,WAAW,WAAW,EACtB,UAAU,SAAS,qBAAqB,UAAU,EAClD,MAAM,uBAAuB,KAAK,QAAQ,EAC1C,MAAM,wBAAwB,MAAM,IAAI,EACxC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,QAAQ;AAEX,0BAAwB,YAAY,UAAU,eAAe;AAE7D,QAAM,WAAW,MAAM,QACpB,WAAW,WAAW,EACtB,UAAU,cAAc,qBAAqB,oBAAoB,EACjE,UAAU,SAAS,sBAAsB,UAAU,EACnD,MAAM,uBAAuB,KAAK,QAAQ,EAC1C,MAAM,wBAAwB,MAAM,IAAI,EACxC,MAAM,yBAAyB,MAAM,IAAI,EACzC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,QAAQ;AAEX,0BAAwB,YAAY,UAAU,eAAe;AAE7D,SAAO,MAAM,KAAK,UAAU;AAC9B;",
4
+ "sourcesContent": ["import type { Kysely } from 'kysely'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\n\ninterface AclRow {\n user_id: string\n features_json: unknown\n is_super_admin: boolean\n}\n\nfunction normalizeFeatures(features: unknown): string[] | undefined {\n if (!Array.isArray(features)) return undefined\n const normalized = features.filter((feature): feature is string => typeof feature === 'string')\n return normalized.length ? normalized : undefined\n}\n\n/**\n * Extract user IDs from ACL rows that have the required feature or are super admins.\n */\nfunction collectUsersWithFeature(\n userIdsSet: Set<string>,\n rows: AclRow[],\n requiredFeature: string\n): void {\n for (const row of rows) {\n const features = normalizeFeatures(row.features_json) ?? []\n if (authorizeFeatures([requiredFeature], {\n grantedFeatures: features,\n unrestricted: row.is_super_admin,\n })) {\n userIdsSet.add(row.user_id)\n }\n }\n}\n\nexport async function getScopedNotificationRecipientUserIds(\n db: Kysely<any>,\n tenantId: string,\n organizationId: string | null,\n recipientUserIds: string[],\n): Promise<string[]> {\n const builder: any = db\n let query = builder\n .selectFrom('users')\n .where('users.id', 'in', recipientUserIds)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n\n if (organizationId) {\n query = query.where('users.organization_id', '=', organizationId)\n }\n\n const users = await query\n .select('users.id as user_id')\n .execute() as Array<{ user_id: string }>\n\n return users.map((user) => user.user_id)\n}\n\nexport async function getRecipientUserIdsForRole(\n db: Kysely<any>,\n tenantId: string,\n roleId: string\n): Promise<string[]> {\n const builder: any = db\n const userRoles = await builder\n .selectFrom('user_roles')\n .innerJoin('users', 'user_roles.user_id', 'users.id')\n .where('user_roles.role_id', '=', roleId)\n .where('user_roles.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select('users.id as user_id')\n .execute() as Array<{ user_id: string }>\n\n return userRoles.map((row) => row.user_id)\n}\n\nexport async function getRecipientUserIdsForFeature(\n db: Kysely<any>,\n tenantId: string,\n requiredFeature: string\n): Promise<string[]> {\n const userIdsSet = new Set<string>()\n const builder: any = db\n\n const userAcls = await builder\n .selectFrom('user_acls')\n .innerJoin('users', 'user_acls.user_id', 'users.id')\n .where('user_acls.tenant_id', '=', tenantId)\n .where('user_acls.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select([\n 'users.id as user_id',\n 'user_acls.features_json',\n 'user_acls.is_super_admin',\n ])\n .execute() as AclRow[]\n\n collectUsersWithFeature(userIdsSet, userAcls, requiredFeature)\n\n const roleAcls = await builder\n .selectFrom('role_acls')\n .innerJoin('user_roles', 'role_acls.role_id', 'user_roles.role_id')\n .innerJoin('users', 'user_roles.user_id', 'users.id')\n .where('role_acls.tenant_id', '=', tenantId)\n .where('role_acls.deleted_at', 'is', null)\n .where('user_roles.deleted_at', 'is', null)\n .where('users.deleted_at', 'is', null)\n .where('users.tenant_id', '=', tenantId)\n .select([\n 'users.id as user_id',\n 'role_acls.features_json',\n 'role_acls.is_super_admin',\n ])\n .execute() as AclRow[]\n\n collectUsersWithFeature(userIdsSet, roleAcls, requiredFeature)\n\n return Array.from(userIdsSet)\n}\n"],
5
+ "mappings": "AACA,SAAS,yBAAyB;AAQlC,SAAS,kBAAkB,UAAyC;AAClE,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACrC,QAAM,aAAa,SAAS,OAAO,CAAC,YAA+B,OAAO,YAAY,QAAQ;AAC9F,SAAO,WAAW,SAAS,aAAa;AAC1C;AAKA,SAAS,wBACP,YACA,MACA,iBACM;AACN,aAAW,OAAO,MAAM;AACtB,UAAM,WAAW,kBAAkB,IAAI,aAAa,KAAK,CAAC;AAC1D,QAAI,kBAAkB,CAAC,eAAe,GAAG;AAAA,MACvC,iBAAiB;AAAA,MACjB,cAAc,IAAI;AAAA,IACpB,CAAC,GAAG;AACF,iBAAW,IAAI,IAAI,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;AAEA,eAAsB,sCACpB,IACA,UACA,gBACA,kBACmB;AACnB,QAAM,UAAe;AACrB,MAAI,QAAQ,QACT,WAAW,OAAO,EAClB,MAAM,YAAY,MAAM,gBAAgB,EACxC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ;AAEzC,MAAI,gBAAgB;AAClB,YAAQ,MAAM,MAAM,yBAAyB,KAAK,cAAc;AAAA,EAClE;AAEA,QAAM,QAAQ,MAAM,MACjB,OAAO,qBAAqB,EAC5B,QAAQ;AAEX,SAAO,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO;AACzC;AAEA,eAAsB,2BACpB,IACA,UACA,QACmB;AACnB,QAAM,UAAe;AACrB,QAAM,YAAY,MAAM,QACrB,WAAW,YAAY,EACvB,UAAU,SAAS,sBAAsB,UAAU,EACnD,MAAM,sBAAsB,KAAK,MAAM,EACvC,MAAM,yBAAyB,MAAM,IAAI,EACzC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO,qBAAqB,EAC5B,QAAQ;AAEX,SAAO,UAAU,IAAI,CAAC,QAAQ,IAAI,OAAO;AAC3C;AAEA,eAAsB,8BACpB,IACA,UACA,iBACmB;AACnB,QAAM,aAAa,oBAAI,IAAY;AACnC,QAAM,UAAe;AAErB,QAAM,WAAW,MAAM,QACpB,WAAW,WAAW,EACtB,UAAU,SAAS,qBAAqB,UAAU,EAClD,MAAM,uBAAuB,KAAK,QAAQ,EAC1C,MAAM,wBAAwB,MAAM,IAAI,EACxC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,QAAQ;AAEX,0BAAwB,YAAY,UAAU,eAAe;AAE7D,QAAM,WAAW,MAAM,QACpB,WAAW,WAAW,EACtB,UAAU,cAAc,qBAAqB,oBAAoB,EACjE,UAAU,SAAS,sBAAsB,UAAU,EACnD,MAAM,uBAAuB,KAAK,QAAQ,EAC1C,MAAM,wBAAwB,MAAM,IAAI,EACxC,MAAM,yBAAyB,MAAM,IAAI,EACzC,MAAM,oBAAoB,MAAM,IAAI,EACpC,MAAM,mBAAmB,KAAK,QAAQ,EACtC,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,QAAQ;AAEX,0BAAwB,YAAY,UAAU,eAAe;AAE7D,SAAO,MAAM,KAAK,UAAU;AAC9B;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import { BasicQueryEngine, resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
2
2
  import { HybridQueryEngine } from "./lib/engine.js";
3
+ import { CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY } from "@open-mercato/shared/lib/crud/types";
3
4
  function toEntityTypeFromEvent(event) {
4
5
  const parts = event.split(".");
5
6
  if (parts.length !== 3) return null;
@@ -61,6 +62,7 @@ function register(container) {
61
62
  }
62
63
  const makeUpsertHandler = (entityType) => async (payload, ctx) => {
63
64
  try {
65
+ if (payload?.[CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY] === true) return;
64
66
  const em = ctx.resolve("em");
65
67
  let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null;
66
68
  let tenantId = payload?.tenantId || ctx?.tenantId || null;
@@ -109,6 +111,7 @@ function register(container) {
109
111
  };
110
112
  const makeDeleteHandler = (entityType) => async (payload, ctx) => {
111
113
  try {
114
+ if (payload?.[CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY] === true) return;
112
115
  const em = ctx.resolve("em");
113
116
  let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null;
114
117
  let tenantId = payload?.tenantId || ctx?.tenantId || null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/query_index/di.ts"],
4
- "sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { BasicQueryEngine, resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { HybridQueryEngine } from './lib/engine'\nimport { markDeleted } from './lib/indexer'\nimport type { EventBus } from '@open-mercato/events'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\n\nfunction toEntityTypeFromEvent(event: string): string | null {\n // Expect '<module>.<entity>.<action>'\n const parts = event.split('.')\n if (parts.length !== 3) return null\n const [mod, ent] = parts\n return `${mod}:${ent}`\n}\n\nexport function register(container: AppContainer) {\n // Override queryEngine with hybrid that prefers JSONB index when available\n try {\n const em = (container.resolve('em') as any)\n const basic = new BasicQueryEngine(\n em,\n undefined,\n () => {\n try {\n return container.resolve('tenantEncryptionService') as any\n } catch {\n return null\n }\n },\n )\n const hybrid = new HybridQueryEngine(\n em,\n basic,\n () => {\n try {\n return (container.resolve('eventBus') as EventBus)\n } catch {\n return null\n }\n },\n () => {\n try {\n return (container.resolve('vectorIndexService') as VectorIndexService)\n } catch {\n return null\n }\n },\n () => {\n try {\n return container.resolve('tenantEncryptionService') as any\n } catch {\n return null\n }\n },\n )\n // Replace existing registration\n ;(container as any).register({ queryEngine: { resolve: () => hybrid } })\n } catch {}\n\n // Subscribe to CRUD events and forward to query_index subscribers for unified handling\n const setup = () => {\n let bus: any\n try { bus = (container.resolve('eventBus') as any) } catch { bus = null }\n if (!bus) { setTimeout(setup, 0); return }\n\n const makeUpsertHandler = (entityType: string) => async (payload: any, ctx: any) => {\n try {\n const em = ctx.resolve('em')\n let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null\n let tenantId = payload?.tenantId || ctx?.tenantId || null\n const id = String(payload?.id || payload?.recordId || '')\n if (!id) return\n if (!orgId || !tenantId) {\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['organization_id' as any, 'tenant_id' as any])\n .where('id' as any, '=', id)\n .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined\n orgId = row?.organization_id ?? orgId\n tenantId = row?.tenant_id ?? tenantId\n } catch {}\n }\n // Optional: only index when custom field definitions exist for this entity (org/global)\n try {\n const db = (em as any).getKysely()\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['id' as any])\n .where('entity_id' as any, '=', entityType)\n .where('is_active' as any, '=', true)\n if (orgId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', orgId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('organization_id' as any, 'is', null as any)\n }\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n const hasCf = await cfQuery.executeTakeFirst()\n if (!hasCf) return\n } catch {}\n try {\n const bus = ctx.resolve('eventBus') as any\n await bus.emitEvent('query_index.upsert_one', { entityType, recordId: id, organizationId: orgId, tenantId })\n } catch {}\n } catch {}\n }\n const makeDeleteHandler = (entityType: string) => async (payload: any, ctx: any) => {\n try {\n const em = ctx.resolve('em')\n let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null\n let tenantId = payload?.tenantId || ctx?.tenantId || null\n const id = String(payload?.id || payload?.recordId || '')\n if (!id) return\n if (!orgId || !tenantId) {\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['organization_id' as any, 'tenant_id' as any])\n .where('id' as any, '=', id)\n .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined\n orgId = row?.organization_id ?? orgId\n tenantId = row?.tenant_id ?? tenantId\n } catch {}\n }\n try {\n const bus = ctx.resolve('eventBus') as any\n await bus.emitEvent('query_index.delete_one', { entityType, recordId: id, organizationId: orgId, tenantId })\n } catch {}\n } catch {}\n }\n\n // Build list of entity ids to subscribe to\n try {\n const em = (container.resolve('em') as any)\n const db = (em as any).getKysely()\n const cfEntityIds: string[] = []\n db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .execute()\n .then((rows: any[]) => {\n for (const r of rows || []) cfEntityIds.push(String(r.entity_id))\n })\n .catch(() => {})\n .finally(() => {\n const proceed = (ids: string[]) => {\n for (const entityType of Array.from(new Set(ids))) {\n const [mod, ent] = entityType.split(':')\n if (!mod || !ent) continue\n bus.on(`${mod}.${ent}.created`, makeUpsertHandler(entityType), { moduleId: 'query_index' })\n bus.on(`${mod}.${ent}.updated`, makeUpsertHandler(entityType), { moduleId: 'query_index' })\n bus.on(`${mod}.${ent}.deleted`, makeDeleteHandler(entityType), { moduleId: 'query_index' })\n }\n }\n if (cfEntityIds.length > 0) {\n proceed(cfEntityIds)\n } else {\n // Fallback to generated entity ids without await\n import('#generated/entities.ids.generated').then((core) => {\n const flatten = (E: any): string[] => Object.values(E || {}).flatMap((o: any) => Object.values(o || {}) as string[])\n const guesses = new Set<string>([...flatten((core as any).E)])\n proceed(Array.from(guesses))\n }).catch(() => {})\n }\n })\n } catch {}\n }\n\n try { setup() } catch {}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { BasicQueryEngine, resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { HybridQueryEngine } from './lib/engine'\nimport { markDeleted } from './lib/indexer'\nimport type { EventBus } from '@open-mercato/events'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport { CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY } from '@open-mercato/shared/lib/crud/types'\n\nfunction toEntityTypeFromEvent(event: string): string | null {\n // Expect '<module>.<entity>.<action>'\n const parts = event.split('.')\n if (parts.length !== 3) return null\n const [mod, ent] = parts\n return `${mod}:${ent}`\n}\n\nexport function register(container: AppContainer) {\n // Override queryEngine with hybrid that prefers JSONB index when available\n try {\n const em = (container.resolve('em') as any)\n const basic = new BasicQueryEngine(\n em,\n undefined,\n () => {\n try {\n return container.resolve('tenantEncryptionService') as any\n } catch {\n return null\n }\n },\n )\n const hybrid = new HybridQueryEngine(\n em,\n basic,\n () => {\n try {\n return (container.resolve('eventBus') as EventBus)\n } catch {\n return null\n }\n },\n () => {\n try {\n return (container.resolve('vectorIndexService') as VectorIndexService)\n } catch {\n return null\n }\n },\n () => {\n try {\n return container.resolve('tenantEncryptionService') as any\n } catch {\n return null\n }\n },\n )\n // Replace existing registration\n ;(container as any).register({ queryEngine: { resolve: () => hybrid } })\n } catch {}\n\n // Subscribe to CRUD events and forward to query_index subscribers for unified handling\n const setup = () => {\n let bus: any\n try { bus = (container.resolve('eventBus') as any) } catch { bus = null }\n if (!bus) { setTimeout(setup, 0); return }\n\n const makeUpsertHandler = (entityType: string) => async (payload: any, ctx: any) => {\n try {\n // DataEngine emits the canonical query_index.upsert_one itself. The\n // bridge only covers domain events from write paths that do not own an\n // indexer, otherwise failures and error logs are duplicated.\n if (payload?.[CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY] === true) return\n const em = ctx.resolve('em')\n let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null\n let tenantId = payload?.tenantId || ctx?.tenantId || null\n const id = String(payload?.id || payload?.recordId || '')\n if (!id) return\n if (!orgId || !tenantId) {\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['organization_id' as any, 'tenant_id' as any])\n .where('id' as any, '=', id)\n .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined\n orgId = row?.organization_id ?? orgId\n tenantId = row?.tenant_id ?? tenantId\n } catch {}\n }\n // Optional: only index when custom field definitions exist for this entity (org/global)\n try {\n const db = (em as any).getKysely()\n let cfQuery = db\n .selectFrom('custom_field_defs' as any)\n .select(['id' as any])\n .where('entity_id' as any, '=', entityType)\n .where('is_active' as any, '=', true)\n if (orgId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('organization_id' as any, '=', orgId),\n eb('organization_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('organization_id' as any, 'is', null as any)\n }\n if (tenantId != null) {\n cfQuery = cfQuery.where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n } else {\n cfQuery = cfQuery.where('tenant_id' as any, 'is', null as any)\n }\n const hasCf = await cfQuery.executeTakeFirst()\n if (!hasCf) return\n } catch {}\n try {\n const bus = ctx.resolve('eventBus') as any\n await bus.emitEvent('query_index.upsert_one', { entityType, recordId: id, organizationId: orgId, tenantId })\n } catch {}\n } catch {}\n }\n const makeDeleteHandler = (entityType: string) => async (payload: any, ctx: any) => {\n try {\n if (payload?.[CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY] === true) return\n const em = ctx.resolve('em')\n let orgId = payload?.organizationId || payload?.orgId || ctx?.organizationId || null\n let tenantId = payload?.tenantId || ctx?.tenantId || null\n const id = String(payload?.id || payload?.recordId || '')\n if (!id) return\n if (!orgId || !tenantId) {\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['organization_id' as any, 'tenant_id' as any])\n .where('id' as any, '=', id)\n .executeTakeFirst() as { organization_id: string | null; tenant_id: string | null } | undefined\n orgId = row?.organization_id ?? orgId\n tenantId = row?.tenant_id ?? tenantId\n } catch {}\n }\n try {\n const bus = ctx.resolve('eventBus') as any\n await bus.emitEvent('query_index.delete_one', { entityType, recordId: id, organizationId: orgId, tenantId })\n } catch {}\n } catch {}\n }\n\n // Build list of entity ids to subscribe to\n try {\n const em = (container.resolve('em') as any)\n const db = (em as any).getKysely()\n const cfEntityIds: string[] = []\n db\n .selectFrom('custom_field_defs' as any)\n .select(['entity_id' as any])\n .distinct()\n .execute()\n .then((rows: any[]) => {\n for (const r of rows || []) cfEntityIds.push(String(r.entity_id))\n })\n .catch(() => {})\n .finally(() => {\n const proceed = (ids: string[]) => {\n for (const entityType of Array.from(new Set(ids))) {\n const [mod, ent] = entityType.split(':')\n if (!mod || !ent) continue\n bus.on(`${mod}.${ent}.created`, makeUpsertHandler(entityType), { moduleId: 'query_index' })\n bus.on(`${mod}.${ent}.updated`, makeUpsertHandler(entityType), { moduleId: 'query_index' })\n bus.on(`${mod}.${ent}.deleted`, makeDeleteHandler(entityType), { moduleId: 'query_index' })\n }\n }\n if (cfEntityIds.length > 0) {\n proceed(cfEntityIds)\n } else {\n // Fallback to generated entity ids without await\n import('#generated/entities.ids.generated').then((core) => {\n const flatten = (E: any): string[] => Object.values(E || {}).flatMap((o: any) => Object.values(o || {}) as string[])\n const guesses = new Set<string>([...flatten((core as any).E)])\n proceed(Array.from(guesses))\n }).catch(() => {})\n }\n })\n } catch {}\n }\n\n try { setup() } catch {}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["bus"]
7
7
  }
@@ -134,9 +134,9 @@ async function readCoverageSnapshots(db, batch) {
134
134
  }
135
135
  return result;
136
136
  }
137
- async function applyCoverageAdjustments(em, adjustments) {
137
+ async function applyCoverageAdjustments(em, adjustments, options) {
138
138
  if (!adjustments.length) return;
139
- const db = em.getKysely();
139
+ const db = options?.trx ?? em.getKysely();
140
140
  const aggregated = aggregateAdjustments(adjustments);
141
141
  for (const entry of aggregated) {
142
142
  const scope = entry.scope;