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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user