633d0c3c74
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.
75 lines
2.6 KiB
TOML
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]
|