Commit Graph
1078 Commits
Author SHA1 Message Date
Lucas BergerandClaude Opus 4.8 5eef074a57 test(12): re-verify UAT after gap-closure — 6 passed, 1 env-blocked, all 6 gaps confirmed closed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:04:22 -04:00
Lucas Berger 6409c9c3c2 chore: merge executor worktree (worktree-agent-ab0c78658da0b8f33) 2026-06-15 21:33:30 -04:00
Lucas Berger eed76de37f docs(12-05): complete Instance-step gap-closure plan (gaps 1, 3, 4) 2026-06-15 21:32:53 -04:00
Lucas Berger a13fc11556 feat(12-05): preserve Instance fields across Back navigation (gap 4)
- Lift appUrl/oidcIssuer/oidcClientId/vapidPublicKey into SetupPage so Step2 unmount preserves them
- Step2Config now reads/writes these via fields/setFields props
- Fastmail app password stays in Step3 local state, never lifted/persisted, cleared on unmount (T-12-15)
- Tests: Back from Calendar restores all four Instance values; password not persisted across nav
2026-06-15 21:32:06 -04:00
Lucas Berger 35db5c57e6 feat(12-05): drop DB-vs-env aside, add read-only DB-name field (gaps 1, 3)
- Remove the 'written to the database — not your environment file' aside from the Instance step intro
- Render a read-only, disabled DB-name field under App URL, populated from GET /api/setup/status dbName
- Helper text explains DB is configured via Docker env; only dbName is surfaced (T-12-3DB)
- Tests: assert aside absent, DB field readOnly/disabled with mocked dbName, existing DB validation row intact
2026-06-15 21:31:26 -04:00
Lucas Berger 846ae17182 chore: merge executor worktree (worktree-agent-ad2593f5ac87f6852) 2026-06-15 21:25:57 -04:00
Lucas Berger 7c94558de4 docs(12-07): append self-check result to SUMMARY 2026-06-15 21:25:03 -04:00
Lucas Berger 96c49138cb docs(12-07): complete UAT gap-closure plan (gaps 5 & 6 — /setup reverse-gate + ['me'] freshness) 2026-06-15 21:24:42 -04:00
Lucas Berger 2b3569ff20 fix(12-07): make ['me'] fresh on shell entry so post-wizard banner clears (gap 6)
- Root cause confirmed = mechanism (ii): ['me'] staleness, NOT a backend linking gap
  (upsertUser claim preserves users.id → credential stays linked → DB needsProviderSetup=false)
- SetupBanner ['me'] query staleTime 5min → 0 so a pre-claim stale cache entry is
  refetched on mount; banner hides once needsProviderSetup resolves false
- App.tsx boot ['me'] staleTime also set to 0 (committed with Task 1) for the same reason
- Add SetupBanner.test.tsx regression: absent when false, present (no dismiss) when true,
  stale-cache refetch hides banner; success-only dismissal contract preserved (no X button)
- Log pre-existing PWA lint errors (SetupPage.test.tsx, setupClient.contract.test.ts) to deferred-items.md
2026-06-15 21:23:46 -04:00
Lucas Berger fdcb4dc442 feat(12-07): gate /setup route on setupComplete (gap 5)
- Reverse-gate the /setup route: setupComplete===true → SetupPage alreadyLocked
  (Surface 8 'Setup already complete'); loading → no-flash placeholder; else wizard
