Files
2026-04-29 21:07:49 +02:00

254 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02-authentication-foundation
plan: 01
subsystem: auth
tags: [oidc, authentik, kotlinx-serialization, kmp, ktor, gradle-version-catalog, cocoapods, appauth, exposed, testcontainers]
requires:
- phase: 01-project-infrastructure-module-wiring
provides: gradle version catalog, recipe.kotlin.multiplatform convention plugin, shared module scaffold (D-19), Koin/Kermit bootstrap, Ktor server skeleton with ContentNegotiation + Database.migrate, verify-shared-pure.sh + verify-no-version-literals.sh
provides:
- dev.ulfrx.recipe.shared.Constants with OIDC_ISSUER (trailing slash), OIDC_CLIENT_ID = recipe-app, OIDC_REDIRECT_URI = recipe://callback, API_BASE_URL, SERVER_PORT (D-11)
- dev.ulfrx.recipe.shared.dto.User (domain identity)
- dev.ulfrx.recipe.shared.dto.MeResponse (@Serializable wire DTO with toUser() per D-27)
- shared/build.gradle.kts wired with kotlinSerialization plugin and api(libs.kotlinx.serializationJson)
- Phase 2 dependency aliases in gradle/libs.versions.toml (AppAuth, AndroidX Security Crypto, multiplatform-settings + coroutines, Exposed core/jdbc/java-time, HikariCP, Testcontainers postgresql + junit-jupiter, Ktor server auth/JWT/CallLogging/StatusPages, Ktor client core/auth/content-negotiation/logging/okhttp/darwin/cio + MPP serialization-kotlinx-json) without bumping Ktor (stays 3.4.1)
- composeApp/build.gradle.kts with kotlinSerialization + kotlin.native.cocoapods applied, cocoapods block bringing AppAuth-iOS via libs.versions.appauth.ios.get(), Phase 2 commonMain/androidMain/iosMain/jvmMain dependency wiring, manifestPlaceholders["appAuthRedirectScheme"] = "recipe", and locked compose.resources packageOfResClass
- server/build.gradle.kts with Ktor auth/JWT/CallLogging/StatusPages, Exposed DSL trio, Hikari, kotlinx.serialization-json, plus Testcontainers test dependencies
- docs/authentik-setup.md — reproducible Authentik OIDC provider playbook (D-10) with mandatory sections Provider/Scopes/Redirect URI/Server Env Vars/Logout/Manual UAT/Source Audit, plus an exhaustive multi-source audit table mapping AUTH-01..AUTH-06, CONTEXT D-01..D-34, RESEARCH constraints, UI-SPEC, VALIDATION Wave 0, and PATTERNS file map to either this doc or a downstream Phase 2 plan, with all deferred ideas explicitly excluded
affects:
- 02-02-PLAN.md (server JWT validation, JIT users, /api/v1/me — depends on shared MeResponse DTO + ktor server auth/jwt/exposed/hikari/testcontainers aliases)
- 02-03-PLAN.md (common OIDC/store contracts — depends on Constants, multiplatform-settings, ktor client, Coroutines stack)
- 02-04-PLAN.md (Android AppAuth actual + secure store — depends on libs.appauth + libs.androidx.security.crypto + manifestPlaceholders bootstrap)
- 02-05-PLAN.md (iOS AppAuth actual — depends on cocoapods AppAuth pod + libs.ktor.clientDarwin)
- 02-06-PLAN.md (LoginScreen/PostLoginPlaceholder UI — depends on MeResponse DTO and AuthSession contract from 02-03)
- 02-07-PLAN.md (integration glue / phase verification — depends on every prior plan)
tech-stack:
added:
- kotlinx-serialization-json (api scope in shared/commonMain)
- kotlinSerialization Gradle plugin on shared/composeApp/server (server already had it)
- kotlin.native.cocoapods plugin on composeApp (applied by id; bundled with KGP)
- AppAuth-Android (net.openid:appauth 0.11.1)
- AppAuth-iOS (CocoaPod 2.0.0) — Gradle CocoaPods DSL pulls it via libs.versions.appauth.ios.get()
- androidx.security:security-crypto 1.1.0 (Android secure AuthState store)
- com.russhwolf:multiplatform-settings + multiplatform-settings-coroutines 1.3.0
- Ktor client family (core/auth/content-negotiation/logging) + engines (okhttp Android, darwin iOS, cio JVM)
- Ktor server auth + auth-jwt + call-logging + status-pages (3.4.1, no patch bump)
- Exposed 0.55.0 (core + jdbc + java-time) and HikariCP 6.2.1
- Testcontainers 1.21.4 (postgresql + junit-jupiter)
patterns:
- Shared DTO contract pattern — kotlinx.serialization @Serializable data class with explicit camelCase wire keys, decoded with ignoreUnknownKeys for forward compat (Phase 3 will add householdId)
- Catalog-only version pinning — every Phase 2 dependency declared in gradle/libs.versions.toml; module build files reference libs.* only; verify-no-version-literals.sh enforces it
- Cocoapods-via-catalog pattern — pod("AppAuth") { version = libs.versions.appauth.ios.get() } keeps the build script literal-free even with native Pod integration
- Compose Resources package locking — explicit compose.resources { packageOfResClass = "..." } isolates UI code from build-script identity changes (group/version)
- Authentik provider audit — markdown audit table that traces every locked source (REQ/CONTEXT/RESEARCH/UI-SPEC/VALIDATION/PATTERNS) to either an in-doc anchor or a downstream plan, with deferred ideas explicitly listed
key-files:
created:
- shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt
- shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt
- shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt
- shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt
- docs/authentik-setup.md
modified:
- gradle/libs.versions.toml
- shared/build.gradle.kts
- composeApp/build.gradle.kts
- server/build.gradle.kts
- .gitignore (added *.podspec — generated by cocoapods plugin)
key-decisions:
- "Apply kotlin.native.cocoapods plugin by id, not via libs.plugins alias — the plugin ships inside the Kotlin Gradle plugin already on the classpath via recipe.kotlin.multiplatform; aliasing it forces a duplicate version request that Gradle rejects with 'already on the classpath, compatibility cannot be checked'."
- "Add manifestPlaceholders[\"appAuthRedirectScheme\"] = \"recipe\" to composeApp Android defaultConfig from Plan 02-01 (not Plan 02-04). AppAuth-Android's bundled manifest declares a ${appAuthRedirectScheme} placeholder that breaks AGP merge as soon as the dependency is on the classpath, even before any auth wiring. Setting it here is a Rule 3 prerequisite for the dependency to be cataloged."
- "Lock compose.resources packageOfResClass to the Phase 1 historical name. Adding top-level group = \"dev.ulfrx.recipe\" (required by the cocoapods plugin's podspec generator) shifts the generated Res-class package from recipe.composeapp.generated.resources to dev.ulfrx.recipe.composeapp.generated.resources, breaking Phase 1 App.kt imports. Locking the package keeps the diff inside Plan 02-01's stated files."
- "Ship a *.podspec gitignore entry. The Kotlin CocoaPods plugin regenerates composeApp/composeApp.podspec on every Gradle sync and that file legitimately contains 'AppAuth', '2.0.0' as a literal pin (CocoaPods semantics). Tracking it would either fail verify-no-version-literals.sh (if the verifier ever extends to *.podspec) or churn on every clean build."
- "kotlinx.serialization-json declared as api(...) in shared/commonMain so consumers (composeApp, server) inherit the @Serializable runtime without each re-declaring it. shared/commonMain stays free of Ktor / Compose / SQLDelight / Koin / Kermit per D-19 / INFRA-06."
- "Use the MPP variant of ktor-serialization-kotlinx-json for composeApp/commonMain (io.ktor:ktor-serialization-kotlinx-json) and keep the -jvm variant for the server module. Mixing variants between modules is the supported pattern; introducing a single MPP variant on the server breaks the existing ktor.serializationKotlinxJson alias used by the (jvm-only) server."
- "Server-side OIDC config (OIDC_ISSUER / OIDC_AUDIENCE / OIDC_JWKS_URL) is documented as env-var-driven in docs/authentik-setup.md (D-12) but the actual application.conf wiring is deferred to Plan 02-02. Plan 02-01 establishes the contract; 02-02 implements it."
patterns-established:
- "Shared DTO purity: shared/commonMain depends only on kotlin stdlib + kotlinx.serialization. Verified by ./tools/verify-shared-pure.sh."
- "Catalog discipline: every library and plugin version lives in gradle/libs.versions.toml; Gradle artifact identity (group/version) is allowed at the top of module build files but library/plugin pins are not. Verified by ./tools/verify-no-version-literals.sh."
- "TDD gate sequence: RED commit (test(02-01)) followed by GREEN commit (feat(02-01)) — the Phase 2 plans don't all use TDD but Plan 02-01's Task 1 sets the precedent for downstream auth plans."
- "Multi-source audit pattern: docs/authentik-setup.md ## Source Audit table is the template. Future phase docs that span multiple sources (REQ + CONTEXT + RESEARCH + VALIDATION + PATTERNS) should mirror this structure so audits stay reproducible."
requirements-completed: []
duration: 16m
completed: 2026-04-28
---
# Phase 02 Plan 01: Shared Auth Contracts, Dependency Aliases, and Authentik Setup Summary
**Phase 2 foundation: shared MeResponse/User DTOs + Constants, full Phase 2 dependency catalog (AppAuth/Exposed/Testcontainers/Ktor auth) wired into composeApp/server without bumping Ktor 3.4.1, plus the docs/authentik-setup.md reproducible-provider playbook with multi-source audit.**
## Performance
- **Duration:** 16 min
- **Started:** 2026-04-28T08:40:29Z
- **Completed:** 2026-04-28T08:55:58Z
- **Tasks:** 3 (Task 1 ran TDD: RED + GREEN)
- **Files modified:** 9 (5 created, 4 modified)
## Accomplishments
- Locked the `/api/v1/me` wire contract: `MeResponse` is `@Serializable`, decodes with `ignoreUnknownKeys` so Phase 3 can add `householdId` without breaking Phase 2 clients, and round-trips through `toUser()` to a stable domain `User`.
- Stood up `dev.ulfrx.recipe.shared.Constants` with `OIDC_ISSUER` (trailing slash placeholder host), `OIDC_CLIENT_ID = "recipe-app"`, `OIDC_REDIRECT_URI = "recipe://callback"`, plus `API_BASE_URL` and `SERVER_PORT` — the single config object every Phase 2 plan compiles against.
- Cataloged every Phase 2 dependency (AppAuth Android + iOS pod, AndroidX Security Crypto, multiplatform-settings, Ktor client/server auth family, Exposed DSL trio, Hikari, Testcontainers) and wired them into composeApp/server without bumping Ktor off `3.4.1`. CocoaPods integration brings AppAuth-iOS via `libs.versions.appauth.ios.get()` so no version literals leak into build files (`./tools/verify-no-version-literals.sh` stays green).
- Shipped `docs/authentik-setup.md` as a 240-line reproducible Authentik provider playbook covering Provider, Scopes, Redirect URI, Server Env Vars, Logout, Manual UAT (UAT-01..UAT-04), and a Source Audit table that traces every Phase 2 input (GOAL, AUTH-01..AUTH-06, CONTEXT D-01..D-34, RESEARCH, UI-SPEC, VALIDATION Wave 0, PATTERNS) to either this doc or a downstream Phase 2 plan, with all deferred ideas explicitly excluded.
## Task Commits
1. **Task 1 RED — Failing serialization test for MeResponse DTO**`6504b46` (test)
2. **Task 1 GREEN — Constants and MeResponse/User DTOs in shared**`7e73a9a` (feat)
3. **Task 2 — Phase 2 dependency aliases without bumping Ktor**`c1cc713` (feat)
4. **Task 3 — Authentik provider setup and Phase 2 source audit**`62040d4` (docs)
_Note: TDD task 1 produced two commits (RED then GREEN); no REFACTOR commit was needed because the GREEN implementation is already minimal._
## Files Created/Modified
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` — created. OIDC + API config object (D-11). Trailing-slash issuer, exact `recipe://callback` redirect URI, `recipe-app` client id (also `aud` per D-07).
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt` — created. Domain identity DTO; id is `String` (server UUID) so shared/commonMain stays free of UUID library deps.
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` — created. `@Serializable` wire DTO for `GET /api/v1/me` (D-27). One-to-one `toUser()` mapper. Forward-compatible with Phase 3 `householdId` via `ignoreUnknownKeys` decoders.
- `shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt` — created. Three-test contract: round-trip via camelCase wire keys, Phase 3 forward-compat decode, `toUser()` no-data-loss mapping.
- `shared/build.gradle.kts` — modified. Applied `alias(libs.plugins.kotlinSerialization)`; added `api(libs.kotlinx.serializationJson)` so consumers inherit the runtime; `shared/commonMain` purity preserved (still no Ktor/Compose/SQLDelight/Koin/Kermit imports).
- `gradle/libs.versions.toml` — modified. Added Phase 2 versions/libraries/plugins per D-13/D-26/research; Ktor stays at 3.4.1.
- `composeApp/build.gradle.kts` — modified. Added kotlinSerialization + kotlin.native.cocoapods plugins; cocoapods block (AppAuth pod via catalog version); per-source-set Phase 2 deps; manifestPlaceholders for AppAuth-Android scheme; `compose.resources.packageOfResClass` lock to keep Phase 1 App.kt imports valid.
- `server/build.gradle.kts` — modified. Added Ktor server auth/JWT/CallLogging/StatusPages, Exposed core/jdbc/java-time, HikariCP, kotlinx.serialization-json, plus Testcontainers postgresql + junit-jupiter test deps.
- `docs/authentik-setup.md` — created. Reproducible Authentik playbook + Phase 2 source audit (D-10).
- `.gitignore` — modified. Ignore `*.podspec` (regenerated on every Gradle sync by the cocoapods plugin).
## Decisions Made
See frontmatter `key-decisions` for the load-bearing list. Highlights:
- **kotlin.native.cocoapods applied by id, not by alias** — the plugin ships inside KGP already on the classpath via `recipe.kotlin.multiplatform`, so a `libs.plugins.kotlinCocoapods` alias triggers a duplicate-version-request error.
- **manifestPlaceholders["appAuthRedirectScheme"] = "recipe"** lands in this plan, not 02-04 — it's a Rule 3 prerequisite for the AppAuth dependency to be cataloged at all.
- **`compose.resources.packageOfResClass` locked to Phase 1's historical package** — adding `group = "dev.ulfrx.recipe"` (mandatory for the cocoapods podspec generator) would otherwise rewrite the generated `Res` package and break `App.kt` imports.
- **Ktor stays at 3.4.1** — Open Question resolved during planning; auth artifacts catalog against the same `version.ref = "ktor"`. Patch bump deferred unless a concrete incompatibility appears.
- **Server OIDC config wiring deferred to Plan 02-02** — Plan 02-01 documents the env-var contract in `docs/authentik-setup.md` (`OIDC_ISSUER`, `OIDC_AUDIENCE`, `OIDC_JWKS_URL`); Plan 02-02 implements it in `application.conf`.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] Apply kotlin.native.cocoapods by id, not via `alias(libs.plugins.kotlinCocoapods)`**
- **Found during:** Task 2 verification (`:composeApp:dependencies`)
- **Issue:** `Error resolving plugin [id: 'org.jetbrains.kotlin.native.cocoapods', version: '2.3.20']: The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.` The cocoapods plugin ships bundled with the Kotlin Gradle plugin classpath that `recipe.kotlin.multiplatform` already brings in.
- **Fix:** Apply via `id("org.jetbrains.kotlin.native.cocoapods")` (no version) inside the `plugins { ... }` block of `composeApp/build.gradle.kts`. Kept the `kotlinCocoapods` alias in `gradle/libs.versions.toml` per the plan's stated catalog additions; downstream plans can still reference `libs.versions.kotlinCocoapods.version` if they ever need the version programmatically.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:dependencies` and `:composeApp:compileKotlinIosSimulatorArm64` (with `cinteropAppAuthIosSimulatorArm64`) now pass.
- **Committed in:** `c1cc713`
**2. [Rule 3 - Blocking] Add `manifestPlaceholders["appAuthRedirectScheme"] = "recipe"` to composeApp Android defaultConfig**
- **Found during:** Task 2 (`:composeApp:compileDebugKotlinAndroid`)
- **Issue:** `Manifest merger failed : Attribute data@scheme at AndroidManifest.xml requires a placeholder substitution but no value for <appAuthRedirectScheme> is provided.` AppAuth-Android's bundled manifest declares an unsubstituted `${appAuthRedirectScheme}` placeholder that breaks AGP's merger as soon as the dependency is on the classpath, even before Plan 02-04 wires the full `<intent-filter>`.
- **Fix:** Added the placeholder to `defaultConfig.manifestPlaceholders`. Value is `"recipe"`, byte-for-byte consistent with `Constants.OIDC_REDIRECT_URI = "recipe://callback"`. Plan 02-04 will still land the explicit `<intent-filter>` in the Android manifest; this placeholder satisfies AppAuth's *built-in* manifest entry until then.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:compileDebugKotlinAndroid` now passes.
- **Committed in:** `c1cc713`
**3. [Rule 3 - Blocking] Lock `compose.resources.packageOfResClass` to the Phase 1 historical package**
- **Found during:** Task 2 (`:composeApp:compileDebugKotlinAndroid`, after the AppAuth manifest fix)
- **Issue:** Adding `group = "dev.ulfrx.recipe"` and `version = "1.0.0"` at the top of `composeApp/build.gradle.kts` (mandatory for the Kotlin CocoaPods plugin's podspec generator — `cocoapods` requires `project.version` if the block doesn't override it) shifted the Compose Resources `Res` class generated package from `recipe.composeapp.generated.resources` (Phase 1) to `dev.ulfrx.recipe.composeapp.generated.resources`, breaking `App.kt`'s `import recipe.composeapp.generated.resources.Res` and `compose_multiplatform`.
- **Fix:** Added an explicit `compose.resources { packageOfResClass = "recipe.composeapp.generated.resources" }` block to `composeApp/build.gradle.kts`, locking the generated package to the Phase 1 name regardless of `group`. This keeps Plan 02-01's diff inside its stated files; Plan 02-06 will replace `App.kt`'s template body with the real auth gate (D-30) and can choose to migrate the package then.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `:composeApp:compileDebugKotlinAndroid` now passes; `App.kt` imports unchanged.
- **Committed in:** `c1cc713`
**4. [Rule 3 - Housekeeping] Ignore `*.podspec` files generated by the cocoapods plugin**
- **Found during:** Task 2 (`git status` after first cocoapods Gradle invocation)
- **Issue:** Adding the cocoapods plugin causes `composeApp/composeApp.podspec` to be regenerated on every Gradle sync. The file legitimately embeds `'AppAuth', '2.0.0'` as a literal CocoaPods version pin (CocoaPods Ruby DSL semantics), which would either fail `./tools/verify-no-version-literals.sh` if it ever extended to `.podspec` or churn on every clean build.
- **Fix:** Added `*.podspec` to `.gitignore` with an explanatory comment.
- **Files modified:** .gitignore
- **Verification:** `git status --short` shows no untracked `composeApp.podspec`.
- **Committed in:** `c1cc713`
**5. [Rule 1 - Bug] Strip "version = \"2.0.0\"" substring from a comment in composeApp/build.gradle.kts**
- **Found during:** Task 2 (`./tools/verify-no-version-literals.sh`)
- **Issue:** A comment paraphrased the Plan 02-01 acceptance criterion using the literal text `version = "2.0.0"`. The verifier doesn't distinguish comments from code and flagged the line. The plan's `! grep -q 'version = "2.0.0"' composeApp/build.gradle.kts` acceptance criterion ALSO reads from comments, so the comment was fundamentally incompatible with the rule.
- **Fix:** Rewrote the comment to describe the rule without quoting the forbidden literal pattern.
- **Files modified:** composeApp/build.gradle.kts
- **Verification:** `./tools/verify-no-version-literals.sh` exits 0; both `! grep` acceptance criteria now hold.
- **Committed in:** `c1cc713`
**6. [Rule 1 - Bug] Use `debugCompileClasspath` instead of nonexistent `androidMainCompileClasspath` in Task 2 verify command**
- **Found during:** Task 2 (`:composeApp:dependencies --configuration androidMainCompileClasspath`)
- **Issue:** Plan 02-01 Task 2 specifies `--configuration androidMainCompileClasspath`, but under the current AGP/Gradle/Kotlin combination the actual configuration name is `debugCompileClasspath` (or `releaseCompileClasspath`). The plan's command name doesn't exist in the configuration container.
- **Fix:** Ran the functionally equivalent command (`./gradlew :composeApp:dependencies --configuration debugCompileClasspath :server:dependencies --configuration runtimeClasspath`) which resolves the same Phase 2 deps the plan was checking for. Documented in the Task 2 commit message so a future planner can update the plan if needed.
- **Files modified:** none — this is a verification-command rename, not a code change.
- **Verification:** Both classpaths resolve cleanly and contain every Phase 2 dep (AppAuth, AndroidX Security Crypto, Ktor client family, Exposed core/jdbc/java-time, Hikari, Ktor server auth-jwt/call-logging/status-pages, Testcontainers).
- **Committed in:** N/A (procedural; no code change)
---
**Total deviations:** 6 auto-fixed (5 × Rule 3 blocking, 1 × Rule 1 bug, 1 × procedural rename).
**Impact on plan:** All deviations were unavoidable consequences of cataloging the AppAuth/CocoaPods dependency stack and integrating it with Phase 1's existing build setup. Net diff stays inside Plan 02-01's `files_modified` frontmatter list (plus `.gitignore`, which is a build-hygiene artifact). Zero scope creep into Plan 02-02..02-07.
## Issues Encountered
- **STATE.md drift from orchestrator init.** Running `gsd-sdk query init.execute-phase` at agent start mutated `.planning/STATE.md` (advanced `current_plan: 0 → 1`, status `planned → executing`). Per the parallel-execution rules, worktree agents must not modify `STATE.md`; the orchestrator owns those writes. Reverted via `git checkout -- .planning/STATE.md` before staging the first commit so the orchestrator's later state update is the single source of truth. No follow-up needed.
## User Setup Required
None — Plan 02-01 is wiring + docs only. The `docs/authentik-setup.md` Manual UAT section documents what the user will need to configure in Authentik before Plan 02-02..02-07 can be exercised end-to-end, but Plan 02-01 itself doesn't require any external service interaction.
## Next Phase Readiness
- **Plan 02-02 (server JWT validation, JIT users, /api/v1/me)** — All catalog dependencies and DTOs are in place. `MeResponse` DTO is importable from `dev.ulfrx.recipe.shared.dto`; Ktor server auth/JWT/CallLogging/StatusPages, Exposed DSL trio, Hikari, and Testcontainers are wired into `server/build.gradle.kts`. `application.conf` env-var contract is documented in `docs/authentik-setup.md` § Server Env Vars; Plan 02-02 implements it.
- **Plan 02-03 (common OIDC/store contracts, JVM/Wasm actuals)** — `Constants` and `multiplatform-settings` (+ coroutines) are available in `shared`/`composeApp/commonMain`. Ktor client core/auth/content-negotiation/logging are wired into commonMain.
- **Plan 02-04 (Android AppAuth actual + secure store)** — `libs.appauth` and `libs.androidx.security.crypto` are wired into `androidMain`. The `appAuthRedirectScheme=recipe` manifest placeholder is already set; Plan 02-04 only needs to add the explicit `<intent-filter>` and the `RedirectUriReceiverActivity` registration.
- **Plan 02-05 (iOS AppAuth actual)** — The cocoapods block is configured with the `AppAuth` pod at the catalog version; `libs.ktor.clientDarwin` is in `iosMain` deps. The `Info.plist` `CFBundleURLTypes` registration is the remaining iOS step.
- **Plan 02-06 (UI: SplashScreen / LoginScreen / PostLoginPlaceholderScreen)** — No blockers; Compose Resources package is locked to the Phase 1 historical name so existing `App.kt` keeps compiling. Plan 02-06 will replace `App.kt`'s template body with the auth gate (D-30) and optionally migrate the resources package then.
- **Plan 02-07 (integration glue / phase verification)** — All Phase 2 source files in `02-VALIDATION.md` Wave 0 will exist by the end of 02-02..02-06; this plan establishes the catalog and DTOs they depend on.
**No outstanding blockers.** Phase 2's per-plan execution can proceed.
## Self-Check: PASSED
- Created files exist:
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/Constants.kt` — FOUND
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/User.kt` — FOUND
- `shared/src/commonMain/kotlin/dev/ulfrx/recipe/shared/dto/MeResponse.kt` — FOUND
- `shared/src/commonTest/kotlin/dev/ulfrx/recipe/shared/dto/MeResponseSerializationTest.kt` — FOUND
- `docs/authentik-setup.md` — FOUND
- Modified files reflect intended changes:
- `gradle/libs.versions.toml` — FOUND (Phase 2 aliases present)
- `shared/build.gradle.kts` — FOUND (kotlinSerialization plugin + api(libs.kotlinx.serializationJson))
- `composeApp/build.gradle.kts` — FOUND (cocoapods + Phase 2 deps)
- `server/build.gradle.kts` — FOUND (Ktor auth/JWT, Exposed, Hikari, Testcontainers)
- `.gitignore` — FOUND (`*.podspec` ignore)
- Commits exist:
- `6504b46` (test RED) — FOUND
- `7e73a9a` (feat GREEN) — FOUND
- `c1cc713` (Task 2 wiring) — FOUND
- `62040d4` (Task 3 docs) — FOUND
- Plan-level verification:
- `./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata` — PASS
- `./tools/verify-shared-pure.sh` — PASS
- `./tools/verify-no-version-literals.sh` — PASS
- `./gradlew :composeApp:compileDebugKotlinAndroid :server:compileKotlin` — PASS
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64` — PASS (`cinteropAppAuthIosSimulatorArm64` exercised the AppAuth pod end-to-end)
## TDD Gate Compliance
Plan 02-01 frontmatter has `type: execute` (not `type: tdd`), so plan-level RED/GREEN/REFACTOR enforcement does not apply. However, Task 1 was tagged `tdd="true"` and produced the expected gate sequence inside its scope:
- RED: `6504b46` (`test(02-01): add failing serialization test for MeResponse DTO`) — confirmed failing on `MeResponse` / `User` unresolved references.
- GREEN: `7e73a9a` (`feat(02-01): land Constants and MeResponse/User DTOs in shared`) — test now passes.
- REFACTOR: omitted; the GREEN implementation is already minimal.
---
*Phase: 02-authentication-foundation*
*Completed: 2026-04-28*