From c1cc713bbb2bda2389060c289a1e1a09e55bab55 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Tue, 28 Apr 2026 10:52:40 +0200 Subject: [PATCH] feat(02-01): wire Phase 2 dependency aliases without bumping Ktor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 02-01-02. Adds Phase 2 deps to the version catalog and routes them into composeApp + server build files. Ktor stays pinned at 3.4.1 per the resolved Open Question — patch bump deferred unless a concrete incompatibility appears. Catalog (gradle/libs.versions.toml): - Versions: appauth, appauth-ios, androidx-security-crypto, exposed, hikari, multiplatformSettings, testcontainers, plus the kotlinCocoapods plugin alias. - Libraries: ktor server auth/auth-jwt/call-logging/status-pages, ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio, ktor-serializationKotlinxJsonMpp (the multiplatform variant; the -jvm one stays for server), AppAuth, AndroidX Security Crypto, multiplatform-settings + coroutines, Exposed core/jdbc/java-time, HikariCP, Testcontainers postgresql + junit-jupiter. composeApp/build.gradle.kts: - Apply kotlinSerialization (alias) and kotlin.native.cocoapods (by id — the plugin is shipped inside the Kotlin Gradle plugin already on the classpath via recipe.kotlin.multiplatform; alias-applying it would request a fresh version and fail). - Cocoapods block: ComposeApp baseName + isStatic, ../iosApp/Podfile, iOS deployment target 15.0, AppAuth pod pulled from libs.versions.appauth.ios.get() — no literal pin in the build file (verify-no-version-literals.sh stays green). - Common deps: Ktor client family, MPP serialization, multiplatform settings; Android: AppAuth-Android + Security Crypto + OkHttp engine; iOS: Darwin engine; JVM: CIO engine. server/build.gradle.kts: Adds Ktor server auth/JWT/CallLogging/ StatusPages, Exposed DSL trio, Hikari, kotlinx.serialization-json, plus testImplementation testcontainers postgresql + junit-jupiter. Deviations: - Rule 3 (blocking): manifestPlaceholders["appAuthRedirectScheme"] = "recipe" added to Android defaultConfig because AppAuth-Android's bundled manifest declares a ${appAuthRedirectScheme} placeholder that breaks AGP merge before Plan 02-04 lands the full . - Rule 3 (blocking): top-level group/version on composeApp (required by the cocoapods podspec generator) pushes the Compose Resources Res-class package off recipe.composeapp.generated.resources, breaking Phase 1 App.kt imports. Lock the package via compose.resources { packageOfResClass = "recipe.composeapp.generated.resources" }. - Rule 3 (housekeeping): *.podspec is generated by the cocoapods plugin on every build; ignored. Verification: - ./gradlew :composeApp:dependencies --configuration debugCompileClasspath :server:dependencies --configuration runtimeClasspath: PASS (the plan-stated androidMainCompileClasspath name doesn't exist under this AGP/Gradle combo; debugCompileClasspath is the functional equivalent and resolves all new deps). - ./gradlew :composeApp:compileDebugKotlinAndroid :server:compileKotlin: PASS - ./gradlew :composeApp:compileKotlinIosSimulatorArm64: PASS (cinterop pulls AppAuth pod cleanly). - ./tools/verify-no-version-literals.sh: PASS - ./tools/verify-shared-pure.sh: PASS - All Task 2 grep acceptance criteria satisfied. --- .gitignore | 3 ++ composeApp/build.gradle.kts | 75 +++++++++++++++++++++++++++++++++++-- gradle/libs.versions.toml | 42 +++++++++++++++++++++ server/build.gradle.kts | 19 ++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index adfa9bf..ce6213a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ captures !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings node_modules/ + +# Generated by Kotlin CocoaPods plugin (Phase 2 D-01); regenerated on every Gradle sync. +*.podspec diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index d5bc908..fd5b3ca 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -6,9 +6,21 @@ plugins { alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.composeHotReload) + alias(libs.plugins.kotlinSerialization) + // CocoaPods is shipped inside the Kotlin Gradle plugin already on the classpath via + // `recipe.kotlin.multiplatform`. Applying via `alias(libs.plugins.kotlinCocoapods)` + // would request a fresh version and fail with "already on the classpath", so we + // apply it by id only. The catalog still owns the shared Kotlin version. + id("org.jetbrains.kotlin.native.cocoapods") id("recipe.quality") } +// Top-level project version is required by the Kotlin CocoaPods plugin when no explicit +// `version` is set inside the `cocoapods { ... }` block. Mirrors `server/build.gradle.kts` +// — Gradle artifact metadata only, NOT a library/plugin pin (per `verify-no-version-literals.sh`). +group = "dev.ulfrx.recipe" +version = "1.0.0" + android { namespace = "dev.ulfrx.recipe" compileSdk = libs.versions.android.compileSdk.get().toInt() @@ -19,6 +31,14 @@ android { targetSdk = libs.versions.android.targetSdk.get().toInt() versionCode = 1 versionName = "1.0" + + // AppAuth-Android (D-01) bundles a manifest entry for its + // `RedirectUriReceiverActivity` that requires `${appAuthRedirectScheme}` to be + // resolved at merge time. Pin it to the Phase 2 redirect scheme so simply + // pulling AppAuth into the classpath (Plan 02-01) doesn't break AGP's manifest + // merger before Plan 02-04 lands the full `` registration. + // Must match `dev.ulfrx.recipe.shared.Constants.OIDC_REDIRECT_URI` byte-for-byte. + manifestPlaceholders["appAuthRedirectScheme"] = "recipe" } packaging { resources { @@ -37,12 +57,22 @@ android { } kotlin { - // Create the iOS framework Swift imports as `ComposeApp`. - listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> - iosTarget.binaries.framework { + // The Kotlin CocoaPods plugin (D-01) configures the iOS framework on the iOS targets + // declared by `recipe.kotlin.multiplatform`. `baseName = "ComposeApp"` / `isStatic = true` + // keep existing Swift `import ComposeApp` working. The AppAuth iOS pod version comes + // from the version catalog so this build file stays free of literal pins. + cocoapods { + summary = "Recipe Compose Multiplatform shared framework" + homepage = "https://github.com/ulfrxdev/recipe" + ios.deploymentTarget = "15.0" + podfile = project.file("../iosApp/Podfile") + framework { baseName = "ComposeApp" isStatic = true } + pod("AppAuth") { + version = libs.versions.appauth.ios.get() + } } sourceSets { @@ -61,15 +91,45 @@ kotlin { implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation(projects.shared) + + // Phase 2: Ktor client + serialization + secure settings (D-13, D-16, D-17). + // The MPP variant of `ktor-serialization-kotlinx-json` is required here; the + // server module keeps the `-jvm` variant via `libs.ktor.serializationKotlinxJson`. + implementation(libs.ktor.clientCore) + implementation(libs.ktor.clientAuth) + implementation(libs.ktor.clientContentNegotiation) + implementation(libs.ktor.clientLogging) + implementation(libs.ktor.serializationKotlinxJsonMpp) + implementation(libs.kotlinx.serializationJson) + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.coroutines) } androidMain.dependencies { implementation(libs.compose.uiToolingPreview) implementation(libs.androidx.activity.compose) implementation(libs.koin.android) + + // Phase 2 Android: AppAuth-Android + AndroidX Security Crypto for the + // SecureAuthStateStore actual (D-01, D-13). EncryptedSharedPreferences is + // accepted technical debt per Open Question #1; the Keystore-backed + // implementation can replace it without touching AuthSession. + implementation(libs.appauth) + implementation(libs.androidx.security.crypto) + implementation(libs.ktor.clientOkhttp) + } + iosMain.dependencies { + // Phase 2 iOS: Darwin engine for Ktor; AppAuth-iOS arrives via the + // CocoaPods block above so the shared framework links it directly. + implementation(libs.ktor.clientDarwin) } jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) + + // Phase 2 Desktop: CIO is the JVM Ktor engine for the dev-mode auth stub + // (D-02). The full stub lives in Plan 02-04; this just makes the engine + // available so `composeApp:run` still compiles in Phase 2. + implementation(libs.ktor.clientCio) } } } @@ -77,3 +137,12 @@ kotlin { dependencies { debugImplementation(libs.compose.uiTooling) } + +// Adding `group = "dev.ulfrx.recipe"` (required by the Kotlin CocoaPods plugin to render +// the podspec) shifts the Compose Resources `Res` class package from +// `recipe.composeapp.generated.resources` to `dev.ulfrx.recipe.composeapp.generated.resources`, +// breaking the Phase 1 `App.kt` import. Lock the historical package so this plan's wiring +// changes don't cascade into UI code; Plan 02-04+ replaces `App.kt`'s template body anyway. +compose.resources { + packageOfResClass = "recipe.composeapp.generated.resources" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b8435f0..3387a19 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,10 +8,15 @@ androidx-appcompat = "1.7.1" androidx-core = "1.18.0" androidx-espresso = "3.7.0" androidx-lifecycle = "2.10.0" +androidx-security-crypto = "1.1.0" androidx-testExt = "1.3.0" +appauth = "0.11.1" +appauth-ios = "2.0.0" composeHotReload = "1.0.0" composeMultiplatform = "1.10.3" +exposed = "0.55.0" flyway = "12.4.0" +hikari = "6.2.1" junit = "4.13.2" kermit = "2.1.0" koin = "4.2.1" @@ -21,8 +26,10 @@ kotlinx-serialization = "1.7.3" ktor = "3.4.1" logback = "1.5.32" material3 = "1.10.0-alpha05" +multiplatformSettings = "1.3.0" postgresql = "42.7.10" spotless = "8.4.0" +testcontainers = "1.21.4" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -68,6 +75,40 @@ flyway-core = { module = "org.flywaydb:flyway-core", version.ref = "flyway" } flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" } postgresql = { module = "org.postgresql:postgresql", version.ref = "postgresql" } +# Phase 2 — Server: Ktor auth + JWT + call logging + status pages (D-21..D-23) +ktor-serverAuth = { module = "io.ktor:ktor-server-auth-jvm", version.ref = "ktor" } +ktor-serverAuthJwt = { module = "io.ktor:ktor-server-auth-jwt-jvm", version.ref = "ktor" } +ktor-serverCallLogging = { module = "io.ktor:ktor-server-call-logging-jvm", version.ref = "ktor" } +ktor-serverStatusPages = { module = "io.ktor:ktor-server-status-pages-jvm", version.ref = "ktor" } + +# Phase 2 — Client: Ktor client core + auth + content-negotiation + logging + engines (D-16..D-18) +# `ktor-serializationKotlinxJsonMpp` is the multiplatform variant (no `-jvm` classifier) for +# commonMain consumption; the `-jvm` variant above stays available to the server module. +ktor-clientCore = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-clientAuth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" } +ktor-clientContentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-clientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +ktor-clientOkhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-clientDarwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } +ktor-clientCio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-serializationKotlinxJsonMpp = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } + +# Phase 2 — Client: AppAuth + Android secure storage + multiplatform-settings (D-01, D-13, AUTH-02) +appauth = { module = "net.openid:appauth", version.ref = "appauth" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidx-security-crypto" } +multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } +multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } + +# Phase 2 — Server: Exposed DSL + Hikari (D-26) +exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } +exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } +hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } + +# Phase 2 — Server tests: Testcontainers (D-21..D-25) +testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } +testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -78,5 +119,6 @@ kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktor = { id = "io.ktor.plugin", version.ref = "ktor" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } flywayPlugin = { id = "org.flywaydb.flyway", version.ref = "flyway" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index bd093f9..a941470 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -29,13 +29,32 @@ dependencies { implementation(libs.ktor.serverNetty) implementation(libs.ktor.serverContentNegotiation) implementation(libs.ktor.serializationKotlinxJson) + implementation(libs.kotlinx.serializationJson) implementation(libs.logback) implementation(libs.flyway.core) implementation(libs.flyway.database.postgresql) implementation(libs.postgresql) implementation(projects.shared) + + // Phase 2: Ktor auth + JWT validation + observability (D-21..D-23). + implementation(libs.ktor.serverAuth) + implementation(libs.ktor.serverAuthJwt) + implementation(libs.ktor.serverCallLogging) + implementation(libs.ktor.serverStatusPages) + + // Phase 2: Exposed DSL + Hikari connection pool (D-26). + implementation(libs.exposed.core) + implementation(libs.exposed.jdbc) + implementation(libs.exposed.java.time) + implementation(libs.hikari) + testImplementation(libs.ktor.serverTestHost) testImplementation(libs.kotlin.testJunit) + + // Phase 2: Testcontainers for JIT user provisioning + JWT auth integration tests + // (AUTH-03, AUTH-06). Wired here so Plan 02-02 only needs to write tests. + testImplementation(libs.testcontainers.postgresql) + testImplementation(libs.testcontainers.junit.jupiter) } flyway {