Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt

Use this file to discover all available pages before exploring further.

Why Recovery Matters

Passkeys can become unavailable:
  • User switches browsers (Chrome → Firefox)
  • Browser update breaks PRF extension
  • Device reset clears WebAuthn credentials
  • User gets a new device
The dual-key architecture ensures the wallet is always recoverable via PIN.

PIN Backup Flow

When a passkey fails, use the PIN to decrypt:
import { verifyWalletPasskeyDetailed } from "@chipi-stack/chipi-passkey";

// 1. Try passkey first
const result = await verifyWalletPasskeyDetailed();

if (result.valid) {
  // Passkey works — use it normally
  const encryptKey = await getWalletEncryptKey();
  // decrypt wallet with encryptKey
} else if (result.reason === "prf_unavailable") {
  // Passkey exists but PRF failed (browser change)
  // Fall back to PIN
  promptUserForPin();
} else if (result.reason === "no_passkey") {
  // No passkey stored — user needs to enter PIN
  promptUserForPin();
}

Detecting PRF Unavailability

const result = await verifyWalletPasskeyDetailed();

switch (result.reason) {
  case undefined:
    // Valid — passkey works
    break;
  case "no_passkey":
    // No credential in localStorage
    // User needs PIN or to re-register passkey
    break;
  case "prf_unavailable":
    // Credential exists but browser can't derive PRF key
    // DO NOT fall back to PBKDF2 — it produces a different key
    // Use PIN instead
    break;
  case "error":
    // Other error (network, timeout, etc.)
    break;
}
When PRF is unavailable for a credential that was created WITH PRF, the SDK throws instead of silently falling back to PBKDF2. This is intentional — PBKDF2 produces a different key and would fail to decrypt the wallet. Always use PIN backup in this case.

Migration: PIN-Only → Passkey

For wallets created before passkeys were available:
import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs";

function MigrateToPasskey({ wallet, userId }: { wallet: WalletData; userId: string }) {
  const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey();
  const [pin, setPin] = useState("");

  const handleMigrate = async () => {
    const bearerToken = await getToken();

    // User enters their existing PIN to decrypt
    // Then biometric prompt creates the passkey
    const result = await migrateWalletToPasskeyAsync({
      wallet,                    // full WalletData object
      oldEncryptKey: pin,        // current PIN
      externalUserId: userId,    // your user ID
      bearerToken,
    });

    // result.success === true
    // result.credentialId — the new passkey credential
    // result.wallet — updated wallet with authMethod: "passkey+pin"
  };

  return (
    <button onClick={handleMigrate} disabled={isLoading || !pin}>
      Upgrade to Passkey
    </button>
  );
}

Credential Storage

Passkey metadata is stored in localStorage under the key chipi_wallet_passkey_credential:
interface WalletCredentialMetadata {
  credentialId: string;                   // WebAuthn credential ID
  createdAt: string;                      // ISO timestamp
  userId: string;                         // Your external user ID
  transports?: AuthenticatorTransport[];  // ["internal", "hybrid"] etc.
  prfSupported?: boolean;                 // Whether PRF worked at creation
}
Important: prfSupported determines whether to attempt PRF on future auth. If true and PRF fails later (browser change), the SDK throws instead of silently using PBKDF2.

Clearing credentials

import { removeStoredCredential } from "@chipi-stack/chipi-passkey";

// On logout or account reset
removeStoredCredential();
// Clears localStorage — user will need PIN on next login

Best Practices

  1. Always require a PIN backup — never create a passkey-only wallet
  2. Check verifyWalletPasskeyDetailed() before transactions — detect failures early
  3. Show clear UI when passkey fails — “Your biometric isn’t working. Enter your PIN instead.”
  4. Don’t auto-retry passkey on failure — if the browser can’t do PRF, retrying won’t help
  5. Store prfSupported with the wallet — send it to your backend for conditional fallback logic