mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
docs: operator console guide + fix example recipes + document all env vars
- New docs/operations/CONSOLE.md: reaching /admin, NIP-98 login, the defaults->env->DB precedence model, per-section runtime-safe vs restart-required table, expected_versions concurrency, restart_apply_mode (with the systemd Restart=always requirement for auto), clearing a DB override. Linked from README + OPERATOR_SETUP. - All six example recipes gained an explicit chain-enablement block (FEATURE_XMR/WOW on, BTC/LTC/GRIN off) so they no longer hard-fail Config::validate in production (default-on chains with empty sources). - Documented all 16 previously-undocumented env vars in .env.example (RESTART_APPLY_MODE, TOR_ADMIN_ONION, DEPLOYMENT_MODE, TIP_SHARE_BASE_URL, ...); RELAY.md gained the premium-post policy + RELAY_WRITE_ALLOWLIST_NPUBS; ADMIN_PUBKEYS noted as reserved/inert.
This commit is contained in:
+85
-1
@@ -13,6 +13,19 @@ SERVER_PORT=8080
|
|||||||
# so every real deployment must replace it.
|
# so every real deployment must replace it.
|
||||||
PUBLIC_API_URL=https://backend.example.org/api/v1
|
PUBLIC_API_URL=https://backend.example.org/api/v1
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
# Deployment topology: single (default) permits in-process challenge/nonce state;
|
||||||
|
# fleet means a load-balanced set of instances and requires shared/stateless
|
||||||
|
# challenge state. Must be single|fleet or boot fails.
|
||||||
|
DEPLOYMENT_MODE=single
|
||||||
|
# Comma-separated CIDRs or bare IPs whose X-Forwarded-For is trusted for the
|
||||||
|
# client IP (rate-limiting + audit). Empty (default) = always use the real TCP
|
||||||
|
# peer. Set this to your reverse proxy's address(es) when it terminates TLS.
|
||||||
|
TRUSTED_PROXIES=
|
||||||
|
# Comma-separated browser origins allowed by CORS (e.g. the web wallet). Empty
|
||||||
|
# (default) allows any origin — safe here because auth is a Bearer token, not a
|
||||||
|
# cookie, so no ambient credentials ride a cross-origin request. Credentials are
|
||||||
|
# never allowed.
|
||||||
|
CORS_ALLOWED_ORIGINS=
|
||||||
|
|
||||||
# ── Database ──────────────────────────────────────────────────────────────
|
# ── Database ──────────────────────────────────────────────────────────────
|
||||||
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
|
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
|
||||||
@@ -36,6 +49,10 @@ FEATURE_POW=false
|
|||||||
ALTCHA_HMAC_KEY=
|
ALTCHA_HMAC_KEY=
|
||||||
POW_REQUIRED=false
|
POW_REQUIRED=false
|
||||||
ALTCHA_COST=100000
|
ALTCHA_COST=100000
|
||||||
|
# Testing aid: comma-separated lowercase pubkey hashes that ALWAYS require PoW,
|
||||||
|
# even when POW_REQUIRED=false (opt-in, for exercising the gate). Requires
|
||||||
|
# FEATURE_POW=true or boot fails closed. Leave empty in normal deployments.
|
||||||
|
TEST_POW_REQUIRED_FOR_PUBKEYS=
|
||||||
|
|
||||||
# ── Registration gates (composable; each advertised via /capabilities) ────
|
# ── Registration gates (composable; each advertised via /capabilities) ────
|
||||||
# Require an operator-minted invite code to create a NEW wallet. Mint codes with
|
# Require an operator-minted invite code to create a NEW wallet. Mint codes with
|
||||||
@@ -74,10 +91,32 @@ PAYMENT_EXPIRES_MINUTES=60
|
|||||||
# enabled the two modes are identical.
|
# enabled the two modes are identical.
|
||||||
REGISTRATION_GATE_MODE=all
|
REGISTRATION_GATE_MODE=all
|
||||||
|
|
||||||
|
# ── Chain enablement (each chain is independently feature-flagged) ─────────
|
||||||
|
# All default to TRUE. FAIL-CLOSED RULE: in production, an ENABLED chain with no
|
||||||
|
# source configured makes the server REFUSE TO BOOT — a chain that cannot be
|
||||||
|
# served must not advertise itself as available via /capabilities. So set the
|
||||||
|
# ones you do not run to false, or configure the source for each you keep on:
|
||||||
|
# BTC / LTC — an electrum/Fulcrum URL (BTC_ELECTRUM_URL / LTC_ELECTRUM_URL) or
|
||||||
|
# a fallback list.
|
||||||
|
# XMR / WOW — the *_LWS_ADMIN_KEY (plus its LWS + daemon URLs).
|
||||||
|
# GRIN — GRIN_OWNER_API_SECRET (plus the GRIN_* wallet/node wiring).
|
||||||
|
# In development these are warnings rather than a hard boot error.
|
||||||
|
FEATURE_BTC=true
|
||||||
|
FEATURE_LTC=true
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_GRIN=true
|
||||||
|
|
||||||
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
|
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
|
||||||
BTC_NETWORK=mainnet
|
BTC_NETWORK=mainnet
|
||||||
BTC_ELECTRUM_URL=
|
BTC_ELECTRUM_URL=
|
||||||
BTC_ELECTRUM_FALLBACKS=
|
BTC_ELECTRUM_FALLBACKS=
|
||||||
|
# Require CA-verified TLS (webpki + hostname) for ssl:// electrum servers. Applies
|
||||||
|
# to BOTH BTC and LTC. Default false: public electrum servers are self-signed by
|
||||||
|
# convention and their data is treated as hostile regardless, so accepting any
|
||||||
|
# cert keeps the public fallbacks usable. Set true if you run your own CA-cert'd
|
||||||
|
# Fulcrum and want to harden.
|
||||||
|
ELECTRUM_STRICT_TLS=false
|
||||||
|
|
||||||
# ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ────
|
# ── Litecoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ────
|
||||||
LTC_NETWORK=mainnet
|
LTC_NETWORK=mainnet
|
||||||
@@ -142,8 +181,26 @@ WALLET_RESTORE_POW_MAX_BITS=24
|
|||||||
FEATURE_PRICES=true
|
FEATURE_PRICES=true
|
||||||
PRICES_PROVIDER=coingecko
|
PRICES_PROVIDER=coingecko
|
||||||
PRICES_FETCH_INTERVAL_SECS=300
|
PRICES_FETCH_INTERVAL_SECS=300
|
||||||
|
# Which assets the price feed quotes. UNSET = all supported (btc,ltc,xmr,wow,grin);
|
||||||
|
# a subset like "btc,xmr" narrows it; PRESENT-BUT-EMPTY (PRICES_ASSETS= with no
|
||||||
|
# value) means none. Leave commented to serve all.
|
||||||
|
#PRICES_ASSETS=
|
||||||
|
# Currency the feed quotes in. One of: usd, eur, gbp, jpy, cny, aud, cad, chf, btc.
|
||||||
|
# A typo fails closed at boot (rather than serving an empty feed).
|
||||||
|
PRICES_CURRENCY=usd
|
||||||
|
# Nostr-native identity (NIP-98 login/link, NIP-05 directory). ON by default; when
|
||||||
|
# on, PUBLIC_API_URL is required (and must be https in production).
|
||||||
|
FEATURE_NOSTR_IDENTITY=true
|
||||||
|
# Grin slatepack relay: a non-custodial encrypted store-and-forward mailbox for
|
||||||
|
# interactive Grin transfers. ON by default; can be disabled independently of Grin
|
||||||
|
# chain access.
|
||||||
|
FEATURE_GRIN_RELAY=true
|
||||||
# Tips are parked for the conditional-transfer redesign; off until that lands.
|
# Tips are parked for the conditional-transfer redesign; off until that lands.
|
||||||
FEATURE_TIPS=false
|
FEATURE_TIPS=false
|
||||||
|
# Base URL for public-tip share links (a tip's share URL is {base}/{tip_id}).
|
||||||
|
# REQUIRED when FEATURE_TIPS=true (boot fails closed without it); never hardcode a
|
||||||
|
# host, so federated/self-hosted instances build their own share links.
|
||||||
|
TIP_SHARE_BASE_URL=
|
||||||
|
|
||||||
# ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ────
|
# ── Nostr relay (optional messaging plane; see docs/operations/RELAY.md) ────
|
||||||
# Off by default. When on, the operator runs a relay (the packaged
|
# Off by default. When on, the operator runs a relay (the packaged
|
||||||
@@ -205,11 +262,21 @@ FEED_OWNER_NPUB=
|
|||||||
FEED_ALLOWLIST_NPUBS=
|
FEED_ALLOWLIST_NPUBS=
|
||||||
FEED_EXTRA_RELAYS=
|
FEED_EXTRA_RELAYS=
|
||||||
|
|
||||||
# ── Admin surface (see docs; default posture is loopback/Tor) ──────────────
|
# ── Admin surface (see docs/operations/CONSOLE.md; default posture is loopback/Tor) ─
|
||||||
# Admin auth is "Sign in with Smirk": only admin PUBLIC keys are stored here,
|
# 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.
|
# never a seed or private key. The allowlist is mutated CLI/loopback-only in v1.
|
||||||
|
# When enabled, the embedded Operator Console SPA is served at /admin on this plane.
|
||||||
ADMIN_ENABLED=false
|
ADMIN_ENABLED=false
|
||||||
ADMIN_BIND=127.0.0.1:8081
|
ADMIN_BIND=127.0.0.1:8081
|
||||||
|
# Allow ADMIN_BIND to be a NON-loopback address. Default false: boot REFUSES a
|
||||||
|
# public admin bind (confidentiality is by socket). Only set true if you front the
|
||||||
|
# plane with Tor client-auth or a trusted proxy and firewall it.
|
||||||
|
ADMIN_ALLOW_PUBLIC_BIND=false
|
||||||
|
# Tor onion host you reach the admin plane at (e.g. <hash>.onion). REQUIRED for the
|
||||||
|
# Tor-onion posture: it is added to the admin Host allowlist, so without it a
|
||||||
|
# request whose Host header is the onion is rejected 403 (anti DNS-rebinding).
|
||||||
|
# Never logged or surfaced publicly. Leave empty for the loopback/SSH-tunnel path.
|
||||||
|
TOR_ADMIN_ONION=
|
||||||
# Public absolute URL of THIS node's admin surface. The per-node id that binds
|
# Public absolute URL of THIS node's admin surface. The per-node id that binds
|
||||||
# signed admin actions (so a challenge signed for one node can't be replayed to
|
# signed admin actions (so a challenge signed for one node can't be replayed to
|
||||||
# another across a fleet) is DERIVED from this value — give each node in a fleet a
|
# another across a fleet) is DERIVED from this value — give each node in a fleet a
|
||||||
@@ -221,6 +288,17 @@ ADMIN_JWT_SECRET=
|
|||||||
ADMIN_KEY_INTEGRITY_SECRET=
|
ADMIN_KEY_INTEGRITY_SECRET=
|
||||||
ADMIN_MAX_KEYS=8
|
ADMIN_MAX_KEYS=8
|
||||||
ADMIN_PENDING_KEY_TTL_DAYS=7
|
ADMIN_PENDING_KEY_TTL_DAYS=7
|
||||||
|
# RESERVED / inert: ADMIN_PUBKEYS is parsed but NOT consumed anywhere in this
|
||||||
|
# version — it does not gate admin access. Authorization is the MAC-protected key
|
||||||
|
# allowlist in the database (managed via `smirk-admin` and the console Keys tab),
|
||||||
|
# never this variable. Leave it unset; setting it has no effect.
|
||||||
|
ADMIN_PUBKEYS=
|
||||||
|
# How the Operator Console applies a RESTART-REQUIRED config change (see CONSOLE.md):
|
||||||
|
# manual (default) — persist + flag; you apply it with a graceful restart.
|
||||||
|
# auto — the instance self-restarts after saving. REQUIRES a
|
||||||
|
# supervisor that brings it back (systemd Restart=always);
|
||||||
|
# otherwise it goes down and stays down.
|
||||||
|
RESTART_APPLY_MODE=manual
|
||||||
|
|
||||||
# ── Public landing page (OFF by default; full + per-field tunable when on) ──
|
# ── Public landing page (OFF by default; full + per-field tunable when on) ──
|
||||||
PUBLIC_LANDING_ENABLED=false
|
PUBLIC_LANDING_ENABLED=false
|
||||||
@@ -234,9 +312,15 @@ PUBLIC_STATS_ENABLED=false
|
|||||||
PUBLIC_STATS_CACHE_HOURS=24
|
PUBLIC_STATS_CACHE_HOURS=24
|
||||||
|
|
||||||
# ── Data retention / erasure ──────────────────────────────────────────────
|
# ── Data retention / erasure ──────────────────────────────────────────────
|
||||||
|
# When ERASURE_ENABLED=true, self-service erasure needs ADMIN_KEY_INTEGRITY_SECRET
|
||||||
|
# (its audit trail is the integrity-MAC'd hash chain) and PUBLIC_API_URL (its
|
||||||
|
# proofs bind that URL); boot fails closed otherwise.
|
||||||
ERASURE_ENABLED=false
|
ERASURE_ENABLED=false
|
||||||
ERASURE_PURGE_LOGIN_EVENTS=true
|
ERASURE_PURGE_LOGIN_EVENTS=true
|
||||||
ERASURE_EXPORT_PER_DAY=3
|
ERASURE_EXPORT_PER_DAY=3
|
||||||
|
# Grace window (hours) between a confirmed erasure and its execution. Max 87600
|
||||||
|
# (10 years).
|
||||||
|
ERASURE_GRACE_PERIOD_HOURS=72
|
||||||
# Log retention (auto-deletion); 0 = keep indefinitely.
|
# Log retention (auto-deletion); 0 = keep indefinitely.
|
||||||
RETENTION_LOGIN_EVENTS_DAYS=90
|
RETENTION_LOGIN_EVENTS_DAYS=90
|
||||||
RETENTION_AUDIT_DAYS=365
|
RETENTION_AUDIT_DAYS=365
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ Operator/admin functions live on a **separate loopback listener** (front it with
|
|||||||
Tor or an SSH tunnel) — confidentiality by socket, never merged into the public
|
Tor or an SSH tunnel) — confidentiality by socket, never merged into the public
|
||||||
router or OpenAPI.
|
router or OpenAPI.
|
||||||
|
|
||||||
|
- **Operator Console**: an embedded single-page app served at `/admin` on the
|
||||||
|
loopback admin plane. Sign in with your admin key, then review status, manage
|
||||||
|
admin keys and invite codes, and edit the runtime config overlay (env plus a
|
||||||
|
validated DB layer). Guide: [docs/operations/CONSOLE.md](docs/operations/CONSOLE.md).
|
||||||
- **Sign-in-with-Smirk admin auth** — a NIP-98 *signed action* over a single-use
|
- **Sign-in-with-Smirk admin auth** — a NIP-98 *signed action* over a single-use
|
||||||
nonce (AUTHN) + a MAC-protected key allowlist (AUTHZ), composed so a route can't
|
nonce (AUTHN) + a MAC-protected key allowlist (AUTHZ), composed so a route can't
|
||||||
run without both.
|
run without both.
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Operator Console: admin surface guide
|
||||||
|
|
||||||
|
The Operator Console is an embedded single-page app served at `/admin` on the
|
||||||
|
backend's loopback admin plane. It is the browser front end for the same admin
|
||||||
|
JSON API the `smirk-admin` CLI complements: sign in with your admin key, review
|
||||||
|
status, manage admin keys and invite codes, and edit the runtime config overlay.
|
||||||
|
|
||||||
|
The console ships inside the binary (built from `admin-ui/` and embedded at
|
||||||
|
compile time), so self-hosting needs no separate front-end deploy. It is off
|
||||||
|
unless the admin plane is enabled.
|
||||||
|
|
||||||
|
## 1. Enable and reach the admin plane
|
||||||
|
|
||||||
|
The admin plane is a separate listener from the public API; confidentiality is by
|
||||||
|
socket, not by middleware ordering. It never joins the public router or the
|
||||||
|
OpenAPI surface.
|
||||||
|
|
||||||
|
Enable it in your `.env`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ADMIN_ENABLED=true
|
||||||
|
ADMIN_BIND=127.0.0.1:8081 # loopback by default
|
||||||
|
ADMIN_PUBLIC_URL=https://admin.example.org
|
||||||
|
ADMIN_JWT_SECRET=<openssl rand -hex 32>
|
||||||
|
ADMIN_KEY_INTEGRITY_SECRET=<openssl rand -hex 32> # back this up; losing it locks out admin
|
||||||
|
```
|
||||||
|
|
||||||
|
`ADMIN_BIND` defaults to loopback. Boot refuses a non-loopback bind unless you
|
||||||
|
also set `ADMIN_ALLOW_PUBLIC_BIND=true`, so the plane cannot be exposed on a
|
||||||
|
public interface by accident. Never bind it to a public address; reach it one of
|
||||||
|
two ways instead:
|
||||||
|
|
||||||
|
- **SSH tunnel.** `ssh -L 8081:127.0.0.1:8081 you@your-server`, then open
|
||||||
|
`http://127.0.0.1:8081/admin` in your local browser. Because the Host is
|
||||||
|
`127.0.0.1`, the admin plane's Host allowlist accepts it with no extra config.
|
||||||
|
- **Tor onion service.** Point a hidden service at `127.0.0.1:8081` and set
|
||||||
|
`TOR_ADMIN_ONION=<hash>.onion`. The onion host is added to the admin Host
|
||||||
|
allowlist; without `TOR_ADMIN_ONION`, a request whose `Host` header is the
|
||||||
|
onion is rejected with `403` (the allowlist defends the loopback socket against
|
||||||
|
DNS rebinding, and only loopback names plus the one configured onion pass).
|
||||||
|
Then browse `http://<hash>.onion/admin` in Tor Browser.
|
||||||
|
|
||||||
|
The console shell (`GET /admin`, `/admin/`, `/admin/assets/*`) is served
|
||||||
|
unauthenticated: it loads without a session and runs the login client-side. Every
|
||||||
|
JSON route it calls (`/admin/keys`, `/admin/config`, `/admin/invites`, ...) is
|
||||||
|
guarded, so serving the shell openly exposes nothing.
|
||||||
|
|
||||||
|
### `ADMIN_PUBLIC_URL` is the per-node identity
|
||||||
|
|
||||||
|
`ADMIN_PUBLIC_URL` is the value your wallet's signed action binds against (the
|
||||||
|
NIP-98 `u` tag), and a per-node id is derived from it so a challenge signed for
|
||||||
|
one node cannot be replayed to another node in a fleet. Give each node in a fleet
|
||||||
|
a distinct `ADMIN_PUBLIC_URL`. The server dictates and verifies this value from
|
||||||
|
config, never from the request `Host` header, so the value you reach the plane at
|
||||||
|
(loopback, tunnel, onion) does not have to equal `ADMIN_PUBLIC_URL`.
|
||||||
|
|
||||||
|
## 2. Sign in with your admin key
|
||||||
|
|
||||||
|
Login is a NIP-98 signed action ("Sign in with Smirk"): you prove control of your
|
||||||
|
admin private key by signing a single-use, server-issued challenge with your
|
||||||
|
Smirk wallet. Only admin public keys are ever stored; the server never holds an
|
||||||
|
admin seed or private key.
|
||||||
|
|
||||||
|
1. Bootstrap a first admin key if you have not already (see
|
||||||
|
[OPERATOR_SETUP.md](OPERATOR_SETUP.md) section 5): `smirk-admin setup --pubkey
|
||||||
|
<x-only-hex>` seeds your pubkey active, or `smirk-admin create-admin-wallet`
|
||||||
|
generates a pending key that activates on its first login.
|
||||||
|
2. Open the console and start login. It calls `POST /admin/auth/challenge`, which
|
||||||
|
returns a nonce, the `u` URL to bind, and the instance id.
|
||||||
|
3. Sign the kind-27235 (NIP-98) challenge event with the Smirk wallet holding your
|
||||||
|
admin key. The console posts it to `POST /admin/auth/verify`.
|
||||||
|
4. On success you receive an admin access token plus a refresh token, and the
|
||||||
|
console loads the authenticated tabs. A pending key activates on this first
|
||||||
|
login.
|
||||||
|
|
||||||
|
Every admin-auth rejection (bad signature, a valid signature for a non-admin key,
|
||||||
|
a missing session) collapses to the same opaque `401`, so the surface is not an
|
||||||
|
enumeration oracle. Login and privileged actions are written to a hash-chained,
|
||||||
|
tamper-evident audit trail.
|
||||||
|
|
||||||
|
## 3. Console tabs
|
||||||
|
|
||||||
|
- **Status**: the effective feature/capability view, including any downgrades
|
||||||
|
(a flag that is on in config but serving as disabled because its secret or URL
|
||||||
|
is unset, e.g. `xmr` on with `XMR_LWS_ADMIN_KEY` empty).
|
||||||
|
- **Keys**: add, list, revoke, and rotate admin keys. A new key is added pending
|
||||||
|
and activates on its holder's first login. Revoking the last live key over the
|
||||||
|
network is refused (use the CLI); rotate is allowed on a solo key because it is
|
||||||
|
add-plus-revoke, the in-band recovery for a compromised sole key.
|
||||||
|
- **Invites**: mint single-use invite codes (`POST /admin/invites`) and list
|
||||||
|
them (`GET /admin/invites`). Raw codes are shown once; only their hash is
|
||||||
|
stored.
|
||||||
|
- **Config**: the runtime config overlay editor (below).
|
||||||
|
|
||||||
|
## 4. Config: the env-vs-DB precedence model
|
||||||
|
|
||||||
|
Effective config is layered:
|
||||||
|
|
||||||
|
```
|
||||||
|
defaults -> environment (.env) -> DB overlay (operator settings) -> validate
|
||||||
|
```
|
||||||
|
|
||||||
|
The DB overlay is a sparse, per-section patch stored in the `operator_settings`
|
||||||
|
table, one MAC'd row per section. A field present in the overlay overrides env for
|
||||||
|
that field; an absent field keeps its env (or default) value. `GET /admin/config`
|
||||||
|
returns, for every editable field, its effective value, the DB overlay, the
|
||||||
|
per-section version, its runtime class, and whether a restart is pending. So the
|
||||||
|
console can show each field's current value and whether its source is env/default
|
||||||
|
or the DB overlay.
|
||||||
|
|
||||||
|
Editing goes through `PUT /admin/config` with a sparse patch. The patch is merged
|
||||||
|
onto the persisted overlay and re-run through the exact same `Config::validate`
|
||||||
|
that boot uses, so a bad edit is rejected with the identical error boot would
|
||||||
|
raise and nothing invalid is ever persisted or applied. Secrets are excluded from
|
||||||
|
the editable surface by construction: no overlay field maps to a secret (JWT
|
||||||
|
secrets, peppers, the ALTCHA HMAC key, the payment processor API key, the relay
|
||||||
|
admission bind), so the console can neither read nor set one. Secrets stay in env.
|
||||||
|
|
||||||
|
The nine editable sections are: `landing`, `retention`, `restore`, `console`,
|
||||||
|
`registration`, `pow`, `features`, `relay`, `premium`.
|
||||||
|
|
||||||
|
### Runtime-safe vs restart-required fields
|
||||||
|
|
||||||
|
Each field is classified as runtime-safe (the change hot-swaps onto the live
|
||||||
|
config and takes effect on the next request) or restart-required (the change is
|
||||||
|
persisted and applied on the next graceful restart, because a boot-built client,
|
||||||
|
socket, or worker captured the old value). The classification is the single
|
||||||
|
source of truth in `runtime_class()` and is returned per field by
|
||||||
|
`GET /admin/config`. The exact classification:
|
||||||
|
|
||||||
|
| Section | Class | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `landing.*` | runtime-safe | read per request on the landing/render path |
|
||||||
|
| `retention.*` | runtime-safe | **except** `retention.erasure_enabled` |
|
||||||
|
| `retention.erasure_enabled` | restart-required | gates a boot-spawned erasure sweep worker |
|
||||||
|
| `restore.*` | runtime-safe | restore policy is read per request |
|
||||||
|
| `console.*` | runtime-safe | `restart_apply_mode` is read at save time |
|
||||||
|
| `registration.*` | runtime-safe | **except** `registration.require_payment` |
|
||||||
|
| `registration.require_payment` | restart-required | gates the boot-built payment client (the provider is `None` at boot unless payment or premium is on) |
|
||||||
|
| `pow.*` | runtime-safe | the PoW gate is read per request |
|
||||||
|
| `features.*` | restart-required | every field: feature flags and per-chain enablement gate boot-built clients/workers (price poller, chain adapters) |
|
||||||
|
| `relay.*` | restart-required | the relay/admission service is boot-bound and the URL is advertised at boot |
|
||||||
|
| `premium.*` | restart-required | premium is enforced by the boot-bound relay admission service |
|
||||||
|
|
||||||
|
Any `(section, field)` not listed defaults to restart-required, the safe default:
|
||||||
|
never hot-swap something whose consumers have not been reasoned about.
|
||||||
|
|
||||||
|
A `PUT` that touches only runtime-safe fields hot-swaps just that patch onto the
|
||||||
|
running config and reports `applied: "runtime"`. A `PUT` that touches any
|
||||||
|
restart-required field persists the change and reports `applied:
|
||||||
|
"restart-required"` with `restart_pending: true`. `restart_pending` stays true
|
||||||
|
until a restart applies it, and it reflects any restart-required change saved by
|
||||||
|
an earlier `PUT`, even when the current edit was itself live.
|
||||||
|
|
||||||
|
### Optimistic concurrency (`expected_versions`)
|
||||||
|
|
||||||
|
Each section carries a version that increments on every write. To guard against
|
||||||
|
two operators editing at once, send `expected_versions` in the `PUT` body with the
|
||||||
|
version you last read per touched section. If the stored version has moved on, the
|
||||||
|
write is rejected with `409 Conflict` and nothing is persisted; reload and retry.
|
||||||
|
Omitting `expected_versions` skips the check (last write wins).
|
||||||
|
|
||||||
|
### `restart_apply_mode`: manual vs auto
|
||||||
|
|
||||||
|
`console.restart_apply_mode` (env `RESTART_APPLY_MODE`, default `manual`) decides
|
||||||
|
how a restart-required change is applied:
|
||||||
|
|
||||||
|
- `manual` (default): the change is persisted and flagged `restart_pending`. You
|
||||||
|
apply it yourself with a graceful restart of the service.
|
||||||
|
- `auto`: after saving a restart-required change, the instance self-restarts
|
||||||
|
shortly after responding so the change takes effect. The console warns before
|
||||||
|
saving in this mode.
|
||||||
|
|
||||||
|
Auto mode REQUIRES the service to be brought back on exit. The self-restart is a
|
||||||
|
graceful shutdown (it drains in-flight requests and exits); nothing relaunches the
|
||||||
|
process on its own. Run under systemd with `Restart=always` (or an equivalent
|
||||||
|
supervisor) so the instance comes back with the new config applied. Without that,
|
||||||
|
`auto` takes the instance down and leaves it down. Do not use `auto` for a
|
||||||
|
process you start by hand.
|
||||||
|
|
||||||
|
### Clearing a DB override to fall back to env
|
||||||
|
|
||||||
|
The overlay is stored per section, and a value you set through the console is a DB
|
||||||
|
override that wins over env for that field. Note the behavior in this version:
|
||||||
|
|
||||||
|
- There is no per-field null clear. A `null` in a `PUT` patch means "leave this
|
||||||
|
field unchanged" and is skipped, so sending `null` does not remove an existing
|
||||||
|
override.
|
||||||
|
- To make a field's effect match env again, set it back through the console to the
|
||||||
|
same value env provides. The effective value then equals env, though the field's
|
||||||
|
source still reports `db`.
|
||||||
|
- `landing.title` is the one special case: setting it to an empty string clears it
|
||||||
|
back to the env value (unset).
|
||||||
|
- To fully drop a section's DB overlay so its source returns to env/default (and
|
||||||
|
future env edits for that section flow through again), delete that section's row
|
||||||
|
from the `operator_settings` table and restart, for example:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM operator_settings WHERE section = 'landing';
|
||||||
|
```
|
||||||
|
|
||||||
|
On load, any section whose row is absent, whose MAC fails, or which fails to
|
||||||
|
decode is ignored and falls back to env, fail-closed on tamper and
|
||||||
|
fail-open-to-env on a missing section. There is no console/API/CLI command to
|
||||||
|
clear a section in this version; the direct table delete is the supported path.
|
||||||
|
|
||||||
|
## 5. Reserved / inert config
|
||||||
|
|
||||||
|
- `ADMIN_PUBKEYS` is parsed from the environment but is NOT consumed anywhere in
|
||||||
|
this version: it does not gate admin access. Admin authorization is the
|
||||||
|
MAC-protected key allowlist in the database (managed via `smirk-admin` and the
|
||||||
|
Keys tab), never this variable. Treat `ADMIN_PUBKEYS` as reserved and leave it
|
||||||
|
unset; setting it has no effect and does not grant access.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [OPERATOR_SETUP.md](OPERATOR_SETUP.md): full deployment and admin bootstrap.
|
||||||
|
- [RELAY.md](RELAY.md): the optional Nostr relay messaging plane.
|
||||||
|
- The root [`.env.example`](../../.env.example) documents every environment key.
|
||||||
@@ -100,6 +100,19 @@ Expose the admin plane over a Tor onion or an SSH tunnel; never bind it to a
|
|||||||
public interface. `ADMIN_BIND` defaults to loopback, and a non-loopback bind
|
public interface. `ADMIN_BIND` defaults to loopback, and a non-loopback bind
|
||||||
requires an explicit `ADMIN_ALLOW_PUBLIC_BIND=true`.
|
requires an explicit `ADMIN_ALLOW_PUBLIC_BIND=true`.
|
||||||
|
|
||||||
|
- **SSH tunnel:** `ssh -L 8081:127.0.0.1:8081 you@server`, then reach the plane at
|
||||||
|
`http://127.0.0.1:8081`. The loopback Host passes the admin allowlist with no
|
||||||
|
extra config.
|
||||||
|
- **Tor onion:** point a hidden service at `127.0.0.1:8081` and set
|
||||||
|
`TOR_ADMIN_ONION=<hash>.onion`. That onion host is added to the admin Host
|
||||||
|
allowlist; without it, a request whose `Host` header is the onion is rejected
|
||||||
|
(`403`), because the allowlist only accepts loopback names plus the one
|
||||||
|
configured onion (anti DNS-rebinding).
|
||||||
|
|
||||||
|
The Operator Console (an embedded admin SPA served at `/admin` on this same
|
||||||
|
plane) gives you a browser front end for keys, invites, status, and the runtime
|
||||||
|
config overlay. See [CONSOLE.md](CONSOLE.md).
|
||||||
|
|
||||||
## 6. Registration policy (optional)
|
## 6. Registration policy (optional)
|
||||||
|
|
||||||
Gates are composable and advertised via `/capabilities`; returning wallets and
|
Gates are composable and advertised via `/capabilities`; returning wallets and
|
||||||
|
|||||||
@@ -37,11 +37,29 @@ recipient), which a static pubkey whitelist can't do.
|
|||||||
| `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) |
|
| `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) |
|
| `author-allowlist` | only registered Smirk npubs may publish anything (no external inbound) |
|
||||||
| `open` | accept everything (the relay's resource caps still apply) |
|
| `open` | accept everything (the relay's resource caps still apply) |
|
||||||
|
| `premium-post` | registered users post wallet-functional events (DMs, tips, swap coordination) free; **general** notes require an active premium subscription (see the premium tier below). This is the flagship monetized relay policy |
|
||||||
|
|
||||||
`RELAY_INBOUND_POW_BITS` (0 = off) requires a NIP-13 proof-of-work on
|
`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
|
cross-ecosystem (non-registered-author) inbound events — spam friction without an
|
||||||
author allowlist. Only the `inbox-outbox` external-inbound branch is gated.
|
author allowlist. Only the `inbox-outbox` external-inbound branch is gated.
|
||||||
|
|
||||||
|
`RELAY_WRITE_ALLOWLIST_NPUBS` (comma-separated `npub1…` or 64-char hex; empty by
|
||||||
|
default) lists write-exempt npubs that may publish **any** kind regardless of the
|
||||||
|
write policy or premium status. The intended use is an announcements or
|
||||||
|
feed-owner account that seeds a `premium-post` feed without itself holding a
|
||||||
|
subscription. It is an exemption list, not the `author-allowlist` policy.
|
||||||
|
|
||||||
|
## Premium tier (`premium-post`)
|
||||||
|
|
||||||
|
The `premium-post` policy pairs with the premium subscription block
|
||||||
|
(`PREMIUM_ENABLED=true`, `PREMIUM_CURRENCY`, `PREMIUM_PLANS`) and the `PAYMENT_*`
|
||||||
|
processor. Wallet use stays free; premium unlocks general Nostr posting to your
|
||||||
|
relay. `Config::validate` fails closed if `PREMIUM_ENABLED` is set without
|
||||||
|
`RELAY_ENABLED=true`, `RELAY_WRITE_POLICY=premium-post`, the processor
|
||||||
|
credentials, and at least one priced `PREMIUM_PLANS` entry. Full design:
|
||||||
|
[../design/premium-relay.md](../design/premium-relay.md); ready-to-copy config:
|
||||||
|
the [`paid-relay`](../../examples/paid-relay/) recipe.
|
||||||
|
|
||||||
## Enable it
|
## Enable it
|
||||||
|
|
||||||
1. Set the relay env (see `.env.example` → *Nostr relay*):
|
1. Set the relay env (see `.env.example` → *Nostr relay*):
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ RELAY_WRITE_POLICY=author-allowlist
|
|||||||
RELAY_INBOUND_POW_BITS=0
|
RELAY_INBOUND_POW_BITS=0
|
||||||
RELAY_ADMISSION_BIND=127.0.0.1:8090
|
RELAY_ADMISSION_BIND=127.0.0.1:8090
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
|
||||||
|
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
|
||||||
|
# first, THEN flip its flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — see ../../.env.example.
|
# Chain endpoints — see ../../.env.example.
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
XMR_LWS_ADMIN_URL=
|
XMR_LWS_ADMIN_URL=
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ RELAY_WRITE_POLICY=inbox-outbox
|
|||||||
RELAY_INBOUND_POW_BITS=0
|
RELAY_INBOUND_POW_BITS=0
|
||||||
RELAY_ADMISSION_BIND=127.0.0.1:8090
|
RELAY_ADMISSION_BIND=127.0.0.1:8090
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
|
||||||
|
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
|
||||||
|
# first, THEN flip its flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — point at your own nodes / LWS. See ../../.env.example.
|
# Chain endpoints — point at your own nodes / LWS. See ../../.env.example.
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
XMR_LWS_ADMIN_URL=
|
XMR_LWS_ADMIN_URL=
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ FEATURE_PRICES=true
|
|||||||
FEATURE_TIPS=false
|
FEATURE_TIPS=false
|
||||||
RELAY_ENABLED=false
|
RELAY_ENABLED=false
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
|
||||||
|
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
|
||||||
|
# first, THEN flip its flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — see ../../.env.example.
|
# Chain endpoints — see ../../.env.example.
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
XMR_LWS_ADMIN_URL=
|
XMR_LWS_ADMIN_URL=
|
||||||
|
|||||||
@@ -71,6 +71,19 @@ FEATURE_PRICES=true
|
|||||||
FEATURE_TIPS=false
|
FEATURE_TIPS=false
|
||||||
ADMIN_ENABLED=false
|
ADMIN_ENABLED=false
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
|
||||||
|
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
|
||||||
|
# first, THEN flip its flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — see ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, electrum).
|
# Chain endpoints — see ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, electrum).
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
XMR_LWS_ADMIN_URL=
|
XMR_LWS_ADMIN_URL=
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ RELAY_ENABLED=false
|
|||||||
ADMIN_ENABLED=false
|
ADMIN_ENABLED=false
|
||||||
ADMIN_KEY_INTEGRITY_SECRET=
|
ADMIN_KEY_INTEGRITY_SECRET=
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. Running your own
|
||||||
|
# node, you will likely want more chains: to add BTC/LTC set the electrum URL, to
|
||||||
|
# add GRIN set GRIN_OWNER_API_SECRET (+ wiring), THEN flip that chain's flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — point at YOUR nodes / light-wallet servers. See the full list
|
# Chain endpoints — point at YOUR nodes / light-wallet servers. See the full list
|
||||||
# and formats in ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, BTC/LTC electrum).
|
# and formats in ../../.env.example (XMR/WOW LWS + daemon, GRIN_*, BTC/LTC electrum).
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ RELAY_ENABLED=false
|
|||||||
# Admin HTTP plane off at launch; enable + bootstrap a key when you need it.
|
# Admin HTTP plane off at launch; enable + bootstrap a key when you need it.
|
||||||
ADMIN_ENABLED=false
|
ADMIN_ENABLED=false
|
||||||
|
|
||||||
|
# ── Chains: enable ONLY what this recipe wires up ────────────────────────────
|
||||||
|
# FAIL-CLOSED RULE: in production, an ENABLED chain with no source configured makes
|
||||||
|
# the server REFUSE TO BOOT. Every FEATURE_* chain defaults to true, so this recipe
|
||||||
|
# turns off the chains it does not wire up. It wires up Monero + Wownero (the
|
||||||
|
# XMR_/WOW_ blocks below); fill their *_LWS_ADMIN_KEY and it boots. To add BTC/LTC
|
||||||
|
# (electrum URL) or GRIN (GRIN_OWNER_API_SECRET + wiring), configure the source
|
||||||
|
# first, THEN flip its flag to true.
|
||||||
|
FEATURE_XMR=true
|
||||||
|
FEATURE_WOW=true
|
||||||
|
FEATURE_BTC=false
|
||||||
|
FEATURE_LTC=false
|
||||||
|
FEATURE_GRIN=false
|
||||||
|
|
||||||
# Chain endpoints — reuse your existing daemons / LWS. See ../../.env.example.
|
# Chain endpoints — reuse your existing daemons / LWS. See ../../.env.example.
|
||||||
XMR_LWS_URL=
|
XMR_LWS_URL=
|
||||||
XMR_LWS_ADMIN_URL=
|
XMR_LWS_ADMIN_URL=
|
||||||
|
|||||||
Reference in New Issue
Block a user