mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
Adversarial review of the relay admission surface: - FAIL-SAFE on admission death: nostr-rs-relay fails OPEN if its gRPC admission server is unreachable, so a dead admission = an unrestricted relay. The backend now EXITS if the admission service stops (supervisor restarts a clean pair); documented in RELAY.md. Pin the relay image to a digest (compose note). - Registration-oracle protection: validate() now refuses a non-loopback RELAY_ADMISSION_BIND unless RELAY_ADMISSION_ALLOW_PUBLIC=true (+ test). - No N+1 on crafted events: recipient p-tags are capped (20, deduped) and resolved in ONE batched query (db.any_registered_npub) instead of one-per-tag. - Defence-in-depth size cap: admission denies events over max_event_bytes before any DB work (the relay enforces its own limit too). - Docs: RELAY.md Security section (fail-open, loopback oracle, default PoW=0 spam footgun, client sender-verification); config.toml relay port 8088 (not 8080); Docker Hub image; .env.example RELAY_ADMISSION_ALLOW_PUBLIC. Note: the client-side sender-impersonation blocker is fixed separately in the monorepo (verifying gift-wrap unwrap).
192 lines
9.6 KiB
Bash
192 lines
9.6 KiB
Bash
# smirk-backend-core — example environment.
|
|
#
|
|
# Copy to `.env` and fill in for your deployment. NEVER commit a real `.env`.
|
|
# Generate secrets with: openssl rand -hex 32
|
|
|
|
# ── Server ────────────────────────────────────────────────────────────────
|
|
SERVER_HOST=0.0.0.0
|
|
SERVER_PORT=8080
|
|
# Public absolute API base URL. REQUIRED when admin/erasure features are enabled:
|
|
# it is the canonical value the NIP-98 `u` tag is verified against (never the
|
|
# request Host header). Boot fails if unset while those features are on.
|
|
PUBLIC_API_URL=https://backend.example.org/api/v1
|
|
ENVIRONMENT=development
|
|
# Stable per-node identifier. Bound into signed admin/erasure actions so a proof
|
|
# captured against one node cannot be replayed across a load-balanced fleet.
|
|
INSTANCE_ID=
|
|
|
|
# ── Database ──────────────────────────────────────────────────────────────
|
|
DATABASE_URL=postgres://user:password@127.0.0.1:5432/smirk
|
|
|
|
# ── Auth (JWT) ────────────────────────────────────────────────────────────
|
|
JWT_SECRET=
|
|
JWT_EXPIRY_HOURS=24
|
|
|
|
# ── At-rest identity secrets (REQUIRED; generate each: openssl rand -hex 32) ─
|
|
# HMAC peppers keep stored identity values (pubkey_hash, seed_fingerprint)
|
|
# non-reproducible from a candidate seed; the salt unlinks client IPs in abuse
|
|
# tables. Boot fails closed if any is unset or too short.
|
|
SEED_FINGERPRINT_PEPPER=
|
|
REFRESH_TOKEN_PEPPER=
|
|
IP_SALT=
|
|
|
|
# ── Proof-of-work signup gate (ALTCHA v2) ─────────────────────────────────
|
|
ALTCHA_HMAC_KEY=
|
|
POW_REQUIRED=false
|
|
ALTCHA_COST=100000
|
|
|
|
# ── Registration gates (composable; each advertised via /capabilities) ────
|
|
# Require an operator-minted invite code to create a NEW wallet. Mint codes with
|
|
# `smirk-admin mint-invite --count <n>`. Returning wallets bypass it, and
|
|
# self-hosting bypasses all gates (run your own backend). PoW is the ALTCHA gate
|
|
# above. Compose freely (e.g. invite + PoW + pay-to-register).
|
|
REGISTRATION_REQUIRE_INVITE=false
|
|
|
|
# Pay-to-register: require a SETTLED payment invoice to create a new wallet. The
|
|
# invoice is minted at POST /auth/payment-invoice and paid — directly to YOUR own
|
|
# wallet — via an external, non-custodial processor. The `btcpay` adapter speaks
|
|
# the BTCPay-compatible invoice API, which also covers this project's xmrcheckout
|
|
# /wowcheckout apps and the BTCPay Monero plugin, so you pick the coin by pointing
|
|
# at the processor that accepts it. The backend only READS invoice status (pull
|
|
# model) — it never holds funds or keys. Self-hosting bypasses this (leave off).
|
|
REGISTRATION_REQUIRE_PAYMENT=false
|
|
PAYMENT_PROVIDER=btcpay
|
|
# Processor base URL, store/merchant id, and an API key that can create + read
|
|
# invoices (held redacted; never logged). Required when the gate is on.
|
|
PAYMENT_PROVIDER_URL=https://pay.example.org
|
|
PAYMENT_STORE_ID=
|
|
PAYMENT_API_KEY=
|
|
# Registration price + currency ('XMR', or a fiat code the processor converts).
|
|
PAYMENT_AMOUNT=0.01
|
|
PAYMENT_CURRENCY=XMR
|
|
# Confirmations-to-finalize (mapped to the processor's speed policy), and how long
|
|
# an unpaid invoice stays payable before it expires.
|
|
PAYMENT_CONFIRMATIONS=1
|
|
PAYMENT_EXPIRES_MINUTES=60
|
|
|
|
# ── Bitcoin ───────────────────────────────────────────────────────────────
|
|
BTC_NETWORK=mainnet
|
|
BTC_USE_LOCAL_NODE=false
|
|
BTC_RPC_URL=http://127.0.0.1:8332
|
|
BTC_RPC_USER=bitcoinrpc
|
|
BTC_RPC_PASS=
|
|
BTC_ELECTRUM_URL=
|
|
BTC_ELECTRUM_FALLBACKS=
|
|
|
|
# ── Litecoin ──────────────────────────────────────────────────────────────
|
|
LTC_NETWORK=mainnet
|
|
LTC_USE_LOCAL_NODE=false
|
|
LTC_RPC_URL=http://127.0.0.1:9332
|
|
LTC_RPC_USER=litecoinrpc
|
|
LTC_RPC_PASS=
|
|
LTC_ELECTRUM_URL=
|
|
LTC_ELECTRUM_FALLBACKS=
|
|
|
|
# ── Monero (daemon + light wallet server) ─────────────────────────────────
|
|
XMR_LWS_URL=http://127.0.0.1:8443
|
|
XMR_LWS_ADMIN_URL=http://127.0.0.1:9443
|
|
XMR_LWS_ADMIN_KEY=
|
|
XMR_DAEMON_URL=http://127.0.0.1:18081
|
|
|
|
# ── Wownero (daemon + light wallet server) ────────────────────────────────
|
|
WOW_LWS_URL=http://127.0.0.1:18443
|
|
WOW_LWS_ADMIN_URL=http://127.0.0.1:19443
|
|
WOW_LWS_ADMIN_KEY=
|
|
WOW_DAEMON_URL=http://127.0.0.1:34568
|
|
|
|
# ── Grin (wallet owner/foreign API + node API) ────────────────────────────
|
|
GRIN_OWNER_API_URL=http://127.0.0.1:3420/v3/owner
|
|
GRIN_OWNER_API_SECRET=
|
|
GRIN_WALLET_PASSWORD=
|
|
GRIN_FOREIGN_API_URL=http://127.0.0.1:3415/v2/foreign
|
|
GRIN_NODE_API_URL=http://127.0.0.1:3413/v2/owner
|
|
GRIN_NODE_API_USER=grin
|
|
GRIN_NODE_API_PASS=
|
|
GRIN_NODE_FOREIGN_API_URL=http://127.0.0.1:3413/v2/foreign
|
|
GRIN_NODE_FOREIGN_API_SECRET=
|
|
|
|
# ── Wallet restore (import) policy ────────────────────────────────────────
|
|
# How far back a wallet may ask THIS instance to scan on import. The backfill
|
|
# cost lands on your LWS/node, so it is your call; it is advertised via
|
|
# /capabilities so the wallet adapts its import UX. Self-sovereignty is
|
|
# preserved — a user needing a deeper restore runs their own instance or picks
|
|
# an operator with a looser policy.
|
|
# create-only — new wallets only (scan starts at the tip). DEFAULT — and it
|
|
# betrays nothing: self-sovereignty is run-your-own-backend.
|
|
# bounded — restore up to WALLET_MAX_RESTORE_DEPTH_DAYS behind the tip
|
|
# unlimited — any restore height (the natural choice for self-hosting)
|
|
WALLET_RESTORE_POLICY=create-only
|
|
WALLET_MAX_RESTORE_DEPTH_DAYS=365
|
|
# PoW-priced restore: charge the importer compute proportional to the scan depth
|
|
# they impose on you. Free below WALLET_RESTORE_POW_FREE_DAYS, then +1 hashcash
|
|
# difficulty bit per WALLET_RESTORE_POW_DAYS_PER_BIT days of depth (0 = pricing
|
|
# OFF), capped at WALLET_RESTORE_POW_MAX_BITS. Applies within bounded/unlimited.
|
|
WALLET_RESTORE_POW_FREE_DAYS=90
|
|
WALLET_RESTORE_POW_DAYS_PER_BIT=0
|
|
WALLET_RESTORE_POW_MAX_BITS=24
|
|
|
|
# ── Feature flags (each feature ships in the open core, toggled per deployment) ──
|
|
# The wallet client reads GET /api/v1/capabilities and greys out disabled features.
|
|
FEATURE_PRICES=true
|
|
PRICES_PROVIDER=coingecko
|
|
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).
|
|
# It answers "is this npub registered?" (a registration oracle) so it MUST stay
|
|
# loopback; the bundled compose reaches it via the host network.
|
|
RELAY_ADMISSION_BIND=127.0.0.1:8090
|
|
# Allow a NON-loopback admission bind (advanced; e.g. a relay container without
|
|
# host networking). Off by default; you must firewall it if you enable it.
|
|
RELAY_ADMISSION_ALLOW_PUBLIC=false
|
|
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.
|
|
ADMIN_ENABLED=false
|
|
ADMIN_BIND=127.0.0.1:8081
|
|
ADMIN_JWT_SECRET=
|
|
# Integrity MAC secret protecting admin/setup trust anchors against DB tampering.
|
|
# Losing this locks out admin — back it up (see docs/operations).
|
|
ADMIN_KEY_INTEGRITY_SECRET=
|
|
ADMIN_MAX_KEYS=8
|
|
ADMIN_PENDING_KEY_TTL_DAYS=7
|
|
|
|
# ── Public landing page (OFF by default; full + per-field tunable when on) ──
|
|
PUBLIC_LANDING_ENABLED=false
|
|
PUBLIC_EXPOSE_FEATURES=false
|
|
PUBLIC_EXPOSE_UPTIME=false
|
|
PUBLIC_STATS_ENABLED=false
|
|
PUBLIC_STATS_CACHE_HOURS=24
|
|
|
|
# ── Data retention / erasure ──────────────────────────────────────────────
|
|
ERASURE_ENABLED=false
|
|
ERASURE_PURGE_LOGIN_EVENTS=true
|
|
ERASURE_EXPORT_PER_DAY=3
|
|
# Log retention (auto-deletion); 0 = keep indefinitely.
|
|
RETENTION_LOGIN_EVENTS_DAYS=90
|
|
RETENTION_AUDIT_DAYS=365
|