3 Commits
Author SHA1 Message Date
jwinterm 4c83e53839 feat(config): effective config via ArcSwap + DB overlay at boot (Stage 2)
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.
2026-07-18 06:00:16 -04:00
jwinterm 62104ca85e feat(admin): allowlist key CRUD + kill-switches [operator §1.5]
Admin-plane key management, all admin_guard-protected and fail-closed audited:

- POST /admin/keys: add a pending key (validated 64-char lowercase x-only hex;
  created_by_kind=admin; activation_deadline = now + ADMIN_PENDING_KEY_TTL_DAYS).
  Capped at ADMIN_MAX_KEYS live keys (anti allowlist-stuffing).
- GET /admin/keys: list active/pending/revoked (sanitized view + status).
- DELETE /admin/keys/🆔 soft-revoke + cascade-revoke its sessions + audit in ONE
  tx. Refuses to revoke the LAST live key over the network (CLI-only), so an
  operator can't lock everyone out.
- POST /admin/keys/:id/rotate: atomic revoke-old(+sessions)+add-new(pending)+audit
  in one tx — permitted on the last key (add+revoke, not bare revoke), the in-band
  recovery for a compromised solo key.

db: create_admin_key refactored onto a shared insert_admin_key(&mut PgConnection)
helper; create_admin_key_audited / revoke_admin_key_full / rotate_admin_key run
their state change + session cascade + hash-chained audit in a single transaction.
1 L1 test extended to cover add/list/rotate/revoke + malformed-pubkey rejection.

NOTE: the live-key cap and last-key revoke guard are count-based on a table the
shared test DB pollutes across suites, so their boundaries aren't integration-
asserted (covered by review); the happy path raises the cap.
2026-06-26 06:46:07 -04:00
jwinterm 4489c1c540 feat(admin): admin auth flow + guard + loopback plane [operator §1.1-1.4, §2.1]
"Sign in with Smirk" for operators. Auth is a NIP-98 signed action over a
single-use nonce (AUTHN) + the MAC'd allowlist (AUTHZ), composed in admin_guard
so a protected route physically cannot run without both. All rejections collapse
to one opaque 401 (no enumeration oracle).

- src/api/admin.rs: admin_guard (verify access token -> live session by jti ->
  live MAC-verified, activated allowlist entry) + AdminContext; handlers:
  - challenge: mint a 120s admin_login nonce + return the {url, instance_id} the
    wallet must bind.
  - verify: verify_signed_action (purpose/nonce/descriptor/instance) THEN consume
    the nonce (forged sig can't burn a victim's nonce; replay loses the race) ->
    authorize (pending key allowed for first login, which activates it) -> mint +
    persist session AND the login audit row in ONE tx (fail-closed) -> bump
    last_used.
  - refresh: re-run FULL authorization before minting (closes the "8h refresh is
    the real blast radius" gap) + constant-time refresh-hash bind + rotate jti so
    the old access token dies.
  - logout: revoke the session; audit. me: guarded whoami.
- admin_router (lib): served on a SEPARATE loopback socket in main (confidentiality
  by socket, not middleware); no CORS layer (cross-origin browser calls blocked);
  warns if bound non-loopback (hard bind-guard lands with admin posture). Routes
  are intentionally absent from the public OpenAPI.
- config: ADMIN_PUBLIC_URL (the signed-action u-tag base; validated when enabled).
  AppState.admin_sessions (Some only when enabled). AdminSessionManager.mint_access
  (refresh) + db helpers (create_admin_session_audited, touch_admin_key_last_used).
- test harness: admin_router() + request_on(router); 1 L1 test exercising the full
  flow end-to-end incl. user-token rejection, replay rejection, jti rotation, and
  post-logout revocation.

NOTE (for the upcoming S1 review): admin-login timing-oracle hardening (fixed-cost
allowlist probe + jittered 401 floor, spec §1.3) is deferred — the loopback/onion
exposure + single-use nonce + signature gate are the v1 boundary. Per-pubkey
backoff (§0.5) also pending.
2026-06-26 06:36:55 -04:00