mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
chore: initial foundation — server skeleton, autodocs, CI
- minimal Axum server exposing GET /health - OpenAPI contract generated from handlers (gen_openapi -> openapi.json), drift-gated in CI so the spec can never silently diverge from the code - CI: secret-scan + gitleaks, openapi drift gate, fmt/clippy/build/test/doc-test, cargo-deny - Cargo.lock tracked for reproducible builds; publish=false; pinned toolchain - README, SECURITY.md, deny.toml, .env.example
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# ── Proof-of-work signup gate (ALTCHA v2) ─────────────────────────────────
|
||||||
|
ALTCHA_HMAC_KEY=
|
||||||
|
POW_REQUIRED=false
|
||||||
|
ALTCHA_COST=100000
|
||||||
|
|
||||||
|
# ── 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=
|
||||||
|
|
||||||
|
# ── 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
|
||||||
|
|
||||||
|
# ── 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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
secret-scan:
|
||||||
|
name: secret scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: key / seed material
|
||||||
|
run: ./tools/secret-scan.sh
|
||||||
|
- name: gitleaks (full history)
|
||||||
|
run: |
|
||||||
|
curl -sSfL https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz \
|
||||||
|
| tar -xz gitleaks
|
||||||
|
sudo mv gitleaks /usr/local/bin/
|
||||||
|
gitleaks detect --no-banner --redact --source .
|
||||||
|
|
||||||
|
openapi-drift:
|
||||||
|
name: openapi drift gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Regenerate the spec; fail if it drifts from committed openapi.json
|
||||||
|
run: |
|
||||||
|
cargo run --quiet --bin gen_openapi > /tmp/openapi.json
|
||||||
|
diff openapi.json /tmp/openapi.json
|
||||||
|
|
||||||
|
rust:
|
||||||
|
name: fmt / clippy / build / test / doc-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
- run: cargo clippy --all-targets -- -D warnings
|
||||||
|
- run: cargo build --all-targets --locked
|
||||||
|
- run: cargo test --locked
|
||||||
|
- run: cargo test --doc
|
||||||
|
|
||||||
|
supply-chain:
|
||||||
|
name: cargo-deny
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
# Rust build artifacts
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Cargo.lock IS tracked — this is a deployable binary, so we want reproducible builds.
|
||||||
|
# (Do not ignore it.)
|
||||||
|
|
||||||
|
# Environment / secrets — never commit anything but the example.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
secrets/
|
||||||
|
secret_store/
|
||||||
|
|
||||||
|
# Runtime / data
|
||||||
|
*.log
|
||||||
|
data/
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Tor hidden-service keys (admin onion) must never enter the repo.
|
||||||
|
tor/
|
||||||
|
hs_ed25519_secret_key
|
||||||
|
|
||||||
|
# Editor / OS
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
Generated
+4324
File diff suppressed because it is too large
Load Diff
+115
@@ -0,0 +1,115 @@
|
|||||||
|
[package]
|
||||||
|
name = "smirk-backend-core"
|
||||||
|
version = "0.3.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["John Winter Murphy <jw@such.software>"]
|
||||||
|
description = "Open, self-hostable backend for the Smirk non-custodial multi-chain wallet"
|
||||||
|
repository = "https://github.com/smirk-cash/smirk-backend-core"
|
||||||
|
# license: TBD — deciding MIT vs AGPL-3.0-or-later (see README). Left unset until chosen.
|
||||||
|
# Not a library for crates.io; this is a deployable server. Guard against accidental publish.
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "smirk_backend_core"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "smirk-backend-core"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
# Additional binaries (gen_openapi, smirk-admin, ...) are auto-discovered from src/bin/*.rs.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Async Runtime
|
||||||
|
tokio = { version = "1.35", features = ["full"] }
|
||||||
|
|
||||||
|
# Web Framework
|
||||||
|
axum = { version = "0.7", features = ["ws", "macros"] }
|
||||||
|
axum-extra = { version = "0.9", features = ["typed-header"] }
|
||||||
|
tower = { version = "0.4", features = ["limit", "timeout", "load-shed", "util"] }
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "trace", "compression-gzip"] }
|
||||||
|
tower_governor = "0.4"
|
||||||
|
governor = "0.6"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "ipnetwork"] }
|
||||||
|
ipnetwork = "0.20"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
# OpenAPI generation (autodocs): the public API spec is derived from the handlers
|
||||||
|
# via utoipa, then drives the generated TypeScript client. Single source of truth.
|
||||||
|
utoipa = { version = "5", features = ["chrono", "uuid"] }
|
||||||
|
|
||||||
|
# HTTP Client
|
||||||
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||||
|
|
||||||
|
# Cryptography
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
hex = "0.4"
|
||||||
|
jsonwebtoken = "9.2"
|
||||||
|
rand = "0.8"
|
||||||
|
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||||
|
urlencoding = "2.1"
|
||||||
|
base64 = "0.22"
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
# Proof-of-work gate for wallet registration (ALTCHA v2, PBKDF2/SHA-256).
|
||||||
|
altcha = "0.1"
|
||||||
|
# Constant-time byte comparison for token/secret verification. Never use == on secrets.
|
||||||
|
subtle = "2.5"
|
||||||
|
# secp256k1 for Grin ECDH/ECDSA and Nostr (NIP-98) BIP-340 schnorr verification.
|
||||||
|
k256 = { version = "0.13", features = ["ecdh", "ecdsa", "schnorr"] }
|
||||||
|
# Ed25519 for website auth signature verification.
|
||||||
|
ed25519-dalek = "2.1"
|
||||||
|
|
||||||
|
# Bitcoin/Litecoin RPC
|
||||||
|
bitcoincore-rpc = "0.18"
|
||||||
|
|
||||||
|
# Electrum protocol (balance/UTXO/history via public or self-hosted servers)
|
||||||
|
tokio-rustls = "0.25"
|
||||||
|
rustls = "0.22"
|
||||||
|
rustls-pki-types = "1.0"
|
||||||
|
webpki-roots = "0.26"
|
||||||
|
bs58 = "0.5"
|
||||||
|
|
||||||
|
# Monero/Wownero address parsing
|
||||||
|
monero = "0.20"
|
||||||
|
|
||||||
|
# ZeroMQ (LWS real-time 0-conf notifications)
|
||||||
|
zeromq = "0.4"
|
||||||
|
|
||||||
|
# MessagePack (LWS msgpack wire format)
|
||||||
|
rmp-serde = "1.3"
|
||||||
|
|
||||||
|
# URL parsing
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
dotenvy = "0.15"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
|
|
||||||
|
# Time
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
|
# Async utilities
|
||||||
|
futures = "0.3"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
validator = { version = "0.16", features = ["derive"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio-test = "0.4"
|
||||||
|
mockall = "0.12"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "unwind"
|
||||||
|
strip = true
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# smirk-backend-core
|
||||||
|
|
||||||
|
Open, self-hostable backend for the [Smirk](https://smirk.cash) non-custodial,
|
||||||
|
multi-chain wallet. It provides the chain-access and identity surface a Smirk
|
||||||
|
wallet needs — authentication, per-chain wallet access (Bitcoin, Litecoin,
|
||||||
|
Monero, Wownero, Grin), the Grin slatepack relay, and Nostr-based identity —
|
||||||
|
so anyone can run their own backend and a Smirk wallet can connect to it.
|
||||||
|
|
||||||
|
> **Status: early, under active development.** Interfaces and the database
|
||||||
|
> schema may change before the first tagged release. Not yet recommended for
|
||||||
|
> production self-hosting.
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
- **Non-custodial.** The server never holds spend keys or seeds. It brokers
|
||||||
|
access to blockchain data and relays wallet-to-wallet messages.
|
||||||
|
- **Open core, feature-flagged.** Every feature ships in this codebase and is
|
||||||
|
toggled per deployment; the wallet reads `GET /api/v1/capabilities` and
|
||||||
|
adapts to what a given backend offers.
|
||||||
|
- **Identity via "Sign in with Smirk".** Authentication uses a seed-derived
|
||||||
|
Nostr key (NIP-98); the server stores only public keys.
|
||||||
|
- **Privacy first.** No PII is required to use it; operator and user privacy
|
||||||
|
are explicit design goals.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Rust + [Axum](https://github.com/tokio-rs/axum), PostgreSQL via
|
||||||
|
[sqlx](https://github.com/launchbadge/sqlx). The public HTTP contract is
|
||||||
|
generated from the handlers with [utoipa](https://github.com/juhaku/utoipa)
|
||||||
|
(OpenAPI), which in turn drives the wallet's generated TypeScript client —
|
||||||
|
one source of truth, checked for drift in CI.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env # then fill in DATABASE_URL, JWT_SECRET, node URLs
|
||||||
|
cargo run # starts the API server
|
||||||
|
```
|
||||||
|
|
||||||
|
A full self-hosting guide (node requirements, reverse proxy, Tor admin onion,
|
||||||
|
backups) will live in `docs/` as it stabilises.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Please report vulnerabilities privately — see [SECURITY.md](SECURITY.md).
|
||||||
|
Do not open public issues for security reports.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
To be finalized (MIT vs AGPL-3.0-or-later). Until a `LICENSE` file is added,
|
||||||
|
all rights are reserved.
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Please report security issues **privately**. Do not open public issues or pull
|
||||||
|
requests for vulnerabilities.
|
||||||
|
|
||||||
|
- Email: security@smirk.cash
|
||||||
|
- Encrypt sensitive reports with our PGP key (fingerprint published at
|
||||||
|
`https://smirk.cash/.well-known/security.txt`).
|
||||||
|
|
||||||
|
Include enough detail to reproduce: affected endpoint/component, version or
|
||||||
|
commit, and a proof of concept where possible. We aim to acknowledge reports
|
||||||
|
within 72 hours and to keep you updated as we triage and fix.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This repository (`smirk-backend-core`) — the self-hostable backend. Issues in
|
||||||
|
node software it talks to (Bitcoin/Litecoin/Monero/Wownero/Grin daemons,
|
||||||
|
light-wallet servers, Electrum) should be reported to those projects, though
|
||||||
|
we welcome reports about how this backend uses them.
|
||||||
|
|
||||||
|
## Operating securely
|
||||||
|
|
||||||
|
`smirk-backend-core` is non-custodial: it never stores seeds or spend keys.
|
||||||
|
The admin surface authenticates with seed-derived public keys ("Sign in with
|
||||||
|
Smirk") and stores only public keys. Default deployment posture binds admin and
|
||||||
|
setup to loopback / a Tor hidden service. See `docs/` for the hardening guide.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# cargo-deny configuration — supply-chain gate for the public backend.
|
||||||
|
# Run locally with: cargo deny check
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
version = 2
|
||||||
|
# Fail on known-vulnerable or yanked crates (RUSTSEC advisory DB).
|
||||||
|
yanked = "deny"
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
# Duplicate versions bloat the build and the audit surface; surface but don't block.
|
||||||
|
multiple-versions = "warn"
|
||||||
|
wildcards = "deny"
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
version = 2
|
||||||
|
# Permissive + weak-copyleft licenses we accept in the dependency tree.
|
||||||
|
allow = [
|
||||||
|
"MIT",
|
||||||
|
"Apache-2.0",
|
||||||
|
"Apache-2.0 WITH LLVM-exception",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"ISC",
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Zlib",
|
||||||
|
"MPL-2.0",
|
||||||
|
"CC0-1.0",
|
||||||
|
"OpenSSL",
|
||||||
|
]
|
||||||
|
confidence-threshold = 0.9
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
# Only crates.io; no unvetted git or alternate registries.
|
||||||
|
unknown-registry = "deny"
|
||||||
|
unknown-git = "deny"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {
|
||||||
|
"title": "smirk-backend-core",
|
||||||
|
"description": "Open, self-hostable backend for the Smirk non-custodial multi-chain wallet.",
|
||||||
|
"contact": {
|
||||||
|
"name": "John Winter Murphy",
|
||||||
|
"email": "jw@such.software"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
|
"version": "0.3.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"summary": "Liveness probe.",
|
||||||
|
"description": "Returns `200` with `{ \"status\": \"ok\" }` whenever the server is up. Used by\nload balancers and uptime checks; requires no authentication.",
|
||||||
|
"operationId": "health",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Service is up",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HealthResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HealthResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Liveness response.",
|
||||||
|
"required": [
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Always `\"ok\"` when the service is reachable."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "system",
|
||||||
|
"description": "Service health and metadata."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//! Health / liveness endpoint.
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Liveness response.
|
||||||
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct HealthResponse {
|
||||||
|
/// Always `"ok"` when the service is reachable.
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liveness probe.
|
||||||
|
///
|
||||||
|
/// Returns `200` with `{ "status": "ok" }` whenever the server is up. Used by
|
||||||
|
/// load balancers and uptime checks; requires no authentication.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/health",
|
||||||
|
responses((status = 200, description = "Service is up", body = HealthResponse)),
|
||||||
|
tag = "system"
|
||||||
|
)]
|
||||||
|
pub async fn health() -> Json<HealthResponse> {
|
||||||
|
Json(HealthResponse {
|
||||||
|
status: "ok".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//! HTTP API layer: handlers and the OpenAPI assembly.
|
||||||
|
|
||||||
|
pub mod health;
|
||||||
|
pub mod openapi;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//! OpenAPI specification assembly.
|
||||||
|
//!
|
||||||
|
//! `ApiDoc` aggregates the annotated handlers into the spec that is the single
|
||||||
|
//! source of truth for the API contract and the wallet's generated TypeScript
|
||||||
|
//! client. Add a path here once its handler carries `#[utoipa::path(...)]` and
|
||||||
|
//! its request/response types derive `utoipa::ToSchema`.
|
||||||
|
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
info(
|
||||||
|
title = "smirk-backend-core",
|
||||||
|
version = "0.3.0",
|
||||||
|
description = "Open, self-hostable backend for the Smirk non-custodial multi-chain wallet."
|
||||||
|
),
|
||||||
|
paths(crate::api::health::health),
|
||||||
|
components(schemas(crate::api::health::HealthResponse)),
|
||||||
|
tags((name = "system", description = "Service health and metadata."))
|
||||||
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//! Emit the OpenAPI 3 specification to stdout.
|
||||||
|
//!
|
||||||
|
//! The spec is derived from the handlers (`utoipa` annotations) and is the
|
||||||
|
//! single source of truth for the API contract and the generated TypeScript
|
||||||
|
//! client. Regenerate the committed contract with:
|
||||||
|
//!
|
||||||
|
//! cargo run --bin gen_openapi > openapi.json
|
||||||
|
//!
|
||||||
|
//! CI regenerates and diffs against the committed `openapi.json`; a drift is a
|
||||||
|
//! failed build, not a stale doc.
|
||||||
|
|
||||||
|
use smirk_backend_core::api::openapi::ApiDoc;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let spec = ApiDoc::openapi()
|
||||||
|
.to_pretty_json()
|
||||||
|
.expect("serialize OpenAPI spec");
|
||||||
|
println!("{spec}");
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
//! smirk-backend-core
|
||||||
|
//!
|
||||||
|
//! Open, self-hostable backend for the Smirk non-custodial multi-chain wallet:
|
||||||
|
//! authentication, per-chain wallet access (Bitcoin, Litecoin, Monero, Wownero,
|
||||||
|
//! Grin), the Grin slatepack relay, and Nostr-based identity.
|
||||||
|
//!
|
||||||
|
//! The HTTP contract is generated from the handlers (`utoipa`) into
|
||||||
|
//! `openapi.json`, which is the single source of truth for the API and the
|
||||||
|
//! wallet's generated TypeScript client.
|
||||||
|
|
||||||
|
pub mod api;
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
//! smirk-backend-core server entry point.
|
||||||
|
//!
|
||||||
|
//! Minimal server skeleton. Configuration, application state, the full router,
|
||||||
|
//! and background tasks are added as modules land; for now it serves the
|
||||||
|
//! health endpoint so the build, the OpenAPI contract, and deployment wiring
|
||||||
|
//! are exercised end to end.
|
||||||
|
|
||||||
|
use axum::{routing::get, Router};
|
||||||
|
use smirk_backend_core::api::health;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let app = Router::new().route("/health", get(health::health));
|
||||||
|
|
||||||
|
let addr = "0.0.0.0:8080";
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr)
|
||||||
|
.await
|
||||||
|
.expect("bind listener");
|
||||||
|
tracing::info!("smirk-backend-core listening on http://{addr}");
|
||||||
|
axum::serve(listener, app).await.expect("server error");
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# secret-scan: block private keys and seed material from entering the tree.
|
||||||
|
#
|
||||||
|
# Public-safe by design: it matches on secret *shapes* (key blocks, mnemonic
|
||||||
|
# markers), never on project-internal names. gitleaks runs alongside it in CI
|
||||||
|
# for broad token/credential patterns.
|
||||||
|
#
|
||||||
|
# ./tools/secret-scan.sh
|
||||||
|
#
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && [ -n "$(git ls-files)" ]; then
|
||||||
|
mapfile -t FILES < <(git ls-files)
|
||||||
|
else
|
||||||
|
mapfile -t FILES < <(find . -type f -not -path './.git/*' -not -path './target/*' -printf '%P\n')
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Secret shapes (not names): PEM/OpenSSH/PGP private-key blocks and seed phrases.
|
||||||
|
PATTERNS='(-----BEGIN ([A-Z ]+ )?PRIVATE KEY-----|-----BEGIN PGP PRIVATE KEY|BEGIN OPENSSH PRIVATE KEY|\b(mnemonic|seed[_ ]?phrase)\b\s*[:=])'
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
case "$f" in tools/secret-scan.sh) continue ;; esac
|
||||||
|
if grep -EnI "$PATTERNS" "$f" >/dev/null 2>&1; then
|
||||||
|
echo "SECRET-SCAN: possible key/seed material in $f"
|
||||||
|
grep -EnI "$PATTERNS" "$f" | sed 's/^/ /'
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo "SECRET-SCAN FAILED — remove key/seed material before committing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "secret-scan OK (${#FILES[@]} files checked)."
|
||||||
Reference in New Issue
Block a user