fix: single unlock prompt (readable DMs) + accept nsec on import#24
Merged
jjohare merged 2 commits intoJul 23, 2026
Merged
Conversation
…ompt A relying page that logs in fires several key operations back to back — GET_PUBLIC_KEY (identity), SIGN_EVENT (NIP-42 relay AUTH), and nip44.decrypt (gift-wrapped DMs). Each hit ensureUnlocked() on a locked vault, opened its OWN popup, and rejected immediately: the user faced three passphrase prompts, and the rejected nip44.decrypt made encrypted DMs silently un-readable (the app saw a 'locked' error, not a decryptable message). ensureUnlocked() now opens ONE unlock popup and awaits it, coalescing every concurrent caller onto a single shared unlock via chrome.storage.onChanged (which reliably wakes the MV3 service worker). One passphrase entry — which writes the key to chrome.storage.session — resolves them all. A race-window re-check avoids a spurious popup when the key has already landed. Fixes: three-prompts-per-login, and encrypted DMs unreadable under NIP-07. Tests: 141 pass, 0 fail. Lint clean. Bundle rebuilt (gitignored; CI regenerates). Co-Authored-By: jjohare <github@thedreamlab.uk>
Importing an existing key only accepted raw 64-char hex; a pasted NIP-19 nsec1… key (how most Nostr apps display keys) was rejected, which surfaced as a silent failure. handleImportKeypair now normalises the input via normalizeSecretKeyToHex: raw hex passes through (lowercased), an nsec1… is bech32-decoded to hex inline — no format nag, the import just provisions. Anything that is neither throws a neutral 'Invalid key'. Self-contained bech32 (BIP-173) decoder in src/keyformat.js keeps Podkey dependency-light (no nostr-tools/@Scure added). NIP-19 bech32 const (1), rejects wrong-hrp (npub), bad checksum, mixed case, and malformed input. Tests: +10 (canonical NIP-19 spec vector + edge cases); suite 141 -> 151, 0 fail. Co-Authored-By: jjohare <github@thedreamlab.uk>
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.
Two related fixes to the vault/session and key-import paths, found while wiring Podkey up as the NIP-07 signer for a Nostr forum (login → NIP-42 relay AUTH → gift-wrapped DMs).
1. Coalesce concurrent unlocks behind one passphrase prompt
A relying page that logs in fires several key operations back to back:
GET_PUBLIC_KEY(identity)SIGN_EVENT(the NIP-42 relay AUTH, kind-22242)nip44.decrypt(reading gift-wrapped DMs)Each one hit
ensureUnlocked()on a locked vault, opened its own unlock popup, and rejected immediately. Two user-visible symptoms:nip44.decryptreturned a "Podkey is locked" error, which the relying app couldn't distinguish from a genuine decrypt failure, so it showed an empty inbox with no explanation.ensureUnlocked()now opens one popup and awaits it, coalescing every concurrent caller onto a single shared unlock viachrome.storage.onChanged(which reliably wakes the MV3 service worker). One passphrase entry — which writes the key intochrome.storage.session— resolves them all. A race-window re-check avoids a spurious popup when the key has already landed.2. Accept
nseckeys on import, convert to hex inlinehandleImportKeypaironly accepted raw 64-char hex; a pasted NIP-19nsec1…key (how most Nostr apps display keys) was rejected, which surfaced as a silent failure on the import screen.Import now normalises the input through
normalizeSecretKeyToHex: raw hex passes through (lowercased), annsec1…is bech32-decoded to hex inline — no format nag, the import just provisions. Anything that is neither throws a neutralInvalid key.The decode is a new self-contained bech32 (BIP-173) decoder (
src/keyformat.js) so the extension keeps its dependency-light footprint — nonostr-tools/@scureadded, only the existing@nobleprimitives elsewhere. It enforces the NIP-19 bech32 checksum and rejects wrong-HRP (npub), bad checksums, mixed case, and malformed input.Tests
test/keyformat.test.js): the canonical NIP-19 spec vector (nsec1vl029mg…↔67dea2ed…) plus hex passthrough, whitespace, uppercase, wrong-HRP, bad-checksum, mixed-case, and malformed-input cases.How to verify
podkey-extensionartifact → unzip → Load unpacked).nsec1…— it provisions straight into the vault with no error and no "wrong format" message.🤖 Generated by Claude Code