- Add App.test.tsx reverse-gate tests (already-complete surface + active wizard on /setup)
- SetupPage mock now respects the alreadyLocked prop
2026-06-15 21:21:29 -04:00
Lucas Berger 67c17a58eb docs(12-06): complete setup-route gap-closure plan (VAPID equality + DB name) 2026-06-15 21:15:18 -04:00
Lucas Berger fbd3b77bde feat(12-06): expose non-secret DB name via GET /api/setup/status (gap 3)
- status returns { setupComplete, dbName } from process.env.DB_NAME (null fallback)
- only the DB name; never DB_HOST/DB_USER/DB_PASSWORD
- SetupStatusResponse carries dbName?: string | null for the PWA read-only field
2026-06-15 21:13:47 -04:00
Lucas Berger e46e80a15c feat(12-06): validate/vapid asserts submitted key matches env public key (gap 2)
- read app_config.vapid_public_key and compare to process.env.VAPID_PUBLIC_KEY
- mismatched/absent submitted key → 400 before the structural check
- VAPID_PRIVATE_KEY still env-only, never compared or returned (T-12-06)
2026-06-15 21:12:35 -04:00
Lucas Berger e9d07b38fb test(12-06): add failing tests for vapid public-key equality assertion (gap 2)
- mismatched submitted key (BH123) → 400, no VAPID_PRIVATE_KEY leak
- absent app_config.vapid_public_key row → 400
- happy path seeds matching app_config row
2026-06-15 21:11:41 -04:00
Lucas Berger f0fb31348d docs(12): gap-closure plans 05-07 for 6 UAT gaps 2026-06-15 21:05:45 -04:00
Lucas Berger dc7f8d2aa9 test(12): complete UAT - 0 passed, 6 issues across 3 tests 2026-06-15 21:00:31 -04:00
Lucas BergerandClaude Opus 4.8 df93f4fe95 docs(12): code review clean after --fix --auto (10 findings fixed across 3 iterations)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:50:36 -04:00
Lucas BergerandClaude Sonnet 4.6 687f9dc9fa fix(12): WR-01 narrow TOCTOU guard and set claimed=true for OIDC inserts
- apps/api/src/auth/user.ts: upsertUser step-5 insert now sets claimed=true
  for all OIDC-created users. An identity-bound OIDC user is never a pending
  wizard bootstrap user; explicit claimed=true prevents ambiguity with the
  (oidcIss IS NULL AND claimed=false) sentinel used by the TOCTOU guard and
  isSetupLocked. First-login-claims path is unaffected (it updates a
  pre-existing oidcIss=null row; this change only touches the fresh insert).

- apps/api/src/routes/setup.ts: TOCTOU guard in POST /credential now queries
  WHERE oidc_iss IS NULL AND claimed = false FOR UPDATE, matching the exact
  definition of a pending wizard bootstrap user. This provides defense-in-depth
  against any future path that could produce claimed=false OIDC rows.

- apps/api/tests/auth/user.test.ts: new WR-01 test asserts that the fresh
  OIDC insert sets claimed=true in the values passed to db.insert().

- apps/api/tests/routes/setup.test.ts: new WR-01 integration test seeds an
  OIDC user with claimed=false (oidcIss NOT NULL) and verifies POST /credential
  still succeeds (guard ignores the OIDC row, only counts local wizard rows).

