Files
2026-06-18 22:21:38 -04:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
19-local-auth-no-oidc-mode 05 execute 4
19-01
19-03
apps/api/src/auth/devBypass.ts
apps/api/scripts/reset-admin.ts
apps/pwa/e2e/global-setup.ts
apps/pwa/e2e/login.spec.ts
.gitea/workflows/ci.yml
false
AUTH-LOCAL-11
AUTH-LOCAL-16
truths artifacts key_links
With DEV_AUTH_BYPASS=true, every request also carries a real local-session cookie for the dev user, so the PWA login gate skips to the app
The Phase-7/8 harness still reaches the authed PWA without manual login (existing specs unchanged)
A login-specific spec can clear the local-session cookie and exercise the real /login form against the seeded dev credential
global-setup seeds a local_credentials row for the dev user (id=1) and truncates it between runs
CI provides LOCAL_SESSION_SECRET to the harness job and seeds the local_credentials table
The break-glass CLI creates/resets a local admin by username, runs only outside production, and is excluded from the prod image
path provides min_lines
apps/api/scripts/reset-admin.ts break-glass create/reset local admin CLI (dev-only) 30
path provides min_lines
apps/pwa/e2e/login.spec.ts real-login-form e2e covering the gate + form (AUTH-LOCAL-12/15) 25
path provides contains
apps/pwa/e2e/global-setup.ts local_credentials dev seed + truncate local_credentials
from to via pattern
apps/api/src/auth/devBypass.ts apps/api/src/auth/localSession.ts devSessionCookieMiddleware issues a real local-session cookie for DEV_USER (Option C) local-session
from to via pattern
apps/pwa/e2e/global-setup.ts local_credentials table INSERT ... ON DUPLICATE KEY UPDATE seed for dev user id=1 local_credentials
from to via pattern
.gitea/workflows/ci.yml LOCAL_SESSION_SECRET harness job env + table seed LOCAL_SESSION_SECRET
Rework the dev-bypass + Phase-7/8 Playwright harness to coexist with the new login UI (Option C: bypass issues a real `local-session` cookie), add the break-glass CLI, seed `local_credentials` for the dev user, add a real-login e2e spec, and update the CI harness job — all while preserving the D-15 dev-only/no-prod-image guarantees.

Purpose: The new login gate would otherwise break the harness, which reaches the authed PWA purely via DEV_AUTH_BYPASS (D-14). Option C is the minimal-change path: the bypass keeps setting c.get('user') AND now also issues the same local-session cookie the PWA gate expects, so existing specs pass unchanged; a dedicated login spec clears the cookie to test the real form. This is glue + CI + a CLI script (type: execute). D-15 is enforced by the existing IMG-01/02/03 gates plus the .dockerignore apps/api/scripts/ exclusion added in 19-01.

Output: edited devBypass.ts, new reset-admin.ts, edited global-setup.ts, new login.spec.ts, edited ci.yml.

