Owner decision: keep the architecture (static, independently hostable, no
BitLogin-operated OAuth service, shared signer, or central login domain)
and strengthen recovery instead.
Passkey registration now runs the phrase ceremony BEFORE granting a
session -- phrase shown, three words verified, then dashboard -- exactly
as password signup does. The deferred 'Secure your account' card is gone,
along with the securePhrasePending state behind it: that design was
inherited from the OAuth on-ramps, where a service still held a copy of
the credential. Nothing holds a copy for a passkey account, so deferring
the only recovery path was strictly worse than the design it came from.
Note the recovery EXPORT is not an alternative here: by design it carries
no phrase-derived key and cannot recover an account alone (§19.5), so the
phrase ceremony is the only thing that closes the trap.
Also documents the loss mode that surprises people: a passkey is bound to
the site's WebAuthn RP id, so a deployment changing domain leaves its
users' passkeys unable to unlock anything there -- account untouched on
relays, phrase still works.
The e2e now drives the real ceremony (reads the 12 words from the DOM,
answers the three-word quiz) and asserts activeMethod() is null until it
completes, so 'no session before recovery' is enforced against a real
browser. New policy pin: runPasskeyRegistration must hand off to the
phrase screens and must not dispatch a session itself.
269 tests green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Replaces the vague 'modern authenticator stack' line with an observed
snapshot table: Google Password Manager, iCloud Keychain, Bitwarden, and
hmac-secret hardware keys work; KeePassXC 2.7.12 does not (it manages
passkeys and ships credProps, but no prf/hmac-secret -- verified by
binary inspection, and PRF must be evaluated inside the authenticator, so
its browser extension cannot supply it); de-Googled Android is unreliable
and better served by the NIP-46 rail. States plainly that uneven support
is the method's real limitation, that only the ceremony is authoritative,
and that a deployment must never make a passkey the only route to an
account.
Co-Authored-By: Claude Fable 5 <[email protected]>
Owner decision: static only, and no per-site provider registration --
which every OAuth path requires (and GitHub/Facebook additionally require
a server). Both on-ramp implementations (service handshake, serverless
Google Drive) are removed the day they shipped; centralized-onramps.md
stays on the record as the analysis of why, superseded by
passkey-login.md.
The passkey rail: a WebAuthn PRF-capable passkey (synced for most users
by Google Password Manager or iCloud Keychain) deterministically derives
the login name and password of a standard capsule account -- passkey.ts
holds the derivation behind a FROZEN v1 known-answer contract with the
same standing rule as knownAnswer.test.ts. The ordinary password flows
do everything else; nothing is stored outside the user's authenticator,
and the rail needs no network of its own.
Flow is idempotent by construction: sign-in against a passkey with no
account routes into setup with the derived credential
(AccountNotFoundError); setup against an existing account signs in
(AccountAlreadyExistsError). Fresh identity or §SF10 nsec import at
setup; Tier B2 phrase card retained ('your account stays yours even if
you lose your passkey'); rotation is the graduation step and says so.
Pins updated: passkey row with honest custody label inside the menu,
OAuth actions provably absent, element still network-free. 11 passkey
tests incl. the frozen vectors; menu and screens verified in Chrome.
Co-Authored-By: Claude Fable 5 <[email protected]>