feat(search): accept nostr: identifiers, and never search a secret key - #744
Conversation
Every client renders references as `nostr:nevent1…` — NIP-21's URI form —
so that is what the copy button puts on the clipboard and what people
paste into a search box. The bare bech32 the search bars checked for is
the exception, not the rule: a pasted reference fell through to a
full-text search for its own base32, which finds nothing.
$lib/nip19Input reads the identifier once, for note, nevent, npub,
nprofile and naddr alike, in either scheme spelling and any casing. It
decodes rather than prefix-matches, so `nevent1` followed by junk stays
an ordinary search term instead of routing to a page that can only fail.
Four slightly different hand-rolled copies of `.replace('nostr:', '')`
collapse into it.
It also refuses an nsec. NIP-50 puts the search term verbatim into the
filter it sends, so pasting a secret key into the box published it to
every search relay the app talks to — a silent, unrecoverable failure
one keystroke away from an ordinary mis-paste. It is now neither
searched, nor routed, nor written into a URL.
Applied at all four entry points: the header dropdown, the mobile
overlay, both submit paths, and the results page, which redirects with
replaceState so Back reaches where the reader came from rather than a
search that never ran.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
There are remaining paths where secret-key input can still be propagated into UI state/URLs (and some guard logic/docs are inconsistent), which needs tightening to meet the stated “never write/search a secret key” goal.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 3
Open (5)
What changed in this PR
This PR improves search UX by correctly handling NIP-21 nostr: / web+nostr:-prefixed NIP-19 identifiers (so pasted references navigate to the right destination instead of falling through to text search), and adds protection to prevent secret keys from being sent to NIP-50 search relays.
Changes:
- Added
$lib/nip19Inputhelpers to normalize and decode pasted NIP-19 input (with or without scheme/casing/whitespace). - Updated all search entry points (header, mobile overlay, autocomplete,
/searchpage) to route identifiers directly and to refusensecinput. - Added unit tests for the new normalization/parse behavior and bumped app version.
| File | Description |
|---|---|
| src/routes/search/+page.svelte | Redirects identifier queries to their target route; blocks secret-key searches. |
| src/lib/nip19Input.ts | New central parser/normalizer for NIP-19 input and secret-key detection. |
| src/lib/nip19Input.test.ts | Unit tests for prefix stripping, identifier parsing, and secret-key refusal. |
| src/components/TagsSearchAutocomplete.svelte | Uses centralized parsing/secret-key guard for autocomplete behavior. |
| src/components/MobileSearchOverlay.svelte | Routes identifiers directly from mobile search; blocks secret keys. |
| src/components/Header.svelte | Routes identifiers directly from header search; blocks secret keys. |
| package.json | Version bump. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Header/MobileSearchOverlay openTag: refuse nsec before routing, so a secret key can never become `/tag/nsec1…` either. - /search: on secret-key input, replaceState to drop `q` (keeping other params like `type`) so the key leaves the address bar and history. - TagsSearchAutocomplete: drop unused stripNostrPrefix import. - nip19Input: docstring now states exactly what isSecretKeyInput matches (bech32 nsec, prefixed or bare, any case) and why hex is not. - One more test covering web+nostr:, casing, truncation, and hex. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>


Pasting a reference someone shared with you didn't work:
Every client renders references in NIP-21's URI form, so that's what the copy button puts on the clipboard and what people paste. The search bars only checked for bare bech32 — the exception, not the rule — so a pasted reference fell through to a full-text search for its own base32 and found nothing.
One place to read an identifier
$lib/nip19Inputhandlesnote1,nevent1,npub1,nprofile1andnaddr1alike, with or without the scheme, in either spelling (nostr:and the registered-handlerweb+nostr:), any casing, and the whitespace a paste drags along.It decodes rather than prefix-matches:
nevent1followed by junk stays an ordinary search term instead of routing to a page that can only fail. Four slightly different hand-rolled copies of.replace('nostr:', '')collapse into it.Applied at all four entry points — the header dropdown, the mobile overlay, both submit paths, and the
/searchresults page, which redirects withreplaceStateso Back reaches where the reader came from rather than a search that never ran.A secret key was reaching the relays
Worth its own line, because it's a security fix riding along with a UX one.
NIP-50 puts the search term verbatim into the filter it sends. Pasting an
nsecinto the search box — one keystroke away from an ordinary mis-paste — published a private key to every search relay the app talks to, silently and unrecoverably.isSecretKeyInputnow catches it at every entry point: never searched, never routed, never written into a URL. Happy to split this into its own PR if you'd rather review it separately.Testing
View note: nevent1qvzqqqqqqypzqx38d…(prefix stripped), and following it lands on the note — "Notions · 2h · via Primal iOS"npubarriving at/search?q=…redirects to/user/npub1aeh…pnpm buildclean,svelte-checkunchanged from main