Files
codium_nostr_extension/README.md
T

299 lines
17 KiB
Markdown

---
slug: codium-nostr-extension
title: Codium nostr extension
summary: A Codium / VSCode extension to publish files to [Nostr].
image:
tags: [nostr, writing, codium, vscode]
---
# Nostr Publish
A Codium / VSCode extension to publish files to [Nostr](https://github.com/nostr-protocol/nostr).
Publish markdown articles as long-form notes, broadcast raw Nostr events from JSON, and sign everything with either a local key or a remote [`signer`](https://laantungir.net/git/laantungir/signer.git) hardware signer (the Rust port of `n_signer`).
## Features
- **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.
- **Sign events without publishing** — Sign an event in the active editor and replace its contents with the signed version (`id`, `pubkey`, `sig` added), without broadcasting. Works on plain text (wrapped as a kind 1 text note) or JSON event templates. Re-running the command on an edited signed event re-signs it cleanly.
- **Upload files to Blossom servers** — Upload any file (or a markdown book with front-matter) to your [Blossom](https://github.com/hzrd149/blossom) media servers and publish a NIP-94 file-metadata (kind 1063) event linking the blob. Great for posting long documents/books.
- **Multiple signing backends**:
- **Local** — enter your `nsec` or hex key; held in memory only, never written to disk.
- **signer (Unix socket)** — delegate signing to a local `signer` process over an abstract Unix socket. Your key never leaves the signer.
- **signer (Qubes qrexec)** — reach `signer` in another qube via Qubes OS secure IPC.
- **signer (TCP)** — reach `signer` over TCP (FIPS mesh, remote machine). Requires a caller key for auth envelopes when the signer runs `--auth required`.
- **Sidebar GUI** — sign in, manage relays and Blossom servers, create events, upload, 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.
- **Blossom server auto-discovery (BUD-03)** — on sign-in, fetches your kind 10063 server list and uses your own Blossom servers for uploads.
- **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).
## Install
### Prerequisites
- **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 signer backends** (optional): the [`signer`](https://laantungir.net/git/laantungir/signer.git) binary installed and running. See the 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, sign files, publish files, validate. Action buttons show the name of the active file (first 10 chars of the filename + extension) so it's clear which file will be acted upon.
- **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.
- **signer (Unix socket)** — enter the abstract socket name (default `signer01`; the signer auto-names `signer01`, `signer02`, …), the role name (default `main`), and the role path (default `m/44'/1237'/0'/0/0`). Requires `signer --listen unix` running on the same machine.
- **signer (Qubes qrexec)** — enter the target qube name (e.g. `nostr_signer`), role, and role path. Requires the `qubes.SignerRpc` service installed in the target qube and dom0 policy allowing your qube to call it.
- **signer (TCP)** — enter host, port, your caller `nsec` (for auth envelopes, only when the signer runs `--auth required`), role, and role path. Requires `signer --listen tcp:<host>:<port>` running on the target machine.
> **Key selection:** signer selects the Nostr key by **role + role_path**, not by a numeric index. The role is an access token registered on the signer (in its add-role popup or via `--register-role`); the role path is the full BIP-44 derivation path. Both must match a role registered on the signer. To use a different Nostr account, edit the account segment of the path (e.g. `m/44'/1237'/3'/0/0`).
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.
```
2. Edit the front-matter (metadata) and write your article body in Markdown below the `---` line.
3. Click **Validate Front-Matter** to preview the metadata that will be published (dry run — nothing is sent).
4. Click **Publish 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**.
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.
**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).
### 4. Publish a raw event from JSON
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 Event** (or right-click in the JSON editor → Nostr: Publish Event from JSON File).
5. Confirm in the dialog, then broadcast.
The JSON file is a plain Nostr `EventTemplate` — interoperable with any other Nostr tooling that accepts `EventTemplate` JSON.
### 5. Sign an event without publishing
The **Sign as Event** action signs the active editor's contents and replaces them in place with the signed event (adding `id`, `pubkey`, and `sig`), without broadcasting to any relay. This is useful for preparing events to publish via another tool, or for inspecting the signed form.
Two input modes:
- **Plain text** — if the active document is not a JSON object with a `kind` field, the raw text is wrapped as a kind 1 (short text note) event with the text as `content`. For example, type `What's happening?` in an untitled file and click **Sign as Event**:
```json
{
"kind": 1,
"created_at": 1785315205,
"tags": [],
"content": "What's happening?",
"id": "...",
"pubkey": "...",
"sig": "..."
}
```
- **JSON event template** — if the document is a JSON object with a `kind`, it's treated as an event template. Any signed fields already present (`id`, `pubkey`, `sig`) are stripped and recomputed.
**Re-signing after edits**: because signed fields are stripped and recomputed on each run, you can take a signed event, change the `kind` (or `content`, `tags`, `created_at`), and run **Sign as Event** again — it re-signs cleanly with the new values. The edit is undoable.
### 6. Manage relays
- 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`.
### 7. Upload a file to Blossom
Blossom is a protocol for storing blobs (files) on media servers using your Nostr key for identity and authorization. This extension uploads files to your Blossom servers and publishes a NIP-94 file-metadata (kind 1063) event so Nostr clients can discover and display the file.
**Quick upload** (any file):
1. Open the file you want to upload (any type — markdown, PDF, EPUB, image, …).
2. Click **Upload File to Blossom** in the sidebar (or run **Nostr: Upload File to Blossom**).
3. Confirm the target servers and relays in the dialog.
4. The file is uploaded to every checked Blossom server, and a kind 1063 metadata event is signed and published to your relays. The blob URL is offered for copying to the clipboard.
**Header-based upload** (long documents/books):
1. Write a `.md` file with YAML front-matter carrying the book metadata:
```markdown
---
slug: narrative-of-frederick-douglass
title: Narrative of the Life of Frederick Douglass
summary: An 1845 autobiography by Frederick Douglass.
author: Frederick Douglass
language: en
year: 1845
cover: https://cdn.example.com/douglass-cover.png
tags: [history, abolition, autobiography]
---
# Narrative of the Life of Frederick Douglass
Body text…
```
2. Click **Publish to Blossom (with header)** (or run **Nostr: Publish to Blossom (with header)**).
3. The front-matter block is stripped, the markdown body is uploaded as a blob, and a kind 1063 event is built from the descriptor + front-matter (title, author, language, year, cover, tags) and published to your relays.
> **Markdown MIME handling:** markdown blobs are uploaded as `text/plain` so browsers render them inline (no mainstream browser displays `text/markdown`, and servers send `X-Content-Type-Options: nosniff` which forbids guessing). The kind 1063 event's `m` tag still advertises `text/markdown`, so Nostr clients that render markdown can detect the file's true type.
**Blossom servers**:
- The **Blossom Servers** section shows your servers with checkboxes — exactly like the Relays section. Every checked server receives the upload.
- Click **Fetch My Blossom Servers (BUD-03)** to load your kind 10063 server list from the network.
- Fallback: set `nostr.blossomServers` in settings (an array of full URLs) if you have no kind 10063 event.
- Uploads use a signed kind 24242 authorization token (BUD-11) scoped to the server domain and the blob's sha256, valid for `nostr.blossom.authTokenTtlSec` seconds (default 60).
## Configuration
| Setting | Default | Description |
|---|---|---|
| `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 `"signer"`. |
| `nostr.signer.transport` | `"unix"` | `"unix"`, `"qrexec"`, or `"tcp"`. |
| `nostr.signer.socketName` | `"signer01"` | Abstract socket name (without `@`). The signer auto-names `signer01`, `signer02`, … Used when transport=unix. |
| `nostr.signer.role` | `"main"` | signer role name (access token) used to select the Nostr key. Sent as `role` to the `nostr_*` verbs. |
| `nostr.signer.rolePath` | `"m/44'/1237'/0'/0/0"` | Full BIP-44 derivation path for the Nostr key. Sent as `role_path`. Edit the account segment (`m/44'/1237'/N'/0/0`) to pick a different account. |
| `nostr.signer.qrexecQube` | `"nostr_signer"` | Target qube name. Used when transport=qrexec. |
| `nostr.signer.qrexecService` | `"qubes.SignerRpc"` | Qrexec service name. |
| `nostr.signer.tcpHost` | `"127.0.0.1"` | TCP host. Used when transport=tcp. |
| `nostr.signer.tcpPort` | `8080` | TCP port. Used when transport=tcp. |
| `nostr.signer.callerNsec` | `""` | Caller nsec for TCP auth envelopes (kind 22242). Required for transport=tcp when the signer runs `--auth required`. |
| `nostr.blossomServers` | `[]` | Blossom media servers (full URLs) to upload blobs to. Used when no kind 10063 server list is fetched. |
| `nostr.blossom.uploadTimeoutMs` | `30000` | Per-server Blossom upload timeout in milliseconds. |
| `nostr.blossom.authTokenTtlSec` | `60` | Lifetime in seconds of the kind 24242 Blossom authorization token (BUD-11). |
## Commands
| 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: Sign Event** | Sign the active file's contents and replace them with the signed event (no publish). Plain text → kind 1; JSON with a `kind` → re-signed event template. |
| **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: Fetch My Blossom Servers (BUD-03)** | Fetch your kind 10063 Blossom server list. |
| **Nostr: Upload File to Blossom** | Upload the active file to Blossom + publish a kind 1063 metadata event. |
| **Nostr: Publish to Blossom (with header)** | Upload the active markdown file (front-matter stripped) to Blossom + publish a kind 1063 event with book metadata. |
| **Nostr: Insert Blob Header** | Insert a YAML front-matter template for Blossom uploads (slug, title, summary, author, language, year, cover, tags, file). |
| **Nostr: Select Signer Backend** | Switch between local and signer backends (command-palette flow). |
| **Nostr: Reset Published Timestamps** | Clear the local `published_at` tracking map. |
## signer setup
For the signer backends, you need a running `signer` process (the Rust port of `n_signer`). The extension connects to it; it doesn't bundle the binary.
**Unix socket** (local, same machine):
```bash
signer --listen unix
```
The signer auto-picks the first free sequential name (`signer01`, `signer02`, …); override with `--socket-name`.
**Qubes qrexec** (inter-qube):
- Run the persistent signer in the signer qube: `signer --listen unix --socket-name signer --bridge-source-trusted`
- Install the `qubes.SignerRpc` service in the target qube (runs `signer bridge --to signer`).
- Install dom0 qrexec policy allowing your qube to call it.
- The extension spawns `qrexec-client-vm <qube> qubes.SignerRpc` per call.
**TCP** (remote / FIPS mesh):
```bash
signer --listen tcp:[::]:8080
```
When the signer runs with `--auth required`, the extension signs a kind-22242 auth envelope per request with your caller key. With `--auth off` (the default) no caller key is needed.
See the [signer README](https://laantungir.net/git/laantungir/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; signer backends hold the key material instead).
- 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.
- [`plans/blossom-upload.md`](plans/blossom-upload.md) — Blossom server upload (BUD-01..11, NIP-94 kind 1063) design.
## License
MIT