diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac29368..184b2b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: xcodebuild clean test -project TestApp.xcodeproj -scheme TestApp -destination "platform=iOS Simulator,OS=16.2,name=iPhone 12" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO region=${{ secrets.region }} clientId=${{ secrets.clientid }} - name: Upload test result if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: reports path: build/reports/ diff --git a/.gitignore b/.gitignore index e264418..f2f293e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,5 @@ DerivedData/ !*.xcodeproj/xcshareddata/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings -kotlin-js-store/ \ No newline at end of file +kotlin-js-store/ +/.kotlin/ diff --git a/build.gradle.kts b/build.gradle.kts index 667e775..2bb3544 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,15 @@ +@file:OptIn(ExperimentalWasmDsl::class) + import com.android.build.gradle.LibraryExtension import com.liftric.vault.GetVaultSecretTask +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest import org.jetbrains.kotlin.gradle.tasks.* plugins { - kotlin("multiplatform") version libs.versions.kotlin + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.serialization) - id("com.android.library") version libs.versions.android.tools.gradle - alias(libs.plugins.definitions) - alias(libs.plugins.npm.publishing) + alias(libs.plugins.android.library) alias(libs.plugins.versioning) alias(libs.plugins.vault.client) id("maven-publish") @@ -16,24 +17,26 @@ plugins { } repositories { - mavenCentral() google() + mavenCentral() gradlePluginPortal() } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlin { - ios { - binaries.framework() + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework() } - iosSimulatorArm64() - - androidTarget() { + androidTarget { publishAllLibraryVariants() } jvm() @@ -53,6 +56,15 @@ kotlin { } } } + wasmJs { + browser () + binaries.library() + compilations.all { + compileTaskProvider.configure { + compilerOptions.freeCompilerArgs.add("-Xir-minimized-member-names=false") + } + } + } sourceSets { val commonMain by getting { @@ -96,17 +108,8 @@ kotlin { implementation(libs.opt.java) } } - val iosMain by getting { - dependencies { - api(libs.ktor.client.darwin) - } - } - val iosTest by getting - val iosSimulatorArm64Main by getting { - dependsOn(iosMain) - } - val iosSimulatorArm64Test by getting { - dependsOn(iosTest) + iosMain.dependencies { + api(libs.ktor.client.darwin) } val jsMain by getting { dependencies { @@ -118,6 +121,12 @@ kotlin { implementation(kotlin("test-js")) } } + val wasmJsMain by getting { + dependencies { + api(libs.ktor.client.cio) + api(libs.kotlinx.browser) + } + } all { languageSettings { optIn("kotlinx.serialization.ExperimentalSerializationApi") @@ -130,7 +139,7 @@ kotlin { configure { defaultConfig.apply { - compileSdk = 30 + compileSdk = 35 minSdkVersion(21) targetSdkVersion(30) testInstrumentationRunner = "org.robolectric.RobolectricTestRunner" @@ -189,7 +198,7 @@ tasks { doFirst { val (clientId, region) = with(testSecrets.secret.get()) { ((System.getenv("clientId") ?: this["client_id_dev"].toString()) to - (System.getenv("region") ?: this["client_region_dev"].toString())) + (System.getenv("region") ?: this["client_region_dev"].toString())) } mkdir("$buildDir/gen") @@ -214,7 +223,7 @@ tasks { withType { kotlinOptions { jvmTarget = "11" - languageVersion = "1.5" + languageVersion = "2.1" freeCompilerArgs = listOf( "-Xinline-classes" ) @@ -314,41 +323,6 @@ publishing { } } -val npmAccessKey: String? by project - -npmPublish { - organization.set("liftric") - access.set(PUBLIC) - readme.set(rootProject.file("README.md")) - - packages { - named("js") { - packageName.set(project.name) - packageJson { - keywords.set( - listOf( - "kotlin", - "cognito", - "identity-provider", - "liftric", - "aws" - ) - ) - license.set("MIT") - description.set("Lightweight AWS Cognito Identity Provider client.") - homepage.set("https://github.com/liftric/cognito-idp") - } - } - } - - registries { - npmjs { - uri.set(uri("https://registry.npmjs.org")) - authToken.set(npmAccessKey) - } - } -} - signing { val signingKey: String? by project val signingPassword: String? by project diff --git a/gradle.properties b/gradle.properties index 6d6a4e8..4b22d93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,5 @@ kotlin.incremental=true kotlin.incremental.multiplatform=true kotlin.caching.enabled=true kotlin.code.style=official -kotlin.js.generate.externals=true kotlin.native.binary.memoryModel=experimental kotlin.js.ir.output.granularity=whole-program diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f9ffc7c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,37 @@ +[versions] +android-tools-gradle = "8.9.3" +kotlin = "2.1.20" +ktor = "3.1.3" +kotlinx-coroutines = "1.10.2" +kotlinx-serialization = "1.8.1" +kotlinx-browser = "0.3" +androidx-test-core = "1.6.1" +roboelectric = "4.9" +opt-java = "1.3.2" +vault-client = "2.0.0" +versioning = "3.1.0" +npm-publishing = "3.5.3" +definitions = "5.124.0" + +[libraries] +ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } +ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } +ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin-legacy", version.ref = "ktor" } +ktor-client-jvm = { group = "io.ktor", name = "ktor-client-java", version.ref = "ktor" } +ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktor" } +ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } +kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-browser = { group = "org.jetbrains.kotlinx", name = "kotlinx-browser", version.ref = "kotlinx-browser" } +androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } +roboelectric = { group = "org.robolectric", name = "robolectric", version.ref = "roboelectric" } +opt-java = { group = "com.github.bastiaanjansen", name = "otp-java", version.ref = "opt-java" } + +[plugins] +vault-client = { id = "com.liftric.vault-client-plugin", version.ref = "vault-client" } +versioning = { id = "net.nemerosa.versioning", version.ref = "versioning" } +npm-publishing = { id = "dev.petuska.npm.publish", version.ref = "npm-publishing" } +definitions = { id = "io.github.turansky.kfc.definitions", version.ref = "definitions" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "android-tools-gradle" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 27313fb..c6406bc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 3511839..15af1fc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,36 +6,4 @@ pluginManagement { mavenCentral() gradlePluginPortal() } -} - -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - } - - versionCatalogs { - create("libs") { - version("android-tools-gradle", "8.1.0") - version("kotlin", "1.9.10") - version("ktor", "2.3.5") - - library("ktor-client-core", "io.ktor", "ktor-client-core").versionRef("ktor") - library("ktor-client-android", "io.ktor", "ktor-client-android").versionRef("ktor") - library("ktor-client-darwin", "io.ktor", "ktor-client-darwin-legacy").versionRef("ktor") - library("ktor-client-jvm", "io.ktor", "ktor-client-java").versionRef("ktor") - library("ktor-client-js", "io.ktor", "ktor-client-js").versionRef("ktor") - library("kotlinx-coroutines", "org.jetbrains.kotlinx", "kotlinx-coroutines-core").version("1.7.2") - library("kotlinx-serialization", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version("1.5.1") - library("androidx-test-core", "androidx.test", "core").version("1.2.0") - library("roboelectric", "org.robolectric", "robolectric").version("4.9") - library("opt-java", "com.github.bastiaanjansen", "otp-java").version("1.3.2") - - plugin("vault-client", "com.liftric.vault-client-plugin").version("2.0.0") - plugin("versioning", "net.nemerosa.versioning").version("3.0.0") - plugin("npm-publishing", "dev.petuska.npm.publish").version("3.0.0") - plugin("definitions", "io.github.turansky.kfc.definitions").version("5.50.0") - plugin("kotlin.serialization", "org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin") - } - } -} +} \ No newline at end of file diff --git a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt index cc01c4e..f646df9 100644 --- a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt +++ b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt @@ -86,7 +86,7 @@ internal data class ConfirmForgotPassword( val Password: String ) -@JsExport + @Serializable data class UserAttribute( val Name: String, @@ -134,7 +134,7 @@ internal data class SetUserMFAPreference( val SoftwareTokenMfaSettings: MfaSettings? ) -@JsExport + @Serializable data class MfaSettings( val Enabled: Boolean, diff --git a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt index ca61389..fc9a36f 100644 --- a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt +++ b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt @@ -22,7 +22,7 @@ data class SignInResponse( val Session: String? ) -@JsExport + @Serializable data class AuthenticationResult( val AccessToken: String?, @@ -33,14 +33,14 @@ data class AuthenticationResult( val NewDeviceMetadata: NewDeviceMetadata?, ) -@JsExport + @Serializable data class NewDeviceMetadata( val DeviceGroupKey: String?, val DeviceKey: String?, ) -@JsExport + @Serializable data class SignUpResponse( val CodeDeliveryDetails: CodeDeliveryDetails?, @@ -48,13 +48,13 @@ data class SignUpResponse( val UserSub: String ) -@JsExport + @Serializable data class ResendConfirmationCodeResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) -@JsExport + @Serializable data class CodeDeliveryDetails( val AttributeName: String?, @@ -71,7 +71,7 @@ data class GetUserResponse( val Username: String ) -@JsExport + @Serializable data class MFAOptions( val AttributeName: String?, @@ -83,26 +83,26 @@ data class UpdateUserAttributesResponse( val CodeDeliveryDetailsList: List = listOf() ) -@JsExport + @Serializable data class GetAttributeVerificationCodeResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) -@JsExport + @Serializable data class ForgotPasswordResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) -@JsExport + @Serializable data class AssociateSoftwareTokenResponse( val SecretCode: String, val Session: String? ) -@JsExport + @Serializable data class VerifySoftwareTokenResponse( val Session: String?, diff --git a/src/jsMain/kotlin/ResponseJS.kt b/src/jsMain/kotlin/ResponseJS.kt index 61006bd..1da347a 100644 --- a/src/jsMain/kotlin/ResponseJS.kt +++ b/src/jsMain/kotlin/ResponseJS.kt @@ -4,7 +4,7 @@ import com.liftric.cognito.idp.core.* * Adapted [Response.kt] classes for Typescript usage (Map and List aren't compatible for [kotlin.js.JsExport]) */ -@JsExport + data class SignInResponseJS( val AuthenticationResult: AuthenticationResult?, val ChallengeParameters: Array, @@ -30,7 +30,7 @@ data class SignInResponseJS( } } -@JsExport + data class GetUserResponseJS( val MFAOptions: MFAOptions?, val PreferredMfaSetting: String?, @@ -63,7 +63,7 @@ data class GetUserResponseJS( } } -@JsExport + data class UpdateUserAttributesResponseJS( val CodeDeliveryDetailsList: Array = arrayOf() ) { @@ -83,7 +83,7 @@ data class UpdateUserAttributesResponseJS( } } -@JsExport + data class MapEntry(val key: String, val value: String) internal fun Map.toMapEntries(): Array = entries.map { diff --git a/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt b/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt index 5e7bf5e..46cf59a 100644 --- a/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt +++ b/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.promise actual class IdentityProviderClientTests : AbstractIdentityProviderClientTests() -actual fun runTest(block: suspend () -> Unit): dynamic = MainScope().promise { - block.invoke() +actual fun runTest(block: suspend () -> Unit) { + MainScope().promise { + block.invoke() + } } diff --git a/src/wasmJsMain/kotlin/com/liftric/cognito/idp/core/Engine.kt b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/core/Engine.kt new file mode 100644 index 0000000..c9c0c2f --- /dev/null +++ b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/core/Engine.kt @@ -0,0 +1,7 @@ +package com.liftric.cognito.idp.core + +import io.ktor.client.engine.* +import io.ktor.client.engine.cio.* + +actual val Engine: HttpClientEngine + get() = CIO.create() \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt new file mode 100644 index 0000000..3fd70d2 --- /dev/null +++ b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt @@ -0,0 +1,28 @@ +package com.liftric.cognito.idp.jwt + +import kotlinx.browser.window + +internal actual class Base64 { + actual companion object { + actual fun decode(input: String): String? { + // Validate the input to ensure it is proper Base64 + if (!input.matches(Regex("^[A-Za-z0-9+/]*={0,2}\$"))) { + return null + } + + return try { + // Decode using the browser's atob function + val decoded = window.atob(input) + + // Re-encode the result to compare with input (remove padding) + val reencoded = window.btoa(decoded) + .replace(Regex("=+\$"), "") + + if (input.replace(Regex("=+\$"), "") != reencoded) null else decoded + } catch (e: Throwable) { + // Return null if decoding fails + null + } + } + } +} \ No newline at end of file diff --git a/src/wasmJsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt b/src/wasmJsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt new file mode 100644 index 0000000..3122c1d --- /dev/null +++ b/src/wasmJsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientTests.kt @@ -0,0 +1,12 @@ +package com.liftric.cognito.idp + +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.promise + +actual class IdentityProviderClientTests : AbstractIdentityProviderClientTests() + +actual fun runTest(block: suspend () -> Unit) { + MainScope().promise { + block.invoke() + } +}