Derived REQ-IDs covered: AUTH-LOCAL-11 (break-glass CLI, D-13), AUTH-LOCAL-16 (dev-bypass + harness rework, D-14/D-15).

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/19-local-auth-no-oidc-mode/19-RESEARCH.md @.planning/phases/19-local-auth-no-oidc-mode/19-PATTERNS.md @.planning/phases/19-local-auth-no-oidc-mode/19-CONTEXT.md @.planning/phases/19-local-auth-no-oidc-mode/19-01-SUMMARY.md @.planning/phases/19-local-auth-no-oidc-mode/19-03-SUMMARY.md Task 1: Option C — devSessionCookieMiddleware issues a real local-session cookie under bypass - apps/api/src/auth/devBypass.ts (DEV_USER shape lines ~30-36; devAuthBypass() env-guard structure lines ~58-76; the production hard-guard is the FIRST check and must stay first) - apps/api/tests/auth/devBypass.test.ts (the test that must keep passing) - apps/api/src/auth/localSession.ts (issueLocalSessionCookie + getCookie('local-session') — from 19-01) - apps/api/src/index.ts (where devAuthBypass() is mounted — the companion middleware mounts just after it; from 19-03 wiring) - .planning/phases/19-local-auth-no-oidc-mode/19-RESEARCH.md §Dev-Bypass Rework (Option C; D-15 compliance) + Pitfall 7 apps/api/src/auth/devBypass.ts, apps/api/src/index.ts In apps/api/src/auth/devBypass.ts add `export function devSessionCookieMiddleware(): MiddlewareHandler`. Keep the production hard-guard as the FIRST check (return no-op when NODE_ENV==='production') and a no-op when DEV_AUTH_BYPASS!=='true' — identical guard order to devAuthBypass so the IMG-01 boot guard / `assertNotDevBypassInProduction` continues to protect it. When active: on each request that does NOT already have a `local-session` cookie (getCookie), call `issueLocalSessionCookie(c, DEV_USER.id)` so the PWA login gate sees a valid session and skips /login. Then next(). devAuthBypass() itself is unchanged (still sets c.get('user')).
In apps/api/src/index.ts mount `app.use('/api/*', devSessionCookieMiddleware())` immediately AFTER `app.use('/api/*', devAuthBypass())` (it is a no-op outside bypass mode, so it is safe to mount unconditionally like devAuthBypass). Do not change the OIDC-side chain.
pnpm --filter @familysync/api test tests/auth/devBypass.test.ts && pnpm --filter @familysync/api typecheck - `pnpm --filter @familysync/api test tests/auth/devBypass.test.ts` exits 0 (existing bypass behavior intact) - Source assertion: devSessionCookieMiddleware's FIRST conditional is `NODE_ENV === 'production'` returning a no-op (grep the guard order) — D-15 - Source assertion: `grep -c "issueLocalSessionCookie" apps/api/src/auth/devBypass.ts` >= 1 - Source assertion: `grep -c "devSessionCookieMiddleware" apps/api/src/index.ts` >= 1 mounted after devAuthBypass Under DEV_AUTH_BYPASS, a real local-session cookie is issued for the dev user (production-guarded); existing bypass tests still pass. Task 2: Break-glass reset-admin CLI (dev-only) - apps/pwa/e2e/global-setup.ts (mysql2/promise connection lines ~95-101; ON DUPLICATE KEY upsert lines ~125-129; NODE_ENV production guard lines ~34-44 — the "plain Node.js only" inline-hash constraint, Pitfall 11) - .planning/phases/19-local-auth-no-oidc-mode/19-RESEARCH.md §Break-Glass (script contract; tsx run via docker exec) + §Common Pitfalls 11 - .planning/phases/19-local-auth-no-oidc-mode/19-PATTERNS.md §apps/api/scripts/reset-admin.ts (DB connection, idempotent upsert, dev-only guard, --arg parsing, inline hashPassword) - .dockerignore (confirm apps/api/scripts/ is excluded — added in 19-01; this CLI relies on that exclusion for D-15) apps/api/scripts/reset-admin.ts Create apps/api/scripts/reset-admin.ts — a standalone script runnable as `docker exec -it familysync-api node --import=tsx/esm scripts/reset-admin.ts --username admin --password ''`. First statement: a dev-only guard that throws when `NODE_ENV === 'production'` (defense-in-depth; the script is also `.dockerignore`d per 19-01, IMG-02). Parse `--username` and `--password` from process.argv (no new deps). Connect via mysql2/promise using DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME env (same defaults as global-setup.ts). Inline a `hashPassword` (copy the 5-line scrypt PHC implementation — cannot import compiled TS from a plain script, Pitfall 11). Upsert: find-or-insert a users row for the username with `is_admin=true, claimed=true`; then INSERT ... ON DUPLICATE KEY UPDATE the local_credentials row (user_id, username, password_hash). Print the resulting user id. Support a `--dry-run` flag that validates args + connection without writing (used by the validation command). Never log the password value. cd apps/api && node --import=tsx/esm scripts/reset-admin.ts --dry-run --username smoketest --password ignored; echo "exit=$?" - The `--dry-run` invocation exits 0 and prints no password value (grep the output for the literal 'ignored' → absent) - Source assertion: the FIRST executable statement guards `NODE_ENV === 'production'` (throws) — D-13/D-15 - Source assertion: `grep -c "scryptSync" apps/api/scripts/reset-admin.ts` >= 1 (inline hash, no TS import) - Source assertion: `.dockerignore` excludes `apps/api/scripts/` (carried from 19-01) so this file never ships reset-admin.ts creates/resets a local admin by username, refuses to run in production, is excluded from the prod image, and supports --dry-run. Task 3: global-setup local_credentials seed + login.spec.ts + CI harness job env - apps/pwa/e2e/global-setup.ts (TRUNCATE block lines ~106-109; users seed lines ~125-129; member_credentials seed lines ~143-147; the inline-hash constraint Pitfall 11) - apps/pwa/e2e/layout.spec.ts + apps/pwa/e2e/calendar.spec.ts (spec structure, device-profile usage, DEV_AUTH_BYPASS auth-reached precondition, serviceWorkers block) - apps/pwa/playwright.config.ts (iphone/pixel/desktop projects; baseURL; webServer) - .gitea/workflows/ci.yml (the harness job: DEV_AUTH_BYPASS env, dev-stack bring-up, MariaDB seed step) - .planning/phases/19-local-auth-no-oidc-mode/19-RESEARCH.md §Dev-Bypass Rework (global-setup change + CI env) + §PWA Routing Gate - .planning/phases/19-local-auth-no-oidc-mode/19-UI-SPEC.md Surfaces 1-10 (selectors/copy the spec asserts: id="login-username", "Sign in", error copy) apps/pwa/e2e/global-setup.ts, apps/pwa/e2e/login.spec.ts, .gitea/workflows/ci.yml In apps/pwa/e2e/global-setup.ts: add `local_credentials` to the TRUNCATE set; inline a `hashPasswordInline(password)` (scrypt PHC, Pitfall 11 — global-setup is plain Node.js); after the existing users seed, `INSERT INTO local_credentials (user_id, username, password_hash) VALUES (1, 'devuser', ?) ON DUPLICATE KEY UPDATE password_hash = VALUES(password_hash)` with `hashPasswordInline('devpass')`. The existing NODE_ENV/ DEV_AUTH_BYPASS guards already cover the new seed.
Create apps/pwa/e2e/login.spec.ts: in a context that clears the `local-session` cookie (so the bypass-issued cookie does not auto-skip the gate), assert: (1) navigating to the app redirects to /login and the brand slot + username/password form render (id="login-username", "Sign in"); (2) a wrong password shows the single "Incorrect username or password." message; (3) logging in as devuser/devpass navigates into the app. Follow the existing spec structure (device profiles, serviceWorkers block, no-SW-controller precondition). Keep the other specs (layout/calendar/lists) reaching the app via the bypass-issued cookie unchanged.

In .gitea/workflows/ci.yml harness job: add `LOCAL_SESSION_SECRET` to the job env (a fixed dev value >=32 chars, e.g. a documented `dev-secret-change-me-0000000000000000` length-padded) so devSessionCookieMiddleware and global-setup's hash work; if the CI step seeds tables directly, add the `local_credentials` seed there too (mirroring the member_credentials seed). The harness still runs with DEV_AUTH_BYPASS=true; LOCAL_SESSION_SECRET stays dev-only (never in the published image — IMG gates).
pnpm --filter @familysync/pwa test:e2e --grep "login" - `pnpm --filter @familysync/pwa test:e2e --grep "login"` passes (real-login-form spec green on at least the desktop/chromium profile) - Source assertion: `grep -c "local_credentials" apps/pwa/e2e/global-setup.ts` >= 2 (TRUNCATE + INSERT) - Source assertion: `grep -c "LOCAL_SESSION_SECRET" .gitea/workflows/ci.yml` >= 1 in the harness job - Behavior: the existing layout/calendar/lists specs still reach the authed app (run `pnpm --filter @familysync/pwa test:e2e` — full harness green) global-setup seeds + truncates local_credentials; a real-login e2e spec passes; existing harness specs still reach the app via the bypass cookie; CI harness job has LOCAL_SESSION_SECRET + the seed. Task 4: Verify full harness + CI green and D-15 image boundary intact Run the full harness locally + push for the CI run, then pause for human confirmation that all specs and the CI harness job are green and no dev artifact ships. Blocking checkpoint — no code change; the executor presents results and waits for approval. The reworked dev-bypass (Option C) and CI harness. The full Playwright harness (both the new login spec and the unchanged layout/calendar/lists specs) is the automated proof. This checkpoint confirms the CI run is green end-to-end and that no dev artifact leaks into the published image — the D-15 boundary that the IMG-01/02/03 gates and the new .dockerignore exclusion enforce. 1. Run `pnpm --filter @familysync/pwa test:e2e` locally (host dev stack, DEV_AUTH_BYPASS=true, LOCAL_SESSION_SECRET set) → confirm all specs pass, including login.spec.ts and the unchanged layout/calendar/lists specs. 2. Push the branch and confirm the Gitea CI harness job is green (it brings up the dev stack with LOCAL_SESSION_SECRET + seeds local_credentials). 3. Confirm D-15: `.dockerignore` excludes `apps/api/scripts/` (reset-admin.ts) and `apps/pwa/e2e/` (the dev seed); the published image contains no local_credentials dev seed and no reset-admin script. Spot-check the publish.yml image-hygiene assertion still passes. Type "approved" if the full harness + CI are green and no dev artifact ships, or describe the failure.

