@open-mercato/enterprise 0.6.4-develop.4382.1.6b4f656b77 → 0.6.4
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/record_locks/widgets/injection/record-locking/widget.client.js +1 -1
- package/dist/modules/record_locks/widgets/injection/record-locking/widget.client.js.map +2 -2
- package/dist/modules/security/api/enforcement/[id]/route.js +35 -1
- package/dist/modules/security/api/enforcement/[id]/route.js.map +2 -2
- package/dist/modules/security/api/enforcement/_shared.js +63 -1
- package/dist/modules/security/api/enforcement/_shared.js.map +3 -3
- package/dist/modules/security/api/enforcement/compliance/route.js +12 -3
- package/dist/modules/security/api/enforcement/compliance/route.js.map +2 -2
- package/dist/modules/security/api/enforcement/route.js +25 -2
- package/dist/modules/security/api/enforcement/route.js.map +2 -2
- package/dist/modules/security/api/mfa/prepare/route.js +1 -1
- package/dist/modules/security/api/mfa/prepare/route.js.map +2 -2
- package/dist/modules/security/api/mfa/recovery/route.js +1 -1
- package/dist/modules/security/api/mfa/recovery/route.js.map +2 -2
- package/dist/modules/security/api/mfa/verify/route.js +1 -1
- package/dist/modules/security/api/mfa/verify/route.js.map +2 -2
- package/dist/modules/security/api/users/[id]/mfa/reset/route.js +6 -1
- package/dist/modules/security/api/users/[id]/mfa/reset/route.js.map +2 -2
- package/dist/modules/security/api/users/[id]/mfa/status/route.js +13 -2
- package/dist/modules/security/api/users/[id]/mfa/status/route.js.map +2 -2
- package/dist/modules/security/api/users/_shared.js +56 -1
- package/dist/modules/security/api/users/_shared.js.map +2 -2
- package/dist/modules/security/api/users/mfa/compliance/route.js +17 -7
- package/dist/modules/security/api/users/mfa/compliance/route.js.map +2 -2
- package/dist/modules/security/commands/createEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/createEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/deleteEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/deleteEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/commands/resetUserMfa.js +6 -1
- package/dist/modules/security/commands/resetUserMfa.js.map +2 -2
- package/dist/modules/security/commands/updateEnforcementPolicy.js +6 -1
- package/dist/modules/security/commands/updateEnforcementPolicy.js.map +2 -2
- package/dist/modules/security/services/MfaAdminService.js +22 -5
- package/dist/modules/security/services/MfaAdminService.js.map +2 -2
- package/dist/modules/security/services/MfaEnforcementService.js +28 -6
- package/dist/modules/security/services/MfaEnforcementService.js.map +2 -2
- package/dist/modules/security/services/MfaVerificationService.js +30 -10
- package/dist/modules/security/services/MfaVerificationService.js.map +2 -2
- package/dist/modules/security/services/SudoChallengeService.js +14 -3
- package/dist/modules/security/services/SudoChallengeService.js.map +2 -2
- package/dist/modules/sso/api/callback/oidc/route.js +2 -2
- package/dist/modules/sso/api/callback/oidc/route.js.map +2 -2
- package/dist/modules/sso/i18n/de.json +2 -0
- package/dist/modules/sso/i18n/en.json +2 -0
- package/dist/modules/sso/i18n/es.json +2 -0
- package/dist/modules/sso/i18n/pl.json +2 -0
- package/dist/modules/sso/lib/errors.js +21 -0
- package/dist/modules/sso/lib/errors.js.map +7 -0
- package/dist/modules/sso/services/accountLinkingService.js +2 -1
- package/dist/modules/sso/services/accountLinkingService.js.map +2 -2
- package/package.json +7 -8
- package/src/modules/record_locks/widgets/injection/record-locking/widget.client.tsx +1 -1
- package/src/modules/security/api/enforcement/[id]/route.ts +50 -1
- package/src/modules/security/api/enforcement/_shared.ts +83 -2
- package/src/modules/security/api/enforcement/compliance/route.ts +10 -1
- package/src/modules/security/api/enforcement/route.ts +30 -2
- package/src/modules/security/api/mfa/prepare/route.ts +1 -1
- package/src/modules/security/api/mfa/recovery/route.ts +1 -1
- package/src/modules/security/api/mfa/verify/route.ts +1 -1
- package/src/modules/security/api/users/[id]/mfa/reset/route.ts +6 -1
- package/src/modules/security/api/users/[id]/mfa/status/route.ts +13 -2
- package/src/modules/security/api/users/_shared.ts +69 -1
- package/src/modules/security/api/users/mfa/compliance/route.ts +16 -7
- package/src/modules/security/commands/createEnforcementPolicy.ts +6 -1
- package/src/modules/security/commands/deleteEnforcementPolicy.ts +6 -1
- package/src/modules/security/commands/resetUserMfa.ts +6 -1
- package/src/modules/security/commands/updateEnforcementPolicy.ts +6 -1
- package/src/modules/security/services/MfaAdminService.ts +29 -6
- package/src/modules/security/services/MfaEnforcementService.ts +42 -2
- package/src/modules/security/services/MfaVerificationService.ts +33 -10
- package/src/modules/security/services/SudoChallengeService.ts +16 -11
- package/src/modules/sso/api/callback/oidc/route.ts +2 -2
- package/src/modules/sso/i18n/de.json +2 -0
- package/src/modules/sso/i18n/en.json +2 -0
- package/src/modules/sso/i18n/es.json +2 -0
- package/src/modules/sso/i18n/pl.json +2 -0
- package/src/modules/sso/lib/errors.ts +35 -0
- package/src/modules/sso/services/accountLinkingService.ts +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/security/services/MfaEnforcementService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async listPolicies(filters?: EnforcementPolicyListFilters): Promise<MfaEnforcementPolicy[]> {\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAKP,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n EnforcementScope,\n MfaEnforcementPolicy,\n UserMfaMethod,\n} from '../data/entities'\nimport type {\n EnforcementPolicyInput,\n UpdateEnforcementPolicyInput,\n} from '../data/validators'\nimport { emitSecurityEvent } from '../events'\n\ntype EnforcementResult = {\n enforced: boolean\n policy?: MfaEnforcementPolicy\n}\n\ntype ComplianceReport = {\n total: number\n enrolled: number\n pending: number\n overdue: number\n}\n\ntype EnforcementPolicyListFilters = {\n scope?: EnforcementScope\n}\n\nexport type EnforcementActorContext = {\n tenantId: string | null\n isSuperAdmin: boolean\n}\n\ntype UserCompliance = {\n compliant: boolean\n deadline?: Date\n enforced: boolean\n}\n\nexport function isEnforcementDeadlineOverdue(\n deadline?: Date | null,\n now = Date.now(),\n): boolean {\n if (!(deadline instanceof Date)) return false\n const deadlineTime = deadline.getTime()\n if (Number.isNaN(deadlineTime)) return false\n return deadlineTime <= now\n}\n\nexport class MfaEnforcementServiceError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n ) {\n super(message)\n this.name = 'MfaEnforcementServiceError'\n }\n}\n\nexport class MfaEnforcementService {\n constructor(private readonly em: EntityManager) {}\n\n async isEnforced(tenantId: string, orgId?: string): Promise<EnforcementResult> {\n const policy = await this.resolveEffectivePolicy(tenantId, orgId)\n if (!policy || !policy.isEnforced) {\n return { enforced: false, policy: policy ?? undefined }\n }\n return { enforced: true, policy }\n }\n\n async getPolicyById(id: string): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(MfaEnforcementPolicy, { id, deletedAt: null })\n }\n\n async listPolicies(\n filters?: EnforcementPolicyListFilters,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy[]> {\n const tenantConstraint = actor && !actor.isSuperAdmin ? { tenantId: actor.tenantId } : {}\n return this.em.find(\n MfaEnforcementPolicy,\n {\n deletedAt: null,\n ...(filters?.scope ? { scope: filters.scope } : {}),\n ...tenantConstraint,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n async getComplianceReport(\n scope: EnforcementScope,\n scopeId?: string,\n actor?: EnforcementActorContext,\n ): Promise<ComplianceReport> {\n const { tenantId, organizationId } = this.resolveScopeFilters(scope, scopeId)\n this.assertActorOwnsScopeFilters(actor, scope, tenantId)\n const users = await this.em.find(User, {\n deletedAt: null,\n ...(tenantId ? { tenantId } : {}),\n ...(organizationId ? { organizationId } : {}),\n })\n\n const total = users.length\n if (total === 0) {\n return { total: 0, enrolled: 0, pending: 0, overdue: 0 }\n }\n\n const userIds = users.map((user) => user.id)\n const policy = await this.findPolicyByScope(scope, tenantId, organizationId)\n const methodFilter = this.buildAllowedMethodsFilter(policy?.allowedMethods ?? null)\n const methods = await this.em.find(UserMfaMethod, {\n userId: { $in: userIds },\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n const enrolledUserIds = new Set(methods.map((method) => method.userId))\n const enrolled = enrolledUserIds.size\n const unenrolled = Math.max(0, total - enrolled)\n\n const now = Date.now()\n const overdue = isEnforcementDeadlineOverdue(policy?.enforcementDeadline, now) ? unenrolled : 0\n const pending = Math.max(0, unenrolled - overdue)\n\n return {\n total,\n enrolled,\n pending,\n overdue,\n }\n }\n\n async createPolicy(\n data: EnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const normalized = this.normalizePolicyInput(data)\n this.assertActorOwnsScopeFilters(actor, normalized.scope, normalized.tenantId)\n const existing = await this.findPolicyByScope(\n normalized.scope,\n normalized.tenantId ?? undefined,\n normalized.organizationId ?? undefined,\n )\n\n if (existing) {\n existing.isEnforced = normalized.isEnforced\n existing.allowedMethods = normalized.allowedMethods\n existing.enforcementDeadline = normalized.enforcementDeadline\n existing.enforcedBy = adminId\n existing.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: existing.id,\n scope: existing.scope,\n })\n await this.emitDeadlineReminderRequest(existing.id)\n return existing\n }\n\n const now = new Date()\n const policy = this.em.create(MfaEnforcementPolicy, {\n scope: normalized.scope,\n tenantId: normalized.tenantId,\n organizationId: normalized.organizationId,\n isEnforced: normalized.isEnforced,\n allowedMethods: normalized.allowedMethods,\n enforcementDeadline: normalized.enforcementDeadline,\n enforcedBy: adminId,\n createdAt: now,\n updatedAt: now,\n })\n this.em.persist(policy)\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.created', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async updatePolicy(\n id: string,\n data: UpdateEnforcementPolicyInput,\n adminId: string,\n actor?: EnforcementActorContext,\n ): Promise<MfaEnforcementPolicy> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const mergedInput = this.normalizePolicyInput({\n scope: data.scope ?? policy.scope,\n tenantId: data.tenantId ?? policy.tenantId ?? undefined,\n organizationId: data.organizationId ?? policy.organizationId ?? undefined,\n isEnforced: data.isEnforced ?? policy.isEnforced,\n allowedMethods: data.allowedMethods ?? policy.allowedMethods ?? null,\n enforcementDeadline:\n data.enforcementDeadline === undefined\n ? (policy.enforcementDeadline ?? null)\n : data.enforcementDeadline,\n })\n\n this.assertActorOwnsScopeFilters(actor, mergedInput.scope, mergedInput.tenantId)\n\n if (\n mergedInput.scope !== policy.scope ||\n mergedInput.tenantId !== (policy.tenantId ?? null) ||\n mergedInput.organizationId !== (policy.organizationId ?? null)\n ) {\n const conflict = await this.findPolicyByScope(\n mergedInput.scope,\n mergedInput.tenantId ?? undefined,\n mergedInput.organizationId ?? undefined,\n )\n if (conflict && conflict.id !== policy.id) {\n throw new MfaEnforcementServiceError('Enforcement policy already exists for this scope', 409)\n }\n }\n\n policy.scope = mergedInput.scope\n policy.tenantId = mergedInput.tenantId\n policy.organizationId = mergedInput.organizationId\n policy.isEnforced = mergedInput.isEnforced\n policy.allowedMethods = mergedInput.allowedMethods\n policy.enforcementDeadline = mergedInput.enforcementDeadline\n policy.enforcedBy = adminId\n policy.updatedAt = new Date()\n await this.em.flush()\n\n await emitSecurityEvent('security.enforcement.updated', {\n adminId,\n policyId: policy.id,\n scope: policy.scope,\n })\n await this.emitDeadlineReminderRequest(policy.id)\n return policy\n }\n\n async deletePolicy(id: string, actor?: EnforcementActorContext): Promise<void> {\n const policy = await this.em.findOne(MfaEnforcementPolicy, {\n id,\n deletedAt: null,\n })\n if (!policy) {\n throw new MfaEnforcementServiceError('Enforcement policy not found', 404)\n }\n this.assertActorOwnsScopeFilters(actor, policy.scope, policy.tenantId ?? null)\n\n const now = new Date()\n policy.deletedAt = now\n policy.updatedAt = now\n await this.em.flush()\n }\n\n async checkUserCompliance(userId: string): Promise<UserCompliance> {\n const policy = await this.getEffectivePolicyForUser(userId)\n if (!policy || !policy.isEnforced) {\n return { compliant: true, enforced: false }\n }\n\n const methodFilter = this.buildAllowedMethodsFilter(policy.allowedMethods ?? null)\n const methodCount = await this.em.count(UserMfaMethod, {\n userId,\n isActive: true,\n deletedAt: null,\n ...methodFilter,\n })\n\n return {\n compliant: methodCount > 0,\n enforced: true,\n deadline: policy.enforcementDeadline ?? undefined,\n }\n }\n\n async getEffectivePolicyForUser(userId: string): Promise<MfaEnforcementPolicy | null> {\n const user = await this.findUserById(userId)\n if (!user?.tenantId) {\n throw new MfaEnforcementServiceError('User not found', 404)\n }\n\n return this.resolveEffectivePolicy(user.tenantId, user.organizationId ?? undefined)\n }\n\n private async resolveEffectivePolicy(\n tenantId: string,\n orgId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n if (orgId) {\n const organizationPolicy = await this.findPolicyByScope(\n EnforcementScope.ORGANISATION,\n tenantId,\n orgId,\n )\n if (organizationPolicy) return organizationPolicy\n }\n\n const tenantPolicy = await this.findPolicyByScope(EnforcementScope.TENANT, tenantId, undefined)\n if (tenantPolicy) return tenantPolicy\n\n return this.findPolicyByScope(EnforcementScope.PLATFORM, undefined, undefined)\n }\n\n private async findPolicyByScope(\n scope: EnforcementScope,\n tenantId?: string,\n organizationId?: string,\n ): Promise<MfaEnforcementPolicy | null> {\n return this.em.findOne(\n MfaEnforcementPolicy,\n {\n scope,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n deletedAt: null,\n },\n {\n orderBy: { updatedAt: 'desc' },\n },\n )\n }\n\n private assertActorOwnsScopeFilters(\n actor: EnforcementActorContext | undefined,\n scope: EnforcementScope,\n tenantId: string | null | undefined,\n ): void {\n if (!actor || actor.isSuperAdmin) return\n if (scope === EnforcementScope.PLATFORM) {\n throw new MfaEnforcementServiceError(\n 'Platform scope requires platform administrator privileges.',\n 403,\n )\n }\n if (!tenantId || tenantId !== actor.tenantId) {\n throw new MfaEnforcementServiceError('Not authorized for the requested scope.', 403)\n }\n }\n\n private resolveScopeFilters(\n scope: EnforcementScope,\n scopeId?: string,\n ): { tenantId?: string; organizationId?: string } {\n if (scope === EnforcementScope.PLATFORM) {\n return {}\n }\n\n if (!scopeId) {\n throw new MfaEnforcementServiceError('scopeId is required for tenant and organisation scopes', 400)\n }\n\n if (scope === EnforcementScope.TENANT) {\n return { tenantId: scopeId }\n }\n\n const [tenantId, organizationId] = scopeId.split(':')\n if (!tenantId || !organizationId) {\n throw new MfaEnforcementServiceError(\n \"organisation scopeId must use '<tenantId>:<organizationId>' format\",\n 400,\n )\n }\n\n return { tenantId, organizationId }\n }\n\n private normalizePolicyInput(data: {\n scope: EnforcementScope\n tenantId?: string | null\n organizationId?: string | null\n isEnforced?: boolean\n allowedMethods?: string[] | null\n enforcementDeadline?: Date | null\n }): {\n scope: EnforcementScope\n tenantId: string | null\n organizationId: string | null\n isEnforced: boolean\n allowedMethods: string[] | null\n enforcementDeadline: Date | null\n } {\n const isEnforced = data.isEnforced ?? true\n const allowedMethods = this.normalizeAllowedMethods(data.allowedMethods)\n const enforcementDeadline = data.enforcementDeadline ?? null\n\n if (data.scope === EnforcementScope.PLATFORM) {\n return {\n scope: data.scope,\n tenantId: null,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (data.scope === EnforcementScope.TENANT) {\n if (!data.tenantId) {\n throw new MfaEnforcementServiceError('tenantId is required for tenant scope', 400)\n }\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: null,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n if (!data.tenantId || !data.organizationId) {\n throw new MfaEnforcementServiceError(\n 'tenantId and organizationId are required for organisation scope',\n 400,\n )\n }\n\n return {\n scope: data.scope,\n tenantId: data.tenantId,\n organizationId: data.organizationId,\n isEnforced,\n allowedMethods,\n enforcementDeadline,\n }\n }\n\n private normalizeAllowedMethods(allowedMethods?: string[] | null): string[] | null {\n if (!allowedMethods || allowedMethods.length === 0) {\n return null\n }\n return Array.from(new Set(allowedMethods.map((method) => method.trim()).filter(Boolean)))\n }\n\n private buildAllowedMethodsFilter(\n allowedMethods?: string[] | null,\n ): { type?: { $in: string[] } } {\n if (!allowedMethods || allowedMethods.length === 0) {\n return {}\n }\n return { type: { $in: allowedMethods } }\n }\n\n private async findUserById(userId: string): Promise<User | null> {\n return findOneWithDecryption(this.em, User, { id: userId, deletedAt: null }, undefined, {})\n }\n\n private async emitDeadlineReminderRequest(policyId: string): Promise<void> {\n await emitSecurityEvent('security.enforcement.deadline_reminder_requested', {\n policyId,\n })\n }\n}\n\nexport default MfaEnforcementService\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -50,6 +50,7 @@ class MfaVerificationService {
|
|
|
50
50
|
}
|
|
51
51
|
async prepareChallenge(challengeId, methodType, context) {
|
|
52
52
|
const challenge = await this.getValidChallenge(challengeId);
|
|
53
|
+
await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
|
|
53
54
|
const provider = this.mfaProviderRegistry.get(methodType);
|
|
54
55
|
if (!provider) {
|
|
55
56
|
throw new MfaVerificationServiceError(`MFA provider '${methodType}' is not registered`, 400);
|
|
@@ -73,12 +74,9 @@ class MfaVerificationService {
|
|
|
73
74
|
if (challenge.attempts >= this.securityConfig.mfa.maxAttempts) {
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
77
|
+
await this.assertMethodAllowedByPolicy(challenge.userId, methodType);
|
|
76
78
|
if (challenge.methodType && challenge.methodType !== methodType) {
|
|
77
|
-
challenge
|
|
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
|
|
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
|
|
5
|
-
"mappings": "AACA,SAAS,cAAc,qBAAqB;AAE5C,SAAS,yBAAyB;AAKlC,SAAS,gCAAgC;AAkBlC,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,EAEA,MAAM,iBACJ,aACA,YACA,SACmD;AACnD,UAAM,YAAY,MAAM,KAAK,kBAAkB,WAAW;AAC1D,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,EAEA,MAAM,gBACJ,aACA,YACA,SACA,gBACkB;AAClB,UAAM,YAAY,MAAM,KAAK,kBAAkB,WAAW;AAC1D,QAAI,UAAU,YAAY,KAAK,eAAe,IAAI,aAAa;AAC7D,aAAO;AAAA,IACT;AAEA,
|
|
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 =
|
|
476
|
-
|
|
477
|
-
|
|
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": "AAAA,SAAS,YAAY,aAAa,uBAAuB;AACzD,SAAS,SAAS;AAElB,SAAS,YAAY;AACrB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB;AAQlC,SAAS,eAAe,0BAA0B;AAElD,SAAS,gCAAgC;AAezC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,KAAK,EAAE,OAAO;AAAA,EACd,KAAK,EAAE,OAAO;AAAA,EACd,KAAK,EAAE,OAAO,EAAE,SAAS;AAAA,EACzB,KAAK,EAAE,OAAO,EAAE,SAAS;AAAA,EACzB,KAAK,EAAE,OAAO;AAAA,EACd,KAAK,EAAE,OAAO;AAChB,CAAC;AAuBM,MAAM,kCAAkC,MAAM;AAAA,EACnD,YACE,SACgB,YAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,MAAM,qBAAqB;AAAA,EAChC,YACmB,IACA,iBACA,YACA,wBACA,iBAAuC,yBAAyB,GACjF;AALiB;AACA;AACA;AACA;AACA;AAAA,EAChB;AAAA,EAEH,MAAM,YAAY,OAAuD;AACvE,UAAM,KAAK,kCAAkC;AAC7C,UAAM,SAA2C,EAAE,WAAW,KAAK;AACnE,QAAI,SAAS,CAAC,MAAM,cAAc;AAChC,UAAI,CAAC,MAAM,SAAU,QAAO,CAAC;AAC5B,MAAC,OAAmC,MAAM;AAAA,QACzC,EAAE,UAAU,MAAM,SAAS;AAAA,QAC3B,EAAE,UAAU,KAAK;AAAA,MACnB;AAAA,IACF;AACA,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,QACE,SAAS;AAAA,UACP,kBAAkB;AAAA,UAClB,UAAU;AAAA,UACV,gBAAgB;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,IAAY,OAA4D;AAC1F,UAAM,KAAK,kCAAkC;AAC7C,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,qBAAqB,EAAE,IAAI,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,OAAQ,QAAO;AACpB,QAAI,SAAS,CAAC,KAAK,uBAAuB,QAAQ,KAAK,EAAG,QAAO;AACjE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YACJ,kBACA,UACA,gBACmC;AACnC,UAAM,KAAK,kCAAkC;AAE7C,UAAM,aAAa,MAAM,KAAK,GAAG,KAAK,qBAAqB;AAAA,MACzD;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAED,UAAM,WAAW,WACd,OAAO,CAAC,WAAW,KAAK,aAAa,QAAQ,YAAY,MAAM,kBAAkB,IAAI,CAAC,EACtF,KAAK,CAAC,MAAM,UAAU,KAAK,sBAAsB,MAAM,OAAO,YAAY,MAAM,kBAAkB,IAAI,CAAC,EAAE,CAAC;AAE7G,QAAI,CAAC,YAAY,CAAC,SAAS,WAAW;AACpC,aAAO,EAAE,WAAW,MAAM;AAAA,IAC5B;AAEA,WAAO,EAAE,WAAW,MAAM,QAAQ,SAAS;AAAA,EAC7C;AAAA,EAEA,MAAM,SACJ,QACA,kBACA,SAOC;AACD,UAAM,aAAa,MAAM,KAAK;AAAA,MAC5B;AAAA,MACA,SAAS,YAAY;AAAA,MACrB,SAAS,kBAAkB;AAAA,IAC7B;AAEA,QAAI,CAAC,WAAW,aAAa,CAAC,WAAW,QAAQ;AAC/C,aAAO,EAAE,UAAU,MAAM;AAAA,IAC3B;AAEA,UAAM,OAAO,MAAM,KAAK,cAAc,MAAM;AAC5C,QAAI,CAAC,MAAM,UAAU;AACnB,YAAM,IAAI,0BAA0B,kBAAkB,GAAG;AAAA,IAC3D;AAEA,UAAM,cAAc,MAAM,KAAK,WAAW,eAAe,MAAM;AAC/D,UAAM,SAAS,KAAK,uBAAuB,WAAW,OAAO,iBAAiB,YAAY,MAAM;AAEhG,QAAI,eAAe,YAAY,EAAE,EAAE,SAAS,KAAK;AACjD,QAAI;AACJ,QAAI,WAAW,OAAO;AACpB,YAAM,YAAY,MAAM,KAAK,uBAAuB,gBAAgB,MAAM;AAC1E,qBAAe,UAAU;AACzB,4BAAsB,UAAU;AAAA,IAClC;AAEA,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,eAAe,KAAK,qBAAqB;AACtF,UAAM,UAAU,KAAK,GAAG,OAAO,aAAa;AAAA,MAC1C;AAAA,MACA,UAAU,KAAK;AAAA,MACf;AAAA,MACA,iBAAiB;AAAA,MACjB;AAAA,MACA,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,GAAG,QAAQ,OAAO;AACvB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,4BAA4B;AAAA,MAClD;AAAA,MACA,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO;AAAA,MACL,UAAU;AAAA,MACV,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,QACJ,WACA,YACA,SACmD;AACnD,UAAM,UAAU,MAAM,KAAK,kBAAkB,SAAS;AACtD,QAAI,QAAQ,oBAAoB,OAAO;AACrC,YAAM,IAAI,0BAA0B,0CAA0C,GAAG;AAAA,IACnF;AACA,WAAO,KAAK,uBAAuB,iBAAiB,QAAQ,cAAc,YAAY,EAAE,QAAQ,CAAC;AAAA,EACnG;AAAA,EAEA,MAAM,OACJ,WACA,YACA,SACA,SAMA,SACiD;AACjD,UAAM,UAAU,MAAM,KAAK,kBAAkB,SAAS;AACtD,QAAI,QAAQ,kBAAkB,QAAQ,WAAW,QAAQ,gBAAgB;AACvE,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AACA,UAAM,OAAO,MAAM,KAAK,cAAc,QAAQ,MAAM;AACpD,QAAI,CAAC,MAAM,UAAU;AACnB,YAAM,IAAI,0BAA0B,kBAAkB,GAAG;AAAA,IAC3D;AAEA,UAAM,gBAAgB,QAAQ,aAAa,SAAY,QAAQ,WAAW,KAAK;AAC/E,UAAM,sBAAsB,QAAQ,mBAAmB,SAAY,QAAQ,iBAAiB,KAAK;AACjG,UAAM,aAAa,MAAM,KAAK;AAAA,MAC5B,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,WAAW,aAAa,CAAC,WAAW,QAAQ;AAC/C,YAAM,IAAI,0BAA0B,qDAAqD,GAAG;AAAA,IAC9F;AAEA,QAAI,WAAW;AACf,QAAI,aAAqB;AACzB,QAAI,QAAQ,oBAAoB,YAAY;AAC1C,YAAM,WAAW,KAAK,aAAa,OAAO;AAC1C,iBAAW,MAAM,KAAK,gBAAgB,eAAe,QAAQ,QAAQ,QAAQ;AAC7E,mBAAa,wBAAwB;AAAA,IACvC,OAAO;AACL,iBAAW,MAAM,KAAK,uBAAuB,gBAAgB,QAAQ,cAAc,YAAY,SAAS,EAAE,QAAQ,CAAC;AAAA,IACrH;AAEA,QAAI,CAAC,UAAU;AACb,YAAM,kBAAkB,wBAAwB;AAAA,QAC9C,QAAQ,QAAQ;AAAA,QAChB,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,QACrB,kBAAkB,QAAQ;AAAA,QAC1B,QAAQ;AAAA,MACV,CAAC;AACD,YAAM,IAAI,0BAA0B,mCAAmC,GAAG;AAAA,IAC5E;AAEA,UAAM,aAAa,KAAK,aAAa,WAAW,OAAO,UAAU;AACjE,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,aAAa,GAAI;AACzD,UAAM,YAAY,KAAK,UAAU;AAAA,MAC/B,KAAK,QAAQ;AAAA,MACb,KAAK,QAAQ;AAAA,MACb,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK,QAAQ;AAAA,MACb,KAAK,UAAU,QAAQ;AAAA,IACzB,CAAC;AAED,YAAQ,eAAe;AACvB,YAAQ,kBAAkB;AAC1B,YAAQ,YAAY;AACpB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,0BAA0B;AAAA,MAChD,QAAQ,QAAQ;AAAA,MAChB,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,kBAAkB,QAAQ;AAAA,MAC1B,QAAQ;AAAA,MACR,WAAW,UAAU,YAAY;AAAA,IACnC,CAAC;AAED,WAAO,EAAE,WAAW,UAAU;AAAA,EAChC;AAAA,EAEA,MAAM,cACJ,OACA,kBACA,SAKkB;AAClB,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,UAAU,KAAK,gBAAgB,KAAK;AAC1C,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,QAAQ,OAAO,KAAK,IAAI,EAAG,QAAO;AACtC,QAAI,QAAQ,QAAQ,iBAAkB,QAAO;AAC7C,QAAI,SAAS,kBAAkB,QAAQ,QAAQ,QAAQ,eAAgB,QAAO;AAC9E,QAAI,SAAS,aAAa,UAAa,QAAQ,SAAS,QAAQ,YAAY,MAAO,QAAO;AAC1F,QAAI,SAAS,mBAAmB,UAAa,QAAQ,SAAS,QAAQ,kBAAkB,MAAO,QAAO;AAEtG,UAAM,UAAU,MAAM,KAAK,GAAG,QAAQ,aAAa;AAAA,MACjD,IAAI,QAAQ;AAAA,MACZ,QAAQ,QAAQ;AAAA,MAChB,cAAc;AAAA,IAChB,CAA6B;AAE7B,WAAO,QAAQ,WAAW,QAAQ,UAAU,QAAQ,IAAI,KAAK,IAAI,CAAC;AAAA,EACpE;AAAA,EAEA,MAAM,aACJ,OACA,cACA,OAC8B;AAC9B,UAAM,KAAK,kCAAkC;AAC7C,UAAM,gBAAgB,MAAM,YAAY;AACxC,UAAM,sBAAsB,MAAM,kBAAkB;AACpD,SAAK,cAAc,eAAe,mBAAmB;AACrD,QAAI,MAAO,MAAK,iBAAiB,eAAe,qBAAqB,KAAK;AAC1E,UAAM,KAAK,mBAAmB,MAAM,kBAAkB,eAAe,mBAAmB;AAExF,UAAM,SAAS,KAAK,GAAG,OAAO,qBAAqB;AAAA,MACjD,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,OAAO,MAAM,SAAS;AAAA,MACtB,kBAAkB,MAAM;AAAA,MACxB,WAAW,MAAM;AAAA,MACjB,YAAY,KAAK,aAAa,MAAM,UAAU;AAAA,MAC9C,iBAAiB,MAAM;AAAA,MACvB;AAAA,MACA,oBAAoB;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,GAAG,QAAQ,MAAM;AACtB,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,gCAAgC;AAAA,MACtD,IAAI,OAAO;AAAA,MACX,kBAAkB,OAAO;AAAA,MACzB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aACJ,IACA,OACA,cACA,OAC8B;AAC9B,UAAM,KAAK,kCAAkC;AAC7C,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,qBAAqB,EAAE,IAAI,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AACA,QAAI,OAAO;AACT,WAAK,iBAAiB,OAAO,YAAY,MAAM,OAAO,kBAAkB,MAAM,KAAK;AACnF,UAAI,OAAO,sBAAsB,CAAC,MAAM,cAAc;AACpD,cAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,MACzE;AAAA,IACF;AAEA,UAAM,eAAe,MAAM,aAAa,SAAY,MAAM,YAAY,OAAO,OAAO,YAAY;AAChG,UAAM,qBAAqB,MAAM,mBAAmB,SAAY,MAAM,kBAAkB,OAAO,OAAO,kBAAkB;AACxH,UAAM,uBAAuB,MAAM,oBAAoB,OAAO;AAC9D,SAAK,cAAc,cAAc,kBAAkB;AACnD,QAAI,MAAO,MAAK,iBAAiB,cAAc,oBAAoB,KAAK;AACxE,UAAM,KAAK,mBAAmB,sBAAsB,cAAc,oBAAoB,OAAO,EAAE;AAE/F,QAAI,MAAM,aAAa,OAAW,QAAO,WAAW,MAAM,YAAY;AACtE,QAAI,MAAM,mBAAmB,OAAW,QAAO,iBAAiB,MAAM,kBAAkB;AACxF,QAAI,MAAM,UAAU,OAAW,QAAO,QAAQ,MAAM,SAAS;AAC7D,QAAI,MAAM,qBAAqB,OAAW,QAAO,mBAAmB,MAAM;AAC1E,QAAI,MAAM,cAAc,OAAW,QAAO,YAAY,MAAM;AAC5D,QAAI,MAAM,eAAe,OAAW,QAAO,aAAa,KAAK,aAAa,MAAM,UAAU;AAC1F,QAAI,MAAM,oBAAoB,OAAW,QAAO,kBAAkB,MAAM;AACxE,WAAO,eAAe;AACtB,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,gCAAgC;AAAA,MACtD,IAAI,OAAO;AAAA,MACX,kBAAkB,OAAO;AAAA,MACzB;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAa,IAAY,OAAsC;AACnE,UAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,qBAAqB,EAAE,IAAI,WAAW,KAAK,CAAC;AACjF,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AACA,QAAI,OAAO;AACT,WAAK,iBAAiB,OAAO,YAAY,MAAM,OAAO,kBAAkB,MAAM,KAAK;AACnF,UAAI,OAAO,sBAAsB,CAAC,MAAM,cAAc;AACpD,cAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,MACzE;AAAA,IACF;AACA,WAAO,YAAY,oBAAI,KAAK;AAC5B,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,KAAK,GAAG,MAAM;AAEpB,UAAM,kBAAkB,gCAAgC;AAAA,MACtD,IAAI,OAAO;AAAA,MACX,kBAAkB,OAAO;AAAA,IAC3B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,yBACJ,OACe;AACf,UAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,qBAAqB;AAAA,MAC1D,kBAAkB,MAAM;AAAA,MACxB,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,oBAAoB;AAAA,IACtB,CAAC;AAED,QAAI,UAAU;AACZ,eAAS,YAAY;AACrB,eAAS,YAAY;AACrB,eAAS,aAAa,KAAK,aAAa,MAAM,UAAU;AACxD,eAAS,kBAAkB,MAAM,mBAAmB,gBAAgB;AACpE,eAAS,YAAY,oBAAI,KAAK;AAC9B,YAAM,KAAK,GAAG,MAAM;AACpB;AAAA,IACF;AAEA,UAAM,SAAS,KAAK,GAAG,OAAO,qBAAqB;AAAA,MACjD,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,OAAO,MAAM,SAAS;AAAA,MACtB,kBAAkB,MAAM;AAAA,MACxB,WAAW;AAAA,MACX,oBAAoB;AAAA,MACpB,YAAY,KAAK,aAAa,MAAM,UAAU;AAAA,MAC9C,iBAAiB,MAAM,mBAAmB,gBAAgB;AAAA,MAC1D,cAAc;AAAA,MACd,WAAW,oBAAI,KAAK;AAAA,MACpB,WAAW,oBAAI,KAAK;AAAA,IACtB,CAAC;AACD,SAAK,GAAG,QAAQ,MAAM;AACtB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,iBAAkC;AACtC,WAAO,KAAK,GAAG,aAAa,aAAa;AAAA,MACvC,WAAW,EAAE,MAAM,oBAAI,KAAK,EAAE;AAAA,IAChC,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,oCAAmD;AAC/D,UAAM,kBAAkB,6BAA6B;AACrD,UAAM,kBAAkB,gBAAgB,QAAQ,CAAC,UAAU,MAAM,WAAW,CAAC,CAAC;AAC9E,UAAM,kBAAkB,gBAAgB,WAAW,IAAI,qBAAqB,CAAC;AAE7E,eAAW,UAAU,kBAAkB;AAAA,MACrC,GAAG;AAAA,MACH,GAAG;AAAA,IACL,CAAC,GAAG;AACF,YAAM,KAAK,yBAAyB,KAAK,qBAAqB,MAAM,CAAC;AAAA,IACvE;AAAA,EACF;AAAA,EAEQ,qBAAqB,QAAqD;AAChF,WAAO;AAAA,MACL,kBAAkB,OAAO;AAAA,MACzB,OAAO,OAAO,SAAS;AAAA,MACvB,YAAY,OAAO;AAAA,MACnB,iBAAiB,KAAK,kBAAkB,OAAO,eAAe;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,MAAc,mBACZ,kBACA,UACA,gBACA,UACe;AACf,UAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,qBAAqB;AAAA,MAC1D;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,YAAY,SAAS,OAAO,UAAU;AACxC,YAAM,IAAI,0BAA0B,iEAAiE,GAAG;AAAA,IAC1G;AAAA,EACF;AAAA,EAEQ,cAAc,UAAyB,gBAAqC;AAClF,QAAI,kBAAkB,CAAC,UAAU;AAC/B,YAAM,IAAI,0BAA0B,qDAAqD,GAAG;AAAA,IAC9F;AAAA,EACF;AAAA,EAEQ,uBAAuB,QAA6B,OAA+B;AACzF,QAAI,MAAM,aAAc,QAAO;AAC/B,QAAI,CAAC,MAAM,SAAU,QAAO;AAC5B,UAAM,iBAAiB,OAAO,YAAY;AAC1C,QAAI,mBAAmB,KAAM,QAAO;AACpC,WAAO,mBAAmB,MAAM;AAAA,EAClC;AAAA,EAEQ,iBACN,gBACA,sBACA,OACM;AACN,QAAI,MAAM,aAAc;AACxB,QAAI,CAAC,MAAM,UAAU;AACnB,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AACA,SAAK,kBAAkB,UAAU,MAAM,UAAU;AAC/C,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AACA,QACE,MAAM,mBAAmB,UACtB,MAAM,mBAAmB,QACzB,yBAAyB,QACzB,yBAAyB,MAAM,gBAClC;AACA,YAAM,IAAI,0BAA0B,gCAAgC,GAAG;AAAA,IACzE;AAAA,EACF;AAAA,EAEQ,uBACN,kBACA,yBACY;AACZ,QAAI,KAAK,eAAe,IAAI,iBAAiB;AAC3C,aAAO;AAAA,IACT;AACA,QAAI,qBAAqB,gBAAgB,SAAU,QAAO;AAC1D,QAAI,qBAAqB,gBAAgB,KAAK;AAC5C,UAAI,4BAA4B,GAAG;AACjC,cAAM,IAAI,0BAA0B,oEAAoE,GAAG;AAAA,MAC7G;AACA,aAAO;AAAA,IACT;AACA,WAAO,0BAA0B,IAAI,QAAQ;AAAA,EAC/C;AAAA,EAEQ,aAAa,QAA6B,UAAyB,gBAAwC;AACjH,QAAI,OAAO,gBAAgB;AACzB,aAAO,OAAO,mBAAmB,kBAAkB,OAAO,aAAa;AAAA,IACzE;AACA,QAAI,OAAO,UAAU;AACnB,aAAO,OAAO,aAAa;AAAA,IAC7B;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,sBACN,MACA,OACA,UACA,gBACQ;AACR,UAAM,YAAY,KAAK,iBAAiB,MAAM,UAAU,cAAc;AACtE,UAAM,aAAa,KAAK,iBAAiB,OAAO,UAAU,cAAc;AACxE,QAAI,cAAc,WAAY,QAAO,YAAY;AACjD,QAAI,KAAK,uBAAuB,MAAM,oBAAoB;AACxD,aAAO,KAAK,qBAAqB,IAAI;AAAA,IACvC;AACA,WAAO,MAAM,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ;AAAA,EAC5D;AAAA,EAEQ,iBACN,QACA,UACA,gBACQ;AACR,QAAI,OAAO,mBAAmB,kBAAkB,OAAO,aAAa,SAAU,QAAO;AACrF,QAAI,CAAC,OAAO,kBAAkB,OAAO,aAAa,SAAU,QAAO;AACnE,QAAI,CAAC,OAAO,kBAAkB,CAAC,OAAO,YAAY,CAAC,OAAO,mBAAoB,QAAO;AACrF,QAAI,CAAC,OAAO,kBAAkB,CAAC,OAAO,YAAY,OAAO,mBAAoB,QAAO;AACpF,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,kBAAkB,WAAyC;AACvE,UAAM,UAAU,MAAM,KAAK,GAAG,QAAQ,aAAa,EAAE,IAAI,UAAU,CAAC;AACpE,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,0BAA0B,oCAAoC,GAAG;AAAA,IAC7E;AACA,QAAI,QAAQ,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAG;AAC7C,YAAM,IAAI,0BAA0B,kCAAkC,GAAG;AAAA,IAC3E;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,cAAc,QAA2C;AACrE,UAAM,OAAO,MAAM;AAAA,MACjB,KAAK;AAAA,MACL;AAAA,MACA,EAAE,IAAI,QAAQ,WAAW,KAAK;AAAA,MAC9B;AAAA,MACA,CAAC;AAAA,IACH;AAEA,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO;AAAA,MACL,IAAI,OAAO,KAAK,EAAE;AAAA,MAClB,UAAU,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI;AAAA,MAClD,gBAAgB,KAAK,iBAAiB,OAAO,KAAK,cAAc,IAAI;AAAA,IACtE;AAAA,EACF;AAAA,EAEQ,aAAa,OAA+B;AAClD,UAAM,WAAW,SAAS,KAAK,eAAe,KAAK;AACnD,WAAO,KAAK;AAAA,MACV,KAAK,IAAI,UAAU,KAAK,eAAe,KAAK,aAAa;AAAA,MACzD,KAAK,eAAe,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEQ,aAAa,SAA0B;AAC7C,QAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,YAAM,IAAI,0BAA0B,wBAAwB,GAAG;AAAA,IACjE;AACA,UAAM,gBAAiB,QAAoC;AAC3D,QAAI,OAAO,kBAAkB,YAAY,cAAc,KAAK,EAAE,WAAW,GAAG;AAC1E,YAAM,IAAI,0BAA0B,wBAAwB,GAAG;AAAA,IACjE;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,UAAU,SAAyC;AACzD,UAAM,iBAAiB,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,EAAE,SAAS,WAAW;AAChF,UAAM,YAAY,WAAW,UAAU,KAAK,cAAc,CAAC,EAAE,OAAO,cAAc,EAAE,OAAO,WAAW;AACtG,WAAO,GAAG,cAAc,IAAI,SAAS;AAAA,EACvC;AAAA,EAEQ,gBAAgB,OAA8C;AACpE,UAAM,CAAC,gBAAgB,SAAS,IAAI,MAAM,MAAM,GAAG;AACnD,QAAI,CAAC,kBAAkB,CAAC,UAAW,QAAO;AAE1C,UAAM,WAAW,WAAW,UAAU,KAAK,cAAc,CAAC,EAAE,OAAO,cAAc,EAAE,OAAO,WAAW;AACrG,UAAM,kBAAkB,OAAO,KAAK,SAAS;AAC7C,UAAM,iBAAiB,OAAO,KAAK,QAAQ;AAC3C,QAAI,gBAAgB,WAAW,eAAe,OAAQ,QAAO;AAC7D,QAAI,CAAC,gBAAgB,iBAAiB,cAAc,EAAG,QAAO;AAE9D,QAAI;AACF,YAAM,SAAS,6BAA6B;AAAA,QAC1C,KAAK,MAAM,OAAO,KAAK,gBAAgB,WAAW,EAAE,SAAS,MAAM,CAAC;AAAA,MACtE;AACA,UAAI,CAAC,OAAO,QAAS,QAAO;AAC5B,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,gBAAwB;AAC9B,WAAO,QAAQ,IAAI,2BACd,QAAQ,IAAI,mBACZ,QAAQ,IAAI,cACZ;AAAA,EACP;AAAA,EAEQ,kBACN,QAC6B;AAC7B,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,eAAO,gBAAgB;AAAA,MACzB,KAAK;AACH,eAAO,gBAAgB;AAAA,MACzB,KAAK;AAAA,MACL;AACE,eAAO,gBAAgB;AAAA,IAC3B;AAAA,EACF;AACF;AAEA,IAAO,+BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { toAbsoluteUrl } from "@open-mercato/shared/lib/url";
|
|
3
3
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
4
|
import { emitSsoEvent } from "../../../events.js";
|
|
5
|
+
import { resolveSsoCallbackErrorCode } from "../../../lib/errors.js";
|
|
5
6
|
const metadata = {
|
|
6
7
|
GET: { requireAuth: false },
|
|
7
8
|
POST: { requireAuth: false }
|
|
@@ -66,8 +67,7 @@ async function handleCallback(req) {
|
|
|
66
67
|
void emitSsoEvent("sso.login.failed", {
|
|
67
68
|
reason: err instanceof Error ? err.message : "callback_failed"
|
|
68
69
|
}).catch((e) => console.error("[SSO Event]", e));
|
|
69
|
-
const
|
|
70
|
-
const errorCode = message.includes("email is not verified") ? "sso_email_not_verified" : "sso_failed";
|
|
70
|
+
const errorCode = resolveSsoCallbackErrorCode(err);
|
|
71
71
|
return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`));
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/sso/api/callback/oidc/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../../services/ssoService'\nimport { emitSsoEvent } from '../../../events'\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nasync function handleCallback(req: Request): Promise<NextResponse> {\n try {\n const url = new URL(req.url)\n const callbackParams: Record<string, string> = {}\n url.searchParams.forEach((value, key) => {\n callbackParams[key] = value\n })\n\n if (req.method === 'POST') {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const form = await req.formData()\n form.forEach((value, key) => {\n callbackParams[key] = String(value)\n })\n }\n }\n\n const stateCookie = parseCookie(req, 'sso_state')\n if (!stateCookie) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_state_missing'))\n }\n\n if (callbackParams.error) {\n void emitSsoEvent('sso.login.failed', {\n reason: callbackParams.error,\n }).catch((e) => console.error('[SSO Event]', e))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))\n }\n\n if (!callbackParams.code || !callbackParams.state) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_params'))\n }\n\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const result = await ssoService.handleOidcCallback(callbackParams, stateCookie, redirectUri)\n\n const res = NextResponse.redirect(toAbsoluteUrl(req, result.redirectUrl))\n\n res.cookies.set('auth_token', result.token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 60 * 60 * 8,\n })\n\n res.cookies.set('session_token', result.sessionToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n expires: result.sessionExpiresAt,\n })\n\n res.cookies.set('sso_state', '', { path: '/', maxAge: 0 })\n\n return res\n } catch (err) {\n console.error('[SSO Callback] Error:', err)\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'callback_failed',\n }).catch((e) => console.error('[SSO Event]', e))\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEvC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { toAbsoluteUrl } from '@open-mercato/shared/lib/url'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SsoService } from '../../../services/ssoService'\nimport { emitSsoEvent } from '../../../events'\nimport { resolveSsoCallbackErrorCode } from '../../../lib/errors'\n\nexport const metadata = {\n GET: { requireAuth: false },\n POST: { requireAuth: false },\n}\n\nfunction parseCookie(req: Request, name: string): string | null {\n const cookie = req.headers.get('cookie') || ''\n const m = cookie.match(new RegExp('(?:^|;\\\\s*)' + name + '=([^;]+)'))\n return m ? decodeURIComponent(m[1]) : null\n}\n\nasync function handleCallback(req: Request): Promise<NextResponse> {\n try {\n const url = new URL(req.url)\n const callbackParams: Record<string, string> = {}\n url.searchParams.forEach((value, key) => {\n callbackParams[key] = value\n })\n\n if (req.method === 'POST') {\n const contentType = req.headers.get('content-type') || ''\n if (contentType.includes('application/x-www-form-urlencoded')) {\n const form = await req.formData()\n form.forEach((value, key) => {\n callbackParams[key] = String(value)\n })\n }\n }\n\n const stateCookie = parseCookie(req, 'sso_state')\n if (!stateCookie) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_state_missing'))\n }\n\n if (callbackParams.error) {\n void emitSsoEvent('sso.login.failed', {\n reason: callbackParams.error,\n }).catch((e) => console.error('[SSO Event]', e))\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_idp_error'))\n }\n\n if (!callbackParams.code || !callbackParams.state) {\n return NextResponse.redirect(toAbsoluteUrl(req, '/login?error=sso_missing_params'))\n }\n\n const redirectUri = toAbsoluteUrl(req, '/api/sso/callback/oidc')\n const container = await createRequestContainer()\n const ssoService = container.resolve<SsoService>('ssoService')\n\n const result = await ssoService.handleOidcCallback(callbackParams, stateCookie, redirectUri)\n\n const res = NextResponse.redirect(toAbsoluteUrl(req, result.redirectUrl))\n\n res.cookies.set('auth_token', result.token, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n maxAge: 60 * 60 * 8,\n })\n\n res.cookies.set('session_token', result.sessionToken, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV !== 'development',\n expires: result.sessionExpiresAt,\n })\n\n res.cookies.set('sso_state', '', { path: '/', maxAge: 0 })\n\n return res\n } catch (err) {\n console.error('[SSO Callback] Error:', err)\n void emitSsoEvent('sso.login.failed', {\n reason: err instanceof Error ? err.message : 'callback_failed',\n }).catch((e) => console.error('[SSO Event]', e))\n const errorCode = resolveSsoCallbackErrorCode(err)\n return NextResponse.redirect(toAbsoluteUrl(req, `/login?error=${errorCode}`))\n }\n}\n\nexport async function GET(req: Request) {\n return handleCallback(req)\n}\n\nexport async function POST(req: Request) {\n return handleCallback(req)\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'SSO',\n summary: 'OIDC callback',\n methods: {\n GET: {\n summary: 'Handle OIDC callback (GET)',\n description: 'Receives the authorization code from the IdP, exchanges it for tokens, resolves the user, and issues auth cookies.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n POST: {\n summary: 'Handle OIDC callback (POST)',\n description: 'Some IdPs send the callback as a POST (form_post response mode). Handles the same flow as the GET variant.',\n tags: ['SSO'],\n responses: [\n { status: 302, description: 'Redirect to application with auth cookies set', mediaType: 'text/html' },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"sso.admin.action.test": "Erkennung überprüfen",
|
|
25
25
|
"sso.admin.activated": "SSO-Konfiguration aktiviert",
|
|
26
26
|
"sso.admin.activity.empty": "Noch keine SSO-Anmeldeaktivität. Aktivitäten werden hier angezeigt, sobald Benutzer sich über SSO anmelden.",
|
|
27
|
+
"sso.admin.backToList": "Zurück zu SSO-Konfigurationen",
|
|
27
28
|
"sso.admin.banner.activateNow": "Jetzt aktivieren",
|
|
28
29
|
"sso.admin.banner.created": "Ihre SSO-Konfiguration wurde erstellt. Möchten Sie sie jetzt aktivieren?",
|
|
29
30
|
"sso.admin.banner.notYet": "Noch nicht",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"sso.admin.error.domainRemoveFailed": "Domain konnte nicht entfernt werden",
|
|
54
55
|
"sso.admin.error.loadFailed": "SSO-Konfiguration konnte nicht geladen werden",
|
|
55
56
|
"sso.admin.error.noDomainsForActivation": "Fügen Sie mindestens eine erlaubte E-Mail-Domain hinzu, bevor Sie aktivieren",
|
|
57
|
+
"sso.admin.error.notFound": "SSO-Konfiguration nicht gefunden.",
|
|
56
58
|
"sso.admin.error.saveFailed": "SSO-Konfiguration konnte nicht gespeichert werden",
|
|
57
59
|
"sso.admin.error.testFailed": "Verbindungstest fehlgeschlagen",
|
|
58
60
|
"sso.admin.field.autoLinkByEmail": "Automatische Verknüpfung per E-Mail",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"sso.admin.action.test": "Verify Discovery",
|
|
25
25
|
"sso.admin.activated": "SSO configuration activated",
|
|
26
26
|
"sso.admin.activity.empty": "No SSO login activity yet. Activity will appear here once users start logging in via SSO.",
|
|
27
|
+
"sso.admin.backToList": "Back to SSO configurations",
|
|
27
28
|
"sso.admin.banner.activateNow": "Activate Now",
|
|
28
29
|
"sso.admin.banner.created": "Your SSO configuration has been created. Would you like to activate it now?",
|
|
29
30
|
"sso.admin.banner.notYet": "Not Yet",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"sso.admin.error.domainRemoveFailed": "Failed to remove domain",
|
|
54
55
|
"sso.admin.error.loadFailed": "Failed to load SSO configuration",
|
|
55
56
|
"sso.admin.error.noDomainsForActivation": "Add at least one allowed email domain before activating",
|
|
57
|
+
"sso.admin.error.notFound": "SSO configuration not found.",
|
|
56
58
|
"sso.admin.error.saveFailed": "Failed to save SSO configuration",
|
|
57
59
|
"sso.admin.error.testFailed": "Connection test failed",
|
|
58
60
|
"sso.admin.field.autoLinkByEmail": "Auto-link by Email",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"sso.admin.action.test": "Verificar descubrimiento",
|
|
25
25
|
"sso.admin.activated": "Configuración SSO activada",
|
|
26
26
|
"sso.admin.activity.empty": "Aún no hay actividad de inicio de sesión SSO. La actividad aparecerá aquí cuando los usuarios comiencen a iniciar sesión a través de SSO.",
|
|
27
|
+
"sso.admin.backToList": "Volver a configuraciones SSO",
|
|
27
28
|
"sso.admin.banner.activateNow": "Activar ahora",
|
|
28
29
|
"sso.admin.banner.created": "Su configuración SSO ha sido creada. ¿Desea activarla ahora?",
|
|
29
30
|
"sso.admin.banner.notYet": "Aún no",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"sso.admin.error.domainRemoveFailed": "Error al eliminar dominio",
|
|
54
55
|
"sso.admin.error.loadFailed": "Error al cargar la configuración SSO",
|
|
55
56
|
"sso.admin.error.noDomainsForActivation": "Agregue al menos un dominio de correo electrónico permitido antes de activar",
|
|
57
|
+
"sso.admin.error.notFound": "Configuración SSO no encontrada.",
|
|
56
58
|
"sso.admin.error.saveFailed": "Error al guardar la configuración SSO",
|
|
57
59
|
"sso.admin.error.testFailed": "La prueba de conexión falló",
|
|
58
60
|
"sso.admin.field.autoLinkByEmail": "Vinculación automática por correo electrónico",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"sso.admin.action.test": "Zweryfikuj Discovery",
|
|
25
25
|
"sso.admin.activated": "Konfiguracja SSO aktywowana",
|
|
26
26
|
"sso.admin.activity.empty": "Brak aktywności logowania SSO. Aktywność pojawi się tutaj, gdy użytkownicy zaczną logować się przez SSO.",
|
|
27
|
+
"sso.admin.backToList": "Wróć do konfiguracji SSO",
|
|
27
28
|
"sso.admin.banner.activateNow": "Aktywuj teraz",
|
|
28
29
|
"sso.admin.banner.created": "Konfiguracja SSO została utworzona. Czy chcesz ją teraz aktywować?",
|
|
29
30
|
"sso.admin.banner.notYet": "Jeszcze nie",
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"sso.admin.error.domainRemoveFailed": "Nie udało się usunąć domeny",
|
|
54
55
|
"sso.admin.error.loadFailed": "Nie udało się załadować konfiguracji SSO",
|
|
55
56
|
"sso.admin.error.noDomainsForActivation": "Dodaj co najmniej jedną dozwoloną domenę e-mail przed aktywacją",
|
|
57
|
+
"sso.admin.error.notFound": "Nie znaleziono konfiguracji SSO.",
|
|
56
58
|
"sso.admin.error.saveFailed": "Nie udało się zapisać konfiguracji SSO",
|
|
57
59
|
"sso.admin.error.testFailed": "Test połączenia nie powiódł się",
|
|
58
60
|
"sso.admin.field.autoLinkByEmail": "Automatyczne łączenie po e-mailu",
|