Orion/1. Projects/Products/Keynctr/sources/keynctr.md
Avi a66996ac10 Orion vault — clean initial history
Knowledge vault (Orion/PARA) migrated from the pre-Orion 484vault on
2026-10-01. Deliberately orphaned: prior history contained a plaintext
password and stays local-only on branch archive/pre-boilerplate-history.
Secrets and live Hermes state are gitignored.
2026-10-02 08:34:48 -05:00

18 KiB
Raw Permalink Blame History

project status last-updated hermes-owned local-path tech-stack
keynctr active 2026-09-25T23:00:00-05:00 true /home/avi/Projects/Keynctr/
Rust
Electron
React
TypeScript

Keynctr

Overview

A desktop GUI for managing Nostr identities and publishing notes, with a focus on key security through vault encryption and a modular signer abstraction layer. Rust backend (JSON-lines IPC on stdio) + Electron/React frontend.

Ultimate goal: Keynctr itself never holds or transmits secret key material. In embedded mode keys live only in the encrypted local vault; in external-signer mode (the most secure of the three ways, and the current focus per Avi's directive) the keys live on the signer device — Amber on the phone — and Keynctr only ever sends signing requests, approving each use at the signer. The Rust core does all signing/relay work; the Electron UI never touches secret material. Usable as GUI and same-core CLI, and doubling as a NIP-46 bunker for other apps.

Architecture

  • Frontend: Electron + React + TypeScript (frontend/); Prettier-formatted.
  • Backend: Rust crate keynectr (src/); cargo build --release binary.
  • Storage: a single encrypted vault file (profiles_vault.json) holding profiles, signer modes, NIP-46 connections, and encrypted connection secrets.
  • Key files:
    • src/main.rs: CLI entry point / JSON-lines IPC serve.
    • src/ipc.rs: dispatcher.
    • src/signer/: Signer trait (permission checks now async), Signing enum, EmbeddedSigner, Nip46ClientSigner (the active path), permissions.rs, backend.rs (SigningBackend, VaultRef, SigningError).
    • src/bunker.rs: legacy server-mode bunker (bunker:// host role).
    • src/vault.rs: vault load/save, migrations, encrypted connection_secrets.
    • tests/nip46_e2e.rs: in-process NIP-46 e2e test (mini relay + fake Amber).
    • CHECKPOINT-encryption.md (repo root): the standing, current checkpoint — always read this first for "where things are."

Current Status

External signer (the most secure mode) works end-to-end, proven by test. Headline commits since the vault was last updated:

  • f917e5e Amber-compatible handshake: bunker:// URIs, deferred identity (URI key is a per-connection comms key, never identity; real identity learned via get_public_key after approval), 120s approval window, fail-closed while Connecting.
  • c096705 vault-load rewrite fix (migration no longer re-saves every start).
  • 85756df bunker:// accepted frontend-side + async permission surface + tests/nip46_e2e.rs — full e2e: local relay, fake Amber with human-approval delay, identity assertions, sign_event verification, vault persistence (remote profiles store no secret material).
  • 38499d4→3d5302f QR pairing (client-initiated nostrconnect:// flow), IPC lazy-init fix, Electron allowlist fix, and real kind-0 display name/picture adoption for paired identities (3s-capped, falls back to label).

Verification at last check (2026-09-12): cargo test 200 + e2e green, clippy 0 warnings, fmt clean, release build green; frontend 116 tests + typecheck + lint + build green.

2026-09-18 — pairing trace milestone

  • 21c522b Durable pairing trace: every pairing decision point now appends a timestamped line to ~/Tools/keynctr-debug/pairing-trace.log (started, inbound 24133, decrypt-fail with real NIP-44 error, exact unparsable payload, pre-handshake method, connect answered, identity adopted, session failed). Backend stderr only reached the Electron console and /tmp logs got cleaned, so failed live handshakes previously left no trace.
  • Found forensics contamination: pairing-capture.jsonl lines from Sep 18 were the e2e harness's loopback fake-scanner events, not Amber — the capture path was hardcoded. Test events pruned (backup kept) and loopback pairings now skip the capture. Net: no real Amber scan has run against the 188b2eb lenient parser yet; the next re-scan's trace.log tail will name exactly where the handshake stops, no terminal capture needed.
  • Verification at 21c522b: cargo test 208 + 2 e2e green, clippy 0 warnings, fmt clean, release rebuilt; frontend all green.

2026-09-16 — pairing debug milestone

  • 188b2eb RawRequest deserializer rewritten as universal coercion: any valid JSON now parses (numeric/missing ids → text, object-shaped params, double-encoded request strings). The strict derive was still dropping Amber's connect request even after aedde8f. Decrypt failures now log the real NIP-44 error (HMAC vs padding vs wrong key) and the exact decrypted payload instead of a generic message.
  • Forensics: all 4 captured pairing frames (~/Tools/keynctr-debug/ pairing-capture.jsonl, latest Sep 16 20:11) are 163-byte spec-valid NIP-44 v2 payloads (plaintext 65–96 bytes; the observed 89 fits) — so Amber's frames decrypt fine and the failure was JSON-shape parsing.
  • IMPORTANT: /tmp/keynctr-el*.log is gone (tmp-cleaner). To see pairing diagnostics launch from a terminal: NOSTR_GUI_DEV_URL=http://localhost:5173 KEYNCTR_ENABLE_GPU=1 npx electron . 2>&1 | tee ~/Tools/keynctr-debug/el.log then re-scan the QR in Amber.
  • Verification at 188b2eb: cargo test 208 + 2 e2e green, clippy 0 warnings, fmt clean, release rebuilt; frontend all green.

2026-09-27 — multi-account switching + permissions UI + updater fix

  • Multi-account switching shipped (c89b31a, Option A): pairing/connecting a second signer PARKS the live session (never revoked; row+secret+client key intact); SelectProfile re-dials the target profile's saved session (local-key profiles leave the session alone); new nip46_cancel_pairing IPC restores the parked session on QR cancel — both Add-profile and Signer Mode cancels use it. Verified: cargo test 216 + 6 e2e green (new two-fake-Amber switch test).
  • Step 4 first slice (adbc7c2): Nip46Status carries declared perms= list + expiry; Signer Mode shows a Permissions panel (grant rows, or a signer-side-enforcement note). Always-allow grants are now kind-scoped: a sign_event grant records the approved request's kind; legacy kind-less grants keep all-kinds meaning (serde(default), never bricked existing vault rows); enforced in bunker.rs + nip46_client.rs via has_signer_grant(peer, method, event_kind).
  • Updater fix (fa59b63): updates.rs run() augments the inherited PATH with ~/.cargo/bin, ~/.local/bin, mise/asdf shims, /usr/local/bin — Check-for-updates was dead under the desktop-launcher env; verified live under env -i PATH=/usr/bin:/bin.
  • Status at end of night: tree clean @ 118f5d3, 7 ahead of origin (push needs per-use token); cargo test 219 + 6 e2e green, clippy 0, frontend 135 tests green, release rebuilt 22:43.
  • Open: live eyeball of Permissions panel (relaunch — running serve predates commits), approve kind-1 "Always allow" then send kind-3 and confirm it prompts; two-account live pass with real Amber; Step 5 KDF; Step 6 undo; Step 7 rename/hygiene.

2026-09-28 — Moi "workshop" themes + no-restart updater

  • Workshop themes (add956a, c18f59a, de804c8): user's "the theme I asked you to add" = the Moi project's Cybernetic Workshop look, NOT Cosmic Stardust. Pitfall: Moi's look isn't just palette tokens — site.css paints a 24px hairline graph-paper grid + mint/clay radial washes over the paper; matching requires the background stack, not just colors. workshop-dark accent moved to #007AFF per request; white logo on dark themes (invert filter).
  • No-restart updater (dcc701f): app:selfupdate IPC runs npm build + cargo build --release with augmented PATH, kills the backend child; the next request picks up the new binary — no app restart needed.
  • Auto-naming fix: pairing a new Amber connection showed generic "Amber"; persistent kind-0 identity backfill (5b60ae3) + stdin EAGAIN crash fix (9770f46). Suite at checkpoint f905cbc: 221 unit + 6 e2e, clippy 0, frontend 139 green.

Active Tasks

  • On-device Amber round-trip: pair from the real Amber app on the phone, sign a note, publish. The e2e test proves the protocol; this proves Amber.
  • Reroute kind-0 profile metadata publish through the external signer path.
  • Step 4: external-signer permissions UI (grants persisted AND enforced).

2026-09-26 — live publish CONFIRMED + forensics log cleaned

  • End-to-end Amber signing verified on-chain: kind:1 notes from npub1qn0w4a… (ids 5191172d01f9…, fe9a256f597f…) confirmed accepted on primal/damus/snort/nos.lol at exactly the sign_event timestamps in el.log (Sep 25 ~19:50). Every named milestone of the pairing project is now live-verified except one optional scanless-restart eyeball.
  • False alarms retired: the scary recurring "restored signer answered as a different account" and stray "auto-name attempt" lines in pairing-trace.log were e2e TEST traffic (wrong-identity refusal test + loopback enrichment loop), not live Amber failures. Fixed by gating fail() + auto-name traces on live_relays() like every other trace site (332ab64); measured proof: an e2e run now leaves the trace file byte-identical.
  • Live vault pruned again: legacy keyless fac852dc… connection row removed (backup profiles_vault.json.backup-cron-20260926) so startup restore targets only the restorable 4148a9a1… pairing. Serve smoke test on the real vault fires the restore with the persisted client key, no errors.
  • 216 unit + 5 e2e green; clippy 0; fmt clean; release rebuilt at 332ab64. Checkpoint 1b4655c.

2026-09-24/25 — session restore + live sign-in + auto-naming milestones

  • Session restore shipped (0982dad, checkpoint aa3c514): NIP-46 client key persisted in the vault; re-dial at startup/unlock — no re-scan after restart. expected_identity cross-account guard refuses a wrong-identity connection. e2e covers restart + wrong-identity refusal (216 unit + 5 e2e green, clippy 0). Pre-commit bug fixed: client-key re-keying was nested in the pairing-secret branch.
  • 11 profileless nip46_connections rows pruned from the live vault (backup profiles_vault.json.backup-prune-20260924). Legacy connection rows predate client-key persistence — each needs one last fresh scan.
  • LIVE Amber sign-in confirmed (Sep 25 PM) through the new Add-profile flow; active remote profile npub1qn0w4a…. Session restore live-verified against real Amber at 01ce5de (fix: restored sessions skip the connect secret re-echo — already-approved peers don't re-send it).
  • Pairing label step removed — one click → QR (seed label 'Amber'). Auto-name enrichment hardened to 4 retries × 20s; confirmed end-to-end on the live account: seed label shown at pairing, retry loop fetched kind-0 from nos.lol, vault row upgraded to 'web5osint' + picture. UI fc2fe93, backend bc736ff, checkpoint 704addc.
  • Repo renamed on Forgejo 2026-09-25: avi/Nostr_Keynctr → avi/Keynctr (see Forgejo git.atitlan.io).
  • Open hardening item: account kind-0 still lives ONLY on nos.lol — publishing it to primal/damus (one Amber approval) remains open.

Next Steps

  • Publish account kind-0 to primal/damus (currently only on nos.lol).
  • Remote picture/nip05 edits, KDF upgrade (Step 5), rename pass (Step 7).
  • Consider swapping wss://relay.nostr.band out of the user's enabled relays (settings.json) too — it hangs handshakes today and is pay-to-read.
  • Adopt Polaris Vault Workflow boilerplate conventions for the repo (directive 2026-09-25).

Completed

  • LIVE PAIRING CONFIRMED + sign_event timeout fix (f53bc56, Sep 23): two full live pairings against real Amber recorded in the trace log (15:17 + 15:37 CDT): inbound 24133 -> secret echo -> identity adopted: npub1f3tura… — CONNECTED. The "Dev" profile row now exists in profiles_vault.json (nip46_client mode) — the original Sep problem (no profile row, no persisted connection) is CLOSED. Remaining failure diagnosed from el.log: valid Amber signatures arriving ~61s after publication were discarded ("stale/duplicate response: no waiter") because sign_event used the 30s ordinary-RPC leash; every sign needs a human tap in Amber. sign_event now has a 120s SIGN_TIMEOUT (handshake leash). 213 unit + 4 e2e green, clippy 0, fmt clean, release rebuilt at f53bc56.
  • Feed author resolution + kind-0 via signer (a76d8df, 6f4dbb2, Sep 23): feed shows names/pictures from batched kind-0 lookups; "Publish name" routes through Amber.
  • First live pairing attempt diagnosed + relay-set fix (c789cb4, Sep 22): trace log recorded the first real session (started 18:01:29 CDT, timed out 18:06:39, zero inbound 24133). Read-only relay sweep: the ephemeral key had NO 24133 anywhere. Anonymous write+readback canary (canary-24133.py, throwaway keys): purplepag.es blocks kind 24133, nostr.band hangs handshake; damus/primal/nos.lol accept+store, snort accepts for live push. New pairing set: damus.io, primal.net, nos.lol, snort.social, with a regression test pinning the blockers out. Also removed inspect.py from the debug dir (shadowed stdlib inspect, leaked stale output into terminals). 209 unit + 3 e2e green, clippy 0, fmt clean, release rebuilt.
  • Pairing forensics fully de-noised (f6bf6a9, Sep 21): the identity adopted — CONNECTED trace line had no loopback gate, so every cargo test run appended fake CONNECTED entries to pairing-trace.log (Sep 18–21 evening entries were all test traffic, incl. this cron's own runs). Gated on live_relays(); verified by re-running the e2e suite and confirming the trace file stays untouched. Added a paired: connection stored handover trace line so a stall between the connect echo and get_public_key names itself. Full read-through audit of the QR pairing flow found no further defects; nothing left to fix without live Amber data. 208+3 tests green, clippy 0, fmt clean, release rebuilt.
  • e2e vault-isolation bug found + fixed (d52fa58+deeb4f9, Sep 20): the e2e tests seeded their isolation vault one directory too shallow ($XDG_DATA_HOME/ instead of $XDG_DATA_HOME/keynectr/), so every e2e run silently migrated the legacy repo vault — the user's real profile with a plaintext secret key — into the test process. Forensic proof: two legacy-vault backups timestamped Sep 19 20:43:54 + 20:44:30, exactly the test runs around the root-cause commit. Also means the Sep 19 identity adopted trace lines were e2e traffic, NOT a live Amber scan — no live scan has happened against the fixed build yet (zero pairing started trace lines). Fix seeds the vault at the correct path and asserts emptiness after every e2e load so this can never pass silently again. Verified: repo legacy vault byte-identical, backup count unchanged, 208+3 tests green.
  • Amber pairing root cause found + fixed (edd4e56, Sep 19): the pairing loop only accepted an inbound {"method":"connect"} request; NIP-46 says a nostrconnect:// signer sends a connect response ({"id","result":"<secret>"}, secret echo IS the handshake). Amber's approval was silently dropped as pre-handshake '' ignored — hence "Amber says connected, Keynctr shows nothing". Now the echo is verified directly, ack accepted, signer errors fail fast; locked by a new e2e test (nip46_qr_pairing_connect_response_shape, proven red-on-old/green-on-new). Awaiting live re-scan to confirm.
  • Steps 1–3 (vault encryption, packaging, SigningBackend abstraction, vault-integrated connection secrets, IPC reroute, end-to-end external signing in publish + upload auth)
  • Per-profile signer modes + persisted NIP-46 connections (2c61830)
  • Fail-closed key export (6eff510), hash-chained audit log (caed722)
  • Amber-compatible deferred-identity handshake (f917e5e)
  • Oct 1: Step 4 shipped — approval-time kind scope (d09c4ec, checkpoint 38612a4): "Always allow…" on a sign_event opens an inline kind editor prefilled with the request's kind; grants store the edited scope; legacy vault rows keep old meaning. Latent fix: legacy "Always allow" never recorded a grant (AppProvider dropped always). Suite: 227 unit + 6 e2e, clippy 0, frontend 148 green.
  • Oct 1 evening cron hygiene (41d4a60, checkpoint 2b91aea, pushed): lockfile refresh from the in-app update committed; pairing problem re-checked and still closed (3 nip46_connections, latest Sep 28 10:23); debug dirs cleaned up — no parse-failure evidence since closure.
  • Sep 28: pairing problem closed. Vault now holds 3 persisted nip46_connections with matching signer_mode: nip46_client rows (latest Sep 28 10:23, active profile) — Amber connect events parse and persist; original failure no longer reproduces. Supporting fixes: stdin EAGAIN crash (9770f46), persistent kind-0 identity backfill for generic "Amber" labels (5b60ae3). Suite: 221 unit + 6 e2e, clippy 0, frontend 139 green (checkpoint f905cbc).
  • Vault-load rewrite fix (c096705)
  • NIP-46 e2e test harness + frontend bunker:// support (85756df)
  • QR pairing + IPC/Electron wiring + real identity metadata (38499d4..3d5302f)
  • Connect immediately after identity; kind-0 metadata fetched in background so the UI stops hanging on slow relays (286bbca)
  • Always-allow grants: approvals gained an "Always allow" option; standing permission stored per (app pubkey, method) in the encrypted vault, listed with Revoke on the Signer screen (93892fa)

Known Issues

  • package.json → homepage still reads https://github.com/avi/Keynctr (Step-7 hygiene item).
  • Legacy Python files + root profiles_vault.json* + dead stub src/signer/nip46_external.rs are hygiene leftovers (Step-7 pass).
  • Fixed since last update: migrate_vault_signer_modes no longer reports a change on every load (c096705).

References