useRecovery

Account recovery operations for when users lose access to their passkeys.

Recovery Flow: Start recovery with an email address, then complete it with passkeys or backup codes from the active recovery session.

Import

import { useRecovery } from '@heliorim/sdk-react';

Type Signature

interface RecoveryState {
  isInitiating: boolean;
  isRecovering: boolean;
  recoveryMethod: RecoveryMethod | null;
  availableMethods: RecoveryMethod[];
  backupCodes: string[];
  recoverySession: RecoverySession | null;
  error: Error | null;
}

interface RecoveryActions {
  initiateRecovery: (email: string) => Promise<void>;
  selectMethod: (method: RecoveryMethod) => Promise<void>;
  completePasskeyRecovery: () => Promise<Session>;
  generateBackupCodes: () => Promise<string[]>;
  verifyBackupCode: (code: string) => Promise<Session>;
  addRecoveryPasskey: () => Promise<void>;
  cancelRecovery: () => Promise<void>;
  clearError: () => void;
}

type RecoveryMethod = 'passkey' | 'guardian' | 'backup-codes';

interface RecoverySession {
  id: string;
  expiresAt: number;
  attemptsRemaining: number;
}

type UseRecoveryReturn = RecoveryState & RecoveryActions;

Basic Usage

function AccountRecovery() {
  const {
    isInitiating,
    isRecovering,
    availableMethods,
    recoverySession,
    initiateRecovery,
    selectMethod,
    completePasskeyRecovery,
    verifyBackupCode
  } = useRecovery();

  const [email, setEmail] = useState('');

  const startRecovery = async () => {
    await initiateRecovery(email);
    await selectMethod('passkey');
    await completePasskeyRecovery();
  };

  const useBackupCode = async (code: string) => {
    await selectMethod('backup-codes');
    await verifyBackupCode(code);
  };

  return (
    <div>
      <h2>Lost Your Passkey?</h2>

      <div>
        <h3>Start recovery</h3>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter your email"
        />
        <button onClick={startRecovery} disabled={isInitiating || isRecovering}>
          Recover account
        </button>
      </div>

      {recoverySession ? (
        <p>
          Available methods: {availableMethods.join(', ')}. Attempts remaining:
          {' '}{recoverySession.attemptsRemaining}
        </p>
      ) : null}
    </div>
  );
}

Recovery Methods

Passkey Recovery

const {
  initiateRecovery,
  selectMethod,
  completePasskeyRecovery
} = useRecovery();

// Step 1: Identify the account by email
await initiateRecovery('user@example.com');

// Step 2: Choose passkey recovery
await selectMethod('passkey');

// Step 3: Run the WebAuthn assertion flow
await completePasskeyRecovery();

Backup Codes

const {
  initiateRecovery,
  selectMethod,
  generateBackupCodes,
  verifyBackupCode,
  backupCodes
} = useRecovery();

// Generate codes while authenticated
const codes = await generateBackupCodes();
// Returns single-use codes like: "XXXX-XXXX-XXXX"

// Save component to display codes
function BackupCodeDisplay({ codes }) {
  const downloadCodes = () => {
    const blob = new Blob([codes.join('\n')], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'heliorim-backup-codes.txt';
    a.click();
  };

  return (
    <div className="backup-codes">
      <h3>⚠️ Save These Codes</h3>
      <p>Each code can only be used once</p>
      <ul>
        {codes.map(code => <li key={code}>{code}</li>)}
      </ul>
      <button onClick={downloadCodes}>Download Codes</button>
    </div>
  );
}

// Use a backup code for an active recovery session
await initiateRecovery('user@example.com');
await selectMethod('backup-codes');
await verifyBackupCode('ABCD-EFGH-IJKL');

Recovery Passkey

const { addRecoveryPasskey } = useRecovery();

// Recovery passkey registration is reserved for a future backend release.
// The current hook throws until that support ships.
function AddRecoveryDevice() {
  const handleAdd = async () => {
    try {
      await addRecoveryPasskey();
    } catch (err) {
      console.error('Recovery passkey registration not yet implemented in backend');
    }
  };

  return (
    <button onClick={handleAdd}>
      Add Recovery Device (coming soon)
    </button>
  );
}

Complete Recovery Flow

function RecoveryWizard() {
  const {
    availableMethods,
    recoverySession,
    initiateRecovery,
    selectMethod,
    completePasskeyRecovery,
    verifyBackupCode,
    cancelRecovery,
    isInitiating,
    isRecovering,
    error,
    recoveryMethod
  } = useRecovery();

  const [email, setEmail] = useState('');
  const [backupCode, setBackupCode] = useState('');

  const startRecovery = async () => {
    try {
      await initiateRecovery(email);
    } catch (err) {
      console.error('Recovery initiation failed:', err);
    }
  };

  const recoverWithPasskey = async () => {
    try {
      await selectMethod('passkey');
      await completePasskeyRecovery();
    } catch (err) {
      console.error('Passkey recovery failed:', err);
    }
  };

  const recoverWithBackupCode = async () => {
    try {
      await selectMethod('backup-codes');
      await verifyBackupCode(backupCode);
    } catch (err) {
      console.error('Backup-code recovery failed:', err);
    }
  };

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Your email"
      />
      <button onClick={startRecovery} disabled={isInitiating || isRecovering}>
        Start recovery
      </button>

      {recoverySession ? (
        <div>
          <p>Available methods: {availableMethods.join(', ')}</p>
          <p>Attempts remaining: {recoverySession.attemptsRemaining}</p>
          <button onClick={recoverWithPasskey} disabled={isRecovering}>
            Recover with passkey
          </button>
          <input
            type="text"
            value={backupCode}
            onChange={(e) => setBackupCode(e.target.value)}
            placeholder="Backup code"
          />
          <button onClick={recoverWithBackupCode} disabled={isRecovering}>
            Recover with backup code
          </button>
          <button onClick={cancelRecovery}>Cancel</button>
        </div>
      ) : null}

      <p>Selected method: {recoveryMethod ?? 'none'}</p>
      {error ? <p>{error.message}</p> : null}
    </div>
  );
}

Security Best Practices

Recommended Setup

  • Register passkeys on multiple devices
  • Generate and securely store backup codes
  • Keep recovery email up-to-date
  • Test recovery process periodically

Important Notes

  • Backup codes are single-use only
  • Recovery sessions expire after 15 minutes
  • Recovery requires a verified account email
  • Backup codes are only available after authenticated setup

Related Hooks