Commit graph

296 commits

Author SHA1 Message Date
Avi
a809a67023 feat(ui): name the signer connection before the QR; prune stale vault rows
The pairing path minted its profile under the backend's 'Remote Signer'
default, so several test pairings left indistinguishable rows. The
'Sign in with a signer app (Amber)' button now shows a prefilled
('Amber') Connection name step first; the QR step starts only from
there and the typed name reaches nip46_pair_start as the profile label.
Enter submits directly (input focused, prefilled).

Also cleaned the live vault (not in git): dropped 2 stale 'Remote
Signer' connections and the secret-less 'Dev' remote stub tied to them,
plus 13 orphaned connection_secrets entries; kept the live Amber
pairing (npub1qn0w4a…, its connection + client key) and Testing123.

frontend: 125 tests passed (label step covered: prefill, typed label
reaches nip46_pair_start), typecheck/lint/format/electron:build/build
green. Rust untouched.
2026-09-25 16:01:57 -05:00
Avi
2ef2cd1181 docs(checkpoint): Add-profile Amber sign-in choice at 5b13162 (2026-09-25) 2026-09-25 15:35:39 -05:00
Avi
5b13162a36 feat(ui): Add profile now offers 'Sign in with a signer (Amber)' first
The Amber pairing QR existed but only behind the sidebar's Signer Mode,
so clicking 'Add profile' expectedly led to a local-key form and users
never found the signer flow. The modal now opens as a choice:

- 'Sign in with a signer app (Amber)' — mints the nostrconnect:// QR
  inline, polls signer status every 2s (same channel as Signer Mode),
  and flips to an 'Amber is now your signer!' confirmation once the
  handshake lands; cancelling mid-pairing aborts it cleanly.
- 'Create a new key on this computer' — the previous local-key form,
  unchanged, with a Back step.

frontend: 125 tests passed (CreateProfileModal suite rewritten to cover
choice, QR start, connected poll, cancel-abort; App.test updated for the
new dialog title), typecheck/lint/format:check/electron:build/build green.
2026-09-25 15:34:28 -05:00
Avi
224df38598 docs(checkpoint): e2e flake killed at a6a4e6f (2026-09-24 evening) 2026-09-24 18:27:17 -05:00
Avi
a6a4e6f485 test(nip46): kill the e2e flake — local relay for strict kind-0, poison-tolerant vault lock
The suite failed intermittently (1 in ~4 runs) with 3-5 simultaneous
failures. Two stacked causes:

1. REAL flake: nip46_bunker_connect_params_match_spec_against_strict_amber
   published its kind-0 through the DEFAULT relay set — the real internet
   (wss://relay.damus.io + the known-hanging relay.nostr.band) — so
   'at least one relay must accept the signed kind-0' was a network
   lottery against the 6s send timeout. The QR test already pins settings
   to the in-process relay; the strict test shipped without it. Now pinned
   too: zero network dependency, deterministic.

2. CASCADE: a panic while holding VAULT_ENV_LOCK poisoned the mutex, so
   every later test died on PoisonError and one flake reported as many.
   The lock only serializes process-global XDG_DATA_HOME, so all four
   sites now unwrap_or_else into_inner — a real failure reports as ONE.

10/10 consecutive green e2e runs (was ~1 in 4 failing); suite time now
uniform ~21.5s (was bimodal — the long tail was network waiting).
cargo test 216 unit + 5 e2e green; clippy 0 warnings; fmt clean.
2026-09-24 18:24:15 -05:00
Avi
aa3c514e96 docs(checkpoint): NIP-46 session restore at 0982dad (2026-09-24) 2026-09-24 18:08:48 -05:00
Avi
0982dad566 feat(nip46): restore saved signer sessions on startup/unlock — no fresh scan
Amber remembers our client pubkey for the life of a connection, so the
client secret key minted at pairing is now persisted in the vault
(encrypted like connection secrets, keyed by the same VaultRef, re-keyed
to the identity ref when the handshake resolves it). A restart re-dials
the saved session with the exact keypair, re-sends connect, and enforces
expected_identity in adopt_identity: a signer answering as a different
account is refused, never adopted. Restore hooks run at serve() for
unencrypted vaults and after UnlockVault; failures never block the GUI.
Legacy rows without a stored client key are skipped (one fresh scan
makes them restorable).
2026-09-24 18:07:12 -05:00
Avi
73bf17c3c8 docs(checkpoint): sign_event timeout leash at f53bc56 (2026-09-23 evening) 2026-09-23 20:33:11 -05:00
Avi
f53bc56497 fix(nip46): give sign_event the human-approval timeout leash
Live pairing (Sep 23) proved the signer round-trip works: connect,
get_public_key and the first sign_event all succeeded against real
Amber. Subsequent signs failed because the client half used the 30s
ordinary-RPC leash for sign_event, while every sign waits on a human
approving the prompt on the signer's device. The log shows a valid
signature arriving ~61s after publication, after the waiter had been
removed: 'stale/duplicate response: no waiter ... dropped' — the user
watched failures while Amber was signing correctly.

sign_event now uses a 120s SIGN_TIMEOUT (same leash as the connect
handshake); get_public_key keeps the 30s retry-cadence timeout.
2026-09-23 20:31:37 -05:00
Avi
8117b7913b docs(checkpoint): feed author resolution at a76d8df (2026-09-23) 2026-09-23 16:06:33 -05:00
Avi
a76d8dff4e feat(feed): resolve author names and pictures from kind-0 metadata 2026-09-23 16:06:19 -05:00
Avi
cd8b6711a4 docs(checkpoint): kind-0 via signer at 6f4dbb2 (2026-09-23) 2026-09-23 15:30:49 -05:00
Avi
6f4dbb2ca1 feat(nip46): publish kind-0 metadata through the connected signer 2026-09-23 15:30:35 -05:00
Avi
327bf0ffe0 docs(checkpoint): silent poll fix at 3d1c306, session-restore gap named (2026-09-23) 2026-09-23 15:09:51 -05:00
Avi
3d1c30662a fix(ui): silent background state poll - no refetch churn when vault unchanged 2026-09-23 15:09:37 -05:00
Avi
5714349ea3 docs(checkpoint): remote-profile rename at 6ebc364 (2026-09-23) 2026-09-23 14:44:29 -05:00
Avi
6ebc364fcf fix(profiles): label-only rename for secretless remote-signer profiles 2026-09-23 14:44:18 -05:00
Avi
ca62111092 docs(checkpoint): first live Amber pairing succeeded, notifications root cause (2026-09-23) 2026-09-23 14:30:21 -05:00
Avi
a8d112b368 fix(ui): poll vault state so background Amber pairing appears without reload 2026-09-23 14:30:07 -05:00
Avi
fb149763d5 docs(checkpoint): sync to e02417b - Amber-side silence, damus rejoins pairing set (2026-09-23) 2026-09-23 13:57:53 -05:00
Avi
e02417b18c fix(pairing): offer relay.damus.io first - Amber never receives on primal/nos.lol 2026-09-23 13:57:41 -05:00
Avi
5456835cde docs(checkpoint): sync to 8f055f7 - NIP-46 spec fix, handshake trace, visible errors (2026-09-23) 2026-09-23 13:02:34 -05:00
Avi
8f055f71bf fix(nip46): spec-correct connect params, full-handshake [NIP46] trace, visible handshake errors 2026-09-23 13:02:19 -05:00
Avi
2bc6321d58 fix(pairing): retry identity RPC; pairing set to proven relays only
The 09:44 live scan proved the failure with the new accepted_by trace:
our get_public_key was published and accepted, but Amber's own connect
echo was created one second LATER — Amber's subscription to our client
key opens milliseconds after we publish, so the first identity request is
already in the relays' past when its listener starts (relays never replay
history to a fresh subscription). The signer-side race is structural and
unfixable from our subscription ordering; adopt_identity now retries
get_public_key up to 4x with 3/8/15s backoff (non-timeout errors still
fail fast), so a later attempt lands while the signer is listening.

