diff --git a/.env.example b/.env.example index 2fb62a7..5df4ac3 100644 --- a/.env.example +++ b/.env.example @@ -133,6 +133,32 @@ PRICES_FETCH_INTERVAL_SECS=300 # Tips are parked for the conditional-transfer redesign; off until that lands. FEATURE_TIPS=false +# ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ──── +# Off by default. When on, the operator runs a relay (the packaged +# nostr-rs-relay under deploy/relay, or an external one) that this backend +# advertises (/capabilities + NIP-05) and write-restricts via a gRPC admission +# service. It is the user's DM INBOX, never the only relay — clients also use the +# public interop relays. Self-hosting concern. +RELAY_ENABLED=false +# bundled (run deploy/relay's nostr-rs-relay) | external (point at your own). +RELAY_MODE=bundled +# Public ws(s):// URL clients connect to (wss required in production). +RELAY_URL= +# Write policy the admission service enforces: +# inbox-outbox — registered npubs publish their own events; anyone may +# deliver a gift-wrapped DM (kind 1059) to a registered user. +# author-allowlist — only registered npubs may publish anything. +# open — accept everything (relay resource caps still apply). +RELAY_WRITE_POLICY=inbox-outbox +# NIP-13 proof-of-work bits required on cross-ecosystem (non-registered-author) +# inbound events; 0 disables the PoW gate. +RELAY_INBOUND_POW_BITS=0 +# Loopback address the gRPC event-admission service binds (the relay calls it). +# Keep it loopback; the bundled compose reaches it via the host network. +RELAY_ADMISSION_BIND=127.0.0.1:8090 +RELAY_MAX_EVENT_BYTES=65536 +RELAY_RETENTION_DAYS=30 + # ── Admin surface (see docs; default posture is loopback/Tor) ────────────── # Admin auth is "Sign in with Smirk": only admin PUBLIC keys are stored here, # never a seed or private key. The allowlist is mutated CLI/loopback-only in v1. diff --git a/deploy/relay/config.toml b/deploy/relay/config.toml new file mode 100644 index 0000000..f62b002 --- /dev/null +++ b/deploy/relay/config.toml @@ -0,0 +1,50 @@ +# nostr-rs-relay config for a locked-down Smirk messaging relay. +# +# REFERENCE TEMPLATE — the exact key set is version-specific; check it against the +# nostr-rs-relay release you pin. The load-bearing piece is [grpc] +# event_admission_server: it points at the Smirk backend's gRPC admission service, +# which enforces RELAY_WRITE_POLICY + the NIP-13 PoW gate per event. Do NOT also +# set a static [authorization] pubkey_whitelist — that would double-gate and +# break the inbox (gift-wraps from non-registered senders). + +[info] +relay_url = "wss://relay.example.org/" +name = "Smirk relay" +description = "Locked-down Nostr relay for a self-hosted Smirk backend (DM inbox)." +# pubkey = "" # optional contact identity + +[database] +# SQLite is simplest for a single-node self-host; switch to postgres to share +# infra. Keep it on a persisted volume (see docker-compose.yml). +engine = "sqlite" +data_directory = "/usr/src/app/db" + +[grpc] +# Smirk backend event-admission (nauthz). With host networking the backend's +# loopback RELAY_ADMISSION_BIND is reachable here. This is what enforces the +# write policy — keep it set. +event_admission_server = "http://127.0.0.1:8090" + +[network] +address = "0.0.0.0" +port = 8080 + +# Conservative, self-host-friendly resource ceilings (the anti-abuse floor even +# for the `open` policy). Tune to your box. +[limits] +messages_per_sec = 10 +subscriptions_per_min = 60 +max_event_bytes = 65536 +max_ws_message_bytes = 131072 +max_ws_frame_bytes = 131072 +broadcast_buffer = 16384 +event_persist_buffer = 4096 +limit_scrapers = true + +[options] +# Reject events dated too far in the future (clock-skew abuse). +reject_future_seconds = 1800 + +[authorization] +# Write restriction is enforced by the gRPC admission service above, NOT a static +# whitelist. Leave pubkey_whitelist unset. diff --git a/deploy/relay/docker-compose.yml b/deploy/relay/docker-compose.yml new file mode 100644 index 0000000..4c26ebd --- /dev/null +++ b/deploy/relay/docker-compose.yml @@ -0,0 +1,35 @@ +# Bundled, locked-down Nostr relay for a self-hosted Smirk backend. +# +# This runs nostr-rs-relay as the messaging-plane relay. WRITE ADMISSION is +# delegated to the Smirk backend's gRPC service (RELAY_ADMISSION_BIND, default +# 127.0.0.1:8090) via nostr-rs-relay's [grpc] event_admission_server hook — so +# the RELAY_WRITE_POLICY (inbox-outbox / author-allowlist / open) + NIP-13 PoW +# are enforced by the backend, not baked into this file. +# +# Networking: the relay uses the HOST network so it can reach the backend's +# loopback admission socket (127.0.0.1:8090) while keeping that socket private. +# host networking is Linux-only; on other hosts, bind RELAY_ADMISSION_BIND to a +# docker-reachable address behind your firewall and drop `network_mode: host`, +# adding `extra_hosts: ["host.docker.internal:host-gateway"]` + a port mapping. +# +# This is a REFERENCE template — verify the config keys against the nostr-rs-relay +# version you pin (https://github.com/scsibug/nostr-rs-relay). +services: + relay: + image: ghcr.io/scsibug/nostr-rs-relay:latest # pin a digest in production + restart: unless-stopped + network_mode: host # reach the backend admission socket on 127.0.0.1 + user: "1000:1000" + volumes: + - ./config.toml:/usr/src/app/config.toml:ro + - relay-data:/usr/src/app/db + environment: + RUST_LOG: info + # Modest, self-host-friendly resource ceilings. + deploy: + resources: + limits: + memory: 512M + +volumes: + relay-data: diff --git a/docs/operations/OPERATOR_SETUP.md b/docs/operations/OPERATOR_SETUP.md index b095d78..902fbbe 100644 --- a/docs/operations/OPERATOR_SETUP.md +++ b/docs/operations/OPERATOR_SETUP.md @@ -110,6 +110,13 @@ self-hosting bypass them. All default off or open. - **Restore policy** — `WALLET_RESTORE_POLICY=create-only|bounded|unlimited` (default `create-only`), with optional proof-of-work pricing on restore depth. - **Pay-to-register** — `REGISTRATION_REQUIRE_PAYMENT=true` against a BTCPay-compatible processor (`PAYMENT_*`); a new wallet settles an invoice, paid to your own wallet, before it can register. +## 7. Nostr relay (optional) + +Run a first-party, locked-down Nostr relay as your users' encrypted-DM inbox +(`RELAY_ENABLED=true` + `RELAY_*`). Off by default; write-restricted via a gRPC +admission service, advertised through `/capabilities` + NIP-05. Full guide: +[RELAY.md](RELAY.md). + ## Testing an instance The `@smirk/smoke-tests` harness drives the API client end-to-end. Point it at an diff --git a/docs/operations/RELAY.md b/docs/operations/RELAY.md new file mode 100644 index 0000000..769e08c --- /dev/null +++ b/docs/operations/RELAY.md @@ -0,0 +1,87 @@ +# Nostr relay (messaging plane) — operator guide + +The relay is **optional**. It gives your users a first-party, **locked-down** +Nostr relay that acts as their **encrypted-DM inbox** — the transport foundation +for messaging (and, later, P2P coordination). It is **off by default**; a Smirk +backend is fully functional without it. + +## What it is (and isn't) + +- **It is the user's inbox relay**, advertised via `/capabilities` and NIP-05 + relay hints, and **write-restricted** by policy (see below). +- **It is NOT the only relay.** For cross-wallet delivery (e.g. to/from Goblin or + any Nostr user), clients also connect to the public interop relays + (`relay.damus.io`, `nos.lol`). A private relay alone would black-hole DMs to + outsiders — so the NIP-05 hints list your relay **first**, then the public set. +- The backend **does not run the relay process** — it *connects to + advertises* + it and enforces the write policy. You run the relay (bundled or external). + +## Architecture + +``` +wallet ──ws──▶ your relay (nostr-rs-relay) ──gRPC EventAdmit──▶ smirk-backend + (RELAY_WRITE_POLICY + + NIP-13 PoW, + registered-npub DB) +``` + +`nostr-rs-relay`'s `[grpc] event_admission_server` calls the backend once per +inbound event; the backend runs the pure policy engine (`src/infra/relay/policy.rs`) +against the user DB and returns PERMIT/DENY. This is dynamic (per-event, per- +recipient), which a static pubkey whitelist can't do. + +## Write policies (`RELAY_WRITE_POLICY`) + +| policy | who may publish | +| --- | --- | +| `inbox-outbox` (default) | registered npubs publish their own events (outbox); **anyone** may deliver a NIP-17 gift-wrap (kind 1059) **addressed to a registered user** (inbox) | +| `author-allowlist` | only registered Smirk npubs may publish anything (no external inbound) | +| `open` | accept everything (the relay's resource caps still apply) | + +`RELAY_INBOUND_POW_BITS` (0 = off) requires a NIP-13 proof-of-work on +cross-ecosystem (non-registered-author) inbound events — spam friction without an +author allowlist. Only the `inbox-outbox` external-inbound branch is gated. + +## Enable it + +1. Set the relay env (see `.env.example` → *Nostr relay*): + ```sh + RELAY_ENABLED=true + RELAY_MODE=bundled # or external + RELAY_URL=wss://relay.yourdomain.tld # what clients connect to (wss in prod) + RELAY_WRITE_POLICY=inbox-outbox + RELAY_INBOUND_POW_BITS=0 + RELAY_ADMISSION_BIND=127.0.0.1:8090 # loopback gRPC admission + ``` + The backend fail-closes at startup on a bad relay config, and for any non-`open` + policy it spawns the gRPC admission service on `RELAY_ADMISSION_BIND`. + +2. **Bundled** (`RELAY_MODE=bundled`): run the reference `nostr-rs-relay` in + [`deploy/relay/`](../../deploy/relay/): + ```sh + # set relay_url + event_admission_server in deploy/relay/config.toml first + docker compose -f deploy/relay/docker-compose.yml up -d + ``` + It uses host networking so the relay reaches the loopback admission socket + while that socket stays private (Linux; see the compose comments for other + hosts). Front `RELAY_URL` with your TLS terminator. + +3. **External** (`RELAY_MODE=external`): point `RELAY_URL` at any relay you run, + and configure *its* `[grpc] event_admission_server` at `RELAY_ADMISSION_BIND`. + +## Verify + +- `GET /capabilities` → `features.nostr_relay: true` and a `messaging` object + (`relay_url`, `write_policy`, `inbound_pow_bits`, `supported_nips`). +- `GET /.well-known/nostr.json?name=` lists your relay first in `relays`. +- Publish a registered-npub event → accepted; a random-author, non-gift-wrap + event → rejected (`inbox-outbox`); an under-PoW cross-ecosystem event → + rejected (when `RELAY_INBOUND_POW_BITS > 0`). + +## Modularity + +`nostr-rs-relay` is the first adapter behind the `RelayProvider` seam +(`src/infra/relay/`). The write policy lives in a pure engine; the gRPC transport +is the adapter's concern. A different relay (strfry, or another messaging +protocol) can enforce the same policy over a different mechanism without touching +the policy or the advertising.