Refresh checkpoint with NIP-46 approval gate
This commit is contained in:
parent
ca3203cfc4
commit
da35af0271
1 changed files with 59 additions and 39 deletions
|
|
@ -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
|
||||
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
|
||||
|
||||
- Project: `/home/avi/Projects/skills/nost-feed-manager`
|
||||
- Git repo: `master` @ `73cb08d` ("Add NIP-46 remote signer (external signing)"), on top of
|
||||
`13efee6` ("Refresh checkpoint with Forgejo README"), `3eb9fa9` ("Rewrite README for Forgejo
|
||||
hosting"), and the history described in the previous checkpoint.
|
||||
- Git repo: `master` @ `ca3203c` ("Require user approval for NIP-46 sign/encrypt requests"),
|
||||
on top of `593fc6a` ("Refresh checkpoint with NIP-46 remote signer"), `73cb08d` ("Add NIP-46
|
||||
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 signer feature is split across one commit (`73cb08d`) that contains both the Rust backend
|
||||
and the Electron/React frontend.
|
||||
- The signer feature spans `73cb08d` (the signer itself) plus `ca3203c` (the approval gate).
|
||||
|
||||
## 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
|
||||
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
|
||||
**auto-approved** once the handshake succeeds, the signer runs **inside the `serve` process as a
|
||||
concurrent tokio task** (sharing `Arc<Mutex<App>>`), and the frontend gets a **dedicated Signer
|
||||
screen** as its own sidebar nav item.
|
||||
with the active profile's keys. The signer runs **inside the `serve` process as a concurrent
|
||||
tokio task** (sharing `Arc<Mutex<App>>`), and the frontend gets a **dedicated Signer screen** as
|
||||
its own sidebar nav item.
|
||||
|
||||
**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()`,
|
||||
`set_connected()`. `parse_connect_uri` accepts `nostrconnect://` links (and clearly rejects the
|
||||
opposite-role `bunker://` scheme). The async `run_sign_task` connects over the chosen relays,
|
||||
listens for kind 24133 requests decrypted with a NIP-44 v2 conversation key, and answers
|
||||
`get_public_key`, `sign_event`, and `nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted
|
||||
and published back to the peer. Six unit tests cover URI parsing, bunker rejection, NIP-44
|
||||
round-trip, get_public_key, sign_event, and unknown-method error responses.
|
||||
- **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` now share `Arc<Mutex<App>>`; new variants
|
||||
`SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`. The connect/disconnect/status
|
||||
requests are serviced by `run()` before the (long-lived) vault lock so a slow signer task never
|
||||
blocks ordinary commands.
|
||||
- **CLI** (`src/main.rs`): `signer status` and `signer connect <uri>`.
|
||||
- **Electron/React frontend**: new **Signer screen** (sidebar nav item with the key icon) where you
|
||||
paste a `nostrconnect://` link, Connect/Disconnect, and see the phase (Not connected /
|
||||
Connecting… / Connected), the connected client's hex pubkey (shortened), the relays in use, and
|
||||
any error. Wired through `api.ts` (`signerConnect`/`signerDisconnect`/`signerStatus`) and
|
||||
`AppProvider`. When the vault is locked, a warning explains the signer needs an unlocked vault.
|
||||
Tests: `SignerScreen.test.tsx` (new) + `apiMock.ts`/`fakeBackend.ts` signer methods.
|
||||
`set_connected()`, `approve()`, and `await_approval()`. `parse_connect_uri` accepts
|
||||
`nostrconnect://` links (and clearly rejects the opposite-role `bunker://` scheme). The async
|
||||
`run_sign_task` connects over the chosen relays, listens for kind 24133 requests decrypted with
|
||||
a NIP-44 v2 conversation key, and answers `get_public_key`, `sign_event`, and
|
||||
`nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted and published back to the peer.
|
||||
Key-using methods route through `gated_response` → `await_approval` (a `HashMap` of pending
|
||||
entries, each with a `oneshot` decision channel) → `approved_response`, while everything else
|
||||
flows through the non-blocking `handle_request` dispatcher. The pending list clears on
|
||||
disconnect and on failure, so a slow/abandoned approval never leaks into a new connection.
|
||||
Unit tests cover URI parsing, bunker rejection, NIP-44 round-trip, get_public_key, sign_event,
|
||||
unknown-method, and the approval gate (wait-then-sign, reject-never-uses-key, unknown-id error).
|
||||
- **IPC** (`src/ipc.rs`): `serve`/`handle`/`run` share `Arc<Mutex<App>>`; variants
|
||||
`SignerConnect { uri }`, `SignerDisconnect`, `SignerStatus`, and the new `SignerApprove { id,
|
||||
approved }`. The signer-status/approve requests are serviced by `run()` before the (long-lived)
|
||||
vault lock so a slow signer task never blocks ordinary commands.
|
||||
- **CLI** (`src/main.rs`): `signer status` and `signer connect <uri>`; `signer status` notes that
|
||||
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)
|
||||
|
||||
- `73cb08d` "Add NIP-46 remote signer (external signing)" — everything above in one commit.
|
||||
Full verification suite re-run green (below).
|
||||
- `ca3203c` "Require user approval for NIP-46 sign/encrypt requests" — the approval gate
|
||||
(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)
|
||||
|
||||
```
|
||||
cargo test # 70 passed
|
||||
cargo test # 75 passed
|
||||
cargo clippy --all-targets # clean
|
||||
cargo fmt --check # clean
|
||||
cargo build --release # builds (rebuilt so the GUI runs the new backend)
|
||||
npm run typecheck # clean (frontend/)
|
||||
npm run lint # clean (pre-existing module warning only)
|
||||
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 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`
|
||||
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:
|
||||
- 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
|
||||
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.)
|
||||
`nostrconnect://` link from a Nostr app to sign for it. When the app sends a sign/decrypt
|
||||
request it appears under "Requests waiting for approval" — click **Approve** or **Reject**.
|
||||
(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`.
|
||||
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.
|
||||
|
||||
## Outstanding / next steps (if you continue)
|
||||
|
||||
- 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
|
||||
not implemented — only the signer role is). Live cross-client verification 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.
|
||||
not implemented — only the signer role is); a CLI-side approval escape hatch for `signer
|
||||
connect` (prompt on stdin) so approvals also work headlessly. Live cross-client verification
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue