refactor(geode): move Nip86Server (+ adminPubkeys) into RelayEngine
KtorRelay was carrying state that wasn't HTTP-specific: the Nip86Server, its InfoHolder adapter, and the adminPubkeys allow-list. Push them into RelayEngine — it already owns the info doc, ban store, and event store the Nip86Server consults; the allow-list is a relay-level "who is admin?" decision, not a transport-level one. RelayEngine gains adminPubkeys: Set<HexKey> = emptySet() and exposes nip86Server as a public property. Future non-HTTP admin transports (in-process tools, hypothetical NIP-86-over-WS) read it directly without re-deriving the wiring. KtorRelay shrinks to mostly Ktor structures — routing block, engine config, lifecycle. It just builds the Nip86HttpHandler around relay.nip86Server. Main.kt and Nip86EndToEndTest pass adminPubkeys to RelayEngine instead of KtorRelay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,13 +23,10 @@ package com.vitorpamplona.geode
|
||||
import com.vitorpamplona.geode.server.Nip11HttpRoute
|
||||
import com.vitorpamplona.geode.server.Nip86HttpRoute
|
||||
import com.vitorpamplona.geode.server.WebSocketSessionPump
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.RelaySession
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.Nip86HttpHandler
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.Nip86Server
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.application.serverConfig
|
||||
import io.ktor.server.cio.CIO
|
||||
@@ -70,14 +67,6 @@ class KtorRelay(
|
||||
/** Pass 0 to let the OS pick a free port. Read [url] after [start] to learn it. */
|
||||
val port: Int = 0,
|
||||
val path: String = "/",
|
||||
/**
|
||||
* Pubkeys allowed to call NIP-86 admin RPCs. Empty (the default)
|
||||
* disables the admin endpoint entirely — POSTs return 403.
|
||||
* Otherwise: HTTP POSTs to [path] with `Content-Type:
|
||||
* application/nostr+json+rpc` are dispatched to [Nip86Server],
|
||||
* gated by NIP-98 HTTP-Auth membership in this set.
|
||||
*/
|
||||
val adminPubkeys: Set<HexKey> = emptySet(),
|
||||
/**
|
||||
* Ktor CIO acceptor-thread count. `null` keeps Ktor's default.
|
||||
* Lift on machines with many cores when targeting 10k+
|
||||
@@ -89,27 +78,12 @@ class KtorRelay(
|
||||
/** Ktor CIO call-handling thread count. `null` keeps Ktor's default. */
|
||||
val callGroupSize: Int? = null,
|
||||
) {
|
||||
private val infoHolder =
|
||||
object : Nip86Server.InfoHolder {
|
||||
override fun get(): Nip11RelayInformation = relay.info.document
|
||||
|
||||
override fun set(info: Nip11RelayInformation) = relay.updateInfo { info }
|
||||
}
|
||||
|
||||
private val nip86Server =
|
||||
Nip86Server(
|
||||
banStore = relay.banStore,
|
||||
infoHolder = infoHolder,
|
||||
onBan = { filter -> relay.store.delete(filter) },
|
||||
allowList = adminPubkeys,
|
||||
)
|
||||
|
||||
/**
|
||||
* Always assembled — when [adminPubkeys] is empty, the route
|
||||
* still runs the canonical NIP-86 flow but every request fails
|
||||
* the allow-list check ([Nip86HttpHandler.Response.NotAdmin] →
|
||||
* 403). Uniform code path: the transport doesn't branch on
|
||||
* "admin enabled?".
|
||||
* NIP-86 HTTP adapter. Wraps the engine's [RelayEngine.nip86Server]
|
||||
* with the NIP-98 HTTP flow ([Nip86HttpHandler]) and binds it to
|
||||
* Ktor's request lifecycle. The admin pubkey allow-list lives on
|
||||
* the engine, not here — turning admin on is a relay-level
|
||||
* decision, not a transport-level one.
|
||||
*
|
||||
* The NIP-98 binding URL is derived from `relay.url` via [toHttp]
|
||||
* because NIP-86 mandates it: admin requests target the same URI
|
||||
@@ -117,10 +91,15 @@ class KtorRelay(
|
||||
* `ws(s)://`. This makes accidental misconfiguration impossible —
|
||||
* the operator's configured `[info].relay_url` is the single
|
||||
* source of truth.
|
||||
*
|
||||
* Always wired: when the engine's admin allow-list is empty, the
|
||||
* handler runs the same flow and the allow-list check fails with
|
||||
* [Nip86HttpHandler.Response.NotAdmin] → 403. No "admin enabled?"
|
||||
* branch.
|
||||
*/
|
||||
private val nip86Route =
|
||||
Nip86HttpRoute(
|
||||
handler = Nip86HttpHandler(server = nip86Server, publicUrl = relay.url.toHttp()),
|
||||
handler = Nip86HttpHandler(server = relay.nip86Server, publicUrl = relay.url.toHttp()),
|
||||
)
|
||||
|
||||
private val nip11Route = Nip11HttpRoute(liveJson = { relay.info.json })
|
||||
@@ -187,7 +166,7 @@ class KtorRelay(
|
||||
}
|
||||
// NIP-86: POST application/nostr+json+rpc with a NIP-98
|
||||
// signed Authorization header → JSON-RPC dispatch.
|
||||
// Always mounted; an empty [adminPubkeys] just means
|
||||
// Always mounted; an empty admin allow-list on the engine just means
|
||||
// every request fails the allow-list check (403).
|
||||
post(path) {
|
||||
nip86Route.handle(call)
|
||||
|
||||
@@ -143,6 +143,7 @@ fun main(args: Array<String>) {
|
||||
policyBuilder,
|
||||
parallelVerify = parallelVerify,
|
||||
negentropySettings = negentropySettings,
|
||||
adminPubkeys = config.admin.pubkeys.toSet(),
|
||||
)
|
||||
val server =
|
||||
KtorRelay(
|
||||
@@ -150,7 +151,6 @@ fun main(args: Array<String>) {
|
||||
host = host,
|
||||
port = port,
|
||||
path = path,
|
||||
adminPubkeys = config.admin.pubkeys.toSet(),
|
||||
connectionGroupSize = config.network.connection_group_size,
|
||||
workerGroupSize = config.network.worker_group_size,
|
||||
callGroupSize = config.network.call_group_size,
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.geode
|
||||
import com.vitorpamplona.geode.config.BannedEntry
|
||||
import com.vitorpamplona.geode.config.RuntimeConfig
|
||||
import com.vitorpamplona.geode.config.RuntimeConfigData
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.IRelayPolicy
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.NostrServer
|
||||
@@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanListPolicy
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore
|
||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.Nip86Server
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@@ -92,6 +94,14 @@ class RelayEngine(
|
||||
* per-connection session cap). Defaults to strfry-parity values.
|
||||
*/
|
||||
negentropySettings: NegentropySettings = NegentropySettings.Default,
|
||||
/**
|
||||
* Pubkeys allowed to invoke NIP-86 admin RPCs. Empty (the default)
|
||||
* effectively disables the admin API: every dispatch returns
|
||||
* `not authorized`. Transports (e.g. [KtorRelay] over HTTP) read
|
||||
* [nip86Server] and bind it to their wire protocol — the engine
|
||||
* owns *who* is admin; the transport owns *how* admins authenticate.
|
||||
*/
|
||||
adminPubkeys: Set<HexKey> = emptySet(),
|
||||
) : AutoCloseable {
|
||||
/**
|
||||
* The runtime state to install at boot: the persisted snapshot if
|
||||
@@ -145,6 +155,29 @@ class RelayEngine(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-86 admin RPC dispatcher. Owns the ban / allow / kind list
|
||||
* mutations, the NIP-11 `changerelay*` mutations (via an
|
||||
* [Nip86Server.InfoHolder] adapter that wraps [info] / [updateInfo]),
|
||||
* 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 =
|
||||
Nip86Server(
|
||||
banStore = banStore,
|
||||
infoHolder =
|
||||
object : Nip86Server.InfoHolder {
|
||||
override fun get(): Nip11RelayInformation = info.document
|
||||
|
||||
override fun set(info: Nip11RelayInformation) = updateInfo { info }
|
||||
},
|
||||
onBan = { filter -> store.delete(filter) },
|
||||
allowList = adminPubkeys,
|
||||
)
|
||||
|
||||
/**
|
||||
* Writes the current state (NIP-11 doc + ban lists) to disk.
|
||||
* No-op when no `stateFile` was configured (see [RuntimeConfig.save]).
|
||||
|
||||
@@ -79,13 +79,12 @@ class Nip86EndToEndTest {
|
||||
// server expects to differ from where it actually listens.
|
||||
val freePort = java.net.ServerSocket(0).use { it.localPort }
|
||||
val url = "ws://127.0.0.1:$freePort/".normalizeRelayUrl()
|
||||
relay = RelayEngine(url = url)
|
||||
relay = RelayEngine(url = url, adminPubkeys = setOf(admin.pubKey))
|
||||
server =
|
||||
KtorRelay(
|
||||
relay = relay,
|
||||
host = "127.0.0.1",
|
||||
port = freePort,
|
||||
adminPubkeys = setOf(admin.pubKey),
|
||||
).start()
|
||||
scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
val builder = BasicOkHttpWebSocket.Builder { _ -> httpClient }
|
||||
|
||||
Reference in New Issue
Block a user