@open-mercato/enterprise 0.6.6-develop.5612.1.d382eb2f33 → 0.6.6-develop.5619.1.29f01e2c42

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 (48) hide show
  1. package/dist/modules/security/api/i18n.js +3 -0
  2. package/dist/modules/security/api/i18n.js.map +2 -2
  3. package/dist/modules/security/api/mfa/prepare/route.js +6 -1
  4. package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
  5. package/dist/modules/security/api/mfa/verify/route.js +7 -1
  6. package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
  7. package/dist/modules/security/api/sudo/prepare/route.js +6 -1
  8. package/dist/modules/security/api/sudo/prepare/route.js.map +2 -2
  9. package/dist/modules/security/api/users/mfa/compliance/route.js +4 -2
  10. package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
  11. package/dist/modules/security/commands/createEnforcementPolicy.js +6 -4
  12. package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
  13. package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -4
  14. package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
  15. package/dist/modules/security/commands/resetUserMfa.js +11 -4
  16. package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
  17. package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -4
  18. package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
  19. package/dist/modules/security/i18n/de.json +3 -0
  20. package/dist/modules/security/i18n/en.json +3 -0
  21. package/dist/modules/security/i18n/es.json +3 -0
  22. package/dist/modules/security/i18n/pl.json +3 -0
  23. package/dist/modules/security/services/MfaAdminService.js +42 -8
  24. package/dist/modules/security/services/MfaAdminService.js.map +2 -2
  25. package/dist/modules/security/services/MfaEnforcementService.js +57 -7
  26. package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
  27. package/dist/modules/security/services/MfaVerificationService.js +15 -6
  28. package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
  29. package/dist/modules/security/services/SudoChallengeService.js +26 -4
  30. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  31. package/package.json +5 -5
  32. package/src/modules/security/api/i18n.ts +3 -0
  33. package/src/modules/security/api/mfa/prepare/route.ts +6 -1
  34. package/src/modules/security/api/mfa/verify/route.ts +7 -1
  35. package/src/modules/security/api/sudo/prepare/route.ts +6 -1
  36. package/src/modules/security/api/users/mfa/compliance/route.ts +6 -3
  37. package/src/modules/security/commands/createEnforcementPolicy.ts +7 -5
  38. package/src/modules/security/commands/deleteEnforcementPolicy.ts +10 -5
  39. package/src/modules/security/commands/resetUserMfa.ts +12 -5
  40. package/src/modules/security/commands/updateEnforcementPolicy.ts +10 -5
  41. package/src/modules/security/i18n/de.json +3 -0
  42. package/src/modules/security/i18n/en.json +3 -0
  43. package/src/modules/security/i18n/es.json +3 -0
  44. package/src/modules/security/i18n/pl.json +3 -0
  45. package/src/modules/security/services/MfaAdminService.ts +101 -8
  46. package/src/modules/security/services/MfaEnforcementService.ts +174 -7
  47. package/src/modules/security/services/MfaVerificationService.ts +58 -4
  48. package/src/modules/security/services/SudoChallengeService.ts +31 -6
@@ -14,7 +14,7 @@ class MfaAdminService {
14
14
  this.em = em;
15
15
  this.mfaEnforcementService = mfaEnforcementService;
16
16
  }
