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)
Introduce three coexisting signing modes:
- Embedded (INTERNAL): vault-held nsec, decrypted in Rust, signs locally.
- Nip46Client (EXTERNAL): Keynctr is the NIP-46 CLIENT; the key never
touches this machine.
- Nip46Bunker: legacy inverted mode (Keynctr as signer serving others).
Data model:
- StoredProfile gains signer_mode (serde-defaults to Embedded for legacy
profiles); SignerMode moves from app.rs to vault.rs to break a circular
dependency; app.rs re-exports it.
- Vault gains nip46_connections (profile-owned) and bumps VAULT_VERSION to
3; migrate_vault_signer_modes() normalises on load (idempotent).
- Nip46Connection gains profile_npub ownership, parsed permissions,
expires_at, and revoked_at.
Signer abstraction (src/signer):
- Signer trait gains pubkey_for() identity validation, a Signing enum
(Local vs External) that re-verifies the returned event, and a permission
surface (permissions/can_*/is_connection_valid) with safe defaults.
- permissions.rs: NIP-46 per-connection permission model (parse, validate,
deny-by-default, no-broadening checks) with 52 unit tests.
- Nip46ClientSigner parses perms from nostrconnect:// URIs, enforces
permissions on every gated request, persists/revokes connections in the
vault, and audits permission denials via the app's audit log.
- App gains audit_log and a nip46_bunker_signer handle; default mode is
Nip46Client (most secure).
Frontend: SignerModeScreen redesigned for the three modes with a
nostr-tools-based SignerManager client, new IPC allowlist entries, and
signer-mode styling.
Verified: cargo test --release 186 passed; clippy/fmt clean; frontend tsc
clean, vitest 110 passed.
- 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.
- 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