# 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 `