@open-mercato/enterprise 0.6.8-develop.7051.1.2e20648fd5 → 0.6.8-develop.7056.1.1a563a270d

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.
@@ -1,5 +1,5 @@
1
1
  import { signJwt, verifyJwt } from "@open-mercato/shared/lib/auth/jwt";
2
- import { readSecurityModuleConfig } from "../lib/security-config.js";
2
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from "../lib/security-config.js";
3
3
  function readClaims(token) {
4
4
  const payload = verifyJwt(token);
5
5
  if (!payload || typeof payload !== "object") return null;
@@ -35,11 +35,14 @@ const interceptors = [
35
35
  if (response.statusCode !== 200) return {};
36
36
  if (response.body.ok !== true || response.body.mfa_required === true) return {};
37
37
  if (typeof response.body.token !== "string" || response.body.token.length === 0) return {};
38
- if (readSecurityModuleConfig().mfa.emergencyBypass) return {};
39
38
  const claims = readClaims(response.body.token);
40
39
  if (!claims) return {};
41
40
  const mfaVerificationService = resolveMfaVerificationService(context.container);
42
41
  if (!mfaVerificationService) return {};
42
+ if (readSecurityModuleConfig().mfa.emergencyBypass) {
43
+ emitMfaEmergencyBypassActiveWarning("login MFA challenge bypassed", { userId: claims.sub });
44
+ return {};
45
+ }
43
46
  try {
44
47
  const challenge = await mfaVerificationService.createChallenge(claims.sub);
45
48
  const pendingToken = signJwt(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/api/interceptors.ts"],
4
- "sourcesContent": ["import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'\nimport { signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype JwtClaims = {\n sub: string\n sid?: string | null\n tenantId?: string | null\n orgId?: string | null\n email?: string | null\n roles?: string[]\n}\n\ntype MfaVerificationServiceLike = {\n createChallenge: (userId: string) => Promise<{\n challengeId: string\n availableMethods: Array<{ type: string; label: string; icon: string }>\n }>\n}\n\nfunction readClaims(token: string): JwtClaims | null {\n const payload = verifyJwt(token)\n if (!payload || typeof payload !== 'object') return null\n const sub = typeof payload.sub === 'string' ? payload.sub : null\n if (!sub) return null\n return {\n sub,\n sid: typeof payload.sid === 'string' ? payload.sid : null,\n tenantId: typeof payload.tenantId === 'string' ? payload.tenantId : null,\n orgId: typeof payload.orgId === 'string' ? payload.orgId : null,\n email: typeof payload.email === 'string' ? payload.email : null,\n roles: Array.isArray(payload.roles) ? payload.roles.filter((value: unknown): value is string => typeof value === 'string') : [],\n }\n}\n\nfunction resolveMfaVerificationService(container: { resolve: (name: string) => unknown }): MfaVerificationServiceLike | null {\n try {\n const resolved = container.resolve('mfaVerificationService')\n if (\n !resolved\n || typeof resolved !== 'object'\n || typeof (resolved as { createChallenge?: unknown }).createChallenge !== 'function'\n ) {\n return null\n }\n return resolved as MfaVerificationServiceLike\n } catch {\n return null\n }\n}\n\nexport const interceptors: ApiInterceptor[] = [\n {\n id: 'security.auth.login.mfa-challenge',\n targetRoute: 'auth/login',\n methods: ['POST'],\n priority: 50,\n async after(_request, response, context) {\n if (response.statusCode !== 200) return {}\n if (response.body.ok !== true || response.body.mfa_required === true) return {}\n if (typeof response.body.token !== 'string' || response.body.token.length === 0) return {}\n if (readSecurityModuleConfig().mfa.emergencyBypass) return {}\n\n const claims = readClaims(response.body.token)\n if (!claims) return {}\n\n const mfaVerificationService = resolveMfaVerificationService(context.container as { resolve: (name: string) => unknown })\n if (!mfaVerificationService) return {}\n\n try {\n const challenge = await mfaVerificationService.createChallenge(claims.sub)\n const pendingToken = signJwt(\n {\n sub: claims.sub,\n sid: claims.sid ?? undefined,\n tenantId: claims.tenantId ?? null,\n orgId: claims.orgId ?? null,\n email: claims.email ?? null,\n roles: claims.roles ?? [],\n mfa_pending: true,\n mfa_verified: false,\n },\n undefined,\n 60 * 10,\n )\n return {\n replace: {\n ok: true,\n mfa_required: true,\n challenge_id: challenge.challengeId,\n available_methods: challenge.availableMethods,\n token: pendingToken,\n },\n }\n } catch {\n return {}\n }\n },\n },\n]\n"],
5
- "mappings": "AACA,SAAS,SAAS,iBAAiB;AACnC,SAAS,gCAAgC;AAkBzC,SAAS,WAAW,OAAiC;AACnD,QAAM,UAAU,UAAU,KAAK;AAC/B,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;AAAA,IACL;AAAA,IACA,KAAK,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAAA,IACrD,UAAU,OAAO,QAAQ,aAAa,WAAW,QAAQ,WAAW;AAAA,IACpE,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,IAC3D,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,IAC3D,OAAO,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,MAAM,OAAO,CAAC,UAAoC,OAAO,UAAU,QAAQ,IAAI,CAAC;AAAA,EAChI;AACF;AAEA,SAAS,8BAA8B,WAAsF;AAC3H,MAAI;AACF,UAAM,WAAW,UAAU,QAAQ,wBAAwB;AAC3D,QACE,CAAC,YACE,OAAO,aAAa,YACpB,OAAQ,SAA2C,oBAAoB,YAC1E;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,eAAiC;AAAA,EAC5C;AAAA,IACE,IAAI;AAAA,IACJ,aAAa;AAAA,IACb,SAAS,CAAC,MAAM;AAAA,IAChB,UAAU;AAAA,IACV,MAAM,MAAM,UAAU,UAAU,SAAS;AACvC,UAAI,SAAS,eAAe,IAAK,QAAO,CAAC;AACzC,UAAI,SAAS,KAAK,OAAO,QAAQ,SAAS,KAAK,iBAAiB,KAAM,QAAO,CAAC;AAC9E,UAAI,OAAO,SAAS,KAAK,UAAU,YAAY,SAAS,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AACzF,UAAI,yBAAyB,EAAE,IAAI,gBAAiB,QAAO,CAAC;AAE5D,YAAM,SAAS,WAAW,SAAS,KAAK,KAAK;AAC7C,UAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,YAAM,yBAAyB,8BAA8B,QAAQ,SAAmD;AACxH,UAAI,CAAC,uBAAwB,QAAO,CAAC;AAErC,UAAI;AACF,cAAM,YAAY,MAAM,uBAAuB,gBAAgB,OAAO,GAAG;AACzE,cAAM,eAAe;AAAA,UACnB;AAAA,YACE,KAAK,OAAO;AAAA,YACZ,KAAK,OAAO,OAAO;AAAA,YACnB,UAAU,OAAO,YAAY;AAAA,YAC7B,OAAO,OAAO,SAAS;AAAA,YACvB,OAAO,OAAO,SAAS;AAAA,YACvB,OAAO,OAAO,SAAS,CAAC;AAAA,YACxB,aAAa;AAAA,YACb,cAAc;AAAA,UAChB;AAAA,UACA;AAAA,UACA,KAAK;AAAA,QACP;AACA,eAAO;AAAA,UACL,SAAS;AAAA,YACP,IAAI;AAAA,YACJ,cAAc;AAAA,YACd,cAAc,UAAU;AAAA,YACxB,mBAAmB,UAAU;AAAA,YAC7B,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,QAAQ;AACN,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'\nimport { signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from '../lib/security-config'\n\ntype JwtClaims = {\n sub: string\n sid?: string | null\n tenantId?: string | null\n orgId?: string | null\n email?: string | null\n roles?: string[]\n}\n\ntype MfaVerificationServiceLike = {\n createChallenge: (userId: string) => Promise<{\n challengeId: string\n availableMethods: Array<{ type: string; label: string; icon: string }>\n }>\n}\n\nfunction readClaims(token: string): JwtClaims | null {\n const payload = verifyJwt(token)\n if (!payload || typeof payload !== 'object') return null\n const sub = typeof payload.sub === 'string' ? payload.sub : null\n if (!sub) return null\n return {\n sub,\n sid: typeof payload.sid === 'string' ? payload.sid : null,\n tenantId: typeof payload.tenantId === 'string' ? payload.tenantId : null,\n orgId: typeof payload.orgId === 'string' ? payload.orgId : null,\n email: typeof payload.email === 'string' ? payload.email : null,\n roles: Array.isArray(payload.roles) ? payload.roles.filter((value: unknown): value is string => typeof value === 'string') : [],\n }\n}\n\nfunction resolveMfaVerificationService(container: { resolve: (name: string) => unknown }): MfaVerificationServiceLike | null {\n try {\n const resolved = container.resolve('mfaVerificationService')\n if (\n !resolved\n || typeof resolved !== 'object'\n || typeof (resolved as { createChallenge?: unknown }).createChallenge !== 'function'\n ) {\n return null\n }\n return resolved as MfaVerificationServiceLike\n } catch {\n return null\n }\n}\n\nexport const interceptors: ApiInterceptor[] = [\n {\n id: 'security.auth.login.mfa-challenge',\n targetRoute: 'auth/login',\n methods: ['POST'],\n priority: 50,\n async after(_request, response, context) {\n if (response.statusCode !== 200) return {}\n if (response.body.ok !== true || response.body.mfa_required === true) return {}\n if (typeof response.body.token !== 'string' || response.body.token.length === 0) return {}\n\n const claims = readClaims(response.body.token)\n if (!claims) return {}\n\n const mfaVerificationService = resolveMfaVerificationService(context.container as { resolve: (name: string) => unknown })\n if (!mfaVerificationService) return {}\n\n if (readSecurityModuleConfig().mfa.emergencyBypass) {\n emitMfaEmergencyBypassActiveWarning('login MFA challenge bypassed', { userId: claims.sub })\n return {}\n }\n\n try {\n const challenge = await mfaVerificationService.createChallenge(claims.sub)\n const pendingToken = signJwt(\n {\n sub: claims.sub,\n sid: claims.sid ?? undefined,\n tenantId: claims.tenantId ?? null,\n orgId: claims.orgId ?? null,\n email: claims.email ?? null,\n roles: claims.roles ?? [],\n mfa_pending: true,\n mfa_verified: false,\n },\n undefined,\n 60 * 10,\n )\n return {\n replace: {\n ok: true,\n mfa_required: true,\n challenge_id: challenge.challengeId,\n available_methods: challenge.availableMethods,\n token: pendingToken,\n },\n }\n } catch {\n return {}\n }\n },\n },\n]\n"],
5
+ "mappings": "AACA,SAAS,SAAS,iBAAiB;AACnC,SAAS,qCAAqC,gCAAgC;AAkB9E,SAAS,WAAW,OAAiC;AACnD,QAAM,UAAU,UAAU,KAAK;AAC/B,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,MAAM,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;AAAA,IACL;AAAA,IACA,KAAK,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAAA,IACrD,UAAU,OAAO,QAAQ,aAAa,WAAW,QAAQ,WAAW;AAAA,IACpE,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,IAC3D,OAAO,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAAA,IAC3D,OAAO,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,MAAM,OAAO,CAAC,UAAoC,OAAO,UAAU,QAAQ,IAAI,CAAC;AAAA,EAChI;AACF;AAEA,SAAS,8BAA8B,WAAsF;AAC3H,MAAI;AACF,UAAM,WAAW,UAAU,QAAQ,wBAAwB;AAC3D,QACE,CAAC,YACE,OAAO,aAAa,YACpB,OAAQ,SAA2C,oBAAoB,YAC1E;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,eAAiC;AAAA,EAC5C;AAAA,IACE,IAAI;AAAA,IACJ,aAAa;AAAA,IACb,SAAS,CAAC,MAAM;AAAA,IAChB,UAAU;AAAA,IACV,MAAM,MAAM,UAAU,UAAU,SAAS;AACvC,UAAI,SAAS,eAAe,IAAK,QAAO,CAAC;AACzC,UAAI,SAAS,KAAK,OAAO,QAAQ,SAAS,KAAK,iBAAiB,KAAM,QAAO,CAAC;AAC9E,UAAI,OAAO,SAAS,KAAK,UAAU,YAAY,SAAS,KAAK,MAAM,WAAW,EAAG,QAAO,CAAC;AAEzF,YAAM,SAAS,WAAW,SAAS,KAAK,KAAK;AAC7C,UAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,YAAM,yBAAyB,8BAA8B,QAAQ,SAAmD;AACxH,UAAI,CAAC,uBAAwB,QAAO,CAAC;AAErC,UAAI,yBAAyB,EAAE,IAAI,iBAAiB;AAClD,4CAAoC,gCAAgC,EAAE,QAAQ,OAAO,IAAI,CAAC;AAC1F,eAAO,CAAC;AAAA,MACV;AAEA,UAAI;AACF,cAAM,YAAY,MAAM,uBAAuB,gBAAgB,OAAO,GAAG;AACzE,cAAM,eAAe;AAAA,UACnB;AAAA,YACE,KAAK,OAAO;AAAA,YACZ,KAAK,OAAO,OAAO;AAAA,YACnB,UAAU,OAAO,YAAY;AAAA,YAC7B,OAAO,OAAO,SAAS;AAAA,YACvB,OAAO,OAAO,SAAS;AAAA,YACvB,OAAO,OAAO,SAAS,CAAC;AAAA,YACxB,aAAa;AAAA,YACb,cAAc;AAAA,UAChB;AAAA,UACA;AAAA,UACA,KAAK;AAAA,QACP;AACA,eAAO;AAAA,UACL,SAAS;AAAA,YACP,IAAI;AAAA,YACJ,cAAc;AAAA,YACd,cAAc,UAAU;AAAA,YACxB,mBAAmB,UAAU;AAAA,YAC7B,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,QAAQ;AACN,eAAO,CAAC;AAAA,MACV;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { createLogger } from "@open-mercato/shared/lib/logger";
2
2
  import { isEnforcementDeadlineOverdue } from "../services/MfaEnforcementService.js";
3
- import { readSecurityModuleConfig } from "./security-config.js";
3
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from "./security-config.js";
4
4
  const MFA_ENROLLMENT_PATH = "/backend/profile/security/mfa";
5
5
  const logger = createLogger("security").child({ component: "mfa-enforcement-redirect" });
6
6
  function resolveDeadlineRedirectState(deadline) {
@@ -46,10 +46,18 @@ async function resolveMfaEnrollmentRedirect(args) {
46
46
  if (!auth || typeof auth.sub !== "string" || auth.sub.length === 0) return null;
47
47
  if (auth.mfa_pending === true) return null;
48
48
  if (isExemptPath(pathname)) return null;
49
- if (readSecurityModuleConfig().mfa.emergencyBypass) return null;
49
+ const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass;
50
50
  const enforcementService = resolveEnforcementService(container);
51
51
  if (!enforcementService.service) {
52
52
  if (!canFailClosed(auth)) return null;
53
+ if (emergencyBypass) {
54
+ emitMfaEmergencyBypassActiveWarning("mfa-enrollment-redirect bypassed", {
55
+ userId: auth.sub,
56
+ pathname,
57
+ reason: "enforcement-service-unavailable"
58
+ });
59
+ return null;
60
+ }
53
61
  logger.error("Unable to resolve MFA enforcement service; redirecting to enrollment", {
54
62
  err: enforcementService.error
55
63
  });
@@ -60,9 +68,27 @@ async function resolveMfaEnrollmentRedirect(args) {
60
68
  if (!compliance.enforced || compliance.compliant) return null;
61
69
  const deadlineState = resolveDeadlineRedirectState(compliance.deadline);
62
70
  if (!deadlineState.shouldRedirect) return null;
71
+ if (emergencyBypass) {
72
+ emitMfaEmergencyBypassActiveWarning("mfa-enrollment-redirect bypassed", {
73
+ userId: auth.sub,
74
+ pathname,
75
+ enforced: compliance.enforced,
76
+ compliant: compliance.compliant,
77
+ overdue: deadlineState.overdue
78
+ });
79
+ return null;
80
+ }
63
81
  return createEnrollmentRedirect(pathname, deadlineState.overdue);
64
82
  } catch (error) {
65
83
  if (canFailClosed(auth)) {
84
+ if (emergencyBypass) {
85
+ emitMfaEmergencyBypassActiveWarning("mfa-enrollment-redirect bypassed", {
86
+ userId: auth.sub,
87
+ pathname,
88
+ reason: "compliance-check-failed"
89
+ });
90
+ return null;
91
+ }
66
92
  logger.error("Unable to verify MFA enforcement compliance; redirecting to enrollment", { err: error });
67
93
  return createEnrollmentRedirect(pathname);
68
94
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/lib/enforcement-redirect.ts"],
4
- "sourcesContent": ["import type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { isEnforcementDeadlineOverdue } from '../services/MfaEnforcementService'\nimport { readSecurityModuleConfig } from './security-config'\n\nconst MFA_ENROLLMENT_PATH = '/backend/profile/security/mfa'\nconst logger = createLogger('security').child({ component: 'mfa-enforcement-redirect' })\n\ntype MfaComplianceResult = {\n compliant: boolean\n enforced: boolean\n deadline?: Date\n}\n\ntype MfaEnforcementServiceLike = {\n checkUserCompliance: (userId: string) => Promise<MfaComplianceResult>\n}\n\ntype ServiceContainerLike = {\n resolve: (name: string) => unknown\n}\n\ntype EnforcementServiceResolution = {\n error?: unknown\n service: MfaEnforcementServiceLike | null\n}\n\nfunction resolveDeadlineRedirectState(deadline?: Date): {\n overdue: boolean\n shouldRedirect: boolean\n} {\n if (!(deadline instanceof Date) || Number.isNaN(deadline.getTime())) {\n return { overdue: false, shouldRedirect: true }\n }\n\n const overdue = isEnforcementDeadlineOverdue(deadline)\n return { overdue, shouldRedirect: overdue }\n}\n\nfunction isExemptPath(pathname: string): boolean {\n return pathname.startsWith('/backend/profile/security')\n}\n\nfunction createEnrollmentRedirect(pathname: string, overdue = false): string {\n const searchParams = new URLSearchParams({\n redirect: pathname,\n reason: 'mfa_enrollment_required',\n })\n if (overdue) {\n searchParams.set('overdue', '1')\n }\n return `${MFA_ENROLLMENT_PATH}?${searchParams.toString()}`\n}\n\nfunction canFailClosed(auth: NonNullable<AuthContext>): boolean {\n return typeof auth.tenantId === 'string' && auth.tenantId.length > 0\n}\n\nfunction resolveEnforcementService(\n container: ServiceContainerLike,\n): EnforcementServiceResolution {\n try {\n const resolved = container.resolve('mfaEnforcementService')\n if (\n !resolved\n || typeof resolved !== 'object'\n || typeof (resolved as { checkUserCompliance?: unknown }).checkUserCompliance !== 'function'\n ) {\n const receivedShape = resolved === null\n ? 'null'\n : Array.isArray(resolved)\n ? 'array'\n : typeof resolved === 'object'\n ? `object with keys: ${Object.keys(resolved).sort((left, right) => left.localeCompare(right)).join(', ') || '(none)'}`\n : typeof resolved\n return {\n error: new TypeError(`[internal] Malformed MFA enforcement service (${receivedShape}); expected checkUserCompliance()`),\n service: null,\n }\n }\n return { service: resolved as MfaEnforcementServiceLike }\n } catch (error) {\n return { error, service: null }\n }\n}\n\nexport async function resolveMfaEnrollmentRedirect(args: {\n auth: AuthContext\n pathname: string\n container: ServiceContainerLike\n}): Promise<string | null> {\n const { auth, pathname, container } = args\n if (!auth || typeof auth.sub !== 'string' || auth.sub.length === 0) return null\n if (auth.mfa_pending === true) return null\n if (isExemptPath(pathname)) return null\n if (readSecurityModuleConfig().mfa.emergencyBypass) return null\n\n const enforcementService = resolveEnforcementService(container)\n if (!enforcementService.service) {\n if (!canFailClosed(auth)) return null\n logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {\n err: enforcementService.error,\n })\n return createEnrollmentRedirect(pathname)\n }\n\n try {\n const compliance = await enforcementService.service.checkUserCompliance(auth.sub)\n if (!compliance.enforced || compliance.compliant) return null\n\n const deadlineState = resolveDeadlineRedirectState(compliance.deadline)\n if (!deadlineState.shouldRedirect) return null\n\n return createEnrollmentRedirect(pathname, deadlineState.overdue)\n } catch (error) {\n if (canFailClosed(auth)) {\n logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })\n return createEnrollmentRedirect(pathname)\n }\n return null\n }\n}\n\nexport default resolveMfaEnrollmentRedirect\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,oCAAoC;AAC7C,SAAS,gCAAgC;AAEzC,MAAM,sBAAsB;AAC5B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,2BAA2B,CAAC;AAqBvF,SAAS,6BAA6B,UAGpC;AACA,MAAI,EAAE,oBAAoB,SAAS,OAAO,MAAM,SAAS,QAAQ,CAAC,GAAG;AACnE,WAAO,EAAE,SAAS,OAAO,gBAAgB,KAAK;AAAA,EAChD;AAEA,QAAM,UAAU,6BAA6B,QAAQ;AACrD,SAAO,EAAE,SAAS,gBAAgB,QAAQ;AAC5C;AAEA,SAAS,aAAa,UAA2B;AAC/C,SAAO,SAAS,WAAW,2BAA2B;AACxD;AAEA,SAAS,yBAAyB,UAAkB,UAAU,OAAe;AAC3E,QAAM,eAAe,IAAI,gBAAgB;AAAA,IACvC,UAAU;AAAA,IACV,QAAQ;AAAA,EACV,CAAC;AACD,MAAI,SAAS;AACX,iBAAa,IAAI,WAAW,GAAG;AAAA,EACjC;AACA,SAAO,GAAG,mBAAmB,IAAI,aAAa,SAAS,CAAC;AAC1D;AAEA,SAAS,cAAc,MAAyC;AAC9D,SAAO,OAAO,KAAK,aAAa,YAAY,KAAK,SAAS,SAAS;AACrE;AAEA,SAAS,0BACP,WAC8B;AAC9B,MAAI;AACF,UAAM,WAAW,UAAU,QAAQ,uBAAuB;AAC1D,QACE,CAAC,YACE,OAAO,aAAa,YACpB,OAAQ,SAA+C,wBAAwB,YAClF;AACA,YAAM,gBAAgB,aAAa,OAC/B,SACA,MAAM,QAAQ,QAAQ,IACpB,UACA,OAAO,aAAa,WAClB,qBAAqB,OAAO,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC,EAAE,KAAK,IAAI,KAAK,QAAQ,KAClH,OAAO;AACf,aAAO;AAAA,QACL,OAAO,IAAI,UAAU,iDAAiD,aAAa,mCAAmC;AAAA,QACtH,SAAS;AAAA,MACX;AAAA,IACF;AACA,WAAO,EAAE,SAAS,SAAsC;AAAA,EAC1D,SAAS,OAAO;AACd,WAAO,EAAE,OAAO,SAAS,KAAK;AAAA,EAChC;AACF;AAEA,eAAsB,6BAA6B,MAIxB;AACzB,QAAM,EAAE,MAAM,UAAU,UAAU,IAAI;AACtC,MAAI,CAAC,QAAQ,OAAO,KAAK,QAAQ,YAAY,KAAK,IAAI,WAAW,EAAG,QAAO;AAC3E,MAAI,KAAK,gBAAgB,KAAM,QAAO;AACtC,MAAI,aAAa,QAAQ,EAAG,QAAO;AACnC,MAAI,yBAAyB,EAAE,IAAI,gBAAiB,QAAO;AAE3D,QAAM,qBAAqB,0BAA0B,SAAS;AAC9D,MAAI,CAAC,mBAAmB,SAAS;AAC/B,QAAI,CAAC,cAAc,IAAI,EAAG,QAAO;AACjC,WAAO,MAAM,wEAAwE;AAAA,MACnF,KAAK,mBAAmB;AAAA,IAC1B,CAAC;AACD,WAAO,yBAAyB,QAAQ;AAAA,EAC1C;AAEA,MAAI;AACF,UAAM,aAAa,MAAM,mBAAmB,QAAQ,oBAAoB,KAAK,GAAG;AAChF,QAAI,CAAC,WAAW,YAAY,WAAW,UAAW,QAAO;AAEzD,UAAM,gBAAgB,6BAA6B,WAAW,QAAQ;AACtE,QAAI,CAAC,cAAc,eAAgB,QAAO;AAE1C,WAAO,yBAAyB,UAAU,cAAc,OAAO;AAAA,EACjE,SAAS,OAAO;AACd,QAAI,cAAc,IAAI,GAAG;AACvB,aAAO,MAAM,0EAA0E,EAAE,KAAK,MAAM,CAAC;AACrG,aAAO,yBAAyB,QAAQ;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AACF;AAEA,IAAO,+BAAQ;",
4
+ "sourcesContent": ["import type { AuthContext } from '@open-mercato/shared/lib/auth/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { isEnforcementDeadlineOverdue } from '../services/MfaEnforcementService'\nimport { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from './security-config'\n\nconst MFA_ENROLLMENT_PATH = '/backend/profile/security/mfa'\nconst logger = createLogger('security').child({ component: 'mfa-enforcement-redirect' })\n\ntype MfaComplianceResult = {\n compliant: boolean\n enforced: boolean\n deadline?: Date\n}\n\ntype MfaEnforcementServiceLike = {\n checkUserCompliance: (userId: string) => Promise<MfaComplianceResult>\n}\n\ntype ServiceContainerLike = {\n resolve: (name: string) => unknown\n}\n\ntype EnforcementServiceResolution = {\n error?: unknown\n service: MfaEnforcementServiceLike | null\n}\n\nfunction resolveDeadlineRedirectState(deadline?: Date): {\n overdue: boolean\n shouldRedirect: boolean\n} {\n if (!(deadline instanceof Date) || Number.isNaN(deadline.getTime())) {\n return { overdue: false, shouldRedirect: true }\n }\n\n const overdue = isEnforcementDeadlineOverdue(deadline)\n return { overdue, shouldRedirect: overdue }\n}\n\nfunction isExemptPath(pathname: string): boolean {\n return pathname.startsWith('/backend/profile/security')\n}\n\nfunction createEnrollmentRedirect(pathname: string, overdue = false): string {\n const searchParams = new URLSearchParams({\n redirect: pathname,\n reason: 'mfa_enrollment_required',\n })\n if (overdue) {\n searchParams.set('overdue', '1')\n }\n return `${MFA_ENROLLMENT_PATH}?${searchParams.toString()}`\n}\n\nfunction canFailClosed(auth: NonNullable<AuthContext>): boolean {\n return typeof auth.tenantId === 'string' && auth.tenantId.length > 0\n}\n\nfunction resolveEnforcementService(\n container: ServiceContainerLike,\n): EnforcementServiceResolution {\n try {\n const resolved = container.resolve('mfaEnforcementService')\n if (\n !resolved\n || typeof resolved !== 'object'\n || typeof (resolved as { checkUserCompliance?: unknown }).checkUserCompliance !== 'function'\n ) {\n const receivedShape = resolved === null\n ? 'null'\n : Array.isArray(resolved)\n ? 'array'\n : typeof resolved === 'object'\n ? `object with keys: ${Object.keys(resolved).sort((left, right) => left.localeCompare(right)).join(', ') || '(none)'}`\n : typeof resolved\n return {\n error: new TypeError(`[internal] Malformed MFA enforcement service (${receivedShape}); expected checkUserCompliance()`),\n service: null,\n }\n }\n return { service: resolved as MfaEnforcementServiceLike }\n } catch (error) {\n return { error, service: null }\n }\n}\n\nexport async function resolveMfaEnrollmentRedirect(args: {\n auth: AuthContext\n pathname: string\n container: ServiceContainerLike\n}): Promise<string | null> {\n const { auth, pathname, container } = args\n if (!auth || typeof auth.sub !== 'string' || auth.sub.length === 0) return null\n if (auth.mfa_pending === true) return null\n if (isExemptPath(pathname)) return null\n\n const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass\n\n const enforcementService = resolveEnforcementService(container)\n if (!enforcementService.service) {\n if (!canFailClosed(auth)) return null\n if (emergencyBypass) {\n emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {\n userId: auth.sub,\n pathname,\n reason: 'enforcement-service-unavailable',\n })\n return null\n }\n logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {\n err: enforcementService.error,\n })\n return createEnrollmentRedirect(pathname)\n }\n\n try {\n const compliance = await enforcementService.service.checkUserCompliance(auth.sub)\n if (!compliance.enforced || compliance.compliant) return null\n\n const deadlineState = resolveDeadlineRedirectState(compliance.deadline)\n if (!deadlineState.shouldRedirect) return null\n\n if (emergencyBypass) {\n emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {\n userId: auth.sub,\n pathname,\n enforced: compliance.enforced,\n compliant: compliance.compliant,\n overdue: deadlineState.overdue,\n })\n return null\n }\n\n return createEnrollmentRedirect(pathname, deadlineState.overdue)\n } catch (error) {\n if (canFailClosed(auth)) {\n if (emergencyBypass) {\n emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {\n userId: auth.sub,\n pathname,\n reason: 'compliance-check-failed',\n })\n return null\n }\n logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })\n return createEnrollmentRedirect(pathname)\n }\n return null\n }\n}\n\nexport default resolveMfaEnrollmentRedirect\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,13 @@
1
1
  import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
2
+ import { createLogger } from "@open-mercato/shared/lib/logger";
2
3
  const TOTP_PERIOD_SECONDS = 30;
3
4
  const TOTP_DIGITS = 6;
4
5
  const DEFAULT_SETUP_TTL_MS = 10 * 60 * 1e3;
5
6
  const DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1e3;
6
7
  const DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1e3;
7
8
  const MIN_SUDO_TTL_SECONDS = 30;
9
+ const securityBypassLogger = createLogger("security").child({ component: "mfa-emergency-bypass" });
10
+ let startupBypassWarningEmitted = false;
8
11
  const defaultSecurityModuleConfig = {
9
12
  totp: {
10
13
  issuer: "Open Mercato",
@@ -110,6 +113,27 @@ function resolveSecurityModuleConfigForRequest(baseConfig, request, env = proces
110
113
  }
111
114
  };
112
115
  }
116
+ function emitMfaEmergencyBypassActiveWarning(context, extra) {
117
+ securityBypassLogger.warn("MFA emergency bypass is active", {
118
+ emergencyBypass: true,
119
+ context,
120
+ ...extra
121
+ });
122
+ }
123
+ function emitMfaEmergencyBypassStartupWarningIfNeeded(env = process.env) {
124
+ if (startupBypassWarningEmitted) return;
125
+ const bypass = parseBooleanWithDefault(
126
+ env.OM_SECURITY_MFA_EMERGENCY_BYPASS,
127
+ defaultSecurityModuleConfig.mfa.emergencyBypass
128
+ );
129
+ if (!bypass) return;
130
+ startupBypassWarningEmitted = true;
131
+ emitMfaEmergencyBypassActiveWarning("startup", { startup: true });
132
+ }
133
+ function resetMfaEmergencyBypassStartupWarningForTests() {
134
+ startupBypassWarningEmitted = false;
135
+ }
136
+ emitMfaEmergencyBypassStartupWarningIfNeeded();
113
137
  function readSecuritySetupTokenSecret(env = process.env) {
114
138
  const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET) ?? readText(env.AUTH_JWT_SECRET) ?? readText(env.AUTH_SECRET) ?? readText(env.JWT_SECRET);
115
139
  if (secret) {
@@ -210,8 +234,11 @@ function clamp(value, min, max) {
210
234
  }
211
235
  export {
212
236
  defaultSecurityModuleConfig,
237
+ emitMfaEmergencyBypassActiveWarning,
238
+ emitMfaEmergencyBypassStartupWarningIfNeeded,
213
239
  readSecurityModuleConfig,
214
240
  readSecuritySetupTokenSecret,
241
+ resetMfaEmergencyBypassStartupWarningForTests,
215
242
  resolveSecurityModuleConfigForRequest
216
243
  };
217
244
  //# sourceMappingURL=security-config.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/lib/security-config.ts"],
4
- "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\n\nconst TOTP_PERIOD_SECONDS = 30\nconst TOTP_DIGITS = 6\nconst DEFAULT_SETUP_TTL_MS = 10 * 60 * 1000\nconst DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000\nconst DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000\nconst MIN_SUDO_TTL_SECONDS = 30\n\nexport type SecurityModuleConfig = {\n totp: {\n issuer: string\n window: number\n digits: number\n periodSeconds: number\n setupTtlMs: number\n }\n otpEmail: {\n expirySeconds: number\n maxAttempts: number\n setupTtlMs: number\n challengeTtlMs: number\n subject: string\n }\n mfa: {\n challengeTtlMs: number\n maxAttempts: number\n emergencyBypass: boolean\n }\n sudo: {\n minTtlSeconds: number\n defaultTtlSeconds: number\n maxTtlSeconds: number\n pendingChallengeTtlMs: number\n }\n webauthn: {\n rpName: string\n rpId: string\n expectedOrigins: string[]\n setupTtlMs: number\n challengeTtlMs: number\n }\n recoveryCodes: {\n count: number\n bcryptCost: number\n }\n}\n\nexport const defaultSecurityModuleConfig: SecurityModuleConfig = {\n totp: {\n issuer: 'Open Mercato',\n window: 1,\n digits: TOTP_DIGITS,\n periodSeconds: TOTP_PERIOD_SECONDS,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n },\n otpEmail: {\n expirySeconds: 600,\n maxAttempts: 5,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: 600 * 1000,\n subject: 'Your Open Mercato verification code',\n },\n mfa: {\n challengeTtlMs: 600 * 1000,\n maxAttempts: 5,\n emergencyBypass: false,\n },\n sudo: {\n minTtlSeconds: MIN_SUDO_TTL_SECONDS,\n defaultTtlSeconds: 300,\n maxTtlSeconds: 1800,\n pendingChallengeTtlMs: DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS,\n },\n webauthn: {\n rpName: 'Open Mercato',\n rpId: 'localhost',\n expectedOrigins: ['http://localhost:3000'],\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS,\n },\n recoveryCodes: {\n count: 10,\n bcryptCost: 10,\n },\n}\n\nexport function readSecurityModuleConfig(\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const otpExpirySeconds = parsePositiveInteger(env.OM_SECURITY_OTP_EXPIRY_SECONDS)\n ?? defaultSecurityModuleConfig.otpEmail.expirySeconds\n const otpMaxAttempts = parsePositiveInteger(env.OM_SECURITY_OTP_MAX_ATTEMPTS)\n ?? defaultSecurityModuleConfig.otpEmail.maxAttempts\n const sudoMaxTtlSeconds = Math.max(\n parsePositiveInteger(env.OM_SECURITY_SUDO_MAX_TTL) ?? defaultSecurityModuleConfig.sudo.maxTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n )\n const sudoDefaultTtlSeconds = clamp(\n parsePositiveInteger(env.OM_SECURITY_SUDO_DEFAULT_TTL) ?? defaultSecurityModuleConfig.sudo.defaultTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n sudoMaxTtlSeconds,\n )\n const webauthnRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n ?? readHostname(env.APP_URL)\n ?? readHostname(env.NEXT_PUBLIC_APP_URL)\n ?? defaultSecurityModuleConfig.webauthn.rpId\n\n return {\n totp: {\n ...defaultSecurityModuleConfig.totp,\n issuer: readText(env.OM_SECURITY_TOTP_ISSUER) ?? defaultSecurityModuleConfig.totp.issuer,\n window: parseNonNegativeInteger(env.OM_SECURITY_TOTP_WINDOW) ?? defaultSecurityModuleConfig.totp.window,\n },\n otpEmail: {\n ...defaultSecurityModuleConfig.otpEmail,\n expirySeconds: otpExpirySeconds,\n maxAttempts: otpMaxAttempts,\n challengeTtlMs: otpExpirySeconds * 1000,\n },\n mfa: {\n challengeTtlMs: otpExpirySeconds * 1000,\n maxAttempts: otpMaxAttempts,\n emergencyBypass: parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n ),\n },\n sudo: {\n ...defaultSecurityModuleConfig.sudo,\n defaultTtlSeconds: sudoDefaultTtlSeconds,\n maxTtlSeconds: sudoMaxTtlSeconds,\n },\n webauthn: {\n ...defaultSecurityModuleConfig.webauthn,\n rpName: readText(env.OM_SECURITY_WEBAUTHN_RP_NAME) ?? defaultSecurityModuleConfig.webauthn.rpName,\n rpId: webauthnRpId,\n expectedOrigins: readWebAuthnOrigins(env, webauthnRpId),\n },\n recoveryCodes: {\n ...defaultSecurityModuleConfig.recoveryCodes,\n count: parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODE_COUNT)\n ?? parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODES_COUNT)\n ?? defaultSecurityModuleConfig.recoveryCodes.count,\n },\n }\n}\n\nexport function resolveSecurityModuleConfigForRequest(\n baseConfig: SecurityModuleConfig,\n request?: Request,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const explicitRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n const requestRpId = readRequestHostname(request)\n const rpId = explicitRpId ?? requestRpId ?? baseConfig.webauthn.rpId\n const expectedOrigins = readWebAuthnOrigins(env, rpId, request, baseConfig.webauthn.expectedOrigins)\n\n if (rpId === baseConfig.webauthn.rpId && areOriginsEqual(expectedOrigins, baseConfig.webauthn.expectedOrigins)) {\n return baseConfig\n }\n\n return {\n ...baseConfig,\n webauthn: {\n ...baseConfig.webauthn,\n rpId,\n expectedOrigins,\n },\n }\n}\n\nexport function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {\n const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)\n ?? readText(env.AUTH_JWT_SECRET)\n ?? readText(env.AUTH_SECRET)\n ?? readText(env.JWT_SECRET)\n\n if (secret) {\n return secret\n }\n\n throw new Error(\n 'Security MFA setup tokens require OM_SECURITY_MFA_SETUP_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET.',\n )\n}\n\nfunction readWebAuthnOrigins(\n env: NodeJS.ProcessEnv,\n rpId: string,\n request?: Request,\n fallbackOrigins?: string[],\n): string[] {\n const fromList = (env.OM_SECURITY_WEBAUTHN_ORIGINS ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value.length > 0)\n const explicitOrigin = readText(env.OM_SECURITY_WEBAUTHN_ORIGIN)\n\n if (fromList.length > 0 || explicitOrigin) {\n return [...new Set([...fromList, ...(explicitOrigin ? [explicitOrigin] : [])])]\n }\n\n const requestOrigin = readRequestOrigin(request)\n const singletons = [\n requestOrigin,\n env.APP_URL,\n env.NEXT_PUBLIC_APP_URL,\n ]\n .map((value) => readText(value))\n .filter((value): value is string => typeof value === 'string')\n\n const origins = [...new Set([...fromList, ...singletons])]\n if (origins.length > 0) {\n return origins\n }\n\n if (fallbackOrigins && fallbackOrigins.length > 0) {\n return [...fallbackOrigins]\n }\n\n if (rpId === 'localhost') {\n return [...defaultSecurityModuleConfig.webauthn.expectedOrigins]\n }\n\n return [`https://${rpId}`]\n}\n\nfunction readText(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nfunction readHostname(value: string | undefined): string | undefined {\n const trimmed = readText(value)\n if (!trimmed) return undefined\n\n try {\n const url = new URL(trimmed)\n return readText(url.hostname)\n } catch {\n return undefined\n }\n}\n\nfunction readRequestHostname(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.hostname) : undefined\n}\n\nfunction readRequestOrigin(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.origin) : undefined\n}\n\nfunction readRequestUrl(request: Request | undefined): URL | null {\n if (!request) return null\n\n const forwardedHost = readForwardedHeader(request.headers.get('x-forwarded-host'))\n const forwardedProto = readForwardedHeader(request.headers.get('x-forwarded-proto'))\n if (forwardedHost) {\n const protocol = forwardedProto ?? 'https'\n try {\n return new URL(`${protocol}://${forwardedHost}`)\n } catch {\n return null\n }\n }\n\n try {\n return new URL(request.url)\n } catch {\n return null\n }\n}\n\nfunction readForwardedHeader(value: string | null): string | undefined {\n const trimmed = readText(value ?? undefined)\n if (!trimmed) return undefined\n const [first] = trimmed.split(',')\n return readText(first)\n}\n\nfunction areOriginsEqual(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false\n return left.every((value, index) => value === right[index])\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction parseNonNegativeInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed < 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst TOTP_PERIOD_SECONDS = 30\nconst TOTP_DIGITS = 6\nconst DEFAULT_SETUP_TTL_MS = 10 * 60 * 1000\nconst DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000\nconst DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000\nconst MIN_SUDO_TTL_SECONDS = 30\n\nconst securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })\nlet startupBypassWarningEmitted = false\n\nexport type SecurityModuleConfig = {\n totp: {\n issuer: string\n window: number\n digits: number\n periodSeconds: number\n setupTtlMs: number\n }\n otpEmail: {\n expirySeconds: number\n maxAttempts: number\n setupTtlMs: number\n challengeTtlMs: number\n subject: string\n }\n mfa: {\n challengeTtlMs: number\n maxAttempts: number\n emergencyBypass: boolean\n }\n sudo: {\n minTtlSeconds: number\n defaultTtlSeconds: number\n maxTtlSeconds: number\n pendingChallengeTtlMs: number\n }\n webauthn: {\n rpName: string\n rpId: string\n expectedOrigins: string[]\n setupTtlMs: number\n challengeTtlMs: number\n }\n recoveryCodes: {\n count: number\n bcryptCost: number\n }\n}\n\nexport const defaultSecurityModuleConfig: SecurityModuleConfig = {\n totp: {\n issuer: 'Open Mercato',\n window: 1,\n digits: TOTP_DIGITS,\n periodSeconds: TOTP_PERIOD_SECONDS,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n },\n otpEmail: {\n expirySeconds: 600,\n maxAttempts: 5,\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: 600 * 1000,\n subject: 'Your Open Mercato verification code',\n },\n mfa: {\n challengeTtlMs: 600 * 1000,\n maxAttempts: 5,\n emergencyBypass: false,\n },\n sudo: {\n minTtlSeconds: MIN_SUDO_TTL_SECONDS,\n defaultTtlSeconds: 300,\n maxTtlSeconds: 1800,\n pendingChallengeTtlMs: DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS,\n },\n webauthn: {\n rpName: 'Open Mercato',\n rpId: 'localhost',\n expectedOrigins: ['http://localhost:3000'],\n setupTtlMs: DEFAULT_SETUP_TTL_MS,\n challengeTtlMs: DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS,\n },\n recoveryCodes: {\n count: 10,\n bcryptCost: 10,\n },\n}\n\nexport function readSecurityModuleConfig(\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const otpExpirySeconds = parsePositiveInteger(env.OM_SECURITY_OTP_EXPIRY_SECONDS)\n ?? defaultSecurityModuleConfig.otpEmail.expirySeconds\n const otpMaxAttempts = parsePositiveInteger(env.OM_SECURITY_OTP_MAX_ATTEMPTS)\n ?? defaultSecurityModuleConfig.otpEmail.maxAttempts\n const sudoMaxTtlSeconds = Math.max(\n parsePositiveInteger(env.OM_SECURITY_SUDO_MAX_TTL) ?? defaultSecurityModuleConfig.sudo.maxTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n )\n const sudoDefaultTtlSeconds = clamp(\n parsePositiveInteger(env.OM_SECURITY_SUDO_DEFAULT_TTL) ?? defaultSecurityModuleConfig.sudo.defaultTtlSeconds,\n defaultSecurityModuleConfig.sudo.minTtlSeconds,\n sudoMaxTtlSeconds,\n )\n const webauthnRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n ?? readHostname(env.APP_URL)\n ?? readHostname(env.NEXT_PUBLIC_APP_URL)\n ?? defaultSecurityModuleConfig.webauthn.rpId\n\n return {\n totp: {\n ...defaultSecurityModuleConfig.totp,\n issuer: readText(env.OM_SECURITY_TOTP_ISSUER) ?? defaultSecurityModuleConfig.totp.issuer,\n window: parseNonNegativeInteger(env.OM_SECURITY_TOTP_WINDOW) ?? defaultSecurityModuleConfig.totp.window,\n },\n otpEmail: {\n ...defaultSecurityModuleConfig.otpEmail,\n expirySeconds: otpExpirySeconds,\n maxAttempts: otpMaxAttempts,\n challengeTtlMs: otpExpirySeconds * 1000,\n },\n mfa: {\n challengeTtlMs: otpExpirySeconds * 1000,\n maxAttempts: otpMaxAttempts,\n emergencyBypass: parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n ),\n },\n sudo: {\n ...defaultSecurityModuleConfig.sudo,\n defaultTtlSeconds: sudoDefaultTtlSeconds,\n maxTtlSeconds: sudoMaxTtlSeconds,\n },\n webauthn: {\n ...defaultSecurityModuleConfig.webauthn,\n rpName: readText(env.OM_SECURITY_WEBAUTHN_RP_NAME) ?? defaultSecurityModuleConfig.webauthn.rpName,\n rpId: webauthnRpId,\n expectedOrigins: readWebAuthnOrigins(env, webauthnRpId),\n },\n recoveryCodes: {\n ...defaultSecurityModuleConfig.recoveryCodes,\n count: parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODE_COUNT)\n ?? parsePositiveInteger(env.OM_SECURITY_RECOVERY_CODES_COUNT)\n ?? defaultSecurityModuleConfig.recoveryCodes.count,\n },\n }\n}\n\nexport function resolveSecurityModuleConfigForRequest(\n baseConfig: SecurityModuleConfig,\n request?: Request,\n env: NodeJS.ProcessEnv = process.env,\n): SecurityModuleConfig {\n const explicitRpId = readText(env.OM_SECURITY_WEBAUTHN_RP_ID)\n const requestRpId = readRequestHostname(request)\n const rpId = explicitRpId ?? requestRpId ?? baseConfig.webauthn.rpId\n const expectedOrigins = readWebAuthnOrigins(env, rpId, request, baseConfig.webauthn.expectedOrigins)\n\n if (rpId === baseConfig.webauthn.rpId && areOriginsEqual(expectedOrigins, baseConfig.webauthn.expectedOrigins)) {\n return baseConfig\n }\n\n return {\n ...baseConfig,\n webauthn: {\n ...baseConfig.webauthn,\n rpId,\n expectedOrigins,\n },\n }\n}\n\nexport function emitMfaEmergencyBypassActiveWarning(context: string, extra?: Record<string, unknown>): void {\n securityBypassLogger.warn('MFA emergency bypass is active', {\n emergencyBypass: true,\n context,\n ...extra,\n })\n}\n\nexport function emitMfaEmergencyBypassStartupWarningIfNeeded(env: NodeJS.ProcessEnv = process.env): void {\n if (startupBypassWarningEmitted) return\n const bypass = parseBooleanWithDefault(\n env.OM_SECURITY_MFA_EMERGENCY_BYPASS,\n defaultSecurityModuleConfig.mfa.emergencyBypass,\n )\n if (!bypass) return\n startupBypassWarningEmitted = true\n emitMfaEmergencyBypassActiveWarning('startup', { startup: true })\n}\n\nexport function resetMfaEmergencyBypassStartupWarningForTests(): void {\n startupBypassWarningEmitted = false\n}\n\nemitMfaEmergencyBypassStartupWarningIfNeeded()\n\nexport function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {\n const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)\n ?? readText(env.AUTH_JWT_SECRET)\n ?? readText(env.AUTH_SECRET)\n ?? readText(env.JWT_SECRET)\n\n if (secret) {\n return secret\n }\n\n throw new Error(\n 'Security MFA setup tokens require OM_SECURITY_MFA_SETUP_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET.',\n )\n}\n\nfunction readWebAuthnOrigins(\n env: NodeJS.ProcessEnv,\n rpId: string,\n request?: Request,\n fallbackOrigins?: string[],\n): string[] {\n const fromList = (env.OM_SECURITY_WEBAUTHN_ORIGINS ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value.length > 0)\n const explicitOrigin = readText(env.OM_SECURITY_WEBAUTHN_ORIGIN)\n\n if (fromList.length > 0 || explicitOrigin) {\n return [...new Set([...fromList, ...(explicitOrigin ? [explicitOrigin] : [])])]\n }\n\n const requestOrigin = readRequestOrigin(request)\n const singletons = [\n requestOrigin,\n env.APP_URL,\n env.NEXT_PUBLIC_APP_URL,\n ]\n .map((value) => readText(value))\n .filter((value): value is string => typeof value === 'string')\n\n const origins = [...new Set([...fromList, ...singletons])]\n if (origins.length > 0) {\n return origins\n }\n\n if (fallbackOrigins && fallbackOrigins.length > 0) {\n return [...fallbackOrigins]\n }\n\n if (rpId === 'localhost') {\n return [...defaultSecurityModuleConfig.webauthn.expectedOrigins]\n }\n\n return [`https://${rpId}`]\n}\n\nfunction readText(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\n}\n\nfunction readHostname(value: string | undefined): string | undefined {\n const trimmed = readText(value)\n if (!trimmed) return undefined\n\n try {\n const url = new URL(trimmed)\n return readText(url.hostname)\n } catch {\n return undefined\n }\n}\n\nfunction readRequestHostname(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.hostname) : undefined\n}\n\nfunction readRequestOrigin(request: Request | undefined): string | undefined {\n const url = readRequestUrl(request)\n return url ? readText(url.origin) : undefined\n}\n\nfunction readRequestUrl(request: Request | undefined): URL | null {\n if (!request) return null\n\n const forwardedHost = readForwardedHeader(request.headers.get('x-forwarded-host'))\n const forwardedProto = readForwardedHeader(request.headers.get('x-forwarded-proto'))\n if (forwardedHost) {\n const protocol = forwardedProto ?? 'https'\n try {\n return new URL(`${protocol}://${forwardedHost}`)\n } catch {\n return null\n }\n }\n\n try {\n return new URL(request.url)\n } catch {\n return null\n }\n}\n\nfunction readForwardedHeader(value: string | null): string | undefined {\n const trimmed = readText(value ?? undefined)\n if (!trimmed) return undefined\n const [first] = trimmed.split(',')\n return readText(first)\n}\n\nfunction areOriginsEqual(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false\n return left.every((value, index) => value === right[index])\n}\n\nfunction parsePositiveInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed <= 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction parseNonNegativeInteger(value: string | undefined): number | undefined {\n if (!value) return undefined\n const parsed = Number(value)\n if (!Number.isFinite(parsed) || parsed < 0) return undefined\n return Math.trunc(parsed)\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,7 @@ import {
14
14
  } from "../data/entities.js";
15
15
  import { emitSecurityEvent } from "../events.js";
16
16
  import { sudoTargets as defaultSudoTargets } from "../security.sudo.js";
17
- import { readSecurityModuleConfig } from "../lib/security-config.js";
17
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from "../lib/security-config.js";
18
18
  const signedSudoTokenPayloadSchema = z.object({
19
19
  sid: z.string(),
20
20
  sub: z.string(),
@@ -92,7 +92,7 @@ class SudoChallengeService {
92
92
  return { required: false };
93
93
  }
94
94
  const userMethods = await this.mfaService.getUserMethods(userId);
95
- const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length);
95
+ const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length, userId, targetIdentifier);
96
96
  let sessionToken = randomBytes(16).toString("hex");
97
97
  let availableMfaMethods;
98
98
  if (method === "mfa") {
@@ -460,10 +460,27 @@ class SudoChallengeService {
460
460
  throw new SudoChallengeServiceError("Sudo configuration not found", 404);
461
461
  }
462
462
  }
463
- resolveChallengeMethod(configuredMethod, availableMfaMethodCount) {
463
+ resolveChallengeMethod(configuredMethod, availableMfaMethodCount, userId, targetIdentifier) {
464
464
  if (this.securityConfig.mfa.emergencyBypass) {
465
+ let wouldHaveBeenMfa = false;
466
+ try {
467
+ wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === "mfa";
468
+ } catch {
469
+ wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA;
470
+ }
471
+ if (wouldHaveBeenMfa) {
472
+ emitMfaEmergencyBypassActiveWarning("sudo challenge downgraded to password", {
473
+ configuredMethod,
474
+ availableMfaMethodCount,
475
+ userId,
476
+ targetIdentifier
477
+ });
478
+ }
465
479
  return "password";
466
480
  }
481
+ return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount);
482
+ }
483
+ resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) {
467
484
  if (configuredMethod === ChallengeMethod.PASSWORD) return "password";
468
485
  if (configuredMethod === ChallengeMethod.MFA) {
469
486
  if (availableMfaMethodCount === 0) {
@@ -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 { 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\ntype BoundPendingSudoSession = SudoSession & {\n targetIdentifier: string\n sudoConfigId: string\n sudoConfigUpdatedAt: Date\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\nfunction readTrimmedEnv(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\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 return this.resolveProtection(this.em, targetIdentifier, tenantId ?? null, organizationId ?? null)\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 user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\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 targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\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 scopeTenantId,\n scopeOrganizationId,\n targetIdentifier,\n sudoConfigId: protection.config.id,\n sudoConfigUpdatedAt: protection.config.updatedAt,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n verifiedAt: null,\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 options: { expectedUserId?: string } = {},\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\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 if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(\n session.sessionToken,\n methodType,\n { request },\n { userId: session.userId },\n )\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 if (user.tenantId !== session.tenantId) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const requestedTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const requestedOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n if (\n options.targetIdentifier !== session.targetIdentifier\n || requestedTenantId !== session.scopeTenantId\n || requestedOrganizationId !== session.scopeOrganizationId\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const scopeTenantId = session.scopeTenantId ?? null\n const scopeOrganizationId = session.scopeOrganizationId ?? null\n const protection = await this.isProtected(\n session.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 if (\n protection.config.id !== session.sudoConfigId\n || protection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt?.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\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(\n session.sessionToken,\n methodType,\n payload,\n { request },\n { userId: session.userId },\n )\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: session.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const pendingToken = session.sessionToken\n const { sudoToken, expiresAt, verifiedAt } = await this.em.transactional(async (transactionalEm) => {\n await transactionalEm.execute('lock table \"sudo_challenge_configs\" in share mode')\n const currentProtection = await this.resolveProtection(\n transactionalEm,\n session.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (\n !currentProtection.protected\n || !currentProtection.config\n || currentProtection.config.id !== session.sudoConfigId\n || currentProtection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const ttlSeconds = this.normalizeTtl(currentProtection.config.ttlSeconds)\n const currentVerifiedAt = new Date()\n const currentExpiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const currentSudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: session.targetIdentifier,\n exp: currentExpiresAt.getTime(),\n })\n const consumedRows = await transactionalEm.nativeUpdate(\n SudoSession,\n {\n id: session.id,\n sessionToken: pendingToken,\n verifiedAt: null,\n expiresAt: { $gt: currentVerifiedAt },\n },\n {\n sessionToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n },\n )\n if (consumedRows !== 1) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n return {\n sudoToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n }\n })\n session.sessionToken = sudoToken\n session.expiresAt = expiresAt\n session.verifiedAt = verifiedAt\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: session.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 targetIdentifier,\n scopeTenantId: payload.tid,\n scopeOrganizationId: payload.oid,\n verifiedAt: { $ne: null },\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 const ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n const challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n const changed = !existing.isEnabled\n || Boolean(existing.deletedAt)\n || existing.ttlSeconds !== ttlSeconds\n || existing.challengeMethod !== challengeMethod\n if (changed) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = ttlSeconds\n existing.challengeMethod = challengeMethod\n existing.updatedAt = new Date()\n await this.em.flush()\n }\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 async resolveProtection(\n em: EntityManager,\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ): Promise<SudoProtectionResolution> {\n const candidates = await em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId, organizationId))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId, organizationId))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\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<BoundPendingSudoSession> {\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.targetIdentifier || !session.sudoConfigId || !session.sudoConfigUpdatedAt) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.verifiedAt) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session as BoundPendingSudoSession\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 const secret =\n readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_SECRET)\n ?? readTrimmedEnv(process.env.JWT_SECRET)\n if (secret) return secret\n throw new Error(\n 'Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set.',\n )\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 { emitMfaEmergencyBypassActiveWarning, 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\ntype BoundPendingSudoSession = SudoSession & {\n targetIdentifier: string\n sudoConfigId: string\n sudoConfigUpdatedAt: Date\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\nfunction readTrimmedEnv(value: string | undefined): string | undefined {\n const trimmed = value?.trim()\n return trimmed ? trimmed : undefined\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 return this.resolveProtection(this.em, targetIdentifier, tenantId ?? null, organizationId ?? null)\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 user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\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 targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length, userId, targetIdentifier)\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 scopeTenantId,\n scopeOrganizationId,\n targetIdentifier,\n sudoConfigId: protection.config.id,\n sudoConfigUpdatedAt: protection.config.updatedAt,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n verifiedAt: null,\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 options: { expectedUserId?: string } = {},\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\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 if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(\n session.sessionToken,\n methodType,\n { request },\n { userId: session.userId },\n )\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 if (user.tenantId !== session.tenantId) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const requestedTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const requestedOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n if (\n options.targetIdentifier !== session.targetIdentifier\n || requestedTenantId !== session.scopeTenantId\n || requestedOrganizationId !== session.scopeOrganizationId\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const scopeTenantId = session.scopeTenantId ?? null\n const scopeOrganizationId = session.scopeOrganizationId ?? null\n const protection = await this.isProtected(\n session.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 if (\n protection.config.id !== session.sudoConfigId\n || protection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt?.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\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(\n session.sessionToken,\n methodType,\n payload,\n { request },\n { userId: session.userId },\n )\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: session.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const pendingToken = session.sessionToken\n const { sudoToken, expiresAt, verifiedAt } = await this.em.transactional(async (transactionalEm) => {\n await transactionalEm.execute('lock table \"sudo_challenge_configs\" in share mode')\n const currentProtection = await this.resolveProtection(\n transactionalEm,\n session.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (\n !currentProtection.protected\n || !currentProtection.config\n || currentProtection.config.id !== session.sudoConfigId\n || currentProtection.config.updatedAt.getTime() !== session.sudoConfigUpdatedAt.getTime()\n ) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 403)\n }\n\n const ttlSeconds = this.normalizeTtl(currentProtection.config.ttlSeconds)\n const currentVerifiedAt = new Date()\n const currentExpiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const currentSudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: session.targetIdentifier,\n exp: currentExpiresAt.getTime(),\n })\n const consumedRows = await transactionalEm.nativeUpdate(\n SudoSession,\n {\n id: session.id,\n sessionToken: pendingToken,\n verifiedAt: null,\n expiresAt: { $gt: currentVerifiedAt },\n },\n {\n sessionToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n },\n )\n if (consumedRows !== 1) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n return {\n sudoToken: currentSudoToken,\n expiresAt: currentExpiresAt,\n verifiedAt: currentVerifiedAt,\n }\n })\n session.sessionToken = sudoToken\n session.expiresAt = expiresAt\n session.verifiedAt = verifiedAt\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: session.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 targetIdentifier,\n scopeTenantId: payload.tid,\n scopeOrganizationId: payload.oid,\n verifiedAt: { $ne: null },\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 const ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n const challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n const changed = !existing.isEnabled\n || Boolean(existing.deletedAt)\n || existing.ttlSeconds !== ttlSeconds\n || existing.challengeMethod !== challengeMethod\n if (changed) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = ttlSeconds\n existing.challengeMethod = challengeMethod\n existing.updatedAt = new Date()\n await this.em.flush()\n }\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 userId?: string,\n targetIdentifier?: string,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n let wouldHaveBeenMfa = false\n try {\n wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === 'mfa'\n } catch {\n wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA\n }\n if (wouldHaveBeenMfa) {\n emitMfaEmergencyBypassActiveWarning('sudo challenge downgraded to password', {\n configuredMethod,\n availableMfaMethodCount,\n userId,\n targetIdentifier,\n })\n }\n return 'password'\n }\n return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount)\n }\n\n private resolveChallengeMethodWithoutBypass(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\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 async resolveProtection(\n em: EntityManager,\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ): Promise<SudoProtectionResolution> {\n const candidates = await em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId, organizationId))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId, organizationId))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\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<BoundPendingSudoSession> {\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.targetIdentifier || !session.sudoConfigId || !session.sudoConfigUpdatedAt) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.verifiedAt) {\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 400)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session as BoundPendingSudoSession\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 const secret =\n readTrimmedEnv(process.env.OM_SECURITY_SUDO_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_JWT_SECRET)\n ?? readTrimmedEnv(process.env.AUTH_SECRET)\n ?? readTrimmedEnv(process.env.JWT_SECRET)\n if (secret) return secret\n throw new Error(\n 'Sudo step-up tokens require OM_SECURITY_SUDO_SECRET, AUTH_JWT_SECRET, AUTH_SECRET, or JWT_SECRET to be set.',\n )\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.8-develop.7051.1.2e20648fd5",
3
+ "version": "0.6.8-develop.7056.1.1a563a270d",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "dependencies": {
68
- "@open-mercato/core": "0.6.8-develop.7051.1.2e20648fd5",
69
- "@open-mercato/ui": "0.6.8-develop.7051.1.2e20648fd5",
68
+ "@open-mercato/core": "0.6.8-develop.7056.1.1a563a270d",
69
+ "@open-mercato/ui": "0.6.8-develop.7056.1.1a563a270d",
70
70
  "@simplewebauthn/browser": "^13.3.0",
71
71
  "@simplewebauthn/server": "^13.3.2",
72
72
  "@simplewebauthn/types": "^12.0.0",
@@ -76,12 +76,12 @@
76
76
  "qrcode": "^1.5.4"
77
77
  },
78
78
  "peerDependencies": {
79
- "@open-mercato/shared": "0.6.8-develop.7051.1.2e20648fd5",
79
+ "@open-mercato/shared": "0.6.8-develop.7056.1.1a563a270d",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@open-mercato/shared": "0.6.8-develop.7051.1.2e20648fd5",
84
+ "@open-mercato/shared": "0.6.8-develop.7056.1.1a563a270d",
85
85
  "@types/jest": "^30.0.0",
86
86
  "@types/react": "^19.2.17",
87
87
  "@types/react-dom": "^19.2.3",
@@ -1,6 +1,6 @@
1
1
  import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
2
2
  import { signJwt, verifyJwt } from '@open-mercato/shared/lib/auth/jwt'
3
- import { readSecurityModuleConfig } from '../lib/security-config'
3
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from '../lib/security-config'
4
4
 
5
5
  type JwtClaims = {
6
6
  sub: string
@@ -59,7 +59,6 @@ export const interceptors: ApiInterceptor[] = [
59
59
  if (response.statusCode !== 200) return {}
60
60
  if (response.body.ok !== true || response.body.mfa_required === true) return {}
61
61
  if (typeof response.body.token !== 'string' || response.body.token.length === 0) return {}
62
- if (readSecurityModuleConfig().mfa.emergencyBypass) return {}
63
62
 
64
63
  const claims = readClaims(response.body.token)
65
64
  if (!claims) return {}
@@ -67,6 +66,11 @@ export const interceptors: ApiInterceptor[] = [
67
66
  const mfaVerificationService = resolveMfaVerificationService(context.container as { resolve: (name: string) => unknown })
68
67
  if (!mfaVerificationService) return {}
69
68
 
69
+ if (readSecurityModuleConfig().mfa.emergencyBypass) {
70
+ emitMfaEmergencyBypassActiveWarning('login MFA challenge bypassed', { userId: claims.sub })
71
+ return {}
72
+ }
73
+
70
74
  try {
71
75
  const challenge = await mfaVerificationService.createChallenge(claims.sub)
72
76
  const pendingToken = signJwt(
@@ -1,7 +1,7 @@
1
1
  import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
2
2
  import { createLogger } from '@open-mercato/shared/lib/logger'
3
3
  import { isEnforcementDeadlineOverdue } from '../services/MfaEnforcementService'
4
- import { readSecurityModuleConfig } from './security-config'
4
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from './security-config'
5
5
 
6
6
  const MFA_ENROLLMENT_PATH = '/backend/profile/security/mfa'
7
7
  const logger = createLogger('security').child({ component: 'mfa-enforcement-redirect' })
@@ -93,11 +93,20 @@ export async function resolveMfaEnrollmentRedirect(args: {
93
93
  if (!auth || typeof auth.sub !== 'string' || auth.sub.length === 0) return null
94
94
  if (auth.mfa_pending === true) return null
95
95
  if (isExemptPath(pathname)) return null
96
- if (readSecurityModuleConfig().mfa.emergencyBypass) return null
96
+
97
+ const emergencyBypass = readSecurityModuleConfig().mfa.emergencyBypass
97
98
 
98
99
  const enforcementService = resolveEnforcementService(container)
99
100
  if (!enforcementService.service) {
100
101
  if (!canFailClosed(auth)) return null
102
+ if (emergencyBypass) {
103
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
104
+ userId: auth.sub,
105
+ pathname,
106
+ reason: 'enforcement-service-unavailable',
107
+ })
108
+ return null
109
+ }
101
110
  logger.error('Unable to resolve MFA enforcement service; redirecting to enrollment', {
102
111
  err: enforcementService.error,
103
112
  })
@@ -111,9 +120,28 @@ export async function resolveMfaEnrollmentRedirect(args: {
111
120
  const deadlineState = resolveDeadlineRedirectState(compliance.deadline)
112
121
  if (!deadlineState.shouldRedirect) return null
113
122
 
123
+ if (emergencyBypass) {
124
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
125
+ userId: auth.sub,
126
+ pathname,
127
+ enforced: compliance.enforced,
128
+ compliant: compliance.compliant,
129
+ overdue: deadlineState.overdue,
130
+ })
131
+ return null
132
+ }
133
+
114
134
  return createEnrollmentRedirect(pathname, deadlineState.overdue)
115
135
  } catch (error) {
116
136
  if (canFailClosed(auth)) {
137
+ if (emergencyBypass) {
138
+ emitMfaEmergencyBypassActiveWarning('mfa-enrollment-redirect bypassed', {
139
+ userId: auth.sub,
140
+ pathname,
141
+ reason: 'compliance-check-failed',
142
+ })
143
+ return null
144
+ }
117
145
  logger.error('Unable to verify MFA enforcement compliance; redirecting to enrollment', { err: error })
118
146
  return createEnrollmentRedirect(pathname)
119
147
  }
@@ -1,4 +1,5 @@
1
1
  import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
2
3
 
3
4
  const TOTP_PERIOD_SECONDS = 30
4
5
  const TOTP_DIGITS = 6
@@ -7,6 +8,9 @@ const DEFAULT_WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000
7
8
  const DEFAULT_SUDO_PENDING_CHALLENGE_TTL_MS = 10 * 60 * 1000
8
9
  const MIN_SUDO_TTL_SECONDS = 30
9
10
 
11
+ const securityBypassLogger = createLogger('security').child({ component: 'mfa-emergency-bypass' })
12
+ let startupBypassWarningEmitted = false
13
+
10
14
  export type SecurityModuleConfig = {
11
15
  totp: {
12
16
  issuer: string
@@ -170,6 +174,31 @@ export function resolveSecurityModuleConfigForRequest(
170
174
  }
171
175
  }
172
176
 
177
+ export function emitMfaEmergencyBypassActiveWarning(context: string, extra?: Record<string, unknown>): void {
178
+ securityBypassLogger.warn('MFA emergency bypass is active', {
179
+ emergencyBypass: true,
180
+ context,
181
+ ...extra,
182
+ })
183
+ }
184
+
185
+ export function emitMfaEmergencyBypassStartupWarningIfNeeded(env: NodeJS.ProcessEnv = process.env): void {
186
+ if (startupBypassWarningEmitted) return
187
+ const bypass = parseBooleanWithDefault(
188
+ env.OM_SECURITY_MFA_EMERGENCY_BYPASS,
189
+ defaultSecurityModuleConfig.mfa.emergencyBypass,
190
+ )
191
+ if (!bypass) return
192
+ startupBypassWarningEmitted = true
193
+ emitMfaEmergencyBypassActiveWarning('startup', { startup: true })
194
+ }
195
+
196
+ export function resetMfaEmergencyBypassStartupWarningForTests(): void {
197
+ startupBypassWarningEmitted = false
198
+ }
199
+
200
+ emitMfaEmergencyBypassStartupWarningIfNeeded()
201
+
173
202
  export function readSecuritySetupTokenSecret(env: NodeJS.ProcessEnv = process.env): string {
174
203
  const secret = readText(env.OM_SECURITY_MFA_SETUP_SECRET)
175
204
  ?? readText(env.AUTH_JWT_SECRET)
@@ -24,7 +24,7 @@ import type { MfaService } from './MfaService'
24
24
  import type { MfaVerificationService } from './MfaVerificationService'
25
25
  import { sudoTargets as defaultSudoTargets } from '../security.sudo'
26
26
  import type { SecurityModuleConfig } from '../lib/security-config'
27
- import { readSecurityModuleConfig } from '../lib/security-config'
27
+ import { emitMfaEmergencyBypassActiveWarning, readSecurityModuleConfig } from '../lib/security-config'
28
28
 
29
29
  type SudoMethod = 'password' | 'mfa'
30
30
 
@@ -168,7 +168,7 @@ export class SudoChallengeService {
168
168
  }
169
169
 
170
170
  const userMethods = await this.mfaService.getUserMethods(userId)
171
- const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)
171
+ const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length, userId, targetIdentifier)
172
172
 
173
173
  let sessionToken = randomBytes(16).toString('hex')
174
174
  let availableMfaMethods: SudoAvailableMethod[] | undefined
@@ -643,10 +643,33 @@ export class SudoChallengeService {
643
643
  private resolveChallengeMethod(
644
644
  configuredMethod: ChallengeMethod,
645
645
  availableMfaMethodCount: number,
646
+ userId?: string,
647
+ targetIdentifier?: string,
646
648
  ): SudoMethod {
647
649
  if (this.securityConfig.mfa.emergencyBypass) {
650
+ let wouldHaveBeenMfa = false
651
+ try {
652
+ wouldHaveBeenMfa = this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount) === 'mfa'
653
+ } catch {
654
+ wouldHaveBeenMfa = configuredMethod === ChallengeMethod.MFA
655
+ }
656
+ if (wouldHaveBeenMfa) {
657
+ emitMfaEmergencyBypassActiveWarning('sudo challenge downgraded to password', {
658
+ configuredMethod,
659
+ availableMfaMethodCount,
660
+ userId,
661
+ targetIdentifier,
662
+ })
663
+ }
648
664
  return 'password'
649
665
  }
666
+ return this.resolveChallengeMethodWithoutBypass(configuredMethod, availableMfaMethodCount)
667
+ }
668
+
669
+ private resolveChallengeMethodWithoutBypass(
670
+ configuredMethod: ChallengeMethod,
671
+ availableMfaMethodCount: number,
672
+ ): SudoMethod {
650
673
  if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'
651
674
  if (configuredMethod === ChallengeMethod.MFA) {
652
675
  if (availableMfaMethodCount === 0) {