@open-mercato/enterprise 0.6.5-develop.4882.1.901c3aa813 → 0.6.5-develop.5033.1.c970204a3f

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 (58) hide show
  1. package/dist/modules/security/api/enforcement/[id]/route.js +35 -1
  2. package/dist/modules/security/api/enforcement/[id]/route.js.map +2 -2
  3. package/dist/modules/security/api/enforcement/_shared.js +63 -1
  4. package/dist/modules/security/api/enforcement/_shared.js.map +3 -3
  5. package/dist/modules/security/api/enforcement/compliance/route.js +12 -3
  6. package/dist/modules/security/api/enforcement/compliance/route.js.map +2 -2
  7. package/dist/modules/security/api/enforcement/route.js +25 -2
  8. package/dist/modules/security/api/enforcement/route.js.map +2 -2
  9. package/dist/modules/security/api/mfa/prepare/route.js +1 -1
  10. package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
  11. package/dist/modules/security/api/mfa/recovery/route.js +1 -1
  12. package/dist/modules/security/api/mfa/recovery/route.js.map +2 -2
  13. package/dist/modules/security/api/mfa/verify/route.js +1 -1
  14. package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
  15. package/dist/modules/security/api/users/[id]/mfa/reset/route.js +6 -1
  16. package/dist/modules/security/api/users/[id]/mfa/reset/route.js.map +2 -2
  17. package/dist/modules/security/api/users/[id]/mfa/status/route.js +13 -2
  18. package/dist/modules/security/api/users/[id]/mfa/status/route.js.map +2 -2
  19. package/dist/modules/security/api/users/_shared.js +56 -1
  20. package/dist/modules/security/api/users/_shared.js.map +2 -2
  21. package/dist/modules/security/api/users/mfa/compliance/route.js +17 -7
  22. package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
  23. package/dist/modules/security/commands/createEnforcementPolicy.js +6 -1
  24. package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
  25. package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -1
  26. package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
  27. package/dist/modules/security/commands/resetUserMfa.js +6 -1
  28. package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
  29. package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -1
  30. package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
  31. package/dist/modules/security/services/MfaAdminService.js +22 -5
  32. package/dist/modules/security/services/MfaAdminService.js.map +2 -2
  33. package/dist/modules/security/services/MfaEnforcementService.js +28 -6
  34. package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
  35. package/dist/modules/security/services/MfaVerificationService.js +30 -10
  36. package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
  37. package/dist/modules/security/services/SudoChallengeService.js +14 -3
  38. package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
  39. package/package.json +5 -5
  40. package/src/modules/security/api/enforcement/[id]/route.ts +50 -1
  41. package/src/modules/security/api/enforcement/_shared.ts +83 -2
  42. package/src/modules/security/api/enforcement/compliance/route.ts +10 -1
  43. package/src/modules/security/api/enforcement/route.ts +30 -2
  44. package/src/modules/security/api/mfa/prepare/route.ts +1 -1
  45. package/src/modules/security/api/mfa/recovery/route.ts +1 -1
  46. package/src/modules/security/api/mfa/verify/route.ts +1 -1
  47. package/src/modules/security/api/users/[id]/mfa/reset/route.ts +6 -1
  48. package/src/modules/security/api/users/[id]/mfa/status/route.ts +13 -2
  49. package/src/modules/security/api/users/_shared.ts +69 -1
  50. package/src/modules/security/api/users/mfa/compliance/route.ts +16 -7
  51. package/src/modules/security/commands/createEnforcementPolicy.ts +6 -1
  52. package/src/modules/security/commands/deleteEnforcementPolicy.ts +6 -1
  53. package/src/modules/security/commands/resetUserMfa.ts +6 -1
  54. package/src/modules/security/commands/updateEnforcementPolicy.ts +6 -1
  55. package/src/modules/security/services/MfaAdminService.ts +29 -6
  56. package/src/modules/security/services/MfaEnforcementService.ts +42 -2
  57. package/src/modules/security/services/MfaVerificationService.ts +33 -10
  58. package/src/modules/security/services/SudoChallengeService.ts +16 -11
@@ -30,20 +30,26 @@ class MfaEnforcementService {
30
30
  }
31
31
  return { enforced: true, policy };
