Add open-source README and MIT license

This commit is contained in:
Avi 2026-08-04 16:28:34 -05:00
commit dbc09745da
2 changed files with 256 additions and 55 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Avi <avi@aiolabs.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

290
README.md
View file

@ -1,24 +1,43 @@
# Nostr Feed Manager # Nostr Feed Manager
A friendly Linux desktop application for managing Nostr profiles and publishing text notes. <!-- CI badge — uncomment and replace <OWNER>/<REPO> once CI (e.g. GitHub Actions) is configured:
[![CI](https://github.com/<OWNER>/<REPO>/actions/workflows/ci.yml/badge.svg)](https://github.com/<OWNER>/<REPO>/actions/workflows/ci.yml)
-->
[![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)]()
It pairs a Rust core (the same library behind the original command-line tool) with a polished A friendly Linux desktop application for managing [Nostr](https://nostr.com/) profiles and
Electron + React interface. All Nostr logic — key generation, signing, relay communication — publishing text notes.
runs in the Rust backend, which the GUI talks to over a JSON-lines IPC channel.
**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.
---
## Features ## Features
- Create and switch between Nostr profiles (`npub` addresses) - **Profile management** — create and switch between Nostr profiles (`npub` addresses)
- Publish short text notes to the Nostr network - **Compose screen** — write or preview a note before publishing:
- Per-relay publish reports: you always know where a note was accepted - **Write / Preview tabs** with a soft length limit and live character count
- Add, remove, enable/disable, and test relays - **Image attachments** — pick a local image, upload it to [nostr.build](https://nostr.build),
- Light / dark / system theme, configurable publish confirmation and key shortening and publish a note that carries NIP-92 `imeta` (plus legacy `image`) tags so clients render it
- Back up your vault from the UI - **Link preview cards** — up to three web links per note get a title/image/description card
- Password-protected vault: secret keys are encrypted at rest with AES-256-GCM under an (fetched locally; your note text is unchanged, clients render their own cards too)
Argon2id-derived key. Without a password set, the vault is stored in plaintext (readable only by - **Publishing with receipts** — per-relay publish reports: you always know where a note was accepted
your user account) and this is disclosed in the app - **Relay management** — add, remove, enable/disable, and test relays
- Reveal a profile's secret key (hex + `nsec1...`) from the app or the CLI — only after the vault - **Encrypted vault** — secret keys are encrypted at rest with AES-256-GCM under an Argon2id-derived
password is entered, so keys stay encrypted at rest 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
## Architecture ## Architecture
@ -34,53 +53,119 @@ runs in the Rust backend, which the GUI talks to over a JSON-lines IPC channel.
- `src/` — the Rust library (`nostr-manager-backend`). Exposes the same functionality as a - `src/` — the Rust library (`nostr-manager-backend`). Exposes the same functionality as a
command-line binary and as a long-running `serve` process. command-line binary and as a long-running `serve` process.
- `frontend/electron/` — the Electron main and preload scripts. The main process spawns the - `frontend/electron/` — the Electron main and preload scripts. The main process spawns the
Rust backend and correlates requests by `id`. Rust backend and correlates requests by `id`. Native-adjacent work that Rust shouldn't do (file
- `frontend/src/` — the React renderer. It talks only to a thin `window.backend` bridge; pickers, HTTP, clipboard) lives here.
profile summaries and publish reports contain no secret material. - `frontend/src/` — the React renderer. It talks only to a thin `window.backend` bridge; profile
summaries and publish reports contain no secret material.
**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.
## Tech stack
**Backend (Rust)**
| Tool | Purpose |
| --- | --- |
| [nostr-sdk](https://crates.io/crates/nostr-sdk) 0.40 (NIP-44, NIP-98) | Nostr protocol, relay client, signing |
| 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)**
| Tool | Purpose |
| --- | --- |
| Electron 33 | desktop shell, native dialogs, IPC host |
| React 18 + TypeScript | renderer |
| Vite 5 | build tool + dev server |
| Vitest + Testing Library | renderer tests against a fake backend |
| ESLint + Prettier | linting and formatting |
| electron-builder | Linux packaging |
## Requirements ## Requirements
- Rust (stable) and Cargo - **Linux** with a display server (X11 or Wayland)
- Node.js 20+ and npm - **Rust** (stable) and Cargo
- Linux with a display server (X11 or Wayland) - **Node.js 20+** and npm
The Rust backend also exists as a standalone CLI if you prefer the terminal. No Docker or database is required. The Rust backend also exists as a standalone CLI if you prefer
the terminal.
## Building and running the GUI ## Installation & quick start
### 1. Clone and build
```sh ```sh
# 1. Build the Rust backend (release) git clone https://github.com/<OWNER>/<REPO>.git
cd nost-feed-manager
# Build the Rust backend (release)
cargo build --release cargo build --release
# 2. Install frontend dependencies # Install frontend dependencies
cd frontend cd frontend
npm install npm install
```
# 3. Run the app (production-style, served from the built bundle via app://) ### 2. Run
```sh
# Production-style: renders from the built bundle via app://
npm start npm start
``` ```
During development you can use Vite's live-reloading renderer instead: 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.
```sh ### 3. Configure
# terminal 1
npm run dev # starts Vite on http://localhost:5173
# terminal 2 The app reads configuration from environment variables — it does not load `.env` files itself, but
NOSTR_GUI_DEV_URL=http://localhost:5173 npm start you can export them from your shell or an `.env` sourced before launching:
```bash
# .env — sourced by your shell, not read directly by the app
# Vault password for non-interactive CLI use (create / publish / show-secret)
NFM_PASSWORD=
# Renderer dev-server URL for hot reload (development only)
NOSTR_GUI_DEV_URL=http://localhost:5173
# Override the data directory (defaults to ~/.local/share/nost-feed-manager)
XDG_DATA_HOME=
``` ```
### Packaging ### 4. Package (optional)
```sh ```sh
cd frontend cd frontend
npm run dist # builds the renderer + backend and runs electron-builder npm run dist # builds renderer + backend and runs electron-builder
``` ```
The unpacked application lands in `frontend/release/linux-unpacked/`; launch it with The unpacked application lands in `frontend/release/linux-unpacked/`; launch it with
`./nost-feed-manager`. `./nost-feed-manager`.
## Command-line usage ## Usage
### 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.
### 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 with both a CLI and the GUI IPC server:
@ -104,38 +189,72 @@ prompted interactively. They are never accepted as command-line arguments. `crea
prompt for the vault password automatically when the vault is encrypted, and so does `show-secret` 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. the key is only displayed after the vault password is entered.
## Storage and migration ### 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:
```json
{"id": 1, "method": "publish_note", "params": {"npub": "...", "content": "Hello world"}}
{"id": 1, "status": "ok", "data": {"npub": "...", "reports": [{"relay": "wss://nos.lol", "accepted": true}]}}
```
## Storage and security
- The vault (`profiles_vault.json`) and settings live in - The vault (`profiles_vault.json`) and settings live in
`$XDG_DATA_HOME/nost-feed-manager/` (defaulting to `~/.local/share/nost-feed-manager/`), `$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. created with permissions `0700` for the directory and `0600` for the files.
- The original CLI saved `profiles_vault.json` in its working directory. On first launch this
app finds that file, copies it to a timestamped `*.backup-<ts>` next to it, and imports your
profiles into the new location. The original file is left untouched.
- The vault is stored in plaintext until you set a password (Settings → Storage, or - 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 `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 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; readable so profiles can be browsed while the vault is locked. You unlock once per session; the
the derived key lives only in memory and is never written to disk. Anyone with access to your derived key lives only in memory and is never written to disk.
user account can still read the vault file, so the password is a defence-in-depth layer, not a - Anyone with access to your user account can still read the vault file, so the password is a
replacement for keeping your account secure. defence-in-depth layer, not a replacement for keeping your account secure.
## Development ## Development guide
### Building from source
```sh ```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
```
### Running with hot reload
```sh
# terminal 1 — Vite dev server
cd frontend
npm run dev
# terminal 2 — Electron pointed at the dev server
NOSTR_GUI_DEV_URL=http://localhost:5173 npm start
```
### Tests and linting
All checks must pass before merging (the test suites exercise the real IPC protocol through a
fake backend):
```sh
# Rust
cargo test # 64 unit tests
cargo fmt --check
cargo clippy --all-targets
# Frontend
cd frontend cd frontend
npm run typecheck # TypeScript (renderer + electron) npm run typecheck # TypeScript (renderer + electron)
npm run lint # ESLint npm run lint # ESLint
npm run format:check # Prettier npm run format:check # Prettier
npm test # Vitest (jsdom), including IPC-level fake backend tests npm test # Vitest (jsdom), 65 tests across 12 files
cd ..
cargo test # Rust unit tests
cargo fmt --check # formatting
cargo clippy --all-targets
``` ```
## Project layout ### Project layout
``` ```
src/ Rust library + CLI + IPC server src/ Rust library + CLI + IPC server
@ -145,13 +264,74 @@ src/ Rust library + CLI + IPC server
ipc.rs JSON-lines serve() loop and request/reply envelope ipc.rs JSON-lines serve() loop and request/reply envelope
main.rs CLI entry point main.rs CLI entry point
profiles.rs profile create/list/select profiles.rs profile create/list/select
publish.rs note publishing with per-relay reports publish.rs note publishing with per-relay reports, image/`imeta` tagging
relays.rs default relays, validation, connection tests relays.rs default relays, validation, connection tests
settings.rs theme and user preferences 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 vault.rs vault storage (plaintext or password-encrypted) and migration
frontend/ frontend/
electron/ Electron main + preload (backend spawn, IPC, clipboard) electron/ Electron main + preload (backend spawn, native dialogs,
src/ React app (components, screens, state, styles) image uploads, link-preview fetching, clipboard)
src/test/ Vitest suite with a fake backend speaking the real protocol src/
screens/ Compose, Home, Profiles, Relays, Settings
lib/ types, api bridge, media helpers (URL extraction)
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 package.json scripts and electron-builder config
``` ```
## 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).
### Reporting bugs
Open an issue on the [issue tracker](https://github.com/<OWNER>/<REPO>/issues) and include:
- A clear title and description of the 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.
### Opening a pull request
1. **Fork** the repository on GitHub.
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** and open a pull request against `master`.
8. In the PR description, summarize the change, link any related 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.
## Community & support
- **Issues** — [bug reports and feature requests](https://github.com/<OWNER>/<REPO>/issues)
- **Discussions** — [questions and ideas](https://github.com/<OWNER>/<REPO>/discussions)
- **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.