From ca03c6099f0b1d5d24b92bf66df465bc704ab6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 12 May 2026 22:33:35 +0200 Subject: [PATCH] chore(release): v1.4.24 Security + accessibility hardening release. Closes the highest-priority findings from a focused security audit of the v1.4.23 release: - Bearer auth fail-closed when a route declares no scope (api-handler) - Public /api/send Umami proxy now rate-limits 120 req/min/IP - Proxy nonce generation switched to btoa(...) for Edge portability - Session-expiry delete race uses deleteMany + catch (no more 500s) - POST /api/import returns the standard apiError envelope on 422 - Runtime Docker image pins Prisma 7.8 + worker deps to minors - Notifications page renders a real load-error alert - Accessibility: switch aria-labels, htmlFor pairing, mobile admin Overview link, deep-link anchors on notification settings - Chart overlays: per-chart comparisonBaseline pref, BMI/bodyFat/ sleep/steps overlay keys, TrendCard layout tolerates long labels Verification: pnpm typecheck && pnpm lint && pnpm test (2244/2244) && pnpm format:check all green. --- .planning/STATE.md | 7 +- .planning/phase-W6-v1423-reconcile-report.md | 30 ++-- .planning/v1423-backlog.md | 2 +- CHANGELOG.md | 153 ++++++++++++++++-- Dockerfile | 4 +- docs/api/openapi.yaml | 30 ++-- docs/audit/v1422-summary.md | 16 +- messages/de.json | 2 + messages/en.json | 2 + package.json | 2 +- scripts/check-openapi.ts | 4 - scripts/generate-openapi.ts | 1 - .../proxy-onboarding-redirect.test.ts | 23 +++ .../api/admin/monitoring/umami-test/route.ts | 8 +- src/app/api/analytics/route.ts | 3 +- src/app/api/auth/codex/device-start/route.ts | 3 +- src/app/api/bugreport/route.ts | 2 +- .../dashboard/chart-overlay-prefs/route.ts | 3 + src/app/api/import/__tests__/route.test.ts | 21 +++ src/app/api/import/route.ts | 5 +- .../insights/generate/__tests__/route.test.ts | 22 ++- .../api/integrations/moodlog/test/route.ts | 3 +- .../api/integrations/withings/test/route.ts | 3 +- src/app/api/medications/route.ts | 2 +- .../api/monitoring/glitchtip/test/route.ts | 3 +- src/app/api/monitoring/umami/test/route.ts | 3 +- .../api/notifications/preferences/route.ts | 2 +- .../api/notifications/web-push/test/route.ts | 3 +- src/app/api/send/__tests__/route.test.ts | 88 ++++++++++ src/app/api/send/route.ts | 6 + src/app/notifications/page.tsx | 28 +++- src/app/page.tsx | 8 +- src/components/admin/admin-shell.tsx | 15 ++ src/components/admin/reminders-section.tsx | 9 +- .../__tests__/chart-overlay-controls.test.tsx | 2 + .../__tests__/health-chart-mini.test.tsx | 12 ++ .../medication-overlay-toggles.test.tsx | 3 + .../charts/__tests__/trend-card.test.tsx | 26 +++ .../charts/chart-overlay-controls.tsx | 48 +++++- src/components/charts/health-chart.tsx | 44 +++-- .../charts/medication-compliance-chart.tsx | 6 +- src/components/charts/mood-chart.tsx | 75 +++++---- src/components/charts/trend-card.tsx | 72 ++++++--- .../insights/coach-panel/coach-drawer.tsx | 2 +- .../insights/coach-panel/message-thread.tsx | 7 +- .../measurements/measurement-list.tsx | 13 +- .../settings/integrations-section.tsx | 13 +- .../settings/notifications-section.tsx | 18 ++- src/lib/__tests__/dashboard-layout.test.ts | 32 ++++ src/lib/__tests__/require-auth-bearer.test.ts | 26 ++- src/lib/ai/provider.ts | 6 +- src/lib/api-handler.ts | 15 +- src/lib/auth/__tests__/session.test.ts | 65 ++++++++ src/lib/auth/session.ts | 4 +- src/lib/dashboard-layout.ts | 9 ++ src/lib/notifications/senders/apns.ts | 2 +- src/proxy.ts | 13 +- .../analytics-sleep-stages.test.ts | 17 +- vitest.config.mts | 1 + 59 files changed, 841 insertions(+), 206 deletions(-) create mode 100644 src/app/api/send/__tests__/route.test.ts create mode 100644 src/lib/auth/__tests__/session.test.ts diff --git a/.planning/STATE.md b/.planning/STATE.md index 75ffb8a0a..6b00989b4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -216,9 +216,10 @@ MEDs, all LOWs, S-05 simplify, pre-existing `coach-prefs.test.ts` mock issue, sandbox `git commit` no-op). Test deltas: W5 ended at 2223 unit. Session A added the `revoke.test.ts` -+ `apns-dispatch` device tests landing at 2227. Session E ended at 2235. -Session F added the forged-X-Device-Id regression — final at 2236 unit -(+45 across the W5+W6 reconcile arc). + +- `apns-dispatch` device tests landing at 2227. Session E ended at 2235. + Session F added the forged-X-Device-Id regression — final at 2236 unit + (+45 across the W5+W6 reconcile arc). ## Wave 7 — Release v1.4.23 (next) diff --git a/.planning/phase-W6-v1423-reconcile-report.md b/.planning/phase-W6-v1423-reconcile-report.md index 370ad73d2..598fd2c89 100644 --- a/.planning/phase-W6-v1423-reconcile-report.md +++ b/.planning/phase-W6-v1423-reconcile-report.md @@ -6,13 +6,13 @@ six-review brief into shippable develop-branch state. ## Per-session SHA chain -| Session | Scope | Commit SHAs | -| --- | --- | --- | -| A | CRITICAL + HIGH 1-3 | `5486507`, `13977bc`, `d5202e5`, `a2dfc5e` | -| B | HIGH 4 + simplify (S-02 → S-04) + HIGH 6 | `3ee6dab`, `dfffe6b`, `d25e50e` | -| D | HIGH 5 (analytics chunked aggregate landed in W5; D session re-confirmed against the W6 senior-dev brief) | `deadc73` | -| E | HIGH 7 (Coach feedback referencing `coach_messages`, plaintext column drop) | `650f150` | -| F | MED cluster (Sec-MED-2 Coolify scrub, Sr-MED-5 isCurrent device-id, SLEEP_DURATION migration verification note) + planning record | `1a46bfe` (MED) + this commit (planning) | +| Session | Scope | Commit SHAs | +| ------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| A | CRITICAL + HIGH 1-3 | `5486507`, `13977bc`, `d5202e5`, `a2dfc5e` | +| B | HIGH 4 + simplify (S-02 → S-04) + HIGH 6 | `3ee6dab`, `dfffe6b`, `d25e50e` | +| D | HIGH 5 (analytics chunked aggregate landed in W5; D session re-confirmed against the W6 senior-dev brief) | `deadc73` | +| E | HIGH 7 (Coach feedback referencing `coach_messages`, plaintext column drop) | `650f150` | +| F | MED cluster (Sec-MED-2 Coolify scrub, Sr-MED-5 isCurrent device-id, SLEEP_DURATION migration verification note) + planning record | `1a46bfe` (MED) + this commit (planning) | ## Counts @@ -28,14 +28,14 @@ six-review brief into shippable develop-branch state. ## Test count delta -| Snapshot | Unit count | -| --- | --- | -| W5 close (commit `72829b1`) | 2223 | -| Session A close (CRIT + HIGH 1-3 added device + revoke tests) | 2227 | -| Session B close (HIGH 4 + simplify + HIGH 6) | ~2229 | -| Session D close (HIGH 5 reconfirm) | ~2229 | -| Session E close (HIGH 7 + coach-feedback updates) | 2235 | -| Session F close (forged X-Device-Id regression) | **2236** | +| Snapshot | Unit count | +| ------------------------------------------------------------- | ---------- | +| W5 close (commit `72829b1`) | 2223 | +| Session A close (CRIT + HIGH 1-3 added device + revoke tests) | 2227 | +| Session B close (HIGH 4 + simplify + HIGH 6) | ~2229 | +| Session D close (HIGH 5 reconfirm) | ~2229 | +| Session E close (HIGH 7 + coach-feedback updates) | 2235 | +| Session F close (forged X-Device-Id regression) | **2236** | Net delta over the W6 reconcile arc: **+13 unit tests**. diff --git a/.planning/v1423-backlog.md b/.planning/v1423-backlog.md index 3efc928f4..65183c8ea 100644 --- a/.planning/v1423-backlog.md +++ b/.planning/v1423-backlog.md @@ -12,7 +12,7 @@ without re-reading the full review pack. the settings cog) duplicates intent. Defer to v1.4.24 once W5 H7 surfaces enough thumbs data to know whether per-user prompt prefs drift from per-message ratings. Source: `phase-W6-v1423-design-review.md` - + `phase-W6-v1423-product-lead-review.md`. + - `phase-W6-v1423-product-lead-review.md`. ## Statistics diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f9fc0f1..ac7ed9014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,130 @@ # Changelog +## [1.4.24] — 2026-05-12 + +Security + accessibility hardening release. Closes the highest-priority +findings from a focused security audit of the v1.4.23 release: a Bearer +token that omits a scope no longer authenticates routes which never +declared one, the public Umami proxy is rate-limited, the proxy nonce +is Edge-runtime portable, a session-expiry delete race can no longer +500 a parallel request, the import-validation envelope now matches the +standard error contract, and the runtime Docker image installs Prisma +7.8 alongside pinned worker dependencies. Accessibility passes on the +notifications surface, admin shell, chart overlays and notification +settings deep-link anchors round out the user-facing work. + +### Security + +- **Bearer auth is fail-closed when a route declares no scope.** + `requireAuth()` previously let any non-revoked, non-expired API token + through whenever a handler omitted the `requiredPermission` argument. + A narrowly-scoped token (e.g. `["medication:ingest"]`) could therefore + reach every handler that called `requireAuth()` bare — including + account-sensitive routes such as `/api/auth/profile`, + `/api/auth/password`, `/api/withings/credentials`, + `/api/settings/data`, `/api/export/full-backup` and `/api/tokens`. + The Bearer path now throws `HttpError(403)` with audit reason + `scope_required` unless the token carries the `["*"]` wildcard + (the iOS app login token), preserving the existing session-cookie + path and the explicit-scope path (`/api/ingest/medications`). The + admin surface is unaffected — `requireAdmin()` was already + cookie-only. +- **Public Umami analytics proxy now rate-limits by client IP.** + `POST /api/send` forwards browser analytics events to the configured + Umami origin. The SSRF allow-list and 64 KB body cap were already in + place, but the endpoint itself was unbounded. Added a 120-events-per- + minute-per-IP gate using the existing `checkRateLimit` infrastructure; + abuse returns the standard `429` envelope without invoking the + upstream fetch. +- **Edge-runtime portable nonce generation in the proxy.** The proxy + previously assembled the CSP nonce via `Buffer.from(...)`, which is + not guaranteed in the Edge runtime. Switched to + `btoa(String.fromCharCode(...new Uint8Array(16)))`. Same 128 bits of + entropy, Node-and-Edge compatible without a runtime declaration. +- **Session-expiry delete race no longer 500s a parallel request.** + `getSession()` reaped expired session rows with `prisma.session +.delete()`; two requests arriving at the same expired session would + race and the loser would surface a 500. Replaced with `deleteMany` + + a catch-all guard, matching the discipline already used in + `destroySession()`. + +### Fixed + +- **`POST /api/import` returns the standard `{ data: null, error }` + envelope on validation failure.** The handler previously returned + the validation message inside `apiSuccess(...)` with status 422, + which clients could not distinguish from a successful import. +- **Runtime Docker image pins Prisma 7.8 + worker dependencies.** The + multi-stage image was installing `prisma@7.4.0`, `@prisma/engines +@7.4.0`, unpinned `pg-boss@12`, `@prisma/adapter-pg@7` and `pg@8` + into the worker prefix while the app code shipped Prisma 7.8 from + the lockfile. Pinned to `prisma@7.8`, `@prisma/engines@7.8`, + `pg-boss@12.18`, `@prisma/adapter-pg@7.8` and `pg@8.20` so + migration engine and generated client cannot drift across a deploy. +- **Notifications page surfaces a load error instead of a silent + "no channels" empty state.** `useQuery` failures now render an + alert with `role="alert"` and `notifications.loadError` copy + (EN/DE), so a transient 500 on `/api/notifications/preferences` + stops looking like a misconfiguration. + +### Accessibility + +- **Notification-preference switches carry accessible names.** Each + switch in the per-event table now exposes an `aria-label` combining + event-type label and channel label; the mobile per-event layout pairs + each `` with its `