All 402 API tests, 253 PWA tests, and typecheck pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:46:41 -04:00
Lucas BergerandClaude Sonnet 4.6 22d1581484 fix(12): IN-01 require https:// on appExternalUrl in configSchema
appExternalUrl is injected as OIDC_AUTH_EXTERNAL_URL (the redirect URI
base); Authelia rejects non-https redirect URIs in production. Added
.refine() guard matching the existing oidcIssuer pattern. Added test
that verifies http:// appExternalUrl is rejected with 400.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:37:03 -04:00
Lucas BergerandClaude Sonnet 4.6 61a869ca7d fix(12): CR-01 guard effective-config branch during wizard in-progress
isSetupLocked() now checks for an unclaimed local wizard user
(oidcIss IS NULL, claimed=false) before firing the effective-config
branch. During the credential→complete window, this sentinel prevents
a production container with VAPID env set from blocking POST /complete
with 423. The explicit setup_complete flag (Check 1) still locks
unconditionally once written. Adds regression test that sets VAPID env
explicitly (no beforeEach clearing) to reproduce the production scenario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:36:52 -04:00
Lucas BergerandClaude Sonnet 4.6 ed4e64a06a fix(12): WR-02 fix TypeScript type annotation for execute() count result
Drizzle mysql2 execute() returns [rows, fields] for SELECT queries; the
generic type parameter alone does not correctly type the result. Use
unknown cast pattern consistent with admin.ts to access the count row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:25:05 -04:00
Lucas BergerandClaude Sonnet 4.6 c86cff5dad fix(12): IN-04 trim email before sending to /api/setup/credential
email.trim() was already used in the saveDisabled guard but not applied to
the mutate call payload. A non-empty value with leading/trailing spaces would
pass the guard and reach the server untrimmed, causing Zod's z.string().email()
to reject it with a generic 400 and no diagnostic path for the user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:22:47 -04:00
Lucas BergerandClaude Sonnet 4.6 b0b5bceaed fix(12): IN-03 replace private web-push source import with Node.js built-in crypto
generate-secrets.mjs was importing from ../apps/api/node_modules/web-push/src/index.js
(a private source path) which breaks if web-push restructures internally or
workspace hoisting moves the package. Replace with Node.js built-in createECDH
('prime256v1') which produces identical base64url-encoded keys, including the
same defensive padding logic as web-push for short key buffers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:22:07 -04:00
Lucas BergerandClaude Sonnet 4.6 3babbfa20e fix(12): IN-02 guard /setup/complete against skipping the credential step
Without a prerequisite check, an operator could call POST /api/setup/complete
directly, setting setup_complete=true with no admin user or credential row,
leaving no recovery path without manual DB surgery.

Add an inner join check for an unclaimed user with an associated credential;
return 422 if absent. Update /complete tests to seed the prerequisite for
the success path and add an explicit 422 regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:20:55 -04:00
Lucas BergerandClaude Sonnet 4.6 d9dfe72aab fix(12): IN-01 return generic error from /validate/oidc instead of raw network detail
The catch block previously echoed err.message (which may contain internal
network addresses like ECONNREFUSED 192.168.1.50:9091) to the pre-auth
caller. Log the raw message server-side only and return a generic user-
facing string with no internal network detail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:18:10 -04:00
Lucas BergerandClaude Sonnet 4.6 7a512a9726 fix(12): WR-03 document and log single-write process.env semantics in oidcFallback
The oidcConfigFallbackMiddleware permanently mutates process.env on first
request then never re-reads from DB. Log an explicit info message when each
value is written so operators can see when a restart is required to pick up
config changes, and add inline documentation of the single-write semantics
to prevent silent misconfiguration after a re-run of the wizard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:17:41 -04:00
Lucas BergerandClaude Sonnet 4.6 a4e0ea4f14 fix(12): WR-02 serialise concurrent /credential calls with FOR UPDATE transaction
Two concurrent POST /api/setup/credential requests could both pass
isSetupLocked(), observe no unclaimed row, and both insert — leaving two
unclaimed admin rows with no recovery path. Wrap the count-check + user
insert in a transaction with SELECT COUNT(*) ... FOR UPDATE to acquire a
row/gap lock, ensuring at most one unclaimed admin row is created.
Returns 409 when a concurrent request already holds an unclaimed row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:17:12 -04:00
Lucas BergerandClaude Sonnet 4.6 3bc38bf1a6 fix(12): WR-01 delete orphaned user row on re-select 503 path in /credential
After $returningId() insert, if the re-select returns null the handler
returned 503 without deleting the just-inserted user row, leaving an
unclaimed admin row with no credential. Delete before returning 503 to
mirror the cleanup already present in the catch block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:14:39 -04:00
Lucas BergerandClaude Sonnet 4.6 066b69f2be fix(12): CR-01 remove extraneous providerType from postSetupCredential wire body
The server's credentialSchema does not declare providerType; it was being
silently stripped by Zod. Remove it from the request body and add a
contract test suite asserting the exact wire keys sent, mirroring the
existing BUG-1 tests for postSetupConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:14:18 -04:00
Lucas Berger ef3e9810a9 docs(12): add code review report 2026-06-15 16:11:50 -04:00
Lucas Berger 836cb38934 docs(phase-12): add security threat verification — 15/15 closed, 0 open 2026-06-15 16:05:14 -04:00
Lucas BergerandClaude Opus 4.8 a07bb5bc67 fix(12): keep phase checkbox open — verification is human_needed, completion via verify-work
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:22:46 -04:00
Lucas BergerandClaude Opus 4.8 8db5b236c4 test(12): persist human verification items as UAT
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:22:18 -04:00
Lucas BergerandClaude Opus 4.8 4f81ccdbfd docs(12): verification passed (9/9) after VAPID gap closure — one human Fastmail UAT remains
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:22:00 -04:00
Lucas Berger 49b82d2ef4 docs(12-04): update SUMMARY + STATE + ROADMAP for CR-01 VAPID gap closure 2026-06-15 15:16:41 -04:00
Lucas BergerandClaude Sonnet 4.6 0d53249b02 fix(12-04): wire validateSetupVapid into setup wizard (close CR-01 / SETUP-02 gap)
- Import validateSetupVapid from api/client.ts in SetupPage.tsx
- Add vapid: ValidationRowState to validationRows state (alongside db/oidc)
- Extend configMutation.onSuccess chain: DB → OIDC → VAPID (sequential)
- Add ValidationRow for VAPID with pending/success/failure text
- Gate setBothPassed(true) on all three rows passing (db AND oidc AND vapid)
- Update anyPending and handleSaveAndValidate reset to include vapid state
- All 249 PWA tests pass; TypeScript clean

Closes CR-01; satisfies SETUP-02 "VAPID private key decodes to 32 bytes"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 15:13:40 -04:00
Lucas BergerandClaude Sonnet 4.6 7d0205df05 test(12-04): RED — wizard must validate VAPID before proceeding (SETUP-02 gap)
- Add 4 VAPID validation tests to SetupPage.test.tsx (CR-01 gap closure)
- Tests assert: validateSetupVapid is called, VAPID row renders, Continue
  is blocked when VAPID fails, Continue appears only after all 3 pass
- 3 tests currently FAIL (RED) — current code lacks validateSetupVapid import
  and has no vapid ValidationRow or vapid gate on bothPassed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 15:12:03 -04:00
Lucas BergerandClaude Opus 4.8 523742c489 fix(12): revert premature phase-complete checkbox — verification pending
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:03:25 -04:00
Lucas BergerandClaude Opus 4.8 d8b4c98592 docs(12): code review — 1 critical, 5 warnings, 4 info
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:02:27 -04:00
Lucas BergerandClaude Opus 4.8 d96dfae17b chore(12-04): drop committed verification screenshot from planning dir
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:54:19 -04:00
Lucas Berger 7a1801f47d docs(12-04): complete setup-wizard PWA plan — BUG 1+2 fixed, playwright verified
Update SUMMARY.md with bug fix details, TDD gate compliance, and playwright-cli
verification confirming /api/setup/config returns 200 with camelCase payload.
Update STATE.md session info. Phase 12 all 4 plans complete.
2026-06-15 14:52:17 -04:00
Lucas Berger 120ce85a59 fix(12-04): align setup config client to camelCase API contract + readable errors
BUG 1: Rename SetupConfigPayload fields from snake_case to camelCase to match
the API configSchema (appExternalUrl, oidcIssuer, oidcClientId, vapidPublicKey).
Update SetupPage.tsx handleSaveAndValidate to send the correct camelCase keys.

BUG 2: Extract human-readable message from ZodError object in postSetupConfig
error handler. When body.error is an object with issues[], use issues[0].message
instead of stringifying the object (which produces "[object Object]").

All 245 PWA tests pass; TypeScript clean.
2026-06-15 14:48:07 -04:00
Lucas Berger 9f20c8b7cc test(12-04): RED regression for setup /config payload contract + error rendering
- Add setupClient.contract.test.ts with 9 tests targeting two bugs:
  BUG 1: SetupConfigPayload interface must use camelCase keys matching
    the API configSchema (appExternalUrl, oidcIssuer, oidcClientId,
    vapidPublicKey) — TypeScript compile error confirms mismatch
  BUG 2: postSetupConfig must throw readable string when API returns
    ZodError object in error field (not [object Object])
- Tests 7-8 fail (BUG 2 confirmed); TypeScript errors confirm BUG 1
2026-06-15 14:47:13 -04:00
Lucas Berger 1d8eed309b docs(12-04): complete setup-wizard PWA plan — paused at human-verify checkpoint 2026-06-15 14:36:20 -04:00
Lucas Berger 1587bca9a0 feat(12-04): App.tsx setup-status gate + /setup route + redirect
- Add setupQuery (queryKey: setupStatus, staleTime: 0) alongside meQuery;
  queries GET /api/setup/status via fetchSetupStatus on every app load
- Add <Route path="/setup" element={<SetupPage />}> as standalone pre-auth route
- Add redirect gate: while loading → aria-hidden div (no flash); setupComplete===false
  → <Navigate to="/setup"> (no AppNav/BottomTabBar rendered); true → normal shell
- Add App.test.tsx covering both branches (setupComplete false/true) + loading state;
  236 tests pass, typecheck clean
2026-06-15 14:31:03 -04:00
Lucas Berger 62d80f6c46 feat(12-04): setup API client functions + SetupPage wizard component
- Add 7 setup functions to client.ts: fetchSetupStatus, postSetupConfig,
  validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential,
  postSetupComplete; plus SetupAlreadyLockedError for 423 handling
- Add SetupPage.tsx: standalone 4-step wizard (Welcome → Instance Configuration
  → Calendar Credential → Terminal/Locked) with Surface 2 step indicator,
  Surface 5 validation rows, Surface 6 action row, Surface 7 terminal screen,
  Surface 8 already-locked screen; role=main, aria-live, no nav shell
- No dangerouslySetInnerHTML; no AppNav/BottomTabBar imports
- All 230 pwa tests pass; typecheck clean; build green
2026-06-15 14:27:25 -04:00
Lucas Berger eb84e6e8e2 test(12-04): add failing tests for SetupPage wizard and setup API client (RED)
- Tests for all 7 setup client function exports (fetchSetupStatus, postSetupConfig,
  validateSetupDb, validateSetupOidc, validateSetupVapid, postSetupCredential, postSetupComplete)
- Tests for SetupPage rendering: page title, Welcome step heading, 4-step indicator,
  Continue button, role=main, aria-live region
- Tests for no AppNav/BottomTabBar in wizard output
- Tests for Already Locked screen (alreadyLocked prop) and Sign-in link
2026-06-15 14:22:19 -04:00
Lucas BergerandClaude Opus 4.8 ee21611607 docs(12-03): record Self-Check PASSED in summary
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:17:41 -04:00
Lucas Berger a36f9ddb78 docs(12-03): complete first-login-claims plan summary 2026-06-15 14:16:59 -04:00
Lucas BergerandClaude Sonnet 4.6 c8894adc3f feat(12-03): upsertUser first-login-claims branch (D-08)
- Add isNull import from drizzle-orm; add appConfig to schema imports
- After identity lookup, read app_config.setup_complete per call (D-10 freshness)
- When setup_complete='true' and unclaimed user exists (isNull(oidcIss) AND claimed=false):
  claim it via db.update() — binds oidcIss/oidcSub, sets claimed=true, preserves is_admin
- shouldBeAdmin gated: flagRow?.value !== 'true' AND admin COUNT === 0 (T-12-11)
- No email keying in claim branch — isNull(oidcIss) AND claimed=false only (D-10/T-12-12)
- 399 tests pass; typecheck clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 14:14:48 -04:00