checkpoint: document embedded signer and NIP-46 client modes
This commit is contained in:
parent
b484bdeb08
commit
4038e2d32a
1 changed files with 78 additions and 163 deletions
|
|
@ -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.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue