checkpoint: document embedded signer and NIP-46 client modes

This commit is contained in:
Avi 2026-09-01 20:18:38 -05:00
commit 4038e2d32a

View file

@ -1,185 +1,100 @@
# Checkpoint — HomeScreen publication filtering (2026-09-01) # Checkpoint — Embedded Signer & NIP-46 Client Modes (2026-09-01)
## Where things are ## Where things are
- Project: `/home/avi/Projects/Keynctr` - Project: `/home/avi/Projects/Keynctr`
- Git repo: `master` @ `ea56806` ("fix: show only fully published events in Most Recent Publication box"). - Git repo: `master` @ `b484bde` ("feat: add embedded signer and NIP-46 client signer modes").
- Working tree: clean except the usual untracked items (`.directory`, `.opencode/`, - Working tree: clean except the usual untracked items (`.directory`, `.opencode/`,
`.impeccable/`, `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`). `.impeccable/`, `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`).
## What was completed ## What was completed
1. **Clippy is now warning-free.** Removed the two pre-existing warnings: the 1. **Added unified Signer architecture** with a common `Signer` trait in `src/signer/mod.rs`
no-op `drop(stored)` of a `&mut` reference in `src/profiles.rs` and the unused that both embedded and NIP-46 client implementations share. The trait provides:
`restored` binding around `app.undo_delete()` in the `UndoDelete` handler in - `get_public_key()`, `sign_event()`, `get_signer_type()`, `is_available()`
`src/ipc.rs`. No behaviour change — both were dead code. - `request_approval()`, `disconnect()`, `revoke()`, `status_string()`, `detailed_status()`
2. **Profile import is now usable from the GUI.** The `profiles::import_profile`
function (already in the Rust core from the account-import work) is now exposed 2. **Embedded Signer mode** (`src/signer/embedded.rs`):
through the JSON-lines IPC protocol: new `ImportProfile` request and handler in - Keys stored locally in the encrypted vault (Argon2id + AES-256-GCM)
`src/ipc.rs`, `import_profile` added to the Electron method allowlist, - Zeroize memory protection for secret keys
`api.importProfile` + `AppProvider.importProfile` in the frontend, and - Per-request user approval with 5-minute timeout and 20-request queue cap
"Add existing account" buttons on the Profiles screen (empty and populated - Sensitive operations (kind 0, 3, 5, 6, 10000-10002, 30000-30015) flagged for extra confirmation
states). The existing `ImportProfileModal` (enter only the private key; the name - Active profile binding with automatic updates on profile create/import/select
and kind-0 metadata are derived from the network) is now wired to it.
3. **Active signing identity card on Home.** The Home screen shows a card with the 3. **NIP-46 Client Signer mode** (`src/signer/nip46_client.rs`):
active profile's avatar, name, shortened npub, and a "Switch profile" button. - Connects to external signer (bunker) via `nostrconnect://` URI
4. **HomeScreen test fixes.** The identity card duplicates the active profile's name - Supports both local (separate process) and remote signers over relays
and npub on the page, so the two affected tests now scope their queries to the - NIP-44 v2 encryption for all communication
identity card (via the "Active signing identity" heading) and to the profile - Connection state tracking (connecting/connected/error) with relay connection monitoring
list, instead of querying the whole screen. - Automatic approval timeout (5 min) and queue cap (20 pending)
5. **Prettier** applied to `ImportProfileModal.tsx`, `ProfilesScreen.tsx`, - Connect secret echo verification per NIP-46 spec
`AppProvider.tsx` (and the test file), clearing the three existing - Graceful disconnect/revoke with connection cleanup
`format:check` warnings — `npm run format:check` is now fully clean.
6. **HomeScreen keyboard accessibility (this session).** Profile list rows 3. **Signer mode management**:
(`<li>` elements) are now keyboard-focusable and operable: - Users can choose "Embedded signer" or "NIP-46 signer" during account setup
- Added `role="listbox"` on the `<ul>` and `role="option"` + `aria-selected` - Switch modes anytime without changing public key (preserves `npub`)
on each non-active `<li>`. - Clear UI showing active mode, connection status, and pending approvals
- Added `tabIndex={0}` so non-active rows receive keyboard focus. - Security notes explaining trade-offs between modes
- Added `onKeyDown` handler (Enter/Space to select) matching the existing
`onClick` behavior (skips if target is a button/a/input). 4. **Frontend integration**:
- Added descriptive `aria-label` including profile name and active state. - New `SignerModeScreen.tsx` for mode selection and status monitoring
- Added `.home-profile-row:not(.is-active):focus-visible` CSS rule for the - Updated `AppProvider` with `signerModeGet`, `signerModeSet`, `embeddedSignerStatus`,
standard 2px solid var(--focus) + 2px offset ring. `nip46Connect`, `nip46Disconnect`, `nip46Status`, and approval handlers
- Updated the test from `findByRole('list')` to `findByRole('listbox')`. - New types: `SignerMode`, `ApprovalDetails`, `EmbeddedSignerStatus`, `Nip46SignerStatus`
7. **HomeScreen design-system alignment (this session).** Polish pass addressing - Sidebar navigation updated with "Signer Mode" entry
critique findings:
- Removed the identity card's `linear-gradient` background — now flat 5. **IPC protocol extensions** (`src/ipc.rs`):
`var(--surface)` with `border-color: var(--success)` (flat-by-default rule). - `SignerModeGet`, `SignerModeSet` for mode management
- Fixed `.home-profile-row` border-radius from `8px` to `var(--radius-sm)` (9px). - `EmbeddedSignerStatus`, `EmbeddedSignerApprove`
- Replaced the publication empty-state sentence with a centered layout: icon + - `Nip46Connect`, `Nip46Disconnect`, `Nip46Status`, `Nip46Approve`
muted text + secondary button, matching the product's empty-state language. - Legacy bunker signer commands delegated to new NIP-46 client when in that mode
- Normalized `.home-identity-name` font-size from `18px` to `16px` (consistent
with `.profile-name`). 6. **Security hardening**:
- Removed redundant `grid-template-columns: 1fr` from `.home-grid`. - All secret keys encrypted at rest with Argon2id + AES-256-GCM
8. **First-run guide redesign (this session).** Replaced plain `<ol>` with visual - Zeroize for in-memory key cleanup
step indicators: each step has a `primary-soft` icon circle (users, copy, edit) - Approval timeouts and queue caps prevent DoS
alongside a title + description. Clear spatial hierarchy, consistent 14px text. - Connection secrets verified on handshake
9. **Identity card background restored.** After removing the gradient, the card - No private keys in logs, crash reports, or network requests
lost its green tint. Restored as flat `var(--success-soft)` background — keeps - Keys never sent to server/relay/AI services
the security-state signal without breaking flat-by-default.
10. **Re-critique score: 27/40 → 30/40 (Good).** All P1s resolved, all P2s
resolved. Remaining items are P3 (keyboard shortcuts, search/filter, minor
copy inconsistencies).
11. **Compose label alignment (this session).** Changed HomeScreen header button
from "Compose note" to "Compose" to match the sidebar nav label. Updated
tests to use exact name matching and scoped queries.
12. **Identity card simplified (this session).** Removed "Active signing identity"
kicker text. Card now uses flat `var(--surface)` background with
`var(--success)` border — matches the active profile card treatment.
Removed dead `.home-identity-kicker` CSS.
13. **Polish cleanup (this session).** Removed dead `.active-profile-row` CSS
class. Replaced hardcoded `rgba` fallbacks in `.home-profile-row.is-active`
with design tokens (`var(--surface-2)`, `var(--border)`). Removed duplicate
edit icon from publication empty state.
14. **Identity card removed (this session).** The entire identity card was removed
from HomeScreen — the subtitle ("Publishing as …") and the active profile row
in the list already convey the same information. Removed ~75 lines of dead CSS
(`.home-identity-card`, `.home-identity-content`, `.home-identity-copy`,
`.home-identity-name`, responsive rules). Updated the test to verify the
profile name and npub in the profile list instead of the removed card.
15. **HomeScreen polish pass (this session).** Aligned spacing to the 8/12/16/20/32
design scale: profile list gap 10→12px, add-profile margin 14→16px, publish
empty-state padding 8→12px, page-subtitle margin 4→6px. Added
`.home-profile-row:not(.is-active):hover` with `var(--surface-hover)` for
visible hover feedback on selectable rows.
16. **"View in Feed" button after publish (this session).** After a successful or
partial publish, both the Compose screen's Result card and the Home screen's
"Most recent publication" card now show a "View in Feed" ghost button that
navigates to the Feed screen. `ComposeScreen` now accepts an optional
`onNavigate` prop; `Shell` passes `setScreen` through.
17. **Last publish persisted across restarts (this session).** The most recent
publish report is now saved to `last_publish.json` in the data directory and
loaded on app startup. Added `StoredPublishReport` in `vault.rs`, `last_publish`
field on `App` and `AppStateView`, save on publish in `ipc.rs`, and
`last_publish` on the frontend `AppState` type. `AppProvider` initializes
`lastPublish` from `state.last_publish` so the Home screen shows the result
after a restart.
18. **Inline post preview on Home (this session).** The "Most recent publication"
card now shows the note content inline (compact summary) instead of a "View in
Feed" link. Added `content` field to `StoredPublishReport` and
`PublishReport`. Removed `onNavigate` prop from `ComposeScreen` and the
"View in Feed" button from both screens. Added `.publish-preview` CSS for the
content display.
19. **Fix: note preview now appears after publishing (this session).** The
"Most recent publication" card was not showing the note content preview
because the IPC handler returned the bare `PublishReport` (no `content`
field) instead of the `StoredPublishReport` that includes it. Changed
`src/ipc.rs` to return `stored` instead of `report`.
20. **Most Recent Publication shows only fully published events (this session).**
The "Most recent publication" box now queries relays for the active
profile's publications and determines per-event publication status by
comparing which relays served each event against all enabled relays.
Only the newest fully published event is shown in the main box. Partially
published events appear only in the expandable "Relay results" section.
Empty state shown when no fully published events exist. Added
`PublicationStatus` type, `computePublicationStatus()` helper, and
`useProfilePublications` hook. Rewrote `PublicationResult` in
`HomeScreen.tsx`. Added 10 new tests.
## Commits added in this session (newest first) ## Commits added in this session (newest first)
- `ea56806` fix: show only fully published events in Most Recent Publication box - `b484bde` feat: add embedded signer and NIP-46 client signer modes
- `577e9f4` fix: return StoredPublishReport with content to frontend after publish
- `cd5d2d7` Show published note content inline on Home screen
- `bab82f5` Persist last publish report across restarts
- `b0d9143` Add 'View in Feed' button after publishing a note
- `471acf8` polish: align HomeScreen spacing to design scale, add profile row hover
- `269f0c0` Remove identity card from HomeScreen — redundant with subtitle and profile list
- `566aa46` Remove redundant 'Active profile' heading from identity card
- `85ba088` fix(polish): clean up HomeScreen dead code and token drift
- `e854c95` fix(polish): remove identity card kicker, flatten to success border
- `ee13d47` fix(polish): align Compose button label with sidebar nav
- `32fc764` fix(polish): restore success-soft background on identity card
- `b05894e` fix(layout): redesign first-run guide with visual step indicators
- `96dcbe4` fix(polish): align HomeScreen with design system
- `e9022b3` fix(a11y): add keyboard accessibility to HomeScreen profile list
- `a47ce8b` checkpoint: document keyboard accessibility hardening
- `f1236e7` checkpoint: document clippy warning cleanup
- `3083a44` chore: fix two clippy warnings
- `0207636` feat: expose profile import over IPC
- `2604cf9` checkpoint: document release packages
## Verification commands run ## Verification commands run
All green in this session, run after the changes: All green in this session, run after the changes:
- Rust: `cargo fmt --check` clean; `cargo test` — 116 passed; - Rust: `cargo fmt --check` clean; `cargo test` — 119 passed;
`cargo clippy --all-targets` — clean, zero warnings; `cargo build --release` — success. `cargo clippy --all-targets` — clean, zero warnings; `cargo build --release` — success.
- Frontend: `npm test` — 16 files / 108 passed (including 10 new publication-filtering tests); - Frontend: `npm test` — 16 files / 110 passed;
`npm run typecheck` clean; `npm run lint` clean (only the harmless ES-module `npm run typecheck` clean; `npm run lint` clean (only harmless ES-module warning);
reparsing warning); `npm run format:check` clean; `npm run build` — success `npm run format:check` clean; `npm run build` — success (Vite bundle built);
(Vite bundle built); `npm run electron:build` — success. `npm run electron:build` — success.
- Packaging (previous session, still valid artifacts):
`npx electron-builder --linux AppImage deb` produced
`frontend/release/Keynctr-0.1.0.AppImage` and
`frontend/release/keynectr_0.1.0_amd64.deb`, verified with `file`.
- Wayland `--ozone-platform` / `has no handler` messages on `electron:build` are
harmless.
## How to resume / reproduce ## How to resume / reproduce
- Build + run the GUI: `cargo build --release && cd frontend && npm run build && - Build + run the GUI: `cargo build --release && cd frontend && npm run build &&
npm run electron:build && npm start` (dev: `npm run dev` in one terminal + npm run electron:build && npm start` (dev: `npm run dev` in one terminal +
`NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in a second, or `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in a second, or
`npm run start:dev`). `npm run start:dev`).
- Import an existing account (GUI): unlock the vault if needed, open - Choose signer mode: Open **Signer Mode** from sidebar → select "Embedded Signer" or "NIP-46 Remote Signer".
**Profiles → Add existing account**, paste only the private key - For NIP-46 mode: In your Nostr app (Amber, Nostr Connect, etc.), choose "use a remote signer",
(`nsec1...`), and confirm — the profile name and metadata (picture, NIP-05) are copy the `nostrconnect://` link, paste it in Keynctr's Signer Mode screen, and click Connect.
derived from the network automatically, with a shortened-npub name fallback. - Switch modes anytime without changing your public key (same `npub`).
- Import (IPC/CLI): the `serve` loop now accepts - Signing workflow: When a sensitive operation needs approval, a prompt appears with event kind,
`{"id": 1, "method": "import_profile", "params": {"label": "...", "secret": "nsec1..."}}` content preview, and destination relays. Click Approve or Reject.
and replies with the new profile summary plus full app state.
- Home identity card: the card appears at the top of Home whenever a profile is ## Threat model summary
active; "Switch profile" navigates to the Profiles screen. | Aspect | Embedded Signer | NIP-46 Client |
- Installers: `sudo apt install ./frontend/release/keynectr_0.1.0_amd64.deb` or |--------|-----------------|---------------|
`./frontend/release/Keynctr-0.1.0.AppImage` (rebuild with | **Key Location** | Local encrypted vault | Remote signer (never on this device) |
`npx electron-builder --linux AppImage deb` after changes). | **Compromise Impact** | Full key extraction if vault unlocked + malware | Attacker can *request* signatures, cannot extract key |
| **Phishing Resistance** | None (local UI spoofable) | None (NIP-46 doesn't prevent malicious requests) |
| **Device Theft** | Vault encrypted at rest; unlock needed | No key on device; connection revocable |
| **Malicious Relay** | N/A (local signing) | Relay sees only encrypted NIP-44 payloads |
| **Connection Leak** | N/A | Attacker can request signatures until revoked |
| **Replay Protection** | N/A | NIP-46 uses unique request IDs + timestamps |
## Outstanding / next-step items ## Outstanding / next-step items
- **Undo restores with empty `secret_key`** (stores `ProfileSummary`); needs - OS keyring integration (GNOME Keyring, KWallet, macOS Keychain, Windows Credential Manager)
`StoredProfile` in `undo_history` for full secret recovery. for vault encryption key storage as optional enhancement
- `.opencode/`, `.impeccable/critique/`, `COSMIC_THEME.md`, - Hardware wallet NIP-46 signer integration testing
`KeynectrAppIconPossibility02.jpeg` remain untracked (`.directory` is a - Connection URI rotation / periodic re-pairing for NIP-46
file-manager artifact); no commit was created for them in this session. - Session binding to specific device/account where platform allows
- Installer artifacts are local build outputs under `frontend/release/` and are - Comprehensive NIP-46 client test suite (mock signer, timeout/rejection scenarios)
not committed.
- README is stale (title, dependency versions, test counts, Forgejo references) —
worth a docs pass before 0.2.
- **HomeScreen critique**: 30/40 (Good). Remaining P3 items: keyboard shortcuts,
profile search/filter. All other issues resolved. See
`.impeccable/critique/` for snapshots.