From a54d9c92a7ba4196b8799027663bef0710710316 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 16:45:03 +0000 Subject: [PATCH] refactor(quartz/relay): move auth identity from policy into connection scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authentication state (who is logged in on a connection) is connection scope, not a policy decision. It was stored on FullAuthPolicy, which forced the AuthScopedPolicy marker, the PolicyStack union, and a downcast in RequestContext just to route it back out as scope. Now the engine owns it: RelaySession holds the authenticatedUsers set behind the (now public) requestContext; the data plane and policies read it through RequestContext. The policy stays pure decision — - onConnect(scope, send): a per-connection policy captures the read-only scope to gate on; shared singletons ignore it. - onAuthenticated(): Boolean: the policy's vote on whether to record the verified pubkey (default false, so blind-accept policies never record an unverified identity). FullAuthPolicy runs authorize() then votes true. - RelaySession.handleAuth performs the single, engine-side commit after the whole chain approves and a verifying policy votes to record. FullAuthPolicy keeps all auth logic (challenge, accept(AuthCmd), gating, authorize) and gains a protected authenticatedUsers accessor over the scope for subclasses (restricted content / filter rewrite). Deletes AuthScopedPolicy, PolicyStack.authenticatedUsers, and the RequestContext downcast. --- quartz/RELAY.md | 6 +- .../nip01Core/relay/server/RelaySession.kt | 47 ++++++++++------ .../relay/server/backend/RequestContext.kt | 22 ++++---- .../relay/server/policies/AuthScopedPolicy.kt | 40 ------------- .../relay/server/policies/FullAuthPolicy.kt | 56 ++++++++++++------- .../relay/server/policies/IRelayPolicy.kt | 37 ++++++++---- .../server/policies/PassThroughPolicy.kt | 6 +- .../relay/server/policies/PolicyStack.kt | 21 +++---- .../relay/server/policies/VerifyPolicy.kt | 6 +- .../relay/server/NostrServerAuthTest.kt | 12 ++-- 10 files changed, 137 insertions(+), 116 deletions(-) delete mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt diff --git a/quartz/RELAY.md b/quartz/RELAY.md index f917a6fc68..dda794bae9 100644 --- a/quartz/RELAY.md +++ b/quartz/RELAY.md @@ -151,8 +151,10 @@ non-storage relay: a single shared `EventSource` instance can serve caller-relative results (trust/relevance scored from the viewer's perspective, "for-you" feeds), restricted content (a pubkey's DMs returned only to that pubkey), or per-connection tenancy — without smuggling auth state through a -side channel. `ctx.authenticatedUsers` reads live from the connection's policy, -so a REQ that arrives after the AUTH sees the freshly authenticated pubkey(s). +side channel. `ctx.authenticatedUsers` is a live view of the engine-owned +connection scope, so a REQ that arrives after the AUTH sees the freshly +recorded pubkey(s). (The engine records them on a successful NIP-42 AUTH; the +policy reads the same scope to gate.) For per-connection state richer than the pubkey (e.g. a backend session token minted in `FullAuthPolicy.authorize`), downcast `ctx.policy` to your own policy diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt index 7a95e4cbb7..7f3e844f3c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/RelaySession.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.server +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage @@ -76,15 +77,24 @@ class RelaySession( private val subscriptions = LargeCache() /** - * The per-connection context handed to the [store] on every REQ/COUNT so a - * source can see who is asking. [RequestContext.authenticatedUsers] reads - * live from [policy], so a REQ that arrives after a NIP-42 AUTH sees the - * freshly authenticated pubkey(s). + * The authenticated-identity store for this connection. The engine is the + * only writer (committed in [handleAuth] on a successful NIP-42 AUTH); the + * policy and the data plane read it through [requestContext]. */ - private val requestContext = + private val authenticatedUsers = mutableSetOf() + + /** + * The per-connection scope. Handed to the [policy] at connect (so gating + * policies can read the authenticated users) and to the [store] on every + * REQ/COUNT (so a source can see who is asking). [RequestContext.authenticatedUsers] + * is a live view of [authenticatedUsers], so a REQ after a NIP-42 AUTH sees + * the freshly recorded pubkey(s). + */ + val requestContext: RequestContext = object : RequestContext { override val connectionId = id override val policy = this@RelaySession.policy + override val authenticatedUsers: Set get() = this@RelaySession.authenticatedUsers } /** NIP-77 negentropy state for this connection. */ @@ -223,16 +233,21 @@ class RelaySession( // The whole policy chain validated the AUTH. onAuthenticated runs any // post-verification I/O (e.g. exchanging the verified event for a - // backend token) AND is where a policy commits the authentication, so a - // throw here cleanly fails the login — nothing was committed to undo. - try { - policy.onAuthenticated(cmd.event.pubKey, cmd.event) - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - send(OkMessage.rejected(cmd.event.id, MachineReadablePrefix.ERROR, e.message ?: "authentication failed")) - return - } + // backend token) and votes on whether to record the identity. A throw + // here cleanly fails the login — the engine records nothing. + val record = + try { + policy.onAuthenticated(cmd.event.pubKey, cmd.event) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + send(OkMessage.rejected(cmd.event.id, MachineReadablePrefix.ERROR, e.message ?: "authentication failed")) + return + } + + // Single, engine-side commit into the connection scope — after the full + // chain approved and a verifying policy voted to record. + if (record) authenticatedUsers.add(cmd.event.pubKey) send(OkMessage(cmd.event.id, true, "")) } @@ -299,7 +314,7 @@ class RelaySession( } init { - policy.onConnect(::send) + policy.onConnect(requestContext, ::send) } companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/RequestContext.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/RequestContext.kt index f097273de7..b492e80017 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/RequestContext.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/backend/RequestContext.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.server.backend import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.server.policies.AuthScopedPolicy import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy /** @@ -30,11 +29,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.IRelayPolicy * shared source can tailor its answer to the caller without smuggling state in * through a side channel. * - * This is what makes NIP-42 useful on a non-storage relay: the engine already - * knows the authenticated pubkey(s) (the connection's [IRelayPolicy] records - * them), and [RequestContext] is the path that carries them to the code that - * actually produces the events. With it, the same `EventSource` instance can - * serve: + * This is the connection scope: the engine owns it (one per + * [com.vitorpamplona.quartz.nip01Core.relay.server.RelaySession]) and records + * the authenticated pubkey(s) into it on a successful NIP-42 AUTH. That is what + * makes NIP-42 useful on a non-storage relay — [RequestContext] is the path + * that carries the caller's identity to the code that produces the events. With + * it, the same `EventSource` instance can serve: * * - **caller-relative results** — trust/relevance scored from * [authenticatedUsers]'s perspective ("for-you" feeds, follow-aware search); @@ -60,10 +60,10 @@ interface RequestContext { val policy: IRelayPolicy /** - * The pubkeys that have authenticated on this connection via NIP-42, read - * live from [policy]. Empty when the connection is unauthenticated or the - * policy does not track auth (i.e. is not an [AuthScopedPolicy]). This - * accessor encapsulates that downcast so a source needn't repeat it. + * The pubkeys that have authenticated on this connection via NIP-42. Empty + * when the connection is unauthenticated. Backed by the engine-owned scope + * and read live, so a REQ that arrives after a successful AUTH sees the + * freshly recorded pubkey(s). */ - val authenticatedUsers: Set get() = (policy as? AuthScopedPolicy)?.authenticatedUsers ?: emptySet() + val authenticatedUsers: Set } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt deleted file mode 100644 index 9dd3a3280a..0000000000 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip01Core.relay.server.policies - -import com.vitorpamplona.quartz.nip01Core.core.HexKey - -/** - * Opt-in mixin for the policies that actually track NIP-42 authentication, so - * the auth concept stays out of the universal [IRelayPolicy]. A relay that does - * no auth never implements this; only [FullAuthPolicy] (and [PolicyStack], which - * unions any auth-tracking members) do. - * - * The engine surfaces it to the data plane through - * [com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext.authenticatedUsers], - * which downcasts the connection's policy to this interface — so a source sees - * the authenticated pubkey(s) without the base policy interface knowing about - * NIP-42. - */ -interface AuthScopedPolicy { - /** The pubkeys that have authenticated on this connection via NIP-42. */ - val authenticatedUsers: Set -} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt index 6730079008..21dc917e10 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/FullAuthPolicy.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.RandomInstance @@ -40,11 +41,13 @@ import com.vitorpamplona.quartz.utils.TimeUtils * * Implements the full NIP-42 challenge/verify handshake: [onConnect] sends the * [challenge] and [accept] (AuthCmd) validates the returned event (expiration, - * freshness, challenge match, relay match). Crucially, [accept] does NOT mutate - * state — the pubkey is recorded in [authenticatedUsers] only by [onAuthenticated], - * which the engine calls once [accept] *and* the rest of the policy chain have - * approved the AUTH. That single, late commit is why there is no rollback to - * reason about: a rejected AUTH simply never reaches it. + * freshness, challenge match, relay match). This policy runs the auth *logic* + * but does not *own* the authenticated-identity store: the engine-owned + * connection [scope] holds it. [accept] does NOT mutate state, and + * [onAuthenticated] only votes `true` (after [authorize]) — the engine performs + * the single recording into the scope once the whole chain has approved. Gating + * decisions read [scope].`authenticatedUsers`. A rejected AUTH simply never + * reaches [onAuthenticated], so there is no rollback to reason about. * * To bridge to an external auth system, override [authorize] (a `suspend` hook) * and do the post-verification I/O there — e.g. exchange the verified event for @@ -53,18 +56,34 @@ import com.vitorpamplona.quartz.utils.TimeUtils */ open class FullAuthPolicy( val relay: NormalizedRelayUrl, -) : IRelayPolicy, - AuthScopedPolicy { +) : IRelayPolicy { /** The challenge string sent to this client for NIP-42 authentication. */ val challenge: String = RandomInstance.randomChars(32) - /** Set of pubkeys that have successfully authenticated on this session. */ - override val authenticatedUsers = mutableSetOf() + /** + * The engine-owned connection scope, captured at [onConnect]. Read-only + * here: this policy reads [RequestContext.authenticatedUsers] to gate, while + * the engine is the only writer. Held safely because a [FullAuthPolicy] is + * built fresh per connection. + */ + private lateinit var scope: RequestContext - /** Returns true if at least one pubkey has authenticated. */ + /** + * The pubkeys authenticated on this connection, read from the engine-owned + * scope. Exposed to subclasses so they can gate or rewrite on the caller's + * identity (restricted content, caller-relative filters) — the same set the + * data plane sees via [RequestContext.authenticatedUsers]. + */ + protected val authenticatedUsers: Set get() = scope.authenticatedUsers + + /** Returns true if at least one pubkey has authenticated on this connection. */ fun isAuthenticated(): Boolean = authenticatedUsers.isNotEmpty() - override fun onConnect(send: (Message) -> Unit) { + override fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) { + this.scope = scope send(AuthMessage(challenge)) } @@ -91,19 +110,18 @@ open class FullAuthPolicy( } /** - * Commits the authentication. The engine calls this only after [accept] and - * the whole policy chain have approved the AUTH, so this is the single point - * where the pubkey is recorded. It runs [authorize] first (which may throw - * to reject) and records the pubkey only on success — the connection is - * never left authenticated behind a failing `OK`. `final`: override - * [authorize], not this. + * Votes to record the authentication. The engine calls this only after + * [accept] and the whole policy chain have approved the AUTH. It runs + * [authorize] first (which may throw to reject — the engine then records + * nothing) and returns `true` so the engine records [pubKey] into the + * connection scope. `final`: override [authorize], not this. */ final override suspend fun onAuthenticated( pubKey: HexKey, event: RelayAuthEvent, - ) { + ): Boolean { authorize(pubKey, event) - authenticatedUsers.add(pubKey) + return true } /** diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt index fa3bbb8d31..7136197fb4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/IRelayPolicy.kt @@ -28,13 +28,24 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent /** * Defines custom behavior for this relay. */ interface IRelayPolicy { - fun onConnect(send: (Message) -> Unit) + /** + * Called once when the connection opens. [scope] is the engine-owned, + * read-only connection scope (id + authenticated users) — a per-connection + * policy may retain it to make later auth-aware decisions; shared singleton + * policies must ignore it and stay stateless. [send] pushes a message to the + * client (e.g. a NIP-42 AUTH challenge). + */ + fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) /** * Evaluates whether an incoming EVENT command should be accepted. @@ -70,24 +81,30 @@ interface IRelayPolicy { /** * Called once an AUTH command has been [accept]ed by this policy *and* the - * rest of the policy chain, before the success `OK` is sent. This is where - * a policy commits the authentication and/or runs post-verification side - * effects that need network or disk I/O — e.g. exchanging the verified - * NIP-42 event for a backend session token — without leaking that logic into - * the transport layer. + * rest of the policy chain, before the success `OK` is sent. Run any + * post-verification side effects that need network or disk I/O here — e.g. + * exchanging the verified NIP-42 event for a backend session token — without + * leaking that logic into the transport layer. + * + * The engine — not the policy — owns the authenticated-identity store. The + * return value is this policy's vote on whether [pubKey] should be recorded + * as authenticated on the connection: return `true` only if this policy + * actually verified the identity. The default returns `false`, so a policy + * that does not authenticate (e.g. a pass-through or a blind-accept) never + * causes an unverified pubkey to be recorded. * * Because it runs only after the whole chain approved the AUTH, throwing - * here cleanly fails the login: the AUTH becomes `OK false` and, since the - * commit lives here too, the connection is never left authenticated. The - * default implementation does nothing. + * here cleanly fails the login: the AUTH becomes `OK false` and the engine + * records nothing. * * @param pubKey The pubkey being authenticated. * @param event The verified NIP-42 auth event. + * @return `true` to have the engine record [pubKey] as authenticated. */ suspend fun onAuthenticated( pubKey: HexKey, event: RelayAuthEvent, - ) {} + ): Boolean = false /** * Inspects a raw inbound message before it is parsed. Return a reason diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt index 79d9a306ca..1d673f6e8b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PassThroughPolicy.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext /** * Convenience base that accepts everything by default. Subclasses @@ -37,7 +38,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd * instantiate this directly. */ open class PassThroughPolicy : IRelayPolicy { - override fun onConnect(send: (Message) -> Unit) {} + override fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) {} override fun accept(cmd: EventCmd): PolicyResult = PolicyResult.Accepted(cmd) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt index 94c7afa79a..d7b1585fdf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/PolicyStack.kt @@ -28,20 +28,19 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent class PolicyStack( vararg policies: IRelayPolicy, -) : IRelayPolicy, - AuthScopedPolicy { +) : IRelayPolicy { val policies = policies.toList() - /** Union of the authenticated pubkeys across the auth-tracking members. */ - override val authenticatedUsers: Set - get() = policies.filterIsInstance().flatMapTo(mutableSetOf()) { it.authenticatedUsers } - - override fun onConnect(send: (Message) -> Unit) { - policies.forEach { it.onConnect(send) } + override fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) { + policies.forEach { it.onConnect(scope, send) } } override fun accept(cmd: EventCmd) = runPolicies(cmd) { p, c -> p.accept(c) } @@ -55,8 +54,10 @@ class PolicyStack( override suspend fun onAuthenticated( pubKey: HexKey, event: RelayAuthEvent, - ) { - policies.forEach { it.onAuthenticated(pubKey, event) } + ): Boolean { + // Run every member (side effects) and record iff any one verified the + // identity. `fold` keeps the call on the left so no member is skipped. + return policies.fold(false) { recorded, p -> p.onAuthenticated(pubKey, event) || recorded } } override fun acceptMessage(message: String): String? = policies.firstNotNullOfOrNull { it.acceptMessage(message) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt index 8961ff9041..a97c1431fa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/VerifyPolicy.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext /** * Verifies the Schnorr signature + id hash of every incoming @@ -43,7 +44,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd open class VerifyEventsAndAuthPolicy( private val verifyEvents: Boolean, ) : IRelayPolicy { - override fun onConnect(send: (Message) -> Unit) { } + override fun onConnect( + scope: RequestContext, + send: (Message) -> Unit, + ) { } override fun accept(cmd: EventCmd) = if (!verifyEvents || cmd.event.verify()) { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerAuthTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerAuthTest.kt index d08f7476ee..d601dcecdd 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerAuthTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerAuthTest.kt @@ -168,7 +168,7 @@ class NostrServerAuthTest { assertEquals(1, okMessages.size) assertTrue(okMessages[0].contains(",true,")) assertTrue((session.policy as FullAuthPolicy).isAuthenticated()) - assertTrue(session.policy.authenticatedUsers.contains(pubkey)) + assertTrue(session.requestContext.authenticatedUsers.contains(pubkey)) server.close() } @@ -313,7 +313,7 @@ class NostrServerAuthTest { assertTrue(okMessages[0].contains(",true,")) assertTrue(okMessages[1].contains(",true,")) - val authedPubkeys = (session.policy as FullAuthPolicy).authenticatedUsers + val authedPubkeys = session.requestContext.authenticatedUsers assertEquals(2, authedPubkeys.size) assertTrue(authedPubkeys.contains(pubkey)) assertTrue(authedPubkeys.contains(pubkey2)) @@ -596,7 +596,7 @@ class NostrServerAuthTest { // still-authenticated connection would be an auth bypass. val authPolicy = session.policy as FullAuthPolicy assertFalse(authPolicy.isAuthenticated()) - assertFalse(authPolicy.authenticatedUsers.contains(pubkey)) + assertFalse(session.requestContext.authenticatedUsers.contains(pubkey)) server.close() } @@ -653,7 +653,7 @@ class NostrServerAuthTest { assertEquals(1, ok.size) assertTrue(ok[0].contains(",false,")) assertFalse(auth.isAuthenticated()) - assertFalse(auth.authenticatedUsers.contains(pubkey)) + assertFalse(session.requestContext.authenticatedUsers.contains(pubkey)) // And a privileged REQ is still gated. session.receive("""["REQ","sub1",{"kinds":[1]}]""") @@ -686,12 +686,12 @@ class NostrServerAuthTest { val msg = OptimizedJsonMapper.fromJsonToMessage(collector.messages[0]) as AuthMessage session.receive(authJson(authEvent(challenge = msg.challenge))) - assertTrue(auth.authenticatedUsers.contains(pubkey)) + assertTrue(session.requestContext.authenticatedUsers.contains(pubkey)) // Second AUTH for the SAME pubkey, rejected downstream. session.receive(authJson(authEvent(challenge = msg.challenge))) assertTrue(auth.isAuthenticated()) - assertTrue(auth.authenticatedUsers.contains(pubkey)) + assertTrue(session.requestContext.authenticatedUsers.contains(pubkey)) server.close() }