# API: passkey unlock Source: https://selfstore.dev/docs/api-passkey Complete reference for selfstore/passkey - passkeyUnlock, PasskeyUnlock and PasskeyUnlockOptions. Open a store with Face, a fingerprint or Windows Hello instead of typing the password, using the WebAuthn PRF extension. Open a store with the device instead of the keyboard. A platform passkey carrying the WebAuthn PRF extension yields a stable 32-byte secret, and this module seals your own secret behind it - normally the backup password - so unlocking becomes a fingerprint instead of typing. ```ts import { passkeyUnlock } from 'selfstore/passkey'; const device = passkeyUnlock({ appName: 'Ledger', salt: 'ledger-unlock-v1' }); // Offer it only where the device can recognise someone. if (await device.available()) { await device.enroll(password); // false when the user cancels, or PRF is absent } // Later, on the opening screen: const password = await device.reveal(); // null means: ask for it ``` ## It carries the password, it does not replace it This is the part to get right before shipping it. The password keeps opening everything, on every device, and stays the thing that encrypts the backup. What this module adds is bound to **one browser profile on one machine**. Losing the passkey costs a typed password, never data, and that asymmetry is the design: granting the data a second independent key widens what a stolen device gives away, which is the opposite of what a [locked cache](https://selfstore.dev/docs/api-store) is for. So the password field belongs on the screen even when a passkey is enrolled. An unlock that cannot fall back is a lockout waiting for the day the authenticator says no. ## What it preserves The PRF secret never leaves the authenticator and demands user verification, so the sealed blob is useless on its own: a copy of the browser profile opens nothing. That is precisely the guarantee a lock-mode cache exists for, so enrolling does not trade it away. ## passkeyUnlock ```ts function passkeyUnlock(options: PasskeyUnlockOptions): PasskeyUnlock; ``` Nothing here runs unless you call it. There is no global switch and nothing turns itself on: an app that never calls `passkeyUnlock()` has no such capability, and `forget()` is the documented way back out. ## PasskeyUnlockOptions ```ts interface PasskeyUnlockOptions { appName: string; // shown by the operating system's passkey prompt salt: string; // domain separator, not a secret storageKey?: string; // default: 'selfstore.passkey' } ``` `salt` may be a constant: the credential's own secret is what makes the derived key unique, so this only separates domains. Changing it rotates every passkey-derived key at once, which orphans every existing enrolment - pick one and leave it alone. Give `storageKey` a distinct value if one origin hosts two apps, so that enrolling in one does not answer for the other. ## PasskeyUnlock ```ts interface PasskeyUnlock { available(): Promise; enrolled(): boolean; enroll(secret: string): Promise; reveal(): Promise; forget(): void; } ``` | Method | What it answers | | --- | --- | | `available()` | A user-verifying platform authenticator exists. **Necessary, not sufficient** - only an actual `enroll()` proves the PRF extension works here, so use this to decide whether to show the control, not to promise it will succeed. | | `enrolled()` | This device currently holds a sealed secret. Synchronous, so an opening screen can branch on it without awaiting anything. | | `enroll(secret)` | Creates the passkey and seals `secret` behind it. `false` on cancel, or where PRF yields nothing. | | `reveal()` | Asks for Face / fingerprint / Hello and returns the secret. `null` on cancel, on a missing enrolment, or on any failure. Never throws. | | `forget()` | Drops the sealed secret from this device. | `forget()` does **not** delete the platform credential: the operating system owns it, and only the person in front of the machine can remove it from their passkey manager. Say that in your interface rather than implying a full erasure. ## It fails closed The dangerous case is a platform that announces an authenticator, lets the credential be created, and returns no PRF result. Storing anything there would leave an enrolment nothing could ever open, so `enroll()` refuses instead and returns `false`. A result of the wrong size is refused too. There is also a self-heal: an enrolment the passkey can no longer open clears itself on the next `reveal()` rather than failing at every attempt for good. Both mean the same thing for your interface - **a `false` or a `null` is a state to say out loud**, not a glitch to swallow. The person is about to wonder why nothing happened. ## Where it works The PRF extension is required: recent Chrome and Edge, Safari 18+, recent Android. Inside a native WebView it depends on the system WebView version. Nothing trusts that blindly - `available()` and `enroll()` feature-detect at runtime, so the option is only ever usable where a real secret comes back. Everywhere else the password stays what it always was. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt