Add open-source README and MIT license
This commit is contained in:
parent
46d5da71b0
commit
dbc09745da
2 changed files with 256 additions and 55 deletions
21
LICENSE
Normal file
21
LICENSE
Normal 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
290
README.md
|
|
@ -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:
|
||||||
|
[](https://github.com/<OWNER>/<REPO>/actions/workflows/ci.yml)
|
||||||
|
-->
|
||||||
|
[]()
|
||||||
|
[](LICENSE)
|
||||||
|
[]()
|
||||||
|
[]()
|
||||||
|
|
||||||
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
|
||||||
|
> 
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> **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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue