Refresh checkpoint with feed aggregation

This commit is contained in:
Avi 2026-08-06 10:32:26 -05:00
commit 1fd057d3e1

View file

@ -1,4 +1,4 @@
# Checkpoint — NIP-46 remote signer + approval gate (2026-08-06)
# Checkpoint — Feed aggregation (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,102 +6,76 @@ verified green at the moment this file was written.
## Where things are
- Project: `/home/avi/Projects/skills/nost-feed-manager`
- 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 spans `73cb08d` (the signer itself) plus `ca3203c` (the approval gate).
- Git repo: `master` @ `37a9d94` ("Add feed aggregation from enabled relays"), on top of the
NIP-46 signer work (`8cbe0b8` README, `ca3203c` approval gate, `73cb08d` signer) described in
earlier checkpoints.
- The working tree is **clean** — this session's feed feature and its checkpoint are committed.
- The feed feature spans the Rust `src/feed.rs` module plus IPC/CLI wiring and the frontend
Feed screen.
## What was completed: NIP-46 remote signer (bunker role)
## What was completed: feed aggregation
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. 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.
The app now has a **Feed screen** (sidebar nav item with a list icon) that shows a read-only,
newest-first list of recent text notes aggregated from the enabled relays. Reading never touches
the user's stored keys: a throwaway identity is used, and the feed never signs or publishes.
**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()`, `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`.
- **Backend** (`src/feed.rs`): `FeedItem` (note id, author hex + npub, content, created_at,
relays seen on), `aggregate_feed(settings, limit)` which connects to every enabled relay,
subscribes to kind 1 notes newer than 24h (capped at `limit`, default 50), collects for up to
15s, then returns de-duplicated, newest-first items. `FeedBuilder` dedupes events across relays
(tracking the extra relay sources) and stops early once the cap is reached. Empty input (no
relays enabled, or no notes) returns an empty list, not an error. Unit tests cover no-relay
empty feed, newest-first sorting, cross-relay dedupe, kind filtering, and the fill cap.
- **IPC** (`src/ipc.rs`): new `Request::FeedGet { limit }` → returns the aggregated items. Needs
no vault unlock, so the feed works even when the vault is locked.
- **CLI** (`src/main.rs`): `feed [limit]` prints a one-line preview per note (content, npub,
timestamp); a friendly "No notes found…" message when the feed is empty.
- **Electron/React frontend**: the **Feed screen** (`FeedScreen.tsx`, wired in `App.tsx` as the
`'feed'` screen). Shows author avatar/npub, content, relative date, a "2 relays" badge for
notes seen on multiple relays, and a **Refresh** button. Empty states for "no relays enabled"
(with a button to jump to the Relays screen) and "no notes found", plus a clear error alert on
failure. Wired through `api.ts` (`feedGet`), `AppProvider`, `navigation.ts` (`'feed'`), and the
sidebar (`Icon 'list'`).
## Commits this session (newest first)
- `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.
- `37a9d94` "Add feed aggregation from enabled relays" — the full feed feature: `src/feed.rs`,
IPC `FeedGet`, CLI `feed`, the frontend Feed screen + tests, and the README update.
## How it was verified (all green)
```
cargo test # 75 passed
cargo test # 81 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 # 71 passed (13 files)
npm test # 75 passed (14 files)
npm run electron:build # compiles the Electron main process
npm run build # rebuilds the React bundle (dist/)
```
CLI smoke checks run earlier in the session: `signer status` printed the active profile + relays;
a `bunker://` link and a non-`nostrconnect` URI were both rejected with clear errors.
## How to resume
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
`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. 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.
`37a9d94` at the top.
3. To try the feed:
- GUI: `cd frontend && npm start`, then the **Feed** screen in the sidebar. It lists recent
notes from the enabled relays; use **Refresh** to re-query. (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 feed 10` prints
up to 10 recent notes from the enabled relays.
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); 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.
- The feed currently shows the *global* stream from the enabled relays. A natural follow-up is a
**contact-aware feed**: fetch the active profile's kind 3 contact list and filter the feed to
those authors (and optionally show link-card previews per note, as hinted in the original
"feed aggregation" description).
- Live end-to-end check of the feed against public relays (network call) has not been run in this
session; the unit + frontend tests cover the deterministic parts.