Pulse oximetry (SpO₂) ships as first-class + 14 CRITICAL/HIGH audit fixes
What's new
- Pulse oximetry (SpO₂) as a first-class measurement type. Withings ScanWatch (type 54) syncs automatically; manual entry, doctor-report PDF row, threshold override, AI insights, OpenAPI spec all wired through. Default bands (95–100% green, 92–94% orange, <92% red) follow consumer pulse-oximeter consensus + NICE NG115; COPD users with a doctor-set baseline of 88–92% can override.
- Body-composition + glucose surfaces finally visible in v1.3 UI. TBW + Bone Mass + Blood Glucose now show in the measurements list filter, badge, mobile icon, edit dialog, and the server-rendered doctor PDF — the v1.3 server already ingested them, but the client list-rendering had drifted to the v1.2 type map.
- Effective-range thresholds for TOTAL_BODY_WATER + BONE_MASS (was returning
nominalfor any value).
Security — 3 CRITICALs closed
- Bearer-scope wildcard handling (a token with
permissions:["medication:ingest"]could DELETE the user account) - Account-deletion completeness cascading through Feedback + AuditLog (GDPR Art. 17)
- Withings webhook secret header migration + idempotency Bearer-resolver + GlitchTip URL-strip
Security — 10 HIGHs closed
- moodLog webhook secret encrypted at rest (AES-256-GCM) with transparent legacy-plaintext rotation
- CSP
chatgpt.com+api.openai.comgated to/settings/ai/**(was a global blanket → DOM-XSS exfil channel) - Web-Push subscription endpoint now requires HTTPS +
isPublicUrl()SSRF guard - IP-geolocation lookup is HTTPS-only by default (was plaintext HTTP — GDPR Art. 32 + 44)
/api/ai/testno longer leaks provider URLs / partial keys/api/importrate-limited (5/h/user)- Trusted-proxy XFF semantics (
TRUST_PROXY_HOPSenv, default 1) — closes per-IP rate-limit rotation bypass - Audit-log retention purge (default 365 days, configurable, GDPR Art. 5(1)(e))
- Idempotency cachable filter pinned as exported, tested function
- Bearer mock tightening — assertions now catch any regression to raw-token comparison
Correctness
- Server-side enum drift cousins closed. 5 hardcoded
MeasurementType[]module arrays now derive frommeasurementTypeEnum.options; 2 contract enums (kindEnum,widgetIdEnum) extended for the new measurements - Truthfulness pass on medical citations: SpO₂ → consumer-pulse-oximeter consensus + NICE NG115; TBW → Watson formula / ICRP Reference Man (was misattributed to ESPEN); steps → Saint-Maurice JAMA 2020 (WHO publishes minutes/week, not steps); body composition → "bioimpedance-estimated, not DEXA-comparable"
isPublicUrlSSRF helper no longer falsely classifies DNS labels starting withfc/fd(e.g.fcm.googleapis.com) as IPv6 unique-local
Tests + docs
- 358/358 tests pass (was 305 at branch start, +53)
- Doctor-PDF text-content tests now use
pdf-parseto assert real DE + EN labels (replaces bytes-only theatre) - README, AGENTS.md, CLAUDE.md, OpenAPI all synced (model count, vitest config name, migration range, SHA-256 → HMAC-SHA-256 wording)
Upgrade notes
- No DB migration required for moodLog encryption — the worker auto-rotates legacy plaintext rows on next boot.
- New env vars (all optional, with safe defaults):
TRUST_PROXY_HOPS(default1) — set to your real proxy hop countAUDIT_LOG_RETENTION_DAYS(default365)IP_GEO_LOOKUP_URL(defaulthttps://ipwho.is) /IP_GEO_LOOKUP_DISABLED=1to opt out of any IP-egress
Multi-arch image
GHCR build pipeline is running for linux/amd64 + linux/arm64; check the Build & Publish workflow for the v1.3.3 image tag.
🤖 End-to-end audit-fix marathon by Claude Code overnight (V3 holistic audit → action plan → execution → multi-agent review → merge → tag)