Files
amethyst/quartz-relay/config.example.toml
T
Claude 633d0c3c74 feat(relay): enforce [limits] + [authorization] config sections
Wires the parsed-but-unenforced config sections into actual relay
behavior via five new IRelayPolicy implementations under
quartz-relay/.../policies/. They compose through the existing
PolicyStack so operators stack only what they need; cheap rejection
paths run before expensive ones (rate-limit → AUTH → future/size/lists
→ signature verification).

New policies:
  - KindAllowDenyPolicy           — kind_whitelist + kind_blacklist
  - PubkeyAllowDenyPolicy         — pubkey_whitelist + pubkey_blacklist
                                    (case-insensitive)
  - RejectFutureEventsPolicy      — options.reject_future_seconds
  - MaxEventBytesPolicy           — limits.max_event_bytes
                                    (size of canonical NIP-01 JSON form)
  - RateLimitPolicy               — token-bucket per session for
                                    messages_per_sec + subscriptions_per_min;
                                    monotonic clock so wall-time jumps
                                    don't reset buckets.

Plus:
  - LocalRelayServer now honors limits.max_ws_frame_bytes /
    limits.max_ws_message_bytes via Ktor's WebSockets.maxFrameSize.
  - Main.kt builds the policy stack from RelayConfig in composePolicy().
    Warning surface is reduced to only the three sections still
    pending (max_subscriptions_per_session, max_filters_per_req,
    network.remote_ip_header).
  - PassThroughPolicy base lets each policy declare only the hook(s)
    it actually enforces, keeping call sites readable.

Tests (20 new):
  - PoliciesTest (16) — per-policy unit coverage for each accept/reject
    boundary, including allow + deny precedence, case-insensitive pubkey
    matching, deterministic rate-limit refill via injected clock, and
    composition via IRelayPolicy.plus.
  - PoliciesIntegrationTest (4) — end-to-end through NostrClient →
    RelayHub → Relay; proves OK false comes back over the wire when
    policies reject (kind blacklist, pubkey allow-list, future
    timestamps, oversize events).

Total :quartz-relay tests: 62, 0 failures.

Also: bump Nip40 deleteExpiredEvents wait from 1500ms to 2500ms — the
previous margin was thin enough to flake on busy CI when the SQLite
unixepoch() rounds down across the wait.
2026-05-07 02:14:42 +00:00

75 lines
2.6 KiB
TOML

# Example config for quartz-relay. Section layout mirrors
# nostr-rs-relay's config.toml so existing operators can port across.
#
# Run with:
# ./gradlew :quartz-relay:run --args="--config /etc/quartz-relay.toml"
#
# CLI flags override individual values: e.g. `--port 8888` wins over
# `[network].port`.
[info]
# The wss:// URL clients use to reach this relay (mandatory for NIP-42
# AUTH challenges). If not set, the relay synthesises one from the
# [network] section.
relay_url = "wss://relay.example.com/"
name = "Example Quartz Relay"
description = "A quartz-relay deployment."
contact = "admin@example.com"
# Operator pubkey (NIP-11). Optional.
# pubkey = "..."
# Override the supported NIPs advertised on the NIP-11 endpoint. If
# omitted, the relay advertises the NIPs it actually implements.
# supported_nips = [1, 9, 11, 40, 42, 45, 50, 62]
[network]
host = "0.0.0.0"
port = 7447
path = "/"
# Set when behind a reverse proxy (nginx/Caddy/Cloudflare). Required
# before any IP-based rate limit means anything. Parsed today, enforced
# once rate limits land.
# remote_ip_header = "X-Forwarded-For"
[database]
# True keeps an in-memory SQLite db (events vanish on restart). Useful
# for tests; set false + `file = "..."` for persistent storage.
in_memory = false
file = "/var/lib/quartz-relay/events.db"
[options]
# Drop events whose Schnorr signature does not verify. Strongly
# recommended for any relay accepting traffic from real clients.
verify_signatures = true
# Require clients to NIP-42 AUTH before REQ/EVENT/COUNT.
require_auth = false
# Reject events whose `created_at` is more than this many seconds in the
# future. Parsed today, enforced once the matching policy lands.
# reject_future_seconds = 1800
[limits]
# Maximum byte size of an EVENT (canonical NIP-01 JSON form).
# Enforced by MaxEventBytesPolicy.
# max_event_bytes = 131072
# Maximum WebSocket frame size. Frames larger than this are dropped at
# the WS layer. (max_ws_message_bytes maps to the same setting since
# Ktor's WebSockets plugin only exposes per-frame caps.)
# max_ws_message_bytes = 1048576
# max_ws_frame_bytes = 1048576
# Per-session token-bucket caps. Enforced by RateLimitPolicy.
# messages_per_sec = 10
# subscriptions_per_min = 60
# Parsed but NOT YET ENFORCED.
# max_subscriptions_per_session = 32
# max_filters_per_req = 10
[authorization]
# Allow / deny lists. Allow is a permissive ceiling; deny still
# removes specific entries inside it. Enforced by Pubkey/KindAllowDenyPolicy.
# pubkey_whitelist = ["abcdef...64hex..."]
# pubkey_blacklist = []
# kind_whitelist = [0, 1, 3, 7, 1059, 30023]
# kind_blacklist = [4]