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.
This commit is contained in:
Avi 2026-10-02 08:34:04 -05:00
commit a66996ac10
233 changed files with 103810 additions and 0 deletions

View file

@ -0,0 +1,244 @@
---
project: keynctr
status: active
last-updated: 2026-09-25T23:00:00-05:00
hermes-owned: true
local-path: /home/avi/Projects/Keynctr/
tech-stack: [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
- [x] 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
- [x] **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`.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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.
- [x] Steps 1–3 (vault encryption, packaging, SigningBackend abstraction,
vault-integrated connection secrets, IPC reroute, end-to-end external
signing in publish + upload auth)
- [x] Per-profile signer modes + persisted NIP-46 connections (`2c61830`)
- [x] Fail-closed key export (`6eff510`), hash-chained audit log (`caed722`)
- [x] Amber-compatible deferred-identity handshake (`f917e5e`)
- [x] **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.
- [x] **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.
- [x] **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`).
- [x] Vault-load rewrite fix (`c096705`)
- [x] NIP-46 e2e test harness + frontend bunker:// support (`85756df`)
- [x] QR pairing + IPC/Electron wiring + real identity metadata (`38499d4`..`3d5302f`)
- [x] Connect immediately after identity; kind-0 metadata fetched in
background so the UI stops hanging on slow relays (`286bbca`)
- [x] 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
- Local code: `/home/avi/Projects/Keynctr/`
- Checkpoint: `/home/avi/Projects/Keynctr/CHECKPOINT-encryption.md`
- [[00-HERMES/Current State]]
- [[00-HERMES/Session Handoff]]