@open-mercato/enterprise 0.6.5-develop.4964.1.ae0edca575 → 0.6.5-develop.5033.1.c970204a3f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ const responseSchema = z.object({
12
12
  clientData: z.record(z.string(), z.unknown()).optional()
13
13
  });
14
14
  const metadata = {
15
- POST: { requireAuth: true }
15
+ POST: { requireAuth: true, rateLimit: { points: 20, duration: 60, keyPrefix: "security_mfa_prepare" } }
16
16
  };
17
17
  async function POST(req) {
18
18
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/prepare/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { mapMfaError, readJsonRecord, readString, resolveMfaRequestContext } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n clientData: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const prepared = await context.mfaVerificationService.prepareChallenge(challengeId, methodType, { request: req })\n return NextResponse.json({ ok: true, ...(prepared.clientData ? { clientData: prepared.clientData } : {}) })\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge prepare routes',\n methods: {\n POST: {\n summary: 'Prepare MFA challenge payload for selected method',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge prepared', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,aAAa,gBAAgB,YAAY,gCAAgC;AAElF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACzD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,iBAAiB,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAChH,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,GAAI,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAG,CAAC;AAAA,EAC5G,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { mapMfaError, readJsonRecord, readString, resolveMfaRequestContext } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n clientData: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 20, duration: 60, keyPrefix: 'security_mfa_prepare' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const prepared = await context.mfaVerificationService.prepareChallenge(challengeId, methodType, { request: req })\n return NextResponse.json({ ok: true, ...(prepared.clientData ? { clientData: prepared.clientData } : {}) })\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge prepare routes',\n methods: {\n POST: {\n summary: 'Prepare MFA challenge payload for selected method',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge prepared', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,aAAa,gBAAgB,YAAY,gCAAgC;AAElF,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAC9B,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AACzD,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,uBAAuB,EAAE;AACxG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,iBAAiB,aAAa,YAAY,EAAE,SAAS,IAAI,CAAC;AAChH,WAAO,aAAa,KAAK,EAAE,IAAI,MAAM,GAAI,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC,EAAG,CAAC;AAAA,EAC5G,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,7 @@ const responseSchema = z.object({
12
12
  redirect: z.string()
13
13
  });
14
14
  const metadata = {
15
- POST: { requireAuth: true }
15
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: "security_mfa_recovery" } }
16
16
  };
