Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build-logic/convention/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,12 @@ gradlePlugin {
description = "Configures Room for the project"
}

// Supabase Config Plugin
register("supabaseConfig") {
id = "org.convention.kmp.supabase.config"
implementationClass = "SupabaseConfigConventionPlugin"
description = "Generates Supabase credentials from secrets file"
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.convention.libs

/**
* Convention plugin that generates Supabase credentials from a JSON secrets file.
*
* This plugin:
* 1. Reads credentials from `secrets/supabaseCredentialsFile.json`
* 2. Generates a `SupabaseCredentials` object implementing `template.core.base.network.SupabaseCredentials`
* 3. Adds the generated source to the commonMain source set
* 4. Adds the supabase-postgrest dependency
*
* ## Usage
*
* Apply the plugin to your module's build.gradle.kts:
* ```kotlin
* plugins {
* alias(libs.plugins.kmp.supabase.config)
* }
*
* // Optional: Configure the package name (defaults to module namespace + ".config")
* supabaseConfig {
* packageName = "com.example.myapp.network.config"
* }
* ```
*
* ## Secrets File Format
*
* Create `secrets/supabaseCredentialsFile.json` in your project root:
* ```json
* {
* "url": "https://your-project.supabase.co",
* "anonKey": "your-anon-key"
* }
* ```
*
* ## Generated Code
*
* The plugin generates:
* ```kotlin
* object SupabaseCredentials : template.core.base.network.SupabaseCredentials {
* override val url = "https://your-project.supabase.co"
* override val anonKey = "your-anon-key"
* }
* ```
*/
class SupabaseConfigConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// Create extension for configuration
val extension = extensions.create("supabaseConfig", SupabaseConfigExtension::class.java)

val generatedDir = layout.buildDirectory.dir("generated/supabase")
val secretsFile = rootProject.file("secrets/supabaseCredentialsFile.json")

// Register the code generation task
val generateTask = tasks.register("generateSupabaseConfig") {
// Determine package name from extension or derive from namespace
val packageName = extension.packageName
?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
?: "${project.group}.config"

val packagePath = packageName.replace(".", "/")
val outputFile = generatedDir.get().asFile
.resolve("$packagePath/SupabaseCredentials.kt")

// Track file content as input property for proper up-to-date checking
val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) {
secretsFile.readText()
} else {
""
}
inputs.property("credentialsContent", fileContent)
inputs.property("packageName", packageName)
outputs.file(outputFile)
Comment on lines +71 to +89
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Extension properties are evaluated eagerly at configuration time — packageName set after plugin application will be ignored.

Lines 73-75 read extension.packageName during task registration (configuration phase). If the user configures the extension after applying the plugin (which is the standard Gradle pattern), packageName will still be null.

Similarly, the outputFile path (line 78-79) and secretsFile content (lines 82-86) are computed eagerly. This violates Gradle's configuration avoidance principle and breaks up-to-date checking when inputs change.

The idiomatic fix is to use Property<String> in the extension and providers in the task:

Suggested approach — use lazy evaluation
 open class SupabaseConfigExtension {
-    var packageName: String? = null
+    val packageName: Property<String> = objects.property(String::class.java)
 }

In the task registration, defer evaluation to doLast or use task inputs with providers:

 val generateTask = tasks.register("generateSupabaseConfig") {
-    val packageName = extension.packageName
-        ?: project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
-        ?: "${project.group}.config"
-    val packagePath = packageName.replace(".", "/")
-    val outputFile = generatedDir.get().asFile
-        .resolve("$packagePath/SupabaseCredentials.kt")
-    val fileContent = if (secretsFile.exists() && secretsFile.length() > 0) {
-        secretsFile.readText()
-    } else {
-        ""
-    }
-    inputs.property("credentialsContent", fileContent)
-    inputs.property("packageName", packageName)
-    outputs.file(outputFile)
+    val resolvedPackageName = extension.packageName.orElse(
+        provider {
+            project.findProperty("android.namespace")?.toString()?.let { "$it.config" }
+                ?: "${project.group}.config"
+        }
+    )
+    inputs.property("packageName", resolvedPackageName)
+    inputs.file(secretsFile).optional()
+    outputs.dir(generatedDir)
     doLast {
-        val currentContent = inputs.properties["credentialsContent"] as String
-        val currentPackage = inputs.properties["packageName"] as String
+        val currentPackage = resolvedPackageName.get()
+        val currentContent = if (secretsFile.exists()) secretsFile.readText() else ""
         // ... rest of doLast
     }
 }
🤖 Prompt for AI Agents
In `@build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt`
around lines 71 - 89, The task eagerly reads extension.packageName,
generatedDir, and secretsFile during registration (generateTask), which breaks
lazy configuration; instead make the extension expose a Property<String> for
packageName and wire task inputs/outputs to Providers so values are resolved at
execution time — replace direct reads of extension.packageName,
generatedDir.get(), and secretsFile.readText() with provider-backed values
(e.g., use extension.packageName.orElse(...).map { ... } and
project.providers.fileProperty()/map to compute packagePath/outputFile and use
inputs.file(secretsFile) or inputs.property("credentialsContent",
secretsFile.asFile.map { it.readText() }), or defer the reads into doLast) so
inputs.property and outputs.file receive provider-backed values and up-to-date
checks work correctly.


doLast {
val currentContent = inputs.properties["credentialsContent"] as String
val currentPackage = inputs.properties["packageName"] as String
val (url, anonKey) = if (currentContent.isNotEmpty()) {
try {
parseCredentials(currentContent)
} catch (e: Exception) {
logger.warn("Failed to parse Supabase credentials file: ${e.message}")
Pair("", "")
}
} else {
logger.warn("Supabase credentials file not found at: ${secretsFile.absolutePath}")
logger.warn("Create secrets/supabaseCredentialsFile.json with 'url' and 'anonKey' fields")
Pair("", "")
}

outputFile.parentFile.mkdirs()

outputFile.writeText(
"""
|/*
| * Copyright 2025 Mifos Initiative
| *
| * This Source Code Form is subject to the terms of the Mozilla Public
| * License, v. 2.0. If a copy of the MPL was not distributed with this
| * file, You can obtain one at https://mozilla.org/MPL/2.0/.
| *
| * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
| */
|package $currentPackage
|
|import template.core.base.network.SupabaseCredentials as BaseSupabaseCredentials
|
|/**
| * Generated Supabase credentials from secrets/supabaseCredentialsFile.json
| * DO NOT EDIT - This file is generated by SupabaseConfigConventionPlugin
| */
|object SupabaseCredentials : BaseSupabaseCredentials {
| override val url: String = "$url"
| override val anonKey: String = "$anonKey"
|}
""".trimMargin()
)
Comment on lines +109 to +133
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Parsed url and anonKey are interpolated into generated Kotlin source without escaping — risk of broken output or code injection.

If the JSON values contain characters like ", \, or $, the generated Kotlin file will have syntax errors or unintended behavior. While Supabase URLs and keys are typically safe, this is a correctness gap for a reusable plugin.

Proposed fix: escape special characters before interpolation
+    private fun escapeKotlinString(value: String): String =
+        value.replace("\\", "\\\\")
+            .replace("\"", "\\\"")
+            .replace("\$", "\\\$")
+
     // In the doLast block:
-    |    override val url: String = "$url"
-    |    override val anonKey: String = "$anonKey"
+    |    override val url: String = "${escapeKotlinString(url)}"
+    |    override val anonKey: String = "${escapeKotlinString(anonKey)}"
🤖 Prompt for AI Agents
In `@build-logic/convention/src/main/kotlin/SupabaseConfigConventionPlugin.kt`
around lines 109 - 133, The generated Kotlin interpolates raw url and anonKey
into double-quoted string literals (in the outputFile.writeText block creating
object SupabaseCredentials), which can break the produced source if values
contain ", \, $, or control chars; add and call a helper like
escapeKotlinString(value: String): String that escapes backslashes, double
quotes, dollar signs and control/newline characters (or encodes them) and use
escapeKotlinString(url) and escapeKotlinString(anonKey) in the template
interpolation so the resulting Kotlin source is always syntactically valid.

}
}

// Configure the Kotlin Multiplatform extension
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.named("commonMain") {
kotlin.srcDir(generatedDir)
dependencies {
implementation(libs.findLibrary("supabase-postgrest").get())
}
}
}

// Make compile and KSP tasks depend on the generate task
tasks.matching {
it.name.startsWith("compileKotlin") || it.name.startsWith("ksp")
}.configureEach {
dependsOn(generateTask)
}
}
}

/**
* Simple JSON parser for extracting url and anonKey fields.
* Avoids dependency on kotlinx-serialization in build-logic.
*/
private fun parseCredentials(jsonContent: String): Pair<String, String> {
val urlPattern = """"url"\s*:\s*"([^"]*)"""".toRegex()
val anonKeyPattern = """"anonKey"\s*:\s*"([^"]*)"""".toRegex()

val url = urlPattern.find(jsonContent)?.groupValues?.getOrNull(1) ?: ""
val anonKey = anonKeyPattern.find(jsonContent)?.groupValues?.getOrNull(1) ?: ""

return Pair(url, anonKey)
}
}

/**
* Extension for configuring SupabaseConfigConventionPlugin.
*/
open class SupabaseConfigExtension {
/**
* The package name for the generated SupabaseCredentials object.
* If not set, defaults to the module's android namespace + ".config"
*/
var packageName: String? = null
}
29 changes: 29 additions & 0 deletions cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,35 @@
| | | +--- io.ktor:ktor-client-auth:3.3.3 (*)
| | | +--- de.jensklingenberg.ktorfit:ktorfit-lib:2.7.1 (*)
| | | +--- co.touchlab:kermit:2.0.8 (*)
| | | +--- io.github.jan-tennert.supabase:postgrest-kt:3.1.1
| | | | \--- io.github.jan-tennert.supabase:postgrest-kt-android:3.1.1
| | | | +--- io.github.jan-tennert.supabase:auth-kt:3.1.1
| | | | | \--- io.github.jan-tennert.supabase:auth-kt-android:3.1.1
| | | | | +--- androidx.startup:startup-runtime:1.2.0 (*)
| | | | | +--- androidx.browser:browser:1.8.0 -> 1.9.0 (*)
| | | | | +--- io.github.jan-tennert.supabase:supabase-kt:3.1.1
| | | | | | \--- io.github.jan-tennert.supabase:supabase-kt-android:3.1.1
| | | | | | +--- androidx.lifecycle:lifecycle-process:2.8.7 -> 2.9.4 (*)
| | | | | | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.1 -> 0.7.1 (*)
| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 -> 1.10.2 (*)
| | | | | | +--- co.touchlab:kermit:2.0.5 -> 2.0.8 (*)
| | | | | | +--- io.ktor:ktor-client-core:3.0.3 -> 3.3.3 (*)
| | | | | | +--- io.ktor:ktor-client-content-negotiation:3.0.3 -> 3.3.3 (*)
| | | | | | +--- io.ktor:ktor-serialization-kotlinx-json:3.0.3 -> 3.3.3 (*)
| | | | | | +--- org.jetbrains.kotlinx:atomicfu:0.27.0
| | | | | | | \--- org.jetbrains.kotlinx:atomicfu-jvm:0.27.0
| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:{prefer 2.1.0} -> 2.2.21 (*)
| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | | +--- com.squareup.okio:okio:3.10.2 -> 3.16.4 (*)
| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | | +--- com.russhwolf:multiplatform-settings-no-arg:1.3.0 (*)
| | | | | +--- com.russhwolf:multiplatform-settings-coroutines:1.3.0 (*)
| | | | | \--- org.kotlincrypto:secure-random:0.3.2
| | | | | \--- org.kotlincrypto:secure-random-jvm:0.3.2
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.2.21 (*)
| | | | +--- org.jetbrains.kotlin:kotlin-reflect:2.1.10
| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.10 -> 2.2.21 (*)
Comment on lines +2077 to +2105
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and understand the repo structure
find . -name "build.gradle.kts" -o -name "build.gradle" -o -name "libs.versions.toml" | head -20

