@open-mercato/core 0.6.7-develop.6842.1.8cd8a8883c → 0.6.7-develop.6859.1.e4727b568a

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 (115) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/bootstrap.js +16 -1
  3. package/dist/bootstrap.js.map +2 -2
  4. package/dist/generated/entities/attachment_quota_reservation/index.js +33 -0
  5. package/dist/generated/entities/attachment_quota_reservation/index.js.map +7 -0
  6. package/dist/generated/entities.ids.generated.js +2 -1
  7. package/dist/generated/entities.ids.generated.js.map +2 -2
  8. package/dist/generated/entity-fields-registry.js +17 -0
  9. package/dist/generated/entity-fields-registry.js.map +2 -2
  10. package/dist/modules/attachments/api/route.js +104 -8
  11. package/dist/modules/attachments/api/route.js.map +2 -2
  12. package/dist/modules/attachments/data/entities.js +67 -1
  13. package/dist/modules/attachments/data/entities.js.map +2 -2
  14. package/dist/modules/attachments/di.js +7 -1
  15. package/dist/modules/attachments/di.js.map +2 -2
  16. package/dist/modules/attachments/lib/drivers/index.js.map +1 -1
  17. package/dist/modules/attachments/lib/drivers/localDriver.js +19 -6
  18. package/dist/modules/attachments/lib/drivers/localDriver.js.map +2 -2
  19. package/dist/modules/attachments/lib/quota-recovery-queue.js +9 -0
  20. package/dist/modules/attachments/lib/quota-recovery-queue.js.map +7 -0
  21. package/dist/modules/attachments/lib/quota-service.js +224 -0
  22. package/dist/modules/attachments/lib/quota-service.js.map +7 -0
  23. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js +13 -0
  24. package/dist/modules/attachments/migrations/Migration20260709234741_attachments.js.map +7 -0
  25. package/dist/modules/attachments/workers/quota-recovery.js +45 -0
  26. package/dist/modules/attachments/workers/quota-recovery.js.map +7 -0
  27. package/dist/modules/auth/lib/setup-app.js +16 -2
  28. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  29. package/dist/modules/dashboards/services/widgetDataService.js +3 -4
  30. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  31. package/dist/modules/entities/api/definitions.js +2 -0
  32. package/dist/modules/entities/api/definitions.js.map +2 -2
  33. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +18 -3
  34. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  35. package/dist/modules/entities/cli.js +193 -19
  36. package/dist/modules/entities/cli.js.map +2 -2
  37. package/dist/modules/entities/components/RecordListRelationCell.js +63 -0
  38. package/dist/modules/entities/components/RecordListRelationCell.js.map +7 -0
  39. package/dist/modules/entities/components/recordListRelationDisplays.js +80 -0
  40. package/dist/modules/entities/components/recordListRelationDisplays.js.map +7 -0
  41. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +14 -1
  42. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  43. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +22 -17
  44. package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
  45. package/dist/modules/messages/api/[id]/route.js +15 -0
  46. package/dist/modules/messages/api/[id]/route.js.map +2 -2
  47. package/dist/modules/query_index/api/reindex.js +10 -0
  48. package/dist/modules/query_index/api/reindex.js.map +2 -2
  49. package/dist/modules/query_index/cli.js +125 -1
  50. package/dist/modules/query_index/cli.js.map +2 -2
  51. package/dist/modules/query_index/lib/coverage.js +19 -7
  52. package/dist/modules/query_index/lib/coverage.js.map +2 -2
  53. package/dist/modules/query_index/lib/reindexer.js +43 -37
  54. package/dist/modules/query_index/lib/reindexer.js.map +2 -2
  55. package/dist/modules/query_index/lib/subscriber-scope.js +2 -3
  56. package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
  57. package/dist/modules/sales/commands/documents.js +137 -22
  58. package/dist/modules/sales/commands/documents.js.map +2 -2
  59. package/dist/modules/sales/data/validators.js +2 -0
  60. package/dist/modules/sales/data/validators.js.map +2 -2
  61. package/dist/modules/sales/workflows.js +10 -7
  62. package/dist/modules/sales/workflows.js.map +2 -2
  63. package/dist/modules/workflows/lib/activity-executor.js +27 -0
  64. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  65. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js +10 -0
  66. package/dist/modules/workflows/widgets/injection/order-approval/context-keys.js.map +7 -0
  67. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +10 -6
  68. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  69. package/generated/entities/attachment_quota_reservation/index.ts +15 -0
  70. package/generated/entities.ids.generated.ts +2 -1
  71. package/generated/entity-fields-registry.ts +17 -0
  72. package/package.json +7 -7
  73. package/src/bootstrap.ts +17 -2
  74. package/src/modules/attachments/api/route.ts +114 -7
  75. package/src/modules/attachments/data/entities.ts +59 -0
  76. package/src/modules/attachments/di.ts +9 -1
  77. package/src/modules/attachments/i18n/de.json +3 -0
  78. package/src/modules/attachments/i18n/en.json +3 -0
  79. package/src/modules/attachments/i18n/es.json +3 -0
  80. package/src/modules/attachments/i18n/pl.json +3 -0
  81. package/src/modules/attachments/lib/drivers/index.ts +1 -1
  82. package/src/modules/attachments/lib/drivers/localDriver.ts +23 -8
  83. package/src/modules/attachments/lib/drivers/types.ts +5 -0
  84. package/src/modules/attachments/lib/quota-recovery-queue.ts +16 -0
  85. package/src/modules/attachments/lib/quota-service.ts +442 -0
  86. package/src/modules/attachments/migrations/.snapshot-open-mercato.json +648 -120
  87. package/src/modules/attachments/migrations/Migration20260709234741_attachments.ts +12 -0
  88. package/src/modules/attachments/workers/quota-recovery.ts +68 -0
  89. package/src/modules/auth/lib/setup-app.ts +20 -2
  90. package/src/modules/dashboards/services/widgetDataService.ts +3 -6
  91. package/src/modules/entities/api/definitions.ts +4 -0
  92. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +94 -77
  93. package/src/modules/entities/cli.ts +238 -21
  94. package/src/modules/entities/components/RecordListRelationCell.tsx +90 -0
  95. package/src/modules/entities/components/recordListRelationDisplays.ts +109 -0
  96. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +17 -1
  97. package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +21 -16
  98. package/src/modules/inbox_ops/components/proposals/types.ts +1 -0
  99. package/src/modules/messages/api/[id]/route.ts +21 -0
  100. package/src/modules/query_index/api/reindex.ts +15 -0
  101. package/src/modules/query_index/cli.ts +180 -1
  102. package/src/modules/query_index/lib/coverage.ts +38 -8
  103. package/src/modules/query_index/lib/reindexer.ts +62 -40
  104. package/src/modules/query_index/lib/subscriber-scope.ts +2 -7
  105. package/src/modules/sales/commands/documents.ts +237 -29
  106. package/src/modules/sales/data/validators.ts +2 -0
  107. package/src/modules/sales/workflows.ts +10 -7
  108. package/src/modules/workflows/i18n/de.json +1 -0
  109. package/src/modules/workflows/i18n/en.json +1 -0
  110. package/src/modules/workflows/i18n/es.json +1 -0
  111. package/src/modules/workflows/i18n/ko.json +1 -0
  112. package/src/modules/workflows/i18n/pl.json +1 -0
  113. package/src/modules/workflows/lib/activity-executor.ts +40 -1
  114. package/src/modules/workflows/widgets/injection/order-approval/context-keys.ts +18 -0
  115. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +15 -6
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/auth/lib/setup-app.ts"],
4
- "sourcesContent": ["import { hash } from 'bcryptjs'\nimport { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'\nimport { normalizeTenantId } from './tenantAccess'\nimport { computeEmailHash } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { getDefaultEncryptionMaps, type Module } from '@open-mercato/shared/modules/registry'\nimport { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'setup' })\n\nconst DEFAULT_ROLE_NAMES = ['employee', 'admin', 'superadmin'] as const\nconst DEMO_SUPERADMIN_EMAIL = 'superadmin@acme.com'\nconst DEFAULT_DERIVED_EMAIL_DOMAIN = DEMO_SUPERADMIN_EMAIL.split('@')[1] ?? 'acme.com'\n\nexport type EnsureRolesOptions = {\n roleNames?: string[]\n tenantId?: string\n}\n\nasync function ensureRolesInContext(\n em: EntityManager,\n roleNames: string[],\n tenantId: string,\n) {\n for (const name of roleNames) {\n const existing = await findOneWithDecryption(em, Role, { name, tenantId }, {}, { tenantId, organizationId: null })\n if (existing) continue\n em.persist(em.create(Role, { name, tenantId, createdAt: new Date() }))\n }\n}\n\nexport async function ensureRoles(em: EntityManager, options: EnsureRolesOptions = {}) {\n const roleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const tenantId = normalizeTenantId(options.tenantId ?? null)\n if (!tenantId) {\n throw new Error('ensureRoles requires a tenantId \u2014 global roles are not supported')\n }\n await em.transactional(async (tem) => {\n await ensureRolesInContext(tem, roleNames, tenantId)\n await tem.flush()\n })\n}\n\nasync function findRoleByName(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role | null> {\n const normalizedTenant = normalizeTenantId(tenantId ?? null) ?? null\n return findOneWithDecryption(em, Role, { name, tenantId: normalizedTenant }, {}, { tenantId: normalizedTenant, organizationId: null })\n}\n\nasync function findRoleByNameOrFail(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role> {\n const role = await findRoleByName(em, name, tenantId)\n if (!role) throw new Error(`ROLE_NOT_FOUND:${name}`)\n return role\n}\n\ntype PrimaryUserInput = {\n email: string\n password?: string\n hashedPassword?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n confirm?: boolean\n}\n\nconst DERIVED_EMAIL_ENV = {\n admin: 'OM_INIT_ADMIN_EMAIL',\n employee: 'OM_INIT_EMPLOYEE_EMAIL',\n} as const\n\nexport type DemoUserRole = 'superadmin' | 'admin' | 'employee'\nexport type DemoUserEmail = { role: DemoUserRole; email: string }\n\n/**\n * Returns the canonical list of demo user emails the setup path may have\n * seeded. Honors OM_INIT_ADMIN_EMAIL / OM_INIT_EMPLOYEE_EMAIL exactly the\n * same way the derived-user seeding branch does, so the deactivation loop\n * never drifts from the seeding loop.\n *\n * Pure function \u2014 no side effects, safe to unit-test in isolation.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport function resolveDemoUserEmails(): DemoUserEmail[] {\n const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n return [\n { role: 'superadmin', email: DEMO_SUPERADMIN_EMAIL },\n { role: 'admin', email: adminEmail },\n { role: 'employee', email: employeeEmail },\n ]\n}\n\nexport type SetupInitialTenantOptions = {\n orgName: string\n primaryUser: PrimaryUserInput\n roleNames?: string[]\n includeDerivedUsers?: boolean\n failIfUserExists?: boolean\n primaryUserRoles?: string[]\n includeSuperadminRole?: boolean\n /** Optional list of enabled modules. When provided, module setup hooks are called. */\n modules?: Module[]\n /**\n * Optional global slug to persist on the new Organization. When set, a\n * pre-flight uniqueness check across all tenants throws OrgSlugExistsError\n * on collision so scriptable callers can fail loudly instead of silently\n * reusing or clobbering an existing organization.\n */\n orgSlug?: string\n /**\n * Opt-in flag that allows seeding the derived admin/employee accounts when\n * the OM_INIT_ADMIN_PASSWORD / OM_INIT_EMPLOYEE_PASSWORD env vars are unset.\n *\n * - In non-production (`NODE_ENV !== 'production'`): the demo accounts are\n * seeded with the well-known `'secret'` password so local dev workflows\n * (e.g. `mercato init`) stay predictable for developers.\n * - In production: the fallback uses a randomly generated 96-bit password\n * (printed once by the CLI) so an operator who deliberately opts in still\n * avoids the historical hardcoded credential. When this flag is false/unset\n * in production and the env vars are missing, `DerivedUserPasswordRequiredError`\n * is thrown instead of silently seeding any account.\n */\n allowDemoDerivedPasswords?: boolean\n}\n\nexport class OrgSlugExistsError extends Error {\n constructor(public readonly slug: string) {\n super(`ORG_SLUG_EXISTS: an organization with slug \"${slug}\" already exists`)\n this.name = 'OrgSlugExistsError'\n }\n}\n\nexport class DerivedUserPasswordRequiredError extends Error {\n constructor(public readonly missing: string[]) {\n super(\n `DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(', ')} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`,\n )\n this.name = 'DerivedUserPasswordRequiredError'\n }\n}\n\nexport type SetupInitialTenantResult = {\n tenantId: string\n organizationId: string\n users: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }>\n reusedExistingUser: boolean\n}\n\nexport async function setupInitialTenant(\n em: EntityManager,\n options: SetupInitialTenantOptions,\n): Promise<SetupInitialTenantResult> {\n const {\n primaryUser,\n includeDerivedUsers = true,\n failIfUserExists = false,\n primaryUserRoles,\n includeSuperadminRole = true,\n } = options\n const primaryRolesInput = primaryUserRoles && primaryUserRoles.length ? primaryUserRoles : ['superadmin']\n const primaryRoles = includeSuperadminRole\n ? primaryRolesInput\n : primaryRolesInput.filter((role) => role !== 'superadmin')\n if (primaryRoles.length === 0) {\n throw new Error('PRIMARY_ROLES_REQUIRED')\n }\n const defaultRoleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const resolvedRoleNames = includeSuperadminRole\n ? defaultRoleNames\n : defaultRoleNames.filter((role) => role !== 'superadmin')\n const roleNames = Array.from(new Set([...resolvedRoleNames, ...primaryRoles]))\n const resolvedModules = options.modules ?? tryGetModules()\n const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules)\n\n const mainEmail = primaryUser.email\n const existingUser = await findOneWithDecryption(em, User, { email: mainEmail }, {}, { tenantId: null, organizationId: null })\n if (existingUser && failIfUserExists) {\n throw new Error('USER_EXISTS')\n }\n\n if (options.orgSlug) {\n const slugConflict = await findOneWithDecryption(\n em,\n Organization,\n { slug: options.orgSlug },\n {},\n { tenantId: null, organizationId: null },\n )\n if (slugConflict) {\n throw new OrgSlugExistsError(options.orgSlug)\n }\n }\n\n let tenantId: string | undefined\n let organizationId: string | undefined\n let reusedExistingUser = false\n const userSnapshots: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }> = []\n\n await em.transactional(async (tem) => {\n if (!existingUser) return\n reusedExistingUser = true\n tenantId = existingUser.tenantId ? String(existingUser.tenantId) : undefined\n organizationId = existingUser.organizationId ? String(existingUser.organizationId) : undefined\n const roleTenantId = normalizeTenantId(existingUser.tenantId ?? null) ?? null\n if (!roleTenantId) {\n throw new Error('Cannot reuse a user without a tenantId \u2014 global roles are not supported')\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n const requiredRoleSet = new Set([...roleNames, ...primaryRoles])\n const links = await findWithDecryption(\n tem,\n UserRole,\n { user: existingUser },\n { populate: ['role'] },\n { tenantId: roleTenantId, organizationId: null },\n )\n const currentRoles = new Set(links.map((link) => link.role.name))\n for (const roleName of requiredRoleSet) {\n if (!currentRoles.has(roleName)) {\n const role = await findRoleByNameOrFail(tem, roleName, roleTenantId)\n tem.persist(tem.create(UserRole, { user: existingUser, role, createdAt: new Date() }))\n }\n }\n await tem.flush()\n const roles = Array.from(new Set([...currentRoles, ...roleNames]))\n userSnapshots.push({ user: existingUser, roles, created: false })\n })\n\n if (!existingUser) {\n const baseUsers: Array<{\n email: string\n roles: string[]\n name?: string | null\n passwordHash?: string | null\n generatedPassword?: string | null\n }> = [\n { email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },\n ]\n if (includeDerivedUsers) {\n const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)\n const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)\n const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const envAdminPwd = readEnvValue('OM_INIT_ADMIN_PASSWORD')\n const envEmployeePwd = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')\n const isProduction = process.env.NODE_ENV === 'production'\n const allowDemo = options.allowDemoDerivedPasswords === true\n if (isProduction && !allowDemo) {\n const missing: string[] = []\n if (!envAdminPwd) missing.push('OM_INIT_ADMIN_PASSWORD')\n if (!envEmployeePwd) missing.push('OM_INIT_EMPLOYEE_PASSWORD')\n if (missing.length) {\n throw new DerivedUserPasswordRequiredError(missing)\n }\n }\n // In non-production, fall back to the well-known DEMO_DERIVED_PASSWORD so\n // local dev workflows stay predictable (admin@/employee@ login with the\n // documented demo password). In production we either have env-supplied\n // values, or \u2014 for opt-in --include-demo-users flows \u2014 we generate a\n // random one-time password and surface it via `generatedPassword` so the\n // operator can capture it. Production callers without env vars and\n // without the opt-in already threw above.\n const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd\n const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd\n const adminPasswordHash = await hash(adminPasswordPlain, 10)\n const employeePasswordHash = await hash(employeePasswordPlain, 10)\n addUniqueBaseUser(baseUsers, {\n email: adminEmail,\n roles: ['admin'],\n passwordHash: adminPasswordHash,\n generatedPassword: envAdminPwd ? null : adminPasswordPlain,\n })\n addUniqueBaseUser(baseUsers, {\n email: employeeEmail,\n roles: ['employee'],\n passwordHash: employeePasswordHash,\n generatedPassword: envEmployeePwd ? null : employeePasswordPlain,\n })\n }\n const passwordHash = await resolvePasswordHash(primaryUser)\n\n await em.transactional(async (tem) => {\n const tenant = tem.create(Tenant, {\n name: options.orgName,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(tenant)\n await tem.flush()\n\n const organization = tem.create(Organization, {\n name: options.orgName,\n slug: options.orgSlug ?? null,\n tenant,\n isActive: true,\n depth: 0,\n ancestorIds: [],\n childIds: [],\n descendantIds: [],\n logoPreserveAspectRatio: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(organization)\n await tem.flush()\n\n tenantId = String(tenant.id)\n organizationId = String(organization.id)\n const roleTenantId = tenantId\n\n if (isTenantDataEncryptionEnabled()) {\n try {\n const kms = createKmsService()\n if (kms.isHealthy()) {\n if (isEncryptionDebugEnabled()) {\n logger.info('Provisioning tenant DEK', { tenantId: String(tenant.id) })\n }\n await kms.createTenantDek(String(tenant.id))\n if (isEncryptionDebugEnabled()) {\n logger.info('Created tenant DEK during setup', { tenantId: String(tenant.id) })\n }\n } else {\n if (isEncryptionDebugEnabled()) {\n logger.warn('KMS not healthy, skipping tenant DEK creation', { tenantId: String(tenant.id) })\n }\n }\n } catch (err) {\n if (isEncryptionDebugEnabled()) {\n logger.warn('Failed to create tenant DEK', { err })\n }\n }\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n if (isTenantDataEncryptionEnabled()) {\n for (const spec of defaultEncryptionMaps) {\n const existing = await findOneWithDecryption(tem, EncryptionMap, { entityId: spec.entityId, tenantId: tenant.id, organizationId: organization.id, deletedAt: null }, {}, { tenantId: String(tenant.id), organizationId: String(organization.id) })\n if (!existing) {\n tem.persist(tem.create(EncryptionMap, {\n entityId: spec.entityId,\n tenantId: tenant.id,\n organizationId: organization.id,\n fieldsJson: spec.fields,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n }))\n } else {\n existing.fieldsJson = spec.fields\n existing.isActive = true\n }\n }\n await tem.flush()\n }\n })\n\n await em.transactional(async (tem) => {\n if (!tenantId || !organizationId) return\n const roleTenantId = tenantId\n const encryptionService = isTenantDataEncryptionEnabled()\n ? new TenantDataEncryptionService(tem as any, { kms: createKmsService() })\n : null\n if (encryptionService) {\n await encryptionService.invalidateMap('auth:user', String(tenantId), String(organizationId))\n await encryptionService.invalidateMap('auth:user', String(tenantId), null)\n }\n\n for (const base of baseUsers) {\n const resolvedPasswordHash = base.passwordHash ?? passwordHash\n let user = await findOneWithDecryption(tem, User, { email: base.email }, {}, { tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const confirm = primaryUser.confirm ?? true\n const encryptedPayload = encryptionService\n ? await encryptionService.encryptEntityPayload('auth:user', { email: base.email }, tenantId, organizationId)\n : { email: base.email, emailHash: computeEmailHash(base.email) }\n if (user) {\n user.passwordHash = resolvedPasswordHash\n user.organizationId = organizationId\n user.tenantId = tenantId\n if (isTenantDataEncryptionEnabled()) {\n user.email = encryptedPayload.email as any\n user.emailHash = (encryptedPayload as any).emailHash ?? computeEmailHash(base.email)\n }\n if (base.name) user.name = base.name\n if (confirm) user.isConfirmed = true\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null })\n } else {\n user = tem.create(User, {\n email: (encryptedPayload as any).email ?? base.email,\n emailHash: isTenantDataEncryptionEnabled() ? (encryptedPayload as any).emailHash ?? computeEmailHash(base.email) : undefined,\n passwordHash: resolvedPasswordHash,\n organizationId,\n tenantId,\n name: base.name ?? undefined,\n isConfirmed: confirm,\n createdAt: new Date(),\n })\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null })\n }\n await tem.flush()\n for (const roleName of base.roles) {\n const role = await findRoleByNameOrFail(tem, roleName, roleTenantId)\n const existingLink = await findOneWithDecryption(tem, UserRole, { user, role }, {}, { tenantId: tenantId ?? null, organizationId: null })\n if (!existingLink) tem.persist(tem.create(UserRole, { user, role, createdAt: new Date() }))\n }\n await tem.flush()\n }\n })\n }\n\n if (!tenantId || !organizationId) {\n throw new Error('SETUP_FAILED')\n }\n\n if (!reusedExistingUser) {\n await rebuildHierarchyForTenant(em, tenantId)\n }\n\n await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole })\n await deactivateDemoUsersIfSelfOnboardingEnabled(em)\n\n // Call module onTenantCreated hooks\n for (const mod of resolvedModules) {\n if (mod.setup?.onTenantCreated) {\n await mod.setup.onTenantCreated({ em, tenantId, organizationId })\n }\n }\n\n return {\n tenantId,\n organizationId,\n users: userSnapshots,\n reusedExistingUser,\n }\n}\n\nfunction resolvePrimaryName(input: PrimaryUserInput): string | null {\n if (input.displayName && input.displayName.trim()) return input.displayName.trim()\n const parts = [input.firstName, input.lastName].map((value) => value?.trim()).filter(Boolean)\n if (parts.length) return parts.join(' ')\n return null\n}\n\nfunction readEnvValue(key: string): string | undefined {\n const value = process.env[key]\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction addUniqueBaseUser(\n baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null }>,\n entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null },\n) {\n if (!entry.email) return\n const normalized = entry.email.toLowerCase()\n if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return\n baseUsers.push(entry)\n}\n\n/**\n * Well-known demo password seeded for derived admin@/employee@ accounts in\n * non-production (`NODE_ENV !== 'production'`) when no env override is set.\n * Keeps the documented `yarn dev` / `mercato init` DX predictable. Never used\n * in production: the production branch either consumes env-supplied values or\n * falls back to `generateDerivedPassword()` so credentials remain non-guessable.\n */\nconst DEMO_DERIVED_PASSWORD = 'secret'\n\n/**\n * Generate a 16-character base64url password (96 bits of entropy) for a derived\n * demo user in production when no env override is provided AND the caller\n * explicitly opted in via `allowDemoDerivedPasswords`. Surfaced via the\n * `users[].generatedPassword` snapshot so CLI callers can print it to the\n * operator \u2014 there is no other recovery path for these credentials.\n */\nfunction generateDerivedPassword(): string {\n return randomBytes(12).toString('base64url')\n}\n\nfunction isDemoModeEnabled(): boolean {\n const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')\n return parsed === false ? false : true\n}\n\nfunction shouldKeepDemoSuperadminDuringInit(): boolean {\n if (process.env.OM_INIT_FLOW !== 'true') return false\n if (!readEnvValue('OM_INIT_SUPERADMIN_EMAIL')) return false\n return isDemoModeEnabled()\n}\n\nasync function resolvePasswordHash(input: PrimaryUserInput): Promise<string | null> {\n if (typeof input.hashedPassword === 'string') return input.hashedPassword\n if (input.password) return hash(input.password, 10)\n return null\n}\n\nexport async function ensureDefaultRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n options: { includeSuperadminRole?: boolean } = {},\n) {\n const includeSuperadminRole = options.includeSuperadminRole ?? true\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const superadminRole = includeSuperadminRole ? await findRoleByName(em, 'superadmin', roleTenantId) : null\n const adminRole = await findRoleByName(em, 'admin', roleTenantId)\n const employeeRole = await findRoleByName(em, 'employee', roleTenantId)\n\n // Merge features from all enabled modules' setup configs\n const builtInRoles = ['superadmin', 'admin', 'employee'] as const\n const superadminFeatures: string[] = []\n const adminFeatures: string[] = []\n const employeeFeatures: string[] = []\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of modules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n if (roleFeatures.superadmin) superadminFeatures.push(...roleFeatures.superadmin)\n if (roleFeatures.admin) adminFeatures.push(...roleFeatures.admin)\n if (roleFeatures.employee) employeeFeatures.push(...roleFeatures.employee)\n\n // Collect features for custom roles (any key not in builtInRoles)\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if ((builtInRoles as readonly string[]).includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n logger.info('Seeded default role features', {\n superadmin: superadminFeatures,\n admin: adminFeatures,\n employee: employeeFeatures,\n ...(customRoleFeatures.size > 0\n ? Object.fromEntries(customRoleFeatures)\n : {}),\n })\n\n if (includeSuperadminRole && superadminRole) {\n await ensureRoleAclFor(em, superadminRole, tenantId, superadminFeatures, { isSuperAdmin: true })\n }\n if (adminRole) {\n await ensureRoleAclFor(em, adminRole, tenantId, adminFeatures)\n }\n if (employeeRole) {\n await ensureRoleAclFor(em, employeeRole, tenantId, employeeFeatures)\n }\n\n // Seed ACLs for custom roles defined by app modules.\n // NOTE: Custom roles may not exist yet if they are created in seedDefaults\n // (which runs after this function). In that case, use ensureCustomRoleAcls()\n // after seedDefaults to pick them up.\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n }\n }\n}\n\n/**\n * Seed ACLs for custom roles defined in module defaultRoleFeatures.\n * Call this AFTER seedDefaults to pick up roles created by app modules.\n * Safe to call multiple times \u2014 ensureRoleAclFor merges features idempotently.\n */\nexport async function ensureCustomRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules?: Module[],\n): Promise<void> {\n const resolvedModules = modules ?? tryGetModules()\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const builtInRoles = ['superadmin', 'admin', 'employee']\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of resolvedModules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if (builtInRoles.includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n if (customRoleFeatures.size === 0) return\n\n let seeded = 0\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n seeded++\n }\n }\n if (seeded > 0) {\n logger.info('Seeded custom role ACLs', { seeded })\n }\n}\n\nasync function ensureRoleAclFor(\n em: EntityManager,\n role: Role,\n tenantId: string,\n features: string[],\n options: { isSuperAdmin?: boolean } = {},\n) {\n const existing = await findOneWithDecryption(em, RoleAcl, { role, tenantId }, {}, { tenantId, organizationId: null })\n if (!existing) {\n const acl = em.create(RoleAcl, {\n role,\n tenantId,\n featuresJson: features,\n isSuperAdmin: !!options.isSuperAdmin,\n createdAt: new Date(),\n })\n await em.persist(acl).flush()\n return\n }\n const currentFeatures = Array.isArray(existing.featuresJson) ? existing.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...features]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) existing.featuresJson = merged\n if (options.isSuperAdmin && !existing.isSuperAdmin) {\n existing.isSuperAdmin = true\n }\n if (changed || options.isSuperAdmin) {\n await em.persist(existing).flush()\n }\n}\n\n/**\n * Neutralizes every demo account the setup path may have seeded\n * (superadmin, admin, employee \u2014 honoring OM_INIT_*_EMAIL overrides)\n * when SELF_SERVICE_ONBOARDING_ENABLED is on and the operator did not\n * opt in to keeping demo credentials via shouldKeepDemoSuperadminDuringInit.\n *\n * Each user is processed in its own try/catch so a single failure (e.g.\n * decryption error on a legacy row) does not skip the remaining accounts.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport async function deactivateDemoUsersIfSelfOnboardingEnabled(em: EntityManager) {\n if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return\n if (shouldKeepDemoSuperadminDuringInit()) return\n for (const { role, email } of resolveDemoUserEmails()) {\n try {\n const user = await findOneWithDecryption(\n em,\n User,\n { email },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!user) continue\n let dirty = false\n if (user.passwordHash) {\n user.passwordHash = null\n dirty = true\n }\n if (user.isConfirmed !== false) {\n user.isConfirmed = false\n dirty = true\n }\n if (dirty) {\n await em.persist(user).flush()\n }\n } catch (error) {\n logger.error('Failed to deactivate demo user', { role, email, err: error })\n }\n }\n}\n\n/**\n * @deprecated Renamed to {@link deactivateDemoUsersIfSelfOnboardingEnabled}\n * because the helper now neutralizes admin/employee demo accounts in addition\n * to superadmin (security tracker finding #5). Kept as an internal alias to\n * avoid breaking any out-of-tree caller that imported the old name; will be\n * removed in a future major.\n */\nconst deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled\n\n/** Try to get modules from runtime registry; returns empty array if not yet registered. */\nfunction tryGetModules(): Module[] {\n try {\n const { getModules } = require('@open-mercato/shared/lib/modules/registry')\n return getModules()\n } catch {\n return []\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { hash } from 'bcryptjs'\nimport { randomBytes } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { Role, RoleAcl, User, UserRole } from '@open-mercato/core/modules/auth/data/entities'\nimport { Tenant, Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport { rebuildHierarchyForTenant } from '@open-mercato/core/modules/directory/lib/hierarchy'\nimport { normalizeTenantId } from './tenantAccess'\nimport { computeEmailHash, emailHashLookupValues } from '@open-mercato/core/modules/auth/lib/emailHash'\nimport { getDefaultEncryptionMaps, type Module } from '@open-mercato/shared/modules/registry'\nimport { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport { EncryptionMap } from '@open-mercato/core/modules/entities/data/entities'\nimport { createKmsService } from '@open-mercato/shared/lib/encryption/kms'\nimport { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'setup' })\n\nconst DEFAULT_ROLE_NAMES = ['employee', 'admin', 'superadmin'] as const\nconst DEMO_SUPERADMIN_EMAIL = 'superadmin@acme.com'\nconst DEFAULT_DERIVED_EMAIL_DOMAIN = DEMO_SUPERADMIN_EMAIL.split('@')[1] ?? 'acme.com'\n\nexport type EnsureRolesOptions = {\n roleNames?: string[]\n tenantId?: string\n}\n\nasync function ensureRolesInContext(\n em: EntityManager,\n roleNames: string[],\n tenantId: string,\n) {\n for (const name of roleNames) {\n const existing = await findOneWithDecryption(em, Role, { name, tenantId }, {}, { tenantId, organizationId: null })\n if (existing) continue\n em.persist(em.create(Role, { name, tenantId, createdAt: new Date() }))\n }\n}\n\nexport async function ensureRoles(em: EntityManager, options: EnsureRolesOptions = {}) {\n const roleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const tenantId = normalizeTenantId(options.tenantId ?? null)\n if (!tenantId) {\n throw new Error('ensureRoles requires a tenantId \u2014 global roles are not supported')\n }\n await em.transactional(async (tem) => {\n await ensureRolesInContext(tem, roleNames, tenantId)\n await tem.flush()\n })\n}\n\nasync function findRoleByName(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role | null> {\n const normalizedTenant = normalizeTenantId(tenantId ?? null) ?? null\n return findOneWithDecryption(em, Role, { name, tenantId: normalizedTenant }, {}, { tenantId: normalizedTenant, organizationId: null })\n}\n\nasync function findRoleByNameOrFail(\n em: EntityManager,\n name: string,\n tenantId: string | null,\n): Promise<Role> {\n const role = await findRoleByName(em, name, tenantId)\n if (!role) throw new Error(`ROLE_NOT_FOUND:${name}`)\n return role\n}\n\ntype PrimaryUserInput = {\n email: string\n password?: string\n hashedPassword?: string | null\n firstName?: string | null\n lastName?: string | null\n displayName?: string | null\n confirm?: boolean\n}\n\nconst DERIVED_EMAIL_ENV = {\n admin: 'OM_INIT_ADMIN_EMAIL',\n employee: 'OM_INIT_EMPLOYEE_EMAIL',\n} as const\n\nexport type DemoUserRole = 'superadmin' | 'admin' | 'employee'\nexport type DemoUserEmail = { role: DemoUserRole; email: string }\n\n/**\n * Returns the canonical list of demo user emails the setup path may have\n * seeded. Honors OM_INIT_ADMIN_EMAIL / OM_INIT_EMPLOYEE_EMAIL exactly the\n * same way the derived-user seeding branch does, so the deactivation loop\n * never drifts from the seeding loop.\n *\n * Pure function \u2014 no side effects, safe to unit-test in isolation.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport function resolveDemoUserEmails(): DemoUserEmail[] {\n const adminEmail = readEnvValue(DERIVED_EMAIL_ENV.admin) ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = readEnvValue(DERIVED_EMAIL_ENV.employee) ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n return [\n { role: 'superadmin', email: DEMO_SUPERADMIN_EMAIL },\n { role: 'admin', email: adminEmail },\n { role: 'employee', email: employeeEmail },\n ]\n}\n\nexport type SetupInitialTenantOptions = {\n orgName: string\n primaryUser: PrimaryUserInput\n roleNames?: string[]\n includeDerivedUsers?: boolean\n failIfUserExists?: boolean\n primaryUserRoles?: string[]\n includeSuperadminRole?: boolean\n /** Optional list of enabled modules. When provided, module setup hooks are called. */\n modules?: Module[]\n /**\n * Optional global slug to persist on the new Organization. When set, a\n * pre-flight uniqueness check across all tenants throws OrgSlugExistsError\n * on collision so scriptable callers can fail loudly instead of silently\n * reusing or clobbering an existing organization.\n */\n orgSlug?: string\n /**\n * Opt-in flag that allows seeding the derived admin/employee accounts when\n * the OM_INIT_ADMIN_PASSWORD / OM_INIT_EMPLOYEE_PASSWORD env vars are unset.\n *\n * - In non-production (`NODE_ENV !== 'production'`): the demo accounts are\n * seeded with the well-known `'secret'` password so local dev workflows\n * (e.g. `mercato init`) stay predictable for developers.\n * - In production: the fallback uses a randomly generated 96-bit password\n * (printed once by the CLI) so an operator who deliberately opts in still\n * avoids the historical hardcoded credential. When this flag is false/unset\n * in production and the env vars are missing, `DerivedUserPasswordRequiredError`\n * is thrown instead of silently seeding any account.\n */\n allowDemoDerivedPasswords?: boolean\n}\n\nexport class OrgSlugExistsError extends Error {\n constructor(public readonly slug: string) {\n super(`ORG_SLUG_EXISTS: an organization with slug \"${slug}\" already exists`)\n this.name = 'OrgSlugExistsError'\n }\n}\n\nexport class DerivedUserPasswordRequiredError extends Error {\n constructor(public readonly missing: string[]) {\n super(\n `DERIVED_USER_PASSWORD_REQUIRED: missing ${missing.join(', ')} (set the env vars, pass allowDemoDerivedPasswords: true / --include-demo-users in non-production, or disable derived user seeding)`,\n )\n this.name = 'DerivedUserPasswordRequiredError'\n }\n}\n\nexport type SetupInitialTenantResult = {\n tenantId: string\n organizationId: string\n users: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }>\n reusedExistingUser: boolean\n}\n\nexport async function setupInitialTenant(\n em: EntityManager,\n options: SetupInitialTenantOptions,\n): Promise<SetupInitialTenantResult> {\n const {\n primaryUser,\n includeDerivedUsers = true,\n failIfUserExists = false,\n primaryUserRoles,\n includeSuperadminRole = true,\n } = options\n const primaryRolesInput = primaryUserRoles && primaryUserRoles.length ? primaryUserRoles : ['superadmin']\n const primaryRoles = includeSuperadminRole\n ? primaryRolesInput\n : primaryRolesInput.filter((role) => role !== 'superadmin')\n if (primaryRoles.length === 0) {\n throw new Error('PRIMARY_ROLES_REQUIRED')\n }\n const defaultRoleNames = options.roleNames ?? [...DEFAULT_ROLE_NAMES]\n const resolvedRoleNames = includeSuperadminRole\n ? defaultRoleNames\n : defaultRoleNames.filter((role) => role !== 'superadmin')\n const roleNames = Array.from(new Set([...resolvedRoleNames, ...primaryRoles]))\n const resolvedModules = options.modules ?? tryGetModules()\n const defaultEncryptionMaps = getDefaultEncryptionMaps(resolvedModules)\n\n const mainEmail = primaryUser.email\n const existingUserByHash = await findOneWithDecryption(\n em,\n User,\n { emailHash: { $in: emailHashLookupValues(mainEmail) }, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n const existingUser = existingUserByHash ?? await findOneWithDecryption(\n em,\n User,\n { email: mainEmail, deletedAt: null },\n {},\n { tenantId: null, organizationId: null },\n )\n if (existingUser && failIfUserExists) {\n throw new Error('USER_EXISTS')\n }\n\n if (options.orgSlug) {\n const slugConflict = await findOneWithDecryption(\n em,\n Organization,\n { slug: options.orgSlug },\n {},\n { tenantId: null, organizationId: null },\n )\n if (slugConflict) {\n throw new OrgSlugExistsError(options.orgSlug)\n }\n }\n\n let tenantId: string | undefined\n let organizationId: string | undefined\n let reusedExistingUser = false\n const userSnapshots: Array<{ user: User; roles: string[]; created: boolean; generatedPassword?: string | null }> = []\n\n await em.transactional(async (tem) => {\n if (!existingUser) return\n reusedExistingUser = true\n tenantId = existingUser.tenantId ? String(existingUser.tenantId) : undefined\n organizationId = existingUser.organizationId ? String(existingUser.organizationId) : undefined\n const roleTenantId = normalizeTenantId(existingUser.tenantId ?? null) ?? null\n if (!roleTenantId) {\n throw new Error('Cannot reuse a user without a tenantId \u2014 global roles are not supported')\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n const requiredRoleSet = new Set([...roleNames, ...primaryRoles])\n const links = await findWithDecryption(\n tem,\n UserRole,\n { user: existingUser },\n { populate: ['role'] },\n { tenantId: roleTenantId, organizationId: null },\n )\n const currentRoles = new Set(links.map((link) => link.role.name))\n for (const roleName of requiredRoleSet) {\n if (!currentRoles.has(roleName)) {\n const role = await findRoleByNameOrFail(tem, roleName, roleTenantId)\n tem.persist(tem.create(UserRole, { user: existingUser, role, createdAt: new Date() }))\n }\n }\n await tem.flush()\n const roles = Array.from(new Set([...currentRoles, ...roleNames]))\n userSnapshots.push({ user: existingUser, roles, created: false })\n })\n\n if (!existingUser) {\n const baseUsers: Array<{\n email: string\n roles: string[]\n name?: string | null\n passwordHash?: string | null\n generatedPassword?: string | null\n }> = [\n { email: primaryUser.email, roles: primaryRoles, name: resolvePrimaryName(primaryUser) },\n ]\n if (includeDerivedUsers) {\n const adminOverride = readEnvValue(DERIVED_EMAIL_ENV.admin)\n const employeeOverride = readEnvValue(DERIVED_EMAIL_ENV.employee)\n const adminEmail = adminOverride ?? `admin@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const employeeEmail = employeeOverride ?? `employee@${DEFAULT_DERIVED_EMAIL_DOMAIN}`\n const envAdminPwd = readEnvValue('OM_INIT_ADMIN_PASSWORD')\n const envEmployeePwd = readEnvValue('OM_INIT_EMPLOYEE_PASSWORD')\n const isProduction = process.env.NODE_ENV === 'production'\n const allowDemo = options.allowDemoDerivedPasswords === true\n if (isProduction && !allowDemo) {\n const missing: string[] = []\n if (!envAdminPwd) missing.push('OM_INIT_ADMIN_PASSWORD')\n if (!envEmployeePwd) missing.push('OM_INIT_EMPLOYEE_PASSWORD')\n if (missing.length) {\n throw new DerivedUserPasswordRequiredError(missing)\n }\n }\n // In non-production, fall back to the well-known DEMO_DERIVED_PASSWORD so\n // local dev workflows stay predictable (admin@/employee@ login with the\n // documented demo password). In production we either have env-supplied\n // values, or \u2014 for opt-in --include-demo-users flows \u2014 we generate a\n // random one-time password and surface it via `generatedPassword` so the\n // operator can capture it. Production callers without env vars and\n // without the opt-in already threw above.\n const fallbackAdminPwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const fallbackEmployeePwd = isProduction ? generateDerivedPassword() : DEMO_DERIVED_PASSWORD\n const adminPasswordPlain = envAdminPwd ?? fallbackAdminPwd\n const employeePasswordPlain = envEmployeePwd ?? fallbackEmployeePwd\n const adminPasswordHash = await hash(adminPasswordPlain, 10)\n const employeePasswordHash = await hash(employeePasswordPlain, 10)\n addUniqueBaseUser(baseUsers, {\n email: adminEmail,\n roles: ['admin'],\n passwordHash: adminPasswordHash,\n generatedPassword: envAdminPwd ? null : adminPasswordPlain,\n })\n addUniqueBaseUser(baseUsers, {\n email: employeeEmail,\n roles: ['employee'],\n passwordHash: employeePasswordHash,\n generatedPassword: envEmployeePwd ? null : employeePasswordPlain,\n })\n }\n const passwordHash = await resolvePasswordHash(primaryUser)\n\n await em.transactional(async (tem) => {\n const tenant = tem.create(Tenant, {\n name: options.orgName,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(tenant)\n await tem.flush()\n\n const organization = tem.create(Organization, {\n name: options.orgName,\n slug: options.orgSlug ?? null,\n tenant,\n isActive: true,\n depth: 0,\n ancestorIds: [],\n childIds: [],\n descendantIds: [],\n logoPreserveAspectRatio: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n tem.persist(organization)\n await tem.flush()\n\n tenantId = String(tenant.id)\n organizationId = String(organization.id)\n const roleTenantId = tenantId\n\n if (isTenantDataEncryptionEnabled()) {\n try {\n const kms = createKmsService()\n if (kms.isHealthy()) {\n if (isEncryptionDebugEnabled()) {\n logger.info('Provisioning tenant DEK', { tenantId: String(tenant.id) })\n }\n await kms.createTenantDek(String(tenant.id))\n if (isEncryptionDebugEnabled()) {\n logger.info('Created tenant DEK during setup', { tenantId: String(tenant.id) })\n }\n } else {\n if (isEncryptionDebugEnabled()) {\n logger.warn('KMS not healthy, skipping tenant DEK creation', { tenantId: String(tenant.id) })\n }\n }\n } catch (err) {\n if (isEncryptionDebugEnabled()) {\n logger.warn('Failed to create tenant DEK', { err })\n }\n }\n }\n\n await ensureRolesInContext(tem, roleNames, roleTenantId)\n await tem.flush()\n\n if (isTenantDataEncryptionEnabled()) {\n // System-scoped maps are resolved from module code, never from a tenant row.\n // Persisting one here would make the tenant-scoped encryption CLIs believe they\n // own that entity and re-wrap its `system:<entityId>` ciphertext under the tenant\n // DEK, which runtime decryption can no longer read.\n for (const spec of defaultEncryptionMaps) {\n if (spec.keyScope === 'system') continue\n const existing = await findOneWithDecryption(tem, EncryptionMap, { entityId: spec.entityId, tenantId: tenant.id, organizationId: organization.id, deletedAt: null }, {}, { tenantId: String(tenant.id), organizationId: String(organization.id) })\n if (!existing) {\n tem.persist(tem.create(EncryptionMap, {\n entityId: spec.entityId,\n tenantId: tenant.id,\n organizationId: organization.id,\n fieldsJson: spec.fields,\n isActive: true,\n createdAt: new Date(),\n updatedAt: new Date(),\n }))\n } else {\n existing.fieldsJson = spec.fields\n existing.isActive = true\n }\n }\n await tem.flush()\n }\n })\n\n await em.transactional(async (tem) => {\n if (!tenantId || !organizationId) return\n const roleTenantId = tenantId\n const encryptionService = isTenantDataEncryptionEnabled()\n ? new TenantDataEncryptionService(tem as any, { kms: createKmsService() })\n : null\n if (encryptionService) {\n await encryptionService.invalidateMap('auth:user', String(tenantId), String(organizationId))\n await encryptionService.invalidateMap('auth:user', String(tenantId), null)\n }\n\n for (const base of baseUsers) {\n const resolvedPasswordHash = base.passwordHash ?? passwordHash\n let user = await findOneWithDecryption(tem, User, { email: base.email }, {}, { tenantId: tenantId ?? null, organizationId: organizationId ?? null })\n const confirm = primaryUser.confirm ?? true\n const encryptedPayload = encryptionService\n ? await encryptionService.encryptEntityPayload('auth:user', { email: base.email }, tenantId, organizationId)\n : { email: base.email, emailHash: computeEmailHash(base.email) }\n if (user) {\n user.passwordHash = resolvedPasswordHash\n user.organizationId = organizationId\n user.tenantId = tenantId\n if (isTenantDataEncryptionEnabled()) {\n user.email = encryptedPayload.email as any\n user.emailHash = (encryptedPayload as any).emailHash ?? computeEmailHash(base.email)\n }\n if (base.name) user.name = base.name\n if (confirm) user.isConfirmed = true\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: false, generatedPassword: base.generatedPassword ?? null })\n } else {\n user = tem.create(User, {\n email: (encryptedPayload as any).email ?? base.email,\n emailHash: isTenantDataEncryptionEnabled() ? (encryptedPayload as any).emailHash ?? computeEmailHash(base.email) : undefined,\n passwordHash: resolvedPasswordHash,\n organizationId,\n tenantId,\n name: base.name ?? undefined,\n isConfirmed: confirm,\n createdAt: new Date(),\n })\n tem.persist(user)\n userSnapshots.push({ user, roles: base.roles, created: true, generatedPassword: base.generatedPassword ?? null })\n }\n await tem.flush()\n for (const roleName of base.roles) {\n const role = await findRoleByNameOrFail(tem, roleName, roleTenantId)\n const existingLink = await findOneWithDecryption(tem, UserRole, { user, role }, {}, { tenantId: tenantId ?? null, organizationId: null })\n if (!existingLink) tem.persist(tem.create(UserRole, { user, role, createdAt: new Date() }))\n }\n await tem.flush()\n }\n })\n }\n\n if (!tenantId || !organizationId) {\n throw new Error('SETUP_FAILED')\n }\n\n if (!reusedExistingUser) {\n await rebuildHierarchyForTenant(em, tenantId)\n }\n\n await ensureDefaultRoleAcls(em, tenantId, resolvedModules, { includeSuperadminRole })\n await deactivateDemoUsersIfSelfOnboardingEnabled(em)\n\n // Call module onTenantCreated hooks\n for (const mod of resolvedModules) {\n if (mod.setup?.onTenantCreated) {\n await mod.setup.onTenantCreated({ em, tenantId, organizationId })\n }\n }\n\n return {\n tenantId,\n organizationId,\n users: userSnapshots,\n reusedExistingUser,\n }\n}\n\nfunction resolvePrimaryName(input: PrimaryUserInput): string | null {\n if (input.displayName && input.displayName.trim()) return input.displayName.trim()\n const parts = [input.firstName, input.lastName].map((value) => value?.trim()).filter(Boolean)\n if (parts.length) return parts.join(' ')\n return null\n}\n\nfunction readEnvValue(key: string): string | undefined {\n const value = process.env[key]\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nfunction addUniqueBaseUser(\n baseUsers: Array<{ email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null }>,\n entry: { email: string; roles: string[]; name?: string | null; passwordHash?: string | null; generatedPassword?: string | null },\n) {\n if (!entry.email) return\n const normalized = entry.email.toLowerCase()\n if (baseUsers.some((user) => user.email.toLowerCase() === normalized)) return\n baseUsers.push(entry)\n}\n\n/**\n * Well-known demo password seeded for derived admin@/employee@ accounts in\n * non-production (`NODE_ENV !== 'production'`) when no env override is set.\n * Keeps the documented `yarn dev` / `mercato init` DX predictable. Never used\n * in production: the production branch either consumes env-supplied values or\n * falls back to `generateDerivedPassword()` so credentials remain non-guessable.\n */\nconst DEMO_DERIVED_PASSWORD = 'secret'\n\n/**\n * Generate a 16-character base64url password (96 bits of entropy) for a derived\n * demo user in production when no env override is provided AND the caller\n * explicitly opted in via `allowDemoDerivedPasswords`. Surfaced via the\n * `users[].generatedPassword` snapshot so CLI callers can print it to the\n * operator \u2014 there is no other recovery path for these credentials.\n */\nfunction generateDerivedPassword(): string {\n return randomBytes(12).toString('base64url')\n}\n\nfunction isDemoModeEnabled(): boolean {\n const parsed = parseBooleanToken(process.env.DEMO_MODE ?? '')\n return parsed === false ? false : true\n}\n\nfunction shouldKeepDemoSuperadminDuringInit(): boolean {\n if (process.env.OM_INIT_FLOW !== 'true') return false\n if (!readEnvValue('OM_INIT_SUPERADMIN_EMAIL')) return false\n return isDemoModeEnabled()\n}\n\nasync function resolvePasswordHash(input: PrimaryUserInput): Promise<string | null> {\n if (typeof input.hashedPassword === 'string') return input.hashedPassword\n if (input.password) return hash(input.password, 10)\n return null\n}\n\nexport async function ensureDefaultRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules: Module[],\n options: { includeSuperadminRole?: boolean } = {},\n) {\n const includeSuperadminRole = options.includeSuperadminRole ?? true\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const superadminRole = includeSuperadminRole ? await findRoleByName(em, 'superadmin', roleTenantId) : null\n const adminRole = await findRoleByName(em, 'admin', roleTenantId)\n const employeeRole = await findRoleByName(em, 'employee', roleTenantId)\n\n // Merge features from all enabled modules' setup configs\n const builtInRoles = ['superadmin', 'admin', 'employee'] as const\n const superadminFeatures: string[] = []\n const adminFeatures: string[] = []\n const employeeFeatures: string[] = []\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of modules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n if (roleFeatures.superadmin) superadminFeatures.push(...roleFeatures.superadmin)\n if (roleFeatures.admin) adminFeatures.push(...roleFeatures.admin)\n if (roleFeatures.employee) employeeFeatures.push(...roleFeatures.employee)\n\n // Collect features for custom roles (any key not in builtInRoles)\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if ((builtInRoles as readonly string[]).includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n logger.info('Seeded default role features', {\n superadmin: superadminFeatures,\n admin: adminFeatures,\n employee: employeeFeatures,\n ...(customRoleFeatures.size > 0\n ? Object.fromEntries(customRoleFeatures)\n : {}),\n })\n\n if (includeSuperadminRole && superadminRole) {\n await ensureRoleAclFor(em, superadminRole, tenantId, superadminFeatures, { isSuperAdmin: true })\n }\n if (adminRole) {\n await ensureRoleAclFor(em, adminRole, tenantId, adminFeatures)\n }\n if (employeeRole) {\n await ensureRoleAclFor(em, employeeRole, tenantId, employeeFeatures)\n }\n\n // Seed ACLs for custom roles defined by app modules.\n // NOTE: Custom roles may not exist yet if they are created in seedDefaults\n // (which runs after this function). In that case, use ensureCustomRoleAcls()\n // after seedDefaults to pick them up.\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n }\n }\n}\n\n/**\n * Seed ACLs for custom roles defined in module defaultRoleFeatures.\n * Call this AFTER seedDefaults to pick up roles created by app modules.\n * Safe to call multiple times \u2014 ensureRoleAclFor merges features idempotently.\n */\nexport async function ensureCustomRoleAcls(\n em: EntityManager,\n tenantId: string,\n modules?: Module[],\n): Promise<void> {\n const resolvedModules = modules ?? tryGetModules()\n const roleTenantId = normalizeTenantId(tenantId) ?? null\n const builtInRoles = ['superadmin', 'admin', 'employee']\n const customRoleFeatures = new Map<string, string[]>()\n\n for (const mod of resolvedModules) {\n const roleFeatures = mod.setup?.defaultRoleFeatures\n if (!roleFeatures) continue\n for (const [roleName, features] of Object.entries(roleFeatures)) {\n if (builtInRoles.includes(roleName)) continue\n if (!Array.isArray(features)) continue\n const existing = customRoleFeatures.get(roleName) ?? []\n existing.push(...features)\n customRoleFeatures.set(roleName, existing)\n }\n }\n\n if (customRoleFeatures.size === 0) return\n\n let seeded = 0\n for (const [roleName, features] of customRoleFeatures) {\n const role = await findRoleByName(em, roleName, roleTenantId)\n if (role) {\n await ensureRoleAclFor(em, role, tenantId, features)\n seeded++\n }\n }\n if (seeded > 0) {\n logger.info('Seeded custom role ACLs', { seeded })\n }\n}\n\nasync function ensureRoleAclFor(\n em: EntityManager,\n role: Role,\n tenantId: string,\n features: string[],\n options: { isSuperAdmin?: boolean } = {},\n) {\n const existing = await findOneWithDecryption(em, RoleAcl, { role, tenantId }, {}, { tenantId, organizationId: null })\n if (!existing) {\n const acl = em.create(RoleAcl, {\n role,\n tenantId,\n featuresJson: features,\n isSuperAdmin: !!options.isSuperAdmin,\n createdAt: new Date(),\n })\n await em.persist(acl).flush()\n return\n }\n const currentFeatures = Array.isArray(existing.featuresJson) ? existing.featuresJson : []\n const merged = Array.from(new Set([...currentFeatures, ...features]))\n const changed =\n merged.length !== currentFeatures.length ||\n merged.some((value, index) => value !== currentFeatures[index])\n if (changed) existing.featuresJson = merged\n if (options.isSuperAdmin && !existing.isSuperAdmin) {\n existing.isSuperAdmin = true\n }\n if (changed || options.isSuperAdmin) {\n await em.persist(existing).flush()\n }\n}\n\n/**\n * Neutralizes every demo account the setup path may have seeded\n * (superadmin, admin, employee \u2014 honoring OM_INIT_*_EMAIL overrides)\n * when SELF_SERVICE_ONBOARDING_ENABLED is on and the operator did not\n * opt in to keeping demo credentials via shouldKeepDemoSuperadminDuringInit.\n *\n * Each user is processed in its own try/catch so a single failure (e.g.\n * decryption error on a legacy row) does not skip the remaining accounts.\n *\n * @internal Exported for tests; not part of the public auth API.\n */\nexport async function deactivateDemoUsersIfSelfOnboardingEnabled(em: EntityManager) {\n if (process.env.SELF_SERVICE_ONBOARDING_ENABLED !== 'true') return\n if (shouldKeepDemoSuperadminDuringInit()) return\n for (const { role, email } of resolveDemoUserEmails()) {\n try {\n const user = await findOneWithDecryption(\n em,\n User,\n { email },\n {},\n { tenantId: null, organizationId: null },\n )\n if (!user) continue\n let dirty = false\n if (user.passwordHash) {\n user.passwordHash = null\n dirty = true\n }\n if (user.isConfirmed !== false) {\n user.isConfirmed = false\n dirty = true\n }\n if (dirty) {\n await em.persist(user).flush()\n }\n } catch (error) {\n logger.error('Failed to deactivate demo user', { role, email, err: error })\n }\n }\n}\n\n/**\n * @deprecated Renamed to {@link deactivateDemoUsersIfSelfOnboardingEnabled}\n * because the helper now neutralizes admin/employee demo accounts in addition\n * to superadmin (security tracker finding #5). Kept as an internal alias to\n * avoid breaking any out-of-tree caller that imported the old name; will be\n * removed in a future major.\n */\nconst deactivateDemoSuperAdminIfSelfOnboardingEnabled = deactivateDemoUsersIfSelfOnboardingEnabled\n\n/** Try to get modules from runtime registry; returns empty array if not yet registered. */\nfunction tryGetModules(): Module[] {\n try {\n const { getModules } = require('@open-mercato/shared/lib/modules/registry')\n return getModules()\n } catch {\n return []\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ import { decryptWithAesGcm } from "@open-mercato/shared/lib/encryption/aes";
3
3
  import { resolveTenantEncryptionService } from "@open-mercato/shared/lib/encryption/customFieldValues";
4
4
  import { resolveEntityIdFromMetadata } from "@open-mercato/shared/lib/encryption/entityIds";
5
5
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { listEntityMetadata } from "@open-mercato/shared/lib/db/entityMetadata";
6
7
  import {
7
8
  resolveDateRange,
8
9
  getPreviousPeriod,
@@ -582,10 +583,8 @@ class WidgetDataService {
582
583
  return this.scope.organizationIds[0] ?? null;
583
584
  }
584
585
  resolveEntityMetadata(tableName) {
585
- const registry = this.em?.getMetadata?.();
586
- if (!registry) return null;
587
- const entries = typeof registry.getAll === "function" && registry.getAll() || (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}));
588
- const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {});
586
+ const metas = listEntityMetadata(this.em);
587
+ if (!metas.length) return null;
589
588
  const match = metas.find((meta) => {
590
589
  const table = meta?.tableName ?? meta?.collection;
591
590
  if (typeof table !== "string") return false;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/dashboards/services/widgetDataService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TenantDek } from '@open-mercato/shared/lib/encryption/kms'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n buildDistinctCurrencyQuery,\n buildGroupSourceRowsQuery,\n resolveGroupExpression,\n} from '../lib/aggregations'\nimport {\n type ExactDecimal,\n EXACT_DECIMAL_ZERO,\n addExactDecimal,\n compareExactDecimal,\n exactDecimalToNumber,\n parseExactDecimal,\n} from '../lib/exactDecimal'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\nimport type { BaseCurrencyResolver } from '../lib/optionalBaseCurrency'\n\nconst logger = createLogger('dashboards').child({ component: 'widget-data-service' })\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\n/**\n * Encrypted group sources cannot be grouped by the database, so the rows are scanned and\n * aggregated in application code. The cap keeps that scan bounded; overflowing it fails loudly\n * instead of charting a silently truncated result (#4622).\n */\nconst ENCRYPTED_GROUP_SCAN_LIMIT = 20_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nexport class WidgetDataScanLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataScanLimitError'\n }\n}\n\n/**\n * Raised when encryption is configured but the group source cannot currently be resolved \u2014 an\n * unhealthy KMS, an unreadable encryption map, or a missing tenant DEK. Grouping would either read\n * ciphertext in SQL or silently collapse every encrypted row into \"Unknown\", so the request fails\n * closed instead (#4622).\n */\nexport class WidgetDataEncryptionUnavailableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataEncryptionUnavailableError'\n }\n}\n\ntype EncryptedGroupSource = {\n entityId: string\n dbColumn: string\n jsonPath: string | null\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\n/**\n * Encryption maps may register a field in either casing (`findKey` in the encryption service\n * accepts both), so match the analytics column tolerantly rather than by exact snake_case.\n */\nfunction matchesColumn(fieldName: string, dbColumn: string): boolean {\n const normalize = (value: string) => value.replace(/_/g, '').toLowerCase()\n return normalize(fieldName) === normalize(dbColumn)\n}\n\nfunction readJsonPath(value: unknown, path: string): unknown {\n let current = value\n for (const part of path.split('.')) {\n if (current === null || typeof current !== 'object') return null\n current = (current as Record<string, unknown>)[part]\n }\n return current\n}\n\n/**\n * Running aggregate state for one group. Values are folded in as they are scanned \u2014 the bucket\n * never holds the row set \u2014 so the scan cost stays linear and independent of the row cap.\n */\ntype GroupBucket = {\n /** Rows with a non-null metric column, mirroring SQL `COUNT(column)`. */\n count: number\n /** Rows whose metric parsed as a decimal, mirroring the row set SQL aggregates operate on. */\n numericCount: number\n /** Exact running sum, so money never accumulates in binary floating point. */\n sum: ExactDecimal\n min: ExactDecimal | null\n max: ExactDecimal | null\n}\n\nfunction createGroupBucket(): GroupBucket {\n return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null }\n}\n\nfunction foldMetricValue(bucket: GroupBucket, value: ExactDecimal): void {\n bucket.numericCount += 1\n bucket.sum = addExactDecimal(bucket.sum, value)\n if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value\n if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value\n}\n\n/**\n * Mirrors the SQL semantics of `buildAggregateExpression` for application-side aggregation. That\n * expression wraps `SUM`/`AVG` in `COALESCE(..., 0)`, so an empty value set aggregates to `0` on\n * both paths, while the uncoalesced `MIN`/`MAX` keep PostgreSQL's `NULL`. An encrypted group source\n * must not report a different value than the same data would report in plaintext (#4622).\n */\nfunction aggregateBucket(aggregate: AggregateFunction, bucket: GroupBucket): number | null {\n switch (aggregate) {\n case 'count':\n return bucket.count\n case 'sum':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum)\n case 'avg':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount\n case 'min':\n return bucket.min === null ? null : exactDecimalToNumber(bucket.min)\n case 'max':\n return bucket.max === null ? null : exactDecimalToNumber(bucket.max)\n default:\n return bucket.count\n }\n}\n\n/**\n * Mirrors the SQL path's `ORDER BY value DESC NULLS LAST`. PostgreSQL defaults DESC to NULLS FIRST,\n * which would let empty buckets displace real ones under a group limit, so both paths state the\n * policy explicitly: highest value first, buckets without a value last.\n */\nfunction compareWidgetDataItemsByValueDesc(\n left: { value: number | null },\n right: { value: number | null },\n): number {\n if (left.value === null && right.value === null) return 0\n if (left.value === null) return 1\n if (right.value === null) return -1\n return right.value - left.value\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n currency?: string | null\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n baseCurrencyResolver?: BaseCurrencyResolver\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n private baseCurrencyResolver?: BaseCurrencyResolver\n private baseCurrencyPromise?: Promise<string | null>\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n this.baseCurrencyResolver = options.baseCurrencyResolver\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const aggregatedRanges =\n shouldFetchComparison && comparisonRange ? [dateRangeResolved, comparisonRange] : [dateRangeResolved]\n\n const [mainResult, comparisonResult, currency] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\n this.resolveCurrencyLabel(request, aggregatedRanges),\n ])\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n currency,\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\n } catch {\n }\n }\n\n return response\n }\n\n /**\n * Resolves the currency the response may be labelled with. The base currency of the\n * scope is only half the answer: it describes how the organizations are configured, not\n * what the aggregated rows are actually denominated in. A PLN-based organization holding\n * both PLN and EUR orders would otherwise sum them and present the total as PLN (#4676),\n * so the base currency survives only when every aggregated row carries exactly that code.\n *\n * The check is deliberately conservative \u2014 anything it cannot prove resolves to `null`\n * and the widgets render an explicitly unlabelled number, which is recoverable, while a\n * confident wrong label is not.\n */\n private async resolveCurrencyLabel(\n request: WidgetDataRequest,\n aggregatedRanges: Array<{ start: Date; end: Date } | undefined>,\n ): Promise<string | null> {\n const baseCurrency = await this.resolveBaseCurrency()\n if (!baseCurrency) return null\n\n try {\n for (const range of aggregatedRanges) {\n const uniform = await this.rowsShareCurrency(request, range, baseCurrency)\n if (!uniform) return null\n }\n return baseCurrency\n } catch (err) {\n logger.warn('Row-currency uniformity check failed; leaving the amount unlabelled', {\n err,\n entityType: request.entityType,\n })\n return null\n }\n }\n\n /**\n * Reads the distinct per-row currencies of the rows one aggregation range would sum.\n * Entities that declare no `currencyField` carry no per-row currency to contradict the\n * base one, so they pass. An empty range has nothing to mislabel and also passes.\n *\n * A missing currency cannot prove the row uses the scope's base currency, so it fails\n * closed just like a conflicting code. An empty range has no row to mislabel and passes.\n */\n private async rowsShareCurrency(\n request: WidgetDataRequest,\n dateRange: { start: Date; end: Date } | undefined,\n expectedCode: string,\n ): Promise<boolean> {\n const query = buildDistinctCurrencyQuery({\n entityType: request.entityType,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) return true\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const resultRows = (Array.isArray(rows) ? rows : []) as Array<Record<string, unknown>>\n\n const recordedCodes = new Set<string>()\n for (const row of resultRows) {\n const code = typeof row.code === 'string' ? row.code.trim().toUpperCase() : ''\n if (!code) return false\n recordedCodes.add(code)\n }\n\n if (recordedCodes.size === 0) return true\n if (recordedCodes.size > 1) return false\n return recordedCodes.has(expectedCode)\n }\n\n /**\n * Resolves the base currency of the current scope so money widgets can label amounts\n * with the tenant's own currency instead of a hard-coded default (#4620). The lookup\n * is soft: a scope spanning organizations with different base currencies, a missing\n * base currency, or an unavailable currencies module all resolve to `null`, and the\n * widgets then render unlabelled numbers rather than a wrong currency.\n */\n private resolveBaseCurrency(): Promise<string | null> {\n if (!this.baseCurrencyPromise) {\n const organizationIds = [...new Set(this.scope.organizationIds ?? [])]\n if (!this.baseCurrencyResolver || organizationIds.length === 0) {\n this.baseCurrencyPromise = Promise.resolve(null)\n } else {\n this.baseCurrencyPromise = this.baseCurrencyResolver\n .resolveBaseCurrency({ tenantId: this.scope.tenantId, organizationIds })\n .then((result) => result.status === 'resolved' ? result.code : null)\n .catch(() => null)\n }\n }\n return this.baseCurrencyPromise\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n if (request.groupBy) {\n const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy)\n if (encryptedSource) {\n return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange)\n }\n }\n\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n /**\n * Reports the encrypted column a groupBy field reads from, or null when the source is stored in\n * plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets\n * ciphertext and renders it to the user (#4622).\n */\n private async resolveEncryptedGroupSource(\n entityType: string,\n groupBy: NonNullable<WidgetDataRequest['groupBy']>,\n ): Promise<EncryptedGroupSource | null> {\n // Encryption is not configured for this deployment, so no column can hold ciphertext and the\n // database may group the source directly.\n if (!isTenantDataEncryptionEnabled()) return null\n\n const resolved = resolveGroupExpression(this.registry, entityType, groupBy)\n if (!resolved) return null\n\n const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName\n if (!tableName) return null\n\n // Without an encryption entity id the table is outside the encryption map entirely, which is\n // the same state the encrypting subscriber sees when it skips the row.\n const entityId = this.resolveEncryptionEntityId(tableName)\n if (!entityId) return null\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n if (!encryptionService) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`,\n )\n }\n\n const organizationId = this.resolveOrganizationId()\n let encryptedFields: string[]\n try {\n // Deliberately independent of KMS health: the map describes how the rows were written, and\n // an unhealthy KMS must not be read as \"this column is plaintext\" (#4622).\n encryptedFields = await encryptionService.getEncryptedFieldNames(\n entityId,\n this.scope.tenantId,\n organizationId,\n { ignoreRuntimeHealth: true },\n )\n } catch (err) {\n logger.error('Failed to resolve encrypted fields for widget grouping', { err, entityId, entityType })\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`,\n )\n }\n\n if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null\n\n if (resolved.jsonPath === null && groupBy.granularity) {\n throw new WidgetDataValidationError(\n `Cannot group encrypted field by granularity: ${groupBy.field}`,\n )\n }\n\n return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath }\n }\n\n /**\n * Aggregates an encrypted group source in application code: rows are scanned, the group column is\n * decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted\n * collapse into the null (\"Unknown\") bucket so ciphertext never reaches the response.\n */\n private async executeEncryptedGroupQuery(\n request: WidgetDataRequest,\n source: EncryptedGroupSource,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildGroupSourceRowsQuery({\n entityType: request.entityType,\n metric: request.metric,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n groupColumn: source.dbColumn,\n rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {\n throw new WidgetDataScanLimitError(\n `Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`,\n )\n }\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n let dek: TenantDek | null = null\n try {\n dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null\n } catch (err) {\n logger.error('Failed to resolve the tenant DEK for widget grouping', { err, entityId: source.entityId })\n dek = null\n }\n\n const buckets = new Map<string | null, GroupBucket>()\n let undecryptableRows = 0\n\n for (const row of results as Array<Record<string, unknown>>) {\n const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null)\n // An undecryptable source joins the null (\"Unknown\") bucket rather than being dropped, so the\n // widget total still matches the underlying rows.\n if (resolvedKey === undefined) undecryptableRows += 1\n const groupKey = resolvedKey === undefined ? null : resolvedKey\n\n let bucket = buckets.get(groupKey)\n if (!bucket) {\n bucket = createGroupBucket()\n buckets.set(groupKey, bucket)\n }\n\n const metricValue = row.metric_value\n if (metricValue === null || metricValue === undefined) continue\n bucket.count += 1\n const numeric = parseExactDecimal(metricValue)\n if (numeric) foldMetricValue(bucket, numeric)\n }\n\n if (undecryptableRows > 0) {\n // No DEK at all means every ciphertext row would collapse into \"Unknown\" and the widget would\n // report a confidently wrong distribution, so fail closed rather than chart it (#4622).\n if (!dek) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`,\n )\n }\n logger.warn('Grouped rows with an undecryptable group source as unknown', {\n entityId: source.entityId,\n column: source.dbColumn,\n rows: undecryptableRows,\n })\n }\n\n let data: WidgetDataItem[] = Array.from(buckets.entries())\n .map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) }))\n .sort(compareWidgetDataItemsByValueDesc)\n\n if (request.groupBy?.limit && request.groupBy.limit > 0) {\n data = data.slice(0, Math.min(request.groupBy.limit, 100))\n }\n\n if (request.groupBy?.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n /**\n * Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or\n * `undefined` when the source is ciphertext that could not be decrypted.\n */\n private resolveDecryptedGroupKey(\n rawValue: unknown,\n source: EncryptedGroupSource,\n dek: string | null,\n ): string | null | undefined {\n if (rawValue === null || rawValue === undefined) return null\n\n let value: unknown = rawValue\n if (typeof value === 'string' && this.isEncryptedPayload(value)) {\n if (!dek) return undefined\n const decrypted = this.decryptWithDek(value, dek)\n if (decrypted === null) return undefined\n value = parseDecryptedFieldValue(decrypted)\n }\n\n if (source.jsonPath !== null) {\n value = readJsonPath(value, source.jsonPath)\n }\n\n if (value === null || value === undefined || value === '') return null\n if (typeof value === 'object') return null\n const key = String(value)\n // Defense in depth: a nested value that is itself ciphertext must never reach the response.\n return this.isEncryptedPayload(key) ? undefined : key\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => {\n if (item.groupKey == null || item.groupKey === '') return { ...item, groupLabel: undefined }\n const label = String(item.groupKey)\n // A ciphertext group key has no meaningful label; echoing it would render encrypted data\n // into the chart legend (#4622).\n return { ...item, groupLabel: this.isEncryptedPayload(label) ? undefined : label }\n })\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n }\n } catch {\n // fall through to SQL resolution\n }\n }\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n /**\n * Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork\n * frequently reports an empty metadata registry, which used to make every group source look\n * unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table\n * name alone is enough for the entity-id lookup, so it is the fallback.\n */\n private resolveEncryptionEntityId(tableName: string): string | null {\n return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName })\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n baseCurrencyResolver?: BaseCurrencyResolver,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache, baseCurrencyResolver })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { listEntityMetadata } from '@open-mercato/shared/lib/db/entityMetadata'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TenantDek } from '@open-mercato/shared/lib/encryption/kms'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n buildDistinctCurrencyQuery,\n buildGroupSourceRowsQuery,\n resolveGroupExpression,\n} from '../lib/aggregations'\nimport {\n type ExactDecimal,\n EXACT_DECIMAL_ZERO,\n addExactDecimal,\n compareExactDecimal,\n exactDecimalToNumber,\n parseExactDecimal,\n} from '../lib/exactDecimal'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\nimport type { BaseCurrencyResolver } from '../lib/optionalBaseCurrency'\n\nconst logger = createLogger('dashboards').child({ component: 'widget-data-service' })\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\n/**\n * Encrypted group sources cannot be grouped by the database, so the rows are scanned and\n * aggregated in application code. The cap keeps that scan bounded; overflowing it fails loudly\n * instead of charting a silently truncated result (#4622).\n */\nconst ENCRYPTED_GROUP_SCAN_LIMIT = 20_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nexport class WidgetDataScanLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataScanLimitError'\n }\n}\n\n/**\n * Raised when encryption is configured but the group source cannot currently be resolved \u2014 an\n * unhealthy KMS, an unreadable encryption map, or a missing tenant DEK. Grouping would either read\n * ciphertext in SQL or silently collapse every encrypted row into \"Unknown\", so the request fails\n * closed instead (#4622).\n */\nexport class WidgetDataEncryptionUnavailableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataEncryptionUnavailableError'\n }\n}\n\ntype EncryptedGroupSource = {\n entityId: string\n dbColumn: string\n jsonPath: string | null\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\n/**\n * Encryption maps may register a field in either casing (`findKey` in the encryption service\n * accepts both), so match the analytics column tolerantly rather than by exact snake_case.\n */\nfunction matchesColumn(fieldName: string, dbColumn: string): boolean {\n const normalize = (value: string) => value.replace(/_/g, '').toLowerCase()\n return normalize(fieldName) === normalize(dbColumn)\n}\n\nfunction readJsonPath(value: unknown, path: string): unknown {\n let current = value\n for (const part of path.split('.')) {\n if (current === null || typeof current !== 'object') return null\n current = (current as Record<string, unknown>)[part]\n }\n return current\n}\n\n/**\n * Running aggregate state for one group. Values are folded in as they are scanned \u2014 the bucket\n * never holds the row set \u2014 so the scan cost stays linear and independent of the row cap.\n */\ntype GroupBucket = {\n /** Rows with a non-null metric column, mirroring SQL `COUNT(column)`. */\n count: number\n /** Rows whose metric parsed as a decimal, mirroring the row set SQL aggregates operate on. */\n numericCount: number\n /** Exact running sum, so money never accumulates in binary floating point. */\n sum: ExactDecimal\n min: ExactDecimal | null\n max: ExactDecimal | null\n}\n\nfunction createGroupBucket(): GroupBucket {\n return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null }\n}\n\nfunction foldMetricValue(bucket: GroupBucket, value: ExactDecimal): void {\n bucket.numericCount += 1\n bucket.sum = addExactDecimal(bucket.sum, value)\n if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value\n if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value\n}\n\n/**\n * Mirrors the SQL semantics of `buildAggregateExpression` for application-side aggregation. That\n * expression wraps `SUM`/`AVG` in `COALESCE(..., 0)`, so an empty value set aggregates to `0` on\n * both paths, while the uncoalesced `MIN`/`MAX` keep PostgreSQL's `NULL`. An encrypted group source\n * must not report a different value than the same data would report in plaintext (#4622).\n */\nfunction aggregateBucket(aggregate: AggregateFunction, bucket: GroupBucket): number | null {\n switch (aggregate) {\n case 'count':\n return bucket.count\n case 'sum':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum)\n case 'avg':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount\n case 'min':\n return bucket.min === null ? null : exactDecimalToNumber(bucket.min)\n case 'max':\n return bucket.max === null ? null : exactDecimalToNumber(bucket.max)\n default:\n return bucket.count\n }\n}\n\n/**\n * Mirrors the SQL path's `ORDER BY value DESC NULLS LAST`. PostgreSQL defaults DESC to NULLS FIRST,\n * which would let empty buckets displace real ones under a group limit, so both paths state the\n * policy explicitly: highest value first, buckets without a value last.\n */\nfunction compareWidgetDataItemsByValueDesc(\n left: { value: number | null },\n right: { value: number | null },\n): number {\n if (left.value === null && right.value === null) return 0\n if (left.value === null) return 1\n if (right.value === null) return -1\n return right.value - left.value\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n currency?: string | null\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n baseCurrencyResolver?: BaseCurrencyResolver\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n private baseCurrencyResolver?: BaseCurrencyResolver\n private baseCurrencyPromise?: Promise<string | null>\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n this.baseCurrencyResolver = options.baseCurrencyResolver\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const aggregatedRanges =\n shouldFetchComparison && comparisonRange ? [dateRangeResolved, comparisonRange] : [dateRangeResolved]\n\n const [mainResult, comparisonResult, currency] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\n this.resolveCurrencyLabel(request, aggregatedRanges),\n ])\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n currency,\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\n } catch {\n }\n }\n\n return response\n }\n\n /**\n * Resolves the currency the response may be labelled with. The base currency of the\n * scope is only half the answer: it describes how the organizations are configured, not\n * what the aggregated rows are actually denominated in. A PLN-based organization holding\n * both PLN and EUR orders would otherwise sum them and present the total as PLN (#4676),\n * so the base currency survives only when every aggregated row carries exactly that code.\n *\n * The check is deliberately conservative \u2014 anything it cannot prove resolves to `null`\n * and the widgets render an explicitly unlabelled number, which is recoverable, while a\n * confident wrong label is not.\n */\n private async resolveCurrencyLabel(\n request: WidgetDataRequest,\n aggregatedRanges: Array<{ start: Date; end: Date } | undefined>,\n ): Promise<string | null> {\n const baseCurrency = await this.resolveBaseCurrency()\n if (!baseCurrency) return null\n\n try {\n for (const range of aggregatedRanges) {\n const uniform = await this.rowsShareCurrency(request, range, baseCurrency)\n if (!uniform) return null\n }\n return baseCurrency\n } catch (err) {\n logger.warn('Row-currency uniformity check failed; leaving the amount unlabelled', {\n err,\n entityType: request.entityType,\n })\n return null\n }\n }\n\n /**\n * Reads the distinct per-row currencies of the rows one aggregation range would sum.\n * Entities that declare no `currencyField` carry no per-row currency to contradict the\n * base one, so they pass. An empty range has nothing to mislabel and also passes.\n *\n * A missing currency cannot prove the row uses the scope's base currency, so it fails\n * closed just like a conflicting code. An empty range has no row to mislabel and passes.\n */\n private async rowsShareCurrency(\n request: WidgetDataRequest,\n dateRange: { start: Date; end: Date } | undefined,\n expectedCode: string,\n ): Promise<boolean> {\n const query = buildDistinctCurrencyQuery({\n entityType: request.entityType,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) return true\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const resultRows = (Array.isArray(rows) ? rows : []) as Array<Record<string, unknown>>\n\n const recordedCodes = new Set<string>()\n for (const row of resultRows) {\n const code = typeof row.code === 'string' ? row.code.trim().toUpperCase() : ''\n if (!code) return false\n recordedCodes.add(code)\n }\n\n if (recordedCodes.size === 0) return true\n if (recordedCodes.size > 1) return false\n return recordedCodes.has(expectedCode)\n }\n\n /**\n * Resolves the base currency of the current scope so money widgets can label amounts\n * with the tenant's own currency instead of a hard-coded default (#4620). The lookup\n * is soft: a scope spanning organizations with different base currencies, a missing\n * base currency, or an unavailable currencies module all resolve to `null`, and the\n * widgets then render unlabelled numbers rather than a wrong currency.\n */\n private resolveBaseCurrency(): Promise<string | null> {\n if (!this.baseCurrencyPromise) {\n const organizationIds = [...new Set(this.scope.organizationIds ?? [])]\n if (!this.baseCurrencyResolver || organizationIds.length === 0) {\n this.baseCurrencyPromise = Promise.resolve(null)\n } else {\n this.baseCurrencyPromise = this.baseCurrencyResolver\n .resolveBaseCurrency({ tenantId: this.scope.tenantId, organizationIds })\n .then((result) => result.status === 'resolved' ? result.code : null)\n .catch(() => null)\n }\n }\n return this.baseCurrencyPromise\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n if (request.groupBy) {\n const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy)\n if (encryptedSource) {\n return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange)\n }\n }\n\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n /**\n * Reports the encrypted column a groupBy field reads from, or null when the source is stored in\n * plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets\n * ciphertext and renders it to the user (#4622).\n */\n private async resolveEncryptedGroupSource(\n entityType: string,\n groupBy: NonNullable<WidgetDataRequest['groupBy']>,\n ): Promise<EncryptedGroupSource | null> {\n // Encryption is not configured for this deployment, so no column can hold ciphertext and the\n // database may group the source directly.\n if (!isTenantDataEncryptionEnabled()) return null\n\n const resolved = resolveGroupExpression(this.registry, entityType, groupBy)\n if (!resolved) return null\n\n const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName\n if (!tableName) return null\n\n // Without an encryption entity id the table is outside the encryption map entirely, which is\n // the same state the encrypting subscriber sees when it skips the row.\n const entityId = this.resolveEncryptionEntityId(tableName)\n if (!entityId) return null\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n if (!encryptionService) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`,\n )\n }\n\n const organizationId = this.resolveOrganizationId()\n let encryptedFields: string[]\n try {\n // Deliberately independent of KMS health: the map describes how the rows were written, and\n // an unhealthy KMS must not be read as \"this column is plaintext\" (#4622).\n encryptedFields = await encryptionService.getEncryptedFieldNames(\n entityId,\n this.scope.tenantId,\n organizationId,\n { ignoreRuntimeHealth: true },\n )\n } catch (err) {\n logger.error('Failed to resolve encrypted fields for widget grouping', { err, entityId, entityType })\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`,\n )\n }\n\n if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null\n\n if (resolved.jsonPath === null && groupBy.granularity) {\n throw new WidgetDataValidationError(\n `Cannot group encrypted field by granularity: ${groupBy.field}`,\n )\n }\n\n return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath }\n }\n\n /**\n * Aggregates an encrypted group source in application code: rows are scanned, the group column is\n * decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted\n * collapse into the null (\"Unknown\") bucket so ciphertext never reaches the response.\n */\n private async executeEncryptedGroupQuery(\n request: WidgetDataRequest,\n source: EncryptedGroupSource,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildGroupSourceRowsQuery({\n entityType: request.entityType,\n metric: request.metric,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n groupColumn: source.dbColumn,\n rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {\n throw new WidgetDataScanLimitError(\n `Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`,\n )\n }\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n let dek: TenantDek | null = null\n try {\n dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null\n } catch (err) {\n logger.error('Failed to resolve the tenant DEK for widget grouping', { err, entityId: source.entityId })\n dek = null\n }\n\n const buckets = new Map<string | null, GroupBucket>()\n let undecryptableRows = 0\n\n for (const row of results as Array<Record<string, unknown>>) {\n const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null)\n // An undecryptable source joins the null (\"Unknown\") bucket rather than being dropped, so the\n // widget total still matches the underlying rows.\n if (resolvedKey === undefined) undecryptableRows += 1\n const groupKey = resolvedKey === undefined ? null : resolvedKey\n\n let bucket = buckets.get(groupKey)\n if (!bucket) {\n bucket = createGroupBucket()\n buckets.set(groupKey, bucket)\n }\n\n const metricValue = row.metric_value\n if (metricValue === null || metricValue === undefined) continue\n bucket.count += 1\n const numeric = parseExactDecimal(metricValue)\n if (numeric) foldMetricValue(bucket, numeric)\n }\n\n if (undecryptableRows > 0) {\n // No DEK at all means every ciphertext row would collapse into \"Unknown\" and the widget would\n // report a confidently wrong distribution, so fail closed rather than chart it (#4622).\n if (!dek) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`,\n )\n }\n logger.warn('Grouped rows with an undecryptable group source as unknown', {\n entityId: source.entityId,\n column: source.dbColumn,\n rows: undecryptableRows,\n })\n }\n\n let data: WidgetDataItem[] = Array.from(buckets.entries())\n .map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) }))\n .sort(compareWidgetDataItemsByValueDesc)\n\n if (request.groupBy?.limit && request.groupBy.limit > 0) {\n data = data.slice(0, Math.min(request.groupBy.limit, 100))\n }\n\n if (request.groupBy?.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n /**\n * Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or\n * `undefined` when the source is ciphertext that could not be decrypted.\n */\n private resolveDecryptedGroupKey(\n rawValue: unknown,\n source: EncryptedGroupSource,\n dek: string | null,\n ): string | null | undefined {\n if (rawValue === null || rawValue === undefined) return null\n\n let value: unknown = rawValue\n if (typeof value === 'string' && this.isEncryptedPayload(value)) {\n if (!dek) return undefined\n const decrypted = this.decryptWithDek(value, dek)\n if (decrypted === null) return undefined\n value = parseDecryptedFieldValue(decrypted)\n }\n\n if (source.jsonPath !== null) {\n value = readJsonPath(value, source.jsonPath)\n }\n\n if (value === null || value === undefined || value === '') return null\n if (typeof value === 'object') return null\n const key = String(value)\n // Defense in depth: a nested value that is itself ciphertext must never reach the response.\n return this.isEncryptedPayload(key) ? undefined : key\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => {\n if (item.groupKey == null || item.groupKey === '') return { ...item, groupLabel: undefined }\n const label = String(item.groupKey)\n // A ciphertext group key has no meaningful label; echoing it would render encrypted data\n // into the chart legend (#4622).\n return { ...item, groupLabel: this.isEncryptedPayload(label) ? undefined : label }\n })\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n }\n } catch {\n // fall through to SQL resolution\n }\n }\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n\n private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const metas = listEntityMetadata(this.em)\n if (!metas.length) return null\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n /**\n * Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork\n * frequently reports an empty metadata registry, which used to make every group source look\n * unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table\n * name alone is enough for the entity-id lookup, so it is the fallback.\n */\n private resolveEncryptionEntityId(tableName: string): string | null {\n return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName })\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n baseCurrencyResolver?: BaseCurrencyResolver,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache, baseCurrencyResolver })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -384,6 +384,7 @@ async function GET(req) {
384
384
  label: d.configJson?.label || d.key,
385
385
  description: d.configJson?.description || void 0,
386
386
  multi: Boolean(d.configJson?.multi),
387
+ relatedEntityId: d.kind === "relation" && typeof d.configJson?.relatedEntityId === "string" ? d.configJson.relatedEntityId.trim() || void 0 : void 0,
387
388
  options: (() => {
388
389
  if (d.kind === "currency") return void 0;
389
390
  const normalizedOptions = normalizeCustomFieldOptions(d.configJson?.options);
@@ -630,6 +631,7 @@ const customFieldDefinitionSchema = z.object({
630
631
  label: z.string(),
631
632
  description: z.string().optional(),
632
633
  multi: z.boolean().optional(),
634
+ relatedEntityId: z.string().optional(),
633
635
  options: z.array(customFieldOptionValueSchema).optional(),
634
636
  optionsUrl: z.string().optional(),
635
637
  filterable: z.boolean().optional(),