Wire the settings overlay into the live config path:
- AppState.config: Config -> Arc<ArcSwap<Config>> (hot-swappable effective config) +
config_base: Arc<Config> (immutable env-only). New AppState::cfg() accessor; every
config read goes through it (state.cfg().field), so a runtime-safe operator edit is
seen on the next request. One Config type, no parallel path.
- Boot (main.rs): load_settings_overlay -> Config::apply_overlay(validate) -> build
every client/worker/router from the EFFECTIVE config. A persisted overlay that no
longer validates is ignored (boot env-only + loud log), never bricks. Env-only when
ADMIN_KEY_INTEGRITY_SECRET is unset.
- Migrated ~59 state.config.X reads to state.cfg().X across api/*; bound guards where a
reference outlives the statement; integrity_secret returns owned String.
- Test harness builds AppState with ArcSwap + config_base (arc-swap dev-dep).
Behavior-preserving: with no operator_settings rows, effective == env config.
cargo check --all-targets clean; lib tests 256/0.
Adversarial review of the auth/registration surface (findings tracked
privately). Backend half:
- F2: npub-native register no longer binds an npub onto an existing
wallet row on a seed_fingerprint match alone (unauthenticated account
takeover). get_or_create_user_by_nostr_pubkey fails closed (409);
linking an npub goes through the authenticated /auth/nostr/link flow.
Integration test rewritten to assert the refuse-and-409 contract.
- F3: `all`-mode gate verifies payment is settled (non-consuming) BEFORE
consuming the single-use invite, so a still-confirming payment can't
permanently burn the invite. verify_payment_settled split out.
- F4: username availability is pre-checked before any gate token is
consumed; collisions return 409, not 500 — a taken username no longer
burns a paid invoice / invite.
- F6: nostr_register now persists the submitted chain keys, so
receive-by-username and restore self-verification work for npub wallets.
- F10: the pay-to-register not-yet-settled response carries a stable
PAYMENT_PENDING code so polling clients don't parse a free-text string.
Validated against Postgres: full suite 283 passed / 0 failed.
Make npub the identity anchor: a wallet can register from its seed-derived Nostr
key alone, no BTC signature. Breaks the link chicken-and-egg (nostr_login is
create-never; nostr_link needs a prior BTC-authed JWT). Additive - /auth/extension
(BTC) and the link flow keep working for legacy backends during the transition.
- get_or_create_user_by_nostr_pubkey (db twin of the BTC path): keys/inserts on
nostr_pubkey (raw, never peppered), pubkey_hash NULL. THE load-bearing dedup:
looks up by seed_fingerprint and MERGES the npub onto an existing (BTC-anchored
or link-less) row rather than splitting one wallet into two identities; a
different npub already on that row fails closed (409). backfill_optionals shared
helper.
- POST /auth/nostr/register + GET /auth/nostr/register-challenge: verify a NIP-98
signed-action over a subject-less single-use nonce (replay-safe), then re-run
the abuse gates keyed on the npub (a create path must not bypass PoW/invite/
payment); pay-to-register invoices still bind to the BTC key carried in keys[].
Empty-body descriptor contract (mirrors /auth/nostr/link); binding the key list
into the proof is a documented hardening follow-up.
- Housekeeping: get_user_count counts npub-only users; record_login_event labels
the nostr path.
3 integration KATs (create-by-npub + idempotent, replay rejected, seed_fingerprint
MERGE dedup) pass against a real DB; 211 lib tests pass; openapi regenerated.