Repository: openMF/kmp-project-template

Length of output: 744


🏁 Script executed:

# Look for the cmp-android module structure
fd -t d "cmp-android" | head -5

Repository: openMF/kmp-project-template

Length of output: 83


🏁 Script executed:

# Check the dependency tree file itself
wc -l cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt 2>/dev/null || echo "File path may vary"

Repository: openMF/kmp-project-template

Length of output: 137


🏁 Script executed:

# Search for where these dependencies are declared
rg "postgrest-kt|supabase-kt" --type gradle --type toml -A 2 -B 2

Repository: openMF/kmp-project-template

Length of output: 504


🏁 Script executed:

# Read the versions file to see actual version values
cat gradle/libs.versions.toml | head -100

Repository: openMF/kmp-project-template

Length of output: 2205


🏁 Script executed:

# Find where Ktor, Kotlin, Coroutines versions are defined
rg "ktor|kotlin|coroutines|supabase" gradle/libs.versions.toml -i

Repository: openMF/kmp-project-template

Length of output: 5989


🏁 Script executed:

# Check the cmp-android build file
cat cmp-android/build.gradle.kts

Repository: openMF/kmp-project-template

Length of output: 5137


🌐 Web query:

postgrest-kt 3.1.1 ktor 3.3.3 compatibility

