Skip to content

feat(search): accept nostr: identifiers, and never search a secret key - #744

Merged
spe1020 merged 2 commits into
zapcooking:mainfrom
dmnyc:feat/search-nostr-prefix
Sep 21, 2026
Merged

spe1020 merged 2 commits into
zapcooking:mainfrom
dmnyc:feat/search-nostr-prefix

Conversation

@dmnyc

@dmnyc dmnyc commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Pasting a reference someone shared with you didn't work:

nostr:nevent1qvzqqqqqqypzqx38dj7dmt98sxqny0qece578vtac8hkzla2r9xqhyjkv9tsms72qqs…

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/nip19Input handles note1, nevent1, npub1, nprofile1 and naddr1 alike, with or without the scheme, in either spelling (nostr: and the registered-handler web+nostr:), any casing, and the whitespace a paste drags along.

It decodes rather than prefix-matches: 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.

Applied at all four entry points — the header dropdown, the mobile overlay, both submit paths, and the /search results page, which redirects with replaceState so 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 nsec into 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.

isSecretKeyInput now 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

  • The reference above, typed into the header search: dropdown shows View note: nevent1qvzqqqqqqypzqx38d… (prefix stripped), and following it lands on the note — "Notions · 2h · via Primal iOS"
  • A prefixed npub arriving at /search?q=… redirects to /user/npub1aeh…
  • 10 new unit tests: both scheme spellings, casing, whitespace, bare identifiers, non-decoding junk, nsec refusal prefixed and bare
  • 1988 tests green, pnpm build clean, svelte-check unchanged from main

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 2 Low severity

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/nip19Input helpers to normalize and decode pasted NIP-19 input (with or without scheme/casing/whitespace).
  • Updated all search entry points (header, mobile overlay, autocomplete, /search page) to route identifiers directly and to refuse nsec input.
  • 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.

Comment thread src/components/Header.svelte
Comment thread src/components/MobileSearchOverlay.svelte
Comment thread src/routes/search/+page.svelte
Comment thread src/components/TagsSearchAutocomplete.svelte Outdated
Comment thread src/lib/nip19Input.ts
- 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>
@spe1020
spe1020 merged commit 2cb30b6 into zapcooking:main Sep 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants