diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt index 4d3eba299..3d6cea8ca 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt @@ -26,7 +26,6 @@ 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.nip42RelayAuth.RelayAuthEvent import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -116,7 +115,7 @@ object CommandKSerializer : KSerializer { } AuthCmd.LABEL -> { - AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject) as RelayAuthEvent) + AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject)) } else -> { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt index 01d41b767..8d187b8e6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay -import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.nip01Core.core.Event class AuthCmd( - val event: RelayAuthEvent, + val event: Event, ) : Command { override fun label(): String = LABEL diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServer.kt index 96476ab24..329f3abd6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServer.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.server import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.verify +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.CoroutineScope @@ -40,11 +41,16 @@ import kotlin.coroutines.CoroutineContext * provided to [connect]. This allows use with any WebSocket library. * * @param store The [EventStore] backing this relay. + * @param relayUrl The URL of this relay, used for NIP-42 authentication. + * @param requireAuth When true, clients must authenticate (NIP-42) before + * sending EVENT, REQ, or COUNT commands. * @param verify Validates incoming events. Defaults to cryptographic * verification (id + signature). Override for testing. */ class NostrServer( private val store: IEventStore, + val relayUrl: NormalizedRelayUrl = NormalizedRelayUrl("wss://relay.example.com/"), + val requireAuth: Boolean = false, private val parentContext: CoroutineContext = SupervisorJob(), private val verify: (Event) -> Boolean = { it.verify() }, ) { @@ -63,6 +69,7 @@ class NostrServer( */ fun connect(send: (String) -> Unit) = RelaySession( + server = this, store = subStore, verify = verify, scope = scope, 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 da27c4a45..8f5737369 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 @@ -21,7 +21,9 @@ package com.vitorpamplona.quartz.nip01Core.relay.server import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult @@ -30,11 +32,15 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd 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.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -42,8 +48,13 @@ import kotlinx.coroutines.launch /** * Represents a single connected client with its active subscriptions. + * + * Supports NIP-42 authentication. Multiple pubkeys may authenticate on + * the same session. When [NostrServer.requireAuth] is true, EVENT, REQ + * and COUNT commands are rejected until at least one pubkey authenticates. */ class RelaySession( + private val server: NostrServer, private val store: LiveEventStore, private val verify: (Event) -> Boolean, private val scope: CoroutineScope, @@ -52,6 +63,26 @@ class RelaySession( ) : AutoCloseable { private val subscriptions = LargeCache() + /** 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. */ + private val authenticatedUsers = mutableSetOf() + + /** Returns true if at least one pubkey has authenticated. */ + fun isAuthenticated(): Boolean = authenticatedUsers.isNotEmpty() + + /** Returns the set of authenticated pubkeys for this session. */ + fun authenticatedPubkeys(): Set = authenticatedUsers.toSet() + + /** + * Sends the AUTH challenge to the client. + * Call this after the WebSocket connection is established. + */ + fun sendAuthChallenge() { + send(AuthMessage(challenge)) + } + fun send(message: Message) { try { onSend(OptimizedJsonMapper.toJson(message)) @@ -101,6 +132,7 @@ class RelaySession( } when (cmd) { + is AuthCmd -> handleAuth(cmd) is EventCmd -> handleEvent(cmd) is ReqCmd -> handleReq(cmd) is CloseCmd -> handleClose(cmd) @@ -109,10 +141,70 @@ class RelaySession( } } + // -- NIP-42: AUTH --------------------------------------------------------- + private fun handleAuth(cmd: AuthCmd) { + val event = cmd.event + + // Must be kind 22242 + if (event.kind != RelayAuthEvent.KIND) { + send(OkMessage(event.id, false, "invalid: wrong event kind")) + return + } + + // Verify signature and id + if (!verify(event)) { + send(OkMessage(event.id, false, "invalid: bad signature or id")) + return + } + + // created_at must be within 10 minutes + val now = TimeUtils.now() + val tenMinutes = 600L + if (event.createdAt < now - tenMinutes || event.createdAt > now + tenMinutes) { + send(OkMessage(event.id, false, "invalid: created_at is too far from the current time")) + return + } + + // Challenge tag must match + val eventChallenge = + event.tags.firstNotNullOfOrNull { tag -> + if (tag.size >= 2 && tag[0] == "challenge") tag[1] else null + } + if (eventChallenge != challenge) { + send(OkMessage(event.id, false, "invalid: challenge does not match")) + return + } + + // Relay tag must match this relay's URL + val eventRelay = + event.tags.firstNotNullOfOrNull { tag -> + if (tag.size >= 2 && tag[0] == "relay") tag[1] else null + } + if (eventRelay == null || !relayUrlMatches(eventRelay)) { + send(OkMessage(event.id, false, "invalid: relay url does not match")) + return + } + + // Authentication successful — add pubkey + authenticatedUsers.add(event.pubKey) + send(OkMessage(event.id, true, "")) + } + + private fun relayUrlMatches(eventRelayUrl: String): Boolean { + val ours = server.relayUrl.url.trimEnd('/') + val theirs = eventRelayUrl.trimEnd('/') + return ours.equals(theirs, ignoreCase = true) + } + // -- NIP-01: EVENT -------------------------------------------------------- private fun handleEvent(cmd: EventCmd) { val event = cmd.event + if (server.requireAuth && !isAuthenticated()) { + send(OkMessage(event.id, false, "auth-required: this relay requires authentication")) + return + } + if (!verify(event)) { send(OkMessage(event.id, false, "invalid: bad signature or id")) return @@ -128,6 +220,11 @@ class RelaySession( // -- NIP-01: REQ ---------------------------------------------------------- private fun handleReq(cmd: ReqCmd) { + if (server.requireAuth && !isAuthenticated()) { + send(ClosedMessage(cmd.subId, "auth-required: this relay requires authentication")) + return + } + // Cancel any existing subscription with the same id (NIP-01 spec). cancelSubscription(cmd.subId) @@ -157,6 +254,11 @@ class RelaySession( // -- NIP-45: COUNT -------------------------------------------------------- private fun handleCount(cmd: CountCmd) { + if (server.requireAuth && !isAuthenticated()) { + send(ClosedMessage(cmd.queryId, "auth-required: this relay requires authentication")) + return + } + val total = store.count(cmd.filters) send(CountMessage(cmd.queryId, CountResult(total))) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt index d9eb0e09c..4eddd9315 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/NostrServerTest.kt @@ -26,19 +26,25 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class NostrServerTest { private val pubkey = "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d" + private val pubkey2 = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" private val sig = "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce" + private val relayUrl = NormalizedRelayUrl("wss://relay.example.com/") private fun hexId(n: Int): String = n.toString().padStart(64, '0') @@ -57,9 +63,12 @@ class NostrServerTest { private fun createServer( store: IEventStore = EventStore(null), dispatcher: kotlinx.coroutines.CoroutineDispatcher, + requireAuth: Boolean = false, ): NostrServer = NostrServer( store = store, + relayUrl = relayUrl, + requireAuth = requireAuth, parentContext = dispatcher, verify = { true }, ) @@ -89,6 +98,31 @@ class NostrServerTest { fun rawMessagesContaining(label: String) = messages.filter { it.contains("\"$label\"") } } + /** + * Builds a kind 22242 auth event for testing. Because verify = { true }, + * the id and signature don't need to be real. + */ + private fun authEvent( + challenge: String, + relay: String = relayUrl.url, + pubKey: String = pubkey, + createdAt: Long = TimeUtils.now(), + ) = Event( + id = hexId(99), + pubKey = pubKey, + createdAt = createdAt, + kind = RelayAuthEvent.KIND, + tags = + arrayOf( + arrayOf("relay", relay), + arrayOf("challenge", challenge), + ), + content = "", + sig = sig, + ) + + private fun authJson(event: Event) = """["AUTH",${event.toJson()}]""" + // -- EVENT command --------------------------------------------------------- @Test @@ -390,6 +424,289 @@ class NostrServerTest { assertEquals(1, collector.messages.size) assertTrue(collector.messages[0].contains("NOTICE")) + server.shutdown() + } + + // -- NIP-42: AUTH ---------------------------------------------------------- + + @Test + fun authChallengeIsSentOnRequest() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + session.sendAuthChallenge() + + assertEquals(1, collector.messages.size) + assertTrue(collector.messages[0].contains("\"AUTH\"")) + assertTrue(collector.messages[0].contains(session.challenge)) + + server.shutdown() + } + + @Test + fun authSucceedsWithValidEvent() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + val event = authEvent(challenge = session.challenge) + session.processMessage(authJson(event)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"true\"")) + assertTrue(session.isAuthenticated()) + assertTrue(session.authenticatedPubkeys().contains(pubkey)) + + server.shutdown() + } + + @Test + fun authFailsWithWrongChallenge() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + val event = authEvent(challenge = "wrong-challenge") + session.processMessage(authJson(event)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"false\"")) + assertTrue(okMessages[0].contains("challenge")) + assertFalse(session.isAuthenticated()) + + server.shutdown() + } + + @Test + fun authFailsWithWrongRelay() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + val event = authEvent(challenge = session.challenge, relay = "wss://wrong.relay.com/") + session.processMessage(authJson(event)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"false\"")) + assertTrue(okMessages[0].contains("relay url")) + assertFalse(session.isAuthenticated()) + + server.shutdown() + } + + @Test + fun authFailsWithExpiredTimestamp() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + val event = + authEvent( + challenge = session.challenge, + createdAt = TimeUtils.now() - 1200L, // 20 minutes ago + ) + session.processMessage(authJson(event)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"false\"")) + assertTrue(okMessages[0].contains("created_at")) + assertFalse(session.isAuthenticated()) + + server.shutdown() + } + + @Test + fun authFailsWithWrongKind() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + // Create an event with wrong kind (1 instead of 22242) + val event = + Event( + id = hexId(99), + pubKey = pubkey, + createdAt = TimeUtils.now(), + kind = 1, + tags = + arrayOf( + arrayOf("relay", relayUrl.url), + arrayOf("challenge", session.challenge), + ), + content = "", + sig = sig, + ) + session.processMessage(authJson(event)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"false\"")) + assertTrue(okMessages[0].contains("wrong event kind")) + assertFalse(session.isAuthenticated()) + + server.shutdown() + } + + @Test + fun multipleUsersCanAuthenticate() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + // First user authenticates + val event1 = authEvent(challenge = session.challenge, pubKey = pubkey) + session.processMessage(authJson(event1)) + + // Second user authenticates on the same session + val event2 = authEvent(challenge = session.challenge, pubKey = pubkey2) + session.processMessage(authJson(event2)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(2, okMessages.size) + assertTrue(okMessages[0].contains("\"true\"")) + assertTrue(okMessages[1].contains("\"true\"")) + + val authedPubkeys = session.authenticatedPubkeys() + assertEquals(2, authedPubkeys.size) + assertTrue(authedPubkeys.contains(pubkey)) + assertTrue(authedPubkeys.contains(pubkey2)) + + server.shutdown() + } + + // -- NIP-42: requireAuth --------------------------------------------------- + + @Test + fun requireAuthRejectsEventWithoutAuth() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher, requireAuth = true) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + val event = testEvent() + session.processMessage("""["EVENT",${event.toJson()}]""") + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"false\"")) + assertTrue(okMessages[0].contains("auth-required:")) + + server.shutdown() + } + + @Test + fun requireAuthRejectsReqWithoutAuth() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher, requireAuth = true) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + session.processMessage("""["REQ","sub1",{"kinds":[1]}]""") + + val closedMessages = collector.rawMessagesContaining("CLOSED") + assertEquals(1, closedMessages.size) + assertTrue(closedMessages[0].contains("auth-required:")) + + server.shutdown() + } + + @Test + fun requireAuthRejectsCountWithoutAuth() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher, requireAuth = true) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + session.processMessage("""["COUNT","q1",{"kinds":[1]}]""") + + val closedMessages = collector.rawMessagesContaining("CLOSED") + assertEquals(1, closedMessages.size) + assertTrue(closedMessages[0].contains("auth-required:")) + + server.shutdown() + } + + @Test + fun requireAuthAllowsCommandsAfterAuth() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val store = EventStore(null) + val server = createServer(store = store, dispatcher = dispatcher, requireAuth = true) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + // Authenticate first + val authEv = authEvent(challenge = session.challenge) + session.processMessage(authJson(authEv)) + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"true\"")) + + // Now EVENT should work + val event = testEvent() + session.processMessage("""["EVENT",${event.toJson()}]""") + + val allOk = collector.rawMessagesContaining("OK") + assertEquals(2, allOk.size) + assertTrue(allOk[1].contains("\"true\"")) + + // REQ should work + session.processMessage("""["REQ","sub1",{"kinds":[1]}]""") + + val eoseMessages = collector.rawMessagesContaining("EOSE") + assertTrue(eoseMessages.isNotEmpty()) + + server.shutdown() + } + + @Test + fun noAuthRequiredAllowsCommandsWithoutAuth() = + runTest { + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val server = createServer(dispatcher = dispatcher, requireAuth = false) + val collector = MessageCollector() + + val session = server.connect(collector.sendCallback) + + // EVENT should work without auth when requireAuth is false + val event = testEvent() + session.processMessage("""["EVENT",${event.toJson()}]""") + + val okMessages = collector.rawMessagesContaining("OK") + assertEquals(1, okMessages.size) + assertTrue(okMessages[0].contains("\"true\"")) + server.shutdown() } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CommandDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CommandDeserializer.kt index f8265d069..40f5cae03 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CommandDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CommandDeserializer.kt @@ -28,7 +28,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode import com.vitorpamplona.quartz.nip01Core.jackson.EventDeserializer import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.ManualFilterDeserializer -import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent class CommandDeserializer : StdDeserializer(Command::class.java) { val eventDeserializer = EventDeserializer() @@ -93,7 +92,7 @@ class CommandDeserializer : StdDeserializer(Command::class.java) { AuthCmd.LABEL -> { jp.nextToken() AuthCmd( - event = eventDeserializer.deserialize(jp, ctxt) as RelayAuthEvent, + event = eventDeserializer.deserialize(jp, ctxt), ) }