2403 lines
132 KiB
Markdown
2403 lines
132 KiB
Markdown
# 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.
|