7c7908d373
Implements the operator-facing JSON-RPC admin protocol on the relay
side, layered on top of the existing NIP-98 HTTP-Auth + NIP-86 wire
types in :quartz. Same path as the NIP-01 WebSocket and NIP-11 GET —
HTTP POST with Content-Type application/nostr+json+rpc, gated by a
NIP-98 Authorization header signed by an operator-listed pubkey.
Architecture:
- BanStore — concurrent in-memory state for runtime
pubkey/event/kind ban + allow lists.
- DynamicBanPolicy — IRelayPolicy that consults BanStore on every
EVENT; auto-prepended to every Relay's policy
stack so admin actions take effect without a
restart.
- Nip98AuthVerifier — parses `Authorization: Nostr <base64-event>`,
verifies kind 27235 + Schnorr sig + ±60 s
clock skew + method/url/payload-hash match.
- Nip86Server — JSON-RPC dispatcher. Mutates BanStore for
ban/allow/list methods, mutates the live
RelayInfo doc for changerelayname/desc/icon
(Relay.info is now @Volatile var, mutation
through Relay.updateInfo).
- LocalRelayServer — adds POST handler at the relay path:
403 if no admin pubkeys configured,
401 if NIP-98 missing/invalid,
403 if signer's pubkey isn't on the admin list,
200 with Nip86Response otherwise.
- RelayConfig.AdminSection — `[admin].pubkeys = [...]` config key.
- RelayInfo.default() now advertises NIP-86 alongside 1/9/11/40/42/45/50/62.
Methods implemented:
supportedmethods, banpubkey, unbanpubkey, listbannedpubkeys,
allowpubkey, unallowpubkey, listallowedpubkeys,
banevent (also deletes from store), allowevent, listbannedevents,
allowkind, disallowkind, listallowedkinds,
changerelayname, changerelaydescription, changerelayicon.
Tests (28 new):
- BanStoreTest (6) — ban/allow round-trips, case-insensitive
pubkey match, kind allow/deny precedence,
audit-trail listing.
- Nip86ServerTest (8) — every method's accept/reject path.
- Nip98AuthVerifierTest (8) — happy path, missing/wrong scheme,
url/method/payload-hash mismatch,
stale created_at, wrong event kind.
- Nip86EndToEndTest (6) — real HTTP POST through LocalRelayServer:
supportedmethods returns 200,
outsider returns 403, no auth header
returns 401 + WWW-Authenticate, banpubkey
blocks subsequent EVENT publish over WS,
changerelayname flows to NIP-11 GET,
admin endpoint is disabled when no
pubkeys configured.
Total :quartz-relay tests: 87, 0 failures.
71 lines
2.5 KiB
TOML
71 lines
2.5 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 = "/"
|
|
|
|
[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 Schnorr signatures on every EVENT. Default: true. Disable
|
|
# only for trusted-input scenarios (test fixtures, mirror replays).
|
|
# 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. Enforced by RejectFutureEventsPolicy.
|
|
# reject_future_seconds = 1800
|
|
|
|
[limits]
|
|
# 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
|
|
|
|
[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]
|
|
|
|
[admin]
|
|
# NIP-86 relay management API. When `pubkeys` is non-empty, the relay
|
|
# accepts HTTP POST application/nostr+json+rpc on the same URL,
|
|
# authenticated with NIP-98 HTTP-Auth. Only events signed by one of
|
|
# the listed pubkeys can run admin RPCs (banpubkey / banevent /
|
|
# changerelayname / …). Empty (the default) disables the endpoint.
|
|
# pubkeys = ["abcdef...64hex..."]
|