checkpoint: document SigningBackend + SigningError foundation (Step 3)

This commit is contained in:
Avi 2026-09-03 15:55:33 -05:00
commit a2307ac475

View file

@ -1,17 +1,42 @@
# Checkpoint — Signer Modes + Fail-Closed Key Export + Packaging Icon (2026-09-03) # Checkpoint — Signer Abstraction (SigningBackend/SigningError) (2026-09-03)
## Where things are ## Where things are
- Project: `/home/avi/Projects/Keynctr` - Project: `/home/avi/Projects/Keynctr`
- Branch: `master` @ **`114b343`** ("fix(packaging): restore Linux app icon so dist builds ship an icon"). - Branch: `master` @ **`e6e4922`** ("feat(signer): add SigningBackend + SigningError + VaultRef").
- Working tree: **effectively clean for tracked files.** No tracked file is modified or - Working tree: **effectively clean for tracked files.** No tracked file is modified or
staged. The only untracked entries are the pre-existing hygiene leftovers and the staged. The only untracked entries are the pre-existing hygiene leftovers and the
source JPEG (all intentionally untracked — see "Still untracked"). Build artifacts source JPEG (all intentionally untracked — see "Still untracked"). Build artifacts
(`release/`, `dist/`) are gitignored. (`release/`, `dist/`) are gitignored.
## What was completed (this session) ## What was completed (this session)
**Step 3 (signer abstraction) — foundation landed.** The approved API types are now in
the codebase as a standalone, independently-testable commit (`e6e4922`), ahead of the
vault-integration and IPC-rerouting sub-steps. `src/signer/backend.rs` (new) adds:
- `SigningBackend { Internal, Remote { vault_ref } }` — the per-profile choice of where
user content is signed. `Remote` holds **only** an opaque `VaultRef`
(`profile_npub` + `signer_pubkey`); the NIP-46 connection secret is **never** inlined
(enforced by a test that serializes a `Remote` backend and asserts no secret appears).
- `VaultRef` — an opaque, secret-free pointer into the vault's encrypted
connection-secret store. Resolution to the decrypted secret happens only at the vault
boundary, while the vault is unlocked (that wiring is the next sub-step).
- `SigningError` — the closed set of signing failures (no active profile, internal key
unavailable, remote connection missing, secret resolution, identity mismatch, not
connected, permission denied, expired, revoked, rejected, timeout, invalid signature,
network, storage, internal). Each carries a stable `ErrorKind`, a user-facing message
(mirroring the existing `AppError` copy), and an optional technical detail.
`From<SigningError> for AppError` converts at the IPC boundary; a best-effort
`SigningError::from_app` lifts upstream `AppError`s back into the signing space.
The existing `Signer` trait, `Signing` enum, `EmbeddedSigner`, `Nip46ClientSigner`, and
permission model are untouched by this commit — they are what the upcoming vault
integration and IPC reroute will drive through `SigningBackend`.
---
The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692) The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692)
was triaged into **three logical, independently-verifiable commits** in a prior session, was triaged into **three logical, independently-verifiable commits** in a prior session,
and this session **landed the Step-2 packaging fix** on top. Order is and the packaging fix landed in `114b343`. Order is
`a → c → b → (packaging)`: `ExportSecretKey` (b) reads the per-profile `signer_mode` `a → c → b → (packaging)`: `ExportSecretKey` (b) reads the per-profile `signer_mode`
field and the `external_signer_not_connected` error that the signer-mode commit (c) field and the `external_signer_not_connected` error that the signer-mode commit (c)
introduces, so (c) had to land first. The only uncommitted *tests* were the export introduces, so (c) had to land first. The only uncommitted *tests* were the export
@ -58,36 +83,20 @@ each feature's tests travel with it.
## Commits added this session (newest first) ## Commits added this session (newest first)
| Hash | Message | | Hash | Message |
|------|---------| |------|---------|
| `114b343` | fix(packaging): restore Linux app icon so dist builds ship an icon | | `e6e4922` | feat(signer): add SigningBackend + SigningError + VaultRef |
(The prior session's three feature commits and their checkpoint `d92371f` remain the (The prior session's feature commits — `114b343` packaging icon, `d92371f` checkpoint,
parent chain: `6eff510` → `2c61830` → `caed722` → … → `d92371f` → `114b343`.) `6eff510` export, `2c61830` signer modes, `caed722` audit log — remain the parent chain.)
## Verification (run this session) ## Verification (run this session)
Full suite per AGENTS.md, run on top of `114b343` (icon is a binary asset, no code Full suite per AGENTS.md, run on top of `e6e4922`:
change, so the suite is expected to hold): - **Rust**: `cargo test` → **196 passed**, 0 failed (186 prior + 10 new
- **Rust**: `cargo test --release` → **186 passed**, 0 failed; `cargo clippy `signer::backend` tests); `cargo clippy --all-targets` → clean (exit 0);
--all-targets` → clean (exit 0); `cargo fmt --check` → clean (exit 0); `cargo build `cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0.
--release` → Finished, exit 0. - **Frontend**: not re-run this session — `e6e4922` is Rust-only (new
- **Frontend**: `npm test` → **116 passed** (16 files); `npm run typecheck` → clean `src/signer/backend.rs` + a module line in `src/signer/mod.rs`), so the frontend
(exit 0); `npm run lint` → clean (exit 0). suite is unchanged from `114b343` (116 passed / typecheck / lint clean). Re-verified
- `npm run format:check` → **exit 1** on 5 files (`src/components/ExportSecretKeyModal.tsx`, in full at the end of Step 3 (IPC reroute), where frontend handlers change.
`src/screens/SignerModeScreen.tsx`, `src/state/AppProvider.tsx`,
`src/test/ExportSecretKey.test.tsx`, `src/test/fakeBackend.ts`). **Pre-existing** —
those files are committed as-is at `6eff510` (prior session) and are clean in the
working tree; this icon-only change touched none of them. Left for the Step-7
hygiene/format pass; not silently auto-fixed here.
- **Packaging (the point of this fix)**: `npm run dist` ran end-to-end. Note the script
is `electron-builder --linux dir`, which builds only the unpacked dir; the declared
`linux.target` (AppImage + deb) was also built directly to prove the icon lands:
- `release/Keynctr-0.1.0.AppImage` — 135,749,547 bytes.
- `release/keynectr_0.1.0_amd64.deb` — 105,810,972 bytes.
- **Icon proven inside both artifacts**: the embedded icon is **byte-identical
(md5 `b3e372f7`)** to the generated `build/icon.png` in all four locations checked —
the deb's `usr/share/icons/hicolor/512x512/apps/keynectr.png`, the AppImage's hicolor
png, the AppImage's `.DirIcon`, and `build/icon.png` itself — all 512x512 RGBA with
real transparency (32,626 opaque px, 226,582 transparent, corners alpha 0), ink
pure black. The deb `.desktop` reads `Icon=keynectr`, matching the hicolor name.
## How to reproduce / exercise ## How to reproduce / exercise
- Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in - Backend: `cargo run --release -- serve` (JSON-lines IPC on stdio) or the CLI in
@ -121,18 +130,23 @@ change, so the suite is expected to hold):
## Deferred / next steps (unchanged, plus new) ## Deferred / next steps (unchanged, plus new)
- **Step 2 (packaging): DONE.** Icon restored, `npm run dist` + declared targets green, - **Step 2 (packaging): DONE.** Icon restored, `npm run dist` + declared targets green,
icon proven inside both artifacts, committed as `114b343`. icon proven inside both artifacts, committed as `114b343`.
- **Step 3 (signer abstraction)** — proposed for sign-off, NOT yet implemented. Current - **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922`
state that motivates the API: `src/signer/mod.rs` already defines a `Signer` trait and (`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests,
a `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode` full Rust suite green). The `Remote { vault_ref }` variant holds **only** a `VaultRef`
— the NIP-46 connection secret is never inlined. **Remaining sub-steps, in order:**
1. **Vault integration** — encrypted connection-secret store keyed by `VaultRef`,
`VaultRef` → decrypted-secret resolution at the vault boundary (only while
unlocked), and removal of the inline `Nip46Connection.secret` field so secrets live
only in the vault. This is where the `vault_ref` constraint becomes load-bearing.
2. **IPC reroute** — drive `src/ipc.rs` handlers through `SigningBackend` (selected
per-profile) so no inline `signer_mode`/handle-presence branching remains; convert
handler results to `AppError` via `From<SigningError>`.
Prior state that motivated the API: `src/signer/mod.rs` already had a `Signer` trait
and `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode`
(`Embedded`/`Nip46Bunker`/`Nip46Client`) lives on `StoredProfile` (per-profile) *and* (`Embedded`/`Nip46Bunker`/`Nip46Client`) lives on `StoredProfile` (per-profile) *and*
is duplicated on `App` (app-level), and `App` holds three separate signer handles is duplicated on `App` (app-level), and `App` holds three separate signer handles
(`embedded_signer`, `nip46_signer`, `nip46_bunker_signer`). The IPC dispatcher (`embedded_signer`, `nip46_signer`, `nip46_bunker_signer`). The IPC dispatcher
(`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites (see the (`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites.
grep list pasted to the user this session). The proposal promotes `src/signer` to the
single `Signer` source of truth, introduces a `SigningBackend { Internal{..},
Remote{..} }` per profile, and routes every IPC handler through the trait so no inline
mode branching remains. **Awaiting the user's sign-off on the API before any
implementation.**
- External-signer permissions (Step 4): wire `src/signer/permissions.rs` into the - External-signer permissions (Step 4): wire `src/signer/permissions.rs` into the
approval modal so grants (kinds, relays, expiry, rate) are persisted AND enforced in approval modal so grants (kinds, relays, expiry, rate) are persisted AND enforced in
the UI, not just parsed. the UI, not just parsed.