From a98f5fddd21f2ffe860d40d671d27d1906fd5bda Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 15:36:40 +0000 Subject: [PATCH] refactor(quartz/relay): move authenticatedUsers off IRelayPolicy to a marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the universal policy interface free of NIP-42: instead of a default authenticatedUsers on IRelayPolicy, add an opt-in AuthScopedPolicy mixin that only FullAuthPolicy (and PolicyStack, which unions its auth-tracking members) implements. RequestContext.authenticatedUsers resolves it via an `as? AuthScopedPolicy` downcast, defaulting to empty — so non-auth relays carry no auth concept, and the accessor now earns its keep by encapsulating that cast (ctx.policy is IRelayPolicy and no longer exposes the set directly). --- .../relay/server/backend/RequestContext.kt | 6 ++- .../relay/server/policies/AuthScopedPolicy.kt | 40 +++++++++++++++++++ .../relay/server/policies/FullAuthPolicy.kt | 3 +- .../relay/server/policies/IRelayPolicy.kt | 10 ----- .../relay/server/policies/PolicyStack.kt | 7 ++-- 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt 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 9309cfe1f0..f097273de7 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,6 +21,7 @@ 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 /** @@ -61,7 +62,8 @@ interface RequestContext { /** * 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 implement auth. + * policy does not track auth (i.e. is not an [AuthScopedPolicy]). This + * accessor encapsulates that downcast so a source needn't repeat it. */ - val authenticatedUsers: Set get() = policy.authenticatedUsers + val authenticatedUsers: Set get() = (policy as? AuthScopedPolicy)?.authenticatedUsers ?: emptySet() } 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 new file mode 100644 index 0000000000..9dd3a3280a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/policies/AuthScopedPolicy.kt @@ -0,0 +1,40 @@ +/* + * 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 bce8a1bc6b..6730079008 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 @@ -53,7 +53,8 @@ import com.vitorpamplona.quartz.utils.TimeUtils */ open class FullAuthPolicy( val relay: NormalizedRelayUrl, -) : IRelayPolicy { +) : IRelayPolicy, + AuthScopedPolicy { /** The challenge string sent to this client for NIP-42 authentication. */ val challenge: String = RandomInstance.randomChars(32) 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 0708f4c098..fa3bbb8d31 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 @@ -34,16 +34,6 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent * Defines custom behavior for this relay. */ interface IRelayPolicy { - /** - * The pubkeys this connection has authenticated as via NIP-42. The engine - * surfaces this to the data plane through - * [com.vitorpamplona.quartz.nip01Core.relay.server.backend.RequestContext.authenticatedUsers] - * so a source can tailor results to the caller. The default is empty — a - * policy that does no auth (e.g. [EmptyPolicy]) never authenticates anyone; - * [FullAuthPolicy] overrides it with the set it commits on a successful AUTH. - */ - val authenticatedUsers: Set get() = emptySet() - fun onConnect(send: (Message) -> Unit) /** 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 288bba7c3d..94c7afa79a 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 @@ -32,12 +32,13 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent class PolicyStack( vararg policies: IRelayPolicy, -) : IRelayPolicy { +) : IRelayPolicy, + AuthScopedPolicy { val policies = policies.toList() - /** Union of the authenticated pubkeys across every composed policy. */ + /** Union of the authenticated pubkeys across the auth-tracking members. */ override val authenticatedUsers: Set - get() = policies.flatMapTo(mutableSetOf()) { it.authenticatedUsers } + get() = policies.filterIsInstance().flatMapTo(mutableSetOf()) { it.authenticatedUsers } override fun onConnect(send: (Message) -> Unit) { policies.forEach { it.onConnect(send) }