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
- 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
staged. The only untracked entries are the pre-existing hygiene leftovers and the
source JPEG (all intentionally untracked — see "Still untracked"). Build artifacts
(`release/`, `dist/`) are gitignored.
## 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)
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`
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
@ -58,36 +83,20 @@ each feature's tests travel with it.
## Commits added this session (newest first)
| 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
parent chain: `6eff510` → `2c61830` → `caed722` → … → `d92371f` → `114b343`.)
(The prior session's feature commits — `114b343` packaging icon, `d92371f` checkpoint,
`6eff510` export, `2c61830` signer modes, `caed722` audit log — remain the parent chain.)
## Verification (run this session)
Full suite per AGENTS.md, run on top of `114b343` (icon is a binary asset, no code
change, so the suite is expected to hold):
- **Rust**: `cargo test --release` → **186 passed**, 0 failed; `cargo clippy
--all-targets` → clean (exit 0); `cargo fmt --check` → clean (exit 0); `cargo build
--release` → Finished, exit 0.
- **Frontend**: `npm test` → **116 passed** (16 files); `npm run typecheck` → clean
(exit 0); `npm run lint` → clean (exit 0).
- `npm run format:check` → **exit 1** on 5 files (`src/components/ExportSecretKeyModal.tsx`,
`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.
Full suite per AGENTS.md, run on top of `e6e4922`:
- **Rust**: `cargo test` → **196 passed**, 0 failed (186 prior + 10 new
`signer::backend` tests); `cargo clippy --all-targets` → clean (exit 0);
`cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, exit 0.
- **Frontend**: not re-run this session — `e6e4922` is Rust-only (new
`src/signer/backend.rs` + a module line in `src/signer/mod.rs`), so the frontend
suite is unchanged from `114b343` (116 passed / typecheck / lint clean). Re-verified
in full at the end of Step 3 (IPC reroute), where frontend handlers change.
## How to reproduce / exercise
- 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)
- **Step 2 (packaging): DONE.** Icon restored, `npm run dist` + declared targets green,
icon proven inside both artifacts, committed as `114b343`.
- **Step 3 (signer abstraction)** — proposed for sign-off, NOT yet implemented. Current
state that motivates the API: `src/signer/mod.rs` already defines a `Signer` trait and
a `Signing` enum (`Local(Keys)` / `External{signer, profile_pubkey}`); `SignerMode`
- **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922`
(`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests,
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*
is duplicated on `App` (app-level), and `App` holds three separate signer handles
(`embedded_signer`, `nip46_signer`, `nip46_bunker_signer`). The IPC dispatcher
(`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites (see the
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.**
(`src/ipc.rs`) branches on `signer_mode`/handle presence in ~12 sites.
- 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
the UI, not just parsed.