website_verify resolved every asset through get_user_by_pubkey_hash, but
users.pubkey_hash only ever holds the BTC key's hash. On this backend only BTC
could ever resolve; ltc, xmr, wow and grin were structurally impossible, and the
failure was indistinguishable from an unregistered user, so the page told people
to register with an extension they already had.
That did not surface while smirk.cash pointed at the legacy backend, which
resolves by asset key against its own database. It becomes the whole bug the
moment the site is repointed here, so it has to land with that change, not after.
get_user_by_pubkey_hash stays as the BTC fallback: of 95 users on this backend,
84 arrived through migrate-legacy, which imported identity without keys, and
would otherwise stop resolving entirely.
The rejection message is unchanged and still says nothing about whether an
account exists.
The repo pins channel=stable (no rustfmt version), so CI now pulls
rustfmt 1.9.0, which reformats code last touched by an older rustfmt.
main itself failed `cargo fmt --check` under the current toolchain;
this is a formatting-only sweep across 24 files to make the fmt gate
green. No behavior change; tests + clippy -D warnings still pass.
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.
9 confirmed findings (review wbdf3c0sd), all fixed:
S4 (capabilities):
- REGRESSION: /capabilities reported grin_relay:false for a working relay (S4
wrongly coupled it to Grin chain serviceability). The relay is a non-custodial
mailbox — wallet broadcasts locally — so it is decoupled again. Removed the now-
dead grin_relay downgrade note.
S3 (bootstrap latch):
- A DELETE of the latch row escaped Locked without Tampered detection on a
CLI-bootstrapped-but-no-users instance (adoption keyed off `users` only). Boot
now adopts on has_any_users() OR has_any_admin_keys(), so a deleted latch
re-latches Locked instead of silently dropping to uninitialized.
- bootstrap_admin's single-bootstrap guard was a check-then-insert race (two
concurrent `setup` could seed two admins). It now takes the admin-keys mutation
advisory lock (shared with add/revoke/rotate) at the top of its tx.
S2 (admin posture + landing):
- Hard bind guard matched loopback by string prefix ("localhost".starts_with) —
"localhost.evil.com" slipped past. Now resolves via to_socket_addrs and requires
EVERY resolved addr to be loopback.
- host_allowed's IPv6 loopback check was an unanchored prefix ("[::1]" matched
"[::1].evil"); now anchored to exactly "[::1]" / "[::1]:port". +regression tests.
- Anti-clickjacking headers (X-Frame-Options/CSP) are now applied on the 403
Host-reject path too (computed on both branches, headers added once).
- GET /api/v1/server-info now returns a BARE 404 when landing is off (the
{error,code} envelope itself hinted the route exists).
- Public landing `chains` now uses the SAME serviceability predicate as
/capabilities (no over-advertising an unserviceable chain).
- Removed the dead/misleading PUBLIC_EXPOSE_FEATURES toggle (never read; the
feature tuple is intentionally never emitted anonymously).
Dismissed after verification: latch chains-by-flag (dup), title length bound
(operator-owned + escaped), prices:true with empty PRICES_ASSETS (documented).
140 lib + 140 integration green, no openapi drift, scans clean.
Admin posture (§2.1/§2.2):
- Hard fail-closed bind guard in main: a non-loopback ADMIN_BIND refuses startup
unless ADMIN_ALLOW_PUBLIC_BIND=true (was a warning).
- admin_plane_guard middleware on the admin router: Host allowlist
(localhost/127.0.0.1/[::1]/configured TOR_ADMIN_ONION; a missing Host is allowed
since rebinding must supply one and the bind is the primary control) to defeat
DNS-rebinding against the loopback socket, plus X-Frame-Options: DENY +
CSP frame-ancestors 'none' (anti-clickjacking). Still no CORS on the admin plane.
Public landing (§2.3), default-OFF, one fingerprint surface:
- GET /api/v1/server-info — built from a hand-written ServerInfo allowlist (never
by serializing config/state). Default projection is minimal { software.name,
status }; version (coarse major.minor), chains, price_feed, and uptime (bool
`up` only) are each individually opt-in. The enabled-feature tuple is NEVER
emitted anonymously (connected clients use /capabilities).
- GET / — minimal HTML (operator title HTML-escaped); bare 404 when off (does not
hint an admin plane exists). server-info also 404s when off.
- config: ADMIN_ALLOW_PUBLIC_BIND, TOR_ADMIN_ONION; PUBLIC_LANDING_TITLE +
per-field PUBLIC_EXPOSE_VERSION/CHAINS/PRICE_FEED toggles.
Tests: host_allowlist + html_escape unit tests; landing off->on minimal-projection
(asserts only software+status, no version/chains/features); admin-plane Host-403 +
anti-clickjacking headers. Landing/admin stay off the public OpenAPI.
Deferred (documented): §2.5 NIP-05 consent-gating (changes existing
/.well-known/nostr.json behavior — needs the standing v0.2.4-compat verification
first), §2.3 k-anon public stats, response-header/TLS-fingerprint stripping (proxy
/ops-doc concern).