The relay pushes events only to subscriptions that exist at delivery time.
Both connect flows spawned the handshake task before run_demux registered the
kind-24133 author subscription, so a fast signer reply (the live Sep 23 Amber
scan: clean connect + secret echo, then get_public_key) landed in the gap and
was silently dropped — the identity RPC timed out 30s later and the session
died with 'The signer would not reveal its public key' after the connection
was already stored, leaving the UI signed in with no profile.
Both run_sign_task (bunker flow) and run_paired (QR pairing) now subscribe
first via subscribe_to_signer and hand the stream + subscription to run_demux.
futures-util promoted from dev-dependency to runtime. cargo test 209+3e2e
green, clippy 0, fmt clean, release rebuilt.
Keynctr is the NIP-46 client; Amber is the scanner. Amber hands out no
link — it scans one — so the signer screen now mints a pairing token:
- start_pairing(): ephemeral key + secret, nostrconnect:// token via
NostrConnectUri::client_with_secret, status().pairing_uri for the GUI
- run_pairing_task(): listens for the signer's connect request, echoes
the secret (anti-spoofing), persists the connection row + secret,
then adopts identity via get_public_key and hands to the demux loop
- pairing subscription is closed at handoff so the demux loop owns the
conversation (relay could otherwise deliver signer replies under the
stale pairing sub id where nobody routes them)
- IPC: nip46_pair_start; status carries pairing_uri
- SignerModeScreen: 'Show QR' button, QR render (qrcode) of the token,
copy-link fallback, cancel; paste-link flow unchanged
- e2e: fake QR scanner consumes the real pairing token end-to-end
(scan -> secret echo -> identity -> sign -> vault persistence)
- SignerManager/SignerModeScreen parse both nostrconnect:// and bunker://
(Amber presents bunker://; signer pubkey extracted before '@')
- Signer permission surface made async (permissions, can_*, is_connection_valid)
- tests/nip46_e2e.rs: full client handshake against fake Amber over a local
relay — NIP-44 round-trip, get_public_key identity, signed-event verification,
vault persistence asserting no secret material for remote profiles
- prettier formatting of touched frontend files
Introduce src/audit.rs: a SHA-256 hash-chained audit log for
security-sensitive operations (key export, NIP-46 connect/revoke,
signing approvals, vault lock/unlock, permission denials).
- AuditEntry carries timestamp, profile npub, action, reason,
success flag, error, and prev/this hash forming a tamper-evident
chain from a genesis hash.
- record() holds a single mutex across the entire read-compute-write-
update cycle so concurrent writers cannot interleave and silently
overwrite entries; appends are atomic (write-all + fsync + rename).
- verify_chain() re-hashes every entry end to end.
- Log lives in the app data dir with 0600 permissions.
Also adds the sha2 dependency. Verified in isolation on top of HEAD:
cargo test --release -> 124 passed (119 prior + 5 audit).
- Add Signer trait with common interface for both signing modes
- Implement EmbeddedSigner: keys stored in encrypted vault (Argon2id + AES-256-GCM)
- Implement Nip46ClientSigner: connects to remote signer via nostrconnect:// URI
- Support both local and remote NIP-46 signers
- Add signer mode selection UI (SignerModeScreen)
- Add IPC endpoints for signer mode management, embedded signer, and NIP-46 client
- Update frontend types, API, and AppProvider
- All tests pass (119 Rust + 110 frontend)
- Crate/binary: nostr-manager-backend -> keynectr
- Data directory: nost-feed-manager -> keynectr, migrated automatically
on first data_dir() call (existing vaults, settings and backups move)
- Electron extraResources/spawn path, executableName, productName,
window title and CLI usage strings updated to match
- Deliberately unchanged: crypto.rs KDF verifier string, so previously
encrypted vault backups remain decryptable
Verified live: existing vault with two profiles migrated to
~/.local/share/keynectr and loads correctly.
Add a remote-signer (bunker) role so other Nostr apps can delegate
signing to this app's active profile keys via nostrconnect:// links.
Backend: new src/signer.rs implementing the NIP-46 protocol (kind 24133
events encrypted with NIP-44 v2 conversation keys). It parses
nostrconnect:// connect URIs, spawns an async task in the serve process
that reads relay requests, auto-approves once the handshake completes,
signs delegate events, and publishes responses. Exposes status, connect,
and disconnect via IPC and CLI (signer status / signer connect).
Other: ipc.rs serve/handle now share Arc<Mutex<App>>; main.rs adds the
signer CLI commands; Cargo.toml enables nostr nip46 feature.
Frontend: new Signer screen (nav item + sidebar entry with key icon)
to paste a nostrconnect:// link, connect/disconnect, and show the
connected peer and relays; wires signer_connect/_disconnect/_status
through api.ts and AppProvider; adds tests and test mocks.
- Encrypt stored secret keys with AES-256-GCM under an Argon2id-derived key;
the vault stays plaintext until a password is set (Settings -> Storage or
the CLI set-password command)
- Only secret keys are encrypted; labels and npubs stay readable so profiles
can be browsed while the vault is locked
- Backend: crypto module, Vault.crypto metadata, unlock/lock/set/remove
password on App, VaultLocked/WrongPassword errors, secret resolution on the
publish path
- IPC: set_vault_password, unlock_vault, lock_vault, remove_vault_password
- CLI: set-password, remove-password, unlock; create/publish prompt when the
vault is locked (NFM_PASSWORD env or hidden prompt, never argv)
- GUI: unlock banner + modal on locked vaults, protect/change/remove password
in Settings, password field styling
- Tests: Rust (argon2/AES round-trips, vault lifecycle) and Vitest (unlock
flow, set/change/remove password), all green
- Rust library (nostr-manager-backend) with CLI and JSON-lines IPC serve mode:
profiles, publishing with per-relay reports, relays, settings, vault storage
and legacy-vault migration
- Electron + React + TypeScript desktop GUI using the same backend over stdio IPC
- Vitest suite with a fake backend speaking the real protocol
- electron-builder linux packaging; README with build and usage instructions