Repository: supabase-community/supabase-kt
Pull request #811 (2024-12-09 12:18:17)

Bump jackson from 2.18.1 to 2.18.2
Bumps `jackson` from 2.18.1 to 2.18.2.
Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.1 to 2.18.2
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/FasterXML/jackson-core/commit/fc1bc9382c4405c789b7757dc2dae19c9c3b2317"><code>fc1bc93</code></a> [maven-release-plugin] prepare release jackson-core-2.18.2</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/44f793eccd2c3d0d3faf35ae4497ee2447e474d4"><code>44f793e</code></a> Prep for 2.18.2</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/715ec43a1c42ffdc3fc2c23c56436ab1950e33e7"><code>715ec43</code></a> Disable scorecard WF from 2.18</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/88b4c947c47cb22a78b65ad2326f4a67c52467ca"><code>88b4c94</code></a> fix issue with incorrect matches for some surrogate chars (<a href="https://redirect.github.com/FasterXML/jackson-core/issues/1360">#1360</a>)</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/cc70bc26dc74bab4c283efc9bfe62408e4a689f4"><code>cc70bc2</code></a> Minor warnings removal</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/f5ed9e739c16df2362c4203b92c95c452edab568"><code>f5ed9e7</code></a> Warnings removal</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/93335d46ffde83becb203da92d40025cc43a30ce"><code>93335d4</code></a> Merge branch '2.17' into 2.18</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/3ec9738f53eda8ddf69f68da75100a3dd957a42d"><code>3ec9738</code></a> Back to snapshot dep</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/88fc49b78a56611b0cdea18b75a2f12856ef7a47"><code>88fc49b</code></a> [maven-release-plugin] prepare for next development iteration</li>
<li><a href="https://github.com/FasterXML/jackson-core/commit/44d1e782d56e46f4cf79c156b5286a8c4c7fbc7f"><code>44d1e78</code></a> [maven-release-plugin] prepare release jackson-core-2.17.3</li>
<li>Additional commits viewable in <a href="https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.1...jackson-core-2.18.2">compare view</a></li>
</ul>
</details>
<br />

Updates `com.fasterxml.jackson.module:jackson-module-kotlin` from 2.18.1 to 2.18.2
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/faea8f27e8609eac10638eef507b8b80c3bbef79"><code>faea8f2</code></a> [maven-release-plugin] prepare release jackson-module-kotlin-2.18.2</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/56014cecabad9915f16c6975b8e9064512fd51f4"><code>56014ce</code></a> Prep for 2.18.2</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/779cf850820e4b1f54d5a55b157ac4a054ca0a60"><code>779cf85</code></a> Merge pull request <a href="https://redirect.github.com/FasterXML/jackson-module-kotlin/issues/855">#855</a> from k163377/844</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/edf95928e3dc8293b4a78106d8720cfacdb6b12d"><code>edf9592</code></a> Add check for <a href="https://redirect.github.com/FasterXML/jackson-module-kotlin/issues/844">#844</a></li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/aa1cb9b0420245ed0899b3fa9bfddac27b7bee1f"><code>aa1cb9b</code></a> Merge pull request <a href="https://redirect.github.com/FasterXML/jackson-module-kotlin/issues/852">#852</a> from k163377/update-ci</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/ebd6e3dd1b6837e0325751cdb43d8ece5e2e7111"><code>ebd6e3d</code></a> Add Kotlin 2.1</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/9f6ea173d70f3799f2ed9c6f40df95329db2a532"><code>9f6ea17</code></a> Update Java and Kotlin versions</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/d4ae171e1232de3c861c745d678efc5eda4d577a"><code>d4ae171</code></a> Merge pull request <a href="https://redirect.github.com/FasterXML/jackson-module-kotlin/issues/850">#850</a> from k163377/GitHub841</li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/1c805d6ac8d432824dc2c3c754e3055b6cfe8dd6"><code>1c805d6</code></a> Add check for <a href="https://redirect.github.com/FasterXML/jackson-module-kotlin/issues/841">#841</a></li>
<li><a href="https://github.com/FasterXML/jackson-module-kotlin/commit/9889989dee744cd26d3e9a225eba49a0a3dbfe8e"><code>9889989</code></a> Merge branch '2.17' into 2.18</li>
<li>Additional commits viewable in <a href="https://github.com/FasterXML/jackson-module-kotlin/compare/jackson-module-kotlin-2.18.1...jackson-module-kotlin-2.18.2">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

jan-tennert:  @dependabot rebase

Patch:
diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt
index 6ea5aa95..774d6bf6 100644
--- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt
+++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt
@@ -4,7 +4,9 @@ import io.github.jan.supabase.SupabaseClient
 import io.github.jan.supabase.SupabaseClientBuilder
 import io.github.jan.supabase.SupabaseSerializer
 import io.github.jan.supabase.annotations.SupabaseInternal
+import io.github.jan.supabase.auth.resolveAccessToken
 import io.github.jan.supabase.logging.SupabaseLogger
+import io.github.jan.supabase.logging.w
 import io.github.jan.supabase.plugins.CustomSerializationConfig
 import io.github.jan.supabase.plugins.CustomSerializationPlugin
 import io.github.jan.supabase.plugins.MainConfig
@@ -92,6 +94,15 @@ sealed interface Realtime : MainPlugin<Realtime.Config>, CustomSerializationPlug
     @SupabaseInternal
     suspend fun send(message: RealtimeMessage)
 
+    /**
+     * Sets the JWT access token used for channel subscription authorization and Realtime RLS.
+     *
+     * If [token] is null, the token will be resolved using the [Realtime.Config.accessToken] provider.
+     *
+     * @param token The JWT access token
+     */
+    suspend fun setAuth(token: String? = null)
+
     /**
      * @property websocketConfig Custom configuration for the Ktor Websocket Client. This only applies if [Realtime.Config.websocketFactory] is null.
      * @property secure Whether to use wss or ws. Defaults to [SupabaseClient.useHTTPS] when null
@@ -114,6 +125,14 @@ sealed interface Realtime : MainPlugin<Realtime.Config>, CustomSerializationPlug
         var disconnectOnNoSubscriptions: Boolean = true,
     ): MainConfig(), CustomSerializationConfig {
 
+        /**
+         * A custom access token provider. If this is set, the [SupabaseClient] will not be used to resolve the access token.
+         */
+        var accessToken: suspend SupabaseClient.() -> String? = { resolveAccessToken(realtime, keyAsFallback = false) }
+            set(value) {
+                logger.w { "You are setting a custom access token provider. This can lead to unexpected behavior." }
+                field = value
+            }
         override var serializer: SupabaseSerializer? = null
 
     }
diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannel.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannel.kt
index 2c461805..60365b51 100644
--- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannel.kt
+++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannel.kt
@@ -46,9 +46,9 @@ sealed interface RealtimeChannel {
     suspend fun subscribe(blockUntilSubscribed: Boolean = false)
 
     /**
-     * Updates the JWT token for this client
+     * Updates the JWT token for this channel
      */
-    suspend fun updateAuth(jwt: String)
+    suspend fun updateAuth(jwt: String?)
 
     /**
      * Unsubscribes from the channel
diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt
index 27bdca12..71197593 100644
--- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt
+++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt
@@ -1,7 +1,6 @@
 package io.github.jan.supabase.realtime
 
 import io.github.jan.supabase.annotations.SupabaseInternal
-import io.github.jan.supabase.auth.resolveAccessToken
 import io.github.jan.supabase.collections.AtomicMutableList
 import io.github.jan.supabase.logging.d
 import io.github.jan.supabase.logging.e
@@ -41,13 +40,14 @@ internal class RealtimeChannelImpl(
     private val _status = MutableStateFlow(RealtimeChannel.Status.UNSUBSCRIBED)
     override val status = _status.asStateFlow()
     override val realtime: Realtime = realtimeImpl
-
     override val supabaseClient = realtimeImpl.supabaseClient
 
     private val broadcastUrl = realtimeImpl.broadcastUrl()
     private val subTopic = topic.replaceFirst(Regex("^realtime:", RegexOption.IGNORE_CASE), "")
     private val httpClient = realtimeImpl.supabaseClient.httpClient
 
+    private suspend fun accessToken() = realtimeImpl.config.accessToken(supabaseClient) ?: realtimeImpl.accessToken
+
     @OptIn(SupabaseInternal::class)
     override suspend fun subscribe(blockUntilSubscribed: Boolean) {
         if(realtimeImpl.status.value != Realtime.Status.CONNECTED) {
@@ -59,7 +59,7 @@ internal class RealtimeChannelImpl(
         }
         _status.value = RealtimeChannel.Status.SUBSCRIBING
         Realtime.logger.d { "Subscribing to channel $topic" }
-        val currentJwt = supabaseClient.resolveAccessToken(realtimeImpl, keyAsFallback = false)
+        val currentJwt = accessToken()
         val postgrestChanges = clientChanges.toList()
         val joinConfig = RealtimeJoinPayload(RealtimeJoinConfig(broadcastJoinConfig, presenceJoinConfig, postgrestChanges, isPrivate))
         val joinConfigObject = buildJsonObject {
@@ -93,7 +93,7 @@ internal class RealtimeChannelImpl(
         realtimeImpl.send(RealtimeMessage(topic, RealtimeChannel.CHANNEL_EVENT_LEAVE, buildJsonObject {}, null))
     }
 
-    override suspend fun updateAuth(jwt: String) {
+    override suspend fun updateAuth(jwt: String?) {
         Realtime.logger.d { "Updating auth token for channel $topic" }
         realtimeImpl.send(RealtimeMessage(topic, RealtimeChannel.CHANNEL_EVENT_ACCESS_TOKEN, buildJsonObject {
             put("access_token", jwt)
@@ -102,12 +102,16 @@ internal class RealtimeChannelImpl(
 
     override suspend fun broadcast(event: String, message: JsonObject) {
         if(status.value != RealtimeChannel.Status.SUBSCRIBED) {
+            val token = accessToken()
             val response = httpClient.postJson(
                 url = broadcastUrl,
                 body = BroadcastApiBody(listOf(BroadcastApiMessage(subTopic, event, message, isPrivate)))
             ) {
                 headers {
                     append("apikey", realtimeImpl.supabaseClient.supabaseKey)
+                    token?.let {
+                        set("Authorization", "Bearer $it")
+                    }
                 }
             }
             @Suppress("MagicNumber")
diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt
index b05f4256..28994f4b 100644
--- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt
+++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeImpl.kt
@@ -17,6 +17,7 @@ import io.github.jan.supabase.realtime.websocket.RealtimeWebsocket
 import io.ktor.client.statement.HttpResponse
 import io.ktor.http.URLProtocol
 import io.ktor.http.path
+import io.ktor.util.decodeBase64String
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -30,7 +31,13 @@ import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import kotlinx.datetime.Clock
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
 import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+import kotlin.io.encoding.ExperimentalEncodingApi
 
 @PublishedApi internal class RealtimeImpl(override val supabaseClient: SupabaseClient, override val config: Realtime.Config) : Realtime {
 
@@ -43,6 +50,7 @@ import kotlinx.serialization.json.buildJsonObject
     override val subscriptions: Map<String, RealtimeChannel> = _subscriptions
     private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
     private val mutex = Mutex()
+    internal var accessToken by atomic<String?>(null)
     var heartbeatJob: Job? = null
     var messageJob: Job? = null
     var ref by atomic(0)
@@ -92,7 +100,7 @@ import kotlinx.serialization.json.buildJsonObject
             supabaseClient.pluginManager.getPluginOrNull(Auth)?.sessionStatus?.collect {
                 if(status.value == Realtime.Status.CONNECTED) {
                     when(it) {
-                        is SessionStatus.Authenticated -> updateJwt(it.session.accessToken)
+                        is SessionStatus.Authenticated -> setAuth(it.session.accessToken)
                         is SessionStatus.NotAuthenticated -> {
                             if(config.disconnectOnSessionLoss) {
                                 Realtime.logger.w { "No auth session found, disconnecting from realtime websocket"}
@@ -166,9 +174,23 @@ import kotlinx.serialization.json.buildJsonObject
         }
     }
 
-    private fun updateJwt(jwt: String) {
+    @OptIn(ExperimentalEncodingApi::class)
+    override suspend fun setAuth(token: String?) {
+        val newToken = token ?: config.accessToken(supabaseClient)
+
+        if(newToken != null) {
+            val parsed = Json.decodeFromString<JsonObject>(newToken.split(".")[1].decodeBase64String())
+            val exp = parsed["exp"]?.jsonPrimitive?.longOrNull ?: error("No exp found in token")
+            val now = Clock.System.now().epochSeconds
+            val diff = exp - now
+            if(diff < 0) {
+                Realtime.logger.w { "Token is expired. Not sending it to realtime." }
+                return
+            }
+        }
+        this.accessToken = newToken
         scope.launch {
-            subscriptions.values.filter { it.status.value == RealtimeChannel.Status.SUBSCRIBED }.forEach { it.updateAuth(jwt) }
+            subscriptions.values.filter { it.status.value == RealtimeChannel.Status.SUBSCRIBED }.forEach { it.updateAuth(accessToken) }
         }
     }
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d93711d9..5da0843a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -18,7 +18,7 @@ apollo-kotlin = "4.1.0"
 korlibs = "6.0.0"
 detekt = "1.23.7"
 moshi = "1.15.2"
-jackson = "2.18.1"
+jackson = "2.18.2"
 browser = "1.8.0"
 googleid = "1.1.1"
 compose = "1.7.1"


Test patch:
diff --git a/Realtime/src/commonTest/kotlin/RealtimeTest.kt b/Realtime/src/commonTest/kotlin/RealtimeTest.kt
index 655c885e..5ff705e6 100644
--- a/Realtime/src/commonTest/kotlin/RealtimeTest.kt
+++ b/Realtime/src/commonTest/kotlin/RealtimeTest.kt
@@ -1,11 +1,15 @@
 import io.github.jan.supabase.realtime.Realtime
+import io.github.jan.supabase.realtime.RealtimeImpl
 import io.github.jan.supabase.realtime.RealtimeMessage
 import io.github.jan.supabase.realtime.realtime
+import io.ktor.util.encodeBase64
 import kotlinx.coroutines.test.runTest
+import kotlinx.datetime.Clock
 import kotlinx.serialization.json.buildJsonObject
 import kotlinx.serialization.json.put
 import kotlin.test.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertNull
 
 class RealtimeTest {
 
@@ -51,4 +55,46 @@ class RealtimeTest {
         }
     }
 
+    @Test
+    fun testSendingExpiredToken() {
+        runTest {
+            createTestClient(
+                wsHandler = { i, _ ->
+
+                },
+                supabaseHandler = {
+                    it.realtime as RealtimeImpl
+                    val expiredToken = generateToken(Clock.System.now().epochSeconds - 10)
+                    it.realtime.setAuth(expiredToken)
+                    assertNull((it.realtime as RealtimeImpl).accessToken)
+                }
+            )
+        }
+    }
+
+    @Test
+    fun testSendingValidToken() {
+        runTest {
+            createTestClient(
+                wsHandler = { i, _ ->
+
+                },
+                supabaseHandler = {
+                    it.realtime as RealtimeImpl
+                    val token = generateToken(Clock.System.now().epochSeconds + 10)
+                    it.realtime.setAuth(token)
+                    assertEquals(token, (it.realtime as RealtimeImpl).accessToken)
+                }
+            )
+        }
+    }
+
+    private fun generateToken(exp: Long) = buildString {
+        append("test.")
+        append(buildJsonObject {
+            put("exp", exp)
+        }.toString().encodeBase64())
+        append(".test")
+    }
+
 }
\ No newline at end of file