From 2314265886fa34783f718b0bda44447c0132d1a1 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sat, 18 Jul 2026 17:49:50 -0400 Subject: [PATCH] . --- README.md | 338 ++++++++++++++++++++++----------------------- install_upgrade.sh | 111 +++++++++++++++ 2 files changed, 280 insertions(+), 169 deletions(-) create mode 100755 install_upgrade.sh diff --git a/README.md b/README.md index 27bea05..1fd5667 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,136 @@ # Nostr Publish -A Codium / VSCode extension to publish files to Nostr. +A Codium / VSCode extension to publish files to [Nostr](https://github.com/nostr-protocol/nostr). -## Phases +Publish markdown articles as long-form notes, broadcast raw Nostr events from JSON, and sign everything with either a local key or a remote [`n_signer`](https://laantungir.net/git/laantungir/n_signer.git) hardware signer. -- **Phase 0**: Publish raw unsigned Nostr events from a `.json` file. - Generate a skeleton event, fill in the fields, validate, sign, and broadcast. -- **Phase 1**: Publish `.md` files as NIP-23 long-form notes - (kind `30023`) with YAML front-matter metadata. -- **Phase 2**: Delegate signing to a running - [`n_signer`](https://laantungir.net/git/laantungir/n_signer.git) process - over a Linux abstract Unix socket. +## Features -See [`plans/`](plans/) for the full design: -- [`plans/phase0-raw-event-publishing.md`](plans/phase0-raw-event-publishing.md) -- [`plans/longform-metadata-strategy.md`](plans/longform-metadata-strategy.md) -- [`plans/implementation-plan.md`](plans/implementation-plan.md) +- **Publish markdown as long-form notes** — Write a `.md` file with YAML front-matter (title, summary, image, tags), click one button, and it's broadcast as a NIP-23 kind-30023 event to your relays. +- **Publish raw events from JSON** — Create a Nostr event skeleton (text note, relay list, profile, contacts, or custom kind), fill in the fields, and publish. +- **Multiple signing backends**: + - **Local** — enter your `nsec` or hex key; held in memory only, never written to disk. + - **n_signer (Unix socket)** — delegate signing to a local `n_signer` process over an abstract Unix socket. Your key never leaves the signer. + - **n_signer (Qubes qrexec)** — reach `n_signer` in another qube via Qubes OS secure IPC. + - **n_signer (TCP)** — reach `n_signer` over TCP (FIPS mesh, remote machine). Requires a caller key for auth envelopes. +- **Sidebar GUI** — sign in, manage relays, create events, and publish — all from the Nostr sidebar in the Activity Bar. +- **NIP-65 relay auto-discovery** — on sign-in, fetches your kind 10002 relay list and uses your own relays for publishing. +- **Avatar display** — fetches your kind 0 profile and shows your avatar + name in the sidebar. +- **`published_at` tracking** — first-publish timestamps are stored per-workspace so updates preserve the original timestamp (NIP-23). -## Prerequisites +## Install -1. **Node.js** ≥ 18. -2. **The laantungir fork of `nostr-tools`** must be cloned as a sibling - directory and built: +### Prerequisites - ```bash - git clone git@laantungir.net:laantungir/nostr-tools.git ../nostr-tools - cd ../nostr-tools - npm install --ignore-scripts # the prepublish hook needs `just`, which we skip - rm -rf lib && bun run build.js # or: node build.js - npx tsc # produces lib/types/*.d.ts +- **Codium** or **VSCode** (≥ 1.85). +- **Node.js** ≥ 18 (for building only — not needed at runtime). +- **The laantungir fork of `nostr-tools`** cloned as a sibling directory and built: + + ```bash + git clone git@laantungir.net:laantungir/nostr-tools.git ../nostr-tools + cd ../nostr-tools + npm install --ignore-scripts + rm -rf lib && bun run build.js # or: node build.js + npx tsc # produces lib/types/*.d.ts + ``` + +- **For n_signer backends** (optional): the [`nsigner`](https://laantungir.net/git/laantungir/n_signer.git) binary installed and running. See the n_signer README for build instructions. + +### Build and install + +```bash +git clone git@laantungir.net:laantungir/nostr-publish.git +cd nostr-publish +npm install +./install_upgrade.sh +``` + +The `install_upgrade.sh` script compiles the extension, packages it into a `.vsix`, and installs it into Codium (or VSCode) with `--force`. Restart your editor after it finishes. + +Alternatively, build and install manually: + +```bash +npm run compile +npx vsce package --no-dependencies +codium --install-extension nostr-publish-0.1.0.vsix --force +``` + +### Updating after code changes + +```bash +./install_upgrade.sh +# restart Codium +``` + +The script handles everything — rebuild, repackage, reinstall. + +## Usage + +### 1. Open the Nostr sidebar + +Click the **Nostr** icon (broadcast glyph) in the Activity Bar on the left side of Codium. The sidebar has three sections: + +- **Actions** — create events, publish files, validate. +- **Relays** — toggle which relays to publish to, fetch your NIP-65 relay list. +- **Identity** — sign in, view your avatar/npub, sign out. + +### 2. Sign in + +In the **Identity** section, choose a sign-in method from the dropdown: + +- **Local (nsec / hex key)** — paste your `nsec1...` or 64-char hex secret key. The key is held in memory only for the session — it's never written to disk. Sign out to clear it. + +- **n_signer (Unix socket)** — enter the abstract socket name (default `nsigner`) and key index. Requires `nsigner --listen unix --socket-name nsigner` running on the same machine. + +- **n_signer (Qubes qrexec)** — enter the target qube name (e.g. `nostr_signer`) and key index. Requires the `qubes.NsignerRpc` service installed in the target qube and dom0 policy allowing your qube to call it. + +- **n_signer (TCP)** — enter host, port, your caller `nsec` (for auth envelopes), and key index. Requires `nsigner --listen tcp::` running on the target machine. + +Click **Sign In**. The extension connects to the signer, fetches your public key, displays your avatar + npub, and auto-loads your NIP-65 relay list. + +### 3. Publish a markdown article + +1. Click **+ New Event** → pick **30023 — Long-Form Article**. A markdown file opens with a YAML front-matter skeleton: + + ```markdown + --- + slug: my-article-slug + title: My Article Title + summary: A one-sentence abstract for feeds. + image: https://example.com/cover.png + tags: [nostr, writing] + --- + + # My Article Title + + Write your article in Markdown here. ``` - The extension depends on it via `"nostr-tools": "file:../nostr-tools"`. +2. Edit the front-matter (metadata) and write your article body in Markdown below the `---` line. -## Install (development) +3. Click **Validate Front-Matter** to preview the metadata that will be published (dry run — nothing is sent). -```bash -npm install -npm run compile # esbuild -> dist/extension.js -``` +4. Click **Publish Current File as Long-Form Note**. A confirmation dialog shows the event preview (slug, title, summary, image, tags, `published_at`, body length, pubkey, relays). Toggle relays on/off, then click **Publish**. -Press `F5` in Codium/VSCode to launch an Extension Development Host with the -extension loaded, or package and install: +The front-matter block is stripped before publishing — readers on Nostr see only the markdown body. The file on disk keeps the front-matter for re-publishing. -```bash -npx vsce package # produces nostr-publish-0.1.0.vsix -code --install-extension nostr-publish-0.1.0.vsix -``` +**Re-publishing an update**: edit the file and publish again. The same `slug` (d-tag) means it's a parameterized-replaceable update — relays replace the old version. The `published_at` timestamp is preserved from the first publish (tracked per-workspace). -## Phase 0 usage +### 4. Publish a raw event from JSON -### 1. Sign in +1. Click **+ New Event** → pick a kind (Short Text Note, Relay List, Profile, Contacts, or Custom). +2. A JSON skeleton opens with the expected tags and placeholder content for that kind. +3. Fill in the `content` and `tags` fields. +4. Click **Publish Current JSON File** (or right-click in the JSON editor → Nostr: Publish Event from JSON File). +5. Confirm in the dialog, then broadcast. -Run **Nostr: Sign In** from the command palette. Enter your secret key as -`nsec1...` or 64-char hex. The key is held in memory only for the session — -it is never written to disk. The status bar shows your `npub1...`. +The JSON file is a plain Nostr `EventTemplate` — interoperable with any other Nostr tooling that accepts `EventTemplate` JSON. -Run **Nostr: Sign Out** to clear the key (the buffer is zeroed). +### 5. Manage relays -### 2. Create an unsigned event - -Run **Nostr: Create Unsigned Event**. Pick a kind (1 = short text note, -30023 = long-form article, 10002 = relay list, 0 = metadata, 3 = contacts, -or Custom…). A skeleton JSON document opens in an untitled editor: - -```json -{ - "kind": 1, - "created_at": 1721329200, - "tags": [], - "content": "" -} -``` - -Fill in `tags` and `content`. Save it as a `.json` file if you want to reuse -it, or publish directly from the untitled buffer. - -### 3. Validate - -Run **Nostr: Validate Event File** (or right-click in a JSON editor → -Nostr: Validate Event File). Checks the shape of the event and reports any -errors or warnings (e.g. unknown fields, or `pubkey`/`id`/`sig` left over -from a previously signed event). - -### 4. Publish - -Run **Nostr: Publish Event from JSON File** (or right-click → Nostr: Publish -Event from JSON File). The extension: - -1. Validates the JSON. -2. Signs the event with your session key (injects `pubkey`, `id`, `sig`). -3. Shows a confirmation dialog with a preview and relay checkboxes. -4. Broadcasts to the selected relays via WebSocket. -5. Reports per-relay OK/failed results and the event id. - -If the JSON contains `pubkey`/`id`/`sig`, they are stripped and the event is -re-signed with your active key (useful for re-signing an event as yourself). +- The **Relays** section shows your configured relays with checkboxes. Toggle individual relays on/off for the next publish. +- Click **Fetch My Relays (NIP-65)** to load your kind 10002 relay list from the network and replace the sidebar's relay set with it. +- Default relays: `wss://relay.damus.io`, `wss://relay.primal.net`, `wss://laantungir.net/relay`. Change them in Codium Settings → `nostr.relays`. ## Configuration @@ -104,103 +139,68 @@ re-signed with your active key (useful for re-signing an event as yourself). | `nostr.relays` | `["wss://relay.damus.io", "wss://relay.primal.net", "wss://laantungir.net/relay"]` | Relays to publish to. | | `nostr.publish.confirm` | `true` | Show a confirmation dialog before broadcasting. | | `nostr.publish.timeoutMs` | `10000` | Per-relay publish timeout in milliseconds. | +| `nostr.signerBackend` | `"local"` | `"local"` or `"nsigner"`. | +| `nostr.nsigner.transport` | `"unix"` | `"unix"`, `"qrexec"`, or `"tcp"`. | +| `nostr.nsigner.socketName` | `"nsigner"` | Abstract socket name (without `@`). Used when transport=unix. | +| `nostr.nsigner.nostrIndex` | `0` | Key index (NIP-06 `m/44'/1237'/N'/0/0`). | +| `nostr.nsigner.qrexecQube` | `"nostr_signer"` | Target qube name. Used when transport=qrexec. | +| `nostr.nsigner.qrexecService` | `"qubes.NsignerRpc"` | Qrexec service name. | +| `nostr.nsigner.tcpHost` | `"127.0.0.1"` | TCP host. Used when transport=tcp. | +| `nostr.nsigner.tcpPort` | `8080` | TCP port. Used when transport=tcp. | +| `nostr.nsigner.callerNsec` | `""` | Caller nsec for TCP auth envelopes. Required for transport=tcp if not signed in locally. | -## File format +## Commands -The `.json` file is a plain Nostr `EventTemplate` — no extension-specific -wrapper. It is interoperable with any other Nostr tooling that accepts -`EventTemplate` JSON: +| Command | Description | +|---|---| +| **Nostr: Sign In** | Sign in with a local nsec/hex key (InputBox prompt). | +| **Nostr: Sign Out** | Clear the session key. | +| **Nostr: Create Unsigned Event** | Generate a kind-specific event skeleton (JSON or markdown). | +| **Nostr: Validate Event File** | Validate the active JSON file as a Nostr event. | +| **Nostr: Publish Event from JSON File** | Sign and publish the active JSON file. | +| **Nostr: Publish Current File as Long-Form Note** | Parse front-matter, build a NIP-23 event, sign, and publish. | +| **Nostr: Validate Front-Matter** | Dry-run preview of the front-matter metadata. | +| **Nostr: Fetch My Relays (NIP-65)** | Fetch your kind 10002 relay list. | +| **Nostr: Select Signer Backend** | Switch between local and n_signer backends (command-palette flow). | +| **Nostr: Reset Published Timestamps** | Clear the local `published_at` tracking map. | -```json -{ - "kind": 30023, - "created_at": 1721329200, - "tags": [ - ["d", "my-post"], - ["title", "My Post"] - ], - "content": "# My Post\n\nBody text..." -} +## n_signer setup + +For the n_signer backends, you need a running `nsigner` process. The extension connects to it; it doesn't bundle the binary. + +**Unix socket** (local, same machine): +```bash +nsigner --listen unix --socket-name nsigner ``` -## Architecture +**Qubes qrexec** (inter-qube): +- Install the `qubes.NsignerRpc` service in the target qube. +- Install dom0 qrexec policy allowing your qube to call it. +- The extension spawns `qrexec-client-vm qubes.NsignerRpc` per call. -- **`Signer` interface**: reuses `nostr-tools`' `Signer` - ([`nostr-tools/signer.ts`](https://laantungir.net/git/laantungir/nostr-tools.git)). - `LocalSigner` wraps `PlainKeySigner`; Phase 2's `NsignerBackend` will - implement the same interface. -- **`LongFormArticle = 30023`** constant from `nostr-tools/kinds` (Phase 1). -- **Relay publishing**: `ws` WebSocket, `["EVENT", signed]`, waits for - `["OK", id, true|false, reason]`. -- **Bundling**: esbuild → single `dist/extension.js`, `vscode` external. +**TCP** (remote / FIPS mesh): +```bash +nsigner --listen tcp:[::]:8080 +``` +The extension signs a kind-27235 auth envelope per request with your caller key. The first request from a new caller prompts for approval at the n_signer terminal. -## Phase 2: n_signer signing backend - -Phase 2 delegates signing to a running -[`n_signer`](https://laantungir.net/git/laantungir/n_signer.git) process over -a Linux abstract Unix socket. The key material stays in `n_signer`'s -mlock'd RAM — the extension never sees it. - -### Setup - -1. Build and run `n_signer` (see its README for build instructions): - ```bash - nsigner --listen unix --socket-name nsigner - ``` - Enter your mnemonic at the prompt. `n_signer` binds the abstract socket - `@nsigner` and shows its status TUI. - -2. In the extension, run **Nostr: Select Signer Backend** (command palette): - - Pick **n_signer (remote signing)**. - - The extension discovers running `n_signer` sockets via `/proc/net/unix` - and lists them. Pick `@nsigner` (or enter a custom socket name). - - The extension calls `get_public_key` to verify reachability and shows - your npub. If it can't connect, it shows an error with the command to - start `n_signer`. - -3. Publish as usual — the extension calls `sign_event` on `n_signer` for each - publish. The first sign from a new caller may prompt for approval at the - `n_signer` terminal (per its deny-by-default policy). - -### Wire contract - -- Transport: Node `net.createConnection({ path: "\u0000nsigner" })` (abstract - socket, the `\u0000` prefix is the Linux abstract-namespace marker). -- Framing: 4-byte big-endian length prefix + UTF-8 JSON payload. -- Verbs: `get_public_key` (params `[{nostr_index: }]`), `sign_event` - (params `[JSON.stringify(event), {nostr_index: }]`). -- No auth envelope needed for unix sockets — identity is UID-based via - `SO_PEERCRED`. - -### Configuration - -| Setting | Default | Description | -|---|---|---| -| `nostr.signerBackend` | `"local"` | `"local"` or `"nsigner"`. | -| `nostr.nsigner.socketName` | `"nsigner"` | Abstract socket name (without `@`). | -| `nostr.nsigner.nostrIndex` | `0` | `nostr_index` for key derivation. | - -## Sidebar - -The Nostr sidebar (Activity Bar icon) shows: - -- **Identity**: signed-in state + npub + signer backend label + Sign In/Out. -- **Relays**: checkboxes to toggle which relays to publish to, plus a - **Fetch My Relays (NIP-65)** button that loads your kind 10002 relay list. -- **Actions**: + New Event, Publish Current File as Long-Form Note, - Validate Front-Matter, Publish Current JSON File, Validate Current File. - -Buttons enable/disable based on the active editor's language (markdown vs -JSON). The sidebar refreshes on sign-in/out and when switching editors. +See the [n_signer README](https://laantungir.net/git/laantungir/n_signer.git) for full build and setup instructions. ## Known limitations -- No key persistence for the local backend — the secret key must be - re-entered each session (by design; Phase 2's `n_signer` backend holds the - key material instead). -- No image upload — `image` URLs in front-matter must be hosted externally - (future Blossom/NIP-96 phase). -- `n_signer` backend is Linux-only (abstract Unix sockets are a Linux - primitive). -- The `published_at` workspace-state map is per-workspace; switching - workspaces loses the first-publish timestamps. +- No key persistence for the local backend — the secret key must be re-entered each session (by design; n_signer backends hold the key material instead). +- No image upload — `image` URLs in front-matter must be hosted externally (future Blossom/NIP-96 phase). +- n_signer backends are Linux-only (abstract Unix sockets, qrexec, and TCP auth envelopes are Linux/Qubes primitives). +- The `published_at` workspace-state map is per-workspace; switching workspaces loses the first-publish timestamps. +- Serial transport (USB CDC) is not supported — needs a native `serialport` module. Use unix, qrexec, or tcp instead. + +## Design docs + +Detailed design documents are in [`plans/`](plans/): +- [`plans/phase0-raw-event-publishing.md`](plans/phase0-raw-event-publishing.md) — raw event publishing. +- [`plans/longform-metadata-strategy.md`](plans/longform-metadata-strategy.md) — YAML front-matter + NIP-23 metadata. +- [`plans/implementation-plan.md`](plans/implementation-plan.md) — full implementation plan with code sketches. + +## License + +MIT diff --git a/install_upgrade.sh b/install_upgrade.sh new file mode 100755 index 0000000..f1748cb --- /dev/null +++ b/install_upgrade.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# install_upgrade.sh — build and install/upgrade the nostr-publish extension +# into Codium (or VSCode). +# +# Usage: +# ./install_upgrade.sh # build + install (auto-detect codium/code) +# ./install_upgrade.sh codium # force codium +# ./install_upgrade.sh code # force code (VSCode) +# +# What it does: +# 1. Ensures ../nostr-tools is built (lib/cjs exists). +# 2. npm install (if node_modules is missing). +# 3. npm run compile (esbuild -> dist/extension.js). +# 4. npx vsce package (produces nostr-publish-.vsix). +# 5. Installs the VSIX with --force (upgrades in place). +# +# After running, restart Codium/VSCode to load the new version. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --- Detect the editor binary ------------------------------------------------ +EDITOR_BIN="${1:-}" +if [[ -z "$EDITOR_BIN" ]]; then + if command -v codium &>/dev/null; then + EDITOR_BIN="codium" + elif command -v code &>/dev/null; then + EDITOR_BIN="code" + else + echo "ERROR: neither 'codium' nor 'code' found in PATH." >&2 + echo "Pass the binary name explicitly: ./install_upgrade.sh codium" >&2 + exit 1 + fi +fi + +if ! command -v "$EDITOR_BIN" &>/dev/null; then + echo "ERROR: '$EDITOR_BIN' not found in PATH." >&2 + exit 1 +fi + +echo "=== nostr-publish install/upgrade ===" +echo "Editor: $EDITOR_BIN" +echo "Source: $SCRIPT_DIR" +echo "" + +# --- Ensure nostr-tools is built --------------------------------------------- +NOSTR_TOOLS_DIR="$SCRIPT_DIR/../nostr-tools" +if [[ ! -d "$NOSTR_TOOLS_DIR" ]]; then + echo "ERROR: $NOSTR_TOOLS_DIR not found." >&2 + echo "Clone the laantungir nostr-tools fork as a sibling directory:" >&2 + echo " git clone git@laantungir.net:laantungir/nostr-tools.git ../nostr-tools" >&2 + exit 1 +fi + +if [[ ! -d "$NOSTR_TOOLS_DIR/lib/cjs" ]]; then + echo "Building nostr-tools (lib/cjs missing)..." + pushd "$NOSTR_TOOLS_DIR" >/dev/null + if [[ ! -d node_modules ]]; then + npm install --ignore-scripts + fi + rm -rf lib + if command -v bun &>/dev/null; then + bun run build.js + else + node build.js + fi + npx tsc + popd >/dev/null + echo "nostr-tools built." + echo "" +fi + +# --- npm install if needed --------------------------------------------------- +if [[ ! -d node_modules ]]; then + echo "Running npm install..." + npm install + echo "" +fi + +# --- Compile ----------------------------------------------------------------- +echo "Compiling (esbuild)..." +npm run compile +echo "" + +# --- Package ----------------------------------------------------------------- +echo "Packaging VSIX..." +VSIX=$(npx vsce package --no-dependencies --baseContentUrl https://laantungir.net/git/laantungir/nostr-publish.git 2>&1 | grep -oP 'Packaged: \K[^ ]+') +if [[ -z "$VSIX" ]]; then + # Fallback: find the vsix in the current directory + VSIX=$(ls -1 nostr-publish-*.vsix 2>/dev/null | head -1) +fi +if [[ -z "$VSIX" || ! -f "$VSIX" ]]; then + echo "ERROR: VSIX packaging failed." >&2 + exit 1 +fi +echo "VSIX: $VSIX" +echo "" + +# --- Install / Upgrade ------------------------------------------------------- +echo "Installing into $EDITOR_BIN (--force)..." +"$EDITOR_BIN" --install-extension "$VSIX" --force +echo "" + +# --- Done -------------------------------------------------------------------- +VERSION=$(node -p "require('./package.json').version") +echo "=== Done ===" +echo "Installed nostr-publish v$VERSION." +echo "Restart $EDITOR_BIN to load the new version."