pairing_relays() narrowed to primal + nos.lol — the only two relays with
proven bidirectional ephemeral-24133 traffic in today's scans. damus.io
503'd reads and silently dropped events; snort persists nothing; user
settings switched to the two proven relays as well.
2026-09-23 10:06:11 -05:00
Avi
be51f67797 docs(checkpoint): point HEAD refs at 9cfc1cf 2026-09-23 09:43:04 -05:00
Avi
88815710ba docs(checkpoint): sync to 9cfc1cf — subscribe race fixed, RPC relay-accept trace added; nostr.band disabled in user settings (2026-09-23) 2026-09-23 09:42:35 -05:00
Avi
9cfc1cfeb1 chore(pairing): trace which relays accepted each identity RPC
The Sep 23 08:59 live scan died at get_public_key with the subscribe race
already fixed, and a post-hoc relay sweep found neither our request nor
Amber's connect reply on any healthy pairing relay — the session lived on
damus.io (503-flapping) and nostr.band (handshake-hanger, still enabled in
user settings and therefore merged into the pairing set). publish_payload
now returns the accepting-relay list and send_rpc traces it for live
sessions (rpc published: method=… accepted_by=…, NONE when no relay took
the event), so the next scan names the dead relay instead of guessing.
2026-09-23 09:33:06 -05:00
Avi
2e98e69ddf fix(pairing): subscribe to signer replies BEFORE the handshake publishes
The relay pushes events only to subscriptions that exist at delivery time.
Both connect flows spawned the handshake task before run_demux registered the
kind-24133 author subscription, so a fast signer reply (the live Sep 23 Amber
scan: clean connect + secret echo, then get_public_key) landed in the gap and
was silently dropped — the identity RPC timed out 30s later and the session
died with 'The signer would not reveal its public key' after the connection
was already stored, leaving the UI signed in with no profile.

Both run_sign_task (bunker flow) and run_paired (QR pairing) now subscribe
first via subscribe_to_signer and hand the stream + subscription to run_demux.
futures-util promoted from dev-dependency to runtime. cargo test 209+3e2e
green, clippy 0, fmt clean, release rebuilt.
2026-09-23 09:00:17 -05:00
Avi
13a66f28d8 feat(onboarding): first-run screen offers import-existing-account and external-signer paths
- Home first-run now has three entry points: create a new profile, I already
  have an account (opens ImportProfileModal), and Sign in with a signer
  (navigates to Signer Mode where Amber/NIP-46 pairing lives).
- Copy states the per-mode truth: local-vault keys vs remote signer where the
  private key never lives on this device.
- Tests updated to assert all three entry points; 116 frontend tests green.
2026-09-23 09:00:07 -05:00
Avi
963b740992 checkpoint: relay-set canary fix at c789cb4 (2026-09-22) 2026-09-22 20:40:46 -05:00
Avi
c789cb4784 fix(pairing): drop purplepag.es and nostr.band from the pairing relay set
Live write+readback canary (Sep 22): purplepag.es rejects kind 24133
outright ('blocked: kind 24133 is not allowed') and relay.nostr.band
hangs the WebSocket handshake. A signer (Amber) that picks a blocking
relay reports 'connected' while its connect event is silently discarded
— exactly the observed failure. The Sep 22 18:01 trace shows a pairing
window that opened, never saw an inbound 24133, and timed out; a
post-hoc relay sweep found zero 24133 events tagged to the ephemeral
key on any relay, consistent with Amber's write being rejected.

