fix: typed config — single source of defaults, load-time validation, key hygiene#231
Conversation
…key hygiene Configuration correctness was enforced ad hoc in three scattered ways, so misconfiguration failed silently at runtime instead of loudly at load. This centralizes it: - Single source of defaults (#171): drop the `set_default` ladder; the serde `default_*` fns (backed by the `DEFAULT_*` consts) are now the only place a default lives. The builder seeds each section as an empty table so `try_deserialize` reaches those defaults even when a section is absent. - Reject-zero / NonZeroUsize for the cache and token sizes (#149, #166): `max_dedup_cache_size`, `max_rate_limit_cache_size`, and `max_tokens_per_event` reject `0` at load; the downstream configs take `NonZeroUsize`, deleting the silent zero-to-default fallbacks in `rate_limiter.rs` and `events.rs`. - LogFormat enum (#150, #142): `logging.format` is a validated enum; unknown values (including the undocumented `off` blackout arm) are rejected with a message pointing at `logging.level = "off"`. - health.bind_address SocketAddr validation (#167): a hostname or out-of-range port is rejected at load, even when the health server is disabled. - Onion relay URL validation (#122): each `relays.onion` entry must parse as a ws://wss:// URL with a `.onion` host, symmetric with clearnet. - Inbox relay change-detection (#124): normalize both sides through `RelayUrl` and select the newest kind-10050 event by `created_at`. - Key-file permission check (#146): on unix, refuse to start when `private_key_file` grants group/other read (mode & 0o077), mirroring the 0600 the generate path enforces. - Private key out of the config Value tree (#156): the inline `server.private_key` and `TRANSPONDER_SERVER_PRIVATE_KEY` value are resolved through a dedicated Zeroizing path before parsing, so the secret never sits in the config crate's un-zeroized buffers. - #196 rider: warn at load when metrics.enabled && !health.enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # config/default.toml
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 17 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Ready to review this PR? Stage has broken it down into 7 individual chapters for you: Chapters generated by Stage for commit 5eab028 on Jul 5, 2026 12:55pm UTC. |
✅ Code Coverage Report
View detailed coverageDownload the Or run locally: cargo llvm-cov --html
open target/llvm-cov/html/index.html |
Summary
Configuration correctness was enforced ad hoc, in three scattered ways, so misconfiguration failed silently at runtime instead of loudly at load. This centralizes it, per the #215 tracker design.
Single source of defaults (#171)
Dropped the
set_default(...)ladder inbase_config_builder. The serdedefault_*fns (backed by the existingDEFAULT_*consts) are now the only place a default lives. The builder seeds each top-level section as an empty table sotry_deserializestill reaches those serde defaults when a section is absent from every source (verified with an empty-TOML test). Env-override behavior (TRANSPONDER_*parsing, comma-list handling, ambient-var skipping, non-Unicode redaction) is byte-compatible.Typed fields, validated at load
NonZeroUsize/ reject-zero formax_rate_limit_cache_size,max_dedup_cache_size,max_tokens_per_event([LOW]server.max_tokens_per_event = 0(andmax_dedup_cache_size = 0) bypassServerConfig::validate()— silent push outage / silent default swap #149, [LOW] RateLimiter::new silently coerces max_entries = 0 to the 100,000 default instead of surfacing the misconfiguration #166). The downstreamRateLimitConfig,TokenRateLimitConfig, andReplayProtectionConfignow carryNonZeroUsize, so the silent fallbacks atrate_limiter.rs(.unwrap_or(DEFAULT_MAX_SIZE)) and the dedupNonZeroUsize::new().unwrap_or()inevents.rsare deleted — a zero is rejected at load with a named-field error.max_concurrent_event_processingkeeps theSemaphore::MAX_PERMITSupper bound (from fix: validate event processing concurrency cap #211) and the documented0 → 1clamp inevent_processing_permits.enum LogFormat { Json, Pretty }([LOW]logging.format = "off"silently disables ALL logging and ignoreslogging.level— an undocumented format value that causes a total log blackout #150, [LOW] Invalidlogging.formatsilently falls back to default formatter #142) with a hand-writtenDeserializethat rejects unknown values (including the undocumentedoffblackout arm) with a message pointing atlogging.level = "off".init_logging's match is now exhaustive over the enum.health.bind_addressis parsed asSocketAddrinAppConfig::validate([LOW] health.bind_address is never validated at config load — a typo silently disables the health/readiness/metrics server while the process runs #167), even when the health server is disabled, so a hostname or out-of-range port is rejected at startup.ws://entry inrelays.oniondefeats the wss-only TLS enforcement, and typo'd.onionURLs are silently dropped #122, [LOW] inbox_relay_event_is_current compares raw config strings and uses events.first(), causing spurious kind-10050 republishing and possibly skipping needed updates #124):validate_relay_confignow validates the onion list symmetrically with clearnet — each entry must parse (via nostr-sdkRelayUrl) asws:///wss://with a.onionhost.inbox_relay_event_is_currentnormalizes both sides throughRelayUrlbefore comparing and selects the newest kind-10050 event bycreated_atinstead ofevents.first().Key-file hygiene (#146)
On unix, before reading
private_key_file, the file is stat'd; if its mode grants group/other read (mode & 0o077 != 0) the server refuses to start, mirroring the0600the generate path enforces.Private key out of the config Value tree (#156)
server.private_key(inline TOML andTRANSPONDER_SERVER_PRIVATE_KEY) is resolved through a dedicatedZeroizingpath — extracted from the parsed TOML document (and env iterator) before the rest of the config reaches theconfigcrate — so the secret never lands in the crate's un-zeroizedValuetree. Precedence (env > file config) is unchanged. Docs now recommendprivate_key_file.#196 rider — not needed
The
metrics.enabled && !health.enabledload-time warning was planned as a rider, but PR #230 (merged into master while this was in flight) landed the structural fix:/metricsis now served onhealth.bind_addresswhenever metrics are enabled, independent ofhealth.enabled, and the health server emits its own targeted warning at bind time. A separate warning here would be redundant and factually wrong (metrics ARE now exposed), so it was dropped after merging master.Breaking for invalid configs
Existing deployments with valid configs load identically. Only previously-silently-tolerated invalid configs now fail at startup:
server.max_dedup_cache_size = 0— was silently swapped for the 100k default; now rejected.server.max_rate_limit_cache_size = 0— was silently swapped for 100k per limiter; now rejected.server.max_tokens_per_event = 0— was a total silent push outage (every event failed token parsing); now rejected.logging.format = "off"— silently disabled ALL logging (including errors); now rejected with a message pointing tologging.level = "off".logging.format = <typo>(e.g."jsno") — silently fell back to the default formatter; now rejected.health.bind_address = <non-SocketAddr>(e.g."localhost:8080"or"127.0.0.1:99999") — was accepted and only failed to bind at runtime; now rejected at load.relays.onionentry that is not aws:///wss://URL with a.onionhost (e.g. a plaintext clearnetws://URL, a bare host, or a misspelled suffix) — was added blindly / silently dropped at connect; now rejected at load (only when thetorfeature is enabled; without it the existing "requires tor feature" error fires first).private_key_filewith group/world-readable permissions (mode& 0o077 != 0) — was loaded silently; now refuses to start.Testing
cargo fmt --checkclean;cargo clippy --all-targets -- -D warningszero (also with--features tor);cargo test --bin transponderall pass (603 default / 606 with tor);cargo check --all-targets --features torclean. Theconfig.rstest module was extended with a test for every new validation branch, and new tests were added inclient.rs(onion validation, inbox normalization, newest-event selection) andmain.rs(key-file permission check,LogFormatinit).Fixes #171
Fixes #167
Fixes #166
Fixes #149
Fixes #150
Fixes #142
Fixes #122
Fixes #124
Fixes #146
Fixes #156
Part of #215.
🤖 Generated with Claude Code