Repository: logto-io/logto
File: packages/core/src/routes/experience/classes/helpers.ts (TypeScript)

Code before:
    }
  }
};

/**
 * Sort MFA factors by display priority to keep client experience consistent.
 * Order: WebAuthn -> TOTP -> Phone -> Email -> Backup code -> others.
 */
export const sortMfaFactors = (factors: MfaFactor[]): MfaFactor[] => {
  const order: MfaFactor[] = [
    MfaFactor.WebAuthn,
    MfaFactor.TOTP,
    MfaFactor.PhoneVerificationCode,
    MfaFactor.EmailVerificationCode,
    MfaFactor.BackupCode,
  ];

  return factors.slice().sort((factorA, factorB) => {
    const indexA = order.indexOf(factorA);
    const indexB = order.indexOf(factorB);
    const normalizedIndexA = indexA === -1 ? order.length : indexA;
    const normalizedIndexB = indexB === -1 ? order.length : indexB;

    return normalizedIndexA - normalizedIndexB;
  });
};

/**
 * @throws {RequestError} -400 if the verification record type is not supported for user identification.
 * @throws {RequestError} -400 if the verification record is not verified.
 * @throws {RequestError} -404 if the user is not found.
 */
// eslint-disable-next-line complexity
export const identifyUserByVerificationRecord = async (
  verificationRecord: VerificationRecord,
  linkSocialIdentity?: boolean
): Promise<{
  user: User;
  /**
   * Returns the social/enterprise SSO synced profiled if the verification record is a social/enterprise SSO verification.
   * - For new linked identity, the synced profile will includes the new social or enterprise SSO identity.
   * - For existing social or enterprise SSO identity, the synced profile will return the synced user profile based on connector settings.
   */
  syncedProfile?: Pick<
    InteractionProfile,
    | 'enterpriseSsoIdentity'
    | 'syncedEnterpriseSsoIdentity'
    | 'jitOrganizationIds'
    | 'socialIdentity'
    | 'avatar'
    | 'name'

Review comment (security):
```suggestion
const order: MfaFactor[] = Object.freeze([
    MfaFactor.WebAuthn,
    MfaFactor.TOTP,
    MfaFactor.PhoneVerificationCode,
    MfaFactor.EmailVerificationCode,
    MfaFactor.BackupCode,
  ]);
  
/**
 * Sort MFA factors by display priority to keep client experience consistent.
 * Order: WebAuthn -> TOTP -> Phone -> Email -> Backup code -> others.
 */
export const sortMfaFactors = (factors: MfaFactor[]): MfaFactor[] => {
  return factors.slice().sort((factorA, factorB) => {
    const indexA = order.indexOf(factorA);
    const indexB = order.indexOf(factorB);
    const normalizedIndexA = indexA === -1 ? order.length : indexA;
    const normalizedIndexB = indexB === -1 ? order.length : indexB;

    return normalizedIndexA - normalizedIndexB;
  });
};
```

Code after:
    }
  }
};

const order: MfaFactor[] = [
  MfaFactor.WebAuthn,
  MfaFactor.TOTP,
  MfaFactor.PhoneVerificationCode,
  MfaFactor.EmailVerificationCode,
  MfaFactor.BackupCode,
];

/**
 * Sort MFA factors by display priority to keep client experience consistent.
 * Order: WebAuthn -> TOTP -> Phone -> Email -> Backup code -> others.
 */
export const sortMfaFactors = (factors: MfaFactor[]): MfaFactor[] => {
  return factors.slice().sort((factorA, factorB) => {
    const indexA = order.indexOf(factorA);
    const indexB = order.indexOf(factorB);
    const normalizedIndexA = indexA === -1 ? order.length : indexA;
    const normalizedIndexB = indexB === -1 ? order.length : indexB;

    return normalizedIndexA - normalizedIndexB;
  });
};

/**
 * @throws {RequestError} -400 if the verification record type is not supported for user identification.
 * @throws {RequestError} -400 if the verification record is not verified.
 * @throws {RequestError} -404 if the user is not found.
 */
// eslint-disable-next-line complexity
export const identifyUserByVerificationRecord = async (
  verificationRecord: VerificationRecord,
  linkSocialIdentity?: boolean
): Promise<{
  user: User;
  /**
   * Returns the social/enterprise SSO synced profiled if the verification record is a social/enterprise SSO verification.
   * - For new linked identity, the synced profile will includes the new social or enterprise SSO identity.
   * - For existing social or enterprise SSO identity, the synced profile will return the synced user profile based on connector settings.
   */
  syncedProfile?: Pick<
    InteractionProfile,
    | 'enterpriseSsoIdentity'
    | 'syncedEnterpriseSsoIdentity'
    | 'jitOrganizationIds'
    | 'socialIdentity'
    | 'avatar'
    | 'name'