💡 Result:

postgrest-kt:3.1.1 is not a clean match for io.ktor:ktor-* :3.3.3.

  • postgrest-kt-jvm:3.1.1 is built against Kotlin stdlib 2.1.10 (and auth-kt-jvm:3.1.1). [1]
  • auth-kt:3.1.1 pulls Ktor server artifacts at 3.0.3 (ktor-server-core, ktor-server-cio). [2]
  • Ktor 3.3.3 updates Kotlin to 2.2.20. [3]

What this means

  • If your app uses ktor-* :3.3.3, Gradle will likely resolve Ktor to 3.3.3 while postgrest-kt:3.1.1 expects 3.0.3-era Ktor (and older Kotlin). That combination can work sometimes, but it’s not guaranteed (possible binary/source incompatibilities and dependency convergence issues). [2][3]

Recommended options

  1. Upgrade postgrest-kt to a newer version that aligns better with newer Ktor/Kotlin (preferred). (3.1.1 is from Feb 2025; newer lines exist.) [4]
  2. If you must stay on postgrest-kt:3.1.1, use Gradle dependency alignment (e.g., enforce io.ktor:ktor-bom:3.3.3) and run tests—treat it as an unsupported override.

Sources: [1] [2] [3] [4]


🌐 Web query:

supabase postgrest-kt 3.1.1 security vulnerabilities CVE

💡 Result:

No public CVE (or GitHub Security Advisory / GHSA) specifically targeting io.github.jan-tennert.supabase:postgrest-kt version 3.1.1 turned up in major public sources I checked (NVD + common package vulnerability aggregators). MavenRepository’s entry for postgrest-kt-jvm also shows no known/indexed vulnerabilities for 3.1.1 (released 2025‑02‑05). [1]

A CVE that sometimes gets cited in “Supabase + PostgREST” searches—CVE‑2024‑24213—is disputed and concerns a SQL-query endpoint in the Supabase dashboard/pg_meta context, not the Kotlin client library postgrest-kt. [2]

References

  • [1] MavenRepository – io.github.jan-tennert.supabase:postgrest-kt-jvm (shows 3.1.1 release date and no vulnerabilities listed). (mvnrepository.com)
  • [2] NVD – CVE‑2024‑24213 (disputed; scoped to Supabase dashboard/pg_meta query endpoint). (nvd.nist.gov)

Citations:


🌐 Web query:

Jan-Tennert supabase 3.1.1 ktor client compatibility

💡 Result:

For Jan Tennert’s supabase-kt version 3.1.1, the published artifacts are built against Ktor 3.0.3 (for example, supabase-kt-linuxx64:3.1.1 depends on io.ktor:ktor-client-core-* / content-negotiation-* / serialization-kotlinx-json-* 3.0.3) [1].

