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