@open-mercato/enterprise 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7152.1.a69e92f9c9
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/security/lib/mfa-provider-interface.js.map +2 -2
- package/dist/modules/security/lib/providers/OtpEmailProvider.js +2 -0
- package/dist/modules/security/lib/providers/OtpEmailProvider.js.map +2 -2
- package/dist/modules/security/services/MfaVerificationService.js +4 -0
- package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
- package/dist/modules/security/subscribers/enforcement-deadline-notification.js +2 -0
- package/dist/modules/security/subscribers/enforcement-deadline-notification.js.map +2 -2
- package/dist/modules/security/subscribers/mfa-reset-notification.js +2 -0
- package/dist/modules/security/subscribers/mfa-reset-notification.js.map +2 -2
- package/dist/modules/security/subscribers/notification.js +2 -0
- package/dist/modules/security/subscribers/notification.js.map +2 -2
- package/package.json +5 -5
- package/src/modules/security/lib/mfa-provider-interface.ts +2 -0
- package/src/modules/security/lib/providers/OtpEmailProvider.ts +2 -0
- package/src/modules/security/services/MfaVerificationService.ts +4 -0
- package/src/modules/security/subscribers/enforcement-deadline-notification.ts +2 -0
- package/src/modules/security/subscribers/mfa-reset-notification.ts +2 -0
- package/src/modules/security/subscribers/notification.ts +2 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:enterprise] found
|
|
1
|
+
[build:enterprise] found 337 entry points
|
|
2
2
|
[build:enterprise] built successfully
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/lib/mfa-provider-interface.ts"],
|
|
4
|
-
"sourcesContent": ["import type { z } from 'zod'\nimport type React from 'react'\n\nexport type MfaMethodRecord = {\n id: string\n type: string\n userId: string\n secret?: string | null\n providerMetadata?: Record<string, unknown> | null\n}\n\nexport type MfaProviderConfirmResult = {\n metadata: Record<string, unknown>\n secret?: string | null\n}\n\nexport type MfaVerifyContext = {\n challenge?: Record<string, unknown> | null\n}\n\nexport type MfaProviderRuntimeContext = {\n request?: Request\n}\n\nexport type MfaProviderUser = {\n id: string\n email?: string | null\n tenantId: string\n organizationId?: string | null\n}\n\nexport interface MfaSetupComponentProps {\n clientData: Record<string, unknown>\n onConfirm: (payload: unknown) => Promise<void>\n onCancel: () => void\n}\n\nexport interface MfaVerifyComponentProps {\n clientData?: Record<string, unknown>\n onVerify: (payload: unknown) => Promise<void>\n onCancel: () => void\n onResend?: () => Promise<void>\n}\n\nexport interface MfaProviderInterface {\n readonly type: string\n readonly label: string\n readonly icon: string\n readonly allowMultiple: boolean\n readonly setupSchema: z.ZodSchema\n readonly verifySchema: z.ZodSchema\n\n resolveSetupPayload?(user: MfaProviderUser, payload: unknown): Promise<unknown> | unknown\n setup(\n userId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ setupId: string; clientData: Record<string, unknown> }>\n confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<MfaProviderConfirmResult>\n prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }>\n verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n}\n\nexport type MfaProviderComponents = {\n setup?: string\n list?: string\n details?: string\n challenge?: string\n}\n\nexport type MfaProviderSetup = MfaProviderInterface & {\n readonly components?: MfaProviderComponents\n}\n\nexport function buildMfaProviderComponentHandles(providerType: string): Required<MfaProviderComponents> {\n return {\n setup: `section:security.mfa.setup.provider:${providerType}`,\n list: `section:security.mfa.providers.list-item:${providerType}`,\n details: `section:security.mfa.provider.details:${providerType}`,\n challenge: `section:security.mfa.challenge.provider:${providerType}`,\n }\n}\n\nexport function createMfaProviderSetup(\n provider: MfaProviderInterface,\n components?: MfaProviderComponents,\n): MfaProviderSetup {\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n allowMultiple: provider.allowMultiple,\n setupSchema: provider.setupSchema,\n verifySchema: provider.verifySchema,\n resolveSetupPayload: provider.resolveSetupPayload?.bind(provider),\n setup: provider.setup.bind(provider),\n confirmSetup: provider.confirmSetup.bind(provider),\n prepareChallenge: provider.prepareChallenge.bind(provider),\n verify: provider.verify.bind(provider),\n components,\n }\n}\n\nexport function isMfaProviderSetup(value: unknown): value is MfaProviderSetup {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<MfaProviderSetup>\n return typeof candidate.type === 'string'\n && typeof candidate.label === 'string'\n && typeof candidate.icon === 'string'\n && typeof candidate.allowMultiple === 'boolean'\n && typeof candidate.setup === 'function'\n && typeof candidate.confirmSetup === 'function'\n && typeof candidate.prepareChallenge === 'function'\n && typeof candidate.verify === 'function'\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { z } from 'zod'\nimport type React from 'react'\n\nexport type MfaMethodRecord = {\n id: string\n type: string\n userId: string\n tenantId?: string\n organizationId?: string | null\n secret?: string | null\n providerMetadata?: Record<string, unknown> | null\n}\n\nexport type MfaProviderConfirmResult = {\n metadata: Record<string, unknown>\n secret?: string | null\n}\n\nexport type MfaVerifyContext = {\n challenge?: Record<string, unknown> | null\n}\n\nexport type MfaProviderRuntimeContext = {\n request?: Request\n}\n\nexport type MfaProviderUser = {\n id: string\n email?: string | null\n tenantId: string\n organizationId?: string | null\n}\n\nexport interface MfaSetupComponentProps {\n clientData: Record<string, unknown>\n onConfirm: (payload: unknown) => Promise<void>\n onCancel: () => void\n}\n\nexport interface MfaVerifyComponentProps {\n clientData?: Record<string, unknown>\n onVerify: (payload: unknown) => Promise<void>\n onCancel: () => void\n onResend?: () => Promise<void>\n}\n\nexport interface MfaProviderInterface {\n readonly type: string\n readonly label: string\n readonly icon: string\n readonly allowMultiple: boolean\n readonly setupSchema: z.ZodSchema\n readonly verifySchema: z.ZodSchema\n\n resolveSetupPayload?(user: MfaProviderUser, payload: unknown): Promise<unknown> | unknown\n setup(\n userId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ setupId: string; clientData: Record<string, unknown> }>\n confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n context?: MfaProviderRuntimeContext,\n ): Promise<MfaProviderConfirmResult>\n prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }>\n verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n}\n\nexport type MfaProviderComponents = {\n setup?: string\n list?: string\n details?: string\n challenge?: string\n}\n\nexport type MfaProviderSetup = MfaProviderInterface & {\n readonly components?: MfaProviderComponents\n}\n\nexport function buildMfaProviderComponentHandles(providerType: string): Required<MfaProviderComponents> {\n return {\n setup: `section:security.mfa.setup.provider:${providerType}`,\n list: `section:security.mfa.providers.list-item:${providerType}`,\n details: `section:security.mfa.provider.details:${providerType}`,\n challenge: `section:security.mfa.challenge.provider:${providerType}`,\n }\n}\n\nexport function createMfaProviderSetup(\n provider: MfaProviderInterface,\n components?: MfaProviderComponents,\n): MfaProviderSetup {\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n allowMultiple: provider.allowMultiple,\n setupSchema: provider.setupSchema,\n verifySchema: provider.verifySchema,\n resolveSetupPayload: provider.resolveSetupPayload?.bind(provider),\n setup: provider.setup.bind(provider),\n confirmSetup: provider.confirmSetup.bind(provider),\n prepareChallenge: provider.prepareChallenge.bind(provider),\n verify: provider.verify.bind(provider),\n components,\n }\n}\n\nexport function isMfaProviderSetup(value: unknown): value is MfaProviderSetup {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Partial<MfaProviderSetup>\n return typeof candidate.type === 'string'\n && typeof candidate.label === 'string'\n && typeof candidate.icon === 'string'\n && typeof candidate.allowMultiple === 'boolean'\n && typeof candidate.setup === 'function'\n && typeof candidate.confirmSetup === 'function'\n && typeof candidate.prepareChallenge === 'function'\n && typeof candidate.verify === 'function'\n}\n"],
|
|
5
|
+
"mappings": "AA2FO,SAAS,iCAAiC,cAAuD;AACtG,SAAO;AAAA,IACL,OAAO,uCAAuC,YAAY;AAAA,IAC1D,MAAM,4CAA4C,YAAY;AAAA,IAC9D,SAAS,yCAAyC,YAAY;AAAA,IAC9D,WAAW,2CAA2C,YAAY;AAAA,EACpE;AACF;AAEO,SAAS,uBACd,UACA,YACkB;AAClB,SAAO;AAAA,IACL,MAAM,SAAS;AAAA,IACf,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS;AAAA,IACf,eAAe,SAAS;AAAA,IACxB,aAAa,SAAS;AAAA,IACtB,cAAc,SAAS;AAAA,IACvB,qBAAqB,SAAS,qBAAqB,KAAK,QAAQ;AAAA,IAChE,OAAO,SAAS,MAAM,KAAK,QAAQ;AAAA,IACnC,cAAc,SAAS,aAAa,KAAK,QAAQ;AAAA,IACjD,kBAAkB,SAAS,iBAAiB,KAAK,QAAQ;AAAA,IACzD,QAAQ,SAAS,OAAO,KAAK,QAAQ;AAAA,IACrC;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,OAA2C;AAC5E,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,YAAY;AAClB,SAAO,OAAO,UAAU,SAAS,YAC5B,OAAO,UAAU,UAAU,YAC3B,OAAO,UAAU,SAAS,YAC1B,OAAO,UAAU,kBAAkB,aACnC,OAAO,UAAU,UAAU,cAC3B,OAAO,UAAU,iBAAiB,cAClC,OAAO,UAAU,qBAAqB,cACtC,OAAO,UAAU,WAAW;AACnC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -104,6 +104,8 @@ class OtpEmailProvider {
|
|
|
104
104
|
await sendEmail({
|
|
105
105
|
to: email,
|
|
106
106
|
subject: this.securityConfig.otpEmail.subject,
|
|
107
|
+
tenantId: method.tenantId,
|
|
108
|
+
organizationId: method.organizationId ?? null,
|
|
107
109
|
react: OtpCodeEmail({
|
|
108
110
|
code,
|
|
109
111
|
expiresInMinutes: Math.max(1, Math.ceil(challengeTtlMs / (60 * 1e3)))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/security/lib/providers/OtpEmailProvider.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomBytes } from 'node:crypto'\nimport { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport type {\n MfaMethodRecord,\n MfaProviderInterface,\n MfaProviderUser,\n MfaVerifyContext,\n} from '../mfa-provider-interface'\nimport { generateOtpCode, hashOtpCode, verifyOtpCode } from '../otp'\nimport OtpCodeEmail from '../../emails/otp-code'\nimport type { SecurityModuleConfig } from '../security-config'\nimport { readSecurityModuleConfig } from '../security-config'\n\nconst setupPayloadSchema = z.object({\n email: z.string().email().optional(),\n label: z.string().min(1).max(100).optional(),\n})\n\nconst confirmPayloadSchema = z.object({\n email: z.string().email().optional(),\n label: z.string().min(1).max(100).optional(),\n})\n\nconst verifyPayloadSchema = z.object({\n code: z.string().regex(/^\\d{6}$/),\n})\n\ntype PendingSetup = {\n userId: string\n email?: string\n label?: string\n createdAt: number\n}\n\ntype PendingOtpChallenge = {\n userId: string\n codeHash: string\n createdAt: number\n}\n\ntype OtpVerifyChallenge = {\n userId: string\n methodId: string\n codeHash: string\n createdAt: number\n}\n\nexport class OtpEmailProvider implements MfaProviderInterface {\n readonly type = 'otp_email'\n readonly label = 'Email OTP'\n readonly icon = 'Mail'\n readonly allowMultiple = false\n readonly setupSchema = setupPayloadSchema\n readonly verifySchema = verifyPayloadSchema\n\n private readonly pendingSetups = new Map<string, PendingSetup>()\n private readonly pendingChallenges = new Map<string, PendingOtpChallenge>()\n\n constructor(\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n resolveSetupPayload(user: MfaProviderUser, payload: unknown): unknown {\n const parsed = setupPayloadSchema.partial().parse(payload ?? {})\n if (parsed.email) {\n return parsed\n }\n\n const email = typeof user.email === 'string' ? user.email.trim() : ''\n if (email.length === 0) {\n throw new Error('Unable to configure Email OTP without a destination email')\n }\n\n return {\n ...parsed,\n email,\n }\n }\n\n async setup(userId: string, payload: unknown): Promise<{ setupId: string; clientData: Record<string, unknown> }> {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const setupId = randomBytes(16).toString('hex')\n const now = Date.now()\n this.cleanupExpiredSetups(now)\n this.pendingSetups.set(setupId, {\n userId,\n email: parsed.email,\n label: parsed.label,\n createdAt: now,\n })\n return {\n setupId,\n clientData: {\n channel: 'email',\n emailHint: this.maskEmail(parsed.email),\n },\n }\n }\n\n async confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n ): Promise<{ metadata: Record<string, unknown> }> {\n const parsed = confirmPayloadSchema.parse(payload ?? {})\n const pending = this.pendingSetups.get(setupId)\n if (!pending || pending.userId !== userId) {\n throw new Error('Email OTP setup session not found')\n }\n if (Date.now() - pending.createdAt > this.securityConfig.otpEmail.setupTtlMs) {\n this.pendingSetups.delete(setupId)\n throw new Error('Email OTP setup session expired')\n }\n this.pendingSetups.delete(setupId)\n const email = parsed.email ?? pending.email\n return {\n metadata: {\n email,\n label: parsed.label ?? pending.label ?? 'Email OTP',\n },\n }\n }\n\n async prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }> {\n if (method.userId !== userId) {\n throw new Error('MFA method does not belong to user')\n }\n const code = generateOtpCode()\n const codeHash = await hashOtpCode(code)\n const now = Date.now()\n this.cleanupExpiredChallenges(now)\n const challengeTtlMs = this.securityConfig.otpEmail.challengeTtlMs\n const challenge: OtpVerifyChallenge = {\n userId,\n methodId: method.id,\n codeHash,\n createdAt: now,\n }\n this.pendingChallenges.set(method.id, challenge)\n const emailValue = method.providerMetadata?.email\n const email = typeof emailValue === 'string' ? emailValue : undefined\n if (!email) {\n throw new Error('Email OTP method is missing a destination email address')\n }\n\n await sendEmail({\n to: email,\n subject: this.securityConfig.otpEmail.subject,\n react: OtpCodeEmail({\n code,\n expiresInMinutes: Math.max(1, Math.ceil(challengeTtlMs / (60 * 1000))),\n }),\n })\n\n const exposeCode = parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n return {\n clientData: {\n channel: 'email',\n emailHint: this.maskEmail(email),\n ...(exposeCode ? { code } : {}),\n expiresAt: new Date(now + challengeTtlMs).toISOString(),\n },\n verifyContext: {\n challenge,\n },\n }\n }\n\n async verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n ): Promise<boolean> {\n const parsed = verifyPayloadSchema.parse(payload)\n if (method.userId !== userId) return false\n const challenge = this.readChallengeContext(context, userId, method.id)\n ?? this.pendingChallenges.get(method.id)\n if (!challenge || challenge.userId !== userId) return false\n if (Date.now() - challenge.createdAt > this.securityConfig.otpEmail.challengeTtlMs) {\n this.pendingChallenges.delete(method.id)\n return false\n }\n const valid = await verifyOtpCode(parsed.code, challenge.codeHash)\n if (valid) {\n this.pendingChallenges.delete(method.id)\n }\n return valid\n }\n\n private cleanupExpiredSetups(now: number): void {\n for (const [setupId, setup] of this.pendingSetups.entries()) {\n if (now - setup.createdAt > this.securityConfig.otpEmail.setupTtlMs) {\n this.pendingSetups.delete(setupId)\n }\n }\n }\n\n private cleanupExpiredChallenges(now: number): void {\n for (const [methodId, challenge] of this.pendingChallenges.entries()) {\n if (now - challenge.createdAt > this.securityConfig.otpEmail.challengeTtlMs) {\n this.pendingChallenges.delete(methodId)\n }\n }\n }\n\n private maskEmail(email?: string): string | undefined {\n if (!email) return undefined\n const [localPart, domain] = email.split('@')\n if (!localPart || !domain) return undefined\n if (localPart.length <= 2) return `${localPart[0] ?? '*'}*@${domain}`\n return `${localPart.slice(0, 2)}***@${domain}`\n }\n\n private readChallengeContext(\n context: MfaVerifyContext | undefined,\n userId: string,\n methodId: string,\n ): OtpVerifyChallenge | null {\n const challenge = context?.challenge\n if (!challenge || typeof challenge !== 'object') return null\n\n const challengeUserId = challenge.userId\n const challengeMethodId = challenge.methodId\n const challengeCodeHash = challenge.codeHash\n const challengeCreatedAt = challenge.createdAt\n\n if (\n typeof challengeUserId !== 'string' ||\n typeof challengeMethodId !== 'string' ||\n typeof challengeCodeHash !== 'string' ||\n typeof challengeCreatedAt !== 'number'\n ) {\n return null\n }\n\n if (challengeUserId !== userId || challengeMethodId !== methodId) {\n return null\n }\n\n return {\n userId: challengeUserId,\n methodId: challengeMethodId,\n codeHash: challengeCodeHash,\n createdAt: challengeCreatedAt,\n }\n }\n}\n\nexport default OtpEmailProvider\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { randomBytes } from 'node:crypto'\nimport { z } from 'zod'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport type {\n MfaMethodRecord,\n MfaProviderInterface,\n MfaProviderUser,\n MfaVerifyContext,\n} from '../mfa-provider-interface'\nimport { generateOtpCode, hashOtpCode, verifyOtpCode } from '../otp'\nimport OtpCodeEmail from '../../emails/otp-code'\nimport type { SecurityModuleConfig } from '../security-config'\nimport { readSecurityModuleConfig } from '../security-config'\n\nconst setupPayloadSchema = z.object({\n email: z.string().email().optional(),\n label: z.string().min(1).max(100).optional(),\n})\n\nconst confirmPayloadSchema = z.object({\n email: z.string().email().optional(),\n label: z.string().min(1).max(100).optional(),\n})\n\nconst verifyPayloadSchema = z.object({\n code: z.string().regex(/^\\d{6}$/),\n})\n\ntype PendingSetup = {\n userId: string\n email?: string\n label?: string\n createdAt: number\n}\n\ntype PendingOtpChallenge = {\n userId: string\n codeHash: string\n createdAt: number\n}\n\ntype OtpVerifyChallenge = {\n userId: string\n methodId: string\n codeHash: string\n createdAt: number\n}\n\nexport class OtpEmailProvider implements MfaProviderInterface {\n readonly type = 'otp_email'\n readonly label = 'Email OTP'\n readonly icon = 'Mail'\n readonly allowMultiple = false\n readonly setupSchema = setupPayloadSchema\n readonly verifySchema = verifyPayloadSchema\n\n private readonly pendingSetups = new Map<string, PendingSetup>()\n private readonly pendingChallenges = new Map<string, PendingOtpChallenge>()\n\n constructor(\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n resolveSetupPayload(user: MfaProviderUser, payload: unknown): unknown {\n const parsed = setupPayloadSchema.partial().parse(payload ?? {})\n if (parsed.email) {\n return parsed\n }\n\n const email = typeof user.email === 'string' ? user.email.trim() : ''\n if (email.length === 0) {\n throw new Error('Unable to configure Email OTP without a destination email')\n }\n\n return {\n ...parsed,\n email,\n }\n }\n\n async setup(userId: string, payload: unknown): Promise<{ setupId: string; clientData: Record<string, unknown> }> {\n const parsed = setupPayloadSchema.parse(payload ?? {})\n const setupId = randomBytes(16).toString('hex')\n const now = Date.now()\n this.cleanupExpiredSetups(now)\n this.pendingSetups.set(setupId, {\n userId,\n email: parsed.email,\n label: parsed.label,\n createdAt: now,\n })\n return {\n setupId,\n clientData: {\n channel: 'email',\n emailHint: this.maskEmail(parsed.email),\n },\n }\n }\n\n async confirmSetup(\n userId: string,\n setupId: string,\n payload: unknown,\n ): Promise<{ metadata: Record<string, unknown> }> {\n const parsed = confirmPayloadSchema.parse(payload ?? {})\n const pending = this.pendingSetups.get(setupId)\n if (!pending || pending.userId !== userId) {\n throw new Error('Email OTP setup session not found')\n }\n if (Date.now() - pending.createdAt > this.securityConfig.otpEmail.setupTtlMs) {\n this.pendingSetups.delete(setupId)\n throw new Error('Email OTP setup session expired')\n }\n this.pendingSetups.delete(setupId)\n const email = parsed.email ?? pending.email\n return {\n metadata: {\n email,\n label: parsed.label ?? pending.label ?? 'Email OTP',\n },\n }\n }\n\n async prepareChallenge(\n userId: string,\n method: MfaMethodRecord,\n ): Promise<{ clientData?: Record<string, unknown>; verifyContext?: MfaVerifyContext }> {\n if (method.userId !== userId) {\n throw new Error('MFA method does not belong to user')\n }\n const code = generateOtpCode()\n const codeHash = await hashOtpCode(code)\n const now = Date.now()\n this.cleanupExpiredChallenges(now)\n const challengeTtlMs = this.securityConfig.otpEmail.challengeTtlMs\n const challenge: OtpVerifyChallenge = {\n userId,\n methodId: method.id,\n codeHash,\n createdAt: now,\n }\n this.pendingChallenges.set(method.id, challenge)\n const emailValue = method.providerMetadata?.email\n const email = typeof emailValue === 'string' ? emailValue : undefined\n if (!email) {\n throw new Error('Email OTP method is missing a destination email address')\n }\n\n await sendEmail({\n to: email,\n subject: this.securityConfig.otpEmail.subject,\n tenantId: method.tenantId,\n organizationId: method.organizationId ?? null,\n react: OtpCodeEmail({\n code,\n expiresInMinutes: Math.max(1, Math.ceil(challengeTtlMs / (60 * 1000))),\n }),\n })\n\n const exposeCode = parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n return {\n clientData: {\n channel: 'email',\n emailHint: this.maskEmail(email),\n ...(exposeCode ? { code } : {}),\n expiresAt: new Date(now + challengeTtlMs).toISOString(),\n },\n verifyContext: {\n challenge,\n },\n }\n }\n\n async verify(\n userId: string,\n method: MfaMethodRecord,\n payload: unknown,\n context?: MfaVerifyContext,\n ): Promise<boolean> {\n const parsed = verifyPayloadSchema.parse(payload)\n if (method.userId !== userId) return false\n const challenge = this.readChallengeContext(context, userId, method.id)\n ?? this.pendingChallenges.get(method.id)\n if (!challenge || challenge.userId !== userId) return false\n if (Date.now() - challenge.createdAt > this.securityConfig.otpEmail.challengeTtlMs) {\n this.pendingChallenges.delete(method.id)\n return false\n }\n const valid = await verifyOtpCode(parsed.code, challenge.codeHash)\n if (valid) {\n this.pendingChallenges.delete(method.id)\n }\n return valid\n }\n\n private cleanupExpiredSetups(now: number): void {\n for (const [setupId, setup] of this.pendingSetups.entries()) {\n if (now - setup.createdAt > this.securityConfig.otpEmail.setupTtlMs) {\n this.pendingSetups.delete(setupId)\n }\n }\n }\n\n private cleanupExpiredChallenges(now: number): void {\n for (const [methodId, challenge] of this.pendingChallenges.entries()) {\n if (now - challenge.createdAt > this.securityConfig.otpEmail.challengeTtlMs) {\n this.pendingChallenges.delete(methodId)\n }\n }\n }\n\n private maskEmail(email?: string): string | undefined {\n if (!email) return undefined\n const [localPart, domain] = email.split('@')\n if (!localPart || !domain) return undefined\n if (localPart.length <= 2) return `${localPart[0] ?? '*'}*@${domain}`\n return `${localPart.slice(0, 2)}***@${domain}`\n }\n\n private readChallengeContext(\n context: MfaVerifyContext | undefined,\n userId: string,\n methodId: string,\n ): OtpVerifyChallenge | null {\n const challenge = context?.challenge\n if (!challenge || typeof challenge !== 'object') return null\n\n const challengeUserId = challenge.userId\n const challengeMethodId = challenge.methodId\n const challengeCodeHash = challenge.codeHash\n const challengeCreatedAt = challenge.createdAt\n\n if (\n typeof challengeUserId !== 'string' ||\n typeof challengeMethodId !== 'string' ||\n typeof challengeCodeHash !== 'string' ||\n typeof challengeCreatedAt !== 'number'\n ) {\n return null\n }\n\n if (challengeUserId !== userId || challengeMethodId !== methodId) {\n return null\n }\n\n return {\n userId: challengeUserId,\n methodId: challengeMethodId,\n codeHash: challengeCodeHash,\n createdAt: challengeCreatedAt,\n }\n }\n}\n\nexport default OtpEmailProvider\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -63,6 +63,8 @@ class MfaVerificationService {
|
|
|
63
63
|
id: method.id,
|
|
64
64
|
type: method.type,
|
|
65
65
|
userId: method.userId,
|
|
66
|
+
tenantId: method.tenantId,
|
|
67
|
+
organizationId: method.organizationId ?? null,
|
|
66
68
|
secret: method.secret ?? null,
|
|
67
69
|
providerMetadata: method.providerMetadata
|
|
68
70
|
}, context);
|
|
@@ -95,6 +97,8 @@ class MfaVerificationService {
|
|
|
95
97
|
id: method.id,
|
|
96
98
|
type: method.type,
|
|
97
99
|
userId: method.userId,
|
|
100
|
+
tenantId: method.tenantId,
|
|
101
|
+
organizationId: method.organizationId ?? null,
|
|
98
102
|
secret: method.secret ?? null,
|
|
99
103
|
providerMetadata: method.providerMetadata
|
|
100
104
|
}, payload, context, runtimeContext);
|
|
@@ -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 type MfaVerificationAuthScope = {\n userId: string\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<boolean> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(\n challengeId: string,\n scope: MfaVerificationAuthScope,\n ): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, {\n id: challengeId,\n userId: scope.userId,\n })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,cAAc,qBAAqB;AAE5C,SAAS,yBAAyB;AAKlC,SAAS,gCAAgC;AAsBlC,MAAM,oCAAoC,MAAM;AAAA,EACrD,YACE,SACgB,YAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,uBAAuB;AAAA,EAClC,YACmB,IACA,qBACA,YACA,uBACA,iBAAuC,yBAAyB,GACjF;AALiB;AACA;AACA;AACA;AACA;AAAA,EAChB;AAAA,EAEH,MAAM,gBAAgB,QAAkD;AACtE,UAAM,UAAU,MAAM,KAAK,iBAAiB,MAAM;AAClD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,4BAA4B,6BAA6B,GAAG;AAAA,IACxE;AAEA,UAAM,YAAY,KAAK,GAAG,OAAO,cAAc;AAAA,MAC7C;AAAA,MACA,UAAU,QAAQ,CAAC,EAAE;AAAA,MACrB,WAAW,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,eAAe,IAAI,cAAc;AAAA,MACvE,UAAU;AAAA,MACV,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,GAAG,QAAQ,SAAS;AACzB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,mBAAmB,QACtB,IAAI,CAAC,WAAW;AACf,YAAM,WAAW,KAAK,oBAAoB,IAAI,OAAO,IAAI;AACzD,UAAI,CAAC,SAAU,QAAO;AACtB,aAAO;AAAA,QACL,MAAM,SAAS;AAAA,QACf,OAAO,SAAS;AAAA,QAChB,MAAM,SAAS;AAAA,QACf,GAAI,SAAS,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC;AAAA,MACnE;AAAA,IACF,CAAC,EACA,OAAO,CAAC,SAAkC,SAAS,IAAI;AAC1D,QAAI,iBAAiB,WAAW,GAAG;AACjC,YAAM,IAAI,4BAA4B,wEAAwE,GAAG;AAAA,IACnH;AAEA,WAAO;AAAA,MACL,aAAa,UAAU;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AAAA,EAmBA,MAAM,iBACJ,aACA,YACA,SACA,OACmD;AACnD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,4BAA4B,2BAA2B,GAAG;AAAA,IACtE;AACA,UAAM,YAAY,MAAM,KAAK,kBAAkB,aAAa,KAAK;AACjE,UAAM,KAAK,4BAA4B,UAAU,QAAQ,UAAU;AACnE,UAAM,WAAW,KAAK,oBAAoB,IAAI,UAAU;AACxD,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,4BAA4B,iBAAiB,UAAU,uBAAuB,GAAG;AAAA,IAC7F;AAEA,UAAM,SAAS,MAAM,KAAK,WAAW,UAAU,QAAQ,UAAU;AACjE,UAAM,SAAS,MAAM,SAAS,iBAAiB,UAAU,QAAQ;AAAA,MAC/D,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,UAAU;AAAA,MACzB,kBAAkB,OAAO;AAAA,IAC3B,GAAG,OAAO;AAEV,cAAU,aAAa;AACvB,cAAU,WAAW,OAAO;AAC5B,cAAU,oBAAoB,OAAO,eAAe,aAAa;AACjE,UAAM,KAAK,GAAG,MAAM;AACpB,WAAO;AAAA,EACT;AAAA,EAqBA,MAAM,gBACJ,aACA,YACA,SACA,gBACA,OACkB;AAClB,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,4BAA4B,2BAA2B,GAAG;AAAA,IACtE;AACA,UAAM,YAAY,MAAM,KAAK,kBAAkB,aAAa,KAAK;AACjE,QAAI,UAAU,YAAY,KAAK,eAAe,IAAI,aAAa;AAC7D,aAAO;AAAA,IACT;AAEA,UAAM,KAAK,4BAA4B,UAAU,QAAQ,UAAU;AAEnE,QAAI,UAAU,cAAc,UAAU,eAAe,YAAY;AAC/D,YAAM,KAAK,sBAAsB,SAAS;AAC1C,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,KAAK,oBAAoB,IAAI,UAAU;AACxD,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,4BAA4B,iBAAiB,UAAU,uBAAuB,GAAG;AAAA,IAC7F;AAEA,UAAM,SAAS,UAAU,WACrB,MAAM,KAAK,eAAe,UAAU,QAAQ,UAAU,QAAQ,IAC9D,MAAM,KAAK,WAAW,UAAU,QAAQ,UAAU;AACtD,UAAM,UAAwC,UAAU,oBACpD,EAAE,WAAW,UAAU,kBAAkB,IACzC;AACJ,UAAM,WAAW,MAAM,SAAS,OAAO,UAAU,QAAQ;AAAA,MACvD,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,UAAU;AAAA,MACzB,kBAAkB,OAAO;AAAA,IAC3B,GAAG,SAAS,SAAS,cAAc;AAEnC,QAAI,UAAU;AACZ,gBAAU,aAAa,oBAAI,KAAK;AAChC,gBAAU,aAAa;AACvB,aAAO,aAAa,oBAAI,KAAK;AAC7B,YAAM,KAAK,GAAG,MAAM;AACpB,YAAM,kBAAkB,yBAAyB;AAAA,QAC/C,QAAQ,UAAU;AAAA,QAClB,aAAa,UAAU;AAAA,QACvB;AAAA,MACF,CAAC;AACD,aAAO;AAAA,IACT;AAEA,UAAM,KAAK,sBAAsB,SAAS;AAC1C,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,QAAgB,MAAgC;AACvE,WAAO,KAAK,WAAW,mBAAmB,QAAQ,IAAI;AAAA,EACxD;AAAA,EAEA,MAAc,kBACZ,aACA,OACuB;AACvB,UAAM,YAAY,MAAM,KAAK,GAAG,QAAQ,cAAc;AAAA,MACpD,IAAI;AAAA,MACJ,QAAQ,MAAM;AAAA,IAChB,CAAC;AACD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,4BAA4B,2BAA2B,GAAG;AAAA,IACtE;AACA,QAAI,UAAU,YAAY;AACxB,YAAM,IAAI,4BAA4B,kCAAkC,GAAG;AAAA,IAC7E;AACA,QAAI,UAAU,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAG;AAC/C,YAAM,IAAI,4BAA4B,yBAAyB,GAAG;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,4BAA4B,QAAgB,YAAmC;AAC3F,UAAM,SAAS,MAAM,KAAK,sBAAsB,0BAA0B,MAAM;AAChF,QAAI,CAAC,QAAQ,cAAc,CAAC,OAAO,gBAAgB,QAAQ;AACzD;AAAA,IACF;AACA,QAAI,CAAC,OAAO,eAAe,SAAS,UAAU,GAAG;AAC/C,YAAM,IAAI,4BAA4B,eAAe,UAAU,8CAA8C,GAAG;AAAA,IAClH;AAAA,EACF;AAAA,EAEA,MAAc,sBAAsB,WAAwC;AAC1E,UAAM,cAAc,KAAK,eAAe,IAAI;AAC5C,UAAM,OAAO,MAAM,KAAK,GAAG,cAAc,EAAE;AAAA,MACzC;AAAA,MACA,CAAC,UAAU,IAAI,WAAW;AAAA,IAC5B;AACA,UAAM,kBAAkB,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,EAAE,QAAQ,IAAI;AACrE,cAAU,WAAW;AACrB,QAAI,mBAAmB,aAAa;AAClC,YAAM,MAAM,oBAAI,KAAK;AACrB,YAAM,KAAK,GAAG,cAAc,EAAE;AAAA,QAC5B;AAAA,QACA,CAAC,KAAK,UAAU,EAAE;AAAA,MACpB;AACA,gBAAU,YAAY;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,MAAc,iBAAiB,QAA0C;AACvE,UAAM,UAAU,MAAM,KAAK,GAAG;AAAA,MAC5B;AAAA,MACA;AAAA,QACE;AAAA,QACA,UAAU;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA;AAAA,QACE,SAAS,EAAE,WAAW,MAAM;AAAA,MAC9B;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,KAAK,sBAAsB,0BAA0B,MAAM;AAChF,QAAI,CAAC,QAAQ,cAAc,CAAC,OAAO,gBAAgB,QAAQ;AACzD,aAAO;AAAA,IACT;AAEA,WAAO,QAAQ,OAAO,CAAC,WAAW,OAAO,gBAAgB,SAAS,OAAO,IAAI,CAAC;AAAA,EAChF;AAAA,EAEA,MAAc,WAAW,QAAgB,YAA4C;AACnF,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,MAClD;AAAA,MACA,MAAM;AAAA,MACN,UAAU;AAAA,MACV,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,4BAA4B,eAAe,UAAU,eAAe,GAAG;AAAA,IACnF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,eAAe,QAAgB,UAA0C;AACrF,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,eAAe;AAAA,MAClD,IAAI;AAAA,MACJ;AAAA,MACA,UAAU;AAAA,MACV,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,4BAA4B,eAAe,QAAQ,eAAe,GAAG;AAAA,IACjF;AACA,WAAO;AAAA,EACT;AACF;AAEA,IAAO,iCAAQ;",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { MfaChallenge, UserMfaMethod } from '../data/entities'\nimport type { MfaProviderRegistry } from '../lib/mfa-provider-registry'\nimport { emitSecurityEvent } from '../events'\nimport type { MfaService } from './MfaService'\nimport type { MfaEnforcementService } from './MfaEnforcementService'\nimport type { MfaProviderRuntimeContext, MfaVerifyContext } from '../lib/mfa-provider-interface'\nimport type { SecurityModuleConfig } from '../lib/security-config'\nimport { readSecurityModuleConfig } from '../lib/security-config'\n\ntype AvailableMethod = {\n type: string\n label: string\n icon: string\n components?: {\n list?: string\n details?: string\n challenge?: string\n }\n}\n\ntype ChallengeCreationResult = {\n challengeId: string\n availableMethods: AvailableMethod[]\n}\n\nexport type MfaVerificationAuthScope = {\n userId: string\n}\n\nexport class MfaVerificationServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaVerificationServiceError'\n }\n}\n\nexport class MfaVerificationService {\n constructor(\n private readonly em: EntityManager,\n private readonly mfaProviderRegistry: MfaProviderRegistry,\n private readonly mfaService: MfaService,\n private readonly mfaEnforcementService: MfaEnforcementService,\n private readonly securityConfig: SecurityModuleConfig = readSecurityModuleConfig(),\n ) {}\n\n async createChallenge(userId: string): Promise<ChallengeCreationResult> {\n const methods = await this.getActiveMethods(userId)\n if (methods.length === 0) {\n throw new MfaVerificationServiceError('No MFA methods configured', 400)\n }\n\n const challenge = this.em.create(MfaChallenge, {\n userId,\n tenantId: methods[0].tenantId,\n expiresAt: new Date(Date.now() + this.securityConfig.mfa.challengeTtlMs),\n attempts: 0,\n createdAt: new Date(),\n })\n this.em.persist(challenge)\n await this.em.flush()\n\n const availableMethods = methods\n .map((method) => {\n const provider = this.mfaProviderRegistry.get(method.type)\n if (!provider) return null\n return {\n type: provider.type,\n label: provider.label,\n icon: provider.icon,\n ...(provider.components ? { components: provider.components } : {}),\n }\n })\n .filter((item): item is AvailableMethod => item !== null)\n if (availableMethods.length === 0) {\n throw new MfaVerificationServiceError('No registered MFA providers are available for the configured methods', 400)\n }\n\n return {\n challengeId: challenge.id,\n availableMethods,\n }\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }>\n async prepareChallenge(\n challengeId: string,\n methodType: string,\n context?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<{ clientData?: Record<string, unknown> }> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = await this.findMethod(challenge.userId, methodType)\n const result = await provider.prepareChallenge(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n tenantId: method.tenantId,\n organizationId: method.organizationId ?? null,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, context)\n\n challenge.methodType = methodType\n challenge.methodId = method.id\n challenge.providerChallenge = result.verifyContext?.challenge ?? null\n await this.em.flush()\n return result\n }\n\n /**\n * @deprecated Since 0.6 \u2014 pass an {@link MfaVerificationAuthScope} bound to the\n * authenticated user (`auth.sub`). The no-scope overload now treats the caller as\n * unknown and rejects every challenge lookup with 404; it will be removed in a\n * future release.\n */\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext: MfaProviderRuntimeContext | undefined,\n scope: MfaVerificationAuthScope,\n ): Promise<boolean>\n async verifyChallenge(\n challengeId: string,\n methodType: string,\n payload: unknown,\n runtimeContext?: MfaProviderRuntimeContext,\n scope?: MfaVerificationAuthScope,\n ): Promise<boolean> {\n if (!scope) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n const challenge = await this.getValidChallenge(challengeId, scope)\n if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {\n return false\n }\n\n await this.assertMethodAllowedByPolicy(challenge.userId, methodType)\n\n if (challenge.methodType && challenge.methodType !== methodType) {\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n const provider = this.mfaProviderRegistry.get(methodType)\n if (!provider) {\n throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400)\n }\n\n const method = challenge.methodId\n ? await this.findMethodById(challenge.userId, challenge.methodId)\n : await this.findMethod(challenge.userId, methodType)\n const context: MfaVerifyContext | undefined = challenge.providerChallenge\n ? { challenge: challenge.providerChallenge }\n : undefined\n const verified = await provider.verify(challenge.userId, {\n id: method.id,\n type: method.type,\n userId: method.userId,\n tenantId: method.tenantId,\n organizationId: method.organizationId ?? null,\n secret: method.secret ?? null,\n providerMetadata: method.providerMetadata,\n }, payload, context, runtimeContext)\n\n if (verified) {\n challenge.verifiedAt = new Date()\n challenge.methodType = methodType\n method.lastUsedAt = new Date()\n await this.em.flush()\n await emitSecurityEvent('security.mfa.verified', {\n userId: challenge.userId,\n challengeId: challenge.id,\n methodType,\n })\n return true\n }\n\n await this.registerFailedAttempt(challenge)\n return false\n }\n\n async verifyRecoveryCode(userId: string, code: string): Promise<boolean> {\n return this.mfaService.verifyRecoveryCode(userId, code)\n }\n\n private async getValidChallenge(\n challengeId: string,\n scope: MfaVerificationAuthScope,\n ): Promise<MfaChallenge> {\n const challenge = await this.em.findOne(MfaChallenge, {\n id: challengeId,\n userId: scope.userId,\n })\n if (!challenge) {\n throw new MfaVerificationServiceError('MFA challenge not found', 404)\n }\n if (challenge.verifiedAt) {\n throw new MfaVerificationServiceError('MFA challenge already verified', 400)\n }\n if (challenge.expiresAt.getTime() <= Date.now()) {\n throw new MfaVerificationServiceError('MFA challenge expired', 400)\n }\n return challenge\n }\n\n private async assertMethodAllowedByPolicy(userId: string, methodType: string): Promise<void> {\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return\n }\n if (!policy.allowedMethods.includes(methodType)) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' is not allowed by the enforcement policy`, 403)\n }\n }\n\n private async registerFailedAttempt(challenge: MfaChallenge): Promise<void> {\n const maxAttempts = this.securityConfig.mfa.maxAttempts\n const rows = await this.em.getConnection().execute<Array<{ attempts: number }>>(\n 'UPDATE mfa_challenges SET attempts = attempts + 1 WHERE id = ? AND verified_at IS NULL AND attempts < ? RETURNING attempts',\n [challenge.id, maxAttempts],\n )\n const updatedAttempts = rows.length > 0 ? Number(rows[0].attempts) : maxAttempts\n challenge.attempts = updatedAttempts\n if (updatedAttempts >= maxAttempts) {\n const now = new Date()\n await this.em.getConnection().execute(\n 'UPDATE mfa_challenges SET expires_at = ? WHERE id = ?',\n [now, challenge.id],\n )\n challenge.expiresAt = now\n }\n }\n\n private async getActiveMethods(userId: string): Promise<UserMfaMethod[]> {\n const methods = await this.em.find(\n UserMfaMethod,\n {\n userId,\n isActive: true,\n deletedAt: null,\n },\n {\n orderBy: { createdAt: 'asc' },\n },\n )\n\n const policy = await this.mfaEnforcementService.getEffectivePolicyForUser(userId)\n if (!policy?.isEnforced || !policy.allowedMethods?.length) {\n return methods\n }\n\n return methods.filter((method) => policy.allowedMethods?.includes(method.type))\n }\n\n private async findMethod(userId: string, methodType: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n userId,\n type: methodType,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodType}' not found`, 404)\n }\n return method\n }\n\n private async findMethodById(userId: string, methodId: string): Promise<UserMfaMethod> {\n const method = await this.em.findOne(UserMfaMethod, {\n id: methodId,\n userId,\n isActive: true,\n deletedAt: null,\n })\n if (!method) {\n throw new MfaVerificationServiceError(`MFA method '${methodId}' not found`, 404)\n }\n return method\n }\n}\n\nexport default MfaVerificationService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -148,6 +148,8 @@ async function notifyEnforcementPolicyChange(payload, ctx) {
|
|
|
148
148
|
await sendEmail({
|
|
149
149
|
to: user.email,
|
|
150
150
|
subject: resolveEmailSubject(message),
|
|
151
|
+
tenantId: user.tenantId,
|
|
152
|
+
organizationId: user.organizationId,
|
|
151
153
|
react: EnforcementDeadlineEmail({
|
|
152
154
|
daysRemaining: message.daysRemaining,
|
|
153
155
|
deadlineIsoDate: message.deadlineIsoDate,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/subscribers/enforcement-deadline-notification.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport EnforcementDeadlineEmail from '../emails/enforcement-deadline'\nimport { MfaEnforcementPolicy, UserMfaMethod } from '../data/entities'\nimport { notificationTypes } from '../notifications'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst ENFORCEMENT_BODY_KEYS = {\n withDeadline: 'security.notifications.enforcementDeadline.bodyWithDeadline',\n immediate: 'security.notifications.enforcementDeadline.bodyImmediate',\n overdue: 'security.notifications.enforcementDeadline.bodyOverdue',\n} as const\n\nexport const metadata = {\n event: 'security.enforcement.created',\n persistent: true,\n id: 'security:enforcement-deadline-notification',\n}\n\ntype EnforcementLifecyclePayload = {\n policyId: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\ntype EnforcementMessage = {\n bodyKey: string\n bodyVariables: Record<string, string> | undefined\n deadlineIsoDate: string | null\n daysRemaining: number | null\n emailMode: 'deadline' | 'immediate' | 'overdue'\n}\n\nfunction toIsoDate(value: Date): string {\n return value.toISOString().slice(0, 10)\n}\n\nfunction resolveSetupUrl(): string {\n const appUrl = process.env.APP_URL?.trim()\n if (!appUrl) return '/backend/profile/security/mfa'\n return `${appUrl.replace(/\\/$/, '')}/backend/profile/security/mfa`\n}\n\nasync function resolveScopedUsers(\n em: EntityManager,\n policy: MfaEnforcementPolicy,\n): Promise<ResolvedUser[]> {\n const users = await em.find(User, {\n deletedAt: null,\n ...(policy.tenantId ? { tenantId: policy.tenantId } : {}),\n ...(policy.organizationId ? { organizationId: policy.organizationId } : {}),\n })\n\n const resolvedUsers: ResolvedUser[] = []\n for (const user of users) {\n if (!user.tenantId) continue\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) continue\n resolvedUsers.push({\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n })\n }\n\n return resolvedUsers\n}\n\nfunction resolveEnforcementMessage(deadline?: Date | null): EnforcementMessage {\n if (!(deadline instanceof Date) || Number.isNaN(deadline.getTime())) {\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.immediate,\n bodyVariables: undefined,\n deadlineIsoDate: null,\n daysRemaining: null,\n emailMode: 'immediate',\n }\n }\n\n const daysRemaining = Math.ceil((deadline.getTime() - Date.now()) / DAY_MS)\n const deadlineIsoDate = toIsoDate(deadline)\n if (daysRemaining <= 0) {\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.overdue,\n bodyVariables: {\n deadline: deadlineIsoDate,\n },\n deadlineIsoDate,\n daysRemaining: null,\n emailMode: 'overdue',\n }\n }\n\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.withDeadline,\n bodyVariables: {\n days: String(daysRemaining),\n deadline: deadlineIsoDate,\n },\n deadlineIsoDate,\n daysRemaining,\n emailMode: 'deadline',\n }\n}\n\nfunction resolveEmailSubject(message: EnforcementMessage): string {\n if (message.emailMode === 'deadline' && message.daysRemaining !== null) {\n return `MFA enrollment required within ${message.daysRemaining} day${message.daysRemaining === 1 ? '' : 's'}`\n }\n if (message.emailMode === 'overdue') {\n return 'MFA enrollment deadline has passed'\n }\n return 'MFA enrollment required immediately'\n}\n\nexport async function notifyEnforcementPolicyChange(\n payload: EnforcementLifecyclePayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.enforcement_deadline')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const notificationService = resolveNotificationService(ctx)\n const policy = await em.findOne(MfaEnforcementPolicy, {\n id: payload.policyId,\n deletedAt: null,\n })\n if (!policy?.isEnforced) return\n\n const users = await resolveScopedUsers(em, policy)\n if (users.length === 0) return\n\n const methods = await em.find(UserMfaMethod, {\n userId: { $in: users.map((user) => user.id) },\n isActive: true,\n deletedAt: null,\n ...(policy.allowedMethods && policy.allowedMethods.length > 0\n ? { type: { $in: policy.allowedMethods } }\n : {}),\n })\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n\n const message = resolveEnforcementMessage(policy.enforcementDeadline)\n const setupUrl = resolveSetupUrl()\n for (const user of users) {\n if (enrolledUserIds.has(user.id)) continue\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: message.bodyVariables,\n sourceEntityType: 'security:mfa_enforcement_policy',\n sourceEntityId: policy.id,\n linkHref: '/backend/profile/security/mfa',\n groupKey: `security.mfa.enforcement_deadline:${policy.id}`,\n })\n\n await notificationService.create(\n {\n ...notificationInput,\n bodyKey: message.bodyKey,\n body: message.bodyKey,\n },\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: resolveEmailSubject(message),\n react: EnforcementDeadlineEmail({\n daysRemaining: message.daysRemaining,\n deadlineIsoDate: message.deadlineIsoDate,\n setupUrl,\n }),\n })\n }\n }\n}\n\nexport default async function enforcementDeadlineNotificationSubscriber(\n payload: EnforcementLifecyclePayload,\n ctx: ResolverContext,\n) {\n await notifyEnforcementPolicyChange(payload, ctx)\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport EnforcementDeadlineEmail from '../emails/enforcement-deadline'\nimport { MfaEnforcementPolicy, UserMfaMethod } from '../data/entities'\nimport { notificationTypes } from '../notifications'\n\nconst DAY_MS = 24 * 60 * 60 * 1000\nconst ENFORCEMENT_BODY_KEYS = {\n withDeadline: 'security.notifications.enforcementDeadline.bodyWithDeadline',\n immediate: 'security.notifications.enforcementDeadline.bodyImmediate',\n overdue: 'security.notifications.enforcementDeadline.bodyOverdue',\n} as const\n\nexport const metadata = {\n event: 'security.enforcement.created',\n persistent: true,\n id: 'security:enforcement-deadline-notification',\n}\n\ntype EnforcementLifecyclePayload = {\n policyId: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\ntype EnforcementMessage = {\n bodyKey: string\n bodyVariables: Record<string, string> | undefined\n deadlineIsoDate: string | null\n daysRemaining: number | null\n emailMode: 'deadline' | 'immediate' | 'overdue'\n}\n\nfunction toIsoDate(value: Date): string {\n return value.toISOString().slice(0, 10)\n}\n\nfunction resolveSetupUrl(): string {\n const appUrl = process.env.APP_URL?.trim()\n if (!appUrl) return '/backend/profile/security/mfa'\n return `${appUrl.replace(/\\/$/, '')}/backend/profile/security/mfa`\n}\n\nasync function resolveScopedUsers(\n em: EntityManager,\n policy: MfaEnforcementPolicy,\n): Promise<ResolvedUser[]> {\n const users = await em.find(User, {\n deletedAt: null,\n ...(policy.tenantId ? { tenantId: policy.tenantId } : {}),\n ...(policy.organizationId ? { organizationId: policy.organizationId } : {}),\n })\n\n const resolvedUsers: ResolvedUser[] = []\n for (const user of users) {\n if (!user.tenantId) continue\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) continue\n resolvedUsers.push({\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n })\n }\n\n return resolvedUsers\n}\n\nfunction resolveEnforcementMessage(deadline?: Date | null): EnforcementMessage {\n if (!(deadline instanceof Date) || Number.isNaN(deadline.getTime())) {\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.immediate,\n bodyVariables: undefined,\n deadlineIsoDate: null,\n daysRemaining: null,\n emailMode: 'immediate',\n }\n }\n\n const daysRemaining = Math.ceil((deadline.getTime() - Date.now()) / DAY_MS)\n const deadlineIsoDate = toIsoDate(deadline)\n if (daysRemaining <= 0) {\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.overdue,\n bodyVariables: {\n deadline: deadlineIsoDate,\n },\n deadlineIsoDate,\n daysRemaining: null,\n emailMode: 'overdue',\n }\n }\n\n return {\n bodyKey: ENFORCEMENT_BODY_KEYS.withDeadline,\n bodyVariables: {\n days: String(daysRemaining),\n deadline: deadlineIsoDate,\n },\n deadlineIsoDate,\n daysRemaining,\n emailMode: 'deadline',\n }\n}\n\nfunction resolveEmailSubject(message: EnforcementMessage): string {\n if (message.emailMode === 'deadline' && message.daysRemaining !== null) {\n return `MFA enrollment required within ${message.daysRemaining} day${message.daysRemaining === 1 ? '' : 's'}`\n }\n if (message.emailMode === 'overdue') {\n return 'MFA enrollment deadline has passed'\n }\n return 'MFA enrollment required immediately'\n}\n\nexport async function notifyEnforcementPolicyChange(\n payload: EnforcementLifecyclePayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.enforcement_deadline')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const notificationService = resolveNotificationService(ctx)\n const policy = await em.findOne(MfaEnforcementPolicy, {\n id: payload.policyId,\n deletedAt: null,\n })\n if (!policy?.isEnforced) return\n\n const users = await resolveScopedUsers(em, policy)\n if (users.length === 0) return\n\n const methods = await em.find(UserMfaMethod, {\n userId: { $in: users.map((user) => user.id) },\n isActive: true,\n deletedAt: null,\n ...(policy.allowedMethods && policy.allowedMethods.length > 0\n ? { type: { $in: policy.allowedMethods } }\n : {}),\n })\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n\n const message = resolveEnforcementMessage(policy.enforcementDeadline)\n const setupUrl = resolveSetupUrl()\n for (const user of users) {\n if (enrolledUserIds.has(user.id)) continue\n\n const notificationInput = buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: message.bodyVariables,\n sourceEntityType: 'security:mfa_enforcement_policy',\n sourceEntityId: policy.id,\n linkHref: '/backend/profile/security/mfa',\n groupKey: `security.mfa.enforcement_deadline:${policy.id}`,\n })\n\n await notificationService.create(\n {\n ...notificationInput,\n bodyKey: message.bodyKey,\n body: message.bodyKey,\n },\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: resolveEmailSubject(message),\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n react: EnforcementDeadlineEmail({\n daysRemaining: message.daysRemaining,\n deadlineIsoDate: message.deadlineIsoDate,\n setupUrl,\n }),\n })\n }\n }\n}\n\nexport default async function enforcementDeadlineNotificationSubscriber(\n payload: EnforcementLifecyclePayload,\n ctx: ResolverContext,\n) {\n await notifyEnforcementPolicyChange(payload, ctx)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -64,6 +64,8 @@ async function mfaResetNotificationSubscriber(payload, ctx) {
|
|
|
64
64
|
await sendEmail({
|
|
65
65
|
to: user.email,
|
|
66
66
|
subject: "Your MFA methods were reset",
|
|
67
|
+
tenantId: user.tenantId,
|
|
68
|
+
organizationId: user.organizationId,
|
|
67
69
|
react: MfaResetEmail({
|
|
68
70
|
reason,
|
|
69
71
|
resetAtIso
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/subscribers/mfa-reset-notification.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport MfaResetEmail from '../emails/mfa-reset'\nimport { notificationTypes } from '../notifications'\n\nexport const metadata = {\n event: 'security.mfa.reset',\n persistent: true,\n id: 'security:mfa-reset-notification',\n}\n\ntype MfaResetPayload = {\n targetUserId: string\n reason: string\n resetAt?: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\nasync function resolveUserById(em: EntityManager, userId: string): Promise<ResolvedUser | null> {\n const user = await em.findOne(User, { id: userId, deletedAt: null })\n if (!user?.tenantId) return null\n\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) return null\n\n return {\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n }\n}\n\nexport default async function mfaResetNotificationSubscriber(\n payload: MfaResetPayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.reset')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const user = await resolveUserById(em, payload.targetUserId)\n if (!user) return\n\n const notificationService = resolveNotificationService(ctx)\n const resetAtIso = payload.resetAt ?? new Date().toISOString()\n const reason = payload.reason.trim() || 'No reason provided'\n\n await notificationService.create(\n buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: {\n reason,\n },\n sourceEntityType: 'security:mfa_reset',\n sourceEntityId: user.id,\n linkHref: '/backend/profile/security/mfa',\n }),\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: 'Your MFA methods were reset',\n react: MfaResetEmail({\n reason,\n resetAtIso,\n }),\n })\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,iBAAiB;AAC1B,OAAO,mBAAmB;AAC1B,SAAS,yBAAyB;AAE3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAmBA,eAAe,gBAAgB,IAAmB,QAA8C;AAC9F,QAAM,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,QAAQ,WAAW,KAAK,CAAC;AACnE,MAAI,CAAC,MAAM,SAAU,QAAO;AAE5B,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,IACzC;AAAA,EACF;AACA,MAAI,CAAC,WAAW,SAAU,QAAO;AAEjC,SAAO;AAAA,IACL,IAAI,UAAU;AAAA,IACd,OAAO,OAAO,UAAU,UAAU,WAAW,UAAU,QAAQ;AAAA,IAC/D,UAAU,UAAU;AAAA,IACpB,gBAAgB,UAAU,kBAAkB;AAAA,EAC9C;AACF;AAEA,eAAO,+BACL,SACA,KACA;AACA,QAAM,UAAU,kBAAkB,KAAK,CAAC,SAAS,KAAK,SAAS,oBAAoB;AACnF,MAAI,CAAC,QAAS;AAEd,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,OAAO,MAAM,gBAAgB,IAAI,QAAQ,YAAY;AAC3D,MAAI,CAAC,KAAM;AAEX,QAAM,sBAAsB,2BAA2B,GAAG;AAC1D,QAAM,aAAa,QAAQ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAC7D,QAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;AAExC,QAAM,oBAAoB;AAAA,IACxB,0BAA0B,SAAS;AAAA,MACjC,iBAAiB,KAAK;AAAA,MACtB,eAAe;AAAA,QACb;AAAA,MACF;AAAA,MACA,kBAAkB;AAAA,MAClB,gBAAgB,KAAK;AAAA,MACrB,UAAU;AAAA,IACZ,CAAC;AAAA,IACD;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,KAAK,OAAO;AACd,UAAM,UAAU;AAAA,MACd,IAAI,KAAK;AAAA,MACT,SAAS;AAAA,MACT,OAAO,cAAc;AAAA,QACnB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport MfaResetEmail from '../emails/mfa-reset'\nimport { notificationTypes } from '../notifications'\n\nexport const metadata = {\n event: 'security.mfa.reset',\n persistent: true,\n id: 'security:mfa-reset-notification',\n}\n\ntype MfaResetPayload = {\n targetUserId: string\n reason: string\n resetAt?: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\nasync function resolveUserById(em: EntityManager, userId: string): Promise<ResolvedUser | null> {\n const user = await em.findOne(User, { id: userId, deletedAt: null })\n if (!user?.tenantId) return null\n\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) return null\n\n return {\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n }\n}\n\nexport default async function mfaResetNotificationSubscriber(\n payload: MfaResetPayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.reset')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const user = await resolveUserById(em, payload.targetUserId)\n if (!user) return\n\n const notificationService = resolveNotificationService(ctx)\n const resetAtIso = payload.resetAt ?? new Date().toISOString()\n const reason = payload.reason.trim() || 'No reason provided'\n\n await notificationService.create(\n buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: {\n reason,\n },\n sourceEntityType: 'security:mfa_reset',\n sourceEntityId: user.id,\n linkHref: '/backend/profile/security/mfa',\n }),\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: 'Your MFA methods were reset',\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n react: MfaResetEmail({\n reason,\n resetAtIso,\n }),\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,iBAAiB;AAC1B,OAAO,mBAAmB;AAC1B,SAAS,yBAAyB;AAE3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAmBA,eAAe,gBAAgB,IAAmB,QAA8C;AAC9F,QAAM,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,QAAQ,WAAW,KAAK,CAAC;AACnE,MAAI,CAAC,MAAM,SAAU,QAAO;AAE5B,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,IACzC;AAAA,EACF;AACA,MAAI,CAAC,WAAW,SAAU,QAAO;AAEjC,SAAO;AAAA,IACL,IAAI,UAAU;AAAA,IACd,OAAO,OAAO,UAAU,UAAU,WAAW,UAAU,QAAQ;AAAA,IAC/D,UAAU,UAAU;AAAA,IACpB,gBAAgB,UAAU,kBAAkB;AAAA,EAC9C;AACF;AAEA,eAAO,+BACL,SACA,KACA;AACA,QAAM,UAAU,kBAAkB,KAAK,CAAC,SAAS,KAAK,SAAS,oBAAoB;AACnF,MAAI,CAAC,QAAS;AAEd,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,OAAO,MAAM,gBAAgB,IAAI,QAAQ,YAAY;AAC3D,MAAI,CAAC,KAAM;AAEX,QAAM,sBAAsB,2BAA2B,GAAG;AAC1D,QAAM,aAAa,QAAQ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAC7D,QAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;AAExC,QAAM,oBAAoB;AAAA,IACxB,0BAA0B,SAAS;AAAA,MACjC,iBAAiB,KAAK;AAAA,MACtB,eAAe;AAAA,QACb;AAAA,MACF;AAAA,MACA,kBAAkB;AAAA,MAClB,gBAAgB,KAAK;AAAA,MACrB,UAAU;AAAA,IACZ,CAAC;AAAA,IACD;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,KAAK,OAAO;AACd,UAAM,UAAU;AAAA,MACd,IAAI,KAAK;AAAA,MACT,SAAS;AAAA,MACT,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,OAAO,cAAc;AAAA,QACnB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -72,6 +72,8 @@ async function notificationSubscriber(payload, ctx) {
|
|
|
72
72
|
await sendEmail({
|
|
73
73
|
to: user.email,
|
|
74
74
|
subject: "Multi-factor authentication enabled",
|
|
75
|
+
tenantId: user.tenantId,
|
|
76
|
+
organizationId: user.organizationId,
|
|
75
77
|
react: MfaEnrolledEmail({
|
|
76
78
|
methodLabel,
|
|
77
79
|
enrolledAtIso
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/subscribers/notification.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport MfaEnrolledEmail from '../emails/mfa-enrolled'\nimport { notificationTypes } from '../notifications'\n\nexport const metadata = {\n event: 'security.mfa.enrolled',\n persistent: true,\n id: 'security:mfa-enrolled-notification',\n}\n\ntype MfaEnrolledPayload = {\n userId: string\n methodId: string\n methodType: string\n enrolledAt?: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\nconst METHOD_LABELS: Record<string, string> = {\n totp: 'Authenticator app',\n passkey: 'Passkey',\n otp_email: 'Email OTP',\n}\n\nfunction resolveMethodLabel(methodType: string): string {\n return METHOD_LABELS[methodType] ?? methodType\n}\n\nasync function resolveUserById(em: EntityManager, userId: string): Promise<ResolvedUser | null> {\n const user = await em.findOne(User, { id: userId, deletedAt: null })\n if (!user?.tenantId) return null\n\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) return null\n\n return {\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n }\n}\n\nexport default async function notificationSubscriber(\n payload: MfaEnrolledPayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.enrolled')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const user = await resolveUserById(em, payload.userId)\n if (!user) return\n\n const notificationService = resolveNotificationService(ctx)\n const methodLabel = resolveMethodLabel(payload.methodType)\n const enrolledAtIso = payload.enrolledAt ?? new Date().toISOString()\n\n await notificationService.create(\n buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: {\n method: methodLabel,\n },\n sourceEntityType: 'security:mfa_method',\n sourceEntityId: payload.methodId,\n linkHref: '/backend/profile/security/mfa',\n }),\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: 'Multi-factor authentication enabled',\n react: MfaEnrolledEmail({\n methodLabel,\n enrolledAtIso,\n }),\n })\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,iBAAiB;AAC1B,OAAO,sBAAsB;AAC7B,SAAS,yBAAyB;AAE3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAoBA,MAAM,gBAAwC;AAAA,EAC5C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AACb;AAEA,SAAS,mBAAmB,YAA4B;AACtD,SAAO,cAAc,UAAU,KAAK;AACtC;AAEA,eAAe,gBAAgB,IAAmB,QAA8C;AAC9F,QAAM,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,QAAQ,WAAW,KAAK,CAAC;AACnE,MAAI,CAAC,MAAM,SAAU,QAAO;AAE5B,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,IACzC;AAAA,EACF;AACA,MAAI,CAAC,WAAW,SAAU,QAAO;AAEjC,SAAO;AAAA,IACL,IAAI,UAAU;AAAA,IACd,OAAO,OAAO,UAAU,UAAU,WAAW,UAAU,QAAQ;AAAA,IAC/D,UAAU,UAAU;AAAA,IACpB,gBAAgB,UAAU,kBAAkB;AAAA,EAC9C;AACF;AAEA,eAAO,uBACL,SACA,KACA;AACA,QAAM,UAAU,kBAAkB,KAAK,CAAC,SAAS,KAAK,SAAS,uBAAuB;AACtF,MAAI,CAAC,QAAS;AAEd,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,OAAO,MAAM,gBAAgB,IAAI,QAAQ,MAAM;AACrD,MAAI,CAAC,KAAM;AAEX,QAAM,sBAAsB,2BAA2B,GAAG;AAC1D,QAAM,cAAc,mBAAmB,QAAQ,UAAU;AACzD,QAAM,gBAAgB,QAAQ,eAAc,oBAAI,KAAK,GAAE,YAAY;AAEnE,QAAM,oBAAoB;AAAA,IACxB,0BAA0B,SAAS;AAAA,MACjC,iBAAiB,KAAK;AAAA,MACtB,eAAe;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,kBAAkB;AAAA,MAClB,gBAAgB,QAAQ;AAAA,MACxB,UAAU;AAAA,IACZ,CAAC;AAAA,IACD;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,KAAK,OAAO;AACd,UAAM,UAAU;AAAA,MACd,IAAI,KAAK;AAAA,MACT,SAAS;AAAA,MACT,OAAO,iBAAiB;AAAA,QACtB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { buildNotificationFromType } from '@open-mercato/core/modules/notifications/lib/notificationBuilder'\nimport { resolveNotificationService } from '@open-mercato/core/modules/notifications/lib/notificationService'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport MfaEnrolledEmail from '../emails/mfa-enrolled'\nimport { notificationTypes } from '../notifications'\n\nexport const metadata = {\n event: 'security.mfa.enrolled',\n persistent: true,\n id: 'security:mfa-enrolled-notification',\n}\n\ntype MfaEnrolledPayload = {\n userId: string\n methodId: string\n methodType: string\n enrolledAt?: string\n}\n\ntype ResolverContext = {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype ResolvedUser = {\n id: string\n email: string | null\n tenantId: string\n organizationId: string | null\n}\n\nconst METHOD_LABELS: Record<string, string> = {\n totp: 'Authenticator app',\n passkey: 'Passkey',\n otp_email: 'Email OTP',\n}\n\nfunction resolveMethodLabel(methodType: string): string {\n return METHOD_LABELS[methodType] ?? methodType\n}\n\nasync function resolveUserById(em: EntityManager, userId: string): Promise<ResolvedUser | null> {\n const user = await em.findOne(User, { id: userId, deletedAt: null })\n if (!user?.tenantId) return null\n\n const decrypted = await findOneWithDecryption(\n em,\n User,\n {\n id: userId,\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId ?? null,\n },\n )\n if (!decrypted?.tenantId) return null\n\n return {\n id: decrypted.id,\n email: typeof decrypted.email === 'string' ? decrypted.email : null,\n tenantId: decrypted.tenantId,\n organizationId: decrypted.organizationId ?? null,\n }\n}\n\nexport default async function notificationSubscriber(\n payload: MfaEnrolledPayload,\n ctx: ResolverContext,\n) {\n const typeDef = notificationTypes.find((type) => type.type === 'security.mfa.enrolled')\n if (!typeDef) return\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const user = await resolveUserById(em, payload.userId)\n if (!user) return\n\n const notificationService = resolveNotificationService(ctx)\n const methodLabel = resolveMethodLabel(payload.methodType)\n const enrolledAtIso = payload.enrolledAt ?? new Date().toISOString()\n\n await notificationService.create(\n buildNotificationFromType(typeDef, {\n recipientUserId: user.id,\n bodyVariables: {\n method: methodLabel,\n },\n sourceEntityType: 'security:mfa_method',\n sourceEntityId: payload.methodId,\n linkHref: '/backend/profile/security/mfa',\n }),\n {\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n },\n )\n\n if (user.email) {\n await sendEmail({\n to: user.email,\n subject: 'Multi-factor authentication enabled',\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n react: MfaEnrolledEmail({\n methodLabel,\n enrolledAtIso,\n }),\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,iCAAiC;AAC1C,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,iBAAiB;AAC1B,OAAO,sBAAsB;AAC7B,SAAS,yBAAyB;AAE3B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAoBA,MAAM,gBAAwC;AAAA,EAC5C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,WAAW;AACb;AAEA,SAAS,mBAAmB,YAA4B;AACtD,SAAO,cAAc,UAAU,KAAK;AACtC;AAEA,eAAe,gBAAgB,IAAmB,QAA8C;AAC9F,QAAM,OAAO,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,QAAQ,WAAW,KAAK,CAAC;AACnE,MAAI,CAAC,MAAM,SAAU,QAAO;AAE5B,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,MACvC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK,kBAAkB;AAAA,IACzC;AAAA,EACF;AACA,MAAI,CAAC,WAAW,SAAU,QAAO;AAEjC,SAAO;AAAA,IACL,IAAI,UAAU;AAAA,IACd,OAAO,OAAO,UAAU,UAAU,WAAW,UAAU,QAAQ;AAAA,IAC/D,UAAU,UAAU;AAAA,IACpB,gBAAgB,UAAU,kBAAkB;AAAA,EAC9C;AACF;AAEA,eAAO,uBACL,SACA,KACA;AACA,QAAM,UAAU,kBAAkB,KAAK,CAAC,SAAS,KAAK,SAAS,uBAAuB;AACtF,MAAI,CAAC,QAAS;AAEd,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,OAAO,MAAM,gBAAgB,IAAI,QAAQ,MAAM;AACrD,MAAI,CAAC,KAAM;AAEX,QAAM,sBAAsB,2BAA2B,GAAG;AAC1D,QAAM,cAAc,mBAAmB,QAAQ,UAAU;AACzD,QAAM,gBAAgB,QAAQ,eAAc,oBAAI,KAAK,GAAE,YAAY;AAEnE,QAAM,oBAAoB;AAAA,IACxB,0BAA0B,SAAS;AAAA,MACjC,iBAAiB,KAAK;AAAA,MACtB,eAAe;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,kBAAkB;AAAA,MAClB,gBAAgB,QAAQ;AAAA,MACxB,UAAU;AAAA,IACZ,CAAC;AAAA,IACD;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAEA,MAAI,KAAK,OAAO;AACd,UAAM,UAAU;AAAA,MACd,IAAI,KAAK;AAAA,MACT,SAAS;AAAA,MACT,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,OAAO,iBAAiB;AAAA,QACtB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@open-mercato/core": "0.7.1-develop.
|
|
69
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
68
|
+
"@open-mercato/core": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
69
|
+
"@open-mercato/ui": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
70
70
|
"@simplewebauthn/browser": "^13.3.0",
|
|
71
71
|
"@simplewebauthn/server": "^13.3.2",
|
|
72
72
|
"@simplewebauthn/types": "^12.0.0",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"qrcode": "^1.5.4"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
79
|
+
"@open-mercato/shared": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"react-dom": "^19.0.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
84
|
+
"@open-mercato/shared": "0.7.1-develop.7152.1.a69e92f9c9",
|
|
85
85
|
"@types/jest": "^30.0.0",
|
|
86
86
|
"@types/react": "^19.2.17",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
@@ -151,6 +151,8 @@ export class OtpEmailProvider implements MfaProviderInterface {
|
|
|
151
151
|
await sendEmail({
|
|
152
152
|
to: email,
|
|
153
153
|
subject: this.securityConfig.otpEmail.subject,
|
|
154
|
+
tenantId: method.tenantId,
|
|
155
|
+
organizationId: method.organizationId ?? null,
|
|
154
156
|
react: OtpCodeEmail({
|
|
155
157
|
code,
|
|
156
158
|
expiresInMinutes: Math.max(1, Math.ceil(challengeTtlMs / (60 * 1000))),
|
|
@@ -123,6 +123,8 @@ export class MfaVerificationService {
|
|
|
123
123
|
id: method.id,
|
|
124
124
|
type: method.type,
|
|
125
125
|
userId: method.userId,
|
|
126
|
+
tenantId: method.tenantId,
|
|
127
|
+
organizationId: method.organizationId ?? null,
|
|
126
128
|
secret: method.secret ?? null,
|
|
127
129
|
providerMetadata: method.providerMetadata,
|
|
128
130
|
}, context)
|
|
@@ -190,6 +192,8 @@ export class MfaVerificationService {
|
|
|
190
192
|
id: method.id,
|
|
191
193
|
type: method.type,
|
|
192
194
|
userId: method.userId,
|
|
195
|
+
tenantId: method.tenantId,
|
|
196
|
+
organizationId: method.organizationId ?? null,
|
|
193
197
|
secret: method.secret ?? null,
|
|
194
198
|
providerMetadata: method.providerMetadata,
|
|
195
199
|
}, payload, context, runtimeContext)
|
|
@@ -199,6 +199,8 @@ export async function notifyEnforcementPolicyChange(
|
|
|
199
199
|
await sendEmail({
|
|
200
200
|
to: user.email,
|
|
201
201
|
subject: resolveEmailSubject(message),
|
|
202
|
+
tenantId: user.tenantId,
|
|
203
|
+
organizationId: user.organizationId,
|
|
202
204
|
react: EnforcementDeadlineEmail({
|
|
203
205
|
daysRemaining: message.daysRemaining,
|
|
204
206
|
deadlineIsoDate: message.deadlineIsoDate,
|
|
@@ -94,6 +94,8 @@ export default async function mfaResetNotificationSubscriber(
|
|
|
94
94
|
await sendEmail({
|
|
95
95
|
to: user.email,
|
|
96
96
|
subject: 'Your MFA methods were reset',
|
|
97
|
+
tenantId: user.tenantId,
|
|
98
|
+
organizationId: user.organizationId,
|
|
97
99
|
react: MfaResetEmail({
|
|
98
100
|
reason,
|
|
99
101
|
resetAtIso,
|
|
@@ -105,6 +105,8 @@ export default async function notificationSubscriber(
|
|
|
105
105
|
await sendEmail({
|
|
106
106
|
to: user.email,
|
|
107
107
|
subject: 'Multi-factor authentication enabled',
|
|
108
|
+
tenantId: user.tenantId,
|
|
109
|
+
organizationId: user.organizationId,
|
|
108
110
|
react: MfaEnrolledEmail({
|
|
109
111
|
methodLabel,
|
|
110
112
|
enrolledAtIso,
|