diff --git a/CHANGELOG.md b/CHANGELOG.md index cda8c020e..7e3b5817a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## [1.38.11] — 2026-09-08 + +"Sign out everywhere" now means everywhere, a refresh cannot outrun a +revocation, and the idempotency cache can no longer answer for a credential +the route has not seen. + +### Fixed + +- **"Sign out everywhere" signs out the other phones, not the one pressing + it.** A native login is two rows: a refresh token and the access token it is + paired with, and the Bearer check consults only the second. Revoking the + refresh tokens alone, which is what the button did, left every other device + working until its access token ran out, up to a day on the default policy. + Worse, a phone pressing the button described itself to the server as a + browser session, so the exception meant to spare the caller matched nothing + and the phone revoked its own refresh token: it was the one device signed + out, at its next rotation. Both ends are fixed. The button revokes the paired + access tokens of every other device inside the same transaction, and a + Bearer caller is named by its access token so its own login is the one kept. + Long-lived API tokens from the settings page are untouched, as before. + +- **A refresh that overlaps a revocation loses.** Rotation read the presented + refresh token as live, minted a new pair, and only then marked the old row + consumed with a condition that checked "not yet used" but not "not yet + revoked". A sign-out-everywhere or a credential rotation landing in that + window was overtaken: the update still matched, and the pair minted a moment + after the family ended was the one live login left. The condition now + includes the revocation, the pair minted inside the window is retired, and + the client hears `revoked`, which it answers with a fresh sign-in, rather + than `already_used`, which it would retry. Pinned by an integration test that + revokes the family from inside the window against real Postgres. + +- **The idempotency cache is keyed by the authority a request carries.** A + retry that replays a cached response runs before the route's own permission + check, by design, so what the cache could answer had to match what the route + would allow. Two gaps: a delegate whose grant had been replaced by a narrower + one, or whose scope had been edited in place, still matched the cell they + filled under the wider grant, because the replay checked only that a live + grant existed; and a narrow-scoped API token of the same account could read + a cell filled by a wildcard credential on a route outside its scope. + Delegated cells now fold the grant's identity, level and scope into the key, + and a narrow token's cells are keyed by the token, so any change of authority + lands a retry in a fresh cell where the route decides. Cookie sessions and + wildcard tokens acting on their own record are keyed byte-for-byte as before; + no client changes. + +### Changed + +- **README: immunizations are in.** The record list still said immunization + records were deliberately out of scope; the vaccination record has shipped + since v1.37.3. + ## [1.38.10] — 2026-09-06 The nightly off-host backup streams and fits a small container, and a diff --git a/README.md b/README.md index 8fd6bbe39..9233f1a47 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Try the [live demo](https://demo.healthlog.dev) to see a working install — sig - **Every vital on one timeline.** Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking — charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024), overridable with the targets your clinician set. - **Clinical signals, opt-in and additive.** Validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7, Sleep Condition Indicator — official questionnaire wording, calm crisis-resource handling), grip strength, a 0–10 pain score, waist and body measurements, and a longevity lab panel — each added through one signal registry, so a new signal wires into charts, correlations, the coach, the FHIR map, and the API at once. -- **Records, structured.** Allergies and family history as encrypted structured records that flow into the doctor report, plus lab values you key in or scan from a printed report — OCR extracts the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. (Immunization records are deliberately out of scope.) +- **Records, structured.** Allergies and family history as encrypted structured records that flow into the doctor report, plus lab values you key in or scan from a printed report — OCR extracts the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. A vaccination record keeps a lifetime immunization log, doses grouped under the disease they protect against, and prints in the doctor report. - **A document vault for the paper trail.** An opt-in module stores every letter, report, and scan encrypted at rest — PDFs and photos preview inline, Office and text files stay download-only, and nothing user-uploaded can ever execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, undo-able delete with a 30-day grace, and per-file/per-account limits the operator tunes. - **Context that explains the numbers.** An opt-in environmental module folds weather, daylight, and temperature in as influence signals (winter darkness against mood, heat against blood pressure) using a home location you set, so a pattern has somewhere to point. - **Your devices, one server.** Withings, WHOOP, Oura and Polar sync over OAuth2, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health `export.zip` folds your full history in, and the native iOS app streams HealthKit live. A per-metric source priority decides which reading is canonical when wearables overlap. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index db7a530c4..218517c87 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.38.10 + version: 1.38.11 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index 322a61212..e9463569f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.38.10", + "version": "1.38.11", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index c50e908a1..2630a9bf1 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.38.10"; + /* @sw-version-fallback */ "v1.38.11"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`