Commit graph

296 commits

Author SHA1 Message Date
Avi
2b91aeae92 docs(checkpoint): cron hygiene run — lockfile refresh @ 41d4a60, pairing problem still closed 2026-10-01 20:08:42 -05:00
Avi
41d4a60336 chore(deps): lockfile refresh from update_apply (yoke-derive 0.8.4, npm tree refresh) 2026-10-01 20:07:35 -05:00
Avi
38612a4a93 docs(checkpoint): Step 4 approval-time kind scope @ d09c4ec 2026-10-01 13:32:21 -05:00
Avi
d09c4ec1f0 feat(signer): interactive kind scope in the approval prompt (Step 4 finish)
'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.
2026-10-01 13:31:36 -05:00
Avi
8ffeb4296a docs(checkpoint): 8070dfc..d1622a0 pushed to origin/master, verified by ls-remote 2026-10-01 11:58:46 -05:00
Avi
d1622a0bf9 docs(checkpoint): placeholder kind-0 fix + grant editing UI @ 83f5940 2026-10-01 10:56:47 -05:00
Avi
83f594074b fix(profiles): never treat placeholder kind-0 as a real display name
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
2026-10-01 10:48:22 -05:00
Avi
e8d2abbd1b feat(signer): inline kind-scope editing for always-allow grants
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.
2026-10-01 10:36:30 -05:00
Avi
8070dfc0a2 docs(checkpoint): grant kind-editing backend @ d7cf2a5; session stopped here 2026-09-30 15:05:08 -05:00
Avi
d7cf2a5fda feat(signer): grant kind-editing backend (vault + IPC + api plumbing)
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.
2026-09-30 15:03:05 -05:00
Avi
ec8b515d05 docs(checkpoint): profile-relay queries @ aef47dd 2026-09-30 14:19:54 -05:00
Avi
aef47dd1ef fix(feed): query profile relays for kind-3 follow lists and kind-0 metadata
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.
2026-09-30 14:18:33 -05:00
Avi
eea6f0e6b1 feat(assets): white monochrome launcher icon
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.
2026-09-30 12:18:07 -05:00
Avi
abc27819cb feat(launcher): one-click update script for the desktop entry
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).
2026-09-30 12:12:26 -05:00
Avi
33ab4fe113 docs(checkpoint): Step 7 done, Forgejo push restored @ 7891ccc 2026-09-30 11:58:56 -05:00
Avi
7891ccce1a docs: Step 7 rename/hygiene pass
- 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
2026-09-30 11:57:49 -05:00
Avi
c553dd5e17 feat(vault): KDF upgrade to 64 MiB / t=3 with transparent migration on unlock
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.
2026-09-30 10:50:48 -05:00
Avi
5e91914ad6 docs(checkpoint): archipelago v2 + desktop launcher 2026-09-30 09:35:48 -05:00
Avi
fac4ba3cde feat(desktop): launch from the applications list (folio-style)
- 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.
2026-09-30 09:35:25 -05:00
Avi
d792a72dc2 feat(theme): Archipelago uses the reference artwork as the canvas
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.
2026-09-30 09:35:10 -05:00
Avi
003f98a58e docs(checkpoint): Archipelago theme 4d0df31 2026-09-30 09:12:35 -05:00
Avi
4d0df31726 feat(theme): Archipelago — synthwave sunset sea from reference art
Teal-navy dusk, coral scanline sun (subtle repeating-linear horizon
stripes + low-center sun glow + pink cloud banks, fixed attachment),
glassmorphic cards/sidebar (blur, translucent surfaces), coral primary
accent, white logo filter on the dark canvas. Parsed as 'archipelago'
in settings; Settings -> Appearance option 'Archipelago - Sunset Sea'.
Verified tokens + all 6 rules compute in the built bundle; 221+6 Rust,
139 frontend tests, clippy 0, electron:build green.
2026-09-30 09:11:55 -05:00
Avi
f905cbcdad docs(checkpoint): Sep 28 evening — pairing resolved (live vault evidence), backfill + stdin fix verified green 2026-09-28 20:13:07 -05:00
Avi
5b60ae3736 feat(profiles): persistent identity backfill for generic pairing labels
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.
2026-09-28 12:03:35 -05:00
Avi
a6182e3cb2 feat(theme): iOS blue accent for Workshop Dark + custom accent color setting
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.
2026-09-28 10:17:03 -05:00
Avi
9770f46d4a fix(ipc): read stdin on a blocking thread — EAGAIN no longer kills the backend
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.
2026-09-28 09:56:22 -05:00
Avi
6bff188714 docs(checkpoint): white logo + no-restart self-update commits 2026-09-28 09:27:35 -05:00
Avi
dcc701f88b feat(updater): apply updates without restarting the app
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.
2026-09-28 09:26:48 -05:00
Avi
4cc04812fe feat(theme): white logo mark on workshop-dark
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.
2026-09-28 09:17:30 -05:00
Avi
ef7b79bfe4 docs(checkpoint): Workshop atmosphere commit de804c8 2026-09-28 09:10:18 -05:00
Avi
de804c8fc5 feat(theme): Workshop atmosphere — Moi's graph-paper grid + mint/clay washes
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.
2026-09-28 09:10:00 -05:00
Avi
8f1c5e0e18 docs(checkpoint): Workshop Dark theme commit c18f59a 2026-09-28 09:02:31 -05:00
Avi
c18f59ad6c feat(theme): Workshop — Dark, the Moi dark material
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).
2026-09-28 09:02:17 -05:00
Avi
17fd47c081 docs(checkpoint): Workshop theme + verified updater apply at add956a (2026-09-28) 2026-09-28 08:54:58 -05:00
Avi
add956abdd feat(theme): add Workshop theme — Cybernetic Workshop identity from Moi DESIGN.md
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'.
2026-09-28 08:54:13 -05:00
Avi
15fa331f46 chore(deps): apply dependency updates from the in-app updater run
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.
2026-09-28 08:30:48 -05:00
Avi
118f5d37bd docs(checkpoint): permissions UI + updater PATH fix at fa59b63 (2026-09-27) 2026-09-27 22:44:58 -05:00
Avi
fa59b63a17 fix(updates): augment spawned PATH so npm/cargo resolve from Electron
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.
2026-09-27 22:43:42 -05:00
Avi
adbc7c2d75 feat(signer): permissions UI — declared grants surfaced, always-allow kind-scoped
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).
2026-09-27 22:37:54 -05:00
Avi
98593cb170 docs(checkpoint): multi-account signer switching (park/switch/cancel) at c89b31a (2026-09-27) 2026-09-27 21:02:21 -05:00
Avi
c89b31aaf1 feat(nip46): pair a second signer account — park the live session, switch re-dials it
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.
2026-09-27 21:01:23 -05:00
Avi
1b4655c07b docs(checkpoint): forensics log cleaned + live publish confirmed at 332ab64 (2026-09-26) 2026-09-26 20:37:21 -05:00
Avi
332ab647c9 fix(nip46): gate remaining trace writes to live relay sessions
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.
2026-09-26 20:34:08 -05:00
Avi
704addc773 docs: checkpoint — auto-naming hardened (retry 4x), nos.lol 502 root-cause 2026-09-25 17:19:58 -05:00
Avi
bc736ff339 fix(nip46): retry the auto-name kind-0 fetch up to 4x, log each attempt
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.
2026-09-25 17:10:39 -05:00
Avi
b5c61deec6 fix(nip46): give the auto-naming kind-0 fetch a 30s budget, not 10s
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.
2026-09-25 16:39:18 -05:00
Avi
fc2fe93161 feat(ui): drop the pairing label step — profile names come from the account
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.
2026-09-25 16:30:18 -05:00
Avi
e8d11701a3 docs(checkpoint): restore echo fix live-verified at 01ce5de (2026-09-25) 2026-09-25 16:21:06 -05:00
Avi
01ce5de417 fix(nip46): restored sessions no longer demand a connect secret re-echo
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.
2026-09-25 16:19:19 -05:00
Avi
929d791240 docs(checkpoint): label step + vault prune at a809a67; live Amber sign-in confirmed (2026-09-25) 2026-09-25 16:02:27 -05:00