From 63ec1fa51409ac45d048e37e727c8dc035b8b84a Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Wed, 22 Oct 2025 08:44:08 +1100 Subject: [PATCH 1/6] Updated wasm support for cognito-idp 4.0.0 --- build.gradle.kts | 11 +++++++++++ gradle/libs.versions.toml | 2 ++ .../kotlin/com/liftric/cognito/idp/core/Engine.kt | 7 +++++++ .../kotlin/com/liftric/cognito/idp/jwt/Base64.kt | 9 +++++++++ .../kotlin/idp/IdentityProviderClientTests.kt | 9 +++++++++ 5 files changed, 38 insertions(+) create mode 100644 src/wasmJsMain/kotlin/com/liftric/cognito/idp/core/Engine.kt create mode 100644 src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt create mode 100644 src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt diff --git a/build.gradle.kts b/build.gradle.kts index 938d0c7..7455aef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,8 @@ +@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.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest @@ -69,6 +72,10 @@ kotlin { } } } + wasmJs { + browser() + binaries.library() + } sourceSets { val commonMain by getting { @@ -125,6 +132,10 @@ kotlin { implementation(kotlin("test-js")) } } + wasmJsMain.dependencies { + api(libs.ktor.client.js) + api(libs.kotlinx.browser) + } all { languageSettings { optIn("kotlinx.serialization.ExperimentalSerializationApi") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5915d49..e1c3063 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ "ktor" = "3.3.1" "otp-java" = "2.1.0" "robolectric" = "4.16" +"browser" = "0.5.0" [libraries] "androidx-test-core" = { module = "androidx.test:core", version.ref = "androidTestCore" } @@ -24,6 +25,7 @@ "ktor-client-js" = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } "otp-java" = { module = "com.github.bastiaanjansen:otp-java", version.ref = "otp-java" } "robolectric" = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +"kotlinx-browser" = { group = "org.jetbrains.kotlinx", name = "kotlinx-browser", version.ref = "browser" } [plugins] "kotlin-serialization" = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 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..96af254 --- /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.HttpClientEngine +import io.ktor.client.engine.js.Js + +internal actual val Engine: HttpClientEngine + get() = Js.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..9c881c6 --- /dev/null +++ b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt @@ -0,0 +1,9 @@ +package com.liftric.cognito.idp.jwt + +internal actual class Base64 { + actual companion object { + actual fun decode(input: String): String? { + TODO("Not yet implemented") + } + } +} \ No newline at end of file diff --git a/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt b/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt new file mode 100644 index 0000000..1582fea --- /dev/null +++ b/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt @@ -0,0 +1,9 @@ +package com.liftric.cognito.idp + +import kotlinx.coroutines.runBlocking + +actual class IdentityProviderClientTests: AbstractIdentityProviderClientTests() + +actual fun runTest(block: suspend () -> Unit) = runBlocking { + block.invoke() +} \ No newline at end of file From 776f501a2912ece200e5063cfcdf0bafd272d7e7 Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Wed, 22 Oct 2025 09:01:29 +1100 Subject: [PATCH 2/6] Fixed build failing due to deprecated JsExport --- .../kotlin/com/liftric/cognito/idp/core/Payload.kt | 2 -- .../kotlin/com/liftric/cognito/idp/core/Response.kt | 10 ---------- src/jsMain/kotlin/ResponseJS.kt | 4 ---- 3 files changed, 16 deletions(-) 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..7b00a95 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,6 @@ internal data class ConfirmForgotPassword( val Password: String ) -@JsExport @Serializable data class UserAttribute( val Name: String, @@ -134,7 +133,6 @@ 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..8dd783c 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,6 @@ data class SignInResponse( val Session: String? ) -@JsExport @Serializable data class AuthenticationResult( val AccessToken: String?, @@ -33,14 +32,12 @@ 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 +45,11 @@ data class SignUpResponse( val UserSub: String ) -@JsExport @Serializable data class ResendConfirmationCodeResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) -@JsExport @Serializable data class CodeDeliveryDetails( val AttributeName: String?, @@ -71,7 +66,6 @@ data class GetUserResponse( val Username: String ) -@JsExport @Serializable data class MFAOptions( val AttributeName: String?, @@ -83,26 +77,22 @@ 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..d7a5dd7 100644 --- a/src/jsMain/kotlin/ResponseJS.kt +++ b/src/jsMain/kotlin/ResponseJS.kt @@ -4,7 +4,6 @@ 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 +29,6 @@ data class SignInResponseJS( } } -@JsExport data class GetUserResponseJS( val MFAOptions: MFAOptions?, val PreferredMfaSetting: String?, @@ -63,7 +61,6 @@ data class GetUserResponseJS( } } -@JsExport data class UpdateUserAttributesResponseJS( val CodeDeliveryDetailsList: Array = arrayOf() ) { @@ -83,7 +80,6 @@ data class UpdateUserAttributesResponseJS( } } -@JsExport data class MapEntry(val key: String, val value: String) internal fun Map.toMapEntries(): Array = entries.map { From 18003b8b648b3ecbcfd072106f98889fcfd565c3 Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Thu, 30 Oct 2025 09:50:31 +1100 Subject: [PATCH 3/6] Revert "Fixed build failing due to deprecated JsExport" This reverts commit 776f501a2912ece200e5063cfcdf0bafd272d7e7. --- .../kotlin/com/liftric/cognito/idp/core/Payload.kt | 2 ++ .../kotlin/com/liftric/cognito/idp/core/Response.kt | 10 ++++++++++ src/jsMain/kotlin/ResponseJS.kt | 4 ++++ 3 files changed, 16 insertions(+) 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 7b00a95..cc01c4e 100644 --- a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt +++ b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Payload.kt @@ -86,6 +86,7 @@ internal data class ConfirmForgotPassword( val Password: String ) +@JsExport @Serializable data class UserAttribute( val Name: String, @@ -133,6 +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 8dd783c..ca61389 100644 --- a/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt +++ b/src/commonMain/kotlin/com/liftric/cognito/idp/core/Response.kt @@ -22,6 +22,7 @@ data class SignInResponse( val Session: String? ) +@JsExport @Serializable data class AuthenticationResult( val AccessToken: String?, @@ -32,12 +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?, @@ -45,11 +48,13 @@ data class SignUpResponse( val UserSub: String ) +@JsExport @Serializable data class ResendConfirmationCodeResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) +@JsExport @Serializable data class CodeDeliveryDetails( val AttributeName: String?, @@ -66,6 +71,7 @@ data class GetUserResponse( val Username: String ) +@JsExport @Serializable data class MFAOptions( val AttributeName: String?, @@ -77,22 +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 d7a5dd7..61006bd 100644 --- a/src/jsMain/kotlin/ResponseJS.kt +++ b/src/jsMain/kotlin/ResponseJS.kt @@ -4,6 +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, @@ -29,6 +30,7 @@ data class SignInResponseJS( } } +@JsExport data class GetUserResponseJS( val MFAOptions: MFAOptions?, val PreferredMfaSetting: String?, @@ -61,6 +63,7 @@ data class GetUserResponseJS( } } +@JsExport data class UpdateUserAttributesResponseJS( val CodeDeliveryDetailsList: Array = arrayOf() ) { @@ -80,6 +83,7 @@ data class UpdateUserAttributesResponseJS( } } +@JsExport data class MapEntry(val key: String, val value: String) internal fun Map.toMapEntries(): Array = entries.map { From a59ba63efef760e8ca1d68ce47178249893abef7 Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Thu, 30 Oct 2025 10:13:47 +1100 Subject: [PATCH 4/6] Moved JsExport from common to js only to support wasm Should be the exact same final functionality on the JS side. This is the only way to add wasm support while maintaining js support. --- .../com/liftric/cognito/idp/core/Payload.kt | 2 - .../com/liftric/cognito/idp/core/Response.kt | 10 -- src/jsMain/kotlin/IdentityProviderJS.kt | 60 ++++---- src/jsMain/kotlin/PayloadJS.kt | 31 +++++ src/jsMain/kotlin/ResponseJS.kt | 129 +++++++++++++++++- 5 files changed, 186 insertions(+), 46 deletions(-) create mode 100644 src/jsMain/kotlin/PayloadJS.kt 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..7b00a95 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,6 @@ internal data class ConfirmForgotPassword( val Password: String ) -@JsExport @Serializable data class UserAttribute( val Name: String, @@ -134,7 +133,6 @@ 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..8dd783c 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,6 @@ data class SignInResponse( val Session: String? ) -@JsExport @Serializable data class AuthenticationResult( val AccessToken: String?, @@ -33,14 +32,12 @@ 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 +45,11 @@ data class SignUpResponse( val UserSub: String ) -@JsExport @Serializable data class ResendConfirmationCodeResponse( val CodeDeliveryDetails: CodeDeliveryDetails ) -@JsExport @Serializable data class CodeDeliveryDetails( val AttributeName: String?, @@ -71,7 +66,6 @@ data class GetUserResponse( val Username: String ) -@JsExport @Serializable data class MFAOptions( val AttributeName: String?, @@ -83,26 +77,22 @@ 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/IdentityProviderJS.kt b/src/jsMain/kotlin/IdentityProviderJS.kt index 48c9f17..76a7c05 100644 --- a/src/jsMain/kotlin/IdentityProviderJS.kt +++ b/src/jsMain/kotlin/IdentityProviderJS.kt @@ -15,16 +15,16 @@ class IdentityProviderClientJS(region: String, clientId: String) { fun signUp( username: String, password: String, - attributes: Array? = null, + attributes: Array? = null, clientMetadata: Array? = null, - ): Promise = + ): Promise = MainScope().promise { provider.signUp( username = username, password = password, - attributes = attributes?.toList(), + attributes = attributes?.map(UserAttributeJS::toUserAttribute)?.toList(), clientMetadata = clientMetadata?.associate { it.key to it.value } - ).getOrWrapThrowable() + ).getOrWrapThrowable().toSignUpResponseJS() } fun confirmSignUp(username: String, confirmationCode: String): Promise = @@ -35,10 +35,10 @@ class IdentityProviderClientJS(region: String, clientId: String) { ).getOrWrapThrowable() } - fun resendConfirmationCode(username: String): Promise = + fun resendConfirmationCode(username: String): Promise = MainScope().promise { provider.resendConfirmationCode(username) - .getOrWrapThrowable() + .getOrWrapThrowable().toResendConfirmationCodeResponseJS() } fun signIn(username: String, password: String): Promise = @@ -46,7 +46,7 @@ class IdentityProviderClientJS(region: String, clientId: String) { provider.signIn(username, password) .getOrWrapThrowable().let { SignInResponseJS( - AuthenticationResult = it.AuthenticationResult, + AuthenticationResult = it.AuthenticationResult?.toAuthenticationResultJS(), ChallengeParameters = it.ChallengeParameters.toMapEntries(), ChallengeName = it.ChallengeName, Session = it.Session, @@ -59,7 +59,7 @@ class IdentityProviderClientJS(region: String, clientId: String) { provider.refresh(refreshToken) .getOrWrapThrowable().let { SignInResponseJS( - AuthenticationResult = it.AuthenticationResult, + AuthenticationResult = it.AuthenticationResult?.toAuthenticationResultJS(), ChallengeParameters = it.ChallengeParameters.toMapEntries(), ChallengeName = it.ChallengeName, Session = it.Session @@ -72,9 +72,9 @@ class IdentityProviderClientJS(region: String, clientId: String) { provider.getUser(accessToken) .getOrWrapThrowable().let { GetUserResponseJS( - MFAOptions = it.MFAOptions, + MFAOptions = it.MFAOptions?.toMFAOptionsJS(), PreferredMfaSetting = it.PreferredMfaSetting, - UserAttributes = it.UserAttributes.toTypedArray(), + UserAttributes = it.UserAttributes.map(UserAttribute::toUserAttributeJS).toTypedArray(), UserMFASettingList = it.UserMFASettingList.toTypedArray(), Username = it.Username ) @@ -83,14 +83,14 @@ class IdentityProviderClientJS(region: String, clientId: String) { fun updateUserAttributes( accessToken: String, - attributes: Array + attributes: Array ): Promise = MainScope().promise { provider.updateUserAttributes( accessToken = accessToken, - attributes = attributes.toList() + attributes = attributes.map(UserAttributeJS::toUserAttribute).toList() ).getOrWrapThrowable().let { - UpdateUserAttributesResponseJS(it.CodeDeliveryDetailsList.toTypedArray()) + UpdateUserAttributesResponseJS(it.CodeDeliveryDetailsList.map(CodeDeliveryDetails::toCodeDeliveryDetailsJS).toTypedArray()) } } @@ -107,10 +107,10 @@ class IdentityProviderClientJS(region: String, clientId: String) { ).getOrWrapThrowable() } - fun forgotPassword(username: String, clientMetadata: Array? = null): Promise = + fun forgotPassword(username: String, clientMetadata: Array? = null): Promise = MainScope().promise { provider.forgotPassword(username, clientMetadata?.associate { it.key to it.value }) - .getOrWrapThrowable() + .getOrWrapThrowable().toForgotPasswordResponseJS() } fun confirmForgotPassword( @@ -130,13 +130,13 @@ class IdentityProviderClientJS(region: String, clientId: String) { accessToken: String, attributeName: String, clientMetadata: Array? = null - ): Promise = + ): Promise = MainScope().promise { provider.getUserAttributeVerificationCode( accessToken = accessToken, attributeName = attributeName, clientMetadata = clientMetadata?.associate { it.key to it.value } - ).getOrWrapThrowable() + ).getOrWrapThrowable().toGetAttributeVerificationCodeResponseJS() } fun verifyUserAttribute( @@ -172,13 +172,13 @@ class IdentityProviderClientJS(region: String, clientId: String) { fun setUserMFAPreference( accessToken: String, - smsMfaSettings: MfaSettings?, - softwareTokenMfaSettings: MfaSettings? + smsMfaSettings: MfaSettingsJS?, + softwareTokenMfaSettings: MfaSettingsJS? ): Promise = MainScope().promise { provider.setUserMFAPreference( accessToken = accessToken, - smsMfaSettings = smsMfaSettings, - softwareTokenMfaSettings = softwareTokenMfaSettings + smsMfaSettings = smsMfaSettings?.toMfaSettings(), + softwareTokenMfaSettings = softwareTokenMfaSettings?.toMfaSettings() ).getOrWrapThrowable() } @@ -193,7 +193,7 @@ class IdentityProviderClientJS(region: String, clientId: String) { session ).getOrWrapThrowable().let { SignInResponseJS( - AuthenticationResult = it.AuthenticationResult, + AuthenticationResult = it.AuthenticationResult?.toAuthenticationResultJS(), ChallengeParameters = it.ChallengeParameters.toMapEntries(), ChallengeName = it.ChallengeName, Session = it.Session @@ -203,42 +203,42 @@ class IdentityProviderClientJS(region: String, clientId: String) { fun associateSoftwareToken( accessToken: String - ): Promise = MainScope().promise { + ): Promise = MainScope().promise { provider.associateSoftwareToken( accessToken = accessToken - ).getOrWrapThrowable() + ).getOrWrapThrowable().toAssociateSoftwareTokenResponseJS() } fun associateSoftwareTokenBySession( session: String - ): Promise = MainScope().promise { + ): Promise = MainScope().promise { provider.associateSoftwareTokenBySession( session = session - ).getOrWrapThrowable() + ).getOrWrapThrowable().toAssociateSoftwareTokenResponseJS() } fun verifySoftwareToken( accessToken: String, friendlyDeviceName: String?, userCode: String - ): Promise = MainScope().promise { + ): Promise = MainScope().promise { provider.verifySoftwareToken( accessToken = accessToken, friendlyDeviceName = friendlyDeviceName, userCode = userCode - ).getOrWrapThrowable() + ).getOrWrapThrowable().toVerifySoftwareTokenResponseJS() } fun verifySoftwareTokenBySession( session: String, friendlyDeviceName: String?, userCode: String - ): Promise = MainScope().promise { + ): Promise = MainScope().promise { provider.verifySoftwareTokenBySession( friendlyDeviceName = friendlyDeviceName, session = session, userCode = userCode - ).getOrWrapThrowable() + ).getOrWrapThrowable().toVerifySoftwareTokenResponseJS() } private fun Result.getOrWrapThrowable(): T = when (value) { diff --git a/src/jsMain/kotlin/PayloadJS.kt b/src/jsMain/kotlin/PayloadJS.kt new file mode 100644 index 0000000..904a214 --- /dev/null +++ b/src/jsMain/kotlin/PayloadJS.kt @@ -0,0 +1,31 @@ +import com.liftric.cognito.idp.core.MfaSettings +import com.liftric.cognito.idp.core.UserAttribute +import kotlinx.serialization.Serializable + +@JsExport +@Serializable +data class UserAttributeJS( + val Name: String, val Value: String +) + +fun UserAttributeJS.toUserAttribute(): UserAttribute { + return UserAttribute(Name, Value) +} + +fun UserAttribute.toUserAttributeJS(): UserAttributeJS { + return UserAttributeJS(Name, Value) +} + +@JsExport +@Serializable +data class MfaSettingsJS( + val Enabled: Boolean, val PreferredMfa: Boolean +) + +fun MfaSettingsJS.toMfaSettings(): MfaSettings { + return MfaSettings(Enabled, PreferredMfa) +} + +fun MfaSettings.toMfaSettingsJS(): MfaSettingsJS { + return MfaSettingsJS(Enabled, PreferredMfa) +} \ No newline at end of file diff --git a/src/jsMain/kotlin/ResponseJS.kt b/src/jsMain/kotlin/ResponseJS.kt index 61006bd..f09104c 100644 --- a/src/jsMain/kotlin/ResponseJS.kt +++ b/src/jsMain/kotlin/ResponseJS.kt @@ -1,4 +1,5 @@ import com.liftric.cognito.idp.core.* +import kotlinx.serialization.Serializable /** * Adapted [Response.kt] classes for Typescript usage (Map and List aren't compatible for [kotlin.js.JsExport]) @@ -6,7 +7,7 @@ import com.liftric.cognito.idp.core.* @JsExport data class SignInResponseJS( - val AuthenticationResult: AuthenticationResult?, + val AuthenticationResult: AuthenticationResultJS?, val ChallengeParameters: Array, val ChallengeName: String?, val Session: String? @@ -32,9 +33,9 @@ data class SignInResponseJS( @JsExport data class GetUserResponseJS( - val MFAOptions: MFAOptions?, + val MFAOptions: MFAOptionsJS?, val PreferredMfaSetting: String?, - val UserAttributes : Array, + val UserAttributes: Array, val UserMFASettingList: Array, val Username: String ) { @@ -65,7 +66,7 @@ data class GetUserResponseJS( @JsExport data class UpdateUserAttributesResponseJS( - val CodeDeliveryDetailsList: Array = arrayOf() + val CodeDeliveryDetailsList: Array = arrayOf() ) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -89,3 +90,123 @@ data class MapEntry(val key: String, val value: String) internal fun Map.toMapEntries(): Array = entries.map { MapEntry(it.key, it.value) }.toTypedArray() + +@JsExport +@Serializable +data class NewDeviceMetadataJS( + val DeviceGroupKey: String?, + val DeviceKey: String?, +) + +fun NewDeviceMetadata.toNewDeviceMetadataJS(): NewDeviceMetadataJS { + return NewDeviceMetadataJS(DeviceGroupKey, DeviceKey) +} + +@JsExport +@Serializable +data class AuthenticationResultJS( + val AccessToken: String?, + val ExpiresIn: Int?, + val IdToken: String?, + val RefreshToken: String?, + val TokenType: String?, + val NewDeviceMetadata: NewDeviceMetadataJS?, +) + +fun AuthenticationResult.toAuthenticationResultJS(): AuthenticationResultJS { + return AuthenticationResultJS( + AccessToken, + ExpiresIn, + IdToken, + RefreshToken, + TokenType, + NewDeviceMetadata?.toNewDeviceMetadataJS() + ) +} + +@JsExport +@Serializable +data class SignUpResponseJS( + val CodeDeliveryDetails: CodeDeliveryDetailsJS?, + val UserConfirmed: Boolean, + val UserSub: String +) + +fun SignUpResponse.toSignUpResponseJS(): SignUpResponseJS { + return SignUpResponseJS(CodeDeliveryDetails?.toCodeDeliveryDetailsJS(), UserConfirmed, UserSub) +} + +@JsExport +@Serializable +data class ResendConfirmationCodeResponseJS( + val CodeDeliveryDetails: CodeDeliveryDetailsJS +) + +fun ResendConfirmationCodeResponse.toResendConfirmationCodeResponseJS(): ResendConfirmationCodeResponseJS { + return ResendConfirmationCodeResponseJS(CodeDeliveryDetails.toCodeDeliveryDetailsJS()) +} + +@JsExport +@Serializable +data class CodeDeliveryDetailsJS( + val AttributeName: String?, + val DeliveryMedium: String?, + val Destination: String? +) + +fun CodeDeliveryDetails.toCodeDeliveryDetailsJS(): CodeDeliveryDetailsJS { + return CodeDeliveryDetailsJS(AttributeName, DeliveryMedium, Destination) +} + +@JsExport +@Serializable +data class MFAOptionsJS( + val AttributeName: String?, + val DeliveryMedium: String? +) + +fun MFAOptions.toMFAOptionsJS(): MFAOptionsJS { + return MFAOptionsJS(AttributeName, DeliveryMedium) +} + +@JsExport +@Serializable +data class GetAttributeVerificationCodeResponseJS( + val CodeDeliveryDetails: CodeDeliveryDetailsJS +) + +fun GetAttributeVerificationCodeResponse.toGetAttributeVerificationCodeResponseJS(): GetAttributeVerificationCodeResponseJS { + return GetAttributeVerificationCodeResponseJS(CodeDeliveryDetails.toCodeDeliveryDetailsJS()) +} + +@JsExport +@Serializable +data class ForgotPasswordResponseJS( + val CodeDeliveryDetails: CodeDeliveryDetailsJS +) + +fun ForgotPasswordResponse.toForgotPasswordResponseJS(): ForgotPasswordResponseJS { + return ForgotPasswordResponseJS(CodeDeliveryDetails.toCodeDeliveryDetailsJS()) +} + +@JsExport +@Serializable +data class AssociateSoftwareTokenResponseJS( + val SecretCode: String, + val Session: String? +) + +fun AssociateSoftwareTokenResponse.toAssociateSoftwareTokenResponseJS(): AssociateSoftwareTokenResponseJS { + return AssociateSoftwareTokenResponseJS(SecretCode, Session) +} + +@JsExport +@Serializable +data class VerifySoftwareTokenResponseJS( + val Session: String?, + val Status: String +) + +fun VerifySoftwareTokenResponse.toVerifySoftwareTokenResponseJS(): VerifySoftwareTokenResponseJS { + return VerifySoftwareTokenResponseJS(Session, Status) +} \ No newline at end of file From 556ab38dcab2350942149553aa94573be449a082 Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Mon, 3 Nov 2025 16:20:08 +1100 Subject: [PATCH 5/6] FIxed failing tests --- .../liftric/cognito/idp/IdentityProviderClientJSTests.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientJSTests.kt b/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientJSTests.kt index 4048e25..68a64a4 100644 --- a/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientJSTests.kt +++ b/src/jsTest/kotlin/com/liftric/cognito/idp/IdentityProviderClientJSTests.kt @@ -2,6 +2,7 @@ package com.liftric.cognito.idp import IdentityProviderClientJS import IdentityProviderExceptionJs +import UserAttributeJS import com.liftric.cognito.idp.core.UserAttribute import env import kotlinx.coroutines.await @@ -31,7 +32,7 @@ class IdentityProviderClientJSTests { provider.signUp( username, password, attributes = arrayOf( - UserAttribute(Name = "custom:target_group", Value = "ROLE_PATIENT") + UserAttributeJS(Name = "custom:target_group", Value = "ROLE_PATIENT") ) ).await().also { println("signUpResponse=$it") @@ -56,7 +57,7 @@ class IdentityProviderClientJSTests { provider.signUp( username, password, attributes = arrayOf( - UserAttribute(Name = "custom:target_group", Value = "ROLE_PATIENT") + UserAttributeJS(Name = "custom:target_group", Value = "ROLE_PATIENT") ), clientMetadata = mapOf("fallback_mode" to "true").toMapEntries(), ).await().also { @@ -81,7 +82,7 @@ class IdentityProviderClientJSTests { provider.signUp( "Username", buildString { (1..260).forEach { _ -> append("A") } }, attributes = arrayOf( - UserAttribute(Name = "custom:target_group", Value = "ROLE_USER") + UserAttributeJS(Name = "custom:target_group", Value = "ROLE_USER") ) ).then { fail("signUp must fail") From ecc0d530386d17f07c0612cb205132fbec979d7f Mon Sep 17 00:00:00 2001 From: EpicSquid Date: Tue, 25 Nov 2025 11:48:47 +1100 Subject: [PATCH 6/6] Fixed remaining tests and wasm issues in prod --- .../com/liftric/cognito/idp/jwt/Base64.kt | 29 +++++++++++++++---- .../kotlin/idp/IdentityProviderClientTests.kt | 11 ++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt index 9c881c6..3fd70d2 100644 --- a/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt +++ b/src/wasmJsMain/kotlin/com/liftric/cognito/idp/jwt/Base64.kt @@ -1,9 +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? { - TODO("Not yet implemented") - } - } + 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/idp/IdentityProviderClientTests.kt b/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt index 1582fea..b7df702 100644 --- a/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt +++ b/src/wasmJsTest/kotlin/idp/IdentityProviderClientTests.kt @@ -1,9 +1,12 @@ package com.liftric.cognito.idp -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.promise -actual class IdentityProviderClientTests: AbstractIdentityProviderClientTests() +actual class IdentityProviderClientTests : AbstractIdentityProviderClientTests() -actual fun runTest(block: suspend () -> Unit) = runBlocking { - block.invoke() +actual fun runTest(block: suspend () -> Unit) { + MainScope().promise { + block.invoke() + } } \ No newline at end of file