<threat_model>

Trust Boundaries

Boundary Description
dev env → published image the D-15 boundary: dev seed, dev session secret, break-glass script must never ship
CI runner → dev stack DEV_AUTH_BYPASS + LOCAL_SESSION_SECRET are dev-only CI values, never production secrets

STRIDE Threat Register (ASVS L1, block on high)

Threat ID Category Component Disposition Mitigation Plan
T-19-23 Elevation of Privilege dev local_credentials seed in prod image mitigate seed lives only in global-setup.ts (apps/pwa/e2e/ — .dockerignore'd) and the CI step; never in a migration or startup code (RESEARCH Pitfall 7)
T-19-24 Elevation of Privilege devSessionCookieMiddleware active in prod mitigate production hard-guard is the FIRST check; assertNotDevBypassInProduction (IMG-01) blocks DEV_AUTH_BYPASS in prod
T-19-25 Tampering break-glass script shipped in image mitigate apps/api/scripts/ excluded in .dockerignore (19-01, IMG-02); NODE_ENV=production guard in the script
T-19-26 Information Disclosure break-glass password in logs mitigate reset-admin never logs the password value; --dry-run validates without writing
T-19-SC Tampering npm installs mitigate zero new packages this plan
</threat_model>
- `pnpm --filter @familysync/pwa test:e2e` full harness green (login + existing specs) - `pnpm --filter @familysync/api test` green (devBypass test intact) - Human checkpoint confirms CI green + D-15 boundary intact (no dev artifact in the image)

<success_criteria>

  • AUTH-LOCAL-16: harness reaches the authed PWA via the bypass-issued local-session cookie; a login spec tests the real form; CI updated
  • AUTH-LOCAL-11: break-glass CLI creates/resets a local admin, dev-only, image-excluded
  • D-15: no dev seed, dev secret, or break-glass script ships in the published image </success_criteria>

<artifacts_produced>

Artifacts this phase produces (Plan 05)

  • Middleware: devSessionCookieMiddleware (apps/api/src/auth/devBypass.ts) — Option C
  • Script: apps/api/scripts/reset-admin.ts (break-glass CLI, dev-only, .dockerignore'd)
  • e2e: apps/pwa/e2e/login.spec.ts (real-login-form spec)
  • global-setup.ts: local_credentials dev seed (devuser/devpass) + TRUNCATE
  • ci.yml: LOCAL_SESSION_SECRET in the harness job + local_credentials seed </artifacts_produced>
Create `.planning/phases/19-local-auth-no-oidc-mode/19-05-SUMMARY.md` when done