Commit Graph

79 Commits

Author SHA1 Message Date
edc2a1d4c8 feat(02-03): define common auth contracts
- Add OIDC result and expect client seam with pinned native AppAuth semantics
- Add secure AuthState JSON store contract and JVM dev actuals for test compilation
2026-04-28 13:48:25 +02:00
3122fdaf37 docs(02-02): complete server auth boundary plan
- add execution summary with verification and deviations

- update state, roadmap progress, and completed auth requirements
2026-04-28 13:46:46 +02:00
7ef222e71e test(02-03): add failing secure auth state store contract
- Covers write overwrite semantics
- Covers clear removing stored AuthState JSON
2026-04-28 13:36:48 +02:00
8cf112a68a feat(02-02): add users migration, JIT PrincipalResolver, /api/v1/me route 2026-04-28 13:14:59 +02:00
36c1b2c822 feat(02-02): wire AuthConfig, JWT verifier, and CallLogging redaction 2026-04-28 13:06:50 +02:00
614b57c34d test(02-02): add failing JWT validation tests for AuthPlugin 2026-04-28 13:04:04 +02:00
fe8c0b6823 docs(phase-02): update tracking after wave 1 2026-04-28 11:00:51 +02:00
9f7cadda7b docs(02-01): complete shared auth contracts and Authentik setup plan
Adds the per-plan SUMMARY for 02-01: shared MeResponse/User DTOs +
Constants, full Phase 2 dependency catalog wired into composeApp/
server without bumping Ktor 3.4.1, and docs/authentik-setup.md
reproducible-provider playbook with multi-source audit.

3 tasks (Task 1 ran TDD: RED + GREEN), 4 commits, 6 deviations
auto-fixed (5 × Rule 3 blocking, 1 × Rule 1 bug), 0 scope creep.
All plan-level verifications PASS.

Per parallel-execution rules, this commit does not modify STATE.md
or ROADMAP.md — the orchestrator owns those updates after the wave
completes.
2026-04-28 10:59:07 +02:00
62040d461a docs(02-01): add Authentik provider setup and Phase 2 source audit
Task 02-01-03. Creates docs/authentik-setup.md as the load-bearing
Phase 2 deliverable (D-10): a reproducible playbook for the
homelab Authentik provider plus the multi-source audit that ties
every Phase 2 input to a covering plan.

Sections (in mandated order):
- Provider — Public + PKCE S256, recipe-app client_id, RS256, single-
  string aud, JWKS URI, end-session endpoint, Issuer trailing slash.
- Scopes — exactly `openid profile email offline_access`; explains
  why offline_access must be both requested AND mapped on the
  provider for refresh tokens (PITFALLS.md Phase 2 Pitfall 2).
- Redirect URI — recipe://callback, registered byte-for-byte in
  Authentik + iOS Info.plist + Android <intent-filter>.
- Server Env Vars — OIDC_ISSUER / OIDC_AUDIENCE / OIDC_JWKS_URL with
  override semantics matching Phase 1's DATABASE_URL pattern.
- Logout — RP-initiated end-session sequence (D-19, D-20).
- Manual UAT — UAT-01 fresh login, UAT-02 reopen with refresh,
  UAT-03 logout returns to login, UAT-04 curl/HTTP verification of
  GET /api/v1/me 200/401 cases including wrong-aud and never-log-
  Authorization assertion.
- Source Audit — exhaustive table mapping GOAL Phase 2, REQ
  AUTH-01..AUTH-06, RESEARCH constraints, CONTEXT D-01..D-34,
  UI-SPEC, VALIDATION Wave 0, and PATTERNS file map to either this
  doc () or a downstream Phase 2 plan (⤳). All deferred ideas
  listed as ✂ excluded: Universal Links/App Links, real Desktop
  OIDC, Wasm OIDC, Apple Sign-in, Authentik provisioning automation,
  per-user AuthState, modal refresh-failure UX, background refresh,
  two-tier logout, BuildConfig OIDC injection, real-Authentik
  integration tests.

Verification:
- grep -E 'openid profile email offline_access|PKCE S256|single-string
  |recipe://callback|/api/v1/me|Source Audit' docs/authentik-setup.md:
  hits all six tokens.
- All Task 3 grep acceptance criteria PASS, including
  AUTH-01.*AUTH-02.*AUTH-03.*AUTH-04.*AUTH-05.*AUTH-06 on a single
  audit-table line and "Universal Links / App Links.*excluded".
2026-04-28 10:55:38 +02:00
c1cc713bbb feat(02-01): wire Phase 2 dependency aliases without bumping Ktor
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 <intent-filter>.
- 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.
2026-04-28 10:52:40 +02:00
7e73a9a820 feat(02-01): land Constants and MeResponse/User DTOs in shared
GREEN phase of TDD task 02-01-01. Adds the load-bearing Phase 2
contract that downstream plans compile against:

- Constants.kt: OIDC_ISSUER (trailing slash, placeholder homelab host),
  OIDC_CLIENT_ID = recipe-app, OIDC_REDIRECT_URI = recipe://callback,
  API_BASE_URL, plus moved SERVER_PORT for one shared config object.
- dto/User.kt: domain identity (id/sub/email/displayName), id is String
  to keep shared free of UUID library deps (D-19 / INFRA-06).
- dto/MeResponse.kt: @Serializable wire DTO for GET /api/v1/me with a
  one-to-one toUser() mapper. Stable for Phase 3 to add householdId
  via ignoreUnknownKeys.
- Removes the now-redundant shared/.gitkeep placeholder.

Verification:
- ./gradlew :shared:jvmTest :shared:compileCommonMainKotlinMetadata: PASS
- ./tools/verify-shared-pure.sh: PASS
- All grep acceptance criteria for Task 1 satisfied
2026-04-28 10:45:04 +02:00
6504b46e40 test(02-01): add failing serialization test for MeResponse DTO
RED phase of TDD task 02-01-01. Locks the wire-format contract for
GET /api/v1/me before the DTO exists:

- camelCase JSON keys (id, sub, email, displayName) per D-27
- ignoreUnknownKeys forward compat for Phase 3 householdId per D-28
- MeResponse.toUser() one-to-one mapping

Wires kotlinx.serialization into shared/build.gradle.kts (api scope so
both client and server inherit the @Serializable runtime) and adds the
kotlinx-serializationJson catalog alias. The shared module remains
pure: only kotlin stdlib + kotlinx.serialization-json are pulled into
commonMain (D-19 / INFRA-06 still holds).

Test currently fails: MeResponse and User unresolved; GREEN follows.
2026-04-28 10:43:15 +02:00
1246e12012 docs(state): mark phase 2 ready for execution 2026-04-28 10:31:02 +02:00
37450291c6 docs(02): fix auth plan verification blockers 2026-04-27 21:11:46 +02:00
0b01bc8bbb fix(02): split auth platform plans 2026-04-27 21:07:18 +02:00
f0462cbca1 docs(02): add missing auth store actuals to plan 2026-04-27 20:59:41 +02:00
29d655828d docs(02): resolve planning verification artifacts 2026-04-27 20:57:05 +02:00
cca3ab7923 docs(02): create authentication foundation plans 2026-04-27 20:54:21 +02:00
ab69cc1dff docs(02): add validation strategy 2026-04-27 20:42:04 +02:00
090027224c docs(02): research phase domain 2026-04-27 20:41:15 +02:00
6ab7960e16 docs(02): approve UI design contract
UI-SPEC verified — all 6 dimensions PASS. No flags. Frontmatter
status flipped from draft to approved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:02:19 +02:00
31b4f4d57e docs(02): UI design contract for auth foundation
Locks scaffold-level visual contract for Phase 2: spacing scale,
typography roles, Material 3 color seed, copywriting keys, and
auth-gate routing — without committing to Liquid-Glass / Haze
(Phase 10) or final font + Polish polish (Phase 11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:59:41 +02:00
830097f5c1 docs(state): record phase 2 context session 2026-04-27 19:29:04 +02:00
f3569b41d6 docs(02): capture phase context 2026-04-27 19:28:57 +02:00
04b3d9b1d5 Remove unnecessary convention plugins 2026-04-26 22:22:28 +02:00
42d134a997 docs(01-07): complete phase gate plan 2026-04-24 20:59:21 +02:00
68655eae1a Phase 1 work 2026-04-24 20:21:03 +02:00
b36058fa79 chore(01-07): add shared package scaffold placeholder
- Create dev.ulfrx.recipe.shared sub-package with .gitkeep marker
- Phase 2+ will populate with cross-target DTOs / domain models
- Satisfies INFRA-06 file-existence criterion for empty package scaffold
2026-04-24 19:46:30 +02:00
81bff1db17 merge(01-04): Koin + Kermit bootstrap across all platforms 2026-04-24 19:45:25 +02:00
eaa88fff36 docs(01-04): add SUMMARY for Koin + Kermit bootstrap plan 2026-04-24 19:44:47 +02:00
fd3e7e1584 feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit
- JVM main: configureLogging() + initKoin() before application { Window }
- Wasm main: configureLogging() + initKoin() before ComposeViewport (PITFALL #8)
- iOSApp.swift: import ComposeApp + init { KoinIosKt.doInitKoin() } (PITFALL #4)
2026-04-24 19:41:51 +02:00
129ee616d5 docs(01-05): add SUMMARY for server /health + Flyway + HOCON plan 2026-04-24 19:41:47 +02:00
8cd608a981 feat(01-04): add Android MainApplication + manifest registration
- MainApplication.onCreate calls configureLogging() then initKoin { androidContext(...) }
- AndroidManifest registers android:name=".MainApplication"
2026-04-24 19:41:22 +02:00
cc5002d1df feat(01-04): add Koin + Kermit bootstrap commonMain + iOS bridge
- initKoin() helper with optional KoinAppDeclaration config
- empty appModule placeholder (Phase 2+ extends)
- configureLogging() sets Kermit tag 'recipe' (D-15)
- iOS doInitKoin() bridge → Swift symbol KoinIosKt.doInitKoin
2026-04-24 19:41:05 +02:00
d7ee6b83fc Add summary for plan phase 1.2 2026-04-24 19:26:41 +02:00
61885455bb merge(01-06): docker-compose + README Local development 2026-04-24 18:41:51 +02:00
6972839fd0 merge(01-05): server /health + Flyway + HOCON + fail-loud DB boot 2026-04-24 18:41:51 +02:00
c79f9218aa merge(01-03): module refactor to recipe.* conventions + drop js 2026-04-24 18:41:51 +02:00
2c786b2fc2 merge(01-02): build-logic scaffold + 5 precompiled plugins 2026-04-24 18:41:43 +02:00
f9d3a0c2d4 docs(01-06): add SUMMARY for dev-ergonomics plan 2026-04-24 18:24:24 +02:00
b8671d6dbb docs(01-03): summary of module build-script conventions wiring
- composeApp/build.gradle.kts: 114 -> 28 lines (role declaration with 4 recipe.* IDs)
- shared/build.gradle.kts: 55 -> 36 lines (3 plugins, explicitApi, Framework baseName "Shared")
- server/build.gradle.kts: 23 -> 18 lines (recipe.jvm.server + recipe.quality + module-only config)
- shared/src/jsMain/ deleted (D-01)
- 0 deviations; both verify-*.sh scripts pass; INFRA-02 + INFRA-06 structural prerequisites delivered
2026-04-24 18:23:41 +02:00
59d069591b test(01-05): rewrite ApplicationTest to assert GET /health without Postgres
- Replace testRoot template assertion with 'health endpoint returns 200 with status ok'
- Compose only configureRouting() in testApplication — NOT Application.module()
- This keeps the test independent of Database.migrate / running Postgres (D-11 test invariant)
- Install ContentNegotiation { json() } inside application { } — production module() does it,
  but the test composes routing directly and must install the plugin itself
- All imports explicit (D-11 allWarningsAsErrors); no wildcards
- Body checked via substring for "status" + "ok" — robust to JSON field ordering

Note: ./gradlew :server:test runtime verification deferred to Plan 07 (integration build)
since build-logic/recipe.jvm.server plugin is being authored in parallel Plan 02 worktree.
2026-04-24 18:23:14 +02:00
60221f66a2 feat(01-02): wire build-logic into root settings + add spotless/flyway classloader hints
- settings.gradle.kts: includeBuild("build-logic") placed inside pluginManagement { } (PITFALL #9)
- build.gradle.kts: 2 new alias(...) apply false entries (spotless, flywayPlugin)
- Existing repositories, module includes, and 8 original apply-false entries preserved verbatim
2026-04-24 18:22:56 +02:00
37f6191523 feat(01-04): wire JVM + Wasm main + Swift iOSApp to bootstrap Koin + Kermit
- Desktop main() calls configureLogging() → initKoin() before application { Window { App() } }
- Wasm main() calls configureLogging() → initKoin() before ComposeViewport { App() } (PITFALL #8 future-proof)
- iOSApp.swift imports ComposeApp and calls KoinIosKt.doInitKoin() in init() — single iOS call site (PITFALL #4)
- MainViewController.kt and App.kt unmodified (anti-pattern guards)
2026-04-24 18:22:47 +02:00
f691400f2b docs(01-06): add Local development section and drop js target
- New "Local development" section documents docker compose + gradlew dev loop
- Covers /health smoke test, env-var overrides (DATABASE_* and PORT)
- Adds spotlessApply + check + down -v reference commands
- Removes legacy js target docs (D-01); wasmJs target preserved
2026-04-24 18:22:41 +02:00
daefe6c26d refactor(01-05): rewrite Application.kt with ContentNegotiation, Flyway boot, /health
- Remove wildcard Ktor imports (D-11 allWarningsAsErrors safety) — all imports explicit
- Install ContentNegotiation { json() } for @Serializable response bodies
- Call Database.migrate(this) at boot — fails loudly if Postgres unreachable
- Extract configureRouting() extension so tests can compose routing without DB
- Replace template root greeting with GET /health → {"status":"ok"} (D-16)
- main() shape unchanged: embeddedServer(Netty, SERVER_PORT, "0.0.0.0", ...)
2026-04-24 18:22:37 +02:00
d316a4805e refactor(01-03): apply recipe.jvm.server + recipe.quality to server module
- Replaces alias(kotlinJvm) + alias(ktor) + application with id("recipe.jvm.server") + id("recipe.quality") — application plugin now applied by the convention
- Removes per-module dep lines (logback, ktor-serverCore, ktor-serverNetty, ktor-serverTestHost, kotlin-testJunit) — all bundled in recipe.jvm.server
- Keeps module-only config: group/version coordinates, application { mainClass.set } + applicationDefaultJvmArgs, implementation(projects.shared)
- File shrinks 23 -> 18 lines; no version literals leak
2026-04-24 18:22:09 +02:00
24018efe67 feat(01-05): add HOCON config, Flyway migration dir, fail-loud Database.migrate
- application.conf: HOCON with ktor.deployment.port (8080 + ${?PORT}) and
  database.url/user/password (localhost defaults + ${?DATABASE_URL/USER/PASSWORD})
- db/migration/.gitkeep: placeholder so classpath:db/migration resolves
- Database.kt: object Database.migrate(app) reads HOCON config, runs Flyway
  with baselineOnMigrate + validateOnMigrate + cleanDisabled, throws
  IllegalStateException on any failure (D-16 fail-loud contract)
- SLF4J (not Kermit); server logs url+user only, never password
2026-04-24 18:22:08 +02:00
4e6192293f feat(01-04): add Android MainApplication + manifest registration
- Create MainApplication : Application() running configureLogging() then initKoin { androidContext(this@MainApplication) } in onCreate
- Register android:name=".MainApplication" on <application> element (MainActivity entry preserved)
- Establishes the canonical init order for Android process boot
2026-04-24 18:22:03 +02:00
6a69910aa7 feat(01-02): scaffold build-logic included build with 5 precompiled plugins
- build-logic/settings.gradle.kts resolves parent catalog via files("../gradle/libs.versions.toml")
- build-logic/build.gradle.kts declares kotlin-dsl + 9 compileOnly asDependency entries
- recipe.quality: Spotless + ktlint + D-11 allWarningsAsErrors safety net (plugins.withId guard)
- recipe.kotlin.multiplatform: D-05 target matrix (androidTarget, iosArm64, iosSimulatorArm64, jvm, wasmJs) + JVM 11/21 split + baseName "ComposeApp" + Koin/Kermit/kotlin-test deps
- recipe.compose.multiplatform: layers on recipe.kotlin.multiplatform (PITFALL #2 avoided) + hot-reload + Compose deps
- recipe.android.application: namespace dev.ulfrx.recipe + findVersion catalog accessor (PITFALL #1)
- recipe.jvm.server: Ktor + Flyway + Postgres with quoted "implementation" configs + cleanDisabled guard
2026-04-24 18:22:02 +02:00