'Always allow…' on a sign_event request now opens an inline kind editor
prefilled with the request's own kind, so the standing grant's scope is
chosen while the user sees the event. Approved with grant_kinds, the
backend records exactly the edited scope (normalised); without them the
fallback stays the request's own kind. Both approval UIs (Signer and
Signer Mode) share the parseKindsInput helper; the bunker status JSON now
carries pending 'details' so the legacy screen can prefill too.
Also fixes a latent bug: AppProvider.signerApprove dropped the 'always'
argument, so the legacy 'Always allow' button never actually recorded a
grant.
An early build auto-published the empty-label default 'My Profile' as
new accounts' kind-0 metadata. That poisoned kind-0 then propagated
back: pairing-time enrichment and the background backfill both copied
it over the name the user typed, so profiles like npub1p437… display
'My Profile' forever and the loop can never resolve a 'real' name.
- network_display_name(): shared resolver that rejects blank AND
placeholder names fetched from the network (unit-tested)
- create_profile / import_profile: never auto-publish a generic
placeholder as kind-0; placeholders stay local until the user names
the profile, and import falls back to the shortened npub
- backfill + pairing enrichment now use the shared resolver
The 'Always-allow permissions' card on the Signer screen now edits an
existing grant's event-kind scope via the signer_grant_update RPC:
Edit toggles an input (comma-separated kinds, client-validated), Save
applies, Revert restores. Grants list is left alone by the 5s poll so
an open editor is never yanked out from under the user.
Step 4 remainder, first half: Vault::update_signer_grant_kinds replaces a
standing grant's kind scope (sorted, deduped; empty = all kinds, a
deliberate broadening matching legacy semantics). New signer_grant_update
IPC + api/AppProvider/fakeBackend plumbing. The Signer-screen editor UI
is the next unit; no UI surface calls the RPC yet.
The 'My contacts' feed and the identity backfill only queried the user's
enabled read relays, but follow lists and metadata live on profile/outbox
relays (purplepag.es aggregates them network-wide). Add PROFILE_RELAYS as
an always-consulted set for profile-scoped queries (notes queries keep
using only enabled relays), de-duplicated with trailing-slash normalising.
Backfill now also upgrades local rows wearing the 'My Profile' create-time
placeholder, not just Nip46Client rows.
The app icon is a dark glyph on transparency — invisible on dark launcher
backgrounds. icon-white.png keeps the exact silhouette/alpha with opaque
pixels recolored white; generated by make_icon_white.py. Used by the
desktop entry (launcher/taskbar); in-app artwork unchanged.
update-keynctr.sh: fetch origin/master, ff-only pull (aborts on dirty
tracked tree), cargo + frontend rebuild, notify-send result. Login-shell
PATH is reconstructed for cargo/node. Idempotent: quiet no-op when
already current. Installed as 'Keynctr Update' desktop entry + right-click
'Update Keynctr' action on the main entry (entries live in
~/.local/share/applications, not tracked).
- package.json homepage: github.com/avi/Keynctr -> git.atitlan.io/avi/Keynctr
- README: title 'Nostr Feed Manager' -> 'Keynctr', resolve all
[YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO> placeholders with the real
Forgejo URL, fix clone dir and packaged-binary name (nost-feed-manager
-> keynectr), data dir default ~/.local/share/keynectr, refresh test
counts (cargo 225+6 e2e, npm 139/19 files) and project layout
(audit/bunker/feed/updates modules, signer/ dir, Feed+SignerMode screens)
- PRODUCT.md: data dir corrected with migration note
New vaults derive their key with Argon2id m=64 MiB, t=3 (OWASP 2024)
instead of the RFC 9106 19 MiB / t=2 defaults. The vault header already
records per-vault KDF parameters, so unlocking an old vault keeps
working unchanged; when it carries exactly the legacy parameters the
unlock transparently re-wraps every secret (profile keys, NIP-46
connection secrets, client keys) under a fresh salt + stronger key and
reports the upgrade so the caller persists it.
Also fixes a real gap surfaced by the new tests: set_password previously
re-encrypted only profile keys — NIP-46 connection secrets and client
keys stayed plaintext in the JSON after 'encrypt my vault'. All three
secret stores now go through one rewrap_secrets() helper shared by
set_password and the KDF upgrade path.
Wrong passwords never touch the header; the upgrade runs only after the
old verifier accepts. Tests: legacy-params upgrade + no-op on current
params, wrong-password leaves header untouched, rewrap covers connection
secrets/client keys for both upgrade and set_password.
- launch-keynctr.sh: builds renderer/electron main if missing, exports
KEYNCTR_ENABLE_GPU=1 (software rendering dies 'GPU process isn't
usable' on this Hyprland box), execs bundled electron with
--class=keynectr for WM_CLASS grouping.
- keynctr.desktop installed at ~/.local/share/applications/ (validated,
icon = public/icon.png, categories Utility, StartupWMClass keynectr).
- requestSingleInstanceLock: second launch focuses the existing window
(app.exit(0) in the doomed instance) instead of a duplicate shell
fighting the vault.
Verified via gtk-launch: 'keynectr | Keynctr' window maps; second
gtk-launch keeps exactly 1 window.
First pass was abstract gradient washes; the reference mock is the
synthwave sun/tower illustration itself behind frosted-glass panels.
Now: real artwork (public/archipelago-bg.jpg from the user's reference)
fills .main cover/fixed under a dark scrim, cards + sidebar are
translucent blurred glass with light hairline borders, coral accent and
is-active nav pill kept. Verified computed background stack in browser;
139 frontend tests + build green.
Pairing-time kind-0 enrichment gives up after ~4 attempts/2 minutes; a
fresh Amber account that has not published metadata yet (or publishes
later) then keeps the generic 'Amber'/'Remote Signer' label forever.
serve() now spawns a slow-cadence backfill: every pass it re-scans for
Nip46Client rows still wearing a GENERIC_PAIRING_LABELS placeholder and
re-fetches kind-0, upgrading label/picture/nip05 when metadata appears.
User renames are never overwritten (placeholder check gates the write);
locked encrypted vaults are skipped; network runs off-lock with a 90s
budget per identity.
Workshop Dark accent is now #007AFF (hover #4da3ff, soft #0f2a44, white labels), including the ambient wash tint. New Settings -> Appearance -> Accent color: a color picker plus hex field that overrides primary/focus for ANY theme, with a 'Use theme default' reset. Persisted in settings.json (accent_color), validated server-side (#rrggbb, empty clears), painted over the theme via inline CSS custom properties with automatic hover/soft mixes and luminance-based label contrast. Verified live: .btn-primary renders rgb(0,122,255)/white under workshop-dark. 220 Rust unit + 6 e2e, 139 frontend tests (4 new), clippy 0, all gates green, release rebuilt.
The user's crash, captured in the app's own console (Tools/keynctr-debug/el.log): 'Could not read from stdin: Resource temporarily unavailable (os error 11)'. tokio::io::stdin flips the Electron pipe non-blocking; under load a read surfaced EAGAIN despite pending data and the serve loop treated it as fatal, exiting code 1 and failing every in-flight request (the 'Rust backend exited unexpectedly' card). Now a dedicated blocking-thread reader feeds an mpsc channel: blocking reads cannot spuriously fail, EOF ends the loop cleanly, transient errors log and continue. Verified live: 200-line request stream -> 200 responses, exit 0; 219 unit + 6 e2e green; clippy 0; release rebuilt 09:54.
New app:selfupdate IPC (main process): npm run build + cargo build --release with the augmented PATH, then kill the backend child, reset the spawn flag so the next request starts the NEW binary, and reloadIgnoringCache every window. The Electron shell keeps running — no manual restart. Settings install now triggers it automatically when restart_required. Honest limits: a change to the Electron main process itself still needs one manual relaunch, and packaged builds report that bundle replacement is the update path.
The logo PNG is dark ink art; invert+brighten it on the dark workshop material (same technique cosmic uses) so it reads as warm paper ink on #12110f. Nav icons are currentColor and already themed. Verified filter present in built bundle.
The Moi dark material is not just tokens: site.css paints a 24px hairline grid (rgba(235,230,220,0.035)) plus pine and clay radial washes over the paper. Applied the identical background stack to .main under both workshop themes (fixed attachment), with per-theme --wk-grid/--wk-wash-mint/--wk-wash-clay/--wk-copper tokens from Moi's light and dark blocks. Sidebar stays a clean opaque surface. Verified against the live Moi site in a browser: both render body #12110f with grid:true, wash:true, pine #7eb89a, copper #d4a574.
Moi DESIGN.md dark column: paper #12110f, surface #1c1a17, stone #26221c, warm ink #ebe6dc, pale pine #7eb89a. Same serif type and radii as the light Workshop theme. Verified rendering live via computed styles (body rgb(18,17,15), cards rgb(28,26,23), pine #7eb89a).
Warm paper (#f3efe6), near-black ink, hairline borders, one pine accent (#215c48), serif display headings, Moi radii (14/9). Tokens verified rendering live via computed styles on the built shell: body bg #f3efe6, card #faf7f0, border #d9d1c3, h2 Iowan/Palatino 500. Settings -> Appearance -> 'Workshop — Cybernetic'.
Applying npm audit fix + npm update + cargo update via the (fixed) updater: clears the high-severity js-yaml advisory (maxTotalMergeKeys CPU use on empty merge sources). Full suites verified green after the bump: 219 Rust unit + 6 e2e, 135 frontend, vite build clean.
The backend spawned by Electron inherits the desktop launcher's
environment, not a login shell: ~/.cargo/bin and the mise/asdf shim
dirs are missing, so 'Check for updates' failed with "'npm' was not
found" even though both tools exist in a terminal.
run() now sets PATH to the inherited value plus the well-known per-user
tool dirs (~/.cargo/bin, ~/.local/bin, mise + asdf shims,
/usr/local/bin), appended after the inherited entries so existing
resolutions keep priority. The NotFound hint now says what to do.
Verified live: update_check under env -i PATH=/usr/bin:/bin returns a
full report (npm advisories + cargo updates) where it previously errored.
Unit tests cover the pure PATH builder incl. missing-env fallback.
Step 4 groundwork, the enforcement half that was invisible or too broad:
- Nip46Status now carries the connection's declared perms= grant list and
its expiry; Signer Mode shows a Permissions panel on a live session
(explicit grant rows, or a plain statement that the signer app approves
each request when no list was declared).
- 'Always allow' grants are kind-scoped: a sign_event grant records the
kind of the request the user actually approved and never covers other
kinds. Legacy kind-less grants keep their all-kinds meaning so existing
vaults keep working. Enforced in both bunker.rs and nip46_client.rs.
- Grants list on the Signer screen renders human labels with kind scope.
Tests: vault kind-scoping unit tests, frontend permission-label unit
tests + two SignerModeScreen tests (declared list, signer-side note).
One live NIP-46 session, many saved ones (Option A):
- start_pairing/connect while a session is live PARKS it instead of
refusing: row, pairing secret, and persisted client key stay intact,
so the parked account is restorable with no fresh scan.
- SelectProfile follows the switch: target has a restorable connection ->
park current + re-dial target's row (expected_identity guard applies);
target is local-key or unpaired -> live session untouched.
- New nip46_cancel_pairing IPC: aborts ONLY an in-flight pairing and
re-dials the parked session, so cancel-after-park is transparent.
The QR cancel paths (Add-profile modal, Signer Mode screen) use it —
plain disconnect would revoke the parked connection.
- e2e: two fake Ambers on one relay; A pairs, B's pairing parks A
(revoked_at none, client key resolvable), switch back re-dials A and
signs; no-op switch; local profile leaves session alone; B restorable.
The wrong-identity refusal test and the auto-name retry loop still wrote
into the live pairing-trace.log on every e2e run: three bogus "restored
signer answered as a different account" npubs and phantom "auto-name
attempt" lines were test traffic, not live Amber misbehaviour, and they
kept derailing the forensics review. fail and the background enrichment
traces now check live_relays like every other trace site.
Verified: running the e2e suite appends zero lines to the trace file.
Live Sep 25: nos.lol — the only relay holding the account's kind-0 —
502'd EVERY connecting client (any UA, nostr-sdk and raw websockets
alike) for minutes at a time, then served the event within 2s. The
single-shot enrichment task gave up once and left the profile on the
seed label permanently. Now: 4 attempts, 20s pause between, 75s budget
each (> fetch_profile_metadata's 10s connect-wait + per-relay fetches),
with a pairing_trace line naming the outcome of every attempt so the
next occurrence is diagnosable from pairing-trace.log alone.
216 unit + 5 e2e green, clippy 0, fmt clean, release rebuilt.
fetch_profile_metadata itself waits up to 10s for the relay pool to
connect before it fetches at all, and the user's relay list includes
relay.nostr.band whose handshake hangs past that. The outer 10s timeout
therefore killed the enrichment task before the first REQ went out —
live Sep 25: nos.lol served the account's kind-0 ('web5osint') within
2s of an anonymous probe, yet the profile row kept the seed label.
30s outer > 10s connect-wait + per-relay fetches.
Sep 25 feedback: the prefilled 'Amber' name had to be cleared letter by
letter before the QR would appear (the input fight), and naming should
be automatic anyway. One click on 'Sign in with a signer app (Amber)'
now goes straight to the QR with the seed label 'Amber'; the existing
adopt_identity background enrichment fetches the account's kind-0 after
the handshake and upgrades the profile row to the account's REAL
display name whenever it still carries our seed (a manual rename in
Profiles always wins and is never overwritten).
frontend: 125 tests green (pairing test asserts the one-click path and
the 'Amber' seed label), typecheck/lint/format/electron:build/build
green. Rust untouched — the auto-naming enrichment already shipped at
a76d8df.
Live Amber sign-in (Sep 25) paired fine, but every restart died with
'the signer did not echo the connection secret': the restore re-sends
connect with the ORIGINAL pairing secret, and an already-approved
signer legitimately answers 'true' without re-echoing — NIP-46 reserves
the echo for proving possession during the INITIAL pairing, and Amber
proved it once. Requiring it on re-dial made session restore fail
100% against real Amber (the e2e missed it because its fake answered a
plain ack with no secret in play).
ConnectUri gains a 'restore' flag (set only by reactivate_saved_sessions).
Fresh handshakes still fail closed on a wrong echo. The restore path's
anti-spoofing is expected_identity in adopt_identity — a peer answering
as any other account is refused, and only the real key holder can
decrypt traffic on the stored conversation key. Log now says
'secret validation: SKIPPED (restored session)' and proceeds.
e2e: run_fake_amber now mirrors Amber's ack shapes (plain ack on first
pairing; 'true' when the client re-presents a secret) and the restore
test seeds a pairing secret so it exercises exactly the live failure —
it fails without the fix and passes with it.
cargo test 216 unit + 5 e2e green; clippy --all-targets 0 warnings;
fmt clean; release rebuilt.