mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
Backend support for two address-privacy features. Both are behind env flags
that default to off, so an unconfigured instance behaves exactly as before:
the routes are not mounted and no provisioning is attempted.
FEATURE_XMR_SUBADDR_PROVISIONING
Adds POST /wallet/lws/provision_subaddrs and provisions account-0 subaddress
indices on the LWS at register/import.
Three money-safety properties worth calling out:
* The provisioned ceiling is read from the LWS response, never echoed back
from the request. It is the contiguous run starting at minor 0, not
max(hi): given ranges [[0,99],[200,299]], reporting 299 would let a wallet
hand out indices 100..199 that the LWS never scans, so funds sent there
would be invisible. No usable run is a hard error, not a guess.
* import_account now always runs the backward rescan even when provisioning
fails. Previously a provisioning error aborted first, leaving the account
registered at the chain tip while every retry returned 200 OK, so a
restoring wallet would show a zero balance permanently.
* A capacity probe reads max_subaddresses from the LWS and refuses up front
with a message naming the required --max-subaddresses value, rather than
failing per-account later. The option defaults to 0 upstream.
Spent outputs now carry their subaddress index. monero-lws emits this as
`sender` (the index the spent output was received at), not `recipient`;
without it a client recomputes a primary key image for a subaddress spend,
treats the mismatch as a decoy, and never subtracts the amount.
FEATURE_UTXO_MULTI_ADDRESS
Adds balance_multi / utxos_multi / history_multi for HD gap-limit wallets.
Capped at 32 addresses, summed with checked_add, per-UTXO address tag
preserved so the client never has to re-guess which key owns an output.
Bounded concurrency with a per-batch deadline returning 504, so a slow
Electrum server cannot stack n * IO_TIMEOUT.
Both flags are advertised in /capabilities so a client can negotiate instead
of calling routes that 404.
344 lines
18 KiB
Bash
344 lines
18 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 — set this to YOUR real domain. It is the canonical
|
|
# value the NIP-98 `u` tag is verified against (never the request Host header). In
|
|
# production, boot fails closed if this is left as a placeholder (contains `example`
|
|
# or `your-`) while Nostr identity (on by default), admin, or erasure is enabled —
|
|
# so every real deployment must replace it.
|
|
PUBLIC_API_URL=https://backend.example.org/api/v1
|
|
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_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) ─────────────────────────────────
|
|
# FEATURE_POW is the master switch; POW_REQUIRED only applies when it is on. Boot
|
|
# fails closed if POW_REQUIRED=true while FEATURE_POW=false (a silently-open gate).
|
|
FEATURE_POW=false
|
|
ALTCHA_HMAC_KEY=
|
|
POW_REQUIRED=false
|
|
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) ────
|
|
# 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
|
|
|
|
# How the enabled invite/payment gates COMBINE (PoW is orthogonal and always
|
|
# applies on top). 'all' (default): a new wallet must satisfy EVERY enabled gate
|
|
# (invite AND payment when both are on). 'any': the enabled gates are
|
|
# ALTERNATIVES and the wallet satisfies exactly ONE (e.g. "an invite code OR pay
|
|
# to register"); the client shows a "pick a method" chooser. With a single gate
|
|
# enabled the two modes are identical.
|
|
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
|
|
|
|
# ── Address-privacy features (opt-in) ─────────────────────────────────────
|
|
# Per-payment receive addresses. Off by default; enable once your node stack
|
|
# meets the requirement noted below.
|
|
#
|
|
# FEATURE_XMR_SUBADDR_PROVISIONING: provisions account-0 subaddress indices on
|
|
# the LWS at register/import so per-payment XMR/WOW receive addresses are
|
|
# scanned. REQUIRES your monero-lws / wownero-lws to run with
|
|
# `--max-subaddresses >= 200` (it defaults to 0, which disables subaddress
|
|
# support entirely). Check yours before enabling.
|
|
FEATURE_XMR_SUBADDR_PROVISIONING=false
|
|
#
|
|
# FEATURE_UTXO_MULTI_ADDRESS: mounts the batched BTC/LTC endpoints
|
|
# (/wallet/utxo/{balance,utxos,history}_multi) used by HD gap-limit wallets,
|
|
# capped at 32 addresses per request. When off, the routes are not mounted and
|
|
# the single-address endpoints are unchanged.
|
|
FEATURE_UTXO_MULTI_ADDRESS=false
|
|
|
|
# ── Bitcoin (Electrum/Fulcrum only; the backend runs no Core RPC node) ─────
|
|
BTC_NETWORK=mainnet
|
|
BTC_ELECTRUM_URL=
|
|
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) ────
|
|
LTC_NETWORK=mainnet
|
|
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=
|
|
|
|
# ── Grin light-wallet-server (grin-lws) — optional add-on to the Grin chain ─
|
|
# When GRIN_LWS_URL is set (and FEATURE_GRIN is on) the Grin scan proxies to
|
|
# grin-lws first, trusting it only when it is provably synced to the tip, and
|
|
# otherwise falling back to the authoritative grin-wallet scan. Leave GRIN_LWS_URL
|
|
# empty to use grin-wallet only. GRIN_LWS_ADMIN_URL / GRIN_LWS_ADMIN_KEY reach
|
|
# grin-lws's bearer-gated admin API (optional).
|
|
GRIN_LWS_URL=
|
|
GRIN_LWS_ADMIN_URL=
|
|
GRIN_LWS_ADMIN_KEY=
|
|
|
|
# ── 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
|
|
# 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.
|
|
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) ────
|
|
# 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).
|
|
# premium-post — registered users post wallet events (DMs) free; general
|
|
# notes require an active PREMIUM_* subscription.
|
|
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
|
|
# Write-exempt npubs (comma-separated; npub1… or 64-char hex): may publish ANY
|
|
# kind regardless of the write policy or premium status. Use this for an
|
|
# announcements / feed-owner account that seeds a premium-post feed without
|
|
# holding a subscription. Empty = no exemptions.
|
|
RELAY_WRITE_ALLOWLIST_NPUBS=
|
|
# 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
|
|
|
|
# ── Premium relay tier (optional; recurring, tiered relay-posting access) ──
|
|
# Wallet use stays free; premium unlocks GENERAL Nostr posting to the relay
|
|
# (wallet-functional events — DMs, tips, swap coordination — stay free). Requires
|
|
# RELAY_ENABLED=true + RELAY_WRITE_POLICY=premium-post + the PAYMENT_* processor
|
|
# block above. Off by default. See docs/design/premium-relay.md.
|
|
PREMIUM_ENABLED=false
|
|
# Price currency: a fiat code the processor converts, or a specific coin.
|
|
PREMIUM_CURRENCY=USD
|
|
# Comma-separated plans, each `id:days:amount` (a built-in tiered discount).
|
|
PREMIUM_PLANS=quarter:90:5,halfyear:182:9,year:365:15
|
|
|
|
# ── Public curated feed (optional; powers a read-only web feed like feed.<domain>) ──
|
|
# Reads FROM the relay, so it needs RELAY_ENABLED. All public display config —
|
|
# what the feed shows. Off by default; advertised via /capabilities.feed when on.
|
|
FEED_ENABLED=false
|
|
FEED_SHOW_OWNER=true
|
|
FEED_SHOW_PREMIUM=true
|
|
# Operator npub (hex or npub1…) for "owner" posts + display; empty if none.
|
|
FEED_OWNER_NPUB=
|
|
# Comma-separated featured npubs, and extra relays to also pull those authors from.
|
|
FEED_ALLOWLIST_NPUBS=
|
|
FEED_EXTRA_RELAYS=
|
|
|
|
# ── 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,
|
|
# 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_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
|
|
# 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
|
|
# distinct one. Must be an absolute URL when ADMIN_ENABLED=true.
|
|
ADMIN_PUBLIC_URL=https://admin.example.org
|
|
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
|
|
# 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_ENABLED=false
|
|
# Optional custom landing title (leave unset for the default).
|
|
#PUBLIC_LANDING_TITLE=
|
|
PUBLIC_EXPOSE_VERSION=false
|
|
PUBLIC_EXPOSE_CHAINS=false
|
|
PUBLIC_EXPOSE_PRICE_FEED=false
|
|
PUBLIC_EXPOSE_UPTIME=false
|
|
PUBLIC_STATS_ENABLED=false
|
|
PUBLIC_STATS_CACHE_HOURS=24
|
|
|
|
# ── 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_PURGE_LOGIN_EVENTS=true
|
|
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.
|
|
RETENTION_LOGIN_EVENTS_DAYS=90
|
|
RETENTION_AUDIT_DAYS=365
|