diff --git a/.planning/phases/02-authentication-foundation/02-03-PLAN.md b/.planning/phases/02-authentication-foundation/02-03-PLAN.md
index 322d3c3..fad34c4 100644
--- a/.planning/phases/02-authentication-foundation/02-03-PLAN.md
+++ b/.planning/phases/02-authentication-foundation/02-03-PLAN.md
@@ -17,7 +17,9 @@ files_modified:
- iosApp/iosApp/iOSApp.swift
- iosApp/Podfile
- composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt
+ - composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt
- composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt
+ - composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStoreContractTest.kt
autonomous: true
requirements: [AUTH-01, AUTH-02, AUTH-04, AUTH-05]
@@ -26,6 +28,7 @@ must_haves:
- "iOS and Android login use AppAuth authorization-code flow with PKCE through system browser and recipe://callback"
- "Requested scopes are exactly openid profile email offline_access"
- "AuthState JSON is stored through explicit iOS Keychain and Android EncryptedSharedPreferences-backed stores"
+ - "Every configured KMP target has a SecureAuthStateStore actual, including JVM and Wasm stubs"
- "JVM target has DEV_AUTH_TOKEN dev stub; Wasm target throws NotImplementedError(\"Wasm OIDC: v2\")"
- "Logout platform clients support RP-initiated end-session and local store clearing"
artifacts:
@@ -38,6 +41,12 @@ must_haves:
- path: "composeApp/src/iosMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.ios.kt"
provides: "iOS Keychain storage with AfterFirstUnlockThisDeviceOnly per D-14"
contains: "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"
+ - path: "composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt"
+ provides: "JVM dev-only in-memory AuthState store actual so desktop tests compile"
+ contains: "actual class SecureAuthStateStore"
+ - path: "composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt"
+ provides: "Wasm non-persistent AuthState store actual so wasm target compiles while OIDC remains v2"
+ contains: "actual class SecureAuthStateStore"
- path: "iosApp/iosApp/Info.plist"
provides: "recipe URL scheme registration"
contains: "CFBundleURLSchemes"
@@ -148,18 +157,24 @@ Output: expect/actual OIDC client, explicit secure auth state store, URL callbac
- composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/main.kt
- composeApp/src/webMain/kotlin/dev/ulfrx/recipe/main.kt
- composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt
+ composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt, composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt, composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt
JVM actual reads `DEV_AUTH_TOKEN` from environment and returns `OidcResult.Success(authStateJson = "dev:$token", accessToken = token, idToken = null, expiresAtEpochMillis = Long.MAX_VALUE)` when present. If missing, return `OidcResult.AuthError("DEV_AUTH_TOKEN is not set")`; do not hardcode a usable bearer token.
+ JVM `SecureAuthStateStore` actual must compile for desktop dev/tests without pretending to be production secure storage. Implement `actual class SecureAuthStateStore` with a private nullable in-memory `authStateJson` property and exact methods `read()`, `write(authStateJson: String)`, and `clear()`. This store is dev-only and process-local; do not use it for mobile targets.
+
Wasm actual throws exactly `NotImplementedError("Wasm OIDC: v2")` for login/refresh/logout per D-03.
+
+ Wasm `SecureAuthStateStore` actual must also exist so `:composeApp:compileKotlinWasmJs` compiles. Implement `actual class SecureAuthStateStore` with the same private nullable in-memory `authStateJson` property and `read()`, `write(authStateJson: String)`, `clear()` methods. Because Wasm OIDC itself throws `NotImplementedError("Wasm OIDC: v2")`, this store is non-persistent and only satisfies the KMP actual contract.
./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs
- `grep -q 'DEV_AUTH_TOKEN' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.jvm.kt`
+ - `grep -q 'actual class SecureAuthStateStore' composeApp/src/jvmMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.jvm.kt`
- `grep -q 'NotImplementedError("Wasm OIDC: v2")' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/OidcClient.wasmJs.kt`
+ - `grep -q 'actual class SecureAuthStateStore' composeApp/src/webMain/kotlin/dev/ulfrx/recipe/auth/SecureAuthStateStore.wasmJs.kt`
- `./gradlew :composeApp:jvmTest :composeApp:compileKotlinWasmJs` exits 0
Secondary targets compile without expanding Phase 2 scope into real Desktop/Wasm OIDC.