feat(desktop): gate sign-out behind key backup + typed confirmation#2424
Conversation
Signing out wipes the identity key and all local data, but the confirm dialog was a single unguarded click — one red button between the user and permanent, irrecoverable key destruction. The dialog now requires two explicit steps before "Delete My Data" unlocks: 1. Back up the key — the nsec is shown inline (masked, reveal/copy via the existing NsecMaskedDisplay); the "I have saved my private key" checkbox only unlocks after the user actually reveals or copies the key. If the key cannot be loaded (keychain error) the checkbox stays usable so sign-out is never soft-bricked. 2. Typed confirmation — the user must type "wipe all my data" (trimmed, case-insensitive). Implementation notes: - The sign-out section moves out of ProfileSettingsCard into a new SignOutSection.tsx, bringing ProfileSettingsCard back under the 1000-line ceiling; its TEMP file-size override is removed. - The confirm button is a plain Button rather than AlertDialogAction: Radix's Action closes the dialog on click, which would drop the pending spinner while the wipe + restart is still in flight. - NsecMaskedDisplay gains an optional onKeyInteraction callback (fired on reveal/copy) so flows can gate on real interaction with the key. - The e2e mock bridge now handles sign_out (no-op; specs assert invocation via __BUZZ_E2E_COMMANDS__). - New signout-confirmation.spec.ts covers gate unlock ordering, wrong phrase, sign_out invocation, cancel-resets-state, and the keychain-error degradation path. Co-authored-by: Wes <wesb@block.xyz> Signed-off-by: Wes <wesb@block.xyz>
|
Princess Donut review at The two gates are independently enforced, only successful copy/reveal unlocks backup attestation, dialog state resets on cancel/failure, late Verification at that SHA: I attempted to record an approval, but the authenticated GitHub account is the PR owner, so GitHub correctly rejected self-approval. |
…ad-lifecycle * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044)
…obile-releasing Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044) feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests (#2206) Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> # Conflicts: # RELEASING.md
…ad-lifecycle * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044) Signed-off-by: npub1dpf98sl35hm9k65t8h6cvh5n6knn2msugh5k5nmysxwnw5wlh7uqn2wgp4 <685253c3f1a5f65b6a8b3df5865e93d5a7356e1c45e96a4f64819d3751dfbfb8@sprout-oss.stage.blox.sqprod.co>
Problem
Signing out wipes the identity key and all local data, but the confirm dialog was a single unguarded click — one red button between the user and permanent, irrecoverable key destruction.
Raised by Wes in
improve-signout-safety.Change
The dialog now requires two explicit steps before "Delete My Data" unlocks:
NsecMaskedDisplay); the "I have saved my private key" checkbox only unlocks after the user actually reveals or copies the key. If the key can't be loaded (keychain error) the checkbox stays usable so sign-out is never soft-bricked.wipe all my data(trimmed, case-insensitive).Cancel remains the focused default (Radix AlertDialog), so Enter can never fire the wipe.
Implementation notes
ProfileSettingsCard.tsxinto a newSignOutSection.tsx, bringingProfileSettingsCardback under the 1000-line ceiling; its TEMP file-size override is removed.Buttonrather thanAlertDialogAction: Radix's Action closes the dialog on click, which would drop the pending spinner while the wipe + restart is in flight.NsecMaskedDisplaygains an optionalonKeyInteractioncallback (fired on reveal/copy) so flows can gate on real interaction with the key.sign_out(no-op; specs assert invocation via__BUZZ_E2E_COMMANDS__).Tests
New
signout-confirmation.spec.ts(registered in the smoke project):sign_outVerified at d6023d1:
pnpm typecheck,pnpm check, fullpnpm test(3401 pass), and smoke e2e for the touched specs (14 pass).Screenshots
New dialog — "Delete My Data" locked until both gates pass:
Settings section (unchanged entry point):