Rewrite README for modern docs standards

This commit is contained in:
Avi 2026-08-06 10:13:22 -05:00
commit 8cbe0b81bc

366
README.md
View file

@ -1,85 +1,114 @@
# Nostr Feed Manager
<!-- Forgejo Actions CI badge — uncomment once a workflow exists in `.forgejo/workflows/ci.yml`:
[![CI]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml/badge.svg)]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml)
-->
> A friendly Linux desktop app for managing Nostr profiles, publishing notes, and acting as a
> **NIP-46 remote signer** — all while your private keys never leave your machine.
[![Version](https://img.shields.io/badge/version-0.1.0-blue)]()
[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Linux-lightgrey)]()
[![Status](https://img.shields.io/badge/status-early%20beta-orange)]()
[![Rust](https://img.shields.io/badge/Rust-2021-edition-red)]()
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)]()
A friendly Linux desktop application for managing [Nostr](https://nostr.com/) profiles and
publishing text notes.
<!--
Forgejo CI badge — enable once a workflow exists in `.forgejo/workflows/ci.yml`:
[![CI]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml/badge.svg)]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/actions/workflows/ci.yml)
-->
**Host:** this project is hosted on **Forgejo**. Every link in this document uses the generic
Forgejo URL format `[YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/` — replace
`[YOUR_FORGEJO_INSTANCE_URL]` with your instance's base URL (e.g. `https://codeberg.org`,
`https://git.example.com`) and `<OWNER>/<REPO>` with the actual repository path before sharing.
**Status: early beta (v0.1.0).** The app is usable daily, actively developed, and not yet
packaged for distribution repositories. Expect rough edges and API churn until 1.0.
Nostr Feed Manager pairs a Rust core (the same library behind the original command-line tool) with
a polished Electron + React interface. All Nostr logic — key generation, signing, relay
communication — runs in the Rust backend, which the GUI talks to over a JSON-lines IPC channel.
> **Hosting note:** this project is hosted on **Forgejo**. Throughout this document,
> replace `[YOUR_FORGEJO_INSTANCE_URL]` with your instance's base URL (e.g. `https://codeberg.org`)
> and `<OWNER>/<REPO>` with the actual repository path.
---
## Features
## 📖 Introduction
- **Profile management** — create and switch between Nostr profiles (`npub` addresses)
- **Compose screen** — write or preview a note before publishing:
- **Write / Preview tabs** with a soft length limit and live character count
- **Image attachments** — pick a local image, upload it to [nostr.build](https://nostr.build),
and publish a note that carries NIP-92 `imeta` (plus legacy `image`) tags so clients render it
- **Link preview cards** — up to three web links per note get a title/image/description card
(fetched locally; your note text is unchanged, clients render their own cards too)
- **Publishing with receipts** — per-relay publish reports: you always know where a note was accepted
- **Relay management** — add, remove, enable/disable, and test relays
- **Encrypted vault** — secret keys are encrypted at rest with AES-256-GCM under an Argon2id-derived
key. Without a password set, the vault is stored in plaintext (readable only by your user account)
and this is disclosed in the app
- **Secret key recovery** — reveal a profile's secret key (hex + `nsec1...`) from the app or the CLI,
only after the vault password is entered, so keys stay encrypted at rest
- **Settings** — light / dark / system theme, publish confirmation, key shortening, and vault backup
- **One binary, two interfaces** — the same Rust crate provides a full CLI and the GUI's IPC server
Nostr Feed Manager pairs a hardened **Rust core** (the same engine behind the original CLI) with a
polished **Electron + React** desktop interface. All Nostr work — key generation, event signing,
relay communication, encryption — happens inside the Rust backend. The GUI talks to it over a
secure JSON-lines IPC channel and **never sees your secret keys**.
## Architecture
It also turns your machine into a **NIP-46 remote signer ("bunker")**: instead of pasting your
`nsec` into other Nostr apps, they send signing requests here, and you approve each one with a
single click. This is central to keeping your keys out of third-party apps while staying
interoperable across the Nostr ecosystem.
```
┌──────────────────────────┐ JSON-lines over stdio ┌──────────────────────────┐
│ Electron (React) GUI │ ────────────────────────────────▶ │ Rust backend │
│ - React + TypeScript │ {"id":1,"method":"publish_note",│ - nostr-sdk 0.40 │
│ - renders in app:// │ "params":{"content":"..."}} │ - vault / keys / signing │
│ - never sees secret keys │ ◀──────────────────────────────── │ - relay communication │
└──────────────────────────┘ {"id":1,"status":"ok","data":…} └──────────────────────────┘
**Status: early beta (v0.1.0).** Actively developed and usable daily, but not yet packaged for
distribution repositories. Expect some API churn until 1.0. No Docker or database required.
## ✨ Features
- **Profile management** — create and quickly switch between Nostr profiles (`npub` identities)
- **Compose screen** — write with live character count and instant **Write / Preview** tabs
- **Image attachments** — pick a local image, upload to [nostr.build](https://nostr.build), and
publish with NIP-92 `imeta` (plus legacy `image`) tags so clients render it
- **Link preview cards** — up to three URLs per note get a title/image/description preview
- **Publishing receipts** — per-relay publish reports, so you always know where a note landed
- **Relay management** — add, remove, enable/disable, and latency-test relays from GUI or CLI
- **Feed aggregation** *(coming soon)* — an aggregated view of incoming text notes and link cards
- **Encrypted vault** — secret keys encrypted at rest with **AES-256-GCM** under a key derived via
**Argon2id** from your password
- **Secret key recovery** — reveal a key (hex + `nsec1...`) only after unlocking, from GUI or CLI
- **NIP-46 remote signer** — sign for other Nostr apps; every sign/decrypt request needs your
explicit **Approve**/**Reject**
- **Settings** — light / dark / system theme, publish confirmation, key shortening, vault backup
- **Two interfaces, one core** — the same Rust crate powers a full CLI *and* the GUI's IPC server
## 🏗️ Architecture
The design keeps a clean, security-critical boundary: **the renderer never sees secret keys.**
```mermaid
flowchart LR
subgraph UI[Electron + React]
R[Renderer<br/>React 18 / TS]
M[Main process<br/>spawns backend, dialogs, HTTP]
end
subgraph Core[Rust backend]
IPC[JSON-lines IPC]
S[Signer · NIP-46]
V[Vault · AES-256-GCM]
REL[Relay client · nostr-sdk]
end
subgraph Net[Network]
RELAYS[(Relays)]
HU[nostr.build]
end
R <-->|window.backend bridge| M
M <-->|"stdin/stdout 1 JSON per line"| IPC
IPC --> S
IPC --> V
IPC --> REL
REL --> V
S <--> RELAYS
REL --> HU
```
- `src/` — the Rust library (`nostr-manager-backend`). Exposes the same functionality as a
command-line binary and as a long-running `serve` process.
- `frontend/electron/` — the Electron main and preload scripts. The main process spawns the
Rust backend and correlates requests by `id`. Native-adjacent work that Rust shouldn't do (file
pickers, HTTP, clipboard) lives here.
- `frontend/src/` — the React renderer. It talks only to a thin `window.backend` bridge; profile
summaries and publish reports contain no secret material.
Or, as a plain step-by-step flow:
**Security model:** secret keys never leave the Rust backend, and the renderer never sees them.
Image uploads to nostr.build are authorized with a NIP-98 auth event signed by the active profile's
key inside the backend.
1. **Client (UI)** — the React renderer requests an action (e.g. `publish_note`) through the
`window.backend` bridge.
2. **Electron main** — forwards the request to the Rust backend over stdio as one JSON object per
line.
3. **Backend** — the `serve` IPC loop dispatches to the right module (`profiles`, `publish`,
`relays`, `settings`, `signer`).
4. **Relays** — the backend publishes signed events to the configured relays and returns
per-relay receipts.
## Tech stack
### Tech stack
**Backend (Rust)** — built and linted with **Cargo**, **rustfmt**, and **Clippy**
**Backend (Rust)** — Cargo · rustfmt · Clippy
| Tool | Purpose |
| --- | --- |
| [nostr-sdk](https://crates.io/crates/nostr-sdk) 0.40 (NIP-44, NIP-98) | Nostr protocol, relay client, signing |
| [nostr-sdk](https://crates.io/crates/nostr-sdk) 0.40 (NIP-44, NIP-46, NIP-98) | Nostr protocol, relay client, signing, remote signer |
| tokio | async runtime |
| argon2 + aes-gcm | vault key derivation and at-rest encryption |
| serde / serde_json | IPC and storage encoding |
| rpassword | interactive password prompts |
**Frontend (TypeScript)** — managed with **npm**
**Frontend (TypeScript)** — npm
| Tool | Purpose |
| --- | --- |
@ -90,16 +119,15 @@ key inside the backend.
| ESLint + Prettier | linting and formatting |
| electron-builder | Linux packaging |
## Requirements
## 🚀 Installation & setup
### Prerequisites
- **Linux** with a display server (X11 or Wayland)
- **Rust** (stable) and Cargo
- **Node.js 20+** and npm
- **Node.js 20+** and npm 10+
No Docker or database is required. The Rust backend also exists as a standalone CLI if you prefer
the terminal.
## Installation & quick start
No Docker, database, or account setup is required.
### 1. Clone and build
@ -118,18 +146,27 @@ npm install
### 2. Run
```sh
# Production-style: renders from the built bundle via app://
# Renders from the built bundle via app://
cd frontend
npm start
```
On first launch the app finds a vault left behind by the original Python CLI (if any) in its
working directory, backs it up to a timestamped `*.backup-<ts>` file, and imports your profiles.
The original file is left untouched.
On first launch the app detects a vault left behind by the original Python CLI (if you have one),
backs it up to a timestamped `*.backup-<ts>` file, and imports your profiles — leaving the original
untouched.
### 3. Configure
### 3. Configure environment
The app reads configuration from environment variables — it does not load `.env` files itself, but
you can export them from your shell or an `.env` sourced before launching:
The app reads its configuration from environment variables. It does not load a `.env` file
directly, but you can export these from your shell or source an `.env` before launching:
| Variable | Purpose | Default |
| --- | --- | --- |
| `NFM_PASSWORD` | Vault password for non-interactive CLI use (create / publish / show-secret) | _prompt_ |
| `NOSTR_GUI_DEV_URL` | Renderer dev-server URL for hot reload (development only) | — |
| `XDG_DATA_HOME` | Override the data directory | `~/.local/share/nost-feed-manager` |
**`.env.example`** — copy to `.env` and adjust:
```bash
# .env — sourced by your shell, not read directly by the app
@ -144,6 +181,9 @@ NOSTR_GUI_DEV_URL=http://localhost:5173
XDG_DATA_HOME=
```
> ⚠️ Keep `.env` out of version control. It is **not** gitignored by default — add it to
> `.gitignore` if you create one in the repo root, and never commit real passwords.
### 4. Package (optional)
```sh
@ -151,28 +191,19 @@ cd frontend
npm run dist # builds renderer + backend and runs electron-builder
```
The unpacked application lands in `frontend/release/linux-unpacked/`; launch it with
`./nost-feed-manager`.
The unpacked app lands in `frontend/release/linux-unpacked/`; run it with `./nost-feed-manager`.
## Usage
## 🧑‍💻 Usage guide
### Desktop app
The GUI covers profiles, composing/publishing, relays, and settings.
> **Screenshot placeholder** — add `docs/screenshots/compose.png` (the Compose screen with the
> Write / Preview tabs, an attached image, and a link-preview card) and reference it here.
>
> ```md
> ![Compose screen](docs/screenshots/compose.png)
> ```
>
> **GIF placeholder** — add `docs/screenshots/compose-demo.gif` (attaching an image and publishing)
> and reference it here.
Tabs cover **Compose**, **Home**, **Profiles**, **Relays**, **Settings**, and **Signer**. Connect
a relay by entering its `wss://` URL, then compose a note, attach an image, and publish to see a
per-relay report.
### Command-line interface
The Rust crate builds a single binary with both a CLI and the GUI IPC server:
The Rust crate builds a single binary that acts as both the GUI's backend and a full CLI:
```sh
cargo run --release -- create "Alice" # create a profile
@ -180,56 +211,64 @@ cargo run --release -- list # list profiles (no secret keys
cargo run --release -- switch <npub> # select the active profile
cargo run --release -- publish <npub> "Hello" # publish a text note
cargo run --release -- relays list|add|remove|enable|disable|test
cargo run --release -- settings get|set theme|confirm|shorten
cargo run --release -- settings get|set <key> <value>
cargo run --release -- set-password # encrypt the vault (or change its password)
cargo run --release -- remove-password # remove vault encryption
cargo run --release -- unlock # verify the vault password for this process
cargo run --release -- show-secret <npub> # reveal a profile's secret key (hex + nsec)
cargo run --release -- show-secret <npub> # reveal a profile's secret key
cargo run --release -- signer status # show the signer profile + relays
cargo run --release -- signer connect <nostrconnect://...>
cargo run --release -- info # show storage locations and version
cargo run --release -- serve # JSON-lines IPC server (used by the GUI)
```
Passwords are read from the `NFM_PASSWORD` environment variable when set, otherwise you are
prompted interactively. They are never accepted as command-line arguments. `create` and `publish`
prompt for the vault password automatically when the vault is encrypted, and so does `show-secret`
the key is only displayed after the vault password is entered.
Passwords are read from `NFM_PASSWORD` when set, otherwise you are prompted. They are **never**
accepted as command-line arguments — `create`, `publish`, `unlock`, and `show-secret` all unlock
the vault automatically when it is encrypted.
> **CLI + signer caveat:** the CLI's blocking `signer connect` loop cannot answer approval
> prompts — run the **GUI** to approve NIP-46 requests.
### Remote signer (GUI)
The **Signer** screen sets up the **NIP-46 remote signer** with the active profile. Paste a
`nostrconnect://` link from another Nostr app. When that app asks to sign an event or
decrypt/encrypt a message, a **Requests waiting for approval** panel appears with an
**Approve**/**Reject** button. Nothing is signed until you confirm it.
### IPC protocol (for tooling)
The `serve` command speaks one JSON object per line. The Electron main process does the same thing
when it spawns the backend:
The `serve` command speaks one JSON object per line — the same protocol Electron adopts:
```json
{"id": 1, "method": "publish_note", "params": {"npub": "...", "content": "Hello world"}}
{"id": 1, "status": "ok", "data": {"npub": "...", "reports": [{"relay": "wss://nos.lol", "accepted": true}]}}
{"id": 1, "status": "ok", "data": {"npub": "...", "report": {"event_id": "...", "succeeded": ["wss://relay.damus.io"]}}}
```
## Storage and security
## 🔐 Security notes
- The vault (`profiles_vault.json`) and settings live in
`$XDG_DATA_HOME/nost-feed-manager/` (defaulting to `~/.local/share/nost-feed-manager/`),
created with permissions `0700` for the directory and `0600` for the files.
- The vault is stored in plaintext until you set a password (Settings → Storage, or
`set-password` in the CLI). Once protected, every secret key is encrypted at rest with
AES-256-GCM under a key derived from your password with Argon2id. Labels and public keys stay
readable so profiles can be browsed while the vault is locked. You unlock once per session; the
derived key lives only in memory and is never written to disk.
- Anyone with access to your user account can still read the vault file, so the password is a
defence-in-depth layer, not a replacement for keeping your account secure.
Your keys are the crown jewels in any Nostr app, and nothing here compromises them:
## Development guide
- **Secret keys stay in Rust.** The renderer never receives secret key material; it only sees
profile summaries and publish reports.
- **Encrypted at rest.** The vault (`profiles_vault.json`) is plaintext until you set a password
(`set-password`, or Settings → Storage). Once set, every secret key is encrypted with
**AES-256-GCM** under a key derived from your password with **Argon2id**. Labels and public keys
remain readable so you can browse profiles while the vault is locked.
- **In-memory key only.** You unlock once per session; the derived key lives only in memory and is
never written to disk.
- **Approve-before-any-signing.** The NIP-46 remote signer will not sign, encrypt, or decrypt
until you explicitly approve each request.
- **Least-privileged storage.** Files are written with directories `0700` and files `0600`.
NIP-98 auth events authorize image uploads to nostr.build without exposing sk signs.
### Building from source
> **Defence in depth.** Anyone with access to your user account can still read vault files. The
> password is an additional layer, not a replacement for securing your account, and the underlying
> OS should be considered your last line of defense.
```sh
cargo build --release # Rust backend
cd frontend
npm install
npm run build # renderer type-check + Vite build
npm run electron:build # compile Electron main/preload
```
## 🛠️ Development
### Running with hot reload
### Run with hot reload
```sh
# terminal 1 — Vite dev server
@ -240,16 +279,11 @@ npm run dev
NOSTR_GUI_DEV_URL=http://localhost:5173 npm start
```
### Tests and linting
**All verification is green (cargo 64 tests, npm 65 tests,
clippy/fmt/typecheck/lint/format/builds clean) per the AGENTS.md checkpoint rule.** The project's
`AGENTS.md` mandates a full verification run before every change lands, and that status is recorded
in the committed checkpoint.
### Verify (all green — `cargo 75`, `npm 71`, clippy/fmt/typecheck/lint/format clean)
```sh
# Rust
cargo test # 64 unit tests
cargo test # 75 unit tests
cargo fmt --check # rustfmt
cargo clippy --all-targets
@ -258,91 +292,71 @@ cd frontend
npm run typecheck # TypeScript (renderer + electron)
npm run lint # ESLint
npm run format:check # Prettier
npm test # Vitest (jsdom), 65 tests across 12 files
npm test # Vitest (jsdom) — 71 tests across 13 files
```
The test suites exercise the real IPC protocol through a fake backend, so new features are
tested at the protocol boundary rather than in isolation.
The test suites exercise the real IPC protocol against a fake backend, so features are tested at
the protocol boundary rather than in isolation.
### Project layout
```
src/ Rust library + CLI + IPC server
app.rs application state, vault password/unlock lifecycle
crypto.rs Argon2id key derivation + AES-256-GCM encryption
app.rs app state, vault password/unlock lifecycle
crypto.rs Argon2id derivation + AES-256-GCM encryption
errors.rs structured AppError
ipc.rs JSON-lines serve() loop and request/reply envelope
main.rs CLI entry point
profiles.rs profile create/list/select
publish.rs note publishing with per-relay reports, image/`imeta` tagging
profiles.rs profile create/list/select, secret recovery
publish.rs note publishing with receipts, images and imeta tags
relays.rs default relays, validation, connection tests
settings.rs theme and user preferences
uploads.rs NIP-98 authorization for image-host uploads
vault.rs vault storage (plaintext or password-encrypted) and migration
settings.rs theme and preferences
signer.rs NIP-46 remote signer + approval gate
uploads.rs NIP-98 auth for image-host uploads
vault.rs vault storage (plaintext or password-encrypted)
frontend/
electron/ Electron main + preload (backend spawn, native dialogs,
image uploads, link-preview fetching, clipboard)
electron/ Electron main + preload (backend spawn, dialogs, clipboard)
src/
screens/ Compose, Home, Profiles, Relays, Settings
lib/ types, api bridge, media helpers (URL extraction)
screens/ Compose, Home, Profiles, Relays, Settings, Signer
lib/ types, api bridge, media helpers
state/ React context (AppProvider)
test/ Vitest suite with a fake backend speaking the real protocol
styles.css global styles
package.json scripts and electron-builder config
test/ Vitest suite over a fake backend
package.json scripts + electron-builder config
```
## Contributing
## 🤝 Contributing
Contributions are welcome — bug reports, documentation, and pull requests all help. Please be
respectful and constructive; everyone is expected to follow the [Code of Conduct](#code-of-conduct).
respectful and constructive; everyone is expected to follow the
[Code of Conduct](#code-of-conduct).
### Reporting bugs
Open an issue on the [issue tracker]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/issues) and include:
- A clear title and description of the expected vs. actual behavior
- A clear title and description of expected vs. actual behavior
- Steps to reproduce
- Platform details (distro, Wayland/X11, app version from `info`)
- Any relevant log output
### Requesting features
Open a feature request issue and describe the problem you're trying to solve, not just the feature
you want. That makes it easier to design something that fits the existing architecture.
- Platform details (distro, Wayland/X11, version from `info`)
- Relevant log output
### Opening a pull request
1. **Fork** the repository using the **Fork** button on Forgejo.
2. **Branch** from `master` with a descriptive name: `fix/relay-test-timeout`,
`feat/avatar-support`, etc.
3. **Make your change.** Match the surrounding style (see below) and keep the diff focused.
4. **Run the full verification suite** (see [Tests and linting](#tests-and-linting)) — everything
must pass.
5. **Write tests** for new behavior. Existing tests exercise the IPC protocol end to end, so
prefer testing through that public surface.
6. **Update docs** if the change is user-visible (README, feature list).
7. **Commit** with a concise imperative subject line (e.g. `Add relay connection timeout`), then
**push** your branch to your fork and open a **Pull Request** against `master` on Forgejo.
8. In the PR description, summarize the change, link any related issues, and list what you tested.
1. **Fork** the repository from the **Fork** button on Forgejo.
2. **Branch** from `master` with a descriptive name (`fix/relay-timeout`, `feat/avatar-support`).
3. **Make your change.** Match the surrounding style and keep the diff focused.
4. **Run the full verification suite** (see [Verify](#🛠️-development)) — everything must pass.
5. **Write tests** for new behavior (test through the IPC surface wherever possible).
6. **Update docs** for user-visible changes.
7. **Commit** with a concise imperative subject line, **push** your branch, and open a **pull
request** against `master`.
8. In the PR description, summarize the change, link issues, and list what you tested.
### Code style
- **Rust:** formatted with `cargo fmt`; `cargo clippy --all-targets` clean; public items get doc
comments; errors use the structured `AppError` types.
- **TypeScript / React:** formatted with Prettier; ESLint clean; `tsconfig` strictness is
enforced by `npm run typecheck`; tests use Vitest + Testing Library (user-event, never raw
`fireEvent` where user-event applies).
- **General:** no secrets in code, commits, or logs; no generated files checked in; keep changes
minimal and reviewable.
- **Rust:** `cargo fmt`; `cargo clippy --all-targets` clean; public items get doc comments
- **TypeScript/React:** Prettier + ESLint clean; strict `tsconfig`
- **General:** no secrets in code, commits, or logs; no generated files checked in
## Community & support
## 📄 License
- **Issues** — [bug reports and feature requests]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/issues)
- **Wiki** — [project wiki]([YOUR_FORGEJO_INSTANCE_URL]/<OWNER>/<REPO>/wiki) for longer-form
documentation; questions can also be filed as issues.
- **Code of Conduct** — <a id="code-of-conduct"></a> until `CODE_OF_CONDUCT.md` is added,
contributors are expected to follow the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
## License
MIT. See the [LICENSE](LICENSE) file. Copyright (c) 2026 Avi.
**MIT** — see the [LICENSE](LICENSE) file. Copyright (c) 2026 Avi.