jwinterm eb98e4d625 feat(premium): recurring tiered relay-posting subscription (premium-post)
A paid premium tier gating GENERAL Nostr posting to the operator's relay, while
wallet-functional events (encrypted DMs, tips, swap coordination) stay free for
registered users. Registration itself stays free (PoW-gated). Off by default.

- Admission: a new `premium-post` write policy gates on kind x membership —
  premium author -> any kind; registered + WALLET_KINDS (1059 gift-wrap, 10050
  DM-relay-list) -> free; registered + general -> premium required; external ->
  gift-wrap DM inbox only. nauthz resolves is_premium_npub (only under premium-post),
  fail-closed.
- Payment: reuses the PaymentProvider seam (PULL model, non-custodial — only reads
  invoice status). POST /premium/invoice {plan} mints a tier invoice bound to the
  JWT user_id; POST /premium/activate verifies Settled + consumes single-use AND
  extends premium_until in ONE transaction (a mid-flight failure keeps the paid
  invoice redeemable); GET /premium/status reads it. Per-user pending-invoice cap.
- Tiered plans (PREMIUM_PLANS id:days:amount) = a built-in discount feature;
  advertised via /capabilities (premium_relay + premium{currency,plans,relay_url}).
- Config fail-closes: PREMIUM_ENABLED needs relay on + premium-post + processor
  creds + >=1 positive-decimal plan + unique ids + days<=3660, and the 0-conf /
  expiry processor floors apply on the premium path too (its common case is
  require_payment=false).
- Tests: 15 policy unit (incl. 5 premium-post) + config fail-closed matrix + plan
  parser + L1 integration (activate single-use/stacking/cross-user, is_premium by
  expiry, feature gate). OpenAPI + migration updated.

Adversarially reviewed (correctness / security-payment / operational-config); the
0-conf-on-premium-path double-spend footgun, non-transactional activate, and
unbounded plan days were caught and fixed before commit.
2026-07-02 17:56:44 -04:00

smirk-backend-core

Open, self-hostable backend for the Smirk non-custodial multi-chain wallet. Rust + Axum + PostgreSQL.

It gives a wallet chain access (balances, history, UTXOs, spend inputs, fee estimation, broadcast), Nostr-native identity, a fiat price feed, and an async Grin slatepack relay — without ever holding a spend key or seed. Run your own; the wallet is backend-agnostic.

Status: v0.3.0 — feature-complete and security-reviewed, but young. The schema/API may still evolve. Run your own chain backends where you can, review the code, and treat early deployments accordingly.

Non-custodial by construction

The server stores public addresses, public keys, and (for Monero/Wownero) incoming view keys only — never a spend key, never a seed. View credentials are forwarded per request to the chain backends that scan with them and are not persisted here. The wallet signs and broadcasts locally; the backend reads chains and relays bytes.

Chains

Chain Source Notes
Bitcoin, Litecoin Electrum / Fulcrum reads, fee estimation, broadcast
Monero, Wownero light-wallet-server (LWS) stateless view-key forwarding
Grin grin-wallet (view-only) + node rewind_hash scan, broadcast, slatepack relay

Each chain is independently feature-flagged; a chain whose source isn't configured is reported enabled: false by /capabilities rather than failing at call time.

API

The HTTP contract is generated from the handlers and committed to openapi.json — the single source of truth for the API and the wallet's generated client. A CI gate fails the build on any drift.

Public surface (/api/v1):

  • Identity & auth — NIP-98 (Nostr) sign-in + link, wallet-signature auth, audience-separated JWT sessions, NIP-05 directory (/.well-known/nostr.json).
  • Chain access — per-chain balance / history / UTXOs / spend inputs / fee / broadcast.
  • Grin relay — non-custodial store-and-forward mailbox for interactive Grin transfers (feature-flagged).
  • Capabilities (/capabilities) — what this instance enables, so the wallet adapts per-instance.
  • Prices (/prices) — cached fiat feed, per-feed operator control.
  • Self-service erasure (/account/erasure, /account/export) — action-bound, two-phase delete-my-data + export.
  • Health (/health) and an optional, default-off public landing (/).

Operator surface

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 router or OpenAPI.

  • 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 run without both.
  • First-run bootstrap — an explicit, MAC-protected latch (no trust-on-first- use); a live deployment adopts as already-bootstrapped; tampering fails closed.
  • smirk-admin CLI — break-glass key management, create-admin-wallet (zeroized), doctor. Talks to Postgres directly, bypassing the network plane.
  • Tamper-evident audit — privileged actions are written to a hash-chained audit trail, fail-closed (the state change rolls back if the audit write fails).

Running it

Requires Rust (stable) and PostgreSQL.

cp .env.example .env          # then edit: DATABASE_URL + the required secrets
createdb smirk_backend_core   # or point DATABASE_URL at an existing database
cargo run --release           # migrations run automatically on startup

The server fails closed: it refuses to start on a missing/weak secret or an inconsistent feature configuration rather than booting with a control silently defeated. Generate secrets with openssl rand -hex 32. See .env.example for the full, documented configuration surface.

First admin (headless bootstrap) — run one of these; both latch, so the other is refused afterward by design:

smirk-admin setup --pubkey <x-only-hex>       # seed YOUR pubkey (active immediately)
smirk-admin create-admin-wallet --out key.hex # generate a key (pending) + latch, no setup needed

<x-only-hex> is the admin's Sign-in-with-Smirk (Nostr) public key; the admin then authenticates with the matching key over NIP-98. Full deployment guide: docs/operations/OPERATOR_SETUP.md.

Security posture

  • Fail-closed configuration — the only place that reads the environment; validates secrets (length + placeholder checks) and feature consistency at boot.
  • Identity at rest is peppered — pubkey_hash / seed_fingerprint are stored as HMACs, so the database is not a seed-existence oracle or a cross-instance linker.
  • Strict JWTs — HS256 pinned, zero leeway, audience-separated; admin tokens are cryptographically distinct from user tokens.
  • Bounded, hardened upstream I/O — TLS with hostname verification, streaming size caps, and per-request timeouts on every external call.
  • Adversarially reviewed — every security-critical subsystem was built clean (never ported) and put through a multi-agent adversarial review before landing.

Report vulnerabilities privately — see SECURITY.md. Please do not open public issues for security reports.

Testing

cargo test                                        # unit + doc tests (no database needed)
TEST_DATABASE_URL=postgres://… cargo test --tests # + L1 integration against Postgres

Integration tests self-skip when TEST_DATABASE_URL is unset. They use only deterministic, non-sensitive test secrets and ephemeral identities — never a funded or otherwise sensitive wallet seed.

License

MIT © Such Software LLC — run it, embed it, modify it. Built for interop.

S
Description
Mirror of github.com/Such-Software/smirk-backend-core
Readme MIT
2.1 MiB
Languages
Rust 97.6%
TypeScript 1.4%
Shell 0.6%
PLpgSQL 0.3%