Replaced with nos.lol and relay.snort.social, both verified to accept
and serve ephemeral kind-24133. Added a regression test pinning the
blockers out of the set.
2026-09-22 20:39:30 -05:00
Avi
2e5938a3fa checkpoint: docs honesty fix at d4d87b8 (2026-09-22) 2026-09-22 17:47:06 -05:00
Avi
d4d87b85b6 docs: replace blanket 'keys never leave the machine' claim with per-mode truth
External NIP-46 signer mode is a stronger posture, not a caveat: the key
never arrives on this machine, so a compromised desktop cannot extract it.
The old claim only holds for embedded/bunker modes and undersold the
external-signer option. UI already ranked modes correctly; no code change.
2026-09-22 17:46:17 -05:00
Avi
e6ddc53261 checkpoint: sync to f6bf6a9 — pairing trace fully de-noised; live Amber scan still pending (2026-09-21) 2026-09-21 20:42:11 -05:00
Avi
f6bf6a979a fix(pairing): keep e2e traffic out of the live pairing trace + trace the handover
The trace log's "identity adopted - CONNECTED" line fired in the e2e
harness too (adopt_identity had no relay gate), so every test run
appended fake CONNECTED entries to the forensics log. Three such lines
landed there during today's cron verification and had to be manually
distinguished from a real Amber scan. Gate the line on live_relays()
(same loopback test the capture already uses) and add a "paired:
connection stored" trace line at the QR-pairing handover so a stall
between connect-echo and get_public_key is visible in the trace.
2026-09-21 20:39:08 -05:00
Avi
4e79d7ed00 checkpoint: sync to deeb4f9 — e2e vault-isolation bug fixed; live Amber scan still pending (2026-09-20) 2026-09-20 20:45:52 -05:00
Avi
deeb4f9e88 test(e2e): assert vault isolation actually isolated
After each e2e App::load(), assert the loaded vault is empty. An
isolated run that loads any profile can only mean load_vault() fell
through to legacy migration (real repo vault with user keys). The bug
fixed in the previous commit passed green for weeks precisely because
nothing checked this; the guard makes it fail loudly and immediately.
2026-09-20 20:44:10 -05:00
Avi
d52fa58354 test(e2e): isolate the e2e vault at the real data_dir path
Both e2e vault setups wrote the isolation vault to $XDG_DATA_HOME/
profiles_vault.json, but vault::data_dir() is $XDG_DATA_HOME/keynectr —
so the app never found the seeded vault and load_vault() fell through to
try_migrate_legacy_vault(), which picked up the legacy repo vault
(CARGO_MANIFEST_DIR/profiles_vault.json — the user's real profile with a
plaintext secret key) and migrated it into the test process.

Forensics: two legacy-vault backups appeared Sep 19 20:43:54 + 20:44:30,
exactly the cargo-test runs around the edd4e56 commit, proving every e2e
run was migrating the user's real legacy vault. The pairing-trace
"identity adopted" lines from that window were e2e sessions, not a live
Amber scan (no "pairing started" line, no new capture events — session-
level traces are not gated by live_capture).

Fix: seed the vault at tmp/keynectr/profiles_vault.json. Verified green
after the change: repo legacy vault byte-identical, backup count
unchanged, 3/3 e2e pass.
2026-09-20 20:42:08 -05:00
Avi
5f9c64fb82 checkpoint: sync to edd4e56 — accept NIP-46 connect *response* shape (root-cause fix) (2026-09-19) 2026-09-19 20:50:12 -05:00
Avi
edd4e565fb fix(pairing): accept the NIP-46 connect *response* shape Amber actually sends
For a client-initiated nostrconnect:// scan, NIP-46 says the signer
sends a connect RESPONSE event — {"id",…,"result":"<secret>"} — not a
connect request: the secret echo IS the handshake, nothing to answer.
run_pairing_task only recognized an inbound {"method":"connect"}
*request*; a bare {"result":…} fell through as 'not a NIP-46 request'
(method empty) or 'pre-handshake ignored', so Amber's approval was
silently dropped and no profile row was ever created.

Now the pairing loop verifies the echoed secret (or 'ack') directly and
proceeds to identity adoption; a signer-sent error fails fast with the
signer's message. Wrong-secret responses are ignored as spoofing, same
as before. The legacy request shape keeps working.

e2e: run_fake_scanner takes a connect_shape; new
nip46_qr_pairing_connect_response_shape pins the Amber shape end-to-end
(fails on the old parser, green on the new one).
2026-09-19 20:45:27 -05:00
Avi
a28d76e7d0 checkpoint: sync to 21c522b — durable pairing trace log, forensics hygiene (2026-09-18) 2026-09-18 21:03:08 -05:00
Avi
21c522ba99 chore(pairing): durable trace log + keep e2e loopback traffic out of forensics files
The Sep 16 forensics file pairing-capture.jsonl turned out to be
polluted: the e2e harness pairs over loopback relays but its fake
scanner events were appended to the same file (lines 6-7 from today's
test runs). Now loopback pairings skip both the capture file and the
new pairing-trace.log.

pairing-trace.log records the full pairing outcome at every decision
point (started, inbound, decrypt-fail, not-a-request, pre-handshake,
connect answered, identity adopted, session failed) with timestamps,
because backend stderr only reaches the Electron console and /tmp logs
get cleaned — a failed live handshake previously left no durable trace.

Release binary rebuilt at this commit.
2026-09-18 21:00:31 -05:00
Avi
e97d8a75f0 checkpoint: sync to 188b2eb — lenient NIP-46 payload parse, real decrypt-error logging (2026-09-16) 2026-09-16 21:12:13 -05:00
Avi
188b2eb61d fix(pairing): never reject a decrypted NIP-46 payload on shape
RawRequest still dropped real Amber connect requests after aedde8f:
tonight's re-scan (20:11) hit the lenient-params build and the payload
parsed no better. The strict derive rejected non-string ids, missing
ids, object-shaped params, and double-encoded request strings. Replace
the derived Deserialize with a coercion-based one: any valid JSON
deserializes, every scalar becomes text, a JSON-string-wrapped object
is unwrapped, and only truly non-JSON reaches the log path — now
dumping the exact payload plus the real decrypt error (HMAC vs padding
vs wrong key) instead of a generic 'wrong conversation key'.
2026-09-16 21:08:34 -05:00
Avi
aedde8ffb8 fix(pairing): accept non-string NIP-46 params (Amber sends requested_perms as a JSON object)
RawRequest used a strict Vec<String>, so Amber's connect request —
whose params[1] is the requested_perms grant object — failed to parse
and was silently dropped. The signer saw a live session; we saw
nothing. Params now coerce non-string values to their JSON text, with
regression tests for both shapes.
2026-09-12 21:38:46 -05:00
Avi
759b5ddfc9 chore(pairing): capture raw inbound pairing events for handshake debugging 2026-09-12 21:33:15 -05:00
Avi
5aa122d92d chore(pairing): 5-min pairing window + inbound-event wiretap logging 2026-09-12 21:26:07 -05:00
Avi
049219b0f1 docs(checkpoint): sync to f59c2b1 pairing-relay widening 2026-09-12 21:09:06 -05:00
Avi
f59c2b1b45 fix(pairing): widen pairing relay set so signer-chosen relays are covered
Amber picks whichever relay it likes from the nostrconnect:// URI and
some relays drop ephemeral kind-24133 traffic, so pairing could appear
successful on the signer while nothing reached us. The pairing set is
now the user's relays UNION a curated fallback of well-known relays;
loopback-only sets (e2e harness) skip widening to keep tests isolated.
2026-09-12 21:07:27 -05:00
Avi
937fcc67cb checkpoint: sync to 22d7c01 — QR pairing, identity adoption, always-allow grants, hygiene (2026-09-12) 2026-09-12 17:56:08 -05:00