Refresh checkpoint with NIP-46 approval gate

This commit is contained in:
Avi 2026-08-06 10:04:59 -05:00
commit da35af0271

View file

@ -1,4 +1,4 @@
# Checkpoint — NIP-46 remote signer (2026-08-05) # Checkpoint — NIP-46 remote signer + approval gate (2026-08-06)
A stopping point you can return to if this session is closed. Everything below was A stopping point you can return to if this session is closed. Everything below was
verified green at the moment this file was written. verified green at the moment this file was written.
@ -6,58 +6,74 @@ verified green at the moment this file was written.
## Where things are ## Where things are
- Project: `/home/avi/Projects/skills/nost-feed-manager` - Project: `/home/avi/Projects/skills/nost-feed-manager`
- Git repo: `master` @ `73cb08d` ("Add NIP-46 remote signer (external signing)"), on top of - Git repo: `master` @ `ca3203c` ("Require user approval for NIP-46 sign/encrypt requests"),
`13efee6` ("Refresh checkpoint with Forgejo README"), `3eb9fa9` ("Rewrite README for Forgejo on top of `593fc6a` ("Refresh checkpoint with NIP-46 remote signer"), `73cb08d` ("Add NIP-46
hosting"), and the history described in the previous checkpoint. remote signer (external signing)"), `13efee6` ("Refresh checkpoint with Forgejo README"),
`3eb9fa9` ("Rewrite README for Forgejo hosting"), and the history described in earlier
checkpoints.
- The working tree is **clean** — everything from this session is committed. - The working tree is **clean** — everything from this session is committed.
- The signer feature is split across one commit (`73cb08d`) that contains both the Rust backend - The signer feature spans `73cb08d` (the signer itself) plus `ca3203c` (the approval gate).
and the Electron/React frontend.
## What was completed: NIP-46 remote signer (bunker role) ## What was completed: NIP-46 remote signer (bunker role)
The app can now act as a **remote signer** (NIP-46, "bunker") for another Nostr app. Instead of The app can now act as a **remote signer** (NIP-46, "bunker") for another Nostr app. Instead of
pasting your nsec into other apps, they send signing requests to this app, which approves them pasting your nsec into other apps, they send signing requests to this app, which approves them
with the active profile's keys. Decisions made this session: the connected peer is with the active profile's keys. The signer runs **inside the `serve` process as a concurrent
**auto-approved** once the handshake succeeds, the signer runs **inside the `serve` process as a tokio task** (sharing `Arc<Mutex<App>>`), and the frontend gets a **dedicated Signer screen** as
concurrent tokio task** (sharing `Arc<Mutex<App>>`), and the frontend gets a **dedicated Signer its own sidebar nav item.
screen** as its own sidebar nav item.
- **Backend** (`src/signer.rs`): a hand-rolled NIP-46 implementation (the SDK has no ready-made **Security change this session (commit `ca3203c`): every key-using request now requires explicit
user approval.** The peer is still "trusted" after the handshake, but `sign_event`,
`nip44_encrypt`, and `nip44_decrypt` are no longer auto-approved. Each such request is parked in a
`pending` queue, exposed in `SignerStatus.pending`, and only runs after the user clicks
**Approve** on the Signer screen (or is answered with **Reject** / a 300-second timeout). All
non-key methods (`connect`, `get_public_key`, `ping`, `get_relays`, `switch_relays`, `logout`)
still run immediately.
- **Backend** (`src/signer.rs`): a hand-rolled NIP-46 implementation (the SDK has no ready-made
NIP-46 server). `Signer` exposes `status()`, `connect()`, `disconnect()`, `fail()`, NIP-46 server). `Signer` exposes `status()`, `connect()`, `disconnect()`, `fail()`,
`set_connected()`. `parse_connect_uri` accepts `nostrconnect://` links (and clearly rejects the `set_connected()`, `approve()`, and `await_approval()`. `parse_connect_uri` accepts
opposite-role `bunker://` scheme). The async `run_sign_task` connects over the chosen relays, `nostrconnect://` links (and clearly rejects the opposite-role `bunker://` scheme). The async
listens for kind 24133 requests decrypted with a NIP-44 v2 conversation key, and answers `run_sign_task` connects over the chosen relays, listens for kind 24133 requests decrypted with
`get_public_key`, `sign_event`, and `nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted a NIP-44 v2 conversation key, and answers `get_public_key`, `sign_event`, and
and published back to the peer. Six unit tests cover URI parsing, bunker rejection, NIP-44 `nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted and published back to the peer.
round-trip, get_public_key, sign_event, and unknown-method error responses. Key-using methods route through `gated_response``await_approval` (a `HashMap` of pending
- **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` now share `Arc<Mutex<App>>`; new variants entries, each with a `oneshot` decision channel) → `approved_response`, while everything else
`SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`. The connect/disconnect/status flows through the non-blocking `handle_request` dispatcher. The pending list clears on
requests are serviced by `run()` before the (long-lived) vault lock so a slow signer task never disconnect and on failure, so a slow/abandoned approval never leaks into a new connection.
blocks ordinary commands. Unit tests cover URI parsing, bunker rejection, NIP-44 round-trip, get_public_key, sign_event,
- **CLI** (`src/main.rs`): `signer status` and `signer connect <uri>`. unknown-method, and the approval gate (wait-then-sign, reject-never-uses-key, unknown-id error).
- **Electron/React frontend**: new **Signer screen** (sidebar nav item with the key icon) where you - **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` share `Arc<Mutex<App>>`; variants
paste a `nostrconnect://` link, Connect/Disconnect, and see the phase (Not connected / `SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`, and the new `SignerApprove { id,
Connecting… / Connected), the connected client's hex pubkey (shortened), the relays in use, and approved }`. The signer-status/approve requests are serviced by `run()` before the (long-lived)
any error. Wired through `api.ts` (`signerConnect`/`signerDisconnect`/`signerStatus`) and vault lock so a slow signer task never blocks ordinary commands.
`AppProvider`. When the vault is locked, a warning explains the signer needs an unlocked vault. - **CLI** (`src/main.rs`): `signer status` and `signer connect <uri>`; `signer status` notes that
Tests: `SignerScreen.test.tsx` (new) + `apiMock.ts`/`fakeBackend.ts` signer methods. approvals happen in the GUI (the CLI's blocking `connect` loop cannot answer approvals itself).
- **Electron/React frontend**: the **Signer screen** (sidebar nav item with the key icon) where you
paste a `nostrconnect://` link, **Connect**/**Disconnect**, see the phase, peer, relays, and any
error, and now review pending requests with **Approve**/**Reject** buttons. The screen polls
`signer_status` every second while mounted so approval requests appear without a manual
refresh. Wired through `api.ts` (`signerConnect`/`signerDisconnect`/`signerStatus`/
`signerApprove`) and `AppProvider`.
## Commits this session (newest first) ## Commits this session (newest first)
- `73cb08d` "Add NIP-46 remote signer (external signing)" — everything above in one commit. - `ca3203c` "Require user approval for NIP-46 sign/encrypt requests" — the approval gate
Full verification suite re-run green (below). (backend pending queue + `SignerApprove` IPC + Signer screen Approve/Reject + tests).
- `593fc6a` "Refresh checkpoint with NIP-46 remote signer" — checkpoint refresh (previous session).
- `73cb08d` "Add NIP-46 remote signer (external signing)" — the signer feature itself.
## How it was verified (all green) ## How it was verified (all green)
``` ```
cargo test # 70 passed cargo test # 75 passed
cargo clippy --all-targets # clean cargo clippy --all-targets # clean
cargo fmt --check # clean cargo fmt --check # clean
cargo build --release # builds (rebuilt so the GUI runs the new backend) cargo build --release # builds (rebuilt so the GUI runs the new backend)
npm run typecheck # clean (frontend/) npm run typecheck # clean (frontend/)
npm run lint # clean (pre-existing module warning only) npm run lint # clean (pre-existing module warning only)
npm run format:check # clean npm run format:check # clean
npm test # 69 passed (13 files) npm test # 71 passed (13 files)
npm run electron:build # compiles the Electron main process npm run electron:build # compiles the Electron main process
npm run build # rebuilds the React bundle (dist/) npm run build # rebuilds the React bundle (dist/)
``` ```
@ -69,19 +85,23 @@ a `bunker://` link and a non-`nostrconnect` URI were both rejected with clear er
1. Open the repo: `cd /home/avi/Projects/skills/nost-feed-manager` 1. Open the repo: `cd /home/avi/Projects/skills/nost-feed-manager`
2. State is committed: `git status` should be clean; `git log --oneline -5` shows 2. State is committed: `git status` should be clean; `git log --oneline -5` shows
`73cb08d` at the top. `ca3203c` at the top.
3. To try the signer: 3. To try the signer:
- GUI: `cd frontend && npm start`, then the **Signer** screen in the sidebar. Paste a - GUI: `cd frontend && npm start`, then the **Signer** screen in the sidebar. Paste a
`nostrconnect://` link from a Nostr app to sign for it. (Note: `npm start` alone does NOT `nostrconnect://` link from a Nostr app to sign for it. When the app sends a sign/decrypt
rebuild the React bundle — run `npm run build` first, or use `npm run dev` + request it appears under "Requests waiting for approval" — click **Approve** or **Reject**.
`NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` for live reload.) (Note: `npm start` alone does NOT rebuild the React bundle — run `npm run build` first, or
use `npm run dev` + `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` for live reload.)
- CLI: `cargo build --release`, then `./target/release/nostr-manager-backend signer status`. - CLI: `cargo build --release`, then `./target/release/nostr-manager-backend signer status`.
Note: the CLI's blocking `signer connect` loop cannot answer approval prompts — run the GUI
for that.
4. Re-run verification with the commands above. 4. Re-run verification with the commands above.
## Outstanding / next steps (if you continue) ## Outstanding / next steps (if you continue)
- Nothing uncommitted. Possible follow-ups: a real feed view (currently Home only shows the last - Nothing uncommitted. Possible follow-ups: a real feed view (currently Home only shows the last
publication); a NIP-46 **client** role (initiating `bunker://` from this app is intentionally publication); a NIP-46 **client** role (initiating `bunker://` from this app is intentionally
not implemented — only the signer role is). Live cross-client verification against an actual not implemented — only the signer role is); a CLI-side approval escape hatch for `signer
Nostr app (e.g. a NIP-46-compatible client that produces a `nostrconnect://` link) has not yet connect` (prompt on stdin) so approvals also work headlessly. Live cross-client verification
been done end-to-end, only the unit and CLI smoke tests. against an actual Nostr app (e.g. a NIP-46-compatible client that produces a `nostrconnect://`
link) has not yet been done end-to-end, only the unit and CLI smoke tests.