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.

Passkeys cover the happy path beautifully. The failure modes — the user buys a new laptop, switches from Chrome to Firefox, drops their phone — need explicit handling. This page is the playbook.

The four failure modes

What happenedWhat the SDK throwsWhat the user seesWhat you do
User cancelled the promptsignShhhMessage rejects with NotAllowedError translated to Error("Passkey authentication was cancelled")”Authentication cancelled”Show the prompt button again. No data loss.
New device, no passkey syncedhasShhhPasskey() returns falseSign-up pageIf the user already has a wallet, route them to recovery (see below).
PRF unavailable after a browser switch (legacy CHIPI v29)getWalletEncryptKey throws "Passkey PRF unavailable…"”Use PIN to sign in”Show PIN field. The PRF-encrypted wallet can be re-encrypted later via useUpdateWalletEncryption.
Lost device, no other passkeynothing throws — user is locked out”Recover your wallet” UISHHH: use the guardian recovery flow. CHIPI v29: only recoverable via the PIN backup.

SHHH wallets — true recovery

A SHHH wallet’s owner set lives on chain. Recovery is a real on-chain primitive, not a workaround. Two paths:
  1. The user still has access (e.g. moved from old phone to new) — propose_add_owner adds the new passkey as a second owner with a 48-hour timelock, then execute_add_owner activates it. See the recovery story in /services/gasless/recovery.
  2. The user lost all access — a pre-registered guardian (a second device, a recovery email’s WebAuthn credential, a “Guardian-as-a-Service” partner) calls initiate_recovery. After a 7-day timelock the guardian can finalize a wallet rotation onto a fresh passkey.
The React side ships <Recover /> which drives both flows. The recipe at a glance:
import { Recover, useGuardianRecovery } from "@chipi-stack/chipi-react";
import { createShhhPasskey } from "@chipi-stack/chipi-passkey";

// 1. User on the new device registers a fresh passkey.
const { credentialId, publicKeyHex } = await createShhhPasskey(userId, userName);

// 2. Drop into <Recover mode="add-device" /> — see the gasless recovery docs for full wiring.

CHIPI v29 wallets — dual-key fallback

A CHIPI v29 wallet’s passkey only encrypts a STARK key; if the passkey is gone, the encryption key is gone with it. The mitigation is the dual-key architecture: store the same private key TWICE, once encrypted by the passkey, once encrypted by a user-chosen PIN. The wallet shape carries two ciphertexts:
type Wallet = {
  publicKey: string;
  encryptedPrivateKey: string;       // passkey-derived key encrypts this
  encryptedPrivateKeyBackup: string; // PIN encrypts this
  authMethod: "passkey+pin";         // marker for the dual-key path
};
Pass both at creation time and at every wallet read:
import { useCreateWallet } from "@chipi-stack/chipi-react";
import { createWalletPasskey } from "@chipi-stack/chipi-passkey";

const { encryptKey: passkeyKey } = await createWalletPasskey(userId, userName);
const pin = await promptForPin();

await createWalletAsync({
  params: {
    externalUserId: userId,
    chain: "STARKNET",
    walletType: "CHIPI",
    encryptKey: passkeyKey,    // primary — encrypts encryptedPrivateKey
    encryptKeyBackup: pin,      // backup — encrypts encryptedPrivateKeyBackup
    authMethod: "passkey+pin",
  },
  bearerToken,
});
Day-to-day signing uses the passkey. If the passkey ever fails (verifyWalletPasskeyDetailed() returns reason: "prf_unavailable" or "no_passkey"), prompt for the PIN and use the encryptedPrivateKeyBackup ciphertext instead. The SDK handles which ciphertext to pick when you pass the right encryptKey.

PIN-only mode is a fallback, not a default

A PIN-only wallet (no passkey) is supported, but it’s the weakest path — see the PIN warning we use across every onboarding doc. Default to passkey; show PIN only when isWebAuthnSupported() returns false or the user explicitly opts in.
import { isWebAuthnSupported } from "@chipi-stack/chipi-passkey";

function SignUpFlow() {
  if (isWebAuthnSupported()) {
    return <PasskeySignUp />;
  }
  // Old browser, very old Android, or a corporate-managed device that
  // disables WebAuthn. Fall back to PIN.
  return <PinSignUp />;
}

Browser switching (the common case)

Even fully-supported browsers don’t always share PRF state. The user registers on Chrome, then opens your app in Firefox a week later. The Firefox WebAuthn ceremony succeeds — the credential is discoverable — but the PRF output is unavailable in that browser. For a SHHH wallet, this is not a problem: the passkey itself is the signer, no encryption key derivation. signShhhMessage works on any browser that supports WebAuthn. For a legacy CHIPI v29 wallet, this is the problem verifyWalletPasskeyDetailed() catches. Surface the PIN fallback UI, decrypt with the PIN, and offer to re-register the passkey on the new browser via the migration hook.