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.Nip11HttpRoute
|
||||||
import com.vitorpamplona.geode.server.Nip86HttpRoute
|
import com.vitorpamplona.geode.server.Nip86HttpRoute
|
||||||
import com.vitorpamplona.geode.server.WebSocketSessionPump
|
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.commands.toClient.NoticeMessage
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.server.RelaySession
|
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.Nip86HttpHandler
|
||||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.Nip86Server
|
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.application.serverConfig
|
import io.ktor.server.application.serverConfig
|
||||||
import io.ktor.server.cio.CIO
|
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. */
|
/** Pass 0 to let the OS pick a free port. Read [url] after [start] to learn it. */
|
||||||
val port: Int = 0,
|
val port: Int = 0,
|
||||||
val path: String = "/",
|
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.
|
* Ktor CIO acceptor-thread count. `null` keeps Ktor's default.
|
||||||
* Lift on machines with many cores when targeting 10k+
|
* 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. */
|
/** Ktor CIO call-handling thread count. `null` keeps Ktor's default. */
|
||||||
val callGroupSize: Int? = null,
|
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
|
* NIP-86 HTTP adapter. Wraps the engine's [RelayEngine.nip86Server]
|
||||||
* still runs the canonical NIP-86 flow but every request fails
|
* with the NIP-98 HTTP flow ([Nip86HttpHandler]) and binds it to
|
||||||
* the allow-list check ([Nip86HttpHandler.Response.NotAdmin] →
|
* Ktor's request lifecycle. The admin pubkey allow-list lives on
|
||||||
* 403). Uniform code path: the transport doesn't branch on
|
* the engine, not here — turning admin on is a relay-level
|
||||||
* "admin enabled?".
|
* decision, not a transport-level one.
|
||||||
*
|
*
|
||||||
* The NIP-98 binding URL is derived from `relay.url` via [toHttp]
|
* The NIP-98 binding URL is derived from `relay.url` via [toHttp]
|
||||||
* because NIP-86 mandates it: admin requests target the same URI
|
* because NIP-86 mandates it: admin requests target the same URI
|
||||||
@@ -117,10 +91,15 @@ class KtorRelay(
|
|||||||
* `ws(s)://`. This makes accidental misconfiguration impossible —
|
* `ws(s)://`. This makes accidental misconfiguration impossible —
|
||||||
* the operator's configured `[info].relay_url` is the single
|
* the operator's configured `[info].relay_url` is the single
|
||||||
* source of truth.
|
* 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 =
|
private val nip86Route =
|
||||||
Nip86HttpRoute(
|
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 })
|
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
|
// NIP-86: POST application/nostr+json+rpc with a NIP-98
|
||||||
// signed Authorization header → JSON-RPC dispatch.
|
// 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).
|
// every request fails the allow-list check (403).
|
||||||
post(path) {
|
post(path) {
|
||||||
nip86Route.handle(call)
|
nip86Route.handle(call)
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ fun main(args: Array<String>) {
|
|||||||
policyBuilder,
|
policyBuilder,
|
||||||
parallelVerify = parallelVerify,
|
parallelVerify = parallelVerify,
|
||||||
negentropySettings = negentropySettings,
|
negentropySettings = negentropySettings,
|
||||||
|
adminPubkeys = config.admin.pubkeys.toSet(),
|
||||||
)
|
)
|
||||||
val server =
|
val server =
|
||||||
KtorRelay(
|
KtorRelay(
|
||||||
@@ -150,7 +151,6 @@ fun main(args: Array<String>) {
|
|||||||
host = host,
|
host = host,
|
||||||
port = port,
|
port = port,
|
||||||
path = path,
|
path = path,
|
||||||
adminPubkeys = config.admin.pubkeys.toSet(),
|
|
||||||
connectionGroupSize = config.network.connection_group_size,
|
connectionGroupSize = config.network.connection_group_size,
|
||||||
workerGroupSize = config.network.worker_group_size,
|
workerGroupSize = config.network.worker_group_size,
|
||||||
callGroupSize = config.network.call_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.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.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
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.server.NostrServer
|
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.nip77Negentropy.NegentropySettings
|
||||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanListPolicy
|
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanListPolicy
|
||||||
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore
|
import com.vitorpamplona.quartz.nip86RelayManagement.server.BanStore
|
||||||
|
import com.vitorpamplona.quartz.nip86RelayManagement.server.Nip86Server
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
@@ -92,6 +94,14 @@ class RelayEngine(
|
|||||||
* per-connection session cap). Defaults to strfry-parity values.
|
* per-connection session cap). Defaults to strfry-parity values.
|
||||||
*/
|
*/
|
||||||
negentropySettings: NegentropySettings = NegentropySettings.Default,
|
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 {
|
) : AutoCloseable {
|
||||||
/**
|
/**
|
||||||
* The runtime state to install at boot: the persisted snapshot if
|
* 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.
|
* Writes the current state (NIP-11 doc + ban lists) to disk.
|
||||||
* No-op when no `stateFile` was configured (see [RuntimeConfig.save]).
|
* 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.
|
// server expects to differ from where it actually listens.
|
||||||
val freePort = java.net.ServerSocket(0).use { it.localPort }
|
val freePort = java.net.ServerSocket(0).use { it.localPort }
|
||||||
val url = "ws://127.0.0.1:$freePort/".normalizeRelayUrl()
|
val url = "ws://127.0.0.1:$freePort/".normalizeRelayUrl()
|
||||||
relay = RelayEngine(url = url)
|
relay = RelayEngine(url = url, adminPubkeys = setOf(admin.pubKey))
|
||||||
server =
|
server =
|
||||||
KtorRelay(
|
KtorRelay(
|
||||||
relay = relay,
|
relay = relay,
|
||||||
host = "127.0.0.1",
|
host = "127.0.0.1",
|
||||||
port = freePort,
|
port = freePort,
|
||||||
adminPubkeys = setOf(admin.pubKey),
|
|
||||||
).start()
|
).start()
|
||||||
scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
val builder = BasicOkHttpWebSocket.Builder { _ -> httpClient }
|
val builder = BasicOkHttpWebSocket.Builder { _ -> httpClient }
|
||||||
|
|||||||
Reference in New Issue
Block a user