So, for best Ktor client compatibility, use Ktor 3.0.3 for your client engine dependencies (e.g., ktor-client-okhttp, ktor-client-cio, ktor-client-darwin, etc.). The project also explicitly recommends using the same Ktor version as supabase-kt to avoid dependency/version mismatches [2].

Sources: [1] [2]


Resolve Ktor version compatibility with PostgREST dependency.

The dependency tree shows Ktor 3.3.3, but postgrest-kt:3.1.1 (and transitive supabase-kt:3.1.1) are built for Ktor 3.0.3. While Gradle will converge to 3.3.3, this mismatch is unsupported and risks compatibility issues at runtime. Either upgrade postgrest-kt to a version compatible with Ktor 3.3.3 (or constrain Ktor back to 3.0.3 if that's a requirement). Verify the resolution with integration tests.

No known CVEs exist for these library versions.

🤖 Prompt for AI Agents
In `@cmp-android/dependencies/prodReleaseRuntimeClasspath.tree.txt` around lines
2077 - 2105, The transitive supabase/postgrest dependency tree shows
ktor-client-core/content-negotiation/serialization at 3.3.3 while
postgrest-kt:3.1.1 and supabase-kt:3.1.1 target Ktor 3.0.3, so either upgrade
postgrest-kt/supabase-kt to a release built for Ktor 3.3.3 or force Ktor back to
3.0.3 with a Gradle dependency constraint/resolution so versions converge;
locate references to postgrest-kt, supabase-kt and the Ktor modules
(ktor-client-core, ktor-client-content-negotiation,
ktor-serialization-kotlinx-json) in your build.gradle(.kts) and update the
dependency version or add the constraint, then run the integration test suite to
verify runtime compatibility.

| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 (*)
| | | +--- io.insert-koin:koin-bom:4.1.1 (*)
| | | +--- io.insert-koin:koin-core:4.1.1 (*)
Expand Down
11 changes: 11 additions & 0 deletions cmp-android/dependencies/prodReleaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ io.coil-kt.coil3:coil-compose:3.3.0
io.coil-kt.coil3:coil-core-android:3.3.0
io.coil-kt.coil3:coil-core:3.3.0
io.coil-kt.coil3:coil:3.3.0
io.github.jan-tennert.supabase:auth-kt-android:3.1.1
io.github.jan-tennert.supabase:auth-kt:3.1.1
io.github.jan-tennert.supabase:postgrest-kt-android:3.1.1
io.github.jan-tennert.supabase:postgrest-kt:3.1.1
io.github.jan-tennert.supabase:supabase-kt-android:3.1.1
io.github.jan-tennert.supabase:supabase-kt:3.1.1
io.github.vinceglb:filekit-coil-android:0.12.0
io.github.vinceglb:filekit-coil:0.12.0
io.github.vinceglb:filekit-core-android:0.12.0
Expand Down Expand Up @@ -389,9 +395,12 @@ org.jetbrains.compose.ui:ui-unit:1.9.3
org.jetbrains.compose.ui:ui-util:1.9.3
org.jetbrains.compose.ui:ui:1.9.3
org.jetbrains.kotlin:kotlin-parcelize-runtime:2.2.21
org.jetbrains.kotlin:kotlin-reflect:2.1.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22
org.jetbrains.kotlin:kotlin-stdlib:2.2.21
org.jetbrains.kotlinx:atomicfu-jvm:0.27.0
org.jetbrains.kotlinx:atomicfu:0.27.0
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.4.0
org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2
Expand All @@ -415,4 +424,6 @@ org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0
org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0
org.kotlincrypto:secure-random-jvm:0.3.2
org.kotlincrypto:secure-random:0.3.2
org.slf4j:slf4j-api:2.0.17
1 change: 1 addition & 0 deletions core-base/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ kotlin {
api(libs.ktor.client.auth)
api(libs.ktorfit.lib)
api(libs.kermit.logging)
api(libs.supabase.postgrest)
}

androidMain.dependencies {
Expand Down
Loading
Loading