32
32
  }
33
- async listPolicies(filters) {
33
+ async getPolicyById(id) {
34
+ return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null });
35
+ }
36
+ async listPolicies(filters, actor) {
37
+ const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {};
34
38
  return this.em.find(
35
39
  MfaEnforcementPolicy,
36
40
  {
37
41
  deletedAt: null,
38
- ...filters?.scope ? { scope: filters.scope } : {}
42
+ ...filters?.scope ? { scope: filters.scope } : {},
43
+ ...tenantConstraint
39
44
  },
40
45
  {
41
46
  orderBy: { updatedAt: "desc" }
42
47
  }
43
48
  );
44
49
  }
45
- async getComplianceReport(scope, scopeId) {
50
+ async getComplianceReport(scope, scopeId, actor) {
46
51
  const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId);
52
+ this.assertActorOwnsScopeFilters(actor, scope, tenantId);
47
53
  const users = await this.em.find(User, {
48
54
  deletedAt: null,
49
55
  ...tenantId ? { tenantId } : {},
@@ -75,8 +81,9 @@ class MfaEnforcementService {
75
81
  overdue
76
82
  };
77
83
  }
78
- async createPolicy(data, adminId) {
84
+ async createPolicy(data, adminId, actor) {
79
85
  const normalized = this.normalizePolicyInput(data);
86
+ this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId);
80
87
  const existing = await this.findPolicyByScope(
81
88
  normalized.scope,
82
89
  normalized.tenantId ?? void 0,
@@ -119,7 +126,7 @@ class MfaEnforcementService {
119
126
  await this.emitDeadlineReminderRequest(policy.id);
120
127
  return policy;
121
128
  }
122
- async updatePolicy(id, data, adminId) {
129
+ async updatePolicy(id, data, adminId, actor) {
123
130
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
124
131
  id,
125
132
  deletedAt: null
@@ -127,6 +134,7 @@ class MfaEnforcementService {
127
134
  if (!policy) {
128
135
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
129
136
  }
137
+ this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
130
138
  const mergedInput = this.normalizePolicyInput({
131
139
  scope: data.scope ?? policy.scope,
132
140
  tenantId: data.tenantId ?? policy.tenantId ?? void 0,
@@ -135,6 +143,7 @@ class MfaEnforcementService {
135
143
  allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,
136
144
  enforcementDeadline: data.enforcementDeadline === void 0 ? policy.enforcementDeadline ?? null : data.enforcementDeadline
137
145
  });
146
+ this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId);
138
147
  if (mergedInput.scope !== policy.scope || mergedInput.tenantId !== (policy.tenantId ?? null) || mergedInput.organizationId !== (policy.organizationId ?? null)) {
139
148
  const conflict = await this.findPolicyByScope(
140
149
  mergedInput.scope,
@@ -162,7 +171,7 @@ class MfaEnforcementService {
162
171
  await this.emitDeadlineReminderRequest(policy.id);
163
172
  return policy;
164
173
  }
165
- async deletePolicy(id) {
174
+ async deletePolicy(id, actor) {
166
175
  const policy = await this.em.findOne(MfaEnforcementPolicy, {
167
176
  id,
168
177
  deletedAt: null
@@ -170,6 +179,7 @@ class MfaEnforcementService {
170
179
  if (!policy) {
171
180
  throw new MfaEnforcementServiceError("Enforcement policy not found", 404);
172
181
  }
182
+ this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null);
173
183
  const now = /* @__PURE__ */ new Date();
174
184
  policy.deletedAt = now;
175
185
  policy.updatedAt = now;
@@ -227,6 +237,18 @@ class MfaEnforcementService {
227
237
  }
228
238
  );
229
239
  }
240
+ assertActorOwnsScopeFilters(actor, scope, tenantId) {
241
+ if (!actor || actor.isSuperAdmin) return;
242
+ if (scope === EnforcementScope.PLATFORM) {
243
+ throw new MfaEnforcementServiceError(
244
+ "Platform scope requires platform administrator privileges.",
245
+ 403
246
+ );
247
+ }
248
+ if (!tenantId || tenantId !== actor.tenantId) {
249
+ throw new MfaEnforcementServiceError("Not authorized for the requested scope.", 403);
250
+ }
251
+ }
230
252
  resolveScopeFilters(scope, scopeId) {
231
253
  if (scope === EnforcementScope.PLATFORM) {
232
254
  return {};
@@ -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\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 listPolicies(filters?: EnforcementPolicyListFilters): Promise<MfaEnforcementPolicy[]> {\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\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 ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\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 ): 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\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 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): 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\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 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": "AACA,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP,SAAS,yBAAyB;AAwB3B,SAAS,6BACd,UACA,MAAM,KAAK,IAAI,GACN;AACT,MAAI,EAAE,oBAAoB,MAAO,QAAO;AACxC,QAAM,eAAe,SAAS,QAAQ;AACtC,MAAI,OAAO,MAAM,YAAY,EAAG,QAAO;AACvC,SAAO,gBAAgB;AACzB;AAEO,MAAM,mCAAmC,MAAM;AAAA,EACpD,YACE,SACgB,YAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,sBAAsB;AAAA,EACjC,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,WAAW,UAAkB,OAA4C;AAC7E,UAAM,SAAS,MAAM,KAAK,uBAAuB,UAAU,KAAK;AAChE,QAAI,CAAC,UAAU,CAAC,OAAO,YAAY;AACjC,aAAO,EAAE,UAAU,OAAO,QAAQ,UAAU,OAAU;AAAA,IACxD;AACA,WAAO,EAAE,UAAU,MAAM,OAAO;AAAA,EAClC;AAAA,EAEA,MAAM,aAAa,SAAyE;AAC1F,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA;AAAA,QACE,WAAW;AAAA,QACX,GAAI,SAAS,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MACnD;AAAA,MACA;AAAA,QACE,SAAS,EAAE,WAAW,OAAO;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,oBACJ,OACA,SAC2B;AAC3B,UAAM,EAAE,UAAU,eAAe,IAAI,KAAK,oBAAoB,OAAO,OAAO;AAC5E,UAAM,QAAQ,MAAM,KAAK,GAAG,KAAK,MAAM;AAAA,MACrC,WAAW;AAAA,MACX,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,GAAI,iBAAiB,EAAE,eAAe,IAAI,CAAC;AAAA,IAC7C,CAAC;AAED,UAAM,QAAQ,MAAM;AACpB,QAAI,UAAU,GAAG;AACf,aAAO,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,EAAE;AAAA,IACzD;AAEA,UAAM,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AAC3C,UAAM,SAAS,MAAM,KAAK,kBAAkB,OAAO,UAAU,cAAc;AAC3E,UAAM,eAAe,KAAK,0BAA0B,QAAQ,kBAAkB,IAAI;AAClF,UAAM,UAAU,MAAM,KAAK,GAAG,KAAK,eAAe;AAAA,MAChD,QAAQ,EAAE,KAAK,QAAQ;AAAA,MACvB,UAAU;AAAA,MACV,WAAW;AAAA,MACX,GAAG;AAAA,IACL,CAAC;AAED,UAAM,kBAAkB,IAAI,IAAI,QAAQ,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC;AACtE,UAAM,WAAW,gBAAgB;AACjC,UAAM,aAAa,KAAK,IAAI,GAAG,QAAQ,QAAQ;AAE/C,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,UAAU,6BAA6B,QAAQ,qBAAqB,GAAG,IAAI,aAAa;AAC9F,UAAM,UAAU,KAAK,IAAI,GAAG,aAAa,OAAO;AAEhD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aACJ,MACA,SAC+B;AAC/B,UAAM,aAAa,KAAK,qBAAqB,IAAI;AACjD,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B,WAAW;AAAA,MACX,WAAW,YAAY;AAAA,MACvB,WAAW,kBAAkB;AAAA,IAC/B;AAEA,QAAI,UAAU;AACZ,eAAS,aAAa,WAAW;AACjC,eAAS,iBAAiB,WAAW;AACrC,eAAS,sBAAsB,WAAW;AAC1C,eAAS,aAAa;AACtB,eAAS,YAAY,oBAAI,KAAK;AAC9B,YAAM,KAAK,GAAG,MAAM;AAEpB,YAAM,kBAAkB,gCAAgC;AAAA,QACtD;AAAA,QACA,UAAU,SAAS;AAAA,QACnB,OAAO,SAAS;AAAA,MAClB,CAAC;AACD,YAAM,KAAK,4BAA4B,SAAS,EAAE;AAClD,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,SAAS,KAAK,GAAG,OAAO,sBAAsB;AAAA,MAClD,OAAO,WAAW;AAAA,MAClB,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW;AAAA,MAC3B,YAAY,WAAW;AAAA,MACvB,gBAAgB,WAAW;AAAA,MAC3B,qBAAqB,WAAW;AAAA,MAChC,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,SAAK,GAAG,QAAQ,MAAM;AACtB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,gCAAgC;AAAA,MACtD;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,OAAO,OAAO;AAAA,IAChB,CAAC;AACD,UAAM,KAAK,4BAA4B,OAAO,EAAE;AAChD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aACJ,IACA,MACA,SAC+B;AAC/B,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,sBAAsB;AAAA,MACzD;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,2BAA2B,gCAAgC,GAAG;AAAA,IAC1E;AAEA,UAAM,cAAc,KAAK,qBAAqB;AAAA,MAC5C,OAAO,KAAK,SAAS,OAAO;AAAA,MAC5B,UAAU,KAAK,YAAY,OAAO,YAAY;AAAA,MAC9C,gBAAgB,KAAK,kBAAkB,OAAO,kBAAkB;AAAA,MAChE,YAAY,KAAK,cAAc,OAAO;AAAA,MACtC,gBAAgB,KAAK,kBAAkB,OAAO,kBAAkB;AAAA,MAChE,qBACE,KAAK,wBAAwB,SACxB,OAAO,uBAAuB,OAC/B,KAAK;AAAA,IACb,CAAC;AAED,QACE,YAAY,UAAU,OAAO,SAC7B,YAAY,cAAc,OAAO,YAAY,SAC7C,YAAY,oBAAoB,OAAO,kBAAkB,OACzD;AACA,YAAM,WAAW,MAAM,KAAK;AAAA,QAC1B,YAAY;AAAA,QACZ,YAAY,YAAY;AAAA,QACxB,YAAY,kBAAkB;AAAA,MAChC;AACA,UAAI,YAAY,SAAS,OAAO,OAAO,IAAI;AACzC,cAAM,IAAI,2BAA2B,oDAAoD,GAAG;AAAA,MAC9F;AAAA,IACF;AAEA,WAAO,QAAQ,YAAY;AAC3B,WAAO,WAAW,YAAY;AAC9B,WAAO,iBAAiB,YAAY;AACpC,WAAO,aAAa,YAAY;AAChC,WAAO,iBAAiB,YAAY;AACpC,WAAO,sBAAsB,YAAY;AACzC,WAAO,aAAa;AACpB,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,gCAAgC;AAAA,MACtD;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,OAAO,OAAO;AAAA,IAChB,CAAC;AACD,UAAM,KAAK,4BAA4B,OAAO,EAAE;AAChD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAa,IAA2B;AAC5C,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,sBAAsB;AAAA,MACzD;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,2BAA2B,gCAAgC,GAAG;AAAA,IAC1E;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,WAAO,YAAY;AACnB,WAAO,YAAY;AACnB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,oBAAoB,QAAyC;AACjE,UAAM,SAAS,MAAM,KAAK,0BAA0B,MAAM;AAC1D,QAAI,CAAC,UAAU,CAAC,OAAO,YAAY;AACjC,aAAO,EAAE,WAAW,MAAM,UAAU,MAAM;AAAA,IAC5C;AAEA,UAAM,eAAe,KAAK,0BAA0B,OAAO,kBAAkB,IAAI;AACjF,UAAM,cAAc,MAAM,KAAK,GAAG,MAAM,eAAe;AAAA,MACrD;AAAA,MACA,UAAU;AAAA,MACV,WAAW;AAAA,MACX,GAAG;AAAA,IACL,CAAC;AAED,WAAO;AAAA,MACL,WAAW,cAAc;AAAA,MACzB,UAAU;AAAA,MACV,UAAU,OAAO,uBAAuB;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,MAAM,0BAA0B,QAAsD;AACpF,UAAM,OAAO,MAAM,KAAK,aAAa,MAAM;AAC3C,QAAI,CAAC,MAAM,UAAU;AACnB,YAAM,IAAI,2BAA2B,kBAAkB,GAAG;AAAA,IAC5D;AAEA,WAAO,KAAK,uBAAuB,KAAK,UAAU,KAAK,kBAAkB,MAAS;AAAA,EACpF;AAAA,EAEA,MAAc,uBACZ,UACA,OACsC;AACtC,QAAI,OAAO;AACT,YAAM,qBAAqB,MAAM,KAAK;AAAA,QACpC,iBAAiB;AAAA,QACjB;AAAA,QACA;AAAA,MACF;AACA,UAAI,mBAAoB,QAAO;AAAA,IACjC;AAEA,UAAM,eAAe,MAAM,KAAK,kBAAkB,iBAAiB,QAAQ,UAAU,MAAS;AAC9F,QAAI,aAAc,QAAO;AAEzB,WAAO,KAAK,kBAAkB,iBAAiB,UAAU,QAAW,MAAS;AAAA,EAC/E;AAAA,EAEA,MAAc,kBACZ,OACA,UACA,gBACsC;AACtC,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA;AAAA,QACE;AAAA,QACA,UAAU,YAAY;AAAA,QACtB,gBAAgB,kBAAkB;AAAA,QAClC,WAAW;AAAA,MACb;AAAA,MACA;AAAA,QACE,SAAS,EAAE,WAAW,OAAO;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBACN,OACA,SACgD;AAChD,QAAI,UAAU,iBAAiB,UAAU;AACvC,aAAO,CAAC;AAAA,IACV;AAEA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,2BAA2B,0DAA0D,GAAG;AAAA,IACpG;AAEA,QAAI,UAAU,iBAAiB,QAAQ;AACrC,aAAO,EAAE,UAAU,QAAQ;AAAA,IAC7B;AAEA,UAAM,CAAC,UAAU,cAAc,IAAI,QAAQ,MAAM,GAAG;AACpD,QAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,UAAU,eAAe;AAAA,EACpC;AAAA,EAEQ,qBAAqB,MAc3B;AACA,UAAM,aAAa,KAAK,cAAc;AACtC,UAAM,iBAAiB,KAAK,wBAAwB,KAAK,cAAc;AACvE,UAAM,sBAAsB,KAAK,uBAAuB;AAExD,QAAI,KAAK,UAAU,iBAAiB,UAAU;AAC5C,aAAO;AAAA,QACL,OAAO,KAAK;AAAA,QACZ,UAAU;AAAA,QACV,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,KAAK,UAAU,iBAAiB,QAAQ;AAC1C,UAAI,CAAC,KAAK,UAAU;AAClB,cAAM,IAAI,2BAA2B,yCAAyC,GAAG;AAAA,MACnF;AACA,aAAO;AAAA,QACL,OAAO,KAAK;AAAA,QACZ,UAAU,KAAK;AAAA,QACf,gBAAgB;AAAA,QAChB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,YAAY,CAAC,KAAK,gBAAgB;AAC1C,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,wBAAwB,gBAAmD;AACjF,QAAI,CAAC,kBAAkB,eAAe,WAAW,GAAG;AAClD,aAAO;AAAA,IACT;AACA,WAAO,MAAM,KAAK,IAAI,IAAI,eAAe,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC;AAAA,EAC1F;AAAA,EAEQ,0BACN,gBAC8B;AAC9B,QAAI,CAAC,kBAAkB,eAAe,WAAW,GAAG;AAClD,aAAO,CAAC;AAAA,IACV;AACA,WAAO,EAAE,MAAM,EAAE,KAAK,eAAe,EAAE;AAAA,EACzC;AAAA,EAEA,MAAc,aAAa,QAAsC;AAC/D,WAAO,sBAAsB,KAAK,IAAI,MAAM,EAAE,IAAI,QAAQ,WAAW,KAAK,GAAG,QAAW,CAAC,CAAC;AAAA,EAC5F;AAAA,EAEA,MAAc,4BAA4B,UAAiC;AACzE,UAAM,kBAAkB,oDAAoD;AAAA,MAC1E;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,IAAO,gCAAQ;",
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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",
6
6
  "names": []
7
7
  }
@@ -50,6 +50,7 @@ class MfaVerificationService {
50
50
  }
51
51
  async prepareChallenge(challengeId, methodType, context) {
52
52
  const challenge = await this.getValidChallenge(challengeId);
53
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
53
54
  const provider = this.mfaProviderRegistry.get(methodType);
54
55
  if (!provider) {
55
56
  throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400);
@@ -73,12 +74,9 @@ class MfaVerificationService {
73
74
  if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
74
75
  return false;
75
76
  }
77
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
76
78
  if (challenge.methodType && challenge.methodType !== methodType) {
77
- challenge.attempts += 1;
78
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
79
- challenge.expiresAt = /* @__PURE__ */ new Date();
80
- }
81
- await this.em.flush();
79
+ await this.registerFailedAttempt(challenge);
82
80
  return false;
83
81
  }
84
82
  const provider = this.mfaProviderRegistry.get(methodType);
@@ -106,11 +104,7 @@ class MfaVerificationService {
106
104
  });
107
105
  return true;
108
106
  }
109
- challenge.attempts += 1;
110
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
111
- challenge.expiresAt = /* @__PURE__ */ new Date();
112
- }
113
- await this.em.flush();
107
+ await this.registerFailedAttempt(challenge);
114
108
  return false;
115
109
  }
116
110
  async verifyRecoveryCode(userId, code) {
@@ -129,6 +123,32 @@ class MfaVerificationService {
129
123
  }
130
124
  return challenge;
131
125
  }
126
+ async assertMethodAllowedByPolicy(userId, methodType) {
127
+ const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId);
128
+ if (!policy?.isEnforced || !policy.allowedMethods?.length) {
129
+ return;
130
+ }
131
+ if (!policy.allowedMethods.includes(methodType)) {
132
+ throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403);
133
+ }
134
+ }
135
+ async registerFailedAttempt(challenge) {
136
+ const maxAttempts = this.securityConfig.mfa.maxAttempts;
137
+ const rows = await this.em.getConnection().execute(
138
+ "UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts",
139
+ [challenge.id, maxAttempts]
140
+ );
141
+ const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts;
142
+ challenge.attempts = updatedAttempts;
143
+ if (updatedAttempts >= maxAttempts) {
144
+ const now = /* @__PURE__ */ new Date();
145
+ await this.em.getConnection().execute(
146
+ "UPDATE mfa_challenges SET expires_at = ? WHERE id = ?",
147
+ [now, challenge.id]
148
+ );
149
+ challenge.expiresAt = now;
150
+ }
151
+ }
132
152
  async getActiveMethods(userId) {
133
153
  const methods = await this.em.find(
134
154
  UserMfaMethod,
@@ -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 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 if (challenge.methodType && challenge.methodType !== methodType) {\n challenge.attempts += 1\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n challenge.expiresAt = new Date()\n }\n await this.em.flush()\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 challenge.attempts += 1\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n challenge.expiresAt = new Date()\n }\n await this.em.flush()\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 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 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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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { z } from "zod";
2
3
  import { User } from "@open-mercato/core/modules/auth/data/entities";
3
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
5
  import {
@@ -14,6 +15,14 @@ import {
14
15
  import { emitSecurityEvent } from "../events.js";
15
16
  import { sudoTargets as defaultSudoTargets } from "../security.sudo.js";
16
17
  import { readSecurityModuleConfig } from "../lib/security-config.js";
18
+ const signedSudoTokenPayloadSchema = z.object({
19
+ sid: z.string(),
20
+ sub: z.string(),
21
+ tid: z.string().nullable(),
22
+ oid: z.string().nullable(),
23
+ tgt: z.string(),
24
+ exp: z.number()
25
+ });
17
26
  class SudoChallengeServiceError extends Error {
18
27
  constructor(message, statusCode) {
19
28
  super(message);
@@ -472,9 +481,11 @@ class SudoChallengeService {
472
481
  if (signatureBuffer.length !== expectedBuffer.length) return null;
473
482
  if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null;
474
483
  try {
475
- const parsed = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
476
- if (!parsed || typeof parsed !== "object") return null;
477
- return parsed;
484
+ const parsed = signedSudoTokenPayloadSchema.safeParse(
485
+ JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"))
486
+ );
487
+ if (!parsed.success) return null;
488
+ return parsed.data;
478
489
  } catch {
479
490
  return null;
480
491
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/SudoChallengeService.ts"],
4
- "sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { EntityManager, FilterQuery } 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 dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\ntype SignedSudoTokenPayload = {\n sid: string\n sub: string\n tid: string | null\n oid: string | null\n tgt: string\n exp: number\n}\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n\n const candidates = await this.em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const protection = await this.isProtected(\n targetIdentifier,\n options?.tenantId ?? null,\n options?.organizationId ?? null,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(session.sessionToken, methodType, { request })\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n options.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(session.sessionToken, methodType, payload, { request })\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const sudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: options.targetIdentifier,\n exp: expiresAt.getTime(),\n })\n\n session.sessionToken = sudoToken\n session.challengeMethod = methodUsed\n session.expiresAt = expiresAt\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n existing.updatedAt = new Date()\n await this.em.flush()\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n return 'password'\n }\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<SudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) as SignedSudoTokenPayload\n if (!parsed || typeof parsed !== 'object') return null\n return parsed\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n return process.env.OM_SECURITY_SUDO_SECRET\n ?? process.env.AUTH_JWT_SECRET\n ?? process.env.JWT_SECRET\n ?? 'open-mercato-sudo-secret'\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager, FilterQuery } 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 dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\nconst signedSudoTokenPayloadSchema = z.object({\n sid: z.string(),\n sub: z.string(),\n tid: z.string().nullable(),\n oid: z.string().nullable(),\n tgt: z.string(),\n exp: z.number(),\n})\n\ntype SignedSudoTokenPayload = z.infer<typeof signedSudoTokenPayloadSchema>\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n\n const candidates = await this.em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const protection = await this.isProtected(\n targetIdentifier,\n options?.tenantId ?? null,\n options?.organizationId ?? null,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(session.sessionToken, methodType, { request })\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n options.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(session.sessionToken, methodType, payload, { request })\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const sudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: options.targetIdentifier,\n exp: expiresAt.getTime(),\n })\n\n session.sessionToken = sudoToken\n session.challengeMethod = methodUsed\n session.expiresAt = expiresAt\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n existing.updatedAt = new Date()\n await this.em.flush()\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n return 'password'\n }\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<SudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = signedSudoTokenPayloadSchema.safeParse(\n JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')),\n )\n if (!parsed.success) return null\n return parsed.data\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n return process.env.OM_SECURITY_SUDO_SECRET\n ?? process.env.AUTH_JWT_SECRET\n ?? process.env.JWT_SECRET\n ?? 'open-mercato-sudo-secret'\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/enterprise",
3
- "version": "0.6.5-develop.4882.1.901c3aa813",
3
+ "version": "0.6.5-develop.5033.1.c970204a3f",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -64,8 +64,8 @@
64
64
  }
65
65
  },
66
66
  "dependencies": {
67
- "@open-mercato/core": "0.6.5-develop.4882.1.901c3aa813",
68
- "@open-mercato/ui": "0.6.5-develop.4882.1.901c3aa813",
67
+ "@open-mercato/core": "0.6.5-develop.5033.1.c970204a3f",
68
+ "@open-mercato/ui": "0.6.5-develop.5033.1.c970204a3f",
69
69
  "@simplewebauthn/browser": "^13.3.0",
70
70
  "@simplewebauthn/server": "^13.3.1",
71
71
  "@simplewebauthn/types": "^12.0.0",
@@ -75,12 +75,12 @@
75
75
  "qrcode": "^1.5.4"
76
76
  },
77
77
  "peerDependencies": {
78
- "@open-mercato/shared": "0.6.5-develop.4882.1.901c3aa813",
78
+ "@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
79
79
  "react": "^19.0.0",
80
80
  "react-dom": "^19.0.0"
81
81
  },
82
82
  "devDependencies": {
83
- "@open-mercato/shared": "0.6.5-develop.4882.1.901c3aa813",
83
+ "@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
84
84
  "@types/jest": "^30.0.0",
85
85
  "@types/react": "^19.2.16",
86
86
  "@types/react-dom": "^19.2.3",