diff --git a/README.md b/README.md index a12206d..ce562fc 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,114 @@ # Nostr Feed Manager - +> 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. + -**Host:** this project is hosted on **Forgejo**. Every link in this document uses the generic -Forgejo URL format `[YOUR_FORGEJO_INSTANCE_URL]///` — replace -`[YOUR_FORGEJO_INSTANCE_URL]` with your instance's base URL (e.g. `https://codeberg.org`, -`https://git.example.com`) and `/` 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 `/` 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
React 18 / TS] + M[Main process
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-` 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-` 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 # select the active profile cargo run --release -- publish "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 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 # reveal a profile's secret key (hex + nsec) +cargo run --release -- show-secret # reveal a profile's secret key +cargo run --release -- signer status # show the signer profile + relays +cargo run --release -- signer connect 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,109 +279,84 @@ 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 fmt --check # rustfmt +cargo test # 75 unit tests +cargo fmt --check # rustfmt cargo clippy --all-targets # Frontend 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 run typecheck # TypeScript (renderer + electron) +npm run lint # ESLint +npm run format:check # Prettier +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]///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]///issues) -- **Wiki** — [project wiki]([YOUR_FORGEJO_INSTANCE_URL]///wiki) for longer-form - documentation; questions can also be filed as issues. -- **Code of Conduct** — 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. \ No newline at end of file