checkpoint: document NIP-46 connection-secrets vault integration (Step 3 sub-step 1)
This commit is contained in:
parent
510cb65e2f
commit
715c99c688
1 changed files with 84 additions and 44 deletions
|
|
@ -1,37 +1,70 @@
|
||||||
# Checkpoint — Signer Abstraction (SigningBackend/SigningError) (2026-09-03)
|
# Checkpoint — NIP-46 Connection Secrets in the Vault (2026-09-04)
|
||||||
|
|
||||||
## Where things are
|
## Where things are
|
||||||
- Project: `/home/avi/Projects/Keynctr`
|
- Project: `/home/avi/Projects/Keynctr`
|
||||||
- Branch: `master` @ **`e6e4922`** ("feat(signer): add SigningBackend + SigningError + VaultRef").
|
- Branch: `master` @ **`510cb65`** ("feat(signer): store NIP-46 connection secrets in
|
||||||
- Working tree: **effectively clean for tracked files.** No tracked file is modified or
|
the vault, keyed by VaultRef").
|
||||||
staged. The only untracked entries are the pre-existing hygiene leftovers and the
|
- Working tree: **clean for tracked files.** No tracked file is modified or staged.
|
||||||
source JPEG (all intentionally untracked — see "Still untracked"). Build artifacts
|
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.
|
(`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
|
**Step 3 sub-step 1 (Vault integration) — landed as `510cb65`.** The NIP-46
|
||||||
the codebase as a standalone, independently-testable commit (`e6e4922`), ahead of the
|
nostrconnect `secret` is a credential, so it no longer lives inline on
|
||||||
vault-integration and IPC-rerouting sub-steps. `src/signer/backend.rs` (new) adds:
|
`Nip46Connection` (which can be serialized and shown to the UI). It is now stored in
|
||||||
|
the vault's **encrypted `connection_secrets` store**, keyed by the opaque
|
||||||
|
`VaultRef` (profile npub + remote signer pubkey), encrypted under the vault key, and
|
||||||
|
resolved **only at the vault boundary while the vault is unlocked** (fail-closed when
|
||||||
|
locked). This is where the `vault_ref` constraint becomes load-bearing.
|
||||||
|
|
||||||
- `SigningBackend { Internal, Remote { vault_ref } }` — the per-profile choice of where
|
- **`src/vault.rs`** — new `ConnectionSecret { ref_: VaultRef, secret }` struct and a
|
||||||
user content is signed. `Remote` holds **only** an opaque `VaultRef`
|
`Vault.connection_secrets: Vec<ConnectionSecret>` store (encrypted under the vault
|
||||||
(`profile_npub` + `signer_pubkey`); the NIP-46 connection secret is **never** inlined
|
key when password-protected, plaintext when the vault has no password — exactly
|
||||||
(enforced by a test that serializes a `Remote` backend and asserts no secret appears).
|
mirroring how profile secrets are handled). Three helpers:
|
||||||
- `VaultRef` — an opaque, secret-free pointer into the vault's encrypted
|
- `store_connection_secret(vault, key, ref_, secret)` — upserts by `VaultRef`;
|
||||||
connection-secret store. Resolution to the decrypted secret happens only at the vault
|
encrypts when the vault is password-protected, else stores plaintext. A locked
|
||||||
boundary, while the vault is unlocked (that wiring is the next sub-step).
|
encrypted vault fails closed (`vault_locked`) rather than silently writing a
|
||||||
- `SigningError` — the closed set of signing failures (no active profile, internal key
|
plaintext secret that would not match once unlocked. Replacing an existing
|
||||||
unavailable, remote connection missing, secret resolution, identity mismatch, not
|
`VaultRef` never leaves a stale secret behind.
|
||||||
connected, permission denied, expired, revoked, rejected, timeout, invalid signature,
|
- `resolve_connection_secret(vault, key, ref_)` — decrypts (or returns plaintext)
|
||||||
network, storage, internal). Each carries a stable `ErrorKind`, a user-facing message
|
for a `VaultRef`; `Ok(None)` when no secret is stored, `Err(vault_locked)` when
|
||||||
(mirroring the existing `AppError` copy), and an optional technical detail.
|
the vault is encrypted but locked. On success the plaintext is `Zeroizing`
|
||||||
`From<SigningError> for AppError` converts at the IPC boundary; a best-effort
|
(shredded on scope exit).
|
||||||
`SigningError::from_app` lifts upstream `AppError`s back into the signing space.
|
- `delete_connection_secret(vault, ref_)` — removes an entry (on disconnect).
|
||||||
|
- **`src/signer/types.rs`** — the inline `secret: Option<String>` field is **removed**
|
||||||
|
from `Nip46Connection`. Legacy vaults that still carry an inline `secret`
|
||||||
|
deserialize fine (serde ignores the absent field) and the dead secret is dropped on
|
||||||
|
the next save.
|
||||||
|
- **`src/signer/backend.rs`** — `VaultRef::from_connection(&Nip46Connection)` is the
|
||||||
|
canonical way a `SigningBackend::Remote` (and the secret store) is keyed by a
|
||||||
|
connection; `profile_npub` is now `Option<String>` (a connection may be created with
|
||||||
|
no local profile active).
|
||||||
|
- **`src/signer/nip46_client.rs`** — on `connect`, the parsed nostrconnect secret is
|
||||||
|
written to the vault store (via `VaultRef::from_connection`) instead of being kept in
|
||||||
|
memory (`Nip46Inner.connect_secret` removed). On `disconnect` the stored secret is
|
||||||
|
dropped. In `run_sign_task`/`send_connect`, the connect secret is now resolved
|
||||||
|
**on-demand from the vault** (the single source of truth) rather than read from an
|
||||||
|
in-memory copy — a locked vault refuses the connect instead of sending it without the
|
||||||
|
secret.
|
||||||
|
- **`src/publish.rs`** — `publish_with_keys` now takes `&Signing` and signs through the
|
||||||
|
`Signing` trait (routes to `Keys::sign_event` for `Local`, or the remote signer for
|
||||||
|
`External`), instead of calling `Keys::sign_event` directly. `publish_active` /
|
||||||
|
`publish_as` wrap their keys in `Signing::Local`. (External signing in the publish
|
||||||
|
path is not wired end-to-end yet — it returns a clear "not yet supported" error —
|
||||||
|
but the routing pattern is in place for the IPC reroute.)
|
||||||
|
- **`src/signer/permissions.rs`** — test fixtures updated for the removed inline
|
||||||
|
`secret` field.
|
||||||
|
|
||||||
The existing `Signer` trait, `Signing` enum, `EmbeddedSigner`, `Nip46ClientSigner`, and
|
Security properties: no secret material is logged; the connect secret is resolved
|
||||||
permission model are untouched by this commit — they are what the upcoming vault
|
fresh from the vault at send time (fail-closed on a locked vault); a serialized
|
||||||
integration and IPC reroute will drive through `SigningBackend`.
|
`Nip46Connection` or `SigningBackend::Remote` carries zero secret material; the
|
||||||
|
decrypted plaintext is `Zeroizing`.
|
||||||
|
|
||||||
|
The previously-landed foundation (`e6e4922` `SigningBackend`/`SigningError`/`VaultRef`,
|
||||||
|
`b2755d8` `Signer` trait returning `SigningError`) is unchanged by this commit — it is
|
||||||
|
what this sub-step wires up.
|
||||||
|
|
||||||
---
|
---
|
||||||
The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692)
|
The previously-uncommitted working tree (27 modified + 10 untracked files, ~1852/692)
|
||||||
|
|
@ -83,20 +116,25 @@ each feature's tests travel with it.
|
||||||
## Commits added this session (newest first)
|
## Commits added this session (newest first)
|
||||||
| Hash | Message |
|
| Hash | Message |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `e6e4922` | feat(signer): add SigningBackend + SigningError + VaultRef |
|
| `510cb65` | feat(signer): store NIP-46 connection secrets in the vault, keyed by VaultRef |
|
||||||
|
|
||||||
(The prior session's feature commits — `114b343` packaging icon, `d92371f` checkpoint,
|
(The parent chain — `b2755d8` Signer trait returns SigningError, `a2307ac` checkpoint,
|
||||||
`6eff510` export, `2c61830` signer modes, `caed722` audit log — remain the parent chain.)
|
`e6e4922` SigningBackend+SigningError+VaultRef, `ee88171` checkpoint, `114b343` packaging
|
||||||
|
icon, `d92371f` checkpoint, `6eff510` export, `2c61830` signer modes, `caed722` audit log
|
||||||
|
— is unchanged.)
|
||||||
|
|
||||||
## Verification (run this session)
|
## Verification (run this session)
|
||||||
Full suite per AGENTS.md, run on top of `e6e4922`:
|
Full suite per AGENTS.md, run on top of `510cb65`:
|
||||||
- **Rust**: `cargo test` → **196 passed**, 0 failed (186 prior + 10 new
|
- **Rust**: `cargo test` → **196 passed**, 0 failed (no new/removed tests — this
|
||||||
`signer::backend` tests); `cargo clippy --all-targets` → clean (exit 0);
|
sub-step refactors existing code paths; the existing `signer::backend`, `vault`, and
|
||||||
|
`nip46_client` tests cover the change); `cargo clippy --all-targets` → clean (exit 0);
|
||||||
`cargo fmt --check` → clean (exit 0); `cargo build --release` → Finished, 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
|
- **Frontend**: `npm test` → passed; `npm run typecheck` → exit 0; `npm run lint` →
|
||||||
`src/signer/backend.rs` + a module line in `src/signer/mod.rs`), so the frontend
|
exit 0; `npm run electron:build` → exit 0; `npm run build` → exit 0.
|
||||||
suite is unchanged from `114b343` (116 passed / typecheck / lint clean). Re-verified
|
`npm run format:check` → **exit 1 on the 5 pre-existing files** (`ExportSecretKeyModal.tsx`,
|
||||||
in full at the end of Step 3 (IPC reroute), where frontend handlers change.
|
`SignerModeScreen.tsx`, `AppProvider.tsx`, `ExportSecretKey.test.tsx`, `fakeBackend.ts`)
|
||||||
|
— these are the documented Step-7 Prettier hygiene failures, **not** introduced by this
|
||||||
|
Rust-only change (none of the 6 modified files are frontend). Left untouched here.
|
||||||
|
|
||||||
## 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
|
||||||
|
|
@ -132,15 +170,17 @@ Full suite per AGENTS.md, run on top of `e6e4922`:
|
||||||
icon proven inside both artifacts, committed as `114b343`.
|
icon proven inside both artifacts, committed as `114b343`.
|
||||||
- **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922`
|
- **Step 3 (signer abstraction)** — foundation **landed** as `e6e4922`
|
||||||
(`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests,
|
(`SigningBackend` + `VaultRef` + `SigningError` in `src/signer/backend.rs`, 10 tests,
|
||||||
full Rust suite green). The `Remote { vault_ref }` variant holds **only** a `VaultRef`
|
full Rust suite green); `b2755d8` made the `Signer` trait return `SigningError`.
|
||||||
— the NIP-46 connection secret is never inlined. **Remaining sub-steps, in order:**
|
**Sub-step 1 (Vault integration) — DONE, landed as `510cb65`**: the encrypted
|
||||||
1. **Vault integration** — encrypted connection-secret store keyed by `VaultRef`,
|
`connection_secrets` store keyed by `VaultRef`, on-demand secret resolution at the
|
||||||
`VaultRef` → decrypted-secret resolution at the vault boundary (only while
|
vault boundary (fail-closed when locked), and the inline `Nip46Connection.secret`
|
||||||
unlocked), and removal of the inline `Nip46Connection.secret` field so secrets live
|
field removed. The `Remote { vault_ref }` variant holds **only** a `VaultRef` — the
|
||||||
only in the vault. This is where the `vault_ref` constraint becomes load-bearing.
|
NIP-46 connection secret is never inlined. **Remaining sub-step:**
|
||||||
2. **IPC reroute** — drive `src/ipc.rs` handlers through `SigningBackend` (selected
|
2. **IPC reroute** — drive `src/ipc.rs` handlers through `SigningBackend` (selected
|
||||||
per-profile) so no inline `signer_mode`/handle-presence branching remains; convert
|
per-profile) so no inline `signer_mode`/handle-presence branching remains; convert
|
||||||
handler results to `AppError` via `From<SigningError>`.
|
handler results to `AppError` via `From<SigningError>`. Also wire end-to-end
|
||||||
|
external (remote) signing in the publish path (`publish_with_keys` currently
|
||||||
|
returns "not yet supported" for `Signing::External`).
|
||||||
Prior state that motivated the API: `src/signer/mod.rs` already had a `Signer` trait
|
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`
|
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*
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue