Requested pre-promotion sweep of login methods, active signers, and anon session
actors. Four real bugs found and fixed:
1. Logging out could wipe EVERY saved account's remembered nsec key. The no-arg
clearStoredSessionActor() removed the whole per-account v2 map, and it was
reachable from SignerManager.logout() (non-persisted nsec cleanup) and from
the blocked-key cleanup in registerPrivateKeySigner — same bug class as the
NIP-46 session-map wipe fixed earlier. Now: the no-arg form clears only the
legacy v1 slot; logout targets the departing account (and skips entirely when
it can't name one); the blocked-key cleanup targets the blocked pubkey
(mirroring the already-targeted twin in unlockStoredSessionActor).
Spec-corrected the test that had encoded the wipe-all as intended behavior.
2. Removing a saved profile left its keys on the device. The switcher's
remove-account flow cleared NWC + the keep-unlocked cache but kept the
remembered (encrypted) nsec key and the NIP-46 session. New
client.forgetStoredAccount(pubkey) (SignerManager.forgetStoredSessionActor +
clearStoredNip46Session) forgets both WITHOUT touching the active session's
in-memory actors; wired into handleProfileLogoutRequest.
3. Logout never actually re-locked a kept-unlocked device. The forget read
detail.pubkey / this.pubkey — both already nulled by authService.logout()
(the detail only carries previousPubkey) — so the cached decrypted key
silently survived logout. Now reads previousPubkey.
4. restoreUnlockedSigner bypassed access control: a since-blacklisted account's
cached key would silently restore its signer. It now accepts a validator
(mirroring unlockStoredSessionActor), forgets the cached key on denial, and
both app-level call sites (_initAutoLogin, ensureEncryptionCapableSigner)
pass an accessControl.canAccess gate.
Also verified-safe during the pass (no action): logout's persisted-nsec keep
logic under the ephemeral session-actor overwrite; NIP-46 logout keepStored;
the ephemeral actor as a harmless extra DM-decrypt candidate; corrupt-map
recovery clears; anon blacklist-seeding now always has a signing-capable actor.
Tests: session-actor multi-account suite spec-corrected + re-verified (no-arg
clear keeps per-account keys); 57 tests across session-actor / nip46 /
unlocked-key / switch / signer-gate / feed-gate suites green. Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move the encrypted-key store from a single slot to a per-pubkey map so
several saved nsec accounts each keep their own key on the device and the
profile switcher can move between them. Previously bitvid:sessionActor:v1
held one key (last-saved wins), so switching among saved nsec accounts
silently failed.
- sessionActor.js: new bitvid:sessionActors:v2 map keyed by pubkey (follows
the existing bitvid:<thing>:<npub> convention). readStoredSessionActorEntry,
persistSessionActor, clearStoredSessionActor take an optional target pubkey;
legacy v1 slot is kept in sync as the last-saved default and migrated into
the map on first read. IndexedDB mirror keyed per-pubkey. Adds
listStoredSessionActorPubkeys().
- client.js: getStoredSessionActorMetadata / unlockStoredSessionActor accept a
target pubkey; access-denial cleanup forgets only the blocked account.
- nsec provider threads expectPubkey into the unlock so a switch unlocks the
requested account, with the expectPubkey guard as a backstop.
- authSessionCoordinator: look up the target account's stored-key metadata.
NIP-46 multi-account (single slot bitvid:nip46:session:v1) remains as a
follow-up. Tests: tests/session-actor-multi-account.test.mjs (7).
Co-Authored-By: Claude Opus 4.8 <[email protected]>