mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
Addresses every Critical/High/Medium finding from the code-quality audit, plus the operator request to persist NIP-86 admin state and NIP-11 doc mutations across restarts. ## Security (audit Critical) - C1 — NIP-98 verifier no longer trusts the client-supplied `Host` header. New `LocalRelayServer.publicUrl` (and `[admin].public_url`) must be set in any production deployment; the verifier compares against this fixed string. Falls back to Host for loopback unit tests with a docstring warning. - C2 — NIP-98 replay protection. Verifier now keeps a bounded LinkedHashMap of recently-accepted event ids (TTL = 2× tolerance, max 1024 entries, LRU evicted) and rejects duplicates. Replay check runs LAST so a one-shot id isn't burned on a request that fails signature/url/method validation. - C3 — NIP-86 admin POST body is now capped at 1 MiB (configurable via `LocalRelayServer.maxAdminBodyBytes`). The `Content-Length` header is honored as a fast pre-read reject; any body that exceeds the cap during streaming returns 413 PayloadTooLarge before being buffered. ## Concurrency / correctness (audit High) - H1 — Per-session writer coroutine + bounded outbound queue (SESSION_OUTGOING_BUFFER = 1024). When the queue fills, the connection is closed cleanly instead of silently `trySend`-ing into a void; subscribers will never silently miss EVENT/EOSE again. - H3 — `BanStore` kind ops serialize through a new `kindLock` so concurrent admin RPCs and policy reads see consistent state. `allowKind` and `disallowKind` are now symmetric: each removes the kind from the opposite set, so `allowKind(K)` after `disallowKind(K)` actually re-allows K. - H4 — `InProcessWebSocket` reconnect after disconnect is now supported. Each `connect()` allocates a fresh CoroutineScope + drainer channel, so a prior `disconnect()` (which cancelled the previous scope) doesn't leave the new connect with a dead drainer. - H5 — `Nip86Server.dispatch` re-throws `CancellationException` through the runCatching's getOrElse so structured concurrency works (cancellation no longer reported as an RPC error). - M1 — `LiveEventStore.query` dedupes events between the historical replay and the live SharedFlow during the in-flight overlap window. Events seen during `store.query` are tracked in a transient set and filtered out of the live stream until EOSE; the set is dropped after EOSE so live-only events don't accumulate memory. ## Operator-facing (audit Medium / Low) - L4 — Audit-trail log: every admin RPC writes a single structured line to stderr (pubkey + method + ok/error). - L3 — RelayConfig.resolveInfo() default supported_nips list now matches RelayInfo.default() (both advertise NIP-86). - M2 — Hex-id and pubkey params validated as 64-char hex on ban/allow/list methods; malformed input returns "invalid params" instead of being silently stored. - M4 — argparse supports `--key=value` form in addition to `--key value`. - M5 — `RelayHub.close()` is idempotent and rejects subsequent `getOrCreate` calls so a relay created mid-shutdown can't leak its store. - M7 — `LocalRelayServer.stop()` sets a `shuttingDown` flag *before* notifying clients; new WebSocket upgrades during the grace window are rejected so they don't miss the NOTICE. - L5 — Shutdown hook runCatching-wraps both `server.stop()` and `relay.close()` so a throw in one doesn't skip the other. - `--verify` was renamed to `--no-verify` semantically (verify is the default). The `--verify` flag is no longer documented. ## On-disk persistence (operator request) New `RelayStateStore` writes a single JSON sidecar file holding the live NIP-11 doc + all NIP-86 ban / allow / kind lists. Atomic write via temp + ATOMIC_MOVE rename so a crash mid-save can never leave the file half-written. - `BanStore` now takes an optional `onMutation: () -> Unit` callback; every mutation fires it. `Relay` wires it to `snapshot()` which captures BanStore + info into a `RelayPersistedState` and calls `RelayStateStore.save`. - `Relay` accepts a new `stateFile: File?` constructor arg. At construction it loads the snapshot via `RelayStateStore.load` and seeds the in-memory state — using a private `seedFromSnapshot` bulk-load that bypasses the mutation callback so we don't write back what we just read. - New config field `[admin].state_file = "/var/lib/quartz-relay/events.db.admin.json"`. Convention: place next to the SQLite event-store file. - Corrupt state files are tolerated: log to stderr and start fresh (refuse to silently overwrite operator data). ## Tests 8 new persistence tests: cold-boot writes nothing, ban/allow/info round-trip across restart, kind allow/deny round-trip, corrupt-file recovery, atomic write (no leftover .tmp), in-memory mode when no state file is configured, full RelayStateStore round-trip across all sections. Total :quartz-relay tests: 95, 0 failures.