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.
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