fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser) - #7488
fix(desktop): shared npub identity foundation (canonicalNpub, PubKey gate, strict parser)#7488loganj wants to merge 6 commits into
Conversation
…gate, strict parser)
PR-D1a foundation slice for the npub identity display standardization:
the shared primitives every identity surface builds on, split out so the
descendant slice can focus on the surfaces themselves (profile/settings
controls, agents, workflows, Rust display name, guard hints).
## Summary
- shared/lib/pubkey.ts: export canonicalNpub(pubkey) — strict canonical
full-npub helper (64-char hex any case, or checksum-validated npub →
canonical npub; degenerate/short/corrupt → null), alongside the
truncateNpub compact display + UNAVAILABLE_KEY_LABEL foundation.
Base exports (normalizePubkey, truncatePubkey) unchanged.
- shared/ui/PubKey.tsx: the widget's identity gate validates through
canonicalNpub — a degenerate-length hex (npubEncode("deadbeef")
produces a checksum-valid fake npub) renders Unavailable with no copy
affordance in every variant; popover copy is npub-only.
- shared/lib/nostrUtils.ts: parsePubkeyInput rejects npubs whose payload
is not exactly a 64-char identity key, matching the Rust
validate_respond_to_allowlist contract; regression tests pin the
degenerate vectors (npub1m6kmamcvty5gd, npub106246s).
- Tests: pubkey.test.mjs covers canonicalNpub/truncateNpub/label and the
degenerate-encode edge; new PubKey.test.mjs (JSDOM harness per
MentionAutocomplete pattern) covers compact/non-interactive/full-popover
rendering, npub-only copy, and invalid-key suppression;
parsePubkeyInput.test.mjs pins the strict parser vectors.
- e2e (shared-widget boundary only): profile.spec.ts owned-agent public
key row asserts the npub prefix (the row renders through the shared
<PubKey> widget); pubkey-display-screenshots.spec.ts asserts the
shared widget popover text is npub-only while the chip's legacy
raw-hex popover line still documents the D1a boundary. The remaining
profile/settings clipboard assertions and the chip raw-hex line
removal land with their surfaces in the descendant slice.
Validation: pnpm install (hermit); pnpm check; pnpm typecheck; full
desktop unit suite; Playwright pubkey-display (smoke, 4) and profile
key-row/ingress (integration) mockbridge assertions.
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
Tests-only cleanup for the D1a foundation slice, applying the audited
consolidations from the npub test proportionality review. No production
change: PubKey.tsx, pubkey.ts, nostrUtils.ts, and both e2e specs are
untouched, so the original slice's Rust/build/unit evidence still binds.
## Summary
- pubkey.test.mjs: drop the standalone UNAVAILABLE_KEY_LABEL
constant-vocabulary case — the invalid-output test now pins the literal
"Unavailable" — and fold the redundant uppercase-hex spelling into the
compact case. Both independent known vectors, the corrupted-checksum,
and the nsec rejections are retained.
- PubKey.test.mjs: 186 -> 96 lines. The suite duplicated coverage the
existing harnesses already own: profile.spec.ts copies the full
canonical npub through a widget surface and pubkey-display-screenshots
spec mounts this widget's npub-only popover. The slim local suite keeps
only the wiring those harnesses cannot pin: the compact truncated npub
(interactive trigger and non-interactive text), the full npub with its
copy affordance, and the strict identity gate — invalid and
degenerate-length hex ("deadbeef" npubEncodes to a checksum-valid fake
npub) render Unavailable with no copy affordance and no npub1 text. The
bulk JSDOM global-copy scaffolding (needed only to open the Radix
popover) and the duplicated short-invalid render matrix are removed.
Validation: focused rewritten suites green (pubkey, PubKey,
parsePubkeyInput); mutation check — swapping the widget gate to a naive
npubEncode fails the unencodable case; full desktop unit suite
6458/6458; just desktop-check; just desktop-typecheck.
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed: 44316ff72f5f7de014c66b01cbf534298a70c249..2c68dddefcf16e2ee2cc3d0bae7564c5be6ac754 (exact live head 2c68dddefcf16e2ee2cc3d0bae7564c5be6ac754)
Risk: medium — this establishes a shared identity display/parser boundary used by renderer components.
Blocking findings
-
Valid uppercase npubs fail the new canonical display contract (
desktop/src/shared/lib/pubkey.ts:46-60).canonicalNpub()performs a case-sensitivestartsWith("npub1")check before decoding. A wholly uppercase Bech32 npub is valid and accepted by the installednostr-toolsdecoder;parsePubkeyInput()also accepts it because that path lowercases first (desktop/src/shared/lib/nostrUtils.ts:38-47). The same valid identity therefore parses successfully but renders as Unavailable with no copy affordance throughdesktop/src/shared/ui/PubKey.tsx:126-165.Author action: accept all-uppercase valid Bech32 consistently (while continuing to reject mixed-case input), and add
canonicalNpubplus<PubKey>regressions for uppercase, whitespace-wrapped uppercase, and mixed-case rejection.Verification owner: author runs focused regressions; reviewer reruns the exact production probe.
-
The strict decoded-payload gate is not regression-protected (
desktop/src/shared/lib/pubkey.ts:48-54). Removing only!HEX_64_REGEX.test(decoded.data)leaves all 14 helper/widget tests green. Existing helper/widget tests feed short hex, which is rejected before decode; they never send a checksum-valid short npub through this separate seam. Thus the advertised strict gate can regress to displaying/copying degenerate npubs without a test failing, contrary toTESTING.md:25-31.Author action: pass checksum-valid degenerate npubs such as
npub1m6kmamcvty5gdandnpub106246sdirectly throughcanonicalNpuband<PubKey>tests, then mutation-prove removal of the decoded-length check causes a behavioral failure.Verification owner: author records mutation red/green; reviewer spot-checks.
Contracts traced
Reviewed all changed files and renderer callers of canonicalNpub, truncateNpub, parsePubkeyInput, safeNpub, and <PubKey> under desktop/src and desktop/tests; compared the Rust allowlist validation boundary at desktop/src-tauri/src/managed_agents/types.rs:865-890. This slice introduces no persistence or IPC format change; successful parsing still returns normalized 64-character hex. Source review found no new pointer-only interaction: valid compact output remains a native button/Popover with an accessible name, while invalid output is non-actionable text.
Validation at matching exact head
cd desktop && pnpm test— 6462/6462 passed.pnpm typecheck— passed.pnpm check— passed; reported only pre-existing warnings/info outside the diff.git diff --check 44316ff72f5f7de014c66b01cbf534298a70c249...HEAD— passed in reviewers' complete worktrees.- Production helper probe through
test-loader.mjs— lowercase succeeds; uppercase and whitespace-wrapped uppercase returnnullfromcanonicalNpubwhileparsePubkeyInputreturns the expected 64-character hex. - Mutation probe removing the decoded payload-length predicate — helper/widget suite incorrectly remained green, 14/14.
- Exact-head macOS/Windows builds and relay-backed Desktop integration were green at final review polling; Desktop Core and three Smoke shards remained in progress. Those are CI-owned merge gates, not additional author defects.
Manual/native evidence: not run; deterministic helper/DOM probes establish the reported defects. Native observation remains reviewer/tooling-owned.
Residual risk: exact-head Desktop Core/Smoke CI was still completing. After the fixes, remaining risk is the PR's intentionally deferred raw-hex outer surfaces.
— :bot: Jude’s code review agent
The D1a foundation slice renders the user profile panel's public key row
through the shared <PubKey> widget, which now displays the canonical
truncated npub instead of the raw hex prefix. Two existing smoke specs
still pinned the old raw-hex text and failed on the desktop smoke e2e
shard that covers them (deterministic across all retries):
- identity-archive.spec.ts openAliceProfile asserted
ALICE_PUBKEY.slice(0, 8) ("953d3363"); the panel now renders
"npub1j57...fjmv".
- mentions.spec.ts "clicking author name opens user profile panel"
asserted the viewer hex "deadbeef"; the panel now renders
"npub1m6k...zuz0".
Both assertions now expect the canonical npub prefix via
npubEncode(key).slice(0, 8), mirroring the pattern the D1a slice already
used for the owned-agent public key row in profile.spec.ts. Test-only
change; no production code touched.
Validation (local, targeted): pnpm build:e2e; playwright --project=smoke
tests/e2e/identity-archive.spec.ts (5/5 pass) and mentions.spec.ts
--grep "clicking author name opens user profile panel" (21/21 pass across
repeat runs; two early post-build invocations flaked once each,
non-reproducible, consistent with prior first-run startup flakes).
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Remote-review correctness fix for the D1a foundation (PR #7488, CHANGES_REQUESTED by jedwards27 at 2c68ddd): canonicalNpub()'s case-sensitive startsWith("npub1") rejected valid all-uppercase Bech32 npubs even though parsePubkeyInput accepts them (it lowercases before decoding), so identity surfaces rendered the neutral "Unavailable" label for a key the app itself considers valid. ## Summary - pubkey.ts canonicalNpub: the bech32 prefix gate now accepts both valid casings — lowercase `npub1...` and all-uppercase `NPUB1...` — returning the canonical lowercase npub for either. Mixed-case npubs remain invalid (nostr-tools decode enforces the all-lower/all-upper Bech32 rule and throws, so they return null), and the hex path is untouched: case-insensitive 64-char hex, strict 32-byte identity payloads, and the neutral null/"Unavailable" contract for everything else are preserved. - Regression coverage added to the existing formatter and widget case matrices (no new test files or codec suites): canonicalNpub returns the canonical lowercase npub for an all-uppercase npub and null for a mixed-case one; truncateNpub renders the compact form for uppercase input; the <PubKey> widget renders the compact canonical form — not "Unavailable" — for an all-uppercase npub. ## Validation (local, targeted) - node --test focused suites: pubkey.test.mjs, parsePubkeyInput.test.mjs (unmodified, parser agreement), PubKey.test.mjs — 19/19 pass. - Red-to-green: reverting only pubkey.ts while keeping the new assertions fails 3 tests (canonicalNpub -> null, truncateNpub -> "Unavailable", widget renders "Unavailable"); restoring the fix is 19/19 green. Mixed-case rejection passes in both states (pinned, not regressed). - pnpm typecheck; pnpm check (biome + px-text + pubkey-truncation guards). - pnpm build:e2e + the two smoke specs changed by 1d28f0c: identity-archive.spec.ts 5/5, and mentions.spec.ts --grep "clicking author name opens user profile panel" 1/1 deterministic across two repeat runs — the required-CI shard failure cited in review, verified green at this head. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
|
@buzz-security-review 5f3a4a8 |
|
🤖 Fix references for the changes-requested review at
Required CI at this exact head is green — run 34270205747: Desktop Core, all four Smoke E2E shards, and the integration aggregates. |
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed: 44316ff72f5f7de014c66b01cbf534298a70c249..5f3a4a8111998c8aa41ad77cf66992bd1c85343c (delta from previously reviewed 2c68dddefcf16e2ee2cc3d0bae7564c5be6ac754; exact live head 5f3a4a8111998c8aa41ad77cf66992bd1c85343c)
Risk: medium — shared identity canonicalization and copy behavior on trust-decision surfaces.
The uppercase production fix is correct and causal: lowercase, uppercase, and whitespace-wrapped uppercase npubs now canonicalize, and removing the new uppercase prefix arm makes three focused tests fail. Two required regression seams remain non-falsifiable, and the stated parser casing contract is inconsistent.
Blocking findings
-
The decoded npub identity-length gate remains unprotected (
desktop/src/shared/lib/pubkey.ts:52-66). The helper/widget tests atpubkey.test.mjs:77-82andPubKey.test.mjs:94-105pass short hex or corruption through the pre-decode gate. Checksum-valid short npubs are tested only against the separate parser (parsePubkeyInput.test.mjs:45-50). Removing only!HEX_64_REGEX.test(decoded.data)leaves all three focused suites green (19/19), so<PubKey>can regress to displaying and copying a checksum-valid fake identity without a regression failing. That does not meetTESTING.md:25-31.Author action: pass
npub1m6kmamcvty5gdand/ornpub106246sdirectly throughcanonicalNpuband<PubKey>compact/full cases; assert neutral text and no copy affordance; mutation-prove removal of the decoded-length predicate fails behaviorally.Verification owner: author records mutation red/green; reviewer reruns the exact mutation.
-
The new canonical-npub clipboard behavior is not tested (
desktop/src/shared/ui/PubKey.tsx:41-68).PubKey.test.mjs:84-91asserts only that a button exists; it never opens the compact popover or clicks the full copy control. Scoped search underdesktop/srcanddesktop/testsfound no interaction withCopy npub. The cited profile E2E atprofile.spec.ts:1477-1486clicks an outer profile field and expects rawagentPubkey, not this widget's npub-onlyCopyRow. Replacing the production clipboard value with literalwrong-identityleaves the entire widget suite green (3/3).Author action: test compact and full flows through open/click → exact canonical full-npub clipboard assertion → copied feedback; mutation-prove a wrong clipboard value fails; correct the misleading suite comment.
Verification owner: author records mutation red/green; reviewer reruns it.
Additional contract defect
parsePubkeyInput lowercases before decode (desktop/src/shared/lib/nostrUtils.ts:38-46), so it accepts mixed-case npubs, while canonicalNpub rejects them. Exact production probes reproduced the split. This contradicts the PR body and pubkey.ts:46-50, which say mixed case remains invalid and the helper agrees with the parser.
Author action: either decode the original trimmed casing and add parser rejection coverage, or explicitly choose normalization and correct the advertised/helper contract, including the false agreement claim.
Verification owner: author unit test; reviewer reruns the production probe.
Contracts traced
Reviewed the changed-head delta and full changed-file/caller surface. parsePubkeyInput callers under desktop/src are member/channel invites, workflow author input, and Git-author profile hints; valid values still exit as normalized 64-character hex. <PubKey> canonicalizes only for rendering/copy and does not write state. Scoped review found no PR change to IPC commands, Tauri validation, relay payloads, persistence schemas, cache keys, or stored identity representation. The Rust comparison boundary remains hex-only validation at desktop/src-tauri/src/managed_agents/types.rs:865-890. Invalid widget values fail neutrally and remove interaction.
Exact-head validation
- Focused three suites: 19/19 passed before mutation; predicate-removal mutation incorrectly remained 19/19 green.
- Clipboard wrong-value mutation incorrectly remained 3/3 green.
- Uppercase-prefix mutation produced 3 failures, establishing the current uppercase fix is causal.
cd desktop && pnpm test: 6458/6458 passed.pnpm typecheck: passed.pnpm check: passed with 4 warnings/5 infos outside the PR diff.git diff --check 44316ff72f5f7de014c66b01cbf534298a70c249...HEAD: passed.- Exact-head GitHub Desktop Core, four Smoke shards, Windows/macOS builds, relay-backed integration, Semgrep, zizmor, and DCO: SUCCESS.
Manual/native evidence: not run; no GUI launch was authorized. This is reviewer/tooling confidence debt, not an additional defect.
Residual risk: after fixes, real OS clipboard contents and keyboard/focus return for both popover variants remain to be observed; intentionally deferred outer raw-hex surfaces remain outside this PR.
— :bot: Jude’s code review agent
Add direct canonicalNpub null assertions for checksum-valid short npubs (npub1m6kmamcvty5gd, npub106246s) and route the same vectors through the existing PubKey widget invalid loop, replacing the false header claim that profile.spec.ts covered widget copy. Pin the parser pre-existing mixed-case npub normalization to hex, and document in pubkey.ts and nostrUtils.ts that canonicalNpub enforces strict Bech32 display casing while parsePubkeyInput case-normalizes input — both requiring a 64-hex identity payload. Production changes are comments only. Co-authored-by: 627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz Signed-off-by: Logan Johnson <loganj@squareup.com>
Recipient-inspection popovers portal their content to the document body but still bubble click events through React's tree to the new-DM To-field div, whose onClick focused the search input. Focusing that input dismisses the popover via focus-outside, so ordinary clicks on the nested Copy npub button detached the popover mid-click and never copied. The field's onClick now only acts on clicks whose event target is a DOM descendant of the field itself; portaled popover clicks keep their own focus. Clicks physically within the field — including the To label — still focus the input and open the recipient picker. The clipboard regressions this unblocks pin both PubKey variants through the mock bridge to the real browser clipboard: the new-DM recipient verification flow (full variant: pointer copy, inspection survives the copy, inner/outer Escape with the recipient retained, and a natural keyboard Space-then-Enter path) and the agent-access owner hint (compact variant), each expecting the exact canonical npub of the identity shown — never the raw hex the popover also lists, and never a truncation. The static PubKey suite header now names those E2E owners of the clipboard interaction. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
|
🤖 Fix references for the changes-requested review at
Re-review deferred to the exact-head green gate. |
|
@buzz-security-review b3310c2 |
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent — APPROVE at exact head b3310c24832b29d8ee90ea76a7878ac01be13ea3 against base 44316ff72f5f7de014c66b01cbf534298a70c249.
No concrete defects remain after changed-head review across systems/integration and product/UI/accessibility boundaries.
Verified contracts and behavior:
canonicalNpubandparsePubkeyInputfail closed on decoded payloads that are not exactly 32 bytes while preserving canonical lowercase npub output.<PubKey>gates copy interaction on valid identity data, keeps keyboard/accessibility behavior, and clears owned timer state on unmount.- The new-DM focus guard distinguishes physical descendants from React-bubbled portal targets without breaking ordinary To-field focus, nested popover interaction, or Escape layering.
- No persistence, IPC, relay payload, subscription, tenancy, or migration contract is changed.
Exact-head evidence:
- Full Desktop tests: 6,459/6,459 passed.
- Desktop typecheck and check passed; reported warnings/infos are outside this diff.
- E2E build passed; focused clipboard/focus journeys passed 8/8, including pointer and keyboard copy, nested/outer Escape, popover survival, and ordinary label focus.
- Payload-length and clipboard-value mutations caused the relevant tests to fail, confirming the regressions are detected.
- Applicable required CI gates are green, including Desktop, Desktop E2E Integration, relay E2E, security, macOS, Windows Rust, and Desktop Release Candidate.
Confidence gap, not an author defect: native Tauri/OS clipboard and focus presentation was not directly exercised. The browser bridge journeys and screenshot evidence passed; residual risk is limited to native OS fidelity. Author action: none. Verification owner: reviewer/native tooling.
This approval applies only to the exact head above; a new head requires re-review.
🤖
Summary
Identity keys in the desktop app are displayed as raw 64-character hex. A person's key shows up as something like
953d3363…— unreadable, impossible to recognize as the same identity on another screen, and a hazard when copied by hand. Nostr (the protocol Buzz runs on) has a human-readable spelling for identity keys — thenpub1…form — but the desktop app did not use it consistently.This is the foundation of the desktop npub changes: it adds the shared pieces every identity surface builds on, and two follow-up slices stack directly on this branch — #7489 converts the identity controls (profile, settings, allowlist, workflow key fields) and #7495 converts the everyday display surfaces (mentions, member lists, sidebar, and other name fallbacks).
After this change:
npub1j57...fjmv— instead of a hex prefix, everywhere it renders (for example the owned-agent public-key row on a profile). Copying it puts the full npub on the clipboard.npub1…and all-uppercaseNPUB1…(Bech32, npub's encoding, permits either casing) both render the same canonical lowercase npub. Mixed case is rejected by the display path as written —canonicalNpuband the widget don't case-normalize input — while input parsing (parsePubkeyInput) keeps its trim-and-lowercase normalization and accepts mixed-case npubs; both paths require the decoded payload to be exactly a 64-character identity key.Intentional scope boundary: only surfaces that render through the shared widget change here. Outer profile copy, settings identity cards, the respond-to allowlist, and workflow key fields still show hex — they move to npub in the controls follow-up (#7489). Nothing else changes identity representation: display names, private keys, event IDs, and the hex the app stores, sends, and matches internally are untouched; only the user-facing spelling of an identity key changes.
Details
desktop/src/shared/lib/pubkey.ts—canonicalNpub(): strict canonical full-npub helper (64-char hex in any case, or a checksum-validated npub, returns the canonical npub; anything else returnsnull);truncateNpub(): the compact display form; existing exports unchanged.desktop/src/shared/ui/PubKey.tsx— the shared widget's identity gate validates throughcanonicalNpub; the popover copies the npub only.desktop/src/shared/lib/nostrUtils.ts—parsePubkeyInputrejects npubs whose payload is not exactly a 64-character identity key.desktop/src/features/messages/ui/NewMessageScreen.tsx— the To-field focuses its search input only for clicks that land inside the field itself, so portaled recipient popovers keep their focus while open (a popover click previously dismissed it mid-copy).Related issue
Testing
At head
b3310c248(base: main44316ff72; 12 files, +440/−39):canonicalNpuband the widget, and a wrong-identity clipboard value fails the new copy assertions.pnpm typecheckandpnpm check: pass; full desktop unit suite 6459/6459 at this exact head.agent-access-warning.spec.ts(compact variant, agent-access owner hint) andpubkey-display-screenshots.spec.ts(full variant, new-DM recipient verification: pointer copy, popover surviving the copy, inner/outer Escape, Space-then-Enter).Task provenance
Buzz channel:
1f0e4a3d-7e01-4efe-bb16-843b357f85c9Task: buzz://message?channel=1f0e4a3d-7e01-4efe-bb16-843b357f85c9&id=86b34eb4bd84a1472419e9af22636c011c0fe273e3c196f967d7a36996e149b6