style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
@@ -17,32 +17,33 @@ must_haves:
truths:
- "Unauthenticated cold load shows a single neutral 'Signing you in' splash — no calendar shell, skeleton, or 'Sign-in required' flash before Authelia (D-10, success criterion 5)"
- "A session that expires mid-use (401 / opaqueredirect from ANY query or mutation) shows a 'Session expired' interstitial and cleanly redirects to /api/login instead of hanging (D-11, success criterion 4)"
- "Every PWA fetch wrapper detects 401/opaqueredirect and throws a typed SessionExpiredError (D-11)"
- 'Every PWA fetch wrapper detects 401/opaqueredirect and throws a typed SessionExpiredError (D-11)'
artifacts:
- path: "apps/pwa/src/api/client.ts"
- path: 'apps/pwa/src/api/client.ts'
provides: "SessionExpiredError class + consistent redirect:'manual' + handleAuthResponse across all fetch wrappers; recurrenceUntil/recurrenceCount on CreateEventPayload; hasRrule on CalendarOccurrence"
contains: "class SessionExpiredError"
- path: "apps/pwa/src/components/AuthSplash.tsx"
provides: "full-screen neutral auth interstitial (loading / redirecting / dead-end states)"
contains: "AuthSplash"
- path: "apps/pwa/src/main.tsx"
provides: "QueryClient wired with QueryCache+MutationCache onError that arms the session-expiry interstitial"
contains: "MutationCache"
contains: 'class SessionExpiredError'
- path: 'apps/pwa/src/components/AuthSplash.tsx'
provides: 'full-screen neutral auth interstitial (loading / redirecting / dead-end states)'
contains: 'AuthSplash'
- path: 'apps/pwa/src/main.tsx'
provides: 'QueryClient wired with QueryCache+MutationCache onError that arms the session-expiry interstitial'
contains: 'MutationCache'
key_links:
- from: "apps/pwa/src/main.tsx"
to: "apps/pwa/src/store/calendarStore.ts"
via: "QueryCache/MutationCache onError → setSessionExpired(true) on SessionExpiredError"
pattern: "SessionExpiredError"
- from: "apps/pwa/src/components/CalendarShell.tsx"
to: "apps/pwa/src/components/AuthSplash.tsx"
via: "meQuery.isLoading/isError and sessionExpired flag render AuthSplash instead of calendar/alert"
pattern: "AuthSplash"
- from: 'apps/pwa/src/main.tsx'
to: 'apps/pwa/src/store/calendarStore.ts'
via: 'QueryCache/MutationCache onError → setSessionExpired(true) on SessionExpiredError'
pattern: 'SessionExpiredError'
- from: 'apps/pwa/src/components/CalendarShell.tsx'
to: 'apps/pwa/src/components/AuthSplash.tsx'
via: 'meQuery.isLoading/isError and sessionExpired flag render AuthSplash instead of calendar/alert'
pattern: 'AuthSplash'
---
<objective>
Smooth the entire auth flow (D-10 + D-11) — the security-relevant slice. A single refactor serves both: gate the app render on auth state so nothing paints before Authelia (999.2), and centralize session-expiry detection so a timed-out session redirects cleanly instead of hanging (999.3).
This plan is the SOLE owner of `apps/pwa/src/api/client.ts`. To keep file ownership exclusive across the wave, it also lands the two non-auth type additions other plans depend on (consumed, not edited, elsewhere):
- `recurrenceUntil?` / `recurrenceCount?` on `CreateEventPayload` (D-06 — the API contract is in Plan 02; the EventForm UI in Plan 06 sends these).
- `hasRrule: boolean` on the client mirror of `CalendarOccurrence` (D-08 — server source-of-truth is Plan 03; the series-edit prompt in Plan 06 reads it). Per PATTERNS Pitfall 4, the mirror must match `expand.ts` exactly.
@@ -68,6 +69,7 @@ Output: `SessionExpiredError` + consistent `redirect:'manual'` in all fetch wrap
<artifacts_this_plan_produces>
NEW symbols introduced here (exclude from drift/convergence checks):
- `class SessionExpiredError extends Error` in `apps/pwa/src/api/client.ts`
- `handleAuthResponse(res, label)` helper in `client.ts`
- `recurrenceUntil?: string` + `recurrenceCount?: number` on `CreateEventPayload` (client.ts)
@@ -75,7 +77,7 @@ NEW symbols introduced here (exclude from drift/convergence checks):
- `AuthSplash` component (`apps/pwa/src/components/AuthSplash.tsx`) with `state: 'loading' | 'redirecting' | 'dead-end'`
- `sessionExpired` boolean + `setSessionExpired` action in the Zustand store (`calendarStore.ts`)
- QueryCache/MutationCache `onError` wiring in `main.tsx`
</artifacts_this_plan_produces>
</artifacts_this_plan_produces>
<context_note_tanstack_v5>
RESEARCH flagged the TanStack Query v5 global-error API as an unverified assumption (A3). It is now RESOLVED via Context7 (`/tanstack/query`): in v5 the global handler is supplied by constructing `new QueryCache({ onError })` and `new MutationCache({ onError })` and passing them into `new QueryClient({ queryCache, mutationCache })`. These `onError` callbacks always fire (unlike `defaultOptions.onError`, which was removed). Do NOT use `defaultOptions.onError`. The executor MUST still run one Context7 `query-docs` confirmation against `/tanstack/query` for the exact `QueryCache`/`MutationCache` constructor signature in version 5.101.0 before coding Task 3, then implement per the confirmed API.
@@ -200,23 +202,25 @@ RESEARCH flagged the TanStack Query v5 global-error API as an unverified assumpt
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Boundary | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| browser → OIDC IdP (Authelia) | Unauthenticated/expired requests cross to the IdP via a full-page navigation to `/api/login`; the `redirect:'manual'` XHR boundary keeps cross-origin IdP redirects from being silently followed. |
| browser → API (`/api/*`) | Any query/mutation may receive a 401/opaqueredirect when the session has expired; this is the boundary where session state is enforced. |
| client render gate | The point where authenticated calendar content is allowed to paint — must occur only after `meQuery.isSuccess`. |
| browser → API (`/api/*`) | Any query/mutation may receive a 401/opaqueredirect when the session has expired; this is the boundary where session state is enforced. |
| client render gate | The point where authenticated calendar content is allowed to paint — must occur only after `meQuery.isSuccess`. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06-05-info | Information Disclosure | CalendarShell pre-auth render (D-10) | mitigate | Gate render on `meQuery.isSuccess`; AuthSplash (no app data) is the only thing painted while auth is unknown. Eliminates the 999.2 flash of calendar shell/skeleton — itself a minor disclosure of app structure before auth. (ASVS V2.) |
| T-06-05-redirect | Tampering (open redirect / loop) | maybeRedirectToLogin one-shot guard re-arm (D-11) | mitigate | Redirect target is the fixed internal `/api/login` string — never derived from user input or a `returnTo`/`next` param, so no open-redirect vector. The one-shot `familysync.loginRedirectAttempted` guard prevents a redirect loop; it is re-armed via `clearLoginRedirect()` only on a genuine session-expiry transition (or successful `/api/me`), bounding re-auth attempts to one per expiry. |
| T-06-05-session | Spoofing | SessionExpiredError detection (D-11) | mitigate | Detection is `res.type==='opaqueredirect' || res.status===401` only — it never trusts a response body to decide auth state. Session remains server-enforced via the existing Authelia httpOnly same-origin cookie contract; the client merely reacts to the server's 401/redirect. No token is read or stored client-side. (ASVS V3.) |
| T-06-05-inflight | Repudiation / data loss | in-flight write on expiry | accept | In-flight write replay is deferred (D-11 nice-to-have, RESEARCH Open Question 3). A write that hits an expired session surfaces a clear re-auth instead of silently succeeding; the user re-submits after re-auth. Acceptable for a two-user household; documented, not silent. |
| T-06-05-SC | Tampering | npm installs | accept | No package installs (zero new deps — RESEARCH Package Legitimacy Audit n/a). |
| Threat ID | Category | Component | Disposition | Mitigation Plan |
| ---------------- | -------------------------------- | ------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T-06-05-info | Information Disclosure | CalendarShell pre-auth render (D-10) | mitigate | Gate render on `meQuery.isSuccess`; AuthSplash (no app data) is the only thing painted while auth is unknown. Eliminates the 999.2 flash of calendar shell/skeleton — itself a minor disclosure of app structure before auth. (ASVS V2.) |
| T-06-05-redirect | Tampering (open redirect / loop) | maybeRedirectToLogin one-shot guard re-arm (D-11) | mitigate | Redirect target is the fixed internal `/api/login` string — never derived from user input or a `returnTo`/`next` param, so no open-redirect vector. The one-shot `familysync.loginRedirectAttempted` guard prevents a redirect loop; it is re-armed via `clearLoginRedirect()` only on a genuine session-expiry transition (or successful `/api/me`), bounding re-auth attempts to one per expiry. |
| T-06-05-session | Spoofing | SessionExpiredError detection (D-11) | mitigate | Detection is `res.type==='opaqueredirect' | | res.status===401` only — it never trusts a response body to decide auth state. Session remains server-enforced via the existing Authelia httpOnly same-origin cookie contract; the client merely reacts to the server's 401/redirect. No token is read or stored client-side. (ASVS V3.) |
| T-06-05-inflight | Repudiation / data loss | in-flight write on expiry | accept | In-flight write replay is deferred (D-11 nice-to-have, RESEARCH Open Question 3). A write that hits an expired session surfaces a clear re-auth instead of silently succeeding; the user re-submits after re-auth. Acceptable for a two-user household; documented, not silent. |
| T-06-05-SC | Tampering | npm installs | accept | No package installs (zero new deps — RESEARCH Package Legitimacy Audit n/a). |
</threat_model>
<verification>
@@ -227,11 +231,12 @@ RESEARCH flagged the TanStack Query v5 global-error API as an unverified assumpt
</verification>
<success_criteria>
- D-10 (success criterion 5): unauthenticated cold load shows only the neutral splash.
- D-11 (success criterion 4): mid-use session expiry redirects cleanly via a global handler.
- Client type contract for D-06 (payload) and D-08 (hasRrule mirror) is in place for Plan 06.
- `client.ts` ownership is exclusive to this plan (no other Wave-1 plan edits it).
</success_criteria>
</success_criteria>
<output>
Create `.planning/phases/06-ux-polish/06-05-SUMMARY.md` when done (RED/GREEN notes for Task 1, the confirmed TanStack v5 API used, and the playwright-cli observations).