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 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,102 +6,76 @@ 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` @ `ca3203c` ("Require user approval for NIP-46 sign/encrypt requests"), - Git repo: `master` @ `37a9d94` ("Add feed aggregation from enabled relays"), on top of the
on top of `593fc6a` ("Refresh checkpoint with NIP-46 remote signer"), `73cb08d` ("Add NIP-46 NIP-46 signer work (`8cbe0b8` README, `ca3203c` approval gate, `73cb08d` signer) described in
remote signer (external signing)"), `13efee6` ("Refresh checkpoint with Forgejo README"), earlier checkpoints.
`3eb9fa9` ("Rewrite README for Forgejo hosting"), and the history described in earlier - The working tree is **clean** — this session's feed feature and its checkpoint are committed.
checkpoints. - The feed feature spans the Rust `src/feed.rs` module plus IPC/CLI wiring and the frontend
- The working tree is **clean** — everything from this session is committed. Feed screen.
- The signer feature spans `73cb08d` (the signer itself) plus `ca3203c` (the approval gate).
## 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 The app now has a **Feed screen** (sidebar nav item with a list icon) that shows a read-only,
pasting your nsec into other apps, they send signing requests to this app, which approves them newest-first list of recent text notes aggregated from the enabled relays. Reading never touches
with the active profile's keys. The signer runs **inside the `serve` process as a concurrent the user's stored keys: a throwaway identity is used, and the feed never signs or publishes.
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 - **Backend** (`src/feed.rs`): `FeedItem` (note id, author hex + npub, content, created_at,
user approval.** The peer is still "trusted" after the handshake, but `sign_event`, relays seen on), `aggregate_feed(settings, limit)` which connects to every enabled relay,
`nip44_encrypt`, and `nip44_decrypt` are no longer auto-approved. Each such request is parked in a subscribes to kind 1 notes newer than 24h (capped at `limit`, default 50), collects for up to
`pending` queue, exposed in `SignerStatus.pending`, and only runs after the user clicks 15s, then returns de-duplicated, newest-first items. `FeedBuilder` dedupes events across relays
**Approve** on the Signer screen (or is answered with **Reject** / a 300-second timeout). All (tracking the extra relay sources) and stops early once the cap is reached. Empty input (no
non-key methods (`connect`, `get_public_key`, `ping`, `get_relays`, `switch_relays`, `logout`) relays enabled, or no notes) returns an empty list, not an error. Unit tests cover no-relay
still run immediately. 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
- **Backend** (`src/signer.rs`): a hand-rolled NIP-46 implementation (the SDK has no ready-made no vault unlock, so the feed works even when the vault is locked.
NIP-46 server). `Signer` exposes `status()`, `connect()`, `disconnect()`, `fail()`, - **CLI** (`src/main.rs`): `feed [limit]` prints a one-line preview per note (content, npub,
`set_connected()`, `approve()`, and `await_approval()`. `parse_connect_uri` accepts timestamp); a friendly "No notes found…" message when the feed is empty.
`nostrconnect://` links (and clearly rejects the opposite-role `bunker://` scheme). The async - **Electron/React frontend**: the **Feed screen** (`FeedScreen.tsx`, wired in `App.tsx` as the
`run_sign_task` connects over the chosen relays, listens for kind 24133 requests decrypted with `'feed'` screen). Shows author avatar/npub, content, relative date, a "2 relays" badge for
a NIP-44 v2 conversation key, and answers `get_public_key`, `sign_event`, and notes seen on multiple relays, and a **Refresh** button. Empty states for "no relays enabled"
`nip44_encrypt`/`nip44_decrypt`. Responses are NIP-44-encrypted and published back to the peer. (with a button to jump to the Relays screen) and "no notes found", plus a clear error alert on
Key-using methods route through `gated_response``await_approval` (a `HashMap` of pending failure. Wired through `api.ts` (`feedGet`), `AppProvider`, `navigation.ts` (`'feed'`), and the
entries, each with a `oneshot` decision channel) → `approved_response`, while everything else sidebar (`Icon 'list'`).
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) ## Commits this session (newest first)
- `ca3203c` "Require user approval for NIP-46 sign/encrypt requests" — the approval gate - `37a9d94` "Add feed aggregation from enabled relays" — the full feed feature: `src/feed.rs`,
(backend pending queue + `SignerApprove` IPC + Signer screen Approve/Reject + tests). IPC `FeedGet`, CLI `feed`, the frontend Feed screen + tests, and the README update.
- `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 # 75 passed cargo test # 81 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 # 71 passed (13 files) npm test # 75 passed (14 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/)
``` ```
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 ## How to resume
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
`ca3203c` at the top. `37a9d94` at the top.
3. To try the signer: 3. To try the feed:
- GUI: `cd frontend && npm start`, then the **Signer** screen in the sidebar. Paste a - GUI: `cd frontend && npm start`, then the **Feed** screen in the sidebar. It lists recent
`nostrconnect://` link from a Nostr app to sign for it. When the app sends a sign/decrypt notes from the enabled relays; use **Refresh** to re-query. (Note: `npm start` alone does
request it appears under "Requests waiting for approval" — click **Approve** or **Reject**. NOT rebuild the React bundle — run `npm run build` first, or use `npm run dev` +
(Note: `npm start` alone does NOT rebuild the React bundle — run `npm run build` first, or `NOSTR_GUI_DEV_URL=http://localhost:5173 npm start` for live reload.)
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
- CLI: `cargo build --release`, then `./target/release/nostr-manager-backend signer status`. up to 10 recent notes from the enabled relays.
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 - The feed currently shows the *global* stream from the enabled relays. A natural follow-up is a
publication); a NIP-46 **client** role (initiating `bunker://` from this app is intentionally **contact-aware feed**: fetch the active profile's kind 3 contact list and filter the feed to
not implemented — only the signer role is); a CLI-side approval escape hatch for `signer those authors (and optionally show link-card previews per note, as hinted in the original
connect` (prompt on stdin) so approvals also work headlessly. Live cross-client verification "feed aggregation" description).
against an actual Nostr app (e.g. a NIP-46-compatible client that produces a `nostrconnect://` - Live end-to-end check of the feed against public relays (network call) has not been run in this
link) has not yet been done end-to-end, only the unit and CLI smoke tests. session; the unit + frontend tests cover the deterministic parts.