Keynctr/CHECKPOINT-encryption.md

2403 lines
132 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Checkpoint — cron hygiene: lockfile refresh committed + pushed (2026-10-01 evening)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`41d4a60`**
("chore(deps): lockfile refresh from update_apply"). Pushed —
`git ls-remote origin master` = `41d4a60` verified.
- Working tree: clean for tracked files (always-untracked: COSMIC_THEME.md,
icon jpeg, deferred/).
- Release binary: 15:23 build from the d09c4ec tree (lockfiles are
dependency-resolution only; no backend source changed since). NOTE: the
live `keynectr serve` (pid 356510) started 09:59, BEFORE the 15:23 rebuild —
relaunch the app to pick up Step 4.
## What was done (user-facing)
- No new feature this run. Evening cron checked the Amber NIP-46 pairing
problem from the standing brief: it remains CLOSED. `profiles_vault.json`
holds 3 `nip46_connections` + matching nip46_client rows (latest pairing
2026-09-28 10:23, "Amber"); the debug capture dir `~/Tools/keynctr-debug/`
and `/tmp/keynctr-el*.log` no longer exist, and no parse-failure evidence
has appeared since the Sep 28 closure. The payload-coercion fix from the
brief is already contained in shipped history (superseded by 5b60ae3 chain).
- Committed the stray lockfile refresh left by the in-app update flow
(yoke-derive 0.8.3→0.8.4, npm tree refresh) so the tree is clean.
## Commits this session (newest first)
- `41d4a60` chore(deps): lockfile refresh from update_apply
- (checkpoint update follows this section's commit)
## Verification (all green, 2026-10-01 ~20:07)
- `cargo test` → 227 unit + 6 e2e, 0 failed · `cargo clippy --all-targets` → 0 warnings
- `cargo fmt --check` clean · frontend `npm test` → 148/148 (19 files) · `npm run typecheck` clean
## Outstanding / next steps
- User relaunch of the app (live serve predates the 15:23 Step 4 rebuild).
- Live pass: approval-time kind editor with Amber; rename npub1p437 + Publish name;
publish kind-0 to primal/damus; KDF live check on real unlock.
---
# Checkpoint — Step 4 finished: interactive kind scope at approval (2026-10-01)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`d09c4ec`**
("feat(signer): interactive kind scope in the approval prompt (Step 4 finish)").
Previous: `8ffeb42` + `d1622a0` (checkpoint updates, pushed), `83f5940`
(placeholder kind-0 fix), `e8d2abb` (grant kind-editing UI).
- Working tree: clean for tracked files (always-untracked: COSMIC_THEME.md,
icon jpeg, deferred/). Push status in Next steps.
- Release binary rebuilt from d09c4ec at 13:30 (real 30s build, mtime verified).
## What was completed (user-facing)
**Step 4 remainder — kind scope chosen AT approval time:**
1. Signer screen and Signer Mode screen: on a pending `sign_event`
request, "Always allow…" opens an inline kind editor prefilled with the
request's own event kind; Allow records the grant with exactly the
edited kinds (sorted+deduped, empty = all kinds = explicit broadening),
Cancel leaves the request pending. Non-signing methods keep the plain
"Always allow" button (no kind dimension).
2. Backend: `Nip46Approve`/`SignerApprove` gained `grant_kinds`
(`serde(default) Option<Vec<u16>>` — old payloads stay valid).
`respond_to_approval_with_always(.., grant_kinds)`: `Some(list)` stores
the edited scope verbatim; `None` keeps the old fallback (kind of the
request being approved). Legacy vault rows untouched.
3. `nip46_status_as_bunker_json` now includes each pending request's
`details`, so the legacy Signer screen can prefill the editor too.
4. Shared `parseKindsInput()` in `lib/permissions.ts` (used by both the
approval editor and the existing grant editor); grant editor refactored
onto it — behaviour unchanged.
5. Latent bug fixed: `AppProvider.signerApprove` dropped the `always`
argument, so the legacy "Always allow" button never recorded a grant.
## Commits this session (newest first)
- `d09c4ec` feat(signer): interactive kind scope in the approval prompt (Step 4 finish)
- `8ffeb42` docs(checkpoint): 8070dfc..d1622a0 pushed to origin/master
- (earlier today) `d1622a0`, `83f5940`, `e8d2abb` — see checkpoint below
## Verification (all green, 2026-10-01)
- `cargo test` → 227 unit + 6 e2e, 0 failed · `cargo clippy --all-targets` → 0 warnings
- `cargo fmt --check` clean · `cargo build --release` rebuilt 13:30 from d09c4ec
- frontend: vitest 148/19 files (6 new: 3 approval-editor tests, 3
parseKindsInput units); `typecheck`, `lint`, `format:check`, `build`,
`electron:build` all green.
## How to verify in the app
1. Fully quit and relaunch Keynctr (new backend, 13:30).
2. Have the connected app request a signature → Signer screen →
"Always allow…" on the pending request → edit kinds → Allow.
The "Always-allow permissions" card shows the scoped grant; a later
request of an UNcovered kind prompts again.
3. e2e-mechanics are covered by the 3 new SignerScreen tests.
## Next steps
- PUSHED 2026-10-01 after this checkpoint (see git remote readback);
token per-use, not stored (revoke when convenient).
- User live pass: relaunch, rename npub1p437… + Publish name, try the new
approval-time kind editor with Amber.
- Optional: remove /tmp/kn-base worktree when done.
---
# Checkpoint — placeholder kind-0 fix + grant editing UI (2026-10-01)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`83f5940`**
("fix(profiles): never treat placeholder kind-0 as a real display name").
- Working tree: clean for tracked files (always-untracked: COSMIC_THEME.md,
icon jpeg, deferred/).
- Pushed to origin/master (see Next steps): remote head == `d1622a0`.
## What was completed (user-facing)
1. **Grant kind-editing UI** (`e8d2abb`): the Signer screen's "Always-allow
permissions" card now edits each grant's event-kind scope inline
(Edit → comma-separated kinds → Save/Revert). Backed by the
`signer_grant_update` RPC from `d7cf2a5`.
2. **Profile-name fix** (`83f5940`): placeholder "My Profile" names no longer
stick. Verified live: npub1p437…'s kind-0 on purplepag.es/damus says
"My Profile" (ts 1789050653) — an early build auto-published the
empty-label default as new accounts' kind-0, and pairing enrichment +
backfill then copied that poisoned value over typed names forever. Now:
- shared `profiles::network_display_name()` rejects blank AND placeholder
names fetched FROM the network (unit-tested);
- create/import never auto-publish a generic placeholder as kind-0;
- import falls back to the shortened npub instead of "My Profile".
## Commits this session (newest first)
- `83f5940` fix(profiles): never treat placeholder kind-0 as a real display name
- `e8d2abb` feat(signer): inline kind-scope editing for always-allow grants
(checkpoint commits interleaved; earlier session: `d7cf2a5`, `ec8b515`, `aef47dd`)
## Verification (all green, 2026-10-01)
- `cargo test` → 227 unit + 6 e2e, 0 failed · `cargo clippy --all-targets` → 0 warnings
- `cargo fmt --check` clean · `cargo build --release` rebuilt 10:48
- frontend: `npm test` 142 passed; `typecheck`, `lint`, `format:check`,
`build`, `electron:build` green. (Full-suite failures seen earlier today
were load-induced 5s timeouts on a busy box — passed on quiet reruns.)
## How to verify in the app
1. Fully quit and relaunch Keynctr (new backend, 10:48).
2. For npub1p437…: Profiles → rename → Publish name (pushes a clean kind-0
network-wide). The backfill will no longer overwrite it with "My Profile".
3. Signer screen → Always-allow permissions → Edit a grant's kinds → Save.
## Next steps
- PUSHED 2026-10-01: `8070dfc..d1622a0` on origin/master, verified by
`ls-remote` readback (remote head == local head). Token was per-use,
not stored (revoke when convenient).
- Optional: remove /tmp/kn-base worktree when done.
---
# Checkpoint — grant kind-editing backend (2026-09-30, session stopped mid-Step-4)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`d7cf2a5`**
("feat(signer): grant kind-editing backend (vault + IPC + api plumbing)").
Previous: `ec8b515` + `aef47dd` (profile-relay queries, checkpoint above),
`eea6f0e` (white launcher icon), `abc2781` (one-click update script).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/SignerConnectionPanel.tsx.wip/`.
- origin/master behind: local ahead by 4 commits (push still blocked — no
Forgejo credentials stored; needs a token via the one-shot extraHeader
method).
## What was completed (this checkpoint)
**Grant kind-editing backend — Step 4 remainder, first half** (session was
stopped by the user mid-build; the UI half is NOT started):
1. `vault.rs`: `Vault::update_signer_grant_kinds(app, method, kinds)` —
replaces a standing grant's kind scope, normalised (sorted + deduped).
Empty list = "all kinds" (same representation legacy grants use), so
broadening is explicit, never from omission. Unknown (app, method)
returns false, changes nothing. New unit test
`signer_grant_kinds_can_be_edited`.
2. `ipc.rs`: `Request::SignerGrantUpdate { app_pubkey, grant_method,
grant_kinds }` (field names avoid the internal `method` tag; kinds use
`serde(default)` so legacy payloads stay valid) + handler that saves the
vault only when a grant actually changed.
3. Frontend plumbing only, NO UI yet: `api.signerGrantUpdate`, the
AppProvider context type + callback + value/deps lists, and the
`signer_grant_update` case in `fakeBackend.ts` mirroring the backend
normalisation.
## Commits added this session (newest first)
- (this checkpoint commit)
- `d7cf2a5` feat(signer): grant kind-editing backend
- `ec8b515` docs(checkpoint): profile-relay queries @ aef47dd
- `aef47dd` fix(feed): query profile relays for kind-3 follow lists and
kind-0 metadata (WIP of the previous dead session, verified + dedupe bug
fixed: trailing-slash normalising via a seen-set)
## Verification (2026-09-30 @ d7cf2a5)
- `cargo test` -> 226 unit passed, 0 failed (e2e 6 green at aef47dd run).
- `cargo clippy --all-targets` -> 0 warnings; `cargo fmt --check` clean.
- `cargo check` green. NOTE: release binary at target/release/keynectr was
built at aef47dd, NOT d7cf2a5 — rebuild before shipping the new RPC.
- `frontend`: `npm run typecheck` clean; `npm test` -> 139 passed (19
files). No new UI tests yet (no UI yet).
## How to resume
- NEXT UNIT (was next when stopped): the Signer-screen grant editor —
inline kind editing on the "Always-allow permissions" card in
`frontend/src/screens/SignerScreen.tsx` (grants list, lines ~243-268):
edit kinds for a `sign_event` grant -> `signerGrantUpdate(app, method,
kinds)`; include an explicit "all kinds" option (empty list), Revoke
stays as is. Add `SignerScreen.test.tsx` cases against the fakeBackend
`signer_grant_update` case, then the full gates + release rebuild +
checkpoint update.
- GUI dev loop: `cd frontend && npx vite --port 5173`, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
- Push when a token is available: `git push origin master` (4 ahead).
## Outstanding / next steps
- Grant editor UI (above) — second half of Step 4 remainder.
- Live pass: "My contacts" with the rebuilt backend (aef47dd fix).
- Push 4 commits to Forgejo.
---
# Checkpoint — profile-relay queries for contacts + metadata backfill (2026-09-30)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`aef47dd`**
("fix(feed): query profile relays for kind-3 follow lists and kind-0
metadata"). Previous: `eea6f0e` (white monochrome launcher icon),
`abc2781` (one-click update script), `7891ccc` (Step 7 rename/hygiene,
checkpoint `33ab4fe`).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/SignerConnectionPanel.tsx.wip/`.
- Release binary rebuilt at `aef47dd` 2026-09-30 14:18 (verified by mtime
after `touch`ing the changed sources — not a cache hit).
## What was completed
**Profile-relay lookup for profile-scoped data** (finishes the empty
"My contacts" diagnosis on the network side): kind-3 follow lists and
kind-0 metadata usually live on profile/outbox relays — purplepag.es
aggregates them network-wide — not on the user's note read relays. Now:
1. `feed.rs`: `PROFILE_RELAYS = ["wss://purplepag.es"]` plus
`profile_lookup_relays(settings)` = enabled relays + profile relays,
de-duplicated with trailing-slash normalising (a user entry
`wss://purplepag.es/` no longer doubles the always-on entry — the WIP
version of this failed its own test; rewritten via a HashSet seen-set).
`contact_pubkeys` (kind-3 fetch) now queries that set; notes queries
deliberately keep using only enabled relays.
2. `ipc.rs` backfill loop: kind-0 fetch also uses `profile_lookup_relays`,
and the candidate filter dropped the `SignerMode::Nip46Client`
restriction — any row still wearing a placeholder gets a real name,
local keys included.
3. `profiles.rs`: `GENERIC_PAIRING_LABELS` now includes "My Profile"
(`normalise_label`'s empty-input default), so locally created
placeholder rows are backfilled too. Test updated: user renames still
never overwritten.
## Commits added this session (newest first)
- `aef47dd` fix(feed): query profile relays for kind-3 follow lists and
kind-0 metadata (includes the fmt fix + dedupe rewrite of the WIP)
- (this checkpoint commit)
## Verification
- `cargo test` -> 225 unit + 6 e2e passed, 0 failed.
- `cargo clippy --all-targets` -> 0 warnings. `cargo fmt --check` -> clean.
- `cargo build --release` -> rebuilt at aef47dd (binary mtime 14:18).
- No frontend files touched -> frontend gates not applicable.
## How to resume / reproduce
- CLI ground truth for the contacts fix:
`target/release/keynectr feed --contacts 20` with the active key that
has a follow list published anywhere on the network — rows should now
appear even when purplepag.es is not in the user's read relays.
- GUI: `cd ~/Projects/Keynctr/frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
(running windows need a relaunch/rebuild to pick up the new backend).
- Backfill trace: `grep -a 'auto-name' ~/Tools/keynctr-debug/pairing-trace.log`.
## Outstanding / next steps
- Live pass: reopen "My contacts" in the GUI with the rebuilt backend and
confirm the feed populates for the account that has follow lists on
profile relays.
- Step 4 permissions UI follow-ups (interactive grant editing in the
approval modal) — the only buildable step left from the plan.
- Live pass: KDF migration on a real unlock + second-Amber account
switching.
---
# Checkpoint — stdin EAGAIN fix, accent theme, identity backfill (2026-09-28 evening)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`fac4ba3`**
("feat(desktop): launch from the applications list (folio-style)").
Previous: `d792a72` (Archipelago uses the reference artwork as canvas),
`4d0df31` (Archipelago theme tokens, first pass). `5b60ae3` (persistent identity backfill for generic pairing
labels), `a6182e3` (iOS blue accent for Workshop Dark + custom accent
setting), `9770f46` (stdin blocking-thread reader — EAGAIN crash fix),
`6bff188` (docs), `dcc701f` (no-restart self-update).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/SignerConnectionPanel.tsx.wip/`.
- Release binary rebuilt 2026-09-28 ~12:40 (from the 4d0df31 tree).
- origin/master is at `a6182e3`; local is 1 commit ahead — push still blocked
(no Forgejo token stored for this session; HTTPS asks for credentials).
## Pairing problem status — RESOLVED (live evidence)
- `~/.local/share/keynectr/profiles_vault.json` now carries **3 persisted
`nip46_connections`** rows with matching `signer_mode: nip46_client` profile
rows (created Sep 25, Sep 27, and **Sep 28 10:23**). The Sep 28 pairing
happened on the current build: Amber's connect event parses, the handshake
completes, and the profile row persists — the original
"Amber says connected but no row appears" failure no longer reproduces.
- The Sep 28 row is the **active profile**. Its label is still the generic
"Amber" because the paired account has not published kind-0 yet; `5b60ae3`
backfill re-checks every ~2-5 min while the backend runs and will rename it
when metadata appears. No further parse-side work outstanding.
## What was completed (this checkpoint's commits)
1. **`9770f46` stdin EAGAIN crash fix** — `keynectr serve` treated a transient
EAGAIN on the Electron pipe as fatal ("Rust backend exited unexpectedly
code 1"). Dedicated blocking-thread reader + mpsc; verified live with a
200-line request stream.
2. **`a6182e3` custom accent color** — iOS blue accent for Workshop Dark plus
a user-settable accent color.
3. **`5b60ae3` persistent identity backfill** — slow-cadence loop in `serve()`
re-fetches kind-0 for Nip46Client rows still wearing generic
"Amber"/"Remote Signer" placeholders; user renames never overwritten;
locked vaults skipped; network off-lock, 90 s budget per identity.
4. **`4d0df31` Archipelago theme** — new `archipelago` theme from the user's
synthwave reference art: teal-navy dusk canvas, coral (#f0685c) scanline
sun glow + pink cloud banks + faint horizontal scanlines on `.main`
(fixed attachment), glassmorphic blur on cards/sidebar, white logo filter.
Settings -> Appearance -> "Archipelago - Sunset Sea".
5. **`d792a72` Archipelago artwork canvas (v2)** — first pass was abstract
washes and did not match the reference; replaced with the actual
illustration (`frontend/public/archipelago-bg.jpg`) cover/fixed under a
dark scrim on `.main`, translucent blur-glass cards + sidebar, light
hairline borders, coral accent. This is what the reference mock shows.
6. **`fac4ba3` applications-list launcher** — `launch-keynctr.sh` +
`~/.local/share/applications/keynctr.desktop` (validated); GPU env set
for Hyprland; single-instance lock focuses the existing window.
Verified by gtk-launch twice: one window, correct WM class.
## Verification (2026-09-28 evening cron pass)
- `cargo test` → 221 unit + 6 e2e passed, 0 failed.
- `cargo clippy --all-targets` → 0 warnings. `cargo fmt --check` → clean.
- `frontend`: `npm test` → 139 passed (19 files); `typecheck`, `lint`,
`format:check` all clean.
## How to resume
- GUI: `cd ~/Projects/Keynctr/frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
- CLI backend: `target/release/keynectr serve` (spawned by Electron).
- Push pending commit when credentials are available: `git push origin master`.
## Outstanding / next steps
- Live confirmation of the backfill rename: pair/keep a fresh Amber account
with no kind-0, publish kind-0 from Amber, confirm the Keynctr row renames
itself within ~5 min without restarting the app.
- Step 4 permissions UI follow-ups (interactive grant editing in the approval
modal) — the only buildable step left.
- Live pass: KDF migration on a real unlock + second-Amber account switching.
---
# Checkpoint — Step 7 rename/hygiene + Forgejo push restored (2026-09-30)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`7891ccc`**
("docs: Step 7 rename/hygiene pass"). Previous: `c553dd5` (KDF upgrade
m=64MiB/t=3 + transparent migration, Sep 30), `5e91914` (launcher), theme
commits, identity backfill.
- **Forgejo push unblocked**: token supplied per-use via
`git -c http.extraHeader="Authorization: Bearer <token>" push origin master`
(token supplied in chat, NOT stored anywhere — revoke when convenient).
`a6182e3..c553dd5` pushed; remote master verified == local. Token is
single-use-per-invocation; re-ask for it next push unless the user stores one.
- Step 7 done in `7891ccc`: `frontend/package.json` homepage ->
`https://git.atitlan.io/avi/Keynctr`; README retitled Keynctr, all
`[YOUR_FORGEJO_INSTANCE_URL]`/`<OWNER>/<REPO>` placeholders resolved, clone
dir + packaged binary name fixed (`keynectr`), data-dir default
`~/.local/share/keynectr` (code auto-migrates the legacy dir), stale test
counts refreshed (cargo 225 unit + 6 e2e; npm 139 tests / 19 files), project
layout updated (audit/bunker/feed/updates, signer/ dir, Feed + SignerMode
screens). PRODUCT.md data-dir corrected.
- `tests/tmp_kdf_probe.rs` (throwaway probe) deleted; not part of the suite.
- Release binary rebuilt from c553dd5 (Sep 30 11:23).
## Verified
- `cargo test`: 225 unit + 6 e2e, 0 failed. `cargo clippy --all-targets`: 0.
`cargo fmt --check`: clean. Frontend: 139/19 vitest green, typecheck, lint,
format:check clean (docs-only commit; no code touched).
- Push verified by `git ls-remote` readback (remote master == c553dd5 at push).
## How to resume
- GUI: `cd ~/Projects/Keynctr/frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
- Push: token needed per use (`http.extraHeader` form above), or user stores
one / registers the SSH key.
## Outstanding / next steps
- Live pass (user): KDF migration on a real unlock; second-Amber switch +
backfill rename confirmation; publish kind-0 to primal/damus.
- Step 4 remainder: interactive grant editing in the approval modal.
- No CI workflow yet — `.forgejo/workflows/ci.yml` badge stays commented.
# Checkpoint — permissions UI + updater fix + Workshop theme (2026-09-27 night)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`add956a`**
("feat(theme): add Workshop theme — Cybernetic Workshop identity from Moi
DESIGN.md"). Previous: `15fa331` (updater-run dependency bumps, clears the
high js-yaml advisory), `fa59b63` (updater PATH fix), `adbc7c2`
(permissions UI), `98593cb` (checkpoint), `c89b31a`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/`.
- Release binary rebuilt at add956a (2026-09-28, after 3m01s real build).
- NOTE: a running Electron app still serves the OLD backend + stale `dist/`
until relaunch; the running serve predates these commits. The user's
"Update problem: The Rust backend exited unexpectedly (code 1)" screenshot
came from that old build; `update_apply` verified green end-to-end on the
new binary (all three steps applied).
## What was completed
1. **Permissions are visible (Step 4, first slice).** `Nip46Status` now
carries the connection's declared `perms=` grant list and expiry. The
Signer Mode screen shows a **Permissions** panel on a live session: one
row per granted method ("Sign events — kinds 1, 30023"), or a plain
statement that the signer app (Amber) approves every request when no
grant list was declared. `frontend/src/lib/permissions.ts` holds the
shared label formatters.
2. **"Always allow" is now kind-scoped (was: method-wide, too broad).**
A `sign_event` grant records the kind of the request the user actually
approved; a kind-1 grant never covers a kind-3 request — uncovered kinds
fall back to the approval prompt. Legacy kind-less grants keep their
all-kinds meaning (stored vaults keep working; new grants are never
created kind-less). Enforced in BOTH `bunker.rs` (bunker mode) and
`nip46_client.rs` (client mode) via `Vault::has_signer_grant(peer,
method, event_kind)`. The Signer screen's grants list renders the human
label with kind scope.
3. **Check-for-updates fixed.** The Electron-spawned backend inherited the
desktop launcher's PATH (no `~/.cargo/bin`, no mise/asdf shims), so
`npm`/`cargo` "didn't exist". `updates.rs::run()` now appends the
well-known per-user tool dirs to the inherited PATH (inherited wins on
conflicts; missing dirs ignored). Verified live under
`env -i PATH=/usr/bin:/bin`: `update_check` returns a full report.
## Commits added
- `adbc7c2` feat(signer): permissions UI — declared grants surfaced, always-allow kind-scoped
- `fa59b63` fix(updates): augment spawned PATH so npm/cargo resolve from Electron
- `15fa331` chore(deps): dependency updates from the in-app updater run (clears high js-yaml advisory)
- `add956a` feat(theme): Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
- `c18f59a` feat(theme): Workshop — Dark, the Moi dark material
- `de804c8` feat(theme): Workshop atmosphere — Moi's graph-paper grid + mint/clay washes
- `4cc0481` feat(theme): white logo mark on workshop-dark
- `dcc701f` feat(updater): apply updates without restarting the app — `app:selfupdate` IPC rebuilds (npm + cargo, augmented PATH), kills the backend child so the next request spawns the NEW binary, reloads all windows. Electron shell stays up; main-process changes still need one manual relaunch; packaged builds report bundle replacement.
## Verification (all green at fa59b63)
- Rust: `cargo test` 219 unit + 6 e2e (NEW: `signer_grants_are_kind_scoped`,
two `augmented_path` tests); `cargo clippy --all-targets` 0; `cargo fmt
--check` clean; `cargo build --release` rebuilt 22:43.
- Frontend: `npm test` 135 (10 new: `permissions.test.ts` unit + 2
SignerModeScreen permission-panel tests), `typecheck`, `lint`,
`format:check`, `build`, `electron:build` all green.
## Deferred / next steps
- Live eyeball: relaunch the app, pair with a `perms=`-carrying client (or
Amber) and check the Permissions panel; approve kind-1 "Always allow",
then send a kind-3 request and confirm it PROMPTS (kind scope).
- Two-account live pass from the previous checkpoint still open (pair
account B in Amber, switch back and forth).
- Publish kind-0 to primal/damus (one Amber approval).
- Step 4 remainder (if wanted): interactive grant editing in the approval
modal (approve-with-narrowing UI); today the modal is Approve / Always
allow (kind-scoped) / Reject.
- Step 5 (KDF upgrade m=64MiB/t=3 + vault header versioning), Step 6 (undo
history), Step 7 (rename/hygiene incl. `homepage` URL).
---
# Checkpoint — multi-account signer switching (Option A) (2026-09-27 eve)
## Where things are
- Project: `/home/avi/Projects/Keynctr`, branch `master` @ **`c89b31a`**
("feat(nip46): pair a second signer account — park the live session,
switch re-dials it"). Previous: `1b4655c` (checkpoint), `332ab64`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/`.
## What was completed (user-facing)
1. **You can now add a second Amber account.** Pairing a new signer while
one is connected no longer says "Already connected — disconnect
first": the current session is PARKED (kept restorable, never revoked)
and the new account pairs.
2. **Switching profiles switches signer accounts.** Click a profile in
Profiles: if it has a saved signer session, the live one is parked and
that profile's session is re-dialed automatically (no scan, identity
guard still enforced). Local-key profiles leave the signer alone.
3. **Cancel on the QR is safe.** Leaving the QR view cancels only the
pairing attempt and brings the parked session back (new
`nip46_cancel_pairing` IPC; the old path revoked).
- One session is live at a time (Amber signs one active account anyway);
all others stay saved and switchable.
## Commits added
- `c89b31a` feat(nip46): pair a second signer account — park the live session, switch re-dials it
## Verification (all green at c89b31a)
- Rust: `cargo test` 216 unit + 6 e2e (NEW: two fake Ambers on one relay —
B's pairing parks A unrevoked with client key intact; switch back
re-dials A and signs; no-op switch; local profile untouched; B
restorable). `cargo clippy --all-targets` 0 warnings; `cargo fmt --check`
clean; `cargo build --release` rebuilt (binary mtime Sep 27 21:00).
- Frontend: `npm test` 125 passed; `typecheck`, `lint`, `format:check`
clean; `npm run build` + `electron:build` green.
## Resume / reproduce
- GUI: relaunch the app (or restart the backend) to pick up the new
release binary. Profiles -> click another paired profile -> log shows
`restoring session` + `identity check on restored session: PASS`.
- Add account B: Create Profile -> Sign in with Amber -> switch to
account B IN AMBER -> scan. Account A stays restorable.
- e2e: `cargo test --test nip46_e2e` (6 tests, loopback relay only).
## Outstanding
- LIVE two-account eyeball by the user (pair account B from a second
Amber profile, switch back and forth) — e2e proves the mechanics, a
real-device pass confirms it end-to-end.
- Live "Check for updates" failure is an ENVIRONMENT issue, not a bug:
the updater shells out to `npm outdated`/`cargo update` in the source
tree; the spawned backend's PATH lacks npm/cargo (Electron-launched
process), so it errors. Fix options: bake a login-shell PATH into the
updater or surface a clearer message. Not started.
- Publish kind-0 to primal/damus (one Amber approval); Step 4
permissions UI; KDF upgrade (Step 5); rename pass (Step 7).
---
# Checkpoint — forensics log cleaned + live publish confirmed (2026-09-26)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`332ab64`** ("fix(nip46): gate remaining trace
writes to live relay sessions"). Previous: `704addc` (checkpoint),
`bc736ff` (auto-name retry), `b5c61de`, `fc2fe93`.
- Working tree clean except the standing untracked files
(COSMIC_THEME.md, icon jpeg, deferred/).
- Release binary rebuilt at 332ab64 (mtime Sep 26 20:34, real 22s
compile, not a cache hit).
## What was completed
1. **LIVE PUBLISH CONFIRMED (was the last open item)** — el.log shows
three sign_event responses ~19:50 Sep 25 and relay probes confirm
kind:1 notes (id 5191172d01f9…, fe9a256f597f…, text "test" +
image) from npub1qn0w4… accepted on primal/damus/snort/nos.lol at
exactly those timestamps. End-to-end Amber signing works.
2. **False alarms retired**: the repeated "restored signer answered as
a different account" and duplicate "auto-name attempt" lines in
pairing-trace.log were E2E TEST traffic (wrong-identity refusal test
+ loopback auto-name loop), not live Amber failures — each bogus
npub appeared exactly at test-run times (17:09 rebuild, 20:07 suite).
3. **Trace gating fix (332ab64)**: `fail()` and the background
auto-name traces now check `live_relays()` like every other site.
Verified by measurement: e2e suite run leaves pairing-trace.log
byte-identical (was 240 lines before, 240 after).
4. **Live vault pruned (not in git)**: dropped the legacy `fac852dc…`
connection row (15:37 pairing, predates client-key persistence,
superseded by 19:35 `4148a9a1…` pairing) so startup restore can't
waste a re-dial on a keyless row. Backup:
profiles_vault.json.backup-cron-20260926. Done with backend down.
## Verified this session
- cargo test: 216 unit + 5 e2e green. clippy --all-targets: 0 warnings.
cargo fmt --check clean. cargo build --release rebuilt at 332ab64.
- Frontend untouched this session (no npm run needed).
- Serve smoke test on the REAL vault (backend was down): startup
restore fires "restoring session: peer=4148a9a1… client
pubkey=64ea18e8…" (the persisted key from the 19:35 pairing), relays
connect, no errors. Full handshake needs Amber online — user test.
- kind-0 'web5osint' confirmed live on nos.lol; profile row already
carries the name + picture in the vault.
## Outstanding / next user steps
- One scanless restart check: launch the GUI with Amber online and
watch for "identity check on restored session: PASS" without
scanning (restore now targets only the restorable 4148a9a1 row).
- Optional: publish kind-0 to primal/damus too so naming doesn't
depend on nos.lol alone (needs one Amber signature).
- reminder: pkill patterns matching their own launch string kill the
cron shell — resolve PID by full binary path first.
---
# Checkpoint — auto-naming hardened (2026-09-25 eve)
## What changed since the label-step checkpoint
- UI: pairing label step REMOVED (user friction: prefilled 'Amber' needed
letter-by-letter deletion). One click 'Sign in with a signer app (Amber)'
→ QR. Seed label 'Amber'; real name comes from kind-0. (fc2fe93)
- Backend: auto-name enrichment now retries 4x/20s apart with 75s budget
and per-attempt pairing_trace lines (bc736ff), after live proof that
nos.lol — the ONLY relay holding this account's kind-0 ('web5osint') —
intermittently 502s EVERY client (any UA; nostr-sdk and raw websockets
alike; worked 16:29, dead 16:40+). Single-shot fetch + flaky relay =
permanently generic label; retries fix that.
## Verified
- fetch_profile_metadata bisected per relay: only nos.lol has the kind-0;
all others return None (account metadata was only ever published there).
- 216 unit + 5 e2e green; clippy 0; fmt clean; release rebuilt bc736ff.
- frontend 125 tests green at fc2fe93.
## Open
- Profile will auto-rename to 'web5osint' on the next successful restore
once nos.lol recovers (retry loop logs 'auto-name: kind-0 fetched').
- Consider publishing the account's kind-0 to primal/damus too so naming
doesn't depend on one relay (needs one Amber signature; user action).
- Stray untracked files from another session left alone: COSMIC_THEME.md,
KeynectrAppIconPossibility02.jpeg, deferred/SignerConnectionPanel.tsx.wip/
# Checkpoint — restore secret-echo fix LIVE-VERIFIED (2026-09-25 late PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`01ce5de`** ("fix(nip46): restored sessions no
longer demand a connect secret re-echo"). Previous: `a809a67` (label
step), `5b13162` (Add-profile choice), `a6a4e6f` (flake kill).
- SESSION RESTORE LIVE-VERIFIED against real Amber at 01ce5de: log shows
restoring session -> secret validation SKIPPED (restore) ->
get_public_key -> identity check PASS -> Connected, no scan. (Amber
answered a plain 'ack' here, not 'true' — the restore path now accepts
any ack shape and trusts the re-proved identity.)
- Remaining live proof: one publish/note approved in Amber within the
120s leash (fresh pairing at 15:37 already exercised sign? — the
last_publish.json entry from 10:57 was BEFORE the fresh pairing; the
fresh profile has not published yet).
## What was completed
1. **Restore secret-echo fix (`01ce5de`)**: session restore failed 100%
against real Amber — the re-dial resent the pairing secret and the
client demanded an echo, but an already-approved signer legitimately
answers without re-echoing (echo = initial-pairing possession proof
only). ConnectUri gained a `restore` flag (only reactivate_saved_sessions
sets it): restore skips the echo and relies on expected_identity in
adopt_identity; fresh pairings still fail closed on a wrong echo.
e2f model updated: run_fake_amber now mirrors Amber ack shapes and
the restore test seeds a pairing secret (fails without the fix).
cargo test 216 unit + 5 e2e green; clippy 0; fmt clean; release
rebuilt at 01ce5de; backend restarted and LIVE restore verified.
---
# Checkpoint — signer pairing label step + vault prune (2026-09-25 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`a809a67`** ("feat(ui): name the signer
connection before the QR; prune stale vault rows"). Previous:
`5b13162` (Add-profile choice), `a6a4e6f` (e2e flake kill).
- LIVE PAIRING CONFIRMED (Sep 25): user signed in with real Amber
through the new Add-profile flow; active remote profile
npub1qn0w4a2hm9f…, client key persisted (restorable). Remaining
live proof: one publish/approval + one restart re-dial.
- Live vault pruned (not in git): 3 conns -> 1 (live Amber only),
stale 'Dev' remote stub + 2 stale 'Remote Signer' rows removed,
14 connection_secrets -> 1, client keys kept for the live conn.
# Checkpoint — Add-profile now offers Amber signer sign-in (2026-09-25)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`5b13162`** ("feat(ui): Add profile now offers
'Sign in with a signer (Amber)' first"). Previous: `a6a4e6f` (e2e
flake kill), `0982dad` (session restore).
- Live settings: relay list expanded to 6 (primal, nos.lol, damus,
snort, mutinywallet, nostr.band) in ~/.local/share/keynectr/
settings.json (backup: settings.json.backup-prerelays-20260924).
Code default relay set and the curated pairing set unchanged.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Frontend `dist/` rebuilt at 5b13162 — reload the Electron window
(Ctrl+R) or relaunch to see the new Add-profile flow. Backend
release binary unchanged since a6a4e6f.
## What was completed this session
1. **Add-profile choice (5b13162)**: clicking "Add profile" now opens a
choice modal: "Sign in with a signer app (Amber)" shows the
nostrconnect:// pairing QR inline, polls signer status every 2s, and
confirms "Amber is now your signer!" when the handshake lands
(cancel aborts mid-pairing). "Create a new key on this computer"
keeps the old local-key form with a Back step. Previously the Amber
QR was only reachable via the sidebar Signer Mode screen, so the
expected entry point hid the main flow.
- Verification: frontend 125 tests passed (CreateProfileModal suite
rewritten: choice step, QR start, connected-poll confirmation,
cancel-abort; App.test dialog title updated), `npm run typecheck`,
`npm run lint`, `npm run format:check`, `npm run electron:build`,
`npm run build` all green. Rust untouched.
## Commits added (newest first)
- `5b13162` feat(ui): Add profile now offers 'Sign in with a signer (Amber)' first
---
# Checkpoint — e2e flake killed (2026-09-24 evening)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`a6a4e6f`** ("test(nip46): kill the e2e flake —
local relay for strict kind-0, poison-tolerant vault lock"). Previous:
`0982dad` (session restore), `73bf17c` (prior checkpoint).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `a6a4e6f`** (test-only commit; same code
as `0982dad` for the app itself). Restart Electron before retesting.
## What was completed this session
1. **Root-caused and killed the intermittent e2e failure** that showed
up as 3–5 simultaneous test failures roughly 1 run in 4:
- REAL flake: `nip46_bunker_connect_params_match_spec_against_strict_amber`
published its kind-0 through the DEFAULT relay set — the real
internet (damus + the known-hanging nostr.band) — so "at least one
relay must accept the signed kind-0" was a network lottery against
the 6s send timeout. The test now pins settings to the in-process
relay like the QR test always did. Suite has zero network
dependency now.
- CASCADE amplifier: a panic while holding the test-only
`VAULT_ENV_LOCK` poisoned the mutex, so every later test died on
`PoisonError`. All four lock sites are now poison-tolerant
(`unwrap_or_else(into_inner)`) — a real failure reports as ONE.
- Verification (all green at `a6a4e6f`): **10/10 consecutive green
e2e runs** (was ~1 in 4 failing), runtime now uniform ~21.5s (was
bimodal — long tail was network waiting). `cargo test` 216 unit + 5
e2e passed, `cargo clippy --all-targets` 0 warnings, `cargo fmt
--check` clean, `cargo build --release` green. Frontend untouched.
## Commits added (newest first)
- `a6a4e6f` test(nip46): kill the e2e flake — local relay for strict kind-0, poison-tolerant vault lock
## How to resume / reproduce
- Restart Electron (new release binary). With the vault already
unlocked/unencrypted, the backend logs `[NIP46] restoring session:
peer=... as npub1...` then `identity check on restored session:
PASS` and the profile goes Connected without showing Amber a new
scan. CLI trace: `~/Tools/keynctr-debug/` logs.
- Flake regression check: `for i in 1..10; do cargo test --test
nip46_e2e; done` — all runs must pass in ~21–22s.
- Then do the still-pending live proof: Publish name / publish a note
and approve in Amber within 2 minutes (120s leash from `f53bc56`).
## Outstanding / next steps
1. **Live sign/publish through real Amber** (covers the 120s leash and
the restored session in one shot).
2. Prune the 11 stale profileless `nip46_connections` rows (cosmetic;
restore already skips them).
---
# Checkpoint — NIP-46 session restore on startup (2026-09-24)
## Where things are
- Branch `master` @ `0982dad`; see git for hashes. Details below are
as written when `0982dad` was HEAD.
## What was completed this session
1. **Session restore without a fresh scan (`0982dad`)**: the WIP from
the Sep-23 session (item 2 of the previous next-steps) is finished
and committed. Amber remembers our *client pubkey* as the identity
of an approved connection for its whole life, so the client keypair
minted at pairing is now persisted in the vault
(`Vault::connection_client_keys` — encrypted under the vault key
exactly like connection secrets, keyed by the same `VaultRef`, and
re-keyed to the identity ref when the handshake resolves it).
2. **Re-dial path**: `reactivate_saved_sessions()` picks the active
profile's live connection (or any other live one), rebuilds the
exact wire identity, re-derives the NIP-44 conversation key, and
re-sends `connect` — no QR/bunker scan. It is called at `serve()`
for unencrypted vaults and after `UnlockVault` for encrypted ones;
a restore failure can never block the GUI.
3. **Cross-account guard**: restored sessions pin `expected_identity`
before dialing; `adopt_identity` refuses (never adopts) a signer
that answers as a different account — different Amber account,
mistyped bunker, or relay spoof all fail closed.
4. Legacy connection rows without a stored client key are skipped
(they need one fresh scan, after which they become restorable too).
- Verification (all green at `0982dad`): `cargo test` **216 unit + 5
e2e passed / 0 failed** — incl. the new
`nip46_session_restore_redials_and_refuses_wrong_identity` e2e
(fresh pairing → simulated restart → re-dial connects → a fake
Amber answering as a different key is refused) and 3 new vault unit
tests (plaintext roundtrip, encrypted fail-closed, legacy-vault
parsing). `cargo clippy --all-targets` 0 warnings, `cargo fmt
--check` clean, `cargo build --release` green. Frontend untouched.
## Commits added (newest first)
- `0982dad` feat(nip46): restore saved signer sessions on startup/unlock — no fresh scan
## How to resume / reproduce
- Restart Electron (new release binary). With the vault already
unlocked/unencrypted, the backend logs `[NIP46] restoring session:
peer=... as npub1...` then `identity check on restored session:
PASS` and the profile goes Connected without showing Amber a new
scan. CLI trace: `~/Tools/keynctr-debug/` logs.
- Then do the still-pending live proof: Publish name / publish a note
and approve in Amber within 2 minutes (120s leash from `f53bc56`).
## Outstanding / next steps
1. **Live sign/publish through real Amber** (covers both the 120s
leash and the restored session in one shot).
2. Prune the 11 stale profileless `nip46_connections` rows (cosmetic;
restore already skips them).
3. Remote picture/nip05 edits, KDF upgrade (Step 5), rename pass
(Step 7) — unchanged.
---
# Checkpoint — sign_event given the human-approval timeout leash (2026-09-23 evening)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`f53bc56`** ("fix(nip46): give sign_event the
human-approval timeout leash"). Previous: `a76d8df` (feed authors).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `f53bc56`**. No frontend changes — no
renderer rebuild needed. Restart Electron before retesting.
## What was completed this session
1. **Confirmed live pairing WORKS**: today's trace log shows two full
successes against real Amber (identity adopted, CONNECTED at 15:17
and 15:37). The "Dev" profile row exists in
`~/.local/share/keynectr/profiles_vault.json` in `nip46_client`
mode — the original complaint (no profile row, no persisted
connection) is resolved as of the `c789cb4` relay-set fix.
2. **Diagnosed + fixed the remaining sign failure (`f53bc56`)**:
logs show `sign_event` timing out at 30s while Amber's valid
signature for the last request arrived ~61s after publication
("stale/duplicate response: no waiter ... dropped"). Every sign
waits on a human approving Amber's prompt, so it was using the
wrong leash. `sign_event` now uses `SIGN_TIMEOUT` = 120s (same as
the connect handshake); `get_public_key` keeps the 30s
retry-cadence timeout unchanged.
- Verification (all green at `f53bc56`): `cargo test` **213 unit + 4
e2e passed / 0 failed** (incl. `nip46_qr_pairing_handshake_and_sign`,
which signs through the changed path), `cargo clippy --all-targets`
0 warnings, `cargo fmt --check` clean, `cargo build --release` green.
Frontend untouched.
## Commits added (newest first)
- `f53bc56` fix(nip46): give sign_event the human-approval timeout leash
## How to resume / reproduce
- Restart Electron (new release binary), re-pair or restore, then
Publish name / publish a note: approve in Amber within 2 minutes and
the signature will be accepted even if the prompt took a while.
- If a sign still fails, check `~/Tools/keynctr-debug/` logs — a
"TIMED OUT after 120s" now means the phone truly never answered.
## Outstanding / next steps
1. **Live sign/publish through real Amber with the 120s leash** (fix
is log-evidence-based; needs one live publish to confirm).
2. **NIP-46 session restore on startup** (re-scan needed after restarts).
3. Prune the 11 stale profileless `nip46_connections` rows (cosmetic).
4. Remote picture/nip05 edits, KDF upgrade (Step 5), rename pass
(Step 7) — unchanged.
---
# Checkpoint — feed shows author names/pictures (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`a76d8df`** ("feat(feed): resolve author names and
pictures from kind-0 metadata"). Previous: `cd8b671` (checkpoint),
`6f4dbb2` (kind-0 via signer).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `a76d8df`**. Renderer rebuilt via
`npm run build`. Restart Electron before retesting.
## What was completed this session
1. **Feed author resolution (`a76d8df`)**: the feed previously rendered
bare npubs + initial avatars — it never looked up author metadata
anywhere. After collecting notes, the backend now runs ONE batched
kind-0 lookup for all distinct authors (8s cap, best-effort; failures
keep the npub fallback) and attaches the newest `author_name`
(display_name preferred) + `author_picture` per note. FeedScreen renders
the name (full npub on hover) and the picture avatar. Unit test covers
newest-wins, display_name preference, and blank→fallback; new
FeedScreen test covers name+picture rendering.
2. Note: anyone WITHOUT published kind-0 (like the user's own new identity
until "Publish name" runs) still shows as npub — correct fallback, not
a bug.
- Verification (all green at `a76d8df`): `cargo test` **213 unit + 4 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green; frontend
`npm test` **121 passed**, `typecheck`, `lint`, `format:check`,
`electron:build`, `build` clean.
## Commits added (newest first)
- `a76d8df` feat(feed): resolve author names and pictures from kind-0
metadata
## How to resume / reproduce
- Restart Electron → Feed: notes from authors WITH kind-0 metadata now
show names + pictures; others still show npubs.
- To see your own name on your posts: Profiles → Publish name (Amber
approval) → wait ~1 min → Feed refreshes with your name/picture.
## Outstanding / next steps
1. **Live "Publish name" + live sign/publish through real Amber.**
2. **NIP-46 session restore on startup.**
3. Prune the 11 stale profileless `nip46_connections` rows (cosmetic).
4. Remote picture/nip05 edits, KDF upgrade (Step 5), rename pass
(Step 7) — unchanged.
---
# Checkpoint — kind-0 publishes through Amber; name goes network-wide (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`6f4dbb2`** ("feat(nip46): publish kind-0 metadata
through the connected signer"). Previous: `327bf0f` (checkpoint),
`3d1c306` (silent poll).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `6f4dbb2`**. Renderer rebuilt via
`npm run build`. Restart Electron before retesting.
## What was completed this session
1. **Closed the P2 kind-0 gap (`6f4dbb2`)**: the "Publish name" IPC path
now routes through the profile's `Signing` source (same pattern as
note publishing). Embedded profiles sign locally as before; a paired
profile's kind-0 (label/picture/nip05) is signed by the remote signer —
Amber shows an approval prompt — so the name becomes visible in every
Nostr client. Disconnected sessions fail closed, never with a local
fallback. CLI keeps the local-only path (no signer instances there).
2. **E2E proof**: the strict-Amber test now also publishes kind-0 through
`App::signing_for` + `publish_metadata_signed` and asserts relay
acceptance — the exact GUI path, with identity validation. Along the way
fixed the test's production wiring (handle registered on `App`, as
`ensure_nip46_signer` does) after a `None`-handle failure poisoned the
shared env lock and cascaded to all 4 e2e tests.
3. Rename modal copy now points remote profiles at "Publish name" (Amber
approval) instead of claiming the signer app publishes it.
- Verification (all green at `6f4dbb2`): `cargo test` **212 unit + 4 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green; frontend
`npm test` **120 passed**, `typecheck`, `lint`, `format:check`,
`electron:build`, `build` clean.
## Commits added (newest first)
- `6f4dbb2` feat(nip46): publish kind-0 metadata through the connected
signer
## How to resume / reproduce
- Restart Electron → Profiles → **Publish name** on the paired profile →
approve the `sign_event` (kind 0) prompt in Amber → per-relay report.
Check any Nostr client: the display name (your vault label) now appears
instead of the bare npub.
- E2E: `cargo test --test nip46_e2e` (4 tests incl. kind-0-via-signer).
## Outstanding / next steps
1. **Live "Publish name" through real Amber** (e2e-proven, never live-run).
2. **NIP-46 session restore on startup** (re-scan needed after restarts).
3. Prune the 11 stale profileless `nip46_connections` rows (cosmetic).
4. Remote picture/nip05 edits, KDF upgrade (Step 5), rename pass
(Step 7) — unchanged.
---
# Checkpoint — silent state poll + session-restore gap named (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`3d1c306`** ("fix(ui): silent background state poll").
Previous: `5714349` (checkpoint), `6ebc364` (remote rename).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: current at `6ebc364` (Rust unchanged since). Renderer
rebuilt via `npm run build`. Restart Electron before retesting.
## What was completed this session
1. **Diagnosed the Home flicker (my `a8d112b` polling regression)**: every
5s poll delivered a fresh state object identity, so Home's publications
loader (`useProfilePublications`, keyed on `settings.relays` identity)
refired forever — "Loading publications…" cycling. Fixed in `3d1c306`:
`refresh()` keeps the previous state object when content-identical (JSON
compare), so idle polls are re-render silent. Poll timer uses global
`setInterval` (testable under fake timers).
2. **Regression test + harness fidelity**: new HomeScreen test proves
`feed_get` fires once across 12s of polling (verified RED without the
guard: 3 fetches). `fakeBackend` `get_state` now deep-copies like the
real IPC boundary — the shared-reference version masked the bug.
3. **Diagnosed the publish failure**: backend restarted 14:57 for the rename
build, which killed the in-memory NIP-46 session (client keys + subs are
memory-only by design). Vault keeps the connection row, but there is NO
session restore on startup — publishing fails closed with "external
signer selected but not connected" while Amber still shows connected.
Re-scanning the QR re-establishes it (proven path). Automatic restore
(fresh `connect` against the stored row) is the follow-up, not this
change.
- Verification (all green at `3d1c306`): frontend `npm test` **120 passed
(18 files)**, `typecheck`, `lint`, `format:check`, `electron:build`,
`build` clean. Rust untouched (last green at `6ebc364`: 212 unit + 4 e2e,
clippy/fmt clean).
## Commits added (newest first)
- `3d1c306` fix(ui): silent background state poll - no refetch churn when
vault unchanged
## How to resume / reproduce
- Restart Electron → Home no longer flickers; Rename works as in `6ebc364`.
- To publish: Signer Mode → Show QR → re-scan in Amber (session does not
survive restarts yet) → Compose → Publish (approval lands in Amber).
## Outstanding / next steps
1. **NIP-46 session restore on startup** (fresh handshake against the stored
connection row) — publishing after any restart currently needs a re-scan.
2. **Live sign/publish through paired Amber** (never exercised live).
3. Prune the 11 stale profileless `nip46_connections` rows (cosmetic).
4. Remote picture/nip05 edits, kind-0 via `Signing` (P2), KDF upgrade
(Step 5), rename pass (Step 7) — unchanged.
---
# Checkpoint — remote-profile rename; live pairing aftermath (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`6ebc364`** ("fix(profiles): label-only rename for
secretless remote-signer profiles"). Previous: `ca62111` (live-pairing
checkpoint), `a8d112b` (state polling).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `6ebc364`** (Rust changed). Renderer rebuilt
via `npm run build`. Restart Electron before retesting.
## What was completed this session
1. **Explained the generic "Remote Signer" label**: the paired identity
(`npub1f3tura…`) has NO kind-0 metadata on damus/primal/nos.lol (direct
REQ check — all EOSE), so the background enrichment correctly found
nothing to upgrade the pairing label with.
2. **Label-only rename for remote profiles (`6ebc364`)**: `rename_profile`
used to resolve the local secret first, so renaming a paired profile
errored. Secretless `Nip46Client` rows now rename vault-side with an
empty publish report (kind-0 signing still awaits the P2 external-signer
reroute). Rename modal reports "saved on this device" for the empty
report instead of a misleading "published to 0 relays". New regression
test `rename_profile_updates_label_for_secretless_remote_profiles`.
- Verification (all green at `6ebc364`): `cargo test` **212 unit + 4 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green; frontend
`npm test` **119 passed**, `typecheck`, `lint`, `format:check`,
`electron:build`, `build` clean.
## Commits added (newest first)
- `6ebc364` fix(profiles): label-only rename for secretless remote-signer
profiles
## How to resume / reproduce
- Restart Electron, Profiles → rename `Remote Signer` to any display name
(e.g. your Nostr name) — saves instantly, no error.
- To show your real Nostr name everywhere instead: publish a kind-0 profile
(name/picture) from Amber or another client; a future enrichment pass can
pick it up (no re-fetch path yet).
## Outstanding / next steps
1. **Live sign/publish through paired Amber** (never exercised live).
2. Prune the 11 stale profileless `nip46_connections` rows (cosmetic).
3. Remote picture/nip05 edits (same local-key limitation as rename had),
`publish_profile_metadata` kind-0 via `Signing` (P2), KDF upgrade
(Step 5), rename pass (Step 7) — unchanged.
---
# Checkpoint — FIRST LIVE END-TO-END AMBER PAIRING SUCCEEDED (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`a8d112b`** ("fix(ui): poll vault state so background
Amber pairing appears without reload"). Previous: `fb14976`
(checkpoint), `e02417b` (damus pairing relays), `8f055f7` (NIP-46 spec
fix + trace).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: rebuilt at `e02417b`; `a8d112b` is frontend-only (no
rebuild needed — Electron loads the renderer from `frontend/dist`, rebuilt
via `npm run build`). Restart Electron to pick up the renderer change.
## What was completed this session
1. **ROOT CAUSE OF THE WHOLE SAGA — Amber's OS notification gate**: Amber's
in-app log showed all four `get_public_key` RPCs arriving on all relays,
each followed by `notifications disabled`. Amber's
`EventNotificationConsumer.consume` returns early when Android
notifications are blocked for the app — every signer request dies
silently after connect. Fix was on the phone: Settings → Apps → Amber →
Notifications → Allow. (Battery "unrestricted" was already set and was
never the issue.)
2. **First live end-to-end pairing**: after enabling notifications, the next
scan completed in ~1s — `identity adopted:
npub=npub1f3tura29nrmhpp5v88z45knjejzdcx7ulvcz2jswau4raa7v25nsh6ltfw —
CONNECTED`. Vault holds the secretless `Nip46Client` profile as active;
connection re-keyed under the identity. Amber shows the app; desktop
Signer screens show Connected.
3. **UI staleness found by the success (`a8d112b`)**: Home/Profiles read the
launch-time state snapshot and never refetch (IPC has no push channel),
so they still showed first-run Welcome after the background pairing.
`AppProvider` now polls `getState()` every 5s (cheap local read, errors
swallowed — screens surface their own failures).
- Verification (all green at `a8d112b`): frontend `npm test` **119 passed
(17 files)**, `typecheck`, `lint`, `format:check`, `electron:build`,
`build` clean. Rust untouched (last green at `e02417b`: 211 unit + 4 e2e,
clippy/fmt clean, release green).
## Commits added (newest first)
- `a8d112b` fix(ui): poll vault state so background Amber pairing appears
without reload
## How to resume / reproduce
- Restart Electron (renderer changed), open Home/Profiles: the `Remote
Signer` profile (`npub1f3tura…`) is listed and active; state refreshes
within ~5s without reload.
- Next: publish a note / sign through the paired Amber (approval appears in
Amber; `sign_event` path was e2e-tested, never yet live-tested).
## Outstanding / next steps
1. **Live sign/publish through paired Amber** (never exercised live).
2. Prune the 11 stale profileless `nip46_connections` rows left by failed
scans (cosmetic; vault-only cleanup).
3. `publish_profile_metadata` kind-0 via `Signing` (P2), KDF upgrade (Step
5), rename pass (Step 7) — unchanged.
---
# Checkpoint — Amber-side silence proven; damus rejoins pairing set (2026-09-23 PM)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`e02417b`** ("fix(pairing): offer relay.damus.io
first"). Previous: `5456835` (checkpoint), `8f055f7` (NIP-46 spec fix).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `e02417b`** — Electron spawns this one.
IMPORTANT: the running Electron/backend (started 10:06) predates every fix
— quit fully and relaunch before any live test.
## What was completed this session
1. **Stale-binary trap found**: the user's live scans ran against a backend
started 10:06, before the 13:01 rebuild. Documented the full-quit +
relaunch procedure (`pkill`, port-5173 conflict fix).
2. **Live scan on the NEW binary (`8f055f7`) analyzed end to end**: the
`[NIP46]` trace is perfect through secret validation, but `get_public_key`
(accepted by both relays, 4 attempts) gets zero responses and the demux
subscription logs zero inbound — nothing is dropped, nothing arrives.
3. **Fetched our own request event off relay.primal.net**: author, `p`-tag,
kind all textbook-correct. Unauthenticated REQ works on all four relays
(no NIP-42 gate). Ephemeral retention is short (<~20 min), so both sides
must be live-subscribed — late joiners get nothing.
4. **Read Amber's own signer source** (`BunkerRequestUtils.kt`, master):
fresh localKey per approval, URI relays honored for nostrconnect://,
listen-REQ-before-response, auto-approve `get_public_key`. Our wire
behavior matches it — with current Amber the halves should meet.
5. **Amber 6.6.5 (current) forensics**: app entry correct (primal+nos.lol),
activity log shows ONLY the Connect ack — Amber never receives our RPC.
Battery already unrestricted.
6. **Relay experiment (`e02417b`)**: Amber's issue history documents NIP-46
working over `relay.damus.io`; same-day write-canary from this network
shows damus connected + accepting ephemeral 24133. Pairing set is now
damus → primal → nos.lol (damus first).
- Verification (all green at `e02417b`): `cargo test` **211 unit + 4 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green. Frontend
untouched (suite last green at `8f055f7`: 119 tests, typecheck, lint,
format, electron:build, build).
## Commits added (newest first)
- `e02417b` fix(pairing): offer relay.damus.io first
## How to resume / reproduce
- **Quit Electron fully first** (`pkill -f "electron ."` → `pgrep` shows
nothing), relaunch vite + Electron (release binary is current at
`e02417b`), verify backend start time is NOW, then Signer Mode → Show QR
(QR now lists damus/primal/nos.lol) → scan in Amber → approve with Amber
foregrounded. Expect `[NIP46] ... UI connection state updated: Connected`.
- If Amber still shows only the Connect ack, its phone-side relay path is
the remaining suspect — report which relays Amber's entry lists and any
new activity lines.
## Outstanding / next steps
1. **Live re-scan against `e02417b`** (decisive; needs app restart first).
2. If damus doesn't help, consider pruning to fewer relays (force both ends
onto one) or capturing Amber's in-app relay/connection log.
3. `publish_profile_metadata` kind-0 via `Signing` (P2), KDF upgrade (Step
5), rename pass (Step 7) — unchanged.
---
# Checkpoint — NIP-46 handshake debugged: connect-params spec fix + full [NIP46] trace + visible errors (2026-09-23)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`8f055f7`** ("fix(nip46): spec-correct connect params,
full-handshake [NIP46] trace, visible handshake errors"). Previous:
`2bc6321` (identity-RPC retry), `9cfc1cf` (relay-accept trace).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `8f055f7`** — Electron spawns this one.
## What was completed this session
1. **Spec-verified the whole signer flow against NIP-46** (20-point checklist
in the session brief). URI generation, keypair handling, subscribe-before-
publish, relay URL encoding, kind-24133 listen, NIP-44 decrypt, secret
validation, identity-via-`get_public_key`, account-state update and UI
polling were all traced file-by-file (`nip46_client.rs`, `ipc.rs`,
`SignerModeScreen.tsx`, `relays.rs`, `nip46_e2e.rs`).
2. **Found + fixed the definitive paste-flow (`bunker://`) bug (`8f055f7`)**:
`run_handshake` sent `connect` params as `[client_pubkey, secret]`, but
NIP-46 (and rust-nostr's own `NostrConnectRequest::Connect` codec) require
`[<remote-signer-pubkey>, <optional_secret>]`. Strict signers answer a
malformed connect with silence, stalling the handshake with zero feedback.
New pure helper `connect_params(peer, secret)` + 2 unit tests (one
round-trips through the library codec).
3. **Client keypair is now always ephemeral** in `connect()` (was: reused the
vault's local secret key when unlocked). Per NIP-46 the client keypair is
disposable and must never be confused with the user's identity or a vault
key. QR flow already did this; both flows now agree.
4. **Full `[NIP46]` diagnostic trace** across both flows (console stderr, in
addition to the existing `pairing-trace.log` forensics): keypair gen,
client pubkey, secret presence (NEVER values), relay connecting/connected,
subscription created/registered (filter + id), URI generated, every inbound
24133 (author, tags, decrypt OK/real-error, method, secret PASS/FAIL,
remote pubkey), every RPC publish/timeout/response-routing decision
(including stale/duplicate ids), relay-health pre-check before each
`get_public_key` retry (fails fast with a useful error when no relay is
connected), user pubkey, account-state update, UI-state update.
5. **Failures are visible now, not silent**: `fail()` keeps the FIRST
(specific) error instead of letting the demux exit overwrite it with
generic "Connect handshake failed"; `SignerModeScreen` renders
`status.error` as an alert in both the pairing and the idle/paste branches
and shows a "Connection request sent — approve it in Amber" hint while a
paste-URI connect is in flight. No fake Connected state anywhere:
`Connected` still requires `get_public_key` to resolve the identity.
6. **Tests**: new strict-Amber e2e
(`nip46_bunker_connect_params_match_spec_against_strict_amber`) — fake
signer rejects `connect` unless params[0] is its own pubkey, then serves
identity + sign like Amber; asserts the REAL user pubkey lands in the
vault as a secretless row AND becomes the active profile. Verified RED on
the old param order, GREEN on the fix. New `SignerModeScreen.test.tsx`
(3 tests: QR waiting hint, connecting hint, failed-handshake error
visible) + `nip46_*` dispatch in `fakeBackend.ts`. Also widened the e2e
`VAULT_ENV_LOCK` to whole-test bodies (data_dir() re-reads process-global
XDG_DATA_HOME on every save — one cross-write flake seen under full-suite
parallelism) with targeted `await_holding_lock` allows.
- Verification (all green at `8f055f7`): `cargo test` **211 unit + 4 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green; frontend
`npm test` **119 passed (17 files)**, `typecheck`, `lint`, `format:check`,
`electron:build`, `build` clean.
## Commits added (newest first)
- `8f055f7` fix(nip46): spec-correct connect params, full-handshake [NIP46]
trace, visible handshake errors
## How to resume / reproduce
- Dev loop: `cd frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
(release binary already rebuilt at `8f055f7`).
- **Live Amber re-scan (still the decisive test)**: Signer Mode → Show QR →
scan in Amber → approve, **keep Amber open in the foreground**. Watch
terminal/backend console for the `[NIP46]` lines in order (see "Expected
logs" in the session report) and `~/Tools/keynctr-debug/pairing-trace.log`.
- E2E: `cargo test --test nip46_e2e` (4 tests, no network).
- Frontend: `cd frontend && npm test -- SignerModeScreen`.
## Outstanding / next steps
1. **Live Amber re-scan against `8f055f7`** — the QR flow's `get_public_key`
silence (5 scans × 4 attempts, publishes accepted by primal+nos.lol, zero
responses) is downstream of our publish: either Amber never receives our
request (e.g. its subscription fails, NIP-42 AUTH on those relays from
mobile, or the app was backgrounded) or it receives and never answers.
The new demux logs distinguish these live: zero inbound lines during the
retries ⇒ Amber-side; inbound-but-dropped lines ⇒ our filter/decrypt.
2. If the live trace shows zero inbound during retries, next step is a relay
experiment (add a no-auth REQ relay to the pairing set) and/or confirming
Amber stays foregrounded with network.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute through
`Signing` for external profiles (P2, pre-existing).
4. Step 5 (KDF upgrade), Step 7 (rename pass incl. `homepage` URL).
---
# Checkpoint — first live Amber pairing succeeded; subscription race fixed (2026-09-23)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`9cfc1cf`** ("chore(pairing): trace which relays
accepted each identity RPC"). Previous: `2e98e69` ("fix(pairing): subscribe
to signer replies BEFORE the handshake publishes"), `13a66f2`
("feat(onboarding): first-run screen offers import-existing-account and
external-signer paths"), `963b740`, `c789cb4` (relay-set canary fix).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `2e98e69` (2026-09-23 ~08:45 CDT)** — Electron
spawns this one. Dev loop: vite :5173 +
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1
KEYNCTR_FORCE_WAYLAND=1 KEYNCTR_NO_RELAUNCH=1 npx electron .` (the
FORCE_WAYLAND/NO_RELAUNCH pair keeps the crash-fallback ladder from drifting
the window onto XWayland, where it fails to map on this Hyprland session).
- What was completed this session:
1. **First genuine live pairing captured** (Sep 23 08:31 CDT): pairing
started → inbound 24133 → `connect` accepted with secret echo →
`paired: connection stored` — the c789cb4 relay fix is proven end to end.
The session then died at identity adoption: `get_public_key` timed out.
2. **Root cause `2e98e69`**: both connect flows spawned the handshake task
BEFORE `run_demux` registered the kind-24133 author subscription, so the
relay delivered Amber's fast identity reply into a gap with no active
subscription and it was dropped; 30s later the RPC timed out, leaving the
vault with a stored connection (`profile_npub: None`) but no profile —
UI said "connected" while Home still showed first-run. Fix: new
`subscribe_to_signer` opens the notification stream + subscription before
any publish; `run_sign_task` and `run_paired` both subscribe first and
pass the stream to `run_demux`. `futures-util` promoted to runtime dep.
3. **Onboarding `13a66f2`**: first-run Home now offers three paths — Create
a new profile / I already have an account (ImportProfileModal) / Sign in
with a signer (navigates to Signer Mode). Copy states per-mode key truth
(local vault vs remote signer key never on this device).
- Verification (all green): `cargo test` **209 unit + 3 e2e passed / 0
failed**, `cargo clippy --all-targets` 0 warnings, `cargo fmt --check`
clean, `cargo build --release` green; frontend `npm test` **116 passed**,
`typecheck`, `lint`, `format:check` clean.
- Resume / reproduce: dev-loop command above; scan the pairing QR with Amber
from Signer Mode. Expected trace in `/home/avi/Tools/keynctr-debug/
pairing-trace.log`: `pairing started → inbound 24133 → connect response
accepted → paired: connection stored → identity adopted … CONNECTED`, and a
profile row appearing in `~/.local/share/keynectr/profiles_vault.json`.
- Outstanding / next steps:
- **Live re-scan against `2e98e69`** — the only missing proof; the stale
half-paired connection row (`Remote Signer`, signer_pubkey 9597b46d…) is
replaced by the new pairing.
- Consider pruning stale `Remote Signer` connection rows on failed
adoption so the vault never keeps a profileless connection.
---
# Previous checkpoint — pairing relay set canary-tested; 24133-blocking relays removed (2026-09-22)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`c789cb4`** ("fix(pairing): drop purplepag.es and
nostr.band from the pairing relay set"). Previous: `2e5938a`, `d4d87b8`,
`f6bf6a9`, `edd4e56` (pairing feature HEAD).
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `c789cb4` (2026-09-22 20:39)** — Electron
spawns this one.
- Verification (all green at `c789cb4`): `cargo test` **209 unit + 3 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green. No frontend
changes (npm suite last green at `edd4e56`).
## What was completed since the last checkpoint
- **First real live pairing attempt was captured — and diagnosed
(`c789cb4`)**: the trace log now shows a genuine live session for the
first time: `pairing started` at 18:01:29 CDT (ephemeral
`7c695714…`, relays damus/nostr.band/primal/purplepag) followed by
`session failed: Pairing timed out` at 18:06:39 — the 5-min window ran
with ZERO inbound 24133 events (capture file untouched since Sep 18).
Post-hoc relay sweep (read-only REQ, results in
`/tmp/probe-results.json` + `/tmp/window-24133.json`) found **no
kind-24133 event tagged to the ephemeral key on any of the four
relays, and no kind-24133 at all in the whole 18:01–18:06 window**.
So Amber connected to a relay and published, but the event was
discarded before storage.
- **Smoking gun via anonymous canary** (throwaway random keys, zero user
data; `~/Tools/keynctr-debug/canary-24133.py`, results
`/tmp/canary-results.json`): writing a kind-24133 event to each pairing
relay → **purplepag.es: `OK false "blocked: kind 24133 is not
allowed"`** — it silently drops signer connect traffic;
**relay.nostr.band: WebSocket handshake hangs** (also pay-to-read);
relay.damus.io + relay.primal.net + nos.lol: accepted + readable;
relay.snort.social: accepted live ("ephemeral: will not be stored"),
fine for pairing push. If Amber picked purplepag.es (it is in the URI),
it would say "connected" while its connect event was rejected —
exactly the observed symptom, and consistent with the earlier
parse-failure theories being dead ends (the payload never arrived).
- **Fix (`c789cb4`)**: `pairing_relays()` is now damus.io, primal.net,
nos.lol, relay.snort.social — both blockers removed, both replacements
canary-verified for ephemeral 24133. Regression test
`pairing_relays_exclude_24133_blockers` pins the blockers out.
- **Debug-dir hygiene**: `inspect.py` removed (it shadowed the stdlib
`inspect` module for any script run from that directory and leaked
stale forensics output into terminal sessions); moved to
`inspect-capture.txt`. Canary + probe scripts kept under
`~/Tools/keynctr-debug/` (untracked tools dir).
## Commits added (newest first)
- `c789cb4` fix(pairing): drop purplepag.es and nostr.band from the
pairing relay set
## How to reproduce / exercise
- **LIVE AMBER RE-SCAN (the decisive test, cannot run unattended)**:
launch the app (release binary is current at `c789cb4`):
`cd frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
Signer mode -> Show QR -> scan in Amber -> approve. The QR's relay list
no longer contains purplepag.es/nostr.band. Expected trace:
`pairing started` -> `inbound 24133 from …` -> `connect response
accepted (secret echo verified)` -> `paired: connection stored;
handing over to identity handshake` -> `identity adopted: npub=… —
CONNECTED`. Watch with `bash ~/Tools/keynctr-debug/watch-pairing.sh 240`.
- Canary: `python3 ~/Tools/keynctr-debug/canary-24133.py` (throwaway
keys; results to /tmp/canary-results.json).
- E2E: `cargo test --test nip46_e2e` (no network; trace file stays
untouched).
## Outstanding / next steps
1. **Live Amber re-scan against `c789cb4`** — the relay-set fix is the
best-evidenced change yet but only a live scan proves it.
2. Consider whether the user's two enabled relays (settings.json:
damus.io + nostr.band) should swap nostr.band for nos.lol for
ordinary traffic too (it hangs the handshake today; also pay-to-read).
3. If trace still shows timeout with the new set, next hypothesis is
Amber not actually publishing (its "connected" = relay socket open);
compare against Amber's own relay debug screen.
4. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
5. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary — done in
`d101b8e`), Step 7 (rename pass incl. `homepage` URL).
---
# Checkpoint — 'keys never leave' claim corrected per-mode (2026-09-22); prior: pairing forensics de-noised
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`d4d87b8`** ("docs: replace blanket 'keys never leave
the machine' claim with per-mode truth"). Previous: `f6bf6a9`, `edd4e56`
(pairing feature HEAD), `deeb4f9`, `d52fa58`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: still built at `f6bf6a9` — `d4d87b8` changed only
Markdown docs, no rebuild needed.
- Verification at `d4d87b8`: `cargo fmt --check` clean, `cargo test` **208
unit + 3 e2e passed / 0 failed** (first run showed 1 e2e flake; both the
targeted `cargo test --test nip46_e2e` rerun and the full rerun were
green). No frontend changes (npm suite last green at `edd4e56`).
## What was completed since the last checkpoint
- **Honest security copy (`d4d87b8`)**: the blanket "keys never leave the
machine" claim in README.md (tagline), PRODUCT.md (purpose, positioning,
principle 1), and DESIGN.md (North Star) was replaced with the per-mode
truth: in embedded/bunker modes keys stay in the local encrypted vault
and never reach the renderer; in external NIP-46 signer mode the key
never *arrives* on this machine — a strictly stronger posture against
desktop compromise. README security notes gained an explicit bullet
saying external signer mode can be *more* secure. App UI
(`SignerModeScreen.tsx`) already ranked external signer "Most Secure /
Private key NEVER on this device" — no code or test changes were needed.
## Commits added (newest first)
- `d4d87b8` docs: replace blanket 'keys never leave the machine' claim with per-mode truth
## How to reproduce / exercise
- **LIVE AMBER TEST (still the only missing step from `f6bf6a9`)**: launch
the app: `cd frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
Signer mode -> Show QR -> scan in Amber -> approve. Expected trace:
`pairing started: ephemeral=…` -> `inbound 24133 from …` -> `connect
response accepted (secret echo verified)` -> `paired: connection stored;
handing over to identity handshake` -> `identity adopted: npub=… —
CONNECTED`. Watch with `bash ~/Tools/keynctr-debug/watch-pairing.sh 240`.
- Docs-only change: `git show d4d87b8`.
## Outstanding / next steps
1. **Live Amber re-scan required** (cannot be done from an unattended run).
2. If trace shows `pre-handshake 'get_public_key' ignored`, relax the
connect-only gate.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
4. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — pairing forensics fully de-noised; Amber fix still awaiting live test (2026-09-21)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`f6bf6a9`** ("fix(pairing): keep e2e traffic out of
the live pairing trace + trace the handover"). Feature HEAD unchanged:
`edd4e56` (connect-response root-cause fix). Previous: `deeb4f9`,
`d52fa58`, `5f9c64f`, `21c522b`, `188b2eb`, `aedde8f`, `759b5dd`,
`5aa122d`, `f59c2b1`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary: **rebuilt at `f6bf6a9` (2026-09-21 ~20:25)** — the
current Sep 19 binary was superseded. Electron spawns this one.
- **STILL NO LIVE AMBER SCAN against the fixed binary.** Proof: trace log
has zero `pairing started` lines (every real pairing writes one) and the
capture file's newest event is Sep 16 21:03. NOTE: the `identity
adopted — CONNECTED` lines dated Sep 18–21 (20:59, 20:43, 20:39–20:44,
20:13) are all **cargo-test / cron-verification e2e traffic**, not live
scans — that exact confusion was the reason for `f6bf6a9` below.
- Verification (all green at `f6bf6a9`): `cargo fmt --check` clean,
`cargo test` **208 unit + 3 e2e passed / 0 failed**, `cargo clippy
--all-targets` 0 warnings, `cargo build --release` green. No frontend
changes (npm suite last green at `edd4e56`).
## What was completed since the last checkpoint
- **Forensics de-noising (`f6bf6a9`)**: `adopt_identity`'s
`identity adopted — CONNECTED` trace line had no loopback gate, so every
e2e run appended fake CONNECTED entries to
`~/Tools/keynctr-debug/pairing-trace.log` — three appeared during this
cron's own `cargo test` runs and had to be manually distinguished from a
real Amber scan. The line is now gated on `live_relays()` (same loopback
test the event capture already uses; verified live: re-running
`cargo test --test nip46_e2e` no longer touches the trace file's mtime).
Also added a `paired: connection stored; handing over to identity
handshake` trace line at the QR-pairing handover, so a stall between the
connect echo and `get_public_key` now names itself in the trace.
- **Post-fix audit of the pairing flow** (read-through, no code change):
the QR path (pairing loop -> connect-response echo -> connection+secret
persisted -> demux + `adopt_identity` -> profile row + vault save) is
coherent end to end; `send_rpc` waiters register before publish; demux
routes responses by id; failure paths call `fail()` which traces
`session failed: …`. Nothing further to fix without live data.
## Commits added (newest first)
- `f6bf6a9` fix(pairing): keep e2e traffic out of the live pairing trace + trace the handover
## How to reproduce / exercise
- **LIVE TEST (the only missing step)**: launch the app (release binary is
current): `cd frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
(or run the packaged app). Signer mode -> Show QR -> scan in Amber ->
approve. Expected trace: `pairing started: ephemeral=…` ->
`inbound 24133 from …` -> `connect response accepted (secret echo
verified)` -> `paired: connection stored; handing over to identity
handshake` -> `identity adopted: npub=… — CONNECTED`; the profile row +
`nip46_connections` entry then land in
`~/.local/share/keynectr/profiles_vault.json` (its mtime is Sep 12 —
untouched since, further proof no live pairing has ever persisted).
- Watch during a live scan: `bash ~/Tools/keynctr-debug/watch-pairing.sh 240`.
- E2E: `cargo test --test nip46_e2e` (no network; 3 tests, both connect
shapes, empty-vault isolation guards; trace file stays untouched).
## Outstanding / next steps
1. **Live Amber re-scan required** (cannot be done from an unattended
run). Trace log names the exact stop point if it stalls.
2. If trace shows `pre-handshake 'get_public_key' ignored`, relax the
connect-only gate.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
4. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — e2e vault-isolation bug found; Amber fix awaiting live test (2026-09-20)
## Where things are (as of 2026-09-20)
- Branch: `master` @ `deeb4f9` + `d52fa58`. Feature HEAD: `edd4e56`.
## What was completed since the last checkpoint
- **e2e vault-isolation bug found and fixed (`d52fa58` + `deeb4f9`)**:
both e2e tests seeded their isolation vault at
`$XDG_DATA_HOME/profiles_vault.json`, but `vault::data_dir()` is
`$XDG_DATA_HOME/keynectr` — so `App::load()` never saw the seed and
`try_migrate_legacy_vault()` silently migrated the **legacy repo vault
(real profile, plaintext secret key)** into the test process. Forensic
proof: legacy-vault backups `profiles_vault.json.backup-1789868634/670`
timestamped Sep 19 20:43:54 + 20:44:30 — exactly the cargo-test runs
around `edd4e56`. Consequence: the `identity adopted` trace lines from
Sep 19 20:43 were **e2e sessions, not a live Amber scan** (session-level
traces are not gated by `live_capture`). Fix: seed at
`tmp/keynectr/profiles_vault.json` + assert-empty guard after every e2e
`App::load()` so any future silent migration fails loudly. Verified after
the fix: repo legacy vault byte-identical, backup count unchanged (77).
- **Pairing status unchanged**: `edd4e56` (accept the NIP-46 connect
*response* shape) remains the root-cause fix; it has never yet faced a
live scan.
## Commits added (newest first)
- `deeb4f9` test(e2e): assert vault isolation actually isolated
- `d52fa58` test(e2e): isolate the e2e vault at the real data_dir path
## How to reproduce / exercise
- **LIVE TEST (the only missing step)**: launch the app (release binary is
current): `cd frontend && npx vite --port 5173` then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`
(or run the packaged app). Signer mode -> Show QR -> scan in Amber ->
approve. Expected: trace shows `connect response accepted (secret echo
verified)` then `identity adopted: npub=… — CONNECTED`, and the profile
row + `nip46_connections` entry land in the vault.
- Watch during a live scan: `bash ~/Tools/keynctr-debug/watch-pairing.sh 240`.
- E2E: `cargo test --test nip46_e2e` (no network; 3 tests, both connect
shapes, empty-vault isolation guards).
## Outstanding / next steps
1. **Live Amber re-scan required** (cannot be done from an unattended
run). Trace log names the exact stop point if it stalls.
2. If trace shows `pre-handshake 'get_public_key' ignored`, relax the
connect-only gate.
3. Consider whether `identity adopted` / `session failed` trace lines
should also carry a live/e2e marker (the empty-vault guard keeps e2e out
of the real vault now, but the trace file can still mix both).
4. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
5. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — Amber pairing: accept the NIP-46 connect *response* shape (2026-09-19)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`edd4e56`** ("fix(pairing): accept the NIP-46 connect
*response* shape Amber actually sends"). Previous feature HEADs:
`21c522b`, `188b2eb`, `aedde8f`, `759b5dd`, `5aa122d`, `f59c2b1`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary rebuilt at `edd4e56` (2026-09-19 ~20:50) — Electron spawns
this one. **No live Amber scan has run against this build yet.**
- Verification (all green at `edd4e56`): `cargo fmt --check` clean,
`cargo test` **208 unit + 3 e2e passed / 0 failed**, `cargo clippy
--all-targets` 0 warnings, `cargo build --release` green. Frontend:
`npm test` **116 passed**, `npm run typecheck` / `lint` /
`format:check` / `build` / `electron:build` all clean.
## What was completed since the last checkpoint
- **The likely root cause of the whole Amber pairing failure (`edd4e56`)**:
for a client-initiated `nostrconnect://` scan, NIP-46 specifies the signer
sends a connect **response** — `{"id":…,"result":"<secret>"}` — not a
connect *request* (spec: "the _remote-signer_ … then sends `connect`
*response* event to the `client-pubkey`"; result is `"ack"` OR the secret;
"Client discovers remote-signer-pubkey from connect response author").
`run_pairing_task` only recognized an inbound `{"method":"connect"}`
request. A bare `{"result":…}` parsed into `RawRequest` with an *empty*
method (the lenient deserializer never fails, so the old "not a NIP-46
request" log couldn't fire) and was silently swallowed as "pre-handshake
'' ignored" — Amber showed "connected", Keynctr sat in the pairing loop
until timeout, no profile row, nothing persisted. That exactly matches the
original symptom and the observed 89-byte plaintext
(`{"id":"…","result":"<16-byte-hex-secret>"}` fits 65–96 B).
The pairing loop now verifies the echoed secret (or `"ack"`) directly and
proceeds to identity adoption; a signer-sent `error` fails fast with the
signer's message; a wrong secret is still ignored as spoofing; the legacy
request shape keeps working. Trace lines added for each outcome.
- **e2e regression lock**: `run_fake_scanner` takes a `connect_shape`
param; new `nip46_qr_pairing_connect_response_shape` test simulates
Amber's spec shape end-to-end. Confirmed RED on the pre-fix parser
(pairing times out) and GREEN with the fix.
## Commits added (newest first)
- `edd4e56` fix(pairing): accept the NIP-46 connect *response* shape Amber
actually sends
## How to reproduce / exercise
- Dev loop (unchanged): `npx vite --port 5173` in `frontend/` FIRST, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`.
- GUI: Signer mode -> "Show QR" -> scan in Amber -> approve. Expected now:
trace log shows `connect response accepted (secret echo verified)` then
`identity adopted: npub=… — CONNECTED`, and a new profile row appears.
- Watch during a live scan: `bash ~/Tools/keynctr-debug/watch-pairing.sh 240`
(new helper — tails trace/capture, prints any new lines).
- E2E: `cargo test --test nip46_e2e` (no network; 3 tests incl. both
connect shapes).
## Outstanding / next steps
1. **Live Amber re-scan required** to confirm end-to-end (cannot be done
from an unattended run). If it still stalls, `pairing-trace.log` names
the exact stop point.
2. If trace shows `pre-handshake 'get_public_key' ignored`, relax the
connect-only gate (the log line names it outright).
3. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
4. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — durable pairing trace log + forensics-file hygiene (2026-09-18)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`21c522b`** ("chore(pairing): durable trace log + keep
e2e loopback traffic out of forensics files"). Previous feature HEADs:
`188b2eb`, `aedde8f`, `759b5dd`, `5aa122d`, `f59c2b1`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary rebuilt at `21c522b` (2026-09-18 ~21:00) — Electron spawns
this one. **No live Amber scan has run against `188b2eb` or `21c522b`
yet**: the capture file shows no real scan since Sep 16 21:03.
- Verification (all green at `21c522b`): `cargo fmt --check` clean,
`cargo test` **208 unit + 2 e2e passed / 0 failed**, `cargo clippy
--all-targets` 0 warnings, `cargo build --release` green. Frontend:
`npm test` **116 passed**, `npm run typecheck` / `lint` /
`format:check` / `build` / `electron:build` all clean.
## What was completed since the last checkpoint
- **Forensics contamination found and fixed**: pairing-capture.jsonl had
grown two new lines (Sep 18 20:08 + 20:42) that were NOT Amber — they were
the e2e harness's fake-scanner events, appended because the capture path
was hardcoded and the loopback e2e test pairs through the same
`run_pairing_task`. Removed the two test events from the capture file
(backup: `pairing-capture.jsonl.bak-20260918`); loopback pairings now skip
the capture file and the pairing-session trace lines entirely (the
session-level `identity adopted` / `session failed` lines are shared with
the bunker flow and can still include a labelled e2e entry — distinguish
by the loopback relay set in the preceding `pairing started` line, which
real sessions never have).
- **Durable pairing trace (`pairing-trace.log`)**: every pairing decision
point now appends a timestamped line to
`~/Tools/keynctr-debug/pairing-trace.log` — pairing started (ephemeral key
+ relay set), inbound 24133, decrypt failure (real NIP-44 error),
not-a-request (exact payload), pre-handshake method, connect answered,
identity adopted (npub), session failed (reason). Backend stderr only
reaches the Electron console and /tmp gets cleaned, so previously a failed
live handshake left NO durable trace. Verified working: the e2e run after
the change wrote `identity adopted ... CONNECTED` lines proving the trace
path end-to-end.
## Commits added (newest first)
- `21c522b` chore(pairing): durable trace log + keep e2e loopback traffic
out of forensics files
## How to reproduce / exercise
- Dev loop (unchanged): `npx vite --port 5173` in `frontend/` FIRST, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`.
- GUI: Signer mode -> "Show QR" -> scan in Amber -> approve. After the scan
(success OR failure), read `~/Tools/keynctr-debug/pairing-trace.log` — the
last lines name exactly where the handshake stopped. Raw frames still
append to `pairing-capture.jsonl` (live pairings only now).
## Outstanding / next steps
1. **Live Amber re-scan still required** — nothing has exercised the
`188b2eb` lenient parser against real Amber traffic yet. The trace log
will show, without any terminal capture, whether the connect arrives,
decrypts, parses, and how far the handshake gets.
2. If trace shows `pre-handshake 'get_public_key' ignored`, relax the
connect-only gate (the log line names it outright).
3. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
4. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — Amber pairing: lenient NIP-46 payload parse + real decrypt-error logging (2026-09-16)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`188b2eb`** ("fix(pairing): never reject a decrypted
NIP-46 payload on shape"). Previous feature HEADs: `aedde8f`, `759b5dd`,
`5aa122d`, `f59c2b1`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred).
- Release binary rebuilt at `188b2eb` (2026-09-16 ~21:06) — the binary the
Sep 15/16 Amber scans ran against was the Sep 12 build; the id-coercion +
payload-dump diagnostics are live NOW.
- Verification (all green at `188b2eb`): `cargo test` **208 unit + 2 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green. Frontend:
`npm test` **116 passed (16 files)**, `npm run typecheck` clean,
`npm run lint` clean, `npm run format:check` clean,
`npm run electron:build` and `npm run build` green.
## What was completed since the last checkpoint
- **Universal-lenient RawRequest parse (`188b2eb`)**: the derived
`Deserialize` (even after `aedde8f`'s params coercion) still rejected real
signer payloads. Replaced with a hand-written coercion deserializer: any
*valid JSON* deserializes — numeric/missing ids become text, object-shaped
`params` become a single param, a double-encoded JSON-string request is
unwrapped. A parse failure is now only possible for non-JSON plaintext,
which the log dumps verbatim. Regression tests for all five shapes.
- **Real decrypt-error logging (`188b2eb`)**: pairing decrypt failures now
log the actual NIP-44 error (invalid HMAC vs invalid padding vs wrong
conversation key) instead of a generic "wrong conversation key", and the
not-a-request log prints the exact decrypted payload.
- **Offline forensics (no commit)**: all four captured pairing frames in
`~/Tools/keynctr-debug/pairing-capture.jsonl` (latest: Sep 16 20:11) are
163-byte NIP-44 v2 payloads = 1 ver + 32 nonce + 98 buffer + 32 MAC.
98 is a VALID final-spec padding bucket (plaintext 65–96 bytes; the
observed 89 fits). So the frames are spec-conformant, decryption
succeeds, and the failure was purely the JSON-shape parse — confirming
the fix targets the right layer. Note `/tmp/keynctr-el*.log` no longer
exists (tmp-cleaner); backend stderr now only reaches the Electron
console — the next failed scan's payload dump needs
`npx electron .` launched from a terminal or with stderr redirected.
## Commits added (newest first)
- `188b2eb` fix(pairing): never reject a decrypted NIP-46 payload on shape
## How to reproduce / exercise
- Dev loop (unchanged): `npx vite --port 5173` in `frontend/` FIRST, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`.
To capture pairing diagnostics: run electron with stderr kept, e.g.
`... npx electron . 2>&1 | tee ~/Tools/keynctr-debug/el.log`.
- E2E: `cargo test --test nip46_e2e` (no network) — green.
- GUI: Signer mode -> "Show QR" -> scan in Amber -> approve. Raw pairing
events keep appending to `~/Tools/keynctr-debug/pairing-capture.jsonl`.
## Outstanding / next steps
1. **Live Amber re-scan required** (cannot be done from an unattended run):
if pairing still fails, the backend log now contains the exact decrypted
89-byte payload and/or the exact NIP-44 error — that text names the last
possible cause.
2. If the payload turns out to be valid JSON but not `method:"connect"`
(e.g. Amber's deferred-approval `get_public_key` first), the log will
show it and the handshake gate can be relaxed accordingly.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute
through `Signing` for external profiles (P2).
4. Step 5 (KDF upgrade), Step 6 (undo preserves ProfileSummary), Step 7
(rename pass incl. `homepage` URL).
---
# Checkpoint — QR pairing, identity adoption, always-allow grants + pairing-relay widening (2026-09-12)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`f59c2b1`** ("fix(pairing): widen pairing relay set so
signer-chosen relays are covered"). Previous feature HEAD: `22d7c01`.
- Working tree: clean for tracked files. Untracked intentionally NOT
committed: `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`,
`deferred/` (stays deferred). `.directory`, `.opencode/`, `.impeccable/`
are now gitignored. Dead stub `src/signer/nip46_external.rs` **deleted**
(was untracked, never declared in `signer/mod.rs`, superseded by
`nip46_client.rs`).
- Verification (all green at `22d7c01`): `cargo test` **201 unit + 2 e2e
passed / 0 failed**, `cargo clippy --all-targets` 0 warnings,
`cargo fmt --check` clean, `cargo build --release` green. Frontend:
`npm test` **116 passed (16 files)**, `npm run typecheck` clean,
`npm run lint` clean, `npm run format:check` clean,
`npm run electron:build` and `npm run build` green.
## What was completed since the last checkpoint (7 feature commits + hygiene)
- **QR pairing (`38499d4`)**: Keynctr is the NIP-46 *client*, Amber scans.
Signer screen mints a `nostrconnect://` pairing token (ephemeral key +
secret), renders it as a QR ("Show QR"), copy-link fallback, cancel.
Backend listens for the signer's connect request, echoes the secret
(anti-spoofing), persists the connection, adopts identity via
`get_public_key`. e2e covers scan -> secret echo -> identity -> sign ->
vault persistence with a fake QR scanner.
- **Electron allowlist (`c5004eb`)**: `nip46_pair_start` added to the
main-process renderer allowlist (was rejected with "not permitted").
- **Lazy signer handle (`dc58f38`)**: all `nip46_*` IPC handlers ensure the
client signer handle exists (fresh backend no longer answers "not
initialized" until the mode is re-saved).
- **Pairing diagnostics (`9cfab4b`)**: pairing start + session failures
logged to stderr (captured by Electron).
- **Real identity adoption (`3d5302f`)**: paired profiles get the signer's
kind-0 display name/picture/nip05 (best-effort, 3s-capped) instead of a
generic pairing label.
- **Instant Connected (`286bbca`)**: session flips to Connected as soon as
identity is verified/persisted; metadata lands in a background task that
never overrides a user-chosen label (fixes "Amber said yes but nothing
changed").
- **Always-allow grants (`81b082f`)**: standing per-(peer pubkey, method)
permission for external signer requests. Approvals gained an "Always
allow" option; grants listed with Revoke on the Signer screen; persist in
the encrypted vault (`src/vault.rs` grant storage).
- **Hygiene (`22d7c01`)**: gitignore editor artifacts, prettier-fix
`SignerScreen.tsx` (format:check had been failing since `81b082f`),
delete dead `nip46_external.rs` stub.
## Commits added (newest first)
- `22d7c01` chore(hygiene): ignore editor artifacts; prettier SignerScreen
- `81b082f` feat(signer): always-allow grants for external signer requests
- `286bbca` fix(signer): connect immediately after identity; fetch kind-0
metadata in background
- `3d5302f` feat(signer): adopt real display name/picture for paired NIP-46
identities
- `9cfab4b` chore(signer): log pairing start and session failures to stderr
- `dc58f38` fix(ipc): lazily initialize the NIP-46 client signer handle
- `c5004eb` fix(electron): allow nip46_pair_start through the renderer method
allowlist
- `38499d4` feat(signer): QR pairing — client-initiated nostrconnect:// flow
for Amber
## How to reproduce / exercise
- Dev loop (unchanged): `npx vite --port 5173` in `frontend/` FIRST, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .`.
- E2E: `cargo test --test nip46_e2e` (no network).
- GUI: Signer mode screen -> "Show QR" -> scan in Amber -> approve ->
identity + display name appear; sign a note; approval dialog offers
"Always allow"; revoke on the Signer screen.
## Outstanding / next steps
1. **On-device Amber verification** of everything above (QR pair + pasted
bunker://, identity/name/pic, sign + publish, always-allow grant, revoke,
re-prompt). Top item — never run against real Amber since `f917e5e`.
2. `publish_profile_metadata` (kind 0) still signs locally — reroute through
`Signing` for external profiles (P2).
3. Step 5 (KDF upgrade m=64MiB/t=3 + vault header versioning, gate
deprecated `RevealSecretKey`), Step 6 (undo preserves `ProfileSummary` ->
secret lost), Step 7 (Keynctr rename pass incl. `homepage` URL).
4. `wip/pairing-relay-widening` branch parked — needs an env seam before it
can merge (breaks e2e isolation as-is).
---
# Checkpoint — NIP-46 e2e test + bunker:// frontend support (2026-09-11)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`85756df`** ("feat(signer): accept bunker:// URIs, async
signer permissions, NIP-46 e2e test"). Previous: `c096705`, `f917e5e`.
- Working tree: clean for tracked files. Untracked junk intentionally NOT
committed: `.opencode/`, `.impeccable/`, `.directory`, `COSMIC_THEME.md`,
`KeynectrAppIconPossibility02.jpeg`, `deferred/`, and the dead stub
`src/signer/nip46_external.rs`.
- Verification (all green this session): `cargo test` **200 + 1 e2e passed / 0
failed**, `cargo clippy --all-targets` 0 warnings, `cargo fmt --check` clean,
`cargo build --release` green. Frontend: `npm test` **116 passed (16 files)**,
`npm run typecheck` clean, `npm run lint` clean, `npm run format:check` clean
(Prettier applied to the 4 previously-warning files), `npm run electron:build`
and `npm run build` green.
## What was completed (this session)
- `tests/nip46_e2e.rs` (new, 450 lines): full in-process NIP-46 client test —
minimal local relay + fake Amber speaking the bunker flow, NIP-44 round-trip,
`connect` handshake, identity asserted to come from `get_public_key` (URI key
must never become identity), `sign_event` result verified against the signer
pubkey, vault persistence asserting `profile.secret_key` stays empty for
remote profiles.
- Frontend bunker:// support landed: `SignerManager.parseExternalSignerURI`
accepts `bunker://` (pubkey = authority before `@`) as well as
`nostrconnect://`; `SignerModeScreen` validates both, placeholder/hint explain
Amber vs Nostr Connect sources.
- Signer trait permission surface made async (`permissions`, `can_*`,
`is_connection_valid` now `async`, `blocking_lock` -> `lock().await`).
- Dev-loop gremlins fixed along the way: zombie vite on :5173 killed, stray
Electron (launched against dead vite, SIGTRAP core dump) cleaned up, app
relaunched and confirmed on screen.
## Commits added (newest first)
- `85756df` feat(signer): accept bunker:// URIs, async signer permissions,
NIP-46 e2e test
## How to reproduce / exercise
- Dev loop: `npm run dev` in `frontend/` (vite on :5173), THEN second terminal
`npm run start:dev` (or `NOSTR_GUI_DEV_URL=http://localhost:5173
KEYNCTR_ENABLE_GPU=1 npx electron .`). Vite must be up FIRST or Electron
crashes on load. Rust edits need `cargo build --release` + backend restart.
- E2E test: `cargo test --test nip46_e2e` (~0.6s, no network).
- GUI: Signer mode screen -> paste Amber `bunker://...` link -> approve in
Amber -> app resolves identity via `get_public_key`.
## Deferred / next steps
1. **Verify Amber end-to-end on device** (pair via Amber, sign a note, publish)
— unchanged, still the top item.
2. Delete dead stub `src/signer/nip46_external.rs`; `deferred/` stays deferred.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute through
`Signing` for external profiles.
4. Step 4 (permissions UI), Step 5 (KDF upgrade), Step 6 (undo history),
Step 7 (rename/hygiene incl. `homepage` URL) — unchanged.
---
# Checkpoint — Vault-load rewrite fix + Amber handshake lands (2026-09-11)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`c096705`** ("fix(vault): stop rewriting the vault on every
load; clippy cleanup"). Previous: `f917e5e` (Amber-compatible handshake).
- Working tree: clean for tracked files (untracked leftovers unchanged — see older
"Still untracked" sections).
- Verification: `cargo test` **200 passed / 0 failed**, `cargo clippy --all-targets`
0 warnings, `cargo fmt --check` clean, `cargo build --release` green.
(Frontend untouched this session; vite dev server still running on :5173.)
## What was completed (this session)
**Carried-forward open question — CLOSED, landed as `c096705`.**
`migrate_vault_signer_modes` used to return `changed = true` unconditionally, so
`App::load` re-encrypted and re-saved the vault on every single start. Now a change
is reported only when `vault.version` actually moves: per-profile `signer_mode`
normalisation was always a no-op (the serde default fills missing fields at parse
time and the current version serialises it explicitly). The idempotency test was
tightened to assert `changed == false` for a current-version vault. Also dropped a
clone-on-Copy in `nip46_client.rs::get_public_key` (clippy warning from `f917e5e`).
**`f917e5e` (committed earlier today, before this session):** Amber-compatible
NIP-46 handshake — `bunker://` URIs accepted, URI authority key no longer treated
as identity (Amber mints a per-connection comms key; real identity learned via
`get_public_key` after the connect ack), 120 s human-approval window with the
session held in Connecting (fail closed), absent `perms=` delegates enforcement to
the signer, `send_rpc` honours its timeout. **On-device Amber round-trip has not
been re-verified since this commit — that is the next task.**
## Commits added (newest first)
- `c096705` fix(vault): stop rewriting the vault on every load; clippy cleanup
- `f917e5e` fix(signer): Amber-compatible handshake — bunker:// URIs, deferred
identity, ack wait (landed earlier today)
## How to reproduce / exercise
- Dev loop (from memory, unchanged): `npx vite --port 5173`, then
`NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron .` in
`frontend/` (backend from `target/release/keynectr serve`). Rust edits need
rebuild + backend restart.
- Exercise vault fix: start the app twice; the vault file's mtime should NOT change
on the second start when nothing was modified.
## Deferred / next steps
1. **Verify Amber end-to-end on device** (pair via Amber, sign a note, publish).
2. Dead stub `src/signer/nip46_external.rs` (untracked, superseded by
`nip46_client.rs`) — delete or fold its docs; `deferred/SignerConnectionPanel.tsx.wip/`
stays deferred.
3. `publish_profile_metadata` (kind 0) still signs locally — reroute through
`Signing` for external profiles.
4. Step 4 (permissions UI), Step 5 (KDF upgrade), Step 6 (undo history),
Step 7 (rename/hygiene incl. `homepage` URL + 5 Prettier files) — unchanged.
---
# Checkpoint — External NIP-46 signing works end-to-end (2026-09-10)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`1af79d8`** ("feat(signer): end-to-end external NIP-46 signing in
publish and upload auth").
- Working tree: clean for tracked files (untracked leftovers unchanged — see older
"Still untracked" sections).
- Verification: `cargo test` **200 passed / 0 failed**, `cargo clippy --all-targets`
clean, `cargo fmt --check` clean, `cargo build --release` green.
## What was completed (this session)
**Step 3 sub-step 2 (IPC reroute) — DONE, landed as `1af79d8`.** Publishing from an
external (NIP-46) profile now signs remotely instead of returning
"External signer not yet supported":
- **`src/signer/nip46_client.rs`** — the outbound/client half of NIP-46:
`send_remote_request` encrypts a request (NIP-44) and publishes it to the signer's
relay; incoming payloads shaped like responses (`result`/`error`) are demultiplexed
to the waiting caller (a response with no registered id is ignored); 30 s
`REQUEST_TIMEOUT`; pending waiters are woken with errors on disconnect/failure so
callers never hang the full timeout. `Signer::sign_event` is real now: permission
check → remote `sign_event` → verify the returned event (a) is signed by the
connected remote identity, (b) matches the exact unsigned event requested, (c) has a
valid signature — then return it. **No local-key fallback anywhere.**
`audit_permission_denied` uses `try_lock` (audit is best-effort; blocking here would
deadlock the very request being denied while the IPC dispatcher holds the App lock).
- **`src/app.rs`** — `App::signing_for(npub)` / `App::signing_active()`: the single
place that maps a profile's `SignerMode` to a `Signing` source.
`Embedded` → `Signing::Local` with the vault-resolved key; `Nip46Client` →
`Signing::External` wrapping the live signer **only when present and connected**,
else `ExternalSignerNotConnected` (fail closed, never a silent local fallback);
`Nip46Bunker` (not wired) also fails closed.
- **`src/publish.rs`** — `publish_with_keys` builds the unsigned event from the
`Signing`'s own pubkey (external identities validate there before any relay work)
and signs via `Signing::sign`; new `publish_signed` entry point for IPC. The CLI's
`publish_active`/`publish_as` keep the local vault path.
- **`src/ipc.rs`** — `PublishNote` and `UploadAuth` route through
`app.signing_active()`; no handler branches on signer mode anymore.
`Nip46Connect`/`Nip46Disconnect` now clone the signer handle and **drop the App
guard before awaiting** `connect()`/`disconnect()` (they re-lock the App
internally — a latent deadlock, fixed).
- **`src/relays.rs`** — keyless relay pool: `open_pool_inner(Option<Keys>, …)` so
external signing can publish/relay without local keys (no relay AUTH).
- **`src/uploads.rs`** — `nip98_authorization(url, method, &Signing)` — NIP-98 upload
auth events sign through the same `Signing` source, so uploads authenticate with
the remote signer for external profiles.
- **`src/profiles.rs`** — `store_remote_profile(vault, npub, label)`: connecting a
NIP-46 signer creates/refreshes a **secretless** `Nip46Client` profile row (empty
`secret_key`, made active) so publish has a selection; refuses to silently convert
an existing local profile into a remote one. `connect()` calls it and adopts the
remote npub as the signer's active profile.
New tests (`src/app.rs`): embedded → `Signing::Local`; external profile with no live
signer → `ExternalSignerNotConnected` (fail closed); no active profile →
`NoActiveProfile`; `store_remote_profile` creates a secretless external profile and
refuses to clobber a local one.
Security properties: remote-signed events are triple-verified (identity, content
match, signature) before publish; external profiles never touch a local key; the
connection secret stays vault-encrypted (Step 3 sub-step 1 unchanged).
## Out of scope this session (deliberate)
- `publish_profile_metadata` (kind 0) still signs locally from the vault — a
synchronous key-based path; rerouting it is a separate follow-up.
- Dead `src/signer/nip46_external.rs` stub cleanup (untracked leftover).
- Step 4 (permissions UI), Step 5 (KDF upgrade), Step 7 (rename/hygiene).
---
# Checkpoint — Undo-delete restores working profiles (2026-09-10)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`d101b8e`** ("fix(undo): restore full profile with secret key on undo-delete").
- Working tree: **clean for tracked files.** Only untracked entries are the pre-existing
hygiene leftovers plus two Rust scratch files (all intentionally untracked — see
"Still untracked").
## What was completed (this session)
**Step 6 (undo history) — the hollow-undo bug is fixed, landed as `d101b8e`.**
Deleting a profile used to keep only a `ProfileSummary` on the undo stack, so
undo re-created the profile with `secret_key = ""` — a dead shell that could never
sign. The undo stack now keeps the full stored record:
- **`src/profiles.rs`** — new `DeletedProfile { summary, stored }` struct;
`delete_profile_record()` returns the real stored secret (plaintext or encrypted
blob, exactly as on disk) plus the safe UI summary; `delete_profile()` stays as
the summary-only wrapper for callers that keep no undo entry.
- **`src/app.rs`** — `App.undo_history` is now `Vec<DeletedProfile>` (secret material
never leaves the backend); `undo_delete()` restores the real `StoredProfile`,
refuses to create a hollow profile (empty secret → entry handed back + error),
and `state_view()` still exposes only `summary` items so the renderer never sees
a secret. New regression test
`undo_delete_restores_working_profile_without_leaking_secret` locks this in.
- **`src/ipc.rs`** — `DeleteProfile` routes through `delete_profile_record` so the
GUI undo entry carries the secret (previously it pushed a summary-only entry,
so GUI undo was still hollow).
- **`src/main.rs`** — CLI `delete-profile`/`undo-delete` use the same record path
(`delete_profile_direct` → `delete_profile_record`, `cli_undo_delete` →
`app.undo_delete()`); also fixes the old "label looked up after removal" bug by
capturing the label before deletion, and drops the now-unused imports.
- Compile fixes included: the inherited work-in-progress did not build (`DeletedProfile`
vs `ProfileSummary` mismatch in `ipc.rs`, partial moves in `undo_delete`); both
resolved, plus `cargo fmt` applied.
Security properties: secret material stays backend-only (`AppStateView.undo_history`
is still `Vec<ProfileSummary>`); undo restores the exact stored blob (no re-derivation,
no logging); empty-secret entries fail closed instead of writing hollow profiles.
## Commits added this session (newest first)
| Hash | Message |
|------|---------|
| `d101b8e` | fix(undo): restore full profile with secret key on undo-delete |
(Parent chain — `1d5940f` display/icons checkpoint, `d580139` icon alpha fix,
`0814a53` Linux display compat, `715c99c`/`510cb65` connection-secrets vault
integration — is unchanged.)
## Verification (run this session, on top of `d101b8e`)
- **Rust**: `cargo test` → **197 passed**, 0 failed (196 pre-existing + 1 new
undo regression test); `cargo clippy --all-targets` → clean (exit 0);
`cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0.
- **Frontend** (in `frontend/`, Rust-only change so no frontend files touched):
`npm test` → **116/116 passed**; `npm run typecheck` → exit 0;
`npm run lint` → exit 0; `npm run electron:build` → exit 0; `npm run build` →
exit 0. `npm run format:check` → warns on the same 5 pre-existing files
(`ExportSecretKeyModal.tsx`, `SignerModeScreen.tsx`, `AppProvider.tsx`,
`ExportSecretKey.test.tsx`, `fakeBackend.ts`) documented in earlier checkpoints —
not introduced here, left untouched.
## How to reproduce / exercise
- Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in
`src/main.rs`.
- GUI: from `frontend/`, `npm run electron:build && electron .` (prod) or `npm run
start:dev` with `NOSTR_GUI_DEV_URL`.
- Exercise undo: Profiles → delete a profile → Undo delete → the restored profile
signs/publishes (previously it came back secret-less). CLI equivalent:
`keynectr delete-profile <npub>` then `keynectr undo-delete`.
## Still untracked (do NOT lose; do NOT commit the hygiene junk)
- **Source JPEG** `KeynectrAppIconPossibility02.jpeg` — intentionally untracked.
- Pre-existing untracked hygiene leftovers: `COSMIC_THEME.md`, `.opencode/`,
`.impeccable/critique/`, `.directory`, `deferred/SignerConnectionPanel.tsx.wip/`.
- Rust scratch files (unreferenced, harmless — neither is wired into the build):
`src/signer/nip46_external.rs` (dead stub, not declared in `src/signer/mod.rs`),
`src/publish.rs.bak` (backup copy). Left alone this session; delete or wire up
in a later pass.
- Build artifacts `release/` and `dist/` are gitignored and not committed.
- `profiles_vault.json*` and `target/` remain correctly untracked and uncommitted.
## Deferred / next steps (unchanged, minus the undo item)
- Step 3 sub-step 2 (IPC reroute) remains the next signer milestone; external
(remote) signing in the publish path still returns "not yet supported".
- External-signer permissions (Step 4), deferred security (Step 5: KDF upgrade,
`--allow-env-secret`, gate deprecated `RevealSecretKey`), hygiene (Step 7:
Keynctr rename incl. `package.json` → `homepage`, legacy Python removal, vault
relocation, Prettier pass over the 5 known files).
- Open question carried forward: `migrate_vault_signer_modes` reports a change on
every load (always `changed = true`), so `App::load` re-saves each start.
---
# Checkpoint — NIP-46 Connection Secrets in the Vault (2026-09-04)
## Where things are
- Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`510cb65`** ("feat(signer): store NIP-46 connection secrets in
the vault, keyed by VaultRef").
- Working tree: **clean for tracked files.** No tracked file is modified or staged.
The only untracked entries are the pre-existing hygiene leftovers and the source
JPEG (all intentionally untracked — see "Still untracked"). Build artifacts
(`release/`, `dist/`) are gitignored.
## What was completed (this session)
**Step 3 sub-step 1 (Vault integration) — landed as `510cb65`.** The NIP-46
nostrconnect `secret` is a credential, so it no longer lives inline on
`Nip46Connection` (which can be serialized and shown to the UI). It is now stored in
the vault's **encrypted `connection_secrets` store**, keyed by the opaque
`VaultRef` (profile npub + remote signer pubkey), encrypted under the vault key, and
resolved **only at the vault boundary while the vault is unlocked** (fail-closed when
locked). This is where the `vault_ref` constraint becomes load-bearing.
- **`src/vault.rs`** — new `ConnectionSecret { ref_: VaultRef, secret }` struct and a
`Vault.connection_secrets: Vec<ConnectionSecret>` store (encrypted under the vault
key when password-protected, plaintext when the vault has no password — exactly
mirroring how profile secrets are handled). Three helpers:
- `store_connection_secret(vault, key, ref_, secret)` — upserts by `VaultRef`;
encrypts when the vault is password-protected, else stores plaintext. A locked
encrypted vault fails closed (`vault_locked`) rather than silently writing a
plaintext secret that would not match once unlocked. Replacing an existing
`VaultRef` never leaves a stale secret behind.
- `resolve_connection_secret(vault, key, ref_)` — decrypts (or returns plaintext)
for a `VaultRef`; `Ok(None)` when no secret is stored, `Err(vault_locked)` when
the vault is encrypted but locked. On success the plaintext is `Zeroizing`
(shredded on scope exit).
- `delete_connection_secret(vault, ref_)` — removes an entry (on disconnect).
- **`src/signer/types.rs`** — the inline `secret: Option<String>` field is **removed**
from `Nip46Connection`. Legacy vaults that still carry an inline `secret`
deserialize fine (serde ignores the absent field) and the dead secret is dropped on
the next save.
- **`src/signer/backend.rs`** — `VaultRef::from_connection(&Nip46Connection)` is the
canonical way a `SigningBackend::Remote` (and the secret store) is keyed by a
connection; `profile_npub` is now `Option<String>` (a connection may be created with
no local profile active).
- **`src/signer/nip46_client.rs`** — on `connect`, the parsed nostrconnect secret is
written to the vault store (via `VaultRef::from_connection`) instead of being kept in
memory (`Nip46Inner.connect_secret` removed). On `disconnect` the stored secret is
dropped. In `run_sign_task`/`send_connect`, the connect secret is now resolved
**on-demand from the vault** (the single source of truth) rather than read from an
in-memory copy — a locked vault refuses the connect instead of sending it without the
secret.
- **`src/publish.rs`** — `publish_with_keys` now takes `&Signing` and signs through the
`Signing` trait (routes to `Keys::sign_event` for `Local`, or the remote signer for
`External`), instead of calling `Keys::sign_event` directly. `publish_active` /
`publish_as` wrap their keys in `Signing::Local`. (External signing in the publish
path is not wired end-to-end yet — it returns a clear "not yet supported" error —
but the routing pattern is in place for the IPC reroute.)
- **`src/signer/permissions.rs`** — test fixtures updated for the removed inline
`secret` field.
Security properties: no secret material is logged; the connect secret is resolved
fresh from the vault at send time (fail-closed on a locked vault); a serialized
`Nip46Connection` or `SigningBackend::Remote` carries zero secret material; the
decrypted plaintext is `Zeroizing`.
The previously-landed foundation (`e6e4922` `SigningBackend`/`SigningError`/`VaultRef`,
`b2755d8` `Signer` trait returning `SigningError`) is unchanged by this commit — it is
what this sub-step wires up.
---
The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692)
was triaged into **three logical, independently-verifiable commits** in a prior session,
and the packaging fix landed in `114b343`. Order is
`a → c → b → (packaging)`: `ExportSecretKey` (b) reads the per-profile `signer_mode`
field and the `external_signer_not_connected` error that the signer-mode commit (c)
introduces, so (c) had to land first. The only uncommitted *tests* were the export
tests and the signer-mode/permission tests, so "(d) tests" is not a separate commit —
each feature's tests travel with it.
1. **Per-profile signer modes + persisted NIP-46 connections (c)** — three coexisting
signing modes (Embedded / Nip46Client / Nip46Bunker), a `signer_mode` field on every
stored profile, a vault `nip46_connections` store (owner-scoped, with parsed
permissions, expiry, revocation), the expanded `Signer` trait (identity validation,
`Signing` enum, permission surface), the NIP-46 permission model, and the redesigned
Signer Mode screen with a nostr-tools-based client.
2. **Fail-closed key export (b)** — `export_secret_key` always re-authenticates, requires
a reason, refuses external-signer profiles, and writes the audit entry *before*
returning the key (fail-closed on audit failure). Frontend `ExportSecretKeyModal`
replaces the old reveal modal.
3. **Hash-chained audit log (a)** — SHA-256 hash-chained append-only log with atomic
append and end-to-end `verify_chain()`; the `sha2` dependency.
4. **Packaging icon restored (this session, `114b343`)** — `frontend/build/icon.png`
was deleted from the working tree, so electron-builder had no Linux icon and every
AppImage/deb it emitted carried the generic Electron placeholder. The icon was
**regenerated from `KeynectrAppIconPossibility02.jpeg`** (not resized): the white
(254) JPEG background was cut to transparent (alpha derived from luma), the art was
flattened to a square canvas with symmetric padding, ink kept pure black (RGB 0,0,0),
and exported as **512x512 RGBA**. The single declared config path
(`linux.icon: build/icon.png`) was kept single — no `build/linux/` fan-out — because
the 512 master satisfies both targets: AppImage downscales to 256 internally (≥256
required) and the deb installs `usr/share/icons/hicolor/512x512/apps/keynectr.png`,
matching the generated `.desktop` `Icon=keynectr`.
### Security properties confirmed
- No secret material is logged or returned except the single, authenticated, audited
export. The export `reason` is logged by design; passwords and nsecs are not.
- Export is **fail-closed**: a failed audit write prevents the key from being returned
(`log.record(...)?` — the earlier `let _ =` that let a key out on audit failure is gone).
- External (Nip46Client) profiles cannot export a secret key — the key is not local.
- Profile identity is resolved server-side (`profiles::find_stored_profile`), not trusted
from the client.
- NIP-46 permissions are deny-by-default and cannot be broadened on reconnect.
- Audit writes are serialized (single mutex across the read-compute-write-update cycle)
and the chain is tamper-evident from a genesis hash.
- Renderer never receives an nsec outside the intentional one-time export.
## Commits added this session (newest first)
| Hash | Message |
|------|---------|
| `510cb65` | feat(signer): store NIP-46 connection secrets in the vault, keyed by VaultRef |
(The parent chain — `b2755d8` Signer trait returns SigningError, `a2307ac` checkpoint,
`e6e4922` SigningBackend+SigningError+VaultRef, `ee88171` checkpoint, `114b343` packaging
icon, `d92371f` checkpoint, `6eff510` export, `2c61830` signer modes, `caed722` audit log
— is unchanged.)
## Verification (run this session)
Full suite per AGENTS.md, run on top of `510cb65`:
- **Rust**: `cargo test` → **196 passed**, 0 failed (no new/removed tests — this
sub-step refactors existing code paths; the existing `signer::backend`, `vault`, and
`nip46_client` tests cover the change); `cargo clippy --all-targets` → clean (exit 0);
`cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0.
- **Frontend**: `npm test` → passed; `npm run typecheck` → exit 0; `npm run lint` →
exit 0; `npm run electron:build` → exit 0; `npm run build` → exit 0.
`npm run format:check` → **exit 1 on the 5 pre-existing files** (`ExportSecretKeyModal.tsx`,
`SignerModeScreen.tsx`, `AppProvider.tsx`, `ExportSecretKey.test.tsx`, `fakeBackend.ts`)
— these are the documented Step-7 Prettier hygiene failures, **not** introduced by this
Rust-only change (none of the 6 modified files are frontend). Left untouched here.
## How to reproduce / exercise
- Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in
`src/main.rs`.
- GUI: from `frontend/`, `npm run electron:build && electron .` (prod) or `npm run
start:dev` with `NOSTR_GUI_DEV_URL`.
- Packaging: from `frontend/`, `npm run dist` (unpacked dir) or `npx electron-builder
--linux AppImage deb` (declared targets) → artifacts in `release/`.
- Exercise export: Profiles → a profile → Export secret key → enter the vault password
and a reason. An external (NIP-46 client) profile shows it cannot export.
- Exercise modes: Signer Mode screen → pick Embedded / NIP-46 client; connect a
`nostrconnect://` URI with a `perms=` parameter to grant scoped permissions.
## Still untracked (do NOT lose; do NOT commit the hygiene junk)
- **Source JPEG** `KeynectrAppIconPossibility02.jpeg` — intentionally **untracked**
(the icon is now derived from it; the JPEG itself is not a build input and stays out
of git, per instruction).
- Pre-existing untracked hygiene leftovers (out of scope, address in the Step-7 hygiene
pass): `COSMIC_THEME.md`, `.opencode/`, `.impeccable/`, `.directory`.
- **`frontend/build/icon.png` is no longer a leftover** — it was regenerated and
committed in `114b343` this session. The prior "deleted icon" item is closed.
- Build artifacts `release/` and `dist/` are gitignored and not committed.
- `profiles_vault.json*` and `target/` remain correctly untracked and uncommitted
(vault is gitignored).
## On the record (not fixed, per instruction)
- **`package.json` → `homepage` still reads `https://github.com/avi/Keynctr`.** This is
the Step-7 rename/hygiene item and was **left untouched** in this session; noted here
so it is on the record rather than silently fixed.
## Deferred / next steps (unchanged, plus new)
- **Step 2 (packaging): DONE.** Icon restored, `npm run dist` + declared targets green,
icon proven inside both artifacts, committed as `114b343`.
- **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922`
(`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests,
full Rust suite green); `b2755d8` made the `Signer` trait return `SigningError`.
**Sub-step 1 (Vault integration) — DONE, landed as `510cb65`**: the encrypted
`connection_secrets` store keyed by `VaultRef`, on-demand secret resolution at the
vault boundary (fail-closed when locked), and the inline `Nip46Connection.secret`
field removed. The `Remote { vault_ref }` variant holds **only** a `VaultRef` — the
NIP-46 connection secret is never inlined. **Remaining sub-step:**
2. **IPC reroute** — drive `src/ipc.rs` handlers through `SigningBackend` (selected
per-profile) so no inline `signer_mode`/handle-presence branching remains; convert
handler results to `AppError` via `From<SigningError>`. Also wire end-to-end
external (remote) signing in the publish path (`publish_with_keys` currently
returns "not yet supported" for `Signing::External`).
Prior state that motivated the API: `src/signer/mod.rs` already had a `Signer` trait
and `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode`
(`Embedded`/`Nip46Bunker`/`Nip46Client`) lives on `StoredProfile` (per-profile) *and*
is duplicated on `App` (app-level), and `App` holds three separate signer handles
(`embedded_signer`, `nip46_signer`, `nip46_bunker_signer`). The IPC dispatcher
(`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites.
- External-signer permissions (Step 4): wire `src/signer/permissions.rs` into the
approval modal so grants (kinds, relays, expiry, rate) are persisted AND enforced in
the UI, not just parsed.
- Deferred security (Step 5): KDF upgrade to m=64 MiB / t=3 with vault-header versioning
+ backward-compatible migration; `--allow-env-secret` flag; remove or gate deprecated
`RevealSecretKey` IPC behind the same fail-closed path.
- Undo history (Step 6): resolve the `ProfileSummary`-loses-the-secret question.
- Hygiene (Step 7): Keynctr rename pass (includes the `homepage` → correct repo fix noted
above), delete legacy Python, migrate root `profiles_vault.json*` into
`~/.local/share/keynectr`, and a Prettier format pass to clear the 5 pre-existing
`format:check` failures.
- Open question carried forward: `migrate_vault_signer_modes` currently reports a change
on every load (always `changed = true`), so `App::load` re-saves the vault each start.
Harmless (idempotent) but wasteful; tighten to only report real changes.