feat(nostr): track which account owns a local key on shared phones - #727
Draft
islandbitcoin wants to merge 8 commits into
Draft
islandbitcoin wants to merge 8 commits into
islandbitcoin wants to merge 8 commits into
Conversation
The keychain entry holding the Nostr nsec survives logout, so on a phone shared by two Flash accounts, account B can start with the key account A generated. Silently registering it on B strands A: the backend refuses a second owner (NPUB_NOT_AVAILABLE) and A's DMs go to a key B advertises. - app/nostr/key-owner.ts records the owning account next to each local key; generateAndStoreKey and saveNewNostrKey stamp it, deleting keys clears it. - NostrKeyEnsurer treats a key owned by another account as prompt-class: unregistered asks once per (account, key) instead of registering; mismatch names the other account and never answers a refusal with advice to delete that account's key. - The ensurer runs once per account instead of once per mount, since nothing above it remounts on logout. - keyForeignMessage / keyForeignRelinkRefused, translated in all locales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
…copy in Reconnect and for unowned keys, stamp owner on nsec import - NostrKeyEnsurer: a pending prompt carries the accountId that produced it and is dropped unless that account is still signed in; logout and account switches clear it and cancel an in-flight check before it writes with the next account's token. - Refusals advise deleting the chat keys only when this account is the key's recorded owner; unowned (pre-record) and foreign keys get back-up advice. Applies to the ensurer and to Settings > Nostr > Reconnect. - reconnectLocalNpub stamps the owner on success. - importNsec records the importing account as owner once the backend accepts the key and drops the replaced key's owner record. - Clear pre-existing lint errors on this PR's changed lines in use-nostr-profile.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
…witch race test exercise the guards - A refused registration of a key with no owner record (every install from before the record) no longer claims the key belongs to another account on this phone. New keyUnownedRelinkRefused gives back-up advice without that claim; keyForeignRelinkRefused is kept for a key another account owns. Translated in all locales. - keyOwnerState (self | other | unknown) replaces the mayAdviseDeletingKey boolean; reconnectLocalNpub reports keyOwner, and a shared relinkRefusedMessage picks the copy for the ensurer and Reconnect. - The "stops a check still running for A once B is signed in" test now keeps B's check from recording an owner (B has an already-answered mismatch), so a resumed A would reach the silent relink. Verified it fails with the isCurrent() guards removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
Both branches independently guarded against async check results that outlive the session that started them: #720 with a render-time session counter, this branch with a per-account run token. Kept this branch's guard and folded the counter into it, so isCurrent() now also rejects a continuation that resolves between the logout render and the logout effect. - Ensurer docstring: "once per app process" from #720 replaced with the per-account behaviour this branch implements. - Reconnect: #720's report-before-refresh ordering with this branch's owner-aware refusal copy. - #720's logout tests re-sign-in the same account. Here that account is re-checked on purpose, so each test now signs back in to a linked state and any alert or write can only come from the stale check. Disabling isCurrent() fails five of them. - Reconnect screen spec: mock the account query and clear owner records between tests; refusal with no owner record expects the neutral copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
…lines-per-function Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
… feat/nostr-multi-account-keys # Conflicts: # app/hooks/use-nostr-profile.ts
…erge The previous merge resolved a one-comment conflict by taking #720's whole copy of use-nostr-profile.ts, which dropped this branch's owner stamp, foreign-key guard and clearNostrKeyOwners. Restores this branch's file with #720's comment wording on the flash_username lint exception. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
… feat/nostr-multi-account-keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of ENG-601 (multi-account support). Stacked on #720. Base is
fix/nostr-npub-auto-relink; retarget tomainonce #720 merges. Draft until then.Problem
The keychain entry holding the Nostr nsec survives logout (
use-logout.tsnever clears it). On a phone shared by two Flash accounts, account B starts with the key account A generated. #720 registers an unregistered local key silently, so B would claim A's key. The backend then refuses A (NPUB_NOT_AVAILABLE) and A's DMs go to a key B advertises.This is the first piece of multi-account support: two or more Flash accounts sharing one phone.
Change
app/nostr/key-owner.ts: records the owning account next to each local key, keyed on the npub.generateAndStoreKeyandsaveNewNostrKeystamp it. Deleting keys clears it.NostrKeyEnsurer:unregisteredwith a key owned by another account → asks once per (account, key) instead of registering.linked→ adopts ownership, so keys from before this record and imported nsecs get an owner.mismatchwith a foreign-owned key → names the other account, and a refusal never advises deleting that account's key.keyForeignMessage/keyForeignRelinkRefused, translated in all locales.Tests
__tests__/nostr/key-owner.spec.ts,__tests__/nostr/signer.spec.ts(owner stamp).nostr-key-ensurer.spec.tsx: foreign-key prompt, take over on confirm, decline remembered, per-key marker, refusal copy, once-per-account across logout/login, account payload without id.use-nostr-profile-relink.spec.ts: never registers a foreign key, owner stamp on fresh generation, delete clears owners.62 tests pass on this branch.
Out of scope, still open for full multi-account
giftwraps, rumors,lastSeen_*) are device-global and survive logout, so B can see A's decrypted history.🤖 Generated with Claude Code
https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX