Refresh checkpoint with NIP-46 remote signer

This commit is contained in:
Avi 2026-08-05 19:28:38 -05:00
commit 593fc6af8b

View file

@ -1,4 +1,4 @@
# Checkpoint — Compose preview + open-source docs (2026-08-04)
# Checkpoint — NIP-46 remote signer (2026-08-05)
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,119 +6,82 @@ verified green at the moment this file was written.
## Where things are
- Project: `/home/avi/Projects/skills/nost-feed-manager`
- Git repo: `master` @ `3eb9fa9` ("Rewrite README for Forgejo hosting"), on top of
`dbc0974` ("Add open-source README and MIT license"), `46d5da7` ("Point checkpoint at current
HEAD"), `9ec01c9` ("Add AGENTS.md checkpoint workflow rule"), `a052851` (checkpoint refresh),
`d677125` ("Show preview images and link cards in Compose"), `03f6877` (NIP-98 upload auth),
`3e3467b` (compose preview + attachments), `17e8ace` (Profiles subtitle copy), and
`8eb6685` (reveal-secret-key).
- 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.
- The working tree is **clean** — everything from this session is committed.
- `AGENTS.md` now records the standing rule: every addition must end with an up-to-date checkpoint.
- `README.md` is an open-source-ready document rewritten for **Forgejo** hosting (no GitHub
references; clone/issues/wiki links use the `[YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>` Forgejo
format; no git remote is configured in the repo yet, so the instance URL is a labeled
placeholder). The new `LICENSE` file grants MIT (Copyright (c) 2026 Avi). The vault is gitignored,
so no secret material is tracked.
- `/home/avi/Projects/nostr_backend/nostr_backendmanager.md` (old-CLI docs) has been updated
to match reality; it lives outside this repo so it is not part of any commit.
- `/home/avi/Projects/nostr_backend/vlog-website/` (separate, untouched).
- The signer feature is split across one commit (`73cb08d`) that contains both the Rust backend
and the Electron/React frontend.
## What was completed: compose preview + image attachments
## What was completed: NIP-46 remote signer (bunker role)
The Compose screen now has a **Write / Preview** tab split. The preview renders the note as it
will appear (profile avatar + npub, the text, and any images), without publishing anything.
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.
- **Photos from hyperlinks**: pasting an image URL (`.jpg/.jpeg/.png/.gif/.webp/.avif`) anywhere in
the note shows the picture in the preview. On publish, the backend detects the same URLs and tags
the event with NIP-92 `imeta` (`url`, `m`) plus the legacy `image` tag, so both new and older
Nostr clients render the images. Detection is by extension only — nothing is downloaded to decide.
- **Attach images**: an "Attach image" button opens the native file dialog (multi-select),
uploads each file to nostr.build (anonymous `POST /api/v2/upload/files`, field `fileToUpload`),
and shows a removable thumbnail chip. The hosted URLs are appended to the note on publish.
- **Where upload lives**: `pick_image` / `upload_image` are handled by the Electron main process
(native dialog + `fetch`), not the Rust backend — they need a file dialog and a one-off HTTP
upload. The renderer still calls them through the same `window.backend.request` envelope.
- The reveal-secret-key feature from the previous commit (`8eb6685`) is unchanged and still works.
- **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.
## Commits this session (newest first)
- `3eb9fa9` "Rewrite README for Forgejo hosting" — README now targets Forgejo: zero GitHub URLs;
clone / issues / wiki links use the generic Forgejo format `[YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>`
with the placeholder labeled as such (repo has no git remote configured, so the instance URL
couldn't be auto-detected). Forgejo Actions CI badge left as a commented placeholder. Full
verification suite re-run green (docs-only change): cargo 64, npm 65, clippy/fmt/typecheck/
lint/format/builds clean.
- `dbc0974` "Add open-source README and MIT license" — `README.md` rewritten for a technical /
open-source audience (badges, feature list incl. compose preview + media, tech-stack tables,
quick start + `.env` example, CLI/IPC usage, screenshots placeholders, dev guide, contributing,
community, license). New `LICENSE` (MIT, Copyright (c) 2026 Avi). Docs-only change; full
verification suite re-run green anyway (cargo 64, npm 65, all lint/format/builds clean).
- `46d5da7` "Point checkpoint at current HEAD" — checkpoint aligned with HEAD before the docs work.
- `9ec01c9` "Add AGENTS.md checkpoint workflow rule" — new `AGENTS.md` records that every addition
must end with an up-to-date checkpoint.
- `a052851` "Refresh checkpoint with preview image/link fixes"
- `d677125` "Show preview images and link cards in Compose" — two fixes from GUI testing:
1. Remote images never rendered because the CSP was `img-src 'self' data:` — added `https:`
(`frontend/index.html`). Attached images and pasted image URLs now appear in the preview.
2. Link previews: a pasted web-page link (not a direct image URL) now fetches the page in the
Electron main process (`link_preview` IPC) and shows an OpenGraph/Twitter card (image, title,
description, site, hostname) in the preview. New `src/lib/media.ts::extractLinkUrls`,
`LinkPreview` type, `api.linkPreview`, `AppProvider.linkPreview`, `LinkPreviewCard` in
ComposeScreen, `styles.css` card styles. Publish is unchanged — link cards are a preview-only
feature (Nostr clients render their own link previews from the URL in the note).
Tests: `media.test.ts` (new), `ComposeScreen.test.tsx`, `apiMock.ts`, `fakeBackend.ts`.
- `03f6877` "Sign NIP-98 auth for nostr.build image uploads" — fixes the attach-image flow, which
failed with "Unauthorized, please provide a valid nip-98 token". nostr.build now requires a
NIP-98 `Authorization` header. Backend: new `src/uploads.rs` (`nip98_authorization` signs a kind
27235 auth event with the active profile's key and returns `Nostr <base64>`), IPC method
`upload_auth { url, http_method }`, `nostr-sdk` feature `nip98` added. Electron:
`frontend/electron/main.ts` fetches the header from the backend and sends it on the upload.
Verified live: throwaway profile → IPC `upload_auth``curl` upload to nostr.build returned
`status: success` (temp data cleaned up).
- `3e3467b` "Add compose preview with image attachments (NIP-92 imeta)" — 11 files, 1 new.
Backend: `src/publish.rs` (`extract_image_urls`, `image_mime_from_url`, `image_tags` + tests).
Electron: `frontend/electron/main.ts` (`pickImage`, `uploadImage`, routed from `backend:request`).
Renderer: `frontend/src/lib/media.ts` (new), `api.ts`, `types.ts`, `AppProvider.tsx`,
`ComposeScreen.tsx`, `styles.css`. Tests: `ComposeScreen.test.tsx`, `apiMock.ts`, `fakeBackend.ts`
(adds request recording + `pick_image`/`upload_image`).
- `17e8ace` "Clarify the Profiles subtitle about private keys" — `ProfilesScreen.tsx` copy.
- `8eb6685` "Add reveal-secret-key after unlock (CLI + GUI)" — previous feature, see history.
- `73cb08d` "Add NIP-46 remote signer (external signing)" — everything above in one commit.
Full verification suite re-run green (below).
## How it was verified (all green)
```
cargo test # 64 passed
cargo test # 70 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 # 65 passed (12 files)
npm test # 69 passed (13 files)
npm run electron:build # compiles the Electron main process
npm run build # rebuilds the React bundle (dist/)
```
Plus live checks: a throwaway-profile `upload_auth` + `curl` upload to nostr.build returned
`status: success`; the link-preview parser (same code as Electron) fetched Wikipedia's og:title +
og:image and example.com's title correctly. Temp files cleaned up.
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 the three commits.
3. To try it:
- GUI: `cd frontend && npm start` — Compose → "Attach image" or paste an image URL → Preview tab.
(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 list` and
`./target/release/nostr-manager-backend show-secret <npub>` (no angle brackets).
2. State is committed: `git status` should be clean; `git log --oneline -5` shows
`73cb08d` 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.)
- CLI: `cargo build --release`, then `./target/release/nostr-manager-backend signer status`.
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); the upload uses the active profile's key for NIP-98 (so an encrypted, locked vault
needs unlocking before attaching an image — the attach error explains this); relay defaults in code
still point at `relay.damus.io` (503 upstream) and `relay.nostr.band` (timeout) — user's local
settings already use `nos.lol` + `relay.primal.net` instead.
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.