diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt index d934b4618..0e4506a84 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlin.concurrent.Volatile class PoolEventOutbox { // @Volatile so the polling path (INostrClient.pendingPublishRelaysFor) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxState.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxState.kt index 28fb62967..4df3f7d53 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxState.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.pool import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.concurrent.Volatile class PoolEventOutboxState( val event: Event, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocket.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocket.kt index 1c4bb53c3..69def1b78 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocket.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/server/inprocess/InProcessWebSocket.kt @@ -65,7 +65,7 @@ class InProcessWebSocket( override fun connect() { if (session != null) return - val newScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val newScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) val newIncoming = Channel(UNLIMITED) val s = server.connect { json -> out.onMessage(json) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt index b88bcccc6..ebd580ee1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip98HttpAuth/Nip98AuthVerifier.kt @@ -55,18 +55,15 @@ class Nip98AuthVerifier( ) { /** * Recently-accepted event ids → expiry epoch second. Bounded to - * [MAX_REPLAY_ENTRIES] (LRU eviction); each entry expires after - * `2 × toleranceSeconds` (twice the accepted window so a token + * [MAX_REPLAY_ENTRIES] (insertion-order eviction); each entry expires + * after `2 × toleranceSeconds` (twice the accepted window so a token * can't be reused by an attacker who buffers across the boundary). * * Guarded by [seenLock] so the eviction sweep + insertion are * atomic. We use a coroutine [Mutex] so the type works in KMP * commonMain (no `synchronized` block). */ - private val seenEventIds: LinkedHashMap = - object : LinkedHashMap(64, 0.75f, true) { - override fun removeEldestEntry(eldest: Map.Entry?): Boolean = size > MAX_REPLAY_ENTRIES - } + private val seenEventIds: LinkedHashMap = LinkedHashMap() private val seenLock = Mutex() @@ -146,6 +143,15 @@ class Nip98AuthVerifier( if (seenEventIds.put(event.id, expiry) != null) { return Result.Malformed("replay: this NIP-98 token has already been used") } + // Cap entries: drop oldest by insertion order. Equivalent to + // the JDK LinkedHashMap.removeEldestEntry hook we used before, + // but works in KMP commonMain. + while (seenEventIds.size > MAX_REPLAY_ENTRIES) { + val eldest = seenEventIds.keys.iterator() + if (!eldest.hasNext()) break + eldest.next() + eldest.remove() + } } return Result.Verified(event.pubKey)