diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 0f2ec49..b8866e4 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -34,7 +34,7 @@ Each requirement maps to exactly one roadmap phase (see Traceability). - [ ] **SETUP-01**: On first run (no admin/credentials configured), the operator is guided through a setup wizard to define bootstrap configuration (app/external URL, OIDC client, session secret, encryption key, VAPID keypair, MariaDB connection, first member's Fastmail app password) instead of hand-editing `.env` / `docker-compose.yml`. - [ ] **SETUP-02**: The wizard **validates each input before completing** — DB connectivity test, VAPID private key decodes to 32 bytes and pairs with the public key, OIDC discovery resolves, and the Fastmail app password reaches CalDAV (PROPFIND). -- [ ] **SETUP-03**: The wizard generates secrets (session secret, encryption key, VAPID keypair) for the operator to copy into env; secrets are **never written to the database or returned in a response body**. +- [x] **SETUP-03**: The wizard generates secrets (session secret, encryption key, VAPID keypair) for the operator to copy into env; secrets are **never written to the database or returned in a response body**. - [ ] **SETUP-04**: Once setup is complete, the setup endpoints are no longer accessible (guard checked on every invocation, not only at startup). ### CI — Gitea continuous integration @@ -86,7 +86,7 @@ Maps each REQ-ID to its phase. v1.1 phases continue v1.0 numbering (v1.0 ended a | NOTIF-06 | Phase 11 (Per-Event Reminders) | Complete | | SETUP-01 | Phase 12 (Initial Setup Wizard) | Pending | | SETUP-02 | Phase 12 (Initial Setup Wizard) | Pending | -| SETUP-03 | Phase 12 (Initial Setup Wizard) | Pending | +| SETUP-03 | Phase 12 (Initial Setup Wizard) | Complete | | SETUP-04 | Phase 12 (Initial Setup Wizard) | Pending | **DB foundation note:** The v1.1 schema migration (`users.is_admin`, `calendar_events.reminder_lead_minutes`, `app_config` table) is not a standalone requirement — it is carried by **Phase 10 (Admin Role & Settings)** (which owns is_admin + app_config) and consumed by **Phase 11 (Per-Event Reminders)** (reminder_lead_minutes) and **Phase 12 (Initial Setup Wizard)** (app_config.setup_complete). Folded per ARCHITECTURE.md ordering rather than created as a migration-only phase. This makes Phase 10 the head of the admin chain (10 → 11, 10 → 12). diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8304cd5..4eb7374 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -254,7 +254,7 @@ Plans: Plans: **Wave 1** -- [ ] 12-01-PLAN.md — Schema migration (nullable OIDC + claimed) + generate-secrets helper (SETUP-03) + Wave-0 scaffolds +- [x] 12-01-PLAN.md — Schema migration (nullable OIDC + claimed) + generate-secrets helper (SETUP-03) + Wave-0 scaffolds **Wave 2** *(blocked on Wave 1 completion)* @@ -424,7 +424,7 @@ At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx` | 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 | | 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 | | 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 | -| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - | +| 12. Initial Setup Wizard | v1.1 | 1/4 | In Progress| | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | | 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 | diff --git a/.planning/STATE.md b/.planning/STATE.md index 30f5847..4c69dfc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish status: executing -stopped_at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next -last_updated: "2026-06-15T17:31:15.247Z" -last_activity: 2026-06-15 -- Phase 12 execution started +stopped_at: Phase 12 Plan 01 complete — schema + secrets + stubs + Wave-0 tests done; plan 2 of 4 is next +last_updated: "2026-06-15T17:46:33.294Z" +last_activity: 2026-06-15 progress: total_phases: 24 completed_phases: 10 total_plans: 40 - completed_plans: 36 + completed_plans: 37 percent: 42 --- @@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position Phase: 12 (initial-setup-wizard) — EXECUTING -Plan: 1 of 4 -Status: Executing Phase 12 -Last activity: 2026-06-15 -- Phase 12 execution started +Plan: 2 of 4 +Status: Ready to execute +Last activity: 2026-06-15 ### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action) @@ -111,6 +111,7 @@ _Updated after each plan completion_ | Phase 18 P02 | 3 | 2 tasks | 2 files | | Phase 18 P03 | 28 | 2 tasks | 4 files | | Phase 18 P04 | 15 | 3 tasks | 3 files | +| Phase 12 P01 | 8 | 4 tasks | 10 files | ## Accumulated Context @@ -119,6 +120,9 @@ _Updated after each plan completion_ Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work: +- D-07-CJS-IMPORT (2026-06-15, 12-01): web-push is CJS — ESM scripts must use default import then destructure (`import webpush from '...'; const { generateVAPIDKeys } = webpush`). Named ESM export form fails at Node 22 (SyntaxError). +- D-07-BACKFILL (2026-06-15, 12-01): 0002 migration appends `UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL` — prevents first-login-claims (D-08) matching pre-existing OIDC users. +- D-07-NULL-UNIQUE (2026-06-15, 12-01): kept uniq_oidc_identity unchanged — MariaDB NULL+NULL pairs are DISTINCT in unique indexes, correctly allowing multiple unclaimed wizard rows. - D-13-ESLint-PIN (2026-06-11, 13-01): eslint pinned to 9.39.4 — ESLint 10 breaks eslint-plugin-react@7.37.5 at runtime ("getFilename is not a function", jsx-eslint#3977). Unpin when plugin releases ESLint 10 support. - D-13-JSX-SCOPE (2026-06-11, 13-01): react/react-in-jsx-scope disabled explicitly — flat.recommended enables it at error; PWA uses jsx:react-jsx (React 19 automatic transform), React import not required in JSX files. - D-PROBE-01 (2026-06-11, 08-01): runs-on must be ubuntu-latest — runner has no self-hosted label; all downstream ci.yml workflows use ubuntu-latest. @@ -257,7 +261,7 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-15T02:46:09.800Z +Last session: 2026-06-15T17:46:33.276Z Stopped at: Phase 18 Plan 03 complete — broker rewire done; plan 4 of 4 is next Resume file: None diff --git a/.planning/phases/12-initial-setup-wizard/12-01-SUMMARY.md b/.planning/phases/12-initial-setup-wizard/12-01-SUMMARY.md new file mode 100644 index 0000000..e1986af --- /dev/null +++ b/.planning/phases/12-initial-setup-wizard/12-01-SUMMARY.md @@ -0,0 +1,149 @@ +--- +phase: 12-initial-setup-wizard +plan: 01 +subsystem: database, api, testing +tags: [drizzle, mariadb, migration, web-push, vapid, vitest, hono] + +# Dependency graph +requires: + - phase: 10-admin-role-settings + provides: app_config table, users.is_admin, member_credentials table — consumed by Phase 12 schema changes +provides: + - users.claimed column (boolean, default false NOT NULL) — distinguishes unclaimed wizard rows from OIDC-bound rows + - users.oidc_iss / users.oidc_sub now nullable — wizard creates local rows before OIDC identity is known + - 0002_lethal_millenium_guard.sql migration — applied to dev DB with backfill UPDATE + - scripts/generate-secrets.mjs — generates SESSION_SECRET, APP_PASSWORD_ENCRYPTION_KEY, VAPID keypair to stdout + - apps/api/src/lib/setupGuard.ts — isSetupLocked() stub (real impl in Plan 02) + - apps/api/src/routes/setup.ts — setupRouter stub Hono router (handlers in Plan 02) + - apps/api/tests/routes/setup.test.ts — Wave-0 RED scaffolds for SETUP-01..04 + 423 guard + - apps/api/tests/auth/user.test.ts — D-08 first-login-claims RED scaffold +affects: + - 12-02-setup-routes (consumes setupGuard + setupRouter stubs, schema claimed column) + - 12-03-pwa-setup-page (consumes /api/setup/* routes) + - 12-04-integration (consumes full setup flow) + +# Tech tracking +tech-stack: + added: [] # No new packages installed (RESEARCH §No New Packages — web-push already present) + patterns: + - drizzle-kit generate+migrate workflow for schema changes (NEVER drizzle-kit push — D-Task5-DDL) + - CommonJS default-import pattern for ESM scripts consuming CJS packages (web-push) + - it.todo() Wave-0 scaffold pattern — RED tests exist before happy path is built + - isSetupLocked() per-call freshness contract (D-10 — never module-cache) + +key-files: + created: + - apps/api/src/db/migrations/0002_lethal_millenium_guard.sql + - apps/api/src/db/migrations/meta/0002_snapshot.json + - scripts/generate-secrets.mjs + - apps/api/src/lib/setupGuard.ts + - apps/api/src/routes/setup.ts + - apps/api/tests/routes/setup.test.ts + modified: + - apps/api/src/db/schema.ts + - apps/api/src/db/migrations/meta/_journal.json + - apps/api/tests/auth/user.test.ts + - package.json + +key-decisions: + - "D-07-CJS-IMPORT: web-push is CJS — ESM scripts must use default import then destructure (import webpush from '...'; const { generateVAPIDKeys } = webpush)" + - "D-07-BACKFILL: 0002 migration appends UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL to prevent first-login-claims (D-08) matching pre-existing OIDC users" + - "D-07-NULL-UNIQUE: MariaDB treats multiple NULL+NULL pairs as DISTINCT in unique indexes — uniq_oidc_identity constraint kept unchanged; multiple unclaimed rows correctly allowed" + +patterns-established: + - "Wave-0 scaffold: create it.todo() tests BEFORE implementing routes — ensures RED gate exists for SETUP-04 423 guard (Pitfall 8)" + - "generate-secrets: stdout-only secret generation — SC-3 compliance checked via grep acceptance gate" + +requirements-completed: [SETUP-03] + +# Metrics +duration: 8min +completed: 2026-06-15 +--- + +# Phase 12 Plan 01: Foundation Summary + +**Schema migration making OIDC identity nullable + claimed marker applied to dev DB; stdout-only secret generator for VAPID keypair; Wave-0 stub router + RED test scaffolds for all four SETUP requirements** + +## Performance + +- **Duration:** 8 min +- **Started:** 2026-06-15T17:37:13Z +- **Completed:** 2026-06-15T17:44:48Z +- **Tasks:** 4 +- **Files modified:** 10 + +## Accomplishments + +- Applied Drizzle migration 0002 to dev DB: oidcIss/oidcSub now nullable, claimed column added, existing OIDC users backfilled claimed=true +- Created `scripts/generate-secrets.mjs` satisfying SETUP-03: prints SESSION_SECRET (64 hex), APP_PASSWORD_ENCRYPTION_KEY (64 hex), VAPID_PUBLIC_KEY (~87 b64url), VAPID_PRIVATE_KEY (~43 b64url) to stdout only — never to disk or DB +- Created Wave-0 import targets: `setupGuard.ts` (isSetupLocked stub) and `setup.ts` (empty setupRouter) so Plan 02 imports compile from day one +- Created 20 RED it.todo() scaffolds in setup.test.ts (SETUP-01..04 + 423 guard + D-10 effective-config) and user.test.ts (D-08 first-login-claims) — suite collects at 375 passed | 20 todo + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Schema nullable OIDC identity + claimed marker + 0002 migration** - `703fad2` (feat) +2. **Task 2: generate-secrets repo helper** - `2d6dc14` (feat) +3. **Task 3: Stub setupGuard.ts + setup.ts router** - `11e8102` (feat) +4. **Task 4: Wave-0 test scaffolds** - `e098be3` (test) + +## Files Created/Modified + +- `apps/api/src/db/schema.ts` — users.oidcIss/oidcSub made nullable; claimed boolean added; Phase 12 app_config keys documented with prohibition comment (D-01/SC-3) +- `apps/api/src/db/migrations/0002_lethal_millenium_guard.sql` — MODIFY COLUMN for nullable + ADD COLUMN claimed + backfill UPDATE +- `apps/api/src/db/migrations/meta/_journal.json` — 0002 entry added +- `apps/api/src/db/migrations/meta/0002_snapshot.json` — Drizzle snapshot for 0002 +- `scripts/generate-secrets.mjs` — Bootstrap secret generator (SETUP-03 / D-05) +- `package.json` — root "generate-secrets" script added +- `apps/api/src/lib/setupGuard.ts` — isSetupLocked() stub (returns false; real impl Plan 02) +- `apps/api/src/routes/setup.ts` — setupRouter = new Hono() stub (empty; handlers Plan 02) +- `apps/api/tests/routes/setup.test.ts` — 15 it.todo() Wave-0 RED scaffolds +- `apps/api/tests/auth/user.test.ts` — 5 it.todo() D-08 first-login-claims scaffolds added + +## Decisions Made + +- **D-07-CJS-IMPORT:** web-push is a CommonJS module — ESM scripts must use `import webpush from '...'` then destructure. Named ESM export form fails at Node 22 (`SyntaxError: Named export 'generateVAPIDKeys' not found`). Fixed inline as Rule 1 bug. +- **D-07-BACKFILL:** Appended `UPDATE users SET claimed=true WHERE oidc_iss IS NOT NULL` to the generated migration SQL so existing OIDC users are pre-marked claimed, preventing the Plan 02 first-login-claims query (D-08) from matching them. +- **D-07-NULL-UNIQUE:** Kept `uniq_oidc_identity` unique constraint on (oidcIss, oidcSub) unchanged — MariaDB treats NULL+NULL pairs as DISTINCT in unique indexes (ISO SQL semantics), allowing multiple unclaimed wizard rows with NULL oidc_iss. No structural change needed (RESEARCH Pitfall 9 awareness). + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] web-push CommonJS ESM named-import failure** +- **Found during:** Task 2 (generate-secrets.mjs execution) +- **Issue:** `import { generateVAPIDKeys } from 'web-push/src/index.js'` throws `SyntaxError: Named export 'generateVAPIDKeys' not found` — web-push is CommonJS and Node 22 ESM loader does not auto-export CJS named exports +- **Fix:** Changed to `import webpush from '.../web-push/src/index.js'; const { generateVAPIDKeys } = webpush;` +- **Files modified:** scripts/generate-secrets.mjs +- **Verification:** `node scripts/generate-secrets.mjs` prints all four correctly-shaped values +- **Committed in:** `2d6dc14` (Task 2 commit) + +--- + +**Total deviations:** 1 auto-fixed (Rule 1 bug — CJS import form) +**Impact on plan:** Essential for generate-secrets to run. No scope creep. + +## Issues Encountered + +- `drizzle-kit migrate` requires DB env vars — ran with `set -a; source .env; set +a; DB_HOST=127.0.0.1 pnpm exec drizzle-kit migrate`. The dev DB hostname in .env is `mariadb` (Docker internal); overriding to `127.0.0.1` is the standard host-side dev pattern. +- `pnpm test -- setup` (filter by name) triggered globalSetup which needs root DB credentials; acceptance criterion verified instead via full suite run with `DB_HOST=127.0.0.1` showing 375 passed | 20 todo with no import errors. + +## Threat Surface Scan + +No new network endpoints introduced in this plan. The schema migration is additive (ALTER + ADD, no DROP/recreate). Threat mitigations T-12-01, T-12-02, T-12-03 all verified: +- T-12-01: generate-secrets.mjs contains no writeFile/appendFile/fetch/db (grep-checked) +- T-12-02: 0002 migration uses MODIFY COLUMN (not DROP/recreate); backfill verified +- T-12-03: prohibition comment in schema.ts for vapid_private_key / app_password_encryption_key + +## Next Phase Readiness + +- Plan 02 (setup routes) can import `isSetupLocked` from setupGuard.ts and extend `setupRouter` in setup.ts — both exist as valid TypeScript import targets +- Plan 02 can also rely on `users.claimed` and nullable `oidcIss`/`oidcSub` being present in the dev DB +- 20 RED it.todo() tests are waiting for Plan 02 and Plan 03 implementations to turn them GREEN +- SETUP-03 (generate-secrets) is fully satisfied by this plan + +--- +*Phase: 12-initial-setup-wizard* +*Completed: 2026-06-15*