17
- async resetUserMfa(adminId, userId, reason, actor) {
17
+ async resetUserMfa(adminId, userId, reason, scope) {
18
18
  if (!adminId.trim()) {
19
19
  throw new MfaAdminServiceError("Admin ID is required", 400);
20
20
  }
@@ -25,11 +25,11 @@ class MfaAdminService {
25
25
  if (!normalizedReason) {
26
26
  throw new MfaAdminServiceError("Reset reason is required", 400);
27
27
  }
28
- const user = await this.findUserById(userId);
28
+ const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
29
+ const user = await this.loadUserForScope(userId, effectiveScope);
29
30
  if (!user) {
30
31
  throw new MfaAdminServiceError("User not found", 404);
31
32
  }
32
- this.assertActorOwnsUser(user, actor);
33
33
  const activeMethods = await this.em.find(UserMfaMethod, {
34
34
  userId,
35
35
  isActive: true,
@@ -97,24 +97,26 @@ class MfaAdminService {
97
97
  compliant: compliance.compliant
98
98
  };
99
99
  }
100
- async bulkComplianceCheck(tenantId, actor) {
100
+ async bulkComplianceCheck(tenantId, scope) {
101
101
  if (!tenantId.trim()) {
102
102
  throw new MfaAdminServiceError("Tenant ID is required", 400);
103
103
  }
104
- if (actor && !actor.isSuperAdmin && tenantId !== actor.tenantId) {
105
- throw new MfaAdminServiceError("Not authorized for the requested tenant scope.", 403);
104
+ const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
105
+ const effectiveTenantId = this.resolveTenantForScope(tenantId, effectiveScope);
106
+ if (!effectiveTenantId) {
107
+ throw new MfaAdminServiceError("Tenant not found", 404);
106
108
  }
107
109
  const users = await findWithDecryption(
108
110
  this.em,
109
111
  User,
110
112
  {
111
- tenantId,
113
+ tenantId: effectiveTenantId,
112
114
  deletedAt: null
113
115
  },
114
116
  {
115
117
  orderBy: { createdAt: "asc" }
116
118
  },
117
- { tenantId, organizationId: null }
119
+ { tenantId: effectiveTenantId, organizationId: null }
118
120
  );
119
121
  const userIds = users.map((user) => user.id);
120
122
  const activeMethods = userIds.length ? await this.em.find(UserMfaMethod, {
@@ -158,6 +160,38 @@ class MfaAdminService {
158
160
  { tenantId: null, organizationId: null }
159
161
  );
160
162
  }
163
+ resolveTenantForScope(requestedTenantId, scope) {
164
+ if (scope.isSuperAdmin) {
165
+ return requestedTenantId;
166
+ }
167
+ if (!scope.tenantId) return null;
168
+ if (scope.tenantId !== requestedTenantId) return null;
169
+ return scope.tenantId;
170
+ }
171
+ async loadUserForScope(userId, scope) {
172
+ if (scope.isSuperAdmin) {
173
+ return findOneWithDecryption(
174
+ this.em,
175
+ User,
176
+ { id: userId, deletedAt: null },
177
+ void 0,
178
+ { tenantId: null, organizationId: null }
179
+ );
180
+ }
181
+ if (!scope.tenantId) return null;
182
+ const user = await findOneWithDecryption(
183
+ this.em,
184
+ User,
185
+ { id: userId, tenantId: scope.tenantId, deletedAt: null },
186
+ void 0,
187
+ { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null }
188
+ );
189
+ if (!user) return null;
190
+ if (scope.organizationId !== void 0 && scope.organizationId !== null && user.organizationId !== null && user.organizationId !== scope.organizationId) {
191
+ return null;
192
+ }
193
+ return user;
194
+ }
161
195
  }
162
196
  var MfaAdminService_default = MfaAdminService;
163
197
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/MfaAdminService.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\ntype ActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n async resetUserMfa(adminId: string, userId: string, reason: string, actor?: ActorContext): Promise<void> {\n if (!adminId.trim()) {\n throw new MfaAdminServiceError('Admin ID is required', 400)\n }\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const normalizedReason = reason.trim()\n if (!normalizedReason) {\n throw new MfaAdminServiceError('Reset reason is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const activeMethods = await this.em.find(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n })\n const activeRecoveryCodes = await this.em.find(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const now = new Date()\n for (const method of activeMethods) {\n method.isActive = false\n method.deletedAt = now\n method.updatedAt = now\n }\n for (const recoveryCode of activeRecoveryCodes) {\n recoveryCode.isUsed = true\n recoveryCode.usedAt = now\n }\n await this.em.flush()\n\n await emitSecurityEvent('security.mfa.reset', {\n adminId,\n targetUserId: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n reason: normalizedReason,\n methodCount: activeMethods.length,\n recoveryCodesInvalidated: activeRecoveryCodes.length,\n resetAt: new Date().toISOString(),\n })\n }\n\n async getUserMfaStatus(userId: string, actor?: ActorContext): Promise<UserMfaStatus> {\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'desc' },\n },\n )\n\n const recoveryCodesRemaining = await this.em.count(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const compliance = await this.mfaEnforcementService.checkUserCompliance(userId)\n\n return {\n enrolled: methods.length > 0,\n methods: methods.map((method) => ({\n type: method.type,\n ...(method.label ? { label: method.label } : {}),\n ...(method.lastUsedAt ? { lastUsed: method.lastUsedAt } : {}),\n })),\n recoveryCodesRemaining,\n compliant: compliance.compliant,\n }\n }\n\n async bulkComplianceCheck(tenantId: string, actor?: ActorContext): Promise<BulkComplianceStatus[]> {\n if (!tenantId.trim()) {\n throw new MfaAdminServiceError('Tenant ID is required', 400)\n }\n if (actor && !actor.isSuperAdmin && tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('Not authorized for the requested tenant scope.', 403)\n }\n\n const users = await findWithDecryption(\n this.em,\n User,\n {\n tenantId,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n { tenantId, organizationId: null },\n )\n\n const userIds = users.map((user) => user.id)\n const activeMethods = userIds.length\n ? await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n })\n : []\n const methodCountByUserId = new Map<string, number>()\n for (const method of activeMethods) {\n const currentCount = methodCountByUserId.get(method.userId) ?? 0\n methodCountByUserId.set(method.userId, currentCount + 1)\n }\n\n const complianceResults = await Promise.all(\n users.map((user) => this.mfaEnforcementService.checkUserCompliance(user.id)),\n )\n\n return users.map((user, index) => {\n const methodCount = methodCountByUserId.get(user.id) ?? 0\n const compliance = complianceResults[index]\n return {\n userId: user.id,\n email: user.email,\n enrolled: methodCount > 0,\n methodCount,\n compliant: compliance.compliant,\n ...(user.lastLoginAt ? { lastLoginAt: user.lastLoginAt } : {}),\n }\n })\n }\n\n private assertActorOwnsUser(user: User, actor?: ActorContext): void {\n if (!actor || actor.isSuperAdmin) return\n if (!user.tenantId || user.tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n }\n}\n\nexport default MfaAdminService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { MfaRecoveryCode, UserMfaMethod } from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\n\nexport type MfaAdminAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype MfaMethodStatus = {\n type: string\n label?: string\n lastUsed?: Date\n}\n\ntype UserMfaStatus = {\n enrolled: boolean\n methods: MfaMethodStatus[]\n recoveryCodesRemaining: number\n compliant: boolean\n}\n\ntype BulkComplianceStatus = {\n userId: string\n email: string\n enrolled: boolean\n methodCount: number\n compliant: boolean\n lastLoginAt?: Date\n}\n\ntype ActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\nexport class MfaAdminServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaAdminServiceError'\n }\n}\n\nexport class MfaAdminService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaEnforcementService: MfaEnforcementService,\n ) {}\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaAdminAuthScope} so the target user is\n * loaded with tenant/organization scoping. The no-scope overload now treats the\n * caller as a non-superadmin with unknown tenant and rejects every load with 404;\n * it will be removed in a future release.\n */\n async resetUserMfa(adminId: string, userId: string, reason: string): Promise<void>\n async resetUserMfa(\n adminId: string,\n userId: string,\n reason: string,\n scope: MfaAdminAuthScope,\n ): Promise<void>\n async resetUserMfa(\n adminId: string,\n userId: string,\n reason: string,\n scope?: MfaAdminAuthScope,\n ): Promise<void> {\n if (!adminId.trim()) {\n throw new MfaAdminServiceError('Admin ID is required', 400)\n }\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const normalizedReason = reason.trim()\n if (!normalizedReason) {\n throw new MfaAdminServiceError('Reset reason is required', 400)\n }\n\n const effectiveScope: MfaAdminAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n const user = await this.loadUserForScope(userId, effectiveScope)\n if (!user) {\n // Unified 404 for both \"missing\" and \"out of scope\" \u2014 prevents existence enumeration.\n throw new MfaAdminServiceError('User not found', 404)\n }\n\n const activeMethods = await this.em.find(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n })\n const activeRecoveryCodes = await this.em.find(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const now = new Date()\n for (const method of activeMethods) {\n method.isActive = false\n method.deletedAt = now\n method.updatedAt = now\n }\n for (const recoveryCode of activeRecoveryCodes) {\n recoveryCode.isUsed = true\n recoveryCode.usedAt = now\n }\n await this.em.flush()\n\n await emitSecurityEvent('security.mfa.reset', {\n adminId,\n targetUserId: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n reason: normalizedReason,\n methodCount: activeMethods.length,\n recoveryCodesInvalidated: activeRecoveryCodes.length,\n resetAt: new Date().toISOString(),\n })\n }\n\n async getUserMfaStatus(userId: string, actor?: ActorContext): Promise<UserMfaStatus> {\n if (!userId.trim()) {\n throw new MfaAdminServiceError('User ID is required', 400)\n }\n\n const user = await this.findUserById(userId)\n if (!user) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n this.assertActorOwnsUser(user, actor)\n\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'desc' },\n },\n )\n\n const recoveryCodesRemaining = await this.em.count(MfaRecoveryCode, {\n userId,\n isUsed: false,\n })\n\n const compliance = await this.mfaEnforcementService.checkUserCompliance(userId)\n\n return {\n enrolled: methods.length > 0,\n methods: methods.map((method) => ({\n type: method.type,\n ...(method.label ? { label: method.label } : {}),\n ...(method.lastUsedAt ? { lastUsed: method.lastUsedAt } : {}),\n })),\n recoveryCodesRemaining,\n compliant: compliance.compliant,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaAdminAuthScope} so the tenant list is\n * enforced against the caller's actual scope rather than the caller-supplied\n * tenantId. The no-scope overload now treats the caller as a non-superadmin\n * with unknown tenant and rejects every request with 404; it will be removed\n * in a future release.\n */\n async bulkComplianceCheck(tenantId: string): Promise<BulkComplianceStatus[]>\n async bulkComplianceCheck(\n tenantId: string,\n scope: MfaAdminAuthScope,\n ): Promise<BulkComplianceStatus[]>\n async bulkComplianceCheck(\n tenantId: string,\n scope?: MfaAdminAuthScope,\n ): Promise<BulkComplianceStatus[]> {\n if (!tenantId.trim()) {\n throw new MfaAdminServiceError('Tenant ID is required', 400)\n }\n\n const effectiveScope: MfaAdminAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n const effectiveTenantId = this.resolveTenantForScope(tenantId, effectiveScope)\n if (!effectiveTenantId) {\n // Unified 404 for missing-scope, cross-tenant mismatch, and unknown tenant \u2014\n // prevents existence enumeration and matches the resetUserMfa contract.\n throw new MfaAdminServiceError('Tenant not found', 404)\n }\n\n const users = await findWithDecryption(\n this.em,\n User,\n {\n tenantId: effectiveTenantId,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n { tenantId: effectiveTenantId, organizationId: null },\n )\n\n const userIds = users.map((user) => user.id)\n const activeMethods = userIds.length\n ? await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n })\n : []\n const methodCountByUserId = new Map<string, number>()\n for (const method of activeMethods) {\n const currentCount = methodCountByUserId.get(method.userId) ?? 0\n methodCountByUserId.set(method.userId, currentCount + 1)\n }\n\n const complianceResults = await Promise.all(\n users.map((user) => this.mfaEnforcementService.checkUserCompliance(user.id)),\n )\n\n return users.map((user, index) => {\n const methodCount = methodCountByUserId.get(user.id) ?? 0\n const compliance = complianceResults[index]\n return {\n userId: user.id,\n email: user.email,\n enrolled: methodCount > 0,\n methodCount,\n compliant: compliance.compliant,\n ...(user.lastLoginAt ? { lastLoginAt: user.lastLoginAt } : {}),\n }\n })\n }\n\n private assertActorOwnsUser(user: User, actor?: ActorContext): void {\n if (!actor || actor.isSuperAdmin) return\n if (!user.tenantId || user.tenantId !== actor.tenantId) {\n throw new MfaAdminServiceError('User not found', 404)\n }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null } as FilterQuery<User>,\n {},\n { tenantId: null, organizationId: null },\n )\n }\n\n private resolveTenantForScope(\n requestedTenantId: string,\n scope: MfaAdminAuthScope,\n ): string | null {\n if (scope.isSuperAdmin) {\n return requestedTenantId\n }\n if (!scope.tenantId) return null\n if (scope.tenantId !== requestedTenantId) return null\n return scope.tenantId\n }\n\n private async loadUserForScope(\n userId: string,\n scope: MfaAdminAuthScope,\n ): Promise<User | null> {\n if (scope.isSuperAdmin) {\n return findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n { tenantId: null, organizationId: null },\n )\n }\n\n if (!scope.tenantId) return null\n\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, tenantId: scope.tenantId, deletedAt: null },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId ?? null },\n )\n if (!user) return null\n\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && user.organizationId !== null\n && user.organizationId !== scope.organizationId\n ) {\n return null\n }\n return user\n }\n}\n\nexport default MfaAdminService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -81,9 +81,10 @@ class MfaEnforcementService {
81
81
  overdue
82
82
  };
83
83
  }
84
- async createPolicy(data, adminId, actor) {
84
+ async createPolicy(data, adminId, scope) {
85
85
  const normalized = this.normalizePolicyInput(data);
86
- this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId);
86
+ const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
87
+ this.assertCreatePolicyScope(normalized, effectiveScope);
87
88
  const existing = await this.findPolicyByScope(
88
89
  normalized.scope,
89
90
  normalized.tenantId ?? void 0,
@@ -126,7 +127,8 @@ class MfaEnforcementService {
126
127
  await this.emitDeadlineReminderRequest(policy.id);
127
128
  return policy;
128
129
  }
129
- async updatePolicy(id, data, adminId, actor) {
130
+ async updatePolicy(id, data, adminId, scope) {
131
+ const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
130
132
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
131
133
  id,
132
134
  deletedAt: null
@@ -134,7 +136,6 @@ class MfaEnforcementService {
134
136
  if (!policy) {
135
137
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
136
138
  }
137
- this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
138
139
  const mergedInput = this.normalizePolicyInput({
139
140
  scope: data.scope ?? policy.scope,
140
141
  tenantId: data.tenantId ?? policy.tenantId ?? void 0,
@@ -143,7 +144,7 @@ class MfaEnforcementService {
143
144
  allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,
144
145
  enforcementDeadline: data.enforcementDeadline === void 0 ? policy.enforcementDeadline ?? null : data.enforcementDeadline
145
146
  });
146
- this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId);
147
+ this.assertUpdatePolicyScope(policy, mergedInput, effectiveScope);
147
148
  if (mergedInput.scope !== policy.scope || mergedInput.tenantId !== (policy.tenantId ?? null) || mergedInput.organizationId !== (policy.organizationId ?? null)) {
148
149
  const conflict = await this.findPolicyByScope(
149
150
  mergedInput.scope,
@@ -171,7 +172,8 @@ class MfaEnforcementService {
171
172
  await this.emitDeadlineReminderRequest(policy.id);
172
173
  return policy;
173
174
  }
174
- async deletePolicy(id, actor) {
175
+ async deletePolicy(id, scope) {
176
+ const effectiveScope = scope ?? { tenantId: null, isSuperAdmin: false };
175
177
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
176
178
  id,
177
179
  deletedAt: null
@@ -179,7 +181,7 @@ class MfaEnforcementService {
179
181
  if (!policy) {
180
182
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
181
183
  }
182
- this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
184
+ this.assertDeletePolicyScope(policy, effectiveScope);
183
185
  const now = /* @__PURE__ */ new Date();
184
186
  policy.deletedAt = now;
185
187
  policy.updatedAt = now;
@@ -223,6 +225,54 @@ class MfaEnforcementService {
223
225
  if (tenantPolicy) return tenantPolicy;
224
226
  return this.findPolicyByScope(EnforcementScope.PLATFORM, void 0, void 0);
225
227
  }
228
+ assertCreatePolicyScope(normalized, scope) {
229
+ if (scope.isSuperAdmin) return;
230
+ if (!scope.tenantId) {
231
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
232
+ }
233
+ if (normalized.scope === EnforcementScope.PLATFORM) {
234
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
235
+ }
236
+ if (normalized.tenantId !== scope.tenantId) {
237
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
238
+ }
239
+ if (normalized.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && normalized.organizationId !== scope.organizationId) {
240
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
241
+ }
242
+ }
243
+ assertUpdatePolicyScope(existing, proposed, scope) {
244
+ if (scope.isSuperAdmin) return;
245
+ if (!scope.tenantId) {
246
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
247
+ }
248
+ if (existing.scope === EnforcementScope.PLATFORM || existing.tenantId !== scope.tenantId) {
249
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
250
+ }
251
+ if (proposed.scope === EnforcementScope.PLATFORM) {
252
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
253
+ }
254
+ if (proposed.tenantId !== scope.tenantId) {
255
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
256
+ }
257
+ if (proposed.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && proposed.organizationId !== scope.organizationId) {
258
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
259
+ }
260
+ if (existing.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && existing.organizationId !== scope.organizationId) {
261
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
262
+ }
263
+ }
264
+ assertDeletePolicyScope(existing, scope) {
265
+ if (scope.isSuperAdmin) return;
266
+ if (!scope.tenantId) {
267
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
268
+ }
269
+ if (existing.scope === EnforcementScope.PLATFORM || existing.tenantId !== scope.tenantId) {
270
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
271
+ }
272
+ if (existing.scope === EnforcementScope.ORGANISATION && scope.organizationId !== void 0 && scope.organizationId !== null && existing.organizationId !== scope.organizationId) {
273
+ throw new MfaEnforcementServiceError("Insufficient scope for enforcement policy", 403);
274
+ }
275
+ }
226
276
  async findPolicyByScope(scope, tenantId, organizationId) {
227
277
  return this.em.findOne(
228
278
  MfaEnforcementPolicy,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/MfaEnforcementService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string, actor?: EnforcementActorContext): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport type MfaEnforcementAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * write is enforced against the caller's actual tenant/organization scope\n * rather than caller-supplied body fields. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every write\n * with 403; it will be removed in a future release.\n */\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy>\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n scope: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy>\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n scope?: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n this.assertCreatePolicyScope(normalized, effectiveScope)\n\n\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * update is enforced against the caller's actual tenant/organization scope\n * rather than caller-supplied body fields. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every write\n * with 403; it will be removed in a future release.\n */\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy>\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n scope: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy>\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n scope?: MfaEnforcementAuthScope,\n ): Promise<MfaEnforcementPolicy> {\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertUpdatePolicyScope(policy, mergedInput, effectiveScope)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaEnforcementAuthScope} so the policy\n * delete is enforced against the caller's actual tenant/organization scope\n * rather than the caller-supplied id alone. The no-scope overload now treats\n * the caller as a non-superadmin with unknown tenant and rejects every delete\n * with 403; it will be removed in a future release.\n */\n async deletePolicy(id: string): Promise<void>\n async deletePolicy(id: string, scope: MfaEnforcementAuthScope): Promise<void>\n async deletePolicy(id: string, scope?: MfaEnforcementAuthScope): Promise<void> {\n const effectiveScope: MfaEnforcementAuthScope = scope ?? { tenantId: null, isSuperAdmin: false }\n\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n this.assertDeletePolicyScope(policy, effectiveScope)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private assertCreatePolicyScope(\n normalized: {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n },\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n // Fail closed when the caller has no concrete tenant context. Mirrors the\n // MfaAdminService.loadUserForScope precedent \u2014 a missing scope.tenantId for\n // a non-superadmin means we cannot prove ownership of anything.\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (normalized.scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (normalized.tenantId !== scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n normalized.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && normalized.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private assertUpdatePolicyScope(\n existing: MfaEnforcementPolicy,\n proposed: {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n },\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.PLATFORM\n || existing.tenantId !== scope.tenantId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (proposed.scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (proposed.tenantId !== scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n proposed.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && proposed.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && existing.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private assertDeletePolicyScope(\n existing: MfaEnforcementPolicy,\n scope: MfaEnforcementAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n\n if (!scope.tenantId) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.PLATFORM\n || existing.tenantId !== scope.tenantId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n\n if (\n existing.scope === EnforcementScope.ORGANISATION\n && scope.organizationId !== undefined\n && scope.organizationId !== null\n && existing.organizationId !== scope.organizationId\n ) {\n throw new MfaEnforcementServiceError('Insufficient scope for enforcement policy', 403)\n }\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -48,8 +48,11 @@ class MfaVerificationService {
48
48
  availableMethods
49
49
  };
50
50
  }
51
- async prepareChallenge(challengeId, methodType, context) {
52
- const challenge = await this.getValidChallenge(challengeId);
51
+ async prepareChallenge(challengeId, methodType, context, scope) {
52
+ if (!scope) {
53
+ throw new MfaVerificationServiceError("MFA challenge not found", 404);
54
+ }
55
+ const challenge = await this.getValidChallenge(challengeId, scope);
53
56
  await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
54
57
  const provider = this.mfaProviderRegistry.get(methodType);
55
58
  if (!provider) {
@@ -69,8 +72,11 @@ class MfaVerificationService {
69
72
  await this.em.flush();
70
73
  return result;
71
74
  }
72
- async verifyChallenge(challengeId, methodType, payload, runtimeContext) {
73
- const challenge = await this.getValidChallenge(challengeId);
75
+ async verifyChallenge(challengeId, methodType, payload, runtimeContext, scope) {
76
+ if (!scope) {
77
+ throw new MfaVerificationServiceError("MFA challenge not found", 404);
78
+ }
79
+ const challenge = await this.getValidChallenge(challengeId, scope);
74
80
  if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
75
81
  return false;
76
82
  }
@@ -110,8 +116,11 @@ class MfaVerificationService {
110
116
  async verifyRecoveryCode(userId, code) {
111
117
  return this.mfaService.verifyRecoveryCode(userId, code);
112
118
  }
113
- async getValidChallenge(challengeId) {
114
- const challenge = await this.em.findOne(MfaChallenge, { id: challengeId });
119
+ async getValidChallenge(challengeId, scope) {
120
+ const challenge = await this.em.findOne(MfaChallenge, {
121
+ id: challengeId,
122
+ userId: scope.userId
123
+ });
115
124
  if (!challenge) {
116
125
  throw new MfaVerificationServiceError("MFA challenge not found", 404);
117
126
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/MfaVerificationService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const challenge = await this.getValidChallenge(challengeId)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const challenge = await this.getValidChallenge(challengeId)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(challengeId: string): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, { id: challengeId })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport type MfaVerificationAuthScope = {\n userId: string\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<boolean> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(\n challengeId: string,\n scope: MfaVerificationAuthScope,\n ): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, {\n id: challengeId,\n userId: scope.userId,\n })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -23,6 +23,10 @@ const signedSudoTokenPayloadSchema = z.object({
23
23
  tgt: z.string(),
24
24
  exp: z.number()
25
25
  });
26
+ function readTrimmedEnv(value) {
27
+ const trimmed = value?.trim();
28
+ return trimmed ? trimmed : void 0;
29
+ }
26
30
  class SudoChallengeServiceError extends Error {
27
31
  constructor(message, statusCode) {
28
32
  super(message);
@@ -128,12 +132,20 @@ class SudoChallengeService {
128
132
  expiresAt
129
133
  };
130
134
  }
131
- async prepare(sessionId, methodType, request) {
135
+ async prepare(sessionId, methodType, options = {}, request) {
132
136
  const session = await this.getPendingSession(sessionId);
137
+ if (options.expectedUserId && session.userId !== options.expectedUserId) {
138
+ throw new SudoChallengeServiceError("Sudo challenge user mismatch", 403);
139
+ }
133
140
  if (session.challengeMethod !== "mfa") {
134
141
  throw new SudoChallengeServiceError("This sudo session does not require MFA", 400);
135
142
  }
136
- return this.mfaVerificationService.prepareChallenge(session.sessionToken, methodType, { request });
143
+ return this.mfaVerificationService.prepareChallenge(
144
+ session.sessionToken,
145
+ methodType,
146
+ { request },
147
+ { userId: session.userId }
148
+ );
137
149
  }
138
150
  async verify(sessionId, methodType, payload, options, request) {
139
151
  const session = await this.getPendingSession(sessionId);
@@ -161,7 +173,13 @@ class SudoChallengeService {
161
173
  verified = await this.passwordService.verifyPassword(session.userId, password);
162
174
  methodUsed = SudoChallengeMethodUsed.PASSWORD;
163
175
  } else {
164
- verified = await this.mfaVerificationService.verifyChallenge(session.sessionToken, methodType, payload, { request });
176
+ verified = await this.mfaVerificationService.verifyChallenge(
177
+ session.sessionToken,
178
+ methodType,
179
+ payload,
180
+ { request },
181
+ { userId: session.userId }
182
+ );
165
183
  }
166
184
  if (!verified) {
167
185
  await emitSecurityEvent("security.sudo.failed", {
@@ -491,7 +509,11 @@ class SudoChallengeService {
491
509
  }
492
510
  }
493
511
  getSudoSecret() {
494
- return process.env.OM_SECURITY_SUDO_SECRET ?? process.env.AUTH_JWT_SECRET ?? process.env.JWT_SECRET ?? "open-mercato-sudo-secret";
512
+ const secret = readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET) ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET) ?? readTrimmedEnv(process.env.AUTH_SECRET) ?? readTrimmedEnv(process.env.JWT_SECRET);
513
+ if (secret) return secret;
514
+ throw new Error(
515
+ "Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set."
516
+ );
495
517
  }
496
518
  toChallengeMethod(method) {
497
519
  switch (method) {