17
17
  async function POST(req) {
18
18
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/recovery/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n code: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const code = readString(body.code)\n if (!code) {\n return securityApiError(400, 'code is required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyRecoveryCode(context.auth.sub, code)\n if (!verified) {\n return securityApiError(401, 'Invalid recovery code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA recovery routes',\n methods: {\n POST: {\n summary: 'Verify MFA recovery code during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'Recovery challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid recovery code', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,OAAO,WAAW,KAAK,IAAI;AACjC,MAAI,CAAC,MAAM;AACT,WAAO,iBAAiB,KAAK,mBAAmB;AAAA,EAClD;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,mBAAmB,QAAQ,KAAK,KAAK,IAAI;AAC/F,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,wBAAwB;AAAA,IACvD;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,+BAA+B,QAAQ,eAAe,CAAC;AAAA,MAC/F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,oBAAoB;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n code: z.string().min(1),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_recovery' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const code = readString(body.code)\n if (!code) {\n return securityApiError(400, 'code is required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyRecoveryCode(context.auth.sub, code)\n if (!verified) {\n return securityApiError(401, 'Invalid recovery code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA recovery routes',\n methods: {\n POST: {\n summary: 'Verify MFA recovery code during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'Recovery challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid recovery code', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,wBAAwB,EAAE;AACzG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,OAAO,WAAW,KAAK,IAAI;AACjC,MAAI,CAAC,MAAM;AACT,WAAO,iBAAiB,KAAK,mBAAmB;AAAA,EAClD;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,mBAAmB,QAAQ,KAAK,KAAK,IAAI;AAC/F,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,wBAAwB;AAAA,IACvD;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,+BAA+B,QAAQ,eAAe,CAAC;AAAA,MAC/F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,yBAAyB,QAAQ,oBAAoB;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,7 @@ const responseSchema = z.object({
14
14
  redirect: z.string()
15
15
  });
16
16
  const metadata = {
17
- POST: { requireAuth: true }
17
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: "security_mfa_verify" } }
18
18
  };
19
19
  async function POST(req) {
20
20
  const context = await resolveMfaRequestContext(req);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/security/api/mfa/verify/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).default({}),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n const payload = body.payload && typeof body.payload === 'object' ? body.payload : {}\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyChallenge(challengeId, methodType, payload, { request: req })\n if (!verified) {\n return securityApiError(401, 'Invalid MFA verification code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge verify routes',\n methods: {\n POST: {\n summary: 'Verify MFA challenge during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid challenge response', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,KAAK;AAC5B;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,QAAM,UAAU,KAAK,WAAW,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,CAAC;AACnF,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,gBAAgB,aAAa,YAAY,SAAS,EAAE,SAAS,IAAI,CAAC;AACxH,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,gCAAgC;AAAA,IAC/D;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,oBAAoB;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { buildSecurityOpenApi, securityErrorSchema } from '../../openapi'\nimport { securityApiError } from '../../i18n'\nimport { issueVerifiedMfaToken, mapMfaError, readJsonRecord, readString, resolveMfaRequestContext, setAuthCookie } from '../_shared'\n\nconst requestSchema = z.object({\n challengeId: z.string().min(1),\n methodType: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).default({}),\n})\n\nconst responseSchema = z.object({\n ok: z.literal(true),\n token: z.string(),\n redirect: z.string(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_verify' } },\n}\n\nexport async function POST(req: Request) {\n const context = await resolveMfaRequestContext(req)\n if (context instanceof NextResponse) return context\n\n if (context.auth.mfa_pending !== true) {\n return securityApiError(403, 'MFA pending token is required.')\n }\n\n const body = await readJsonRecord(req)\n const challengeId = readString(body.challengeId)\n const methodType = readString(body.methodType)\n const payload = body.payload && typeof body.payload === 'object' ? body.payload : {}\n if (!challengeId || !methodType) {\n return securityApiError(400, 'challengeId and methodType are required.')\n }\n\n try {\n const verified = await context.mfaVerificationService.verifyChallenge(challengeId, methodType, payload, { request: req })\n if (!verified) {\n return securityApiError(401, 'Invalid MFA verification code.')\n }\n\n const methods = await context.mfaService.getUserMethods(context.auth.sub)\n const token = issueVerifiedMfaToken(context.auth, methods.map((method) => method.type))\n const response = NextResponse.json({ ok: true, token, redirect: '/backend' })\n setAuthCookie(response, token)\n return response\n } catch (error) {\n return await mapMfaError(error)\n }\n}\n\nexport const openApi = buildSecurityOpenApi({\n summary: 'MFA challenge verify routes',\n methods: {\n POST: {\n summary: 'Verify MFA challenge during login flow',\n requestBody: {\n contentType: 'application/json',\n schema: requestSchema,\n },\n responses: [{ status: 200, description: 'MFA challenge verified', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Invalid payload', schema: securityErrorSchema },\n { status: 401, description: 'Invalid challenge response', schema: securityErrorSchema },\n { status: 403, description: 'Pending MFA context required', schema: securityErrorSchema },\n ],\n },\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,wBAAwB;AACjC,SAAS,uBAAuB,aAAa,gBAAgB,YAAY,0BAA0B,qBAAqB;AAExH,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,IAAI,EAAE,QAAQ,IAAI;AAAA,EAClB,OAAO,EAAE,OAAO;AAAA,EAChB,UAAU,EAAE,OAAO;AACrB,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,WAAW,EAAE,QAAQ,IAAI,UAAU,IAAI,WAAW,sBAAsB,EAAE;AACvG;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,UAAU,MAAM,yBAAyB,GAAG;AAClD,MAAI,mBAAmB,aAAc,QAAO;AAE5C,MAAI,QAAQ,KAAK,gBAAgB,MAAM;AACrC,WAAO,iBAAiB,KAAK,gCAAgC;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,eAAe,GAAG;AACrC,QAAM,cAAc,WAAW,KAAK,WAAW;AAC/C,QAAM,aAAa,WAAW,KAAK,UAAU;AAC7C,QAAM,UAAU,KAAK,WAAW,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,CAAC;AACnF,MAAI,CAAC,eAAe,CAAC,YAAY;AAC/B,WAAO,iBAAiB,KAAK,0CAA0C;AAAA,EACzE;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,uBAAuB,gBAAgB,aAAa,YAAY,SAAS,EAAE,SAAS,IAAI,CAAC;AACxH,QAAI,CAAC,UAAU;AACb,aAAO,iBAAiB,KAAK,gCAAgC;AAAA,IAC/D;AAEA,UAAM,UAAU,MAAM,QAAQ,WAAW,eAAe,QAAQ,KAAK,GAAG;AACxE,UAAM,QAAQ,sBAAsB,QAAQ,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,IAAI,CAAC;AACtF,UAAM,WAAW,aAAa,KAAK,EAAE,IAAI,MAAM,OAAO,UAAU,WAAW,CAAC;AAC5E,kBAAc,UAAU,KAAK;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,MAAM,YAAY,KAAK;AAAA,EAChC;AACF;AAEO,MAAM,UAAU,qBAAqB;AAAA,EAC1C,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,eAAe,CAAC;AAAA,MAC1F,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,oBAAoB;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,8BAA8B,QAAQ,oBAAoB;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,gCAAgC,QAAQ,oBAAoB;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF,CAAC;",
6
6
  "names": []
7
7
  }
@@ -50,6 +50,7 @@ class MfaVerificationService {
50
50
  }
51
51
  async prepareChallenge(challengeId, methodType, context) {
52
52
  const challenge = await this.getValidChallenge(challengeId);
53
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
53
54
  const provider = this.mfaProviderRegistry.get(methodType);
54
55
  if (!provider) {
55
56
  throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400);
@@ -73,12 +74,9 @@ class MfaVerificationService {
73
74
  if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
74
75
  return false;
75
76
  }
77
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
76
78
  if (challenge.methodType && challenge.methodType !== methodType) {
77
- challenge.attempts += 1;
78
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
79
- challenge.expiresAt = /* @__PURE__ */ new Date();
80
- }
81
- await this.em.flush();
79
+ await this.registerFailedAttempt(challenge);
82
80
  return false;
83
81
  }
84
82
  const provider = this.mfaProviderRegistry.get(methodType);
@@ -106,11 +104,7 @@ class MfaVerificationService {
106
104
  });
107
105
  return true;
108
106
  }
109
- challenge.attempts += 1;
110
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
111
- challenge.expiresAt = /* @__PURE__ */ new Date();
112
- }
113
- await this.em.flush();
107
+ await this.registerFailedAttempt(challenge);
114
108
  return false;
115
109
  }
116
110
  async verifyRecoveryCode(userId, code) {
@@ -129,6 +123,32 @@ class MfaVerificationService {
129
123
  }
130
124
  return challenge;
131
125
  }
126
+ async assertMethodAllowedByPolicy(userId, methodType) {
127
+ const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId);
128
+ if (!policy?.isEnforced || !policy.allowedMethods?.length) {
129
+ return;
130
+ }
131
+ if (!policy.allowedMethods.includes(methodType)) {
132
+ throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403);
133
+ }
134
+ }
135
+ async registerFailedAttempt(challenge) {
136
+ const maxAttempts = this.securityConfig.mfa.maxAttempts;
137
+ const rows = await this.em.getConnection().execute(
138
+ "UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts",
139
+ [challenge.id, maxAttempts]
140
+ );
141
+ const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts;
142
+ challenge.attempts = updatedAttempts;
143
+ if (updatedAttempts >= maxAttempts) {
144
+ const now = /* @__PURE__ */ new Date();
145
+ await this.em.getConnection().execute(
146
+ "UPDATE mfa_challenges SET expires_at = ? WHERE id = ?",
147
+ [now, challenge.id]
148
+ );
149
+ challenge.expiresAt = now;
150
+ }
151
+ }
132
152
  async getActiveMethods(userId) {
133
153
  const methods = await this.em.find(
134
154
  UserMfaMethod,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/MfaVerificationService.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const challenge = await this.getValidChallenge(challengeId)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const challenge = await this.getValidChallenge(challengeId)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n challenge.attempts += 1\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n challenge.expiresAt = new Date()\n }\n await this.em.flush()\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n challenge.attempts += 1\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n challenge.expiresAt = new Date()\n }\n await this.em.flush()\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(challengeId: string): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, { id: challengeId })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const challenge = await this.getValidChallenge(challengeId)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean> {\n const challenge = await this.getValidChallenge(challengeId)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(challengeId: string): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, { id: challengeId })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { z } from "zod";
2
3
  import { User } from "@open-mercato/core/modules/auth/data/entities";
3
4
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
5
  import {
@@ -14,6 +15,14 @@ import {
14
15
  import { emitSecurityEvent } from "../events.js";
15
16
  import { sudoTargets as defaultSudoTargets } from "../security.sudo.js";
16
17
  import { readSecurityModuleConfig } from "../lib/security-config.js";
18
+ const signedSudoTokenPayloadSchema = z.object({
19
+ sid: z.string(),
20
+ sub: z.string(),
21
+ tid: z.string().nullable(),
22
+ oid: z.string().nullable(),
23
+ tgt: z.string(),
24
+ exp: z.number()
25
+ });
17
26
  class SudoChallengeServiceError extends Error {
18
27
  constructor(message, statusCode) {
19
28
  super(message);
@@ -472,9 +481,11 @@ class SudoChallengeService {
472
481
  if (signatureBuffer.length !== expectedBuffer.length) return null;
473
482
  if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null;
474
483
  try {
475
- const parsed = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
476
- if (!parsed || typeof parsed !== "object") return null;
477
- return parsed;
484
+ const parsed = signedSudoTokenPayloadSchema.safeParse(
485
+ JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"))
486
+ );
487
+ if (!parsed.success) return null;
488
+ return parsed.data;
478
489
  } catch {
479
490
  return null;
480
491
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/security/services/SudoChallengeService.ts"],
4
- "sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\ntype SignedSudoTokenPayload = {\n sid: string\n sub: string\n tid: string | null\n oid: string | null\n tgt: string\n exp: number\n}\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n\n const candidates = await this.em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const protection = await this.isProtected(\n targetIdentifier,\n options?.tenantId ?? null,\n options?.organizationId ?? null,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(session.sessionToken, methodType, { request })\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n options.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(session.sessionToken, methodType, payload, { request })\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const sudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: options.targetIdentifier,\n exp: expiresAt.getTime(),\n })\n\n session.sessionToken = sudoToken\n session.challengeMethod = methodUsed\n session.expiresAt = expiresAt\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n existing.updatedAt = new Date()\n await this.em.flush()\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n return 'password'\n }\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<SudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) as SignedSudoTokenPayload\n if (!parsed || typeof parsed !== 'object') return null\n return parsed\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n return process.env.OM_SECURITY_SUDO_SECRET\n ?? process.env.AUTH_JWT_SECRET\n ?? process.env.JWT_SECRET\n ?? 'open-mercato-sudo-secret'\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { z } from 'zod'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n dedupeSudoTargets,\n getSecuritySudoTargetEntries,\n type SecuritySudoTarget,\n} from '../lib/module-security-registry'\nimport {\n ChallengeMethod,\n SudoChallengeConfig,\n SudoChallengeMethodUsed,\n SudoSession,\n} from '../data/entities'\nimport { emitSecurityEvent } from '../events'\nimport type {\n SudoConfigInput,\n SudoConfigUpdateInput,\n} from '../data/validators'\nimport type { PasswordService } from './PasswordService'\nimport type { MfaService } from './MfaService'\nimport type { MfaVerificationService } from './MfaVerificationService'\nimport { sudoTargets as defaultSudoTargets } from '../security.sudo'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype SudoMethod = 'password' | 'mfa'\n\nexport type SudoAvailableMethod = {\n type: string\n label: string\n icon: string\n}\n\nexport type SudoProtectionResolution = {\n protected: boolean\n config?: SudoChallengeConfig\n}\n\nconst signedSudoTokenPayloadSchema = z.object({\n sid: z.string(),\n sub: z.string(),\n tid: z.string().nullable(),\n oid: z.string().nullable(),\n tgt: z.string(),\n exp: z.number(),\n})\n\ntype SignedSudoTokenPayload = z.infer<typeof signedSudoTokenPayloadSchema>\n\ntype UserScope = {\n id: string\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type SudoAuthScope = {\n tenantId: string | null\n organizationId?: string | null\n isSuperAdmin?: boolean\n}\n\ntype DeveloperDefaultPayload = {\n targetIdentifier: string\n label?: string | null\n ttlSeconds?: number\n challengeMethod?: ChallengeMethod\n}\n\nexport class SudoChallengeServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'SudoChallengeServiceError'\n }\n}\n\nexport class SudoChallengeService {\n constructor(\n private readonly em: EntityManager,\n private readonly passwordService: PasswordService,\n private readonly mfaService: MfaService,\n private readonly mfaVerificationService: MfaVerificationService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async listConfigs(scope?: SudoAuthScope): Promise<SudoChallengeConfig[]> {\n await this.ensureDeveloperDefaultsRegistered()\n const filter: FilterQuery<SudoChallengeConfig> = { deletedAt: null }\n if (scope && !scope.isSuperAdmin) {\n if (!scope.tenantId) return []\n ;(filter as Record<string, unknown>).$or = [\n { tenantId: scope.tenantId },\n { tenantId: null },\n ]\n }\n return this.em.find(\n SudoChallengeConfig,\n filter,\n {\n orderBy: {\n targetIdentifier: 'asc',\n tenantId: 'asc',\n organizationId: 'asc',\n createdAt: 'asc',\n },\n },\n )\n }\n\n async getConfigById(id: string, scope?: SudoAuthScope): Promise<SudoChallengeConfig | null> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) return null\n if (scope && !this.isConfigVisibleToScope(config, scope)) return null\n return config\n }\n\n async isProtected(\n targetIdentifier: string,\n tenantId?: string | null,\n organizationId?: string | null,\n ): Promise<SudoProtectionResolution> {\n await this.ensureDeveloperDefaultsRegistered()\n\n const candidates = await this.em.find(SudoChallengeConfig, {\n targetIdentifier,\n deletedAt: null,\n })\n\n const resolved = candidates\n .filter((config) => this.matchesScope(config, tenantId ?? null, organizationId ?? null))\n .sort((left, right) => this.compareConfigPriority(left, right, tenantId ?? null, organizationId ?? null))[0]\n\n if (!resolved || !resolved.isEnabled) {\n return { protected: false }\n }\n\n return { protected: true, config: resolved }\n }\n\n async initiate(\n userId: string,\n targetIdentifier: string,\n options?: { tenantId?: string | null; organizationId?: string | null },\n ): Promise<{\n required: boolean\n sessionId?: string\n method?: SudoMethod\n availableMfaMethods?: SudoAvailableMethod[]\n expiresAt?: Date\n }> {\n const protection = await this.isProtected(\n targetIdentifier,\n options?.tenantId ?? null,\n options?.organizationId ?? null,\n )\n\n if (!protection.protected || !protection.config) {\n return { required: false }\n }\n\n const user = await this.findUserScope(userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const userMethods = await this.mfaService.getUserMethods(userId)\n const method = this.resolveChallengeMethod(protection.config.challengeMethod, userMethods.length)\n\n let sessionToken = randomBytes(16).toString('hex')\n let availableMfaMethods: SudoAvailableMethod[] | undefined\n if (method === 'mfa') {\n const challenge = await this.mfaVerificationService.createChallenge(userId)\n sessionToken = challenge.challengeId\n availableMfaMethods = challenge.availableMethods\n }\n\n const expiresAt = new Date(Date.now() + this.securityConfig.sudo.pendingChallengeTtlMs)\n const session = this.em.create(SudoSession, {\n userId,\n tenantId: user.tenantId,\n sessionToken,\n challengeMethod: method,\n expiresAt,\n createdAt: new Date(),\n })\n this.em.persist(session)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.challenged', {\n userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier,\n method,\n })\n\n return {\n required: true,\n sessionId: session.id,\n method,\n availableMfaMethods,\n expiresAt,\n }\n }\n\n async prepare(\n sessionId: string,\n methodType: string,\n request?: Request,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n const session = await this.getPendingSession(sessionId)\n if (session.challengeMethod !== 'mfa') {\n throw new SudoChallengeServiceError('This sudo session does not require MFA', 400)\n }\n return this.mfaVerificationService.prepareChallenge(session.sessionToken, methodType, { request })\n }\n\n async verify(\n sessionId: string,\n methodType: string,\n payload: unknown,\n options: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n targetIdentifier: string\n },\n request?: Request,\n ): Promise<{ sudoToken: string; expiresAt: Date }> {\n const session = await this.getPendingSession(sessionId)\n if (options.expectedUserId && session.userId !== options.expectedUserId) {\n throw new SudoChallengeServiceError('Sudo challenge user mismatch', 403)\n }\n const user = await this.findUserScope(session.userId)\n if (!user?.tenantId) {\n throw new SudoChallengeServiceError('User not found', 404)\n }\n\n const scopeTenantId = options.tenantId !== undefined ? options.tenantId : user.tenantId\n const scopeOrganizationId = options.organizationId !== undefined ? options.organizationId : user.organizationId\n const protection = await this.isProtected(\n options.targetIdentifier,\n scopeTenantId,\n scopeOrganizationId,\n )\n if (!protection.protected || !protection.config) {\n throw new SudoChallengeServiceError('Sudo protection is not configured for this target', 404)\n }\n\n let verified = false\n let methodUsed: string = methodType\n if (session.challengeMethod === 'password') {\n const password = this.readPassword(payload)\n verified = await this.passwordService.verifyPassword(session.userId, password)\n methodUsed = SudoChallengeMethodUsed.PASSWORD\n } else {\n verified = await this.mfaVerificationService.verifyChallenge(session.sessionToken, methodType, payload, { request })\n }\n\n if (!verified) {\n await emitSecurityEvent('security.sudo.failed', {\n userId: session.userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n })\n throw new SudoChallengeServiceError('Unable to verify sudo challenge', 401)\n }\n\n const ttlSeconds = this.normalizeTtl(protection.config.ttlSeconds)\n const expiresAt = new Date(Date.now() + ttlSeconds * 1000)\n const sudoToken = this.signToken({\n sid: session.id,\n sub: session.userId,\n tid: scopeTenantId,\n oid: scopeOrganizationId,\n tgt: options.targetIdentifier,\n exp: expiresAt.getTime(),\n })\n\n session.sessionToken = sudoToken\n session.challengeMethod = methodUsed\n session.expiresAt = expiresAt\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.verified', {\n userId: session.userId,\n tenantId: scopeTenantId,\n organizationId: scopeOrganizationId,\n targetIdentifier: options.targetIdentifier,\n method: methodUsed,\n expiresAt: expiresAt.toISOString(),\n })\n\n return { sudoToken, expiresAt }\n }\n\n async validateToken(\n token: string,\n targetIdentifier: string,\n options?: {\n expectedUserId?: string\n tenantId?: string | null\n organizationId?: string | null\n },\n ): Promise<boolean> {\n if (!token) return false\n const payload = this.readSignedToken(token)\n if (!payload) return false\n if (payload.exp <= Date.now()) return false\n if (payload.tgt !== targetIdentifier) return false\n if (options?.expectedUserId && payload.sub !== options.expectedUserId) return false\n if (options?.tenantId !== undefined && payload.tid !== (options.tenantId ?? null)) return false\n if (options?.organizationId !== undefined && payload.oid !== (options.organizationId ?? null)) return false\n\n const session = await this.em.findOne(SudoSession, {\n id: payload.sid,\n userId: payload.sub,\n sessionToken: token,\n } as FilterQuery<SudoSession>)\n\n return Boolean(session && session.expiresAt.getTime() > Date.now())\n }\n\n async createConfig(\n input: SudoConfigInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const inputTenantId = input.tenantId ?? null\n const inputOrganizationId = input.organizationId ?? null\n this.validateScope(inputTenantId, inputOrganizationId)\n if (scope) this.assertWriteScope(inputTenantId, inputOrganizationId, scope)\n await this.ensureUniqueConfig(input.targetIdentifier, inputTenantId, inputOrganizationId)\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: inputTenantId,\n organizationId: inputOrganizationId,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: input.isEnabled,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod,\n configuredBy,\n isDeveloperDefault: false,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.created', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async updateConfig(\n id: string,\n input: SudoConfigUpdateInput,\n configuredBy: string,\n scope?: SudoAuthScope,\n ): Promise<SudoChallengeConfig> {\n await this.ensureDeveloperDefaultsRegistered()\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n const nextTenantId = input.tenantId !== undefined ? input.tenantId ?? null : config.tenantId ?? null\n const nextOrganizationId = input.organizationId !== undefined ? input.organizationId ?? null : config.organizationId ?? null\n const nextTargetIdentifier = input.targetIdentifier ?? config.targetIdentifier\n this.validateScope(nextTenantId, nextOrganizationId)\n if (scope) this.assertWriteScope(nextTenantId, nextOrganizationId, scope)\n await this.ensureUniqueConfig(nextTargetIdentifier, nextTenantId, nextOrganizationId, config.id)\n\n if (input.tenantId !== undefined) config.tenantId = input.tenantId ?? null\n if (input.organizationId !== undefined) config.organizationId = input.organizationId ?? null\n if (input.label !== undefined) config.label = input.label ?? null\n if (input.targetIdentifier !== undefined) config.targetIdentifier = input.targetIdentifier\n if (input.isEnabled !== undefined) config.isEnabled = input.isEnabled\n if (input.ttlSeconds !== undefined) config.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n if (input.challengeMethod !== undefined) config.challengeMethod = input.challengeMethod\n config.configuredBy = configuredBy\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.updated', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n configuredBy,\n })\n\n return config\n }\n\n async deleteConfig(id: string, scope?: SudoAuthScope): Promise<void> {\n const config = await this.em.findOne(SudoChallengeConfig, { id, deletedAt: null })\n if (!config) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (scope) {\n this.assertWriteScope(config.tenantId ?? null, config.organizationId ?? null, scope)\n if (config.isDeveloperDefault && !scope.isSuperAdmin) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n config.deletedAt = new Date()\n config.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.sudo.config.deleted', {\n id: config.id,\n targetIdentifier: config.targetIdentifier,\n })\n }\n\n async registerDeveloperDefault(\n input: DeveloperDefaultPayload,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier: input.targetIdentifier,\n tenantId: null,\n organizationId: null,\n isDeveloperDefault: true,\n })\n\n if (existing) {\n existing.isEnabled = true\n existing.deletedAt = null\n existing.ttlSeconds = this.normalizeTtl(input.ttlSeconds)\n existing.challengeMethod = input.challengeMethod ?? ChallengeMethod.AUTO\n existing.updatedAt = new Date()\n await this.em.flush()\n return\n }\n\n const config = this.em.create(SudoChallengeConfig, {\n tenantId: null,\n organizationId: null,\n label: input.label ?? null,\n targetIdentifier: input.targetIdentifier,\n isEnabled: true,\n isDeveloperDefault: true,\n ttlSeconds: this.normalizeTtl(input.ttlSeconds),\n challengeMethod: input.challengeMethod ?? ChallengeMethod.AUTO,\n configuredBy: null,\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n this.em.persist(config)\n await this.em.flush()\n }\n\n async cleanupExpired(): Promise<number> {\n return this.em.nativeDelete(SudoSession, {\n expiresAt: { $lte: new Date() },\n })\n }\n\n private async ensureDeveloperDefaultsRegistered(): Promise<void> {\n const registryEntries = getSecuritySudoTargetEntries()\n const registryTargets = registryEntries.flatMap((entry) => entry.targets ?? [])\n const fallbackTargets = registryEntries.length === 0 ? defaultSudoTargets : []\n\n for (const target of dedupeSudoTargets([\n ...registryTargets,\n ...fallbackTargets,\n ])) {\n await this.registerDeveloperDefault(this.readDeveloperDefault(target))\n }\n }\n\n private readDeveloperDefault(target: SecuritySudoTarget): DeveloperDefaultPayload {\n return {\n targetIdentifier: target.identifier,\n label: target.label ?? null,\n ttlSeconds: target.ttlSeconds,\n challengeMethod: this.toChallengeMethod(target.challengeMethod),\n }\n }\n\n private async ensureUniqueConfig(\n targetIdentifier: string,\n tenantId: string | null,\n organizationId: string | null,\n ignoreId?: string,\n ): Promise<void> {\n const existing = await this.em.findOne(SudoChallengeConfig, {\n targetIdentifier,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (existing && existing.id !== ignoreId) {\n throw new SudoChallengeServiceError('A sudo configuration for this target and scope already exists', 409)\n }\n }\n\n private validateScope(tenantId: string | null, organizationId: string | null): void {\n if (organizationId && !tenantId) {\n throw new SudoChallengeServiceError('Organization-scoped sudo config requires a tenant', 400)\n }\n }\n\n private isConfigVisibleToScope(config: SudoChallengeConfig, scope: SudoAuthScope): boolean {\n if (scope.isSuperAdmin) return true\n if (!scope.tenantId) return false\n const configTenantId = config.tenantId ?? null\n if (configTenantId === null) return true\n return configTenantId === scope.tenantId\n }\n\n private assertWriteScope(\n targetTenantId: string | null,\n targetOrganizationId: string | null,\n scope: SudoAuthScope,\n ): void {\n if (scope.isSuperAdmin) return\n if (!scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if ((targetTenantId ?? null) !== scope.tenantId) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n if (\n scope.organizationId !== undefined\n && scope.organizationId !== null\n && targetOrganizationId !== null\n && targetOrganizationId !== scope.organizationId\n ) {\n throw new SudoChallengeServiceError('Sudo configuration not found', 404)\n }\n }\n\n private resolveChallengeMethod(\n configuredMethod: ChallengeMethod,\n availableMfaMethodCount: number,\n ): SudoMethod {\n if (this.securityConfig.mfa.emergencyBypass) {\n return 'password'\n }\n if (configuredMethod === ChallengeMethod.PASSWORD) return 'password'\n if (configuredMethod === ChallengeMethod.MFA) {\n if (availableMfaMethodCount === 0) {\n throw new SudoChallengeServiceError('This sudo target requires MFA, but no MFA methods are configured', 400)\n }\n return 'mfa'\n }\n return availableMfaMethodCount > 0 ? 'mfa' : 'password'\n }\n\n private matchesScope(config: SudoChallengeConfig, tenantId: string | null, organizationId: string | null): boolean {\n if (config.organizationId) {\n return config.organizationId === organizationId && config.tenantId === tenantId\n }\n if (config.tenantId) {\n return config.tenantId === tenantId\n }\n return true\n }\n\n private compareConfigPriority(\n left: SudoChallengeConfig,\n right: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n const leftScore = this.getScopePriority(left, tenantId, organizationId)\n const rightScore = this.getScopePriority(right, tenantId, organizationId)\n if (leftScore !== rightScore) return leftScore - rightScore\n if (left.isDeveloperDefault !== right.isDeveloperDefault) {\n return left.isDeveloperDefault ? 1 : -1\n }\n return right.updatedAt.getTime() - left.updatedAt.getTime()\n }\n\n private getScopePriority(\n config: SudoChallengeConfig,\n tenantId: string | null,\n organizationId: string | null,\n ): number {\n if (config.organizationId === organizationId && config.tenantId === tenantId) return 0\n if (!config.organizationId && config.tenantId === tenantId) return 1\n if (!config.organizationId && !config.tenantId && !config.isDeveloperDefault) return 2\n if (!config.organizationId && !config.tenantId && config.isDeveloperDefault) return 3\n return 4\n }\n\n private async getPendingSession(sessionId: string): Promise<SudoSession> {\n const session = await this.em.findOne(SudoSession, { id: sessionId })\n if (!session) {\n throw new SudoChallengeServiceError('Sudo challenge session not found', 404)\n }\n if (session.expiresAt.getTime() <= Date.now()) {\n throw new SudoChallengeServiceError('Sudo challenge session expired', 400)\n }\n return session\n }\n\n private async findUserScope(userId: string): Promise<UserScope | null> {\n const user = await findOneWithDecryption(\n this.em,\n User,\n { id: userId, deletedAt: null },\n undefined,\n {},\n )\n\n if (!user) return null\n return {\n id: String(user.id),\n tenantId: user.tenantId ? String(user.tenantId) : null,\n organizationId: user.organizationId ? String(user.organizationId) : null,\n }\n }\n\n private normalizeTtl(value?: number | null): number {\n const rawValue = value ?? this.securityConfig.sudo.defaultTtlSeconds\n return Math.min(\n Math.max(rawValue, this.securityConfig.sudo.minTtlSeconds),\n this.securityConfig.sudo.maxTtlSeconds,\n )\n }\n\n private readPassword(payload: unknown): string {\n if (!payload || typeof payload !== 'object') {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n const maybePassword = (payload as Record<string, unknown>).password\n if (typeof maybePassword !== 'string' || maybePassword.trim().length === 0) {\n throw new SudoChallengeServiceError('Password is required', 400)\n }\n return maybePassword\n }\n\n private signToken(payload: SignedSudoTokenPayload): string {\n const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')\n const signature = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n return `${encodedPayload}.${signature}`\n }\n\n private readSignedToken(token: string): SignedSudoTokenPayload | null {\n const [encodedPayload, signature] = token.split('.')\n if (!encodedPayload || !signature) return null\n\n const expected = createHmac('sha256', this.getSudoSecret()).update(encodedPayload).digest('base64url')\n const signatureBuffer = Buffer.from(signature)\n const expectedBuffer = Buffer.from(expected)\n if (signatureBuffer.length !== expectedBuffer.length) return null\n if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null\n\n try {\n const parsed = signedSudoTokenPayloadSchema.safeParse(\n JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')),\n )\n if (!parsed.success) return null\n return parsed.data\n } catch {\n return null\n }\n }\n\n private getSudoSecret(): string {\n return process.env.OM_SECURITY_SUDO_SECRET\n ?? process.env.AUTH_JWT_SECRET\n ?? process.env.JWT_SECRET\n ?? 'open-mercato-sudo-secret'\n }\n\n private toChallengeMethod(\n method: SecuritySudoTarget['challengeMethod'],\n ): ChallengeMethod | undefined {\n switch (method) {\n case 'password':\n return ChallengeMethod.PASSWORD\n case 'mfa':\n return ChallengeMethod.MFA\n case 'auto':\n default:\n return ChallengeMethod.AUTO\n }\n }\n}\n\nexport default SudoChallengeService\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/enterprise",
3
- "version": "0.6.5-develop.4964.1.ae0edca575",
3
+ "version": "0.6.5-develop.5033.1.c970204a3f",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -64,8 +64,8 @@
64
64
  }
65
65
  },
66
66
  "dependencies": {
67
- "@open-mercato/core": "0.6.5-develop.4964.1.ae0edca575",
68
- "@open-mercato/ui": "0.6.5-develop.4964.1.ae0edca575",
67
+ "@open-mercato/core": "0.6.5-develop.5033.1.c970204a3f",
68
+ "@open-mercato/ui": "0.6.5-develop.5033.1.c970204a3f",
69
69
  "@simplewebauthn/browser": "^13.3.0",
70
70
  "@simplewebauthn/server": "^13.3.1",
71
71
  "@simplewebauthn/types": "^12.0.0",
@@ -75,12 +75,12 @@
75
75
  "qrcode": "^1.5.4"
76
76
  },
77
77
  "peerDependencies": {
78
- "@open-mercato/shared": "0.6.5-develop.4964.1.ae0edca575",
78
+ "@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
79
79
  "react": "^19.0.0",
80
80
  "react-dom": "^19.0.0"
81
81
  },
82
82
  "devDependencies": {
83
- "@open-mercato/shared": "0.6.5-develop.4964.1.ae0edca575",
83
+ "@open-mercato/shared": "0.6.5-develop.5033.1.c970204a3f",
84
84
  "@types/jest": "^30.0.0",
85
85
  "@types/react": "^19.2.16",
86
86
  "@types/react-dom": "^19.2.3",
@@ -15,7 +15,7 @@ const responseSchema = z.object({
15
15
  })
16
16
 
17
17
  export const metadata = {
18
- POST: { requireAuth: true },
18
+ POST: { requireAuth: true, rateLimit: { points: 20, duration: 60, keyPrefix: 'security_mfa_prepare' } },
19
19
  }
20
20
 
21
21
  export async function POST(req: Request) {
@@ -15,7 +15,7 @@ const responseSchema = z.object({
15
15
  })
16
16
 
17
17
  export const metadata = {
18
- POST: { requireAuth: true },
18
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_recovery' } },
19
19
  }
20
20
 
21
21
  export async function POST(req: Request) {
@@ -17,7 +17,7 @@ const responseSchema = z.object({
17
17
  })
18
18
 
19
19
  export const metadata = {
20
- POST: { requireAuth: true },
20
+ POST: { requireAuth: true, rateLimit: { points: 10, duration: 60, keyPrefix: 'security_mfa_verify' } },
21
21
  }
22
22
 
23
23
  export async function POST(req: Request) {
@@ -87,6 +87,7 @@ export class MfaVerificationService {
87
87
  context?: MfaProviderRuntimeContext,
88
88
  ): Promise<{ clientData?: Record<string, unknown> }> {
89
89
  const challenge = await this.getValidChallenge(challengeId)
90
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType)
90
91
  const provider = this.mfaProviderRegistry.get(methodType)
91
92
  if (!provider) {
92
93
  throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)
@@ -119,12 +120,10 @@ export class MfaVerificationService {
119
120
  return false
120
121
  }
121
122
 
123
+ await this.assertMethodAllowedByPolicy(challenge.userId, methodType)
124
+
122
125
  if (challenge.methodType && challenge.methodType !== methodType) {
123
- challenge.attempts += 1
124
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
125
- challenge.expiresAt = new Date()
126
- }
127
- await this.em.flush()
126
+ await this.registerFailedAttempt(challenge)
128
127
  return false
129
128
  }
130
129
 
@@ -160,11 +159,7 @@ export class MfaVerificationService {
160
159
  return true
161
160
  }
162
161
 
163
- challenge.attempts += 1
164
- if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
165
- challenge.expiresAt = new Date()
166
- }
167
- await this.em.flush()
162
+ await this.registerFailedAttempt(challenge)
168
163
  return false
169
164
  }
170
165
 
@@ -186,6 +181,34 @@ export class MfaVerificationService {
186
181
  return challenge
187
182
  }
188
183
 
184
+ private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {
185
+ const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)
186
+ if (!policy?.isEnforced || !policy.allowedMethods?.length) {
187
+ return
188
+ }
189
+ if (!policy.allowedMethods.includes(methodType)) {
190
+ throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)
191
+ }
192
+ }
193
+
194
+ private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {
195
+ const maxAttempts = this.securityConfig.mfa.maxAttempts
196
+ const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(
197
+ 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',
198
+ [challenge.id, maxAttempts],
199
+ )
200
+ const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts
201
+ challenge.attempts = updatedAttempts
202
+ if (updatedAttempts >= maxAttempts) {
203
+ const now = new Date()
204
+ await this.em.getConnection().execute(
205
+ 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',
206
+ [now, challenge.id],
207
+ )
208
+ challenge.expiresAt = now
209
+ }
210
+ }
211
+
189
212
  private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {
190
213
  const methods = await this.em.find(
191
214
  UserMfaMethod,
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
2
+ import { z } from 'zod'
2
3
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
3
4
  import { User } from '@open-mercato/core/modules/auth/data/entities'
4
5
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
@@ -38,14 +39,16 @@ export type SudoProtectionResolution = {
38
39
  config?: SudoChallengeConfig
39
40
  }
40
41
 
41
- type SignedSudoTokenPayload = {
42
- sid: string
43
- sub: string
44
- tid: string | null
45
- oid: string | null
46
- tgt: string
47
- exp: number
48
- }
42
+ const signedSudoTokenPayloadSchema = z.object({
43
+ sid: z.string(),
44
+ sub: z.string(),
45
+ tid: z.string().nullable(),
46
+ oid: z.string().nullable(),
47
+ tgt: z.string(),
48
+ exp: z.number(),
49
+ })
50
+
51
+ type SignedSudoTokenPayload = z.infer<typeof signedSudoTokenPayloadSchema>
49
52
 
50
53
  type UserScope = {
51
54
  id: string
@@ -665,9 +668,11 @@ export class SudoChallengeService {
665
668
  if (!timingSafeEqual(signatureBuffer, expectedBuffer)) return null
666
669
 
667
670
  try {
668
- const parsed = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')) as SignedSudoTokenPayload
669
- if (!parsed || typeof parsed !== 'object') return null
670
- return parsed
671
+ const parsed = signedSudoTokenPayloadSchema.safeParse(
672
+ JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')),
673
+ )
674
+ if (!parsed.success) return null
675
+ return parsed.data
671
676
  } catch {
672
677
  return null
673
678
  }