This commit is contained in:
Laan Tungir
2026-07-18 17:49:50 -04:00
parent 8e16e0229e
commit 2314265886
2 changed files with 280 additions and 169 deletions
+169 -169
View File
@@ -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:<host>:<port>` 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 <qube> 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: <n>}]`), `sign_event`
(params `[JSON.stringify(event), {nostr_index: <n>}]`).
- 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
+111
View File
@@ -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-<version>.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."