diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md
index 7717e2d..3aeed92 100644
--- a/CHECKPOINT-encryption.md
+++ b/CHECKPOINT-encryption.md
@@ -1,185 +1,100 @@
-# Checkpoint — HomeScreen publication filtering (2026-09-01)
+# Checkpoint — Embedded Signer & NIP-46 Client Modes (2026-09-01)
## Where things are
- 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/`,
`.impeccable/`, `COSMIC_THEME.md`, `KeynectrAppIconPossibility02.jpeg`).
## What was completed
-1. **Clippy is now warning-free.** Removed the two pre-existing warnings: the
- no-op `drop(stored)` of a `&mut` reference in `src/profiles.rs` and the unused
- `restored` binding around `app.undo_delete()` in the `UndoDelete` handler in
- `src/ipc.rs`. No behaviour change — both were dead code.
-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
- through the JSON-lines IPC protocol: new `ImportProfile` request and handler in
- `src/ipc.rs`, `import_profile` added to the Electron method allowlist,
- `api.importProfile` + `AppProvider.importProfile` in the frontend, and
- "Add existing account" buttons on the Profiles screen (empty and populated
- states). The existing `ImportProfileModal` (enter only the private key; the name
- 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
- active profile's avatar, name, shortened npub, and a "Switch profile" button.
-4. **HomeScreen test fixes.** The identity card duplicates the active profile's name
- and npub on the page, so the two affected tests now scope their queries to the
- identity card (via the "Active signing identity" heading) and to the profile
- list, instead of querying the whole screen.
-5. **Prettier** applied to `ImportProfileModal.tsx`, `ProfilesScreen.tsx`,
- `AppProvider.tsx` (and the test file), clearing the three existing
- `format:check` warnings — `npm run format:check` is now fully clean.
-6. **HomeScreen keyboard accessibility (this session).** Profile list rows
- (`
` elements) are now keyboard-focusable and operable:
- - Added `role="listbox"` on the `` and `role="option"` + `aria-selected`
- on each non-active `- `.
- - Added `tabIndex={0}` so non-active rows receive keyboard focus.
- - Added `onKeyDown` handler (Enter/Space to select) matching the existing
- `onClick` behavior (skips if target is a button/a/input).
- - Added descriptive `aria-label` including profile name and active state.
- - Added `.home-profile-row:not(.is-active):focus-visible` CSS rule for the
- standard 2px solid var(--focus) + 2px offset ring.
- - Updated the test from `findByRole('list')` to `findByRole('listbox')`.
-7. **HomeScreen design-system alignment (this session).** Polish pass addressing
- critique findings:
- - Removed the identity card's `linear-gradient` background — now flat
- `var(--surface)` with `border-color: var(--success)` (flat-by-default rule).
- - Fixed `.home-profile-row` border-radius from `8px` to `var(--radius-sm)` (9px).
- - Replaced the publication empty-state sentence with a centered layout: icon +
- muted text + secondary button, matching the product's empty-state language.
- - Normalized `.home-identity-name` font-size from `18px` to `16px` (consistent
- with `.profile-name`).
- - Removed redundant `grid-template-columns: 1fr` from `.home-grid`.
-8. **First-run guide redesign (this session).** Replaced plain `
` with visual
- step indicators: each step has a `primary-soft` icon circle (users, copy, edit)
- alongside a title + description. Clear spatial hierarchy, consistent 14px text.
-9. **Identity card background restored.** After removing the gradient, the card
- lost its green tint. Restored as flat `var(--success-soft)` background — keeps
- 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.
+1. **Added unified Signer architecture** with a common `Signer` trait in `src/signer/mod.rs`
+ that both embedded and NIP-46 client implementations share. The trait provides:
+ - `get_public_key()`, `sign_event()`, `get_signer_type()`, `is_available()`
+ - `request_approval()`, `disconnect()`, `revoke()`, `status_string()`, `detailed_status()`
+
+2. **Embedded Signer mode** (`src/signer/embedded.rs`):
+ - Keys stored locally in the encrypted vault (Argon2id + AES-256-GCM)
+ - Zeroize memory protection for secret keys
+ - Per-request user approval with 5-minute timeout and 20-request queue cap
+ - Sensitive operations (kind 0, 3, 5, 6, 10000-10002, 30000-30015) flagged for extra confirmation
+ - Active profile binding with automatic updates on profile create/import/select
+
+3. **NIP-46 Client Signer mode** (`src/signer/nip46_client.rs`):
+ - Connects to external signer (bunker) via `nostrconnect://` URI
+ - Supports both local (separate process) and remote signers over relays
+ - NIP-44 v2 encryption for all communication
+ - Connection state tracking (connecting/connected/error) with relay connection monitoring
+ - Automatic approval timeout (5 min) and queue cap (20 pending)
+ - Connect secret echo verification per NIP-46 spec
+ - Graceful disconnect/revoke with connection cleanup
+
+3. **Signer mode management**:
+ - Users can choose "Embedded signer" or "NIP-46 signer" during account setup
+ - Switch modes anytime without changing public key (preserves `npub`)
+ - Clear UI showing active mode, connection status, and pending approvals
+ - Security notes explaining trade-offs between modes
+
+4. **Frontend integration**:
+ - New `SignerModeScreen.tsx` for mode selection and status monitoring
+ - Updated `AppProvider` with `signerModeGet`, `signerModeSet`, `embeddedSignerStatus`,
+ `nip46Connect`, `nip46Disconnect`, `nip46Status`, and approval handlers
+ - New types: `SignerMode`, `ApprovalDetails`, `EmbeddedSignerStatus`, `Nip46SignerStatus`
+ - Sidebar navigation updated with "Signer Mode" entry
+
+5. **IPC protocol extensions** (`src/ipc.rs`):
+ - `SignerModeGet`, `SignerModeSet` for mode management
+ - `EmbeddedSignerStatus`, `EmbeddedSignerApprove`
+ - `Nip46Connect`, `Nip46Disconnect`, `Nip46Status`, `Nip46Approve`
+ - Legacy bunker signer commands delegated to new NIP-46 client when in that mode
+
+6. **Security hardening**:
+ - All secret keys encrypted at rest with Argon2id + AES-256-GCM
+ - Zeroize for in-memory key cleanup
+ - Approval timeouts and queue caps prevent DoS
+ - Connection secrets verified on handshake
+ - No private keys in logs, crash reports, or network requests
+ - Keys never sent to server/relay/AI services
## Commits added in this session (newest first)
-- `ea56806` fix: show only fully published events in Most Recent Publication box
-- `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
+- `b484bde` feat: add embedded signer and NIP-46 client signer modes
## Verification commands run
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.
-- Frontend: `npm test` — 16 files / 108 passed (including 10 new publication-filtering tests);
- `npm run typecheck` clean; `npm run lint` clean (only the harmless ES-module
- reparsing warning); `npm run format:check` clean; `npm run build` — success
- (Vite bundle built); `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.
+- Frontend: `npm test` — 16 files / 110 passed;
+ `npm run typecheck` clean; `npm run lint` clean (only harmless ES-module warning);
+ `npm run format:check` clean; `npm run build` — success (Vite bundle built);
+ `npm run electron:build` — success.
## How to resume / reproduce
- 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 +
`NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` in a second, or
`npm run start:dev`).
-- Import an existing account (GUI): unlock the vault if needed, open
- **Profiles → Add existing account**, paste only the private key
- (`nsec1...`), and confirm — the profile name and metadata (picture, NIP-05) are
- derived from the network automatically, with a shortened-npub name fallback.
-- Import (IPC/CLI): the `serve` loop now accepts
- `{"id": 1, "method": "import_profile", "params": {"label": "...", "secret": "nsec1..."}}`
- 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
- active; "Switch profile" navigates to the Profiles screen.
-- Installers: `sudo apt install ./frontend/release/keynectr_0.1.0_amd64.deb` or
- `./frontend/release/Keynctr-0.1.0.AppImage` (rebuild with
- `npx electron-builder --linux AppImage deb` after changes).
+- Choose signer mode: Open **Signer Mode** from sidebar → select "Embedded Signer" or "NIP-46 Remote Signer".
+- For NIP-46 mode: In your Nostr app (Amber, Nostr Connect, etc.), choose "use a remote signer",
+ copy the `nostrconnect://` link, paste it in Keynctr's Signer Mode screen, and click Connect.
+- Switch modes anytime without changing your public key (same `npub`).
+- Signing workflow: When a sensitive operation needs approval, a prompt appears with event kind,
+ content preview, and destination relays. Click Approve or Reject.
+
+## Threat model summary
+| Aspect | Embedded Signer | NIP-46 Client |
+|--------|-----------------|---------------|
+| **Key Location** | Local encrypted vault | Remote signer (never on this device) |
+| **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
-- **Undo restores with empty `secret_key`** (stores `ProfileSummary`); needs
- `StoredProfile` in `undo_history` for full secret recovery.
-- `.opencode/`, `.impeccable/critique/`, `COSMIC_THEME.md`,
- `KeynectrAppIconPossibility02.jpeg` remain untracked (`.directory` is a
- file-manager artifact); no commit was created for them in this session.
-- Installer artifacts are local build outputs under `frontend/release/` and are
- 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.
+- OS keyring integration (GNOME Keyring, KWallet, macOS Keychain, Windows Credential Manager)
+ for vault encryption key storage as optional enhancement
+- Hardware wallet NIP-46 signer integration testing
+- Connection URI rotation / periodic re-pairing for NIP-46
+- Session binding to specific device/account where platform allows
+- Comprehensive NIP-46 client test suite (mock signer, timeout/rejection scenarios)
\ No newline at end of file