refactor(geode): simplify RelayEngine + trim StaticConfig comments
RelayEngine (224 → 185 lines):
- Extract BanStore <-> RuntimeConfigData mapping helpers
(seedInto, snapshotOf) into geode.config — bidirectional
conversion now has names instead of being inline boilerplate
repeated between the init block and snapshot().
- Collapse the standalone init block into a .apply on the
banStore declaration. Method reference ::snapshot for the
onMutation hook.
- Drop the effectiveAtBoot field's 12-line KDoc (rename to `boot`
locally, terse). Trim verbose property KDocs to the load-bearing
facts (why-not-what).
StaticConfig (255 → 182 lines):
- Drop resolveInfo's unused `advertisedUrl` parameter and the
workaround comment that justified keeping it for "future
fields". YAGNI — add back when actually needed. Two callers
updated.
- Trim trivial KDocs (fromToml/fromFile, field names that
document themselves like require_auth, reject_future_seconds,
supported_nips, database.file).
- Collapse NetworkSection's three thread-pool KDocs (~16 lines)
into a single section-level KDoc.
- Compact parallel_verify, AdminSection, AdminSection.state_file,
and advertisedUrl companion KDocs while keeping the
load-bearing facts (invariants, strfry interop strings,
surprising defaults).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,7 +104,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
val info =
|
val info =
|
||||||
cliInfoFile?.let { RelayInfo.fromFile(it) }
|
cliInfoFile?.let { RelayInfo.fromFile(it) }
|
||||||
?: config.resolveInfo(advertisedUrl)
|
?: config.resolveInfo()
|
||||||
|
|
||||||
val store: IEventStore = EventStore(dbName = dbFile, relay = advertisedUrl)
|
val store: IEventStore = EventStore(dbName = dbFile, relay = advertisedUrl)
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.geode
|
package com.vitorpamplona.geode
|
||||||
|
|
||||||
import com.vitorpamplona.geode.config.BannedEntry
|
|
||||||
import com.vitorpamplona.geode.config.RuntimeConfig
|
import com.vitorpamplona.geode.config.RuntimeConfig
|
||||||
import com.vitorpamplona.geode.config.RuntimeConfigData
|
import com.vitorpamplona.geode.config.RuntimeConfigData
|
||||||
|
import com.vitorpamplona.geode.config.seedInto
|
||||||
|
import com.vitorpamplona.geode.config.snapshotOf
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.server.IRelayPolicy
|
import com.vitorpamplona.quartz.nip01Core.relay.server.IRelayPolicy
|
||||||
@@ -103,67 +104,40 @@ class RelayEngine(
|
|||||||
*/
|
*/
|
||||||
adminPubkeys: Set<HexKey> = emptySet(),
|
adminPubkeys: Set<HexKey> = emptySet(),
|
||||||
) : AutoCloseable {
|
) : AutoCloseable {
|
||||||
/**
|
private val boot: RuntimeConfigData = runtimeConfig.effective()
|
||||||
* The runtime state to install at boot: the persisted snapshot if
|
|
||||||
* the file exists, otherwise the seed baked into [runtimeConfig].
|
|
||||||
* Read once here so the `info` property initializer and the
|
|
||||||
* [banStore] seeding in [init] agree on the same source.
|
|
||||||
*/
|
|
||||||
private val effectiveAtBoot: RuntimeConfigData = runtimeConfig.effective()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-11 doc. Mutable so NIP-86 admin RPCs (`changerelayname`,
|
* Live NIP-11 doc. Mutable via [updateInfo] so NIP-86 admin RPCs
|
||||||
* `changerelaydescription`, `changerelayicon`) can swap the doc
|
* can swap it atomically; readers (NIP-11 GET) re-read every
|
||||||
* atomically. Readers (the NIP-11 GET endpoint) re-read on every
|
* request so changes are visible immediately. The `!!` is load-
|
||||||
* request so changes are visible immediately, no restart needed.
|
* bearing: the seed always has a non-null info, so a null here
|
||||||
*
|
* means a manually corrupted state file — fail loud over serving
|
||||||
* Initialized from [effectiveAtBoot] — a persisted admin override
|
* empty NIP-11.
|
||||||
* wins, otherwise we fall back to the seed baked into
|
|
||||||
* [runtimeConfig]. The `!!` is load-bearing: the seed is always
|
|
||||||
* built with a non-null info, so the only way we reach a null
|
|
||||||
* here is a manually corrupted state file, which we'd rather
|
|
||||||
* crash on than serve empty NIP-11 from.
|
|
||||||
*/
|
*/
|
||||||
@Volatile
|
@Volatile
|
||||||
var info: RelayInfo = RelayInfo(effectiveAtBoot.info!!)
|
var info: RelayInfo = RelayInfo(boot.info!!)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
/** Mutates the live NIP-11 doc. Called by [Nip86Server]. */
|
/** Mutates the live NIP-11 doc and persists the snapshot. */
|
||||||
fun updateInfo(transform: (Nip11RelayInformation) -> Nip11RelayInformation) {
|
fun updateInfo(transform: (Nip11RelayInformation) -> Nip11RelayInformation) {
|
||||||
info = RelayInfo(transform(info.document))
|
info = RelayInfo(transform(info.document))
|
||||||
snapshot()
|
snapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime-mutable ban / allow lists. NIP-86 RPC handlers in
|
* Runtime-mutable ban / allow lists. Mutated by [Nip86Server] via
|
||||||
* [Nip86Server] mutate this; the policy stack consults it on
|
* NIP-86 RPCs; consulted on every EVENT by [BanListPolicy]. Seeded
|
||||||
* every accept call via [BanListPolicy].
|
* at boot from the persisted snapshot (or the static [runtimeConfig]
|
||||||
|
* seed on first boot) without firing the mutation hook.
|
||||||
*/
|
*/
|
||||||
val banStore: BanStore = BanStore(onMutation = { snapshot() })
|
val banStore: BanStore =
|
||||||
|
BanStore(onMutation = ::snapshot)
|
||||||
init {
|
.apply { boot.seedInto(this) }
|
||||||
// Seed the in-memory ban state from [effectiveAtBoot] *without*
|
|
||||||
// firing [snapshot] on every entry — the snapshot is exactly
|
|
||||||
// what we just loaded (or, on first boot, the static seed we
|
|
||||||
// just synthesized).
|
|
||||||
banStore.seedFromSnapshot(
|
|
||||||
bannedPubkeys = effectiveAtBoot.bannedPubkeys.map { it.key to it.reason },
|
|
||||||
allowedPubkeys = effectiveAtBoot.allowedPubkeys.map { it.key to it.reason },
|
|
||||||
bannedEvents = effectiveAtBoot.bannedEvents.map { it.key to it.reason },
|
|
||||||
allowedKinds = effectiveAtBoot.allowedKinds,
|
|
||||||
disallowedKinds = effectiveAtBoot.disallowedKinds,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-86 admin RPC dispatcher. Owns the ban / allow / kind list
|
* NIP-86 admin RPC dispatcher. Transport-agnostic — `KtorRelay`
|
||||||
* mutations, the NIP-11 `changerelay*` mutations (via an
|
* wraps it with `Nip86HttpHandler` for HTTP/NIP-98; an in-process
|
||||||
* [Nip86Server.InfoHolder] adapter that wraps [info] / [updateInfo]),
|
* tool could call `dispatch(adminPubkey, req)` directly.
|
||||||
* the event-store purge on each ban method (via `store.delete`),
|
|
||||||
* and the admin allow-list ([adminPubkeys]). Transport-agnostic:
|
|
||||||
* `KtorRelay` wraps it with `Nip86HttpHandler` for HTTP/NIP-98;
|
|
||||||
* an in-process tool could call `dispatch(adminPubkey, req)`
|
|
||||||
* directly.
|
|
||||||
*/
|
*/
|
||||||
val nip86Server: Nip86Server =
|
val nip86Server: Nip86Server =
|
||||||
Nip86Server(
|
Nip86Server(
|
||||||
@@ -179,25 +153,14 @@ class RelayEngine(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the current state (NIP-11 doc + ban lists) to disk.
|
* Writes the current state (NIP-11 doc + ban lists) to disk via
|
||||||
* No-op when no `stateFile` was configured (see [RuntimeConfig.save]).
|
* [RuntimeConfig.save] — no-op when no state file is configured.
|
||||||
*
|
* Best-effort: I/O failures are logged to stderr and swallowed so
|
||||||
* Best-effort: any I/O failure is logged to stderr and swallowed
|
* an unwritable disk doesn't take the relay down.
|
||||||
* so an unwritable disk doesn't take the relay down. Operators
|
|
||||||
* monitor for missing snapshots out-of-band.
|
|
||||||
*/
|
*/
|
||||||
fun snapshot() {
|
fun snapshot() {
|
||||||
runCatching {
|
runCatching {
|
||||||
runtimeConfig.save(
|
runtimeConfig.save(snapshotOf(info.document, banStore))
|
||||||
RuntimeConfigData(
|
|
||||||
info = info.document,
|
|
||||||
bannedPubkeys = banStore.listBannedPubkeys().map { (k, r) -> BannedEntry(k, r) },
|
|
||||||
allowedPubkeys = banStore.listAllowedPubkeys().map { (k, r) -> BannedEntry(k, r) },
|
|
||||||
bannedEvents = banStore.listBannedEvents().map { (k, r) -> BannedEntry(k, r) },
|
|
||||||
allowedKinds = banStore.listAllowedKinds(),
|
|
||||||
disallowedKinds = banStore.listDisallowedKinds(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
System.err.println("warning: failed to write relay state file: ${it.message}")
|
System.err.println("warning: failed to write relay state file: ${it.message}")
|
||||||
}
|
}
|
||||||
@@ -206,10 +169,9 @@ class RelayEngine(
|
|||||||
val server =
|
val server =
|
||||||
NostrServer(
|
NostrServer(
|
||||||
store,
|
store,
|
||||||
// Always prepend a BanListPolicy so NIP-86 admin actions
|
// Always prepend BanListPolicy so NIP-86 admin actions bite.
|
||||||
// bite. When the operator-supplied builder returns
|
// EmptyPolicy from the caller means "no extra policies" — use
|
||||||
// [EmptyPolicy] we use the dynamic policy alone; otherwise
|
// BanListPolicy alone; otherwise stack so both must accept.
|
||||||
// we stack them so both layers must accept.
|
|
||||||
policyBuilder = {
|
policyBuilder = {
|
||||||
val user = policyBuilder()
|
val user = policyBuilder()
|
||||||
if (user === EmptyPolicy) BanListPolicy(banStore) else user + BanListPolicy(banStore)
|
if (user === EmptyPolicy) BanListPolicy(banStore) else user + BanListPolicy(banStore)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package com.vitorpamplona.geode.config
|
package com.vitorpamplona.geode.config
|
||||||
|
|
||||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -139,3 +140,28 @@ data class BannedEntry(
|
|||||||
val key: String,
|
val key: String,
|
||||||
val reason: String? = null,
|
val reason: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Bulk-load this snapshot into [banStore] without firing its `onMutation` hook. */
|
||||||
|
fun RuntimeConfigData.seedInto(banStore: BanStore) {
|
||||||
|
banStore.seedFromSnapshot(
|
||||||
|
bannedPubkeys = bannedPubkeys.map { it.key to it.reason },
|
||||||
|
allowedPubkeys = allowedPubkeys.map { it.key to it.reason },
|
||||||
|
bannedEvents = bannedEvents.map { it.key to it.reason },
|
||||||
|
allowedKinds = allowedKinds,
|
||||||
|
disallowedKinds = disallowedKinds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Capture the current state of [info] + [banStore] as a fresh snapshot. */
|
||||||
|
fun snapshotOf(
|
||||||
|
info: Nip11RelayInformation,
|
||||||
|
banStore: BanStore,
|
||||||
|
): RuntimeConfigData =
|
||||||
|
RuntimeConfigData(
|
||||||
|
info = info,
|
||||||
|
bannedPubkeys = banStore.listBannedPubkeys().map { (k, r) -> BannedEntry(k, r) },
|
||||||
|
allowedPubkeys = banStore.listAllowedPubkeys().map { (k, r) -> BannedEntry(k, r) },
|
||||||
|
bannedEvents = banStore.listBannedEvents().map { (k, r) -> BannedEntry(k, r) },
|
||||||
|
allowedKinds = banStore.listAllowedKinds(),
|
||||||
|
disallowedKinds = banStore.listDisallowedKinds(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -34,10 +34,8 @@ import java.io.File
|
|||||||
* needs to change while the relay is running lives in [RuntimeConfig].
|
* needs to change while the relay is running lives in [RuntimeConfig].
|
||||||
*
|
*
|
||||||
* Section layout matches nostr-rs-relay's `config.toml` so existing
|
* Section layout matches nostr-rs-relay's `config.toml` so existing
|
||||||
* configs can be ported with little churn.
|
* configs can be ported with little churn. Every section is optional;
|
||||||
*
|
* CLI flags override file values where both exist.
|
||||||
* Every section is optional; values not set fall back to sensible
|
|
||||||
* defaults (or, for fields also exposed on the CLI, the CLI value wins).
|
|
||||||
*/
|
*/
|
||||||
data class StaticConfig(
|
data class StaticConfig(
|
||||||
val info: InfoSection = InfoSection(),
|
val info: InfoSection = InfoSection(),
|
||||||
@@ -48,11 +46,7 @@ data class StaticConfig(
|
|||||||
val admin: AdminSection = AdminSection(),
|
val admin: AdminSection = AdminSection(),
|
||||||
val negentropy: NegentropySection = NegentropySection(),
|
val negentropy: NegentropySection = NegentropySection(),
|
||||||
) {
|
) {
|
||||||
/**
|
fun resolveInfo(): RelayInfo =
|
||||||
* Maps the `[info]` section into a [RelayInfo] used by the NIP-11
|
|
||||||
* endpoint. `relay_url` and CLI overrides take precedence.
|
|
||||||
*/
|
|
||||||
fun resolveInfo(advertisedUrl: NormalizedRelayUrl): RelayInfo =
|
|
||||||
RelayInfo(
|
RelayInfo(
|
||||||
Nip11RelayInformation(
|
Nip11RelayInformation(
|
||||||
name = info.name ?: RelayInfo.NAME,
|
name = info.name ?: RelayInfo.NAME,
|
||||||
@@ -71,12 +65,7 @@ data class StaticConfig(
|
|||||||
language_tags = info.language_tags,
|
language_tags = info.language_tags,
|
||||||
tags = info.tags,
|
tags = info.tags,
|
||||||
),
|
),
|
||||||
).also {
|
)
|
||||||
// Touch [advertisedUrl] so the parameter isn't unused — we keep
|
|
||||||
// it in the signature because future fields (e.g. self-pubkey
|
|
||||||
// selection, fee URLs) will want it.
|
|
||||||
advertisedUrl.url
|
|
||||||
}
|
|
||||||
|
|
||||||
data class InfoSection(
|
data class InfoSection(
|
||||||
val relay_url: String? = null,
|
val relay_url: String? = null,
|
||||||
@@ -87,7 +76,6 @@ data class StaticConfig(
|
|||||||
val icon: String? = null,
|
val icon: String? = null,
|
||||||
val software: String? = null,
|
val software: String? = null,
|
||||||
val version: String? = null,
|
val version: String? = null,
|
||||||
/** NIP numbers as ints (e.g. `[1, 9, 11]`). Stringified at render time. */
|
|
||||||
val supported_nips: List<Int>? = null,
|
val supported_nips: List<Int>? = null,
|
||||||
val privacy_policy: String? = null,
|
val privacy_policy: String? = null,
|
||||||
val terms_of_service: String? = null,
|
val terms_of_service: String? = null,
|
||||||
@@ -96,91 +84,50 @@ data class StaticConfig(
|
|||||||
val tags: List<String>? = null,
|
val tags: List<String>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind config + Ktor CIO thread-pool sizes. The three `*_size`
|
||||||
|
* fields default to Ktor's per-CPU sizing; lift them on big-VM
|
||||||
|
* deployments targeting 10k+ concurrent connections.
|
||||||
|
*/
|
||||||
data class NetworkSection(
|
data class NetworkSection(
|
||||||
val host: String = "0.0.0.0",
|
val host: String = "0.0.0.0",
|
||||||
val port: Int = 7447,
|
val port: Int = 7447,
|
||||||
val path: String = "/",
|
val path: String = "/",
|
||||||
/**
|
|
||||||
* Ktor CIO acceptor-thread count. `null` (default) keeps Ktor's
|
|
||||||
* default sizing — fine up to a few thousand concurrent
|
|
||||||
* connections. On big-VM deployments targeting 10k+
|
|
||||||
* connections, lift this to roughly half the available cores
|
|
||||||
* so the acceptor doesn't starve workers.
|
|
||||||
*/
|
|
||||||
val connection_group_size: Int? = null,
|
val connection_group_size: Int? = null,
|
||||||
/**
|
|
||||||
* Ktor CIO worker-thread count (handles socket I/O). `null`
|
|
||||||
* keeps Ktor's default. Each connection's WebSocket read/write
|
|
||||||
* is dispatched onto this pool; for many idle long-lived
|
|
||||||
* connections the pool can stay small, but 10k+ connections
|
|
||||||
* benefit from sizing this to the full CPU count.
|
|
||||||
*/
|
|
||||||
val worker_group_size: Int? = null,
|
val worker_group_size: Int? = null,
|
||||||
/**
|
|
||||||
* Ktor CIO call-handling thread count. `null` keeps Ktor's
|
|
||||||
* default. Sized higher than [worker_group_size] because each
|
|
||||||
* call (incl. WebSocket upgrade) may suspend on I/O — at
|
|
||||||
* 10k+ connections, ~4× cores is a reasonable starting point.
|
|
||||||
*/
|
|
||||||
val call_group_size: Int? = null,
|
val call_group_size: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DatabaseSection(
|
data class DatabaseSection(
|
||||||
/** True keeps an in-memory SQLite db (default — events vanish on restart). */
|
/** True keeps an in-memory SQLite db (default — events vanish on restart). */
|
||||||
val in_memory: Boolean = true,
|
val in_memory: Boolean = true,
|
||||||
/** Filesystem path for a persistent SQLite db. Ignored when [in_memory] is true. */
|
|
||||||
val file: String? = null,
|
val file: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OptionsSection(
|
data class OptionsSection(
|
||||||
/** Reject events whose `created_at` is more than this many seconds in the future. */
|
|
||||||
val reject_future_seconds: Int? = null,
|
val reject_future_seconds: Int? = null,
|
||||||
/** Require NIP-42 AUTH for REQ/EVENT/COUNT. */
|
|
||||||
val require_auth: Boolean = false,
|
val require_auth: Boolean = false,
|
||||||
/**
|
/**
|
||||||
* Drop events whose Schnorr signature does not verify. **Defaults
|
* Defaults to `true`: any relay accepting real traffic should
|
||||||
* to `true`**: any relay accepting traffic from real clients
|
* verify Schnorr signatures, and verifying-by-default closes
|
||||||
* should verify signatures, and verifying-by-default closes the
|
* the footgun of forgetting the flag. Set false only for
|
||||||
* footgun of forgetting the flag. Set explicitly to `false` only
|
* trusted-input scenarios (test fixtures, mirror replays).
|
||||||
* for trusted-input scenarios (test fixtures, mirror replays).
|
|
||||||
*/
|
*/
|
||||||
val verify_signatures: Boolean = true,
|
val verify_signatures: Boolean = true,
|
||||||
/**
|
/** CPU fan-out verification in the IngestQueue. No-op when [verify_signatures] is false. */
|
||||||
* Run signature verification in parallel inside the IngestQueue
|
|
||||||
* (CPU fan-out across `Dispatchers.Default`) instead of serially
|
|
||||||
* on each connection's WebSocket pump. Tier-3 of the
|
|
||||||
* `event-ingestion-batching` plan. Wins scale with how many
|
|
||||||
* EVENTs a single connection sends back-to-back: ~CPU_COUNT×
|
|
||||||
* verify-step speed-up on burst publishes. Set false to keep
|
|
||||||
* the legacy in-policy verify path.
|
|
||||||
*
|
|
||||||
* Only takes effect when [verify_signatures] is also true.
|
|
||||||
*/
|
|
||||||
val parallel_verify: Boolean = true,
|
val parallel_verify: Boolean = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-77 negentropy tuning. Defaults track strfry
|
* NIP-77 negentropy tuning. Defaults track strfry (`hoytech/strfry`)
|
||||||
* (`hoytech/strfry`) so a Geode relay accepts the same workload
|
* so a Geode relay accepts the same workload shape and exchanges
|
||||||
* shape and exchanges the same NEG-MSG round-trip size as
|
* the same NEG-MSG round-trip size:
|
||||||
* strfry — the de-facto reference implementation.
|
|
||||||
*
|
*
|
||||||
* - [frame_size_limit] mirrors strfry's hard-coded
|
* - [frame_size_limit]: strfry's hard-coded `Negentropy ne(storage, 500'000)`.
|
||||||
* `Negentropy ne(storage, 500'000)` in `RelayNegentropy.cpp`.
|
* - [max_sync_events]: strfry's `relay__negentropy__maxSyncEvents`;
|
||||||
* Hex-encoded that's ~1 MB on the wire per NEG-MSG. Ktor's
|
* NEG-OPEN over this returns `["NEG-ERR", "<subId>", "blocked: too many query results"]`.
|
||||||
* WebSocket layer does not impose a default frame cap, so this
|
* - [max_sessions_per_connection]: NEG sessions held by one connection;
|
||||||
* payload is delivered intact unless a reverse proxy is
|
* overflow returns NOTICE `"too many concurrent NEG requests"`.
|
||||||
* misconfigured.
|
|
||||||
* - [max_sync_events] mirrors strfry's
|
|
||||||
* `relay__negentropy__maxSyncEvents`. NEG-OPEN whose snapshot
|
|
||||||
* exceeds this returns
|
|
||||||
* `["NEG-ERR", "<subId>", "blocked: too many query results"]`.
|
|
||||||
* - [max_sessions_per_connection] caps concurrent NEG-OPEN
|
|
||||||
* sessions held by a single connection. strfry shares its
|
|
||||||
* 200-cap with REQ subs via `relay__maxSubsPerConnection`;
|
|
||||||
* Geode counts NEG independently for now (REQ has no cap yet).
|
|
||||||
* Overflow returns NOTICE
|
|
||||||
* `"too many concurrent NEG requests"` (matches strfry).
|
|
||||||
*/
|
*/
|
||||||
data class NegentropySection(
|
data class NegentropySection(
|
||||||
val frame_size_limit: Long = 500_000L,
|
val frame_size_limit: Long = 500_000L,
|
||||||
@@ -196,30 +143,19 @@ data class StaticConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-86 relay management API. When [pubkeys] is non-empty,
|
* NIP-86 admin. [pubkeys] non-empty opens the POST endpoint at the
|
||||||
* `KtorRelay` exposes a POST endpoint at the relay path
|
* relay path; only NIP-98 tokens signed by these pubkeys dispatch.
|
||||||
* that accepts JSON-RPC admin requests authenticated via NIP-98
|
* The NIP-98 URL binding uses `[info].relay_url` with the scheme
|
||||||
* HTTP-Auth. Only requests signed by one of these pubkeys are
|
* swapped (`ws(s)://` → `http(s)://`) per NIP-86 — set
|
||||||
* dispatched.
|
* `[info].relay_url` to the canonical public URL when behind TLS
|
||||||
*
|
* termination or a reverse proxy.
|
||||||
* The NIP-98 URL binding compares the signed `u` tag against the
|
|
||||||
* `http(s)://` equivalent of the relay's WebSocket URL — i.e.
|
|
||||||
* `[info].relay_url` with the scheme swapped (per NIP-86's
|
|
||||||
* "same URI as `ws(s)://`, called via `http(s)://`"). Set
|
|
||||||
* `[info].relay_url` to the canonical public URL when running
|
|
||||||
* behind TLS termination or a reverse proxy.
|
|
||||||
*/
|
*/
|
||||||
data class AdminSection(
|
data class AdminSection(
|
||||||
val pubkeys: List<String> = emptyList(),
|
val pubkeys: List<String> = emptyList(),
|
||||||
/**
|
/**
|
||||||
* Path for the JSON snapshot that backs [RuntimeConfig] —
|
* JSON snapshot path for [RuntimeConfig] (ban lists + live
|
||||||
* NIP-86 admin state (ban lists + the live NIP-11 doc) that
|
* NIP-11 doc). Convention: place next to the SQLite event file,
|
||||||
* survives restarts. When unset, runtime config is in-memory
|
* e.g. `events.db` ↔ `events.db.admin.json`.
|
||||||
* only.
|
|
||||||
*
|
|
||||||
* Convention: place next to the SQLite event-store file —
|
|
||||||
* e.g. `[database].file = "/var/lib/geode/events.db"`
|
|
||||||
* pairs with `[admin].state_file = "/var/lib/geode/events.db.admin.json"`.
|
|
||||||
*/
|
*/
|
||||||
val state_file: String? = null,
|
val state_file: String? = null,
|
||||||
)
|
)
|
||||||
@@ -227,24 +163,16 @@ data class StaticConfig(
|
|||||||
companion object {
|
companion object {
|
||||||
private val mapper = tomlMapper { }
|
private val mapper = tomlMapper { }
|
||||||
|
|
||||||
/** Parse a TOML string. */
|
|
||||||
fun fromToml(toml: String): StaticConfig = mapper.decode<StaticConfig>(toml)
|
fun fromToml(toml: String): StaticConfig = mapper.decode<StaticConfig>(toml)
|
||||||
|
|
||||||
/** Load a TOML config file. */
|
|
||||||
fun fromFile(file: File): StaticConfig = mapper.decode<StaticConfig>(file.toPath())
|
fun fromFile(file: File): StaticConfig = mapper.decode<StaticConfig>(file.toPath())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the URL the relay advertises in NIP-11 and NIP-42
|
* URL the relay advertises in NIP-11 and NIP-42 challenges:
|
||||||
* challenges. Picks (in order):
|
* `info.relay_url` if set, else built from `network.host:port/path`
|
||||||
* 1. `info.relay_url` from the config
|
* with `0.0.0.0` rewritten to `127.0.0.1`.
|
||||||
* 2. The `network` section's host/port/path (with 0.0.0.0 → 127.0.0.1)
|
|
||||||
* 3. The CLI override (handled in `Main.kt`).
|
|
||||||
*/
|
*/
|
||||||
fun advertisedUrl(config: StaticConfig): NormalizedRelayUrl =
|
fun advertisedUrl(config: StaticConfig): NormalizedRelayUrl = (config.info.relay_url ?: defaultUrl(config.network)).normalizeRelayUrl()
|
||||||
(
|
|
||||||
config.info.relay_url
|
|
||||||
?: defaultUrl(config.network)
|
|
||||||
).normalizeRelayUrl()
|
|
||||||
|
|
||||||
private fun defaultUrl(net: NetworkSection): String {
|
private fun defaultUrl(net: NetworkSection): String {
|
||||||
val host = if (net.host == "0.0.0.0") "127.0.0.1" else net.host
|
val host = if (net.host == "0.0.0.0") "127.0.0.1" else net.host
|
||||||
|
|||||||
@@ -105,13 +105,7 @@ class StaticConfigTest {
|
|||||||
supported_nips = [1, 11, 42]
|
supported_nips = [1, 11, 42]
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
val info =
|
val info = c.resolveInfo()
|
||||||
c.resolveInfo(
|
|
||||||
"ws://127.0.0.1:7447/".let {
|
|
||||||
com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
|
||||||
.normalize(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assertEquals(listOf("1", "11", "42"), info.document.supported_nips)
|
assertEquals(listOf("1", "11", "42"), info.document.supported_nips)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user