mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
df67b604202c42ab49ae246e080512b8b5dfd643
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
37e9f32f56 |
fix(ai): require active consent before external-provider egress; rate-limit and harden Fitbit sync
Close the launch-blocking gap where the ConsentReceipt infrastructure was built but never enforced as a precondition: a direct API caller with a valid token could forward a health snapshot to a server-managed external LLM with no receipt on file. Consent was enforced client-side only. Add a consent guard (src/lib/ai/consent-guard.ts) and call it before the first external-provider call on the server-managed path. The gate fires only when the resolved chain could egress via the operator's global key (admin-openai); a user's own BYOK key (openai/anthropic), their own ChatGPT OAuth account, and the self-hosted local provider stay ungated — that egress is the user's own act and the settings flow is its consent. The check fails closed: a BYOK-primary chain with an admin-openai fallback still requires a receipt, since the runner may cascade to the operator key. The surface maps to the consent kind (coach -> ai_coach, insights -> ai_insights_only; master ai_full satisfies either). - Interactive routes (insights/chat, insights/generate POST) throw ConsentRequiredError; api-handler renders a 403 with meta.errorCode = "consent.ai.required", mirroring assistant.disabled.* so the client can render an inline grant-consent notice. - Off-request pipelines fail closed quietly: comprehensive generation returns a typed skipped:no-consent outcome, and the per-metric status family (runStatusCompletion, gating every status card + the period-narrative warm + the off-budget Coach memory workers) surfaces the no-key fallback instead of egressing. Also rate-limit POST /api/fitbit/sync (the one Fitbit route without a limiter): a 5/60s baseline plus a tighter 1/hour bucket on the expensive fullSync walk, keyed by user id, matching the sibling Fitbit routes. Harden the Fitbit OAuth redirect_uri: assert it is an absolute https origin (or http on localhost), lands on /api/fitbit/callback, and stays same-origin with NEXT_PUBLIC_APP_URL — defence-in-depth behind Google's registered-URI check so a Host-coerced or misconfigured app URL cannot redirect the authorization code off-origin. (cherry picked from commit 46b5fc2cea1b6e80bee0445bfdacf5790442c67d) |
||
|
|
7bef199039 |
v1.4.33 — polish and reliability
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path. Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container. Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app. The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces. |
||
|
|
5ea813e20c |
chore(release): v1.4.31
Operator-side assistant feature-flag matrix, /insights tab-strip blocking fix on mobile, and the GHCR-propagation-race Coolify auto-deploy fix. Full notes in CHANGELOG. |
||
|
|
4f09c3f862 |
chore(release): v1.4.27
v1.4.27 — Mobile capability and maintainer-finding cleanup. ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop) applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer (graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line release notes attached to the GitHub Release. |
||
|
|
17b8d8d2c2 |
chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites). See CHANGELOG.md for the full per-section delta. |
||
|
|
ca03c6099f |
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. |
||
|
|
643e6ec3a0 |
release: v1.4.2 — resurrect v1.4.1 work + bug-fix triple (#152)
* Revert "revert: roll source tree back to v1.4.0 to restore production"
This reverts commit
|
||
|
|
e444a90b10 |
revert: roll source tree back to v1.4.0 to restore production
Production at healthlog.bombeck.io has been 503-ing since the v1.4.1
deploys started landing on apps-01 (Coolify). The container boots —
Next.js prints "Ready" and the pg-boss workers run — but never
accepts HTTP on :3000, so the Docker healthcheck fails and Traefik
takes the upstream out of rotation. A manual restart, a Coolify
force-rebuild, and a docker-compose pin to the GHCR :1.4.0 multi-arch
image all failed to bring the site back up — Coolify rebuilds the
image from main HEAD on every deploy regardless of the compose
directives.
This commit resets the working tree to commit
|
||
|
|
70353882eb |
fix(security): close moodLog SSRF + central error redaction; fix citation drift (#148)
The v1.4.1 audit pass turned up one CRITICAL, two HIGH, and four medical-citation drift findings. This commit addresses everything that's safe to land in a hardening release; deeper architectural fixes (idempotency-race transactional rewrite, encryption-key fallback gate, refresh-token reuse-detection serialisation, moodLog secret HMAC lookup column) are tracked for v1.4.2 in docs/ops/v141-followup-issues.md. Security -------- CRITICAL — moodLog SSRF src/lib/validations/moodlog.ts now refines the credentials URL with isPublicUrl() so an authenticated user cannot store http://169.254.169.254/ (cloud metadata) or RFC1918 hosts. src/lib/moodlog/sync.ts re-checks isPublicUrl(baseUrl) at the fetch site (so legacy rows stored before the credential guard are also refused) and switches to redirect: "manual" + 3xx → fail so a public host cannot 302 the request to an internal target with the user's apiKey on the redirect hop. New unit test src/lib/validations/__tests__/moodlog.test.ts asserts the guard rejects 169.254.x.x, 10.x, 172.16.x.x, 192.168.x.x, 127.0.0.1, and localhost. HIGH — error-message secret leakage New src/lib/logging/redact.ts redacts Bearer tokens (incl. our hlk_/hlr_), Telegram bot URLs (bot<digits>:<token>), and query-string secrets (?secret=, ?code=, ?token=, ?api_key=). The redactor is called from WideEventBuilder.setError() — every error reaching Loki is scrubbed once, centrally — and from the reportToGlitchtip() call in api-handler.ts so the Glitchtip incident UI is also protected. New unit suite covers the four patterns plus over-redaction (Bearer's \\S+ greedily consumes anything up to the next whitespace, which is the safer side of the trade-off). Truthfulness — citation drift fixes (audit found 0 hallucinations, 4 drift items) - messages/{en,de}.json bpClassificationTitle and avg30dEsc: "ESC/ESH 2018" → "ESH 2023". User-visible on the dashboard and doctor-report PDF. The numeric thresholds are unchanged from 2018; the 2023 update is ESH-only since ESC withdrew from joint authoring. - src/lib/analytics/classifications.ts: section header, getBpTargetsByAge() docblock, and inline comment updated to cite ESH 2023 with the same "numerically unchanged" caveat. - src/app/api/insights/targets/route.ts:375: source label "WHO" → "Saint-Maurice JAMA 2020". The AI-prompt anti-pattern guard explicitly forbids citing WHO for a step number; this surface was the last "WHO" mislabel in the codebase. - Saint-Maurice "mortality plateau 8000-12000" attribution softened to "continued dose-response benefit through ~12,000 steps/day, not a plateau" in src/lib/medical-citations.ts, src/lib/analytics/effective-range.ts, src/lib/analytics/classifications.ts, and both AI prompts. The original JAMA 2020 paper shows continuing benefit (HR 0.49 at 8k vs 4k, HR 0.35 at 12k); the plateau-shaped finding belongs to Paluch 2022 Lancet PH (PMID 35247352), not Saint-Maurice. Quality gates pnpm typecheck clean pnpm test 669 / 669 (was 658; +11 from new suites) pnpm test:integration 10 / 10 (untouched) pnpm lint 0 errors |
||
|
|
98a57eb786 |
audit(v1.3.3): SpO2 + body-composition + 14 CRITICAL/HIGH security/correctness fixes (#121)
* fix(measurements): close issue #109 + sync body composition across UI/PDF/thresholds User-visible: TOTAL_BODY_WATER, BONE_MASS, and BLOOD_GLUCOSE now appear in the measurements filter, badge, mobile icon, and edit dialog (issue #109). Server-rendered doctor report now includes body composition rows (audit C-9). Body water + bone mass have proper effective-range thresholds so severity logic doesn't silently return `nominal` (audit C-15). Structural: extracted the per-type display maps from measurement-list.tsx into measurement-list-meta.ts as a deliberate first step toward the single-source manifest planned for phase P5. Coverage tests now fail-fast if a future enum addition skips any of: list label/icon/color, doctor-PDF label/unit/vital-types, chart label, threshold metric order/label/bounds. Audit-2026-05-07 / phase P0: - closes #109 (filter UI gap, root cause for the German raw-enum display) - closes audit C-9 (server PDF used a stale type map) - closes audit C-15 (no warn/critical bands for body composition) Tests: 282 → 296 (14 new). Typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: align OpenAPI spec + migration comment with body-composition reality - OpenAPI MeasurementType enum: add TOTAL_BODY_WATER + BONE_MASS (was at v1.3.0 baseline; spec drifted past the v1.3 server release) - Bump openapi version 1.3.0 → 1.3.2 to match package.json - Migration 0022 comment: TOTAL_BODY_WATER stores kg (water content), not percent of body weight. Validators (VALUE_RANGES) and the Withings client (type 77 → kg) treat it as kg already; the comment was the lone outlier and would mislead the next reviewer. Audit-2026-05-07 / phase P1a — closes audit M-B1 (partial; OpenAPI enum) and M-B2 (migration unit comment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): close 3 audit CRITICALs (Withings header / idempotency Bearer / GlitchTip URL strip) C-3 (Withings webhook secret in URL): Now reads X-Withings-Webhook-Secret header in preference; URL ?secret=… fallback retained for legacy Withings configurations and emits a Wide Event warning so operators can spot still-using-the-old-flow integrators. The query-string secret was leaking into Coolify access logs and any GlitchTip event whose request.url got captured. C-4 (idempotency dead for Bearer-token clients): The default userIdResolver only consulted getSession(). Bearer-authed clients (the iOS / n8n / headless-API fleet that PR #117 was built for) fell through, so every retry created duplicate measurements. The default now tries cookie session first, then Bearer token via hashToken+apiToken.findUnique with revoked/expiry checks. All three existing call sites (measurements, mood-entries, medications/intake) get the fix automatically. H-B7 (request.url with secrets shipped to GlitchTip): reportToGlitchtip now strips the query string before forwarding the URL, so Withings legacy ?secret=… and any OAuth ?code=… that errors during the callback don't end up in someone else's incident UI. Audit-2026-05-07 / phase P2. Tests: 296 → 301 (5 new in idempotency.test for the Bearer fallback + revoked/expired/null cases). Deferred to a follow-up (needs DB migration / lock primitive): C-2 moodLog webhook → AES-GCM at rest + HMAC-over-body C-5 idempotency TOCTOU advisory_lock or in-flight sentinel row Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(measurements): add OXYGEN_SATURATION (SpO2) as first-class measurement type — v1.3.3 Closes the OxygenSaturation feature request from #109. - Migration 0024 extends MeasurementType enum (additive, IF NOT EXISTS) - Zod validators: plausibility 50–100% (below 50 is incompatible with sustained life; above 100 is physically impossible) - effective-range thresholds: green 95–100, orange 92–94, red <92. Sources: BTS Guideline 2017 (target 94–98%), ATS clinical practice (≥95% at rest is healthy), AHA/clinical literature (<92% = call provider, <88% = ER). Lower-only concern — upper orange wing collapses to greenMax since saturation cannot exceed 100%. - Personalisation: existing thresholds-override UI lets COPD / chronic-respiratory users with a doctor-set 88–92 baseline tune the bands. METRIC_ORDER + METRIC_LABEL_KEYS extended in thresholds-section.tsx; the exhaustive `Record<ThresholdMetric>` forces TS to catch any missed surface. - Withings ScanWatch type 54 mapping (best-effort — only fires for pulse-ox-capable devices). - Wired through measurement-form, measurement-list-meta (icon: Wind), health-chart, both doctor-report-pdf renderers (core + browser-side near-duplicate), OpenAPI spec (version 1.3.0 → 1.3.3), and i18n in 4 sections × 2 locales. - iOS DTO already declared `OXYGEN_SATURATION` — server enum addition closes the long-standing drift flagged in audit V2. - 4 new tests: numeric green band, upper-wing collapse, user-override (COPD baseline), METRIC_BOUNDS shape. Tests: 301 → 305. Typecheck: clean. Audit-2026-05-07 / v1.3.3 / closes #109 SpO2 feature request. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(analytics): truthfulness pass on medical citations + clamp override orangeMax Doctor-agent + hallucination-hunter audit V3 found six medical citations in effective-range.ts that the code did not actually implement. Plus a latent bug in the user-override path that produced physically-impossible orangeMax values (>100% saturation, <0% body-fat). Citation rewrites (no numeric changes): - ACTIVITY_STEPS: "WHO ≥8000 steps/day" → Saint-Maurice JAMA 2020. WHO publishes activity in min/week, not steps. Step-count was a misattribution that also leaked into AI prompts. - TOTAL_BODY_WATER: "ESPEN Body Composition Guidance 2017" → Watson formula / ICRP Reference Man. ESPEN 2017 is BIA practice guidance, it does not publish kg cut-offs. - BONE_MASS: "DEXA-equivalent bone mineral content" → "Bioimpedance- estimated; not DEXA-comparable". Withings BIA values typically run 5–7% below DEXA — calling them DEXA-equivalent in a doctor-report PDF was a real legal-exposure misattribution. - OXYGEN_SATURATION: "BTS Guideline 2017 (target 94–98%) + ATS clinical practice (≥95% at rest is healthy)" → "consumer pulse- oximeter consensus (≥95% normal at rest); slightly tighter than BTS 2017 (94–98%); NICE NG115 ≤92% escalation, BMJ panel / FDA pulse-oximeter labelling ≤88% ER threshold". Code uses 95-100/92-100 band — that's an ATS/consumer-consensus number, not BTS's literal 94-98 target. Soften the citation to match the code, not vice versa. Bug fix (1 latent, 1 real-today): - effective-range override path now clamps orangeMin/orangeMax to METRIC_BOUNDS.{min,max}. Without the clamp, a COPD user setting SpO2 override to {95,100} emitted orangeMax = 100.75 (impossible saturation), and a BODY_FAT override of {2,80} emitted orangeMin = -9.7 (negative percent). Default-range case 210 already hard-coded orangeMax: 100 for the same reason — the override path was the only branch missing the invariant. Tests: 305 → 306 (1 new: clamps override orangeMax to physiological 100%). Typecheck: clean. Audit-2026-05-07-V3 / closes Doctor + Senior-Dev medical-citation findings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): Bearer-scope wildcard handling + GDPR Art.17 account-delete erasure Two CRITICAL fixes from audit V3. NEW-V3-1: Bearer-scope bypass. apiToken.permissions ["*"] is meant to be the wildcard "session-equivalent" scope (the iOS app receives this on login). The previous check permissions.includes(requiredPermission) treated "*" as a literal string — never matched. Today many sensitive routes call requireAuth() *without* a requiredPermission, so a leaked iOS token can act as a full-scope token (account delete, settings wipe). Once those routes adopt requireAuth("scope:name"), this wildcard handling keeps the iOS app working while narrower-scoped tokens (e.g. user-issued ["medication:ingest"]) get correctly 403'd. Same wildcard handling added to the standalone permission checks in src/app/api/ingest/medication/route.ts:65, 119. NEW-V3-2: GDPR Art. 17 account-delete erasure completeness. Feedback + AuditLog rows had `onDelete: SetNull` in the schema, which preserved PII (free-text symptom descriptions, IP addresses, login-city geolocation) after the user was deleted. We now explicitly prisma.feedback.deleteMany + prisma.auditLog.deleteMany before the cascade-delete, making account deletion genuinely complete erasure. Tests 306/306 still pass; typecheck clean. Audit V3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(pdf): real text-content assertions for body composition + SpO2 rows Replaces bytes-only "renders body composition rows" theatre test with pdf-parse-driven text extraction that asserts the actual rendered labels + values for TOTAL_BODY_WATER, BONE_MASS, OXYGEN_SATURATION (DE + EN). Closes V3 audit finding STILL-V2-NEW-3: the previous test only verified "%PDF-" header + byte-count, so a regression that drops a row would still pass. Now a missing label fails the test. - adds dev dep: pdf-parse@^2.4.5 - 308/308 tests pass (was 306, +2 SpO2 + EN-locale coverage) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(measurements): close enum drift cousins — derive 5 type-arrays from canonical enum + extend 2 contract enums V3 audit finding "enum drift cousins": 7 hardcoded module-level type arrays silently dropped SpO2/TBW/BoneMass/BloodGlucose from their respective domains. Adding a new measurement type required hunting these down. Now all aggregation paths derive from `measurementTypeEnum.options`: - /api/insights/comprehensive (AI insights) - /api/dashboard/summary (iOS dashboard adapter) - /api/analytics (analytics aggregator) - /lib/insights/general-status (AI general-status fetch) - /api/import (Zod schema — round-trip now covers all 11 types) External-contract enums extended additively (no breakage for old clients): - /api/measurements/series kindEnum: + oxygen, totalBodyWater, boneMass - /api/dashboard/widgets widgetIdEnum: + oxygenSaturation - DashboardWidgetId + DEFAULT_DASHBOARD_LAYOUT: + oxygenSaturation row - formatMeasurementsForExport: glucoseContext now round-trips Coverage test (`measurement-type-enum-coverage.test.ts`) asserts the canonical enum stays the source of truth and documents PDF exclusions (BLOOD_GLUCOSE renders via context section, SLEEP/STEPS lifestyle-only). 313/313 tests pass (was 308, +5 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): close 6 V3-audit HIGH findings (CSP, web-push SSRF, IP geo, AI provider leak, import rate-limit) - CSP: chatgpt.com + api.openai.com only on /settings/ai/** routes (was blanket connect-src on every page including /auth/login → DOM-XSS exfil channel) - web-push: webPushSubscriptionSchema.endpoint requires HTTPS + isPublicUrl refinement; was z.url() only → blind SSRF probe via Push subscription - isPublicUrl: also fixes a regression where DNS labels starting with "fc"/"fd" (fcm.googleapis.com etc.) were falsely classified as IPv6 unique-local. Check is now gated on a colon being present in the host. - /api/ai/test: stop returning provider err.message + bodyExcerpt to the client (leaked provider URLs / partial keys / internal headers). Now returns categorised generic message; full details land in the wide-event via annotate() for the operator. - /lib/geo (auditLog IP geo lookup): switch default provider to ipwho.is over HTTPS, accept ipwho.is + ip-api pro response shapes, add IP_GEO_LOOKUP_DISABLED env, refuse non-HTTPS configured URLs. Previously leaked IP+timestamp over plaintext HTTP every auth event (GDPR Art. 32 + Art. 44). - /api/import: add 5/hour rate-limit (was unlimited; bulk-injection vector) Tests: 325/325 pass (was 313, +12 new — webPushSubscriptionSchema SSRF guards, fc/fd-prefix domain regression, geo HTTPS-only, geo provider shapes, geo opt-out env). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security,gdpr): trusted-proxy IP, audit-log retention, idempotency cachable filter, Bearer mock tightening V3 audit follow-on bundle covering 4 HIGH findings. (I) getClientIp — TRUST_PROXY_HOPS env (default 1) replaces blind XFF trust. Reads chain right-to-left so client-supplied leftmost entries cannot rotate the per-IP rate-limit bucket. Default 1 matches typical single-proxy self-host (Coolify/Caddy/Cloudflare-Tunnel); 0 forces x-real-ip / null. New 7-test suite covers the rotation attack + malformed entries + zero-hop fallback. (F) Audit-log retention job — daily 03:15 cleanup of audit_logs older than AUDIT_LOG_RETENTION_DAYS (default 365). Closes GDPR Art. 5(1)(e) "storage limitation" gap; previously IPs + city + login events accumulated forever. 6-test suite covers default + override + misconfig guard (rejects <7 days). (G) isCachableStatus — extracted the inline 4xx/5xx filter into a named, exported, tested function. 7 cases cover the full do-not-cache contract (401 / 403 / 408 / 429 / any 5xx) and confirm 2xx + 4xx validation are still cached. (H) Bearer-mock tightening — apiToken.findUnique mocks in require-auth-bearer.test + idempotency.test now assert the where.tokenHash shape uses the hashed value (not the raw bearer). A regression that switched back to raw-token comparison would have been silent without this assertion. Tests: 345/345 pass (was 332, +13 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): encrypt moodLog webhook secret at rest with AES-256-GCM (V3 audit STILL-V2-C-2) The mood_log_webhook_secret column previously stored a plaintext secret that the webhook handler compared timing-safe against every enabled user. A DB dump or backup snapshot leaked active webhook credentials. New contract — no schema migration required: - Writes go through encryptMoodLogSecret() (AES-256-GCM via existing crypto helper, ENCRYPTION_KEY). - Reads go through readMoodLogSecret() which transparently decrypts envelopes and tolerates legacy plaintext rows during the transition (graceful upgrade path — no breaking change for self-hosted users). - Webhook lookup decrypts each candidate before timing-safe compare. - Status endpoint decrypts for the user's settings page. One-shot startup migration in reminder-worker rotates any leftover plaintext rows on next worker boot. Idempotent; encrypted rows are skipped. Tests: 350/350 pass (was 345, +5 new — round-trip, legacy passthrough, isLegacyPlaintext semantics, rotateLegacy idempotence). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: sync README + AGENTS + CLAUDE + CHANGELOG + OpenAPI for v1.3.3 (V3 audit doc-drift) V3 audit Doku Specialist findings — drift between code and meta-docs. - README §Key Features now mentions pulse oximetry (SpO₂) + Withings ScanWatch sync; Tech Stack model count corrected 23 → 25. - AGENTS.md status promoted v1.3.2 → v1.3.3; vitest config corrected `vitest.config.ts` → `vitest.config.mts`; migration range corrected `0001–0022` → `0001–0024`; model count + list updated to 25 (adds Device, IdempotencyKey). - CLAUDE.md model count updated (2 occurrences). - CHANGELOG.md v1.3.3 expanded with Security + Internal sections that were missing the audit-fix-marathon work (Bearer-scope, account-delete cascade, moodLog encryption, CSP tightening, Web-Push SSRF, IP-geo HTTPS, AI-test leak fix, import rate-limit, trusted-proxy XFF, audit-log retention, idempotency cachable filter, Bearer mock tightening, enum-drift cousins, PDF text-content tests). - OpenAPI Bearer description corrected `SHA-256 hashes` → `HMAC-SHA-256 hashes (keyed with API_TOKEN_HMAC_KEY)` so the docs match the actual server-side implementation. Wildcard scope contract documented. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(audit-review): close 4 HIGH + 2 MEDIUM findings from multi-agent review pass Multi-agent review of the audit-fix branch surfaced four HIGH issues and a couple of MEDIUM hardenings. All addressed before merge. HIGH-1 — OpenAPI MetricKind enum drift (iOS strict-decode crash risk). The dashboard-summary route now emits totalBodyWater/boneMass/ oxygenSaturation MetricCards, but the spec still listed only the original 7 kinds. Generated iOS Codable would have crashed the dash the moment a user logged SpO2. MetricKind extended with the 3 new kinds + comment documenting unknown-value defensive decoding. HIGH-2 — kindEnum naming consistency on /api/measurements/series. The series adapter shipped "oxygen" while the dashboard summary used "oxygenSaturation"; both routes now use "oxygenSaturation" so iOS decoders can share a single MetricKind type across endpoints. HIGH-3 — /api/ai/test no-leak guard had no test. Added 4 tests that mock provider errors with sk-leaked-key + provider URLs in the err fields and assert the response body never echoes them, plus the 3-status-categorised generic messages (401/429/5xx/other). HIGH-4 — /api/import 5/hour rate-limit had no test. Added 2 tests that mock checkRateLimit allowed/disallowed and assert the 429 path uses the right key shape (`import:<userId>`, 5, 1h) and that the within- quota path proceeds. MEDIUM-3 — getClientIp under-length XFF chain regression. With TRUST_PROXY_HOPS=2 and a chain of length 1, the previous clamp returned the leftmost (attacker-controlled) entry, silently re-introducing the rotation attack the helper was meant to close. Now: when chain is shorter than configured hops, refuse XFF entirely (fall back to x-real-ip / null). Added regression test. LOW-1 — TRUST_PROXY_HOPS=garbage now throws at boot instead of silently degrading to hops=0. Added regression test. Tests: 358/358 pass (was 350, +8 new). Typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
54f689247e |
feat(auth): accept Bearer tokens for headless API clients
`requireAuth()` keeps the existing cookie-session path as the primary check; when no session cookie is present it now falls back to validating `Authorization: Bearer hlk_*` against the existing `ApiToken` table (HMAC-SHA-256 hash, expiry + revocation enforced). Behaviour for browser clients is unchanged — the cookie path runs first and short-circuits before the header lookup. `requireAdmin()` stays cookie-only by design. Wide-Event annotation tags `auth_method: "bearer"` so the source is distinguishable in Loki. `auth.bearer.success` and `auth.bearer.failure` audit entries record the token id + reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
908532be73 |
feat(security): SSRF strict IPv4, PAT logging redaction, threshold rate-limit, N+1 fix, pg-boss SIGTERM, blocking CI
Security hardening from 2026-04-26 audit:
- SEC-002: isPublicUrl() now uses a strict IPv4 parser that rejects
leading-zero forms like 010.0.0.1, which historically slipped past
the h.startsWith('10.') check. Also covers CGNAT (100.64/10),
IPv6 unique-local (fc00::/7), and additional metadata-service edge
cases. Documented as input-time only (DNS rebinding still possible
at fetch time).
- SEC-004: GitHub PAT is now redacted from logged error bodies in
publish-github.ts before flowing to Wide Events / Loki. Adds an
escapeRegex helper for safe RegExp construction.
- SEC-006: PUT /api/user/thresholds now goes through checkRateLimit
(30 writes / 5 min, per-user) — audit-log was already there but
unbounded. Makes audit-log enumeration unattractive.
- PERF-001: insights/comprehensive route now batches medication-intake
events into a single 'medicationId IN (...)' query and groups in
memory instead of N+1 per medication. Latency was scaling poorly
for users with many active meds.
- REL-003: pg-boss now installs SIGTERM/SIGINT handlers that call
boss.stop({ graceful: true, timeout: 30s }) so in-flight handlers
drain on docker stop / coolify redeploy instead of being killed.
- LOG-001: Replaced console.log/error in reminder-worker with a
workerLog helper. Lifecycle errors still go to stderr (only audience
outside any handler context); routine init messages are silent
because pg-boss own events + Wide Events from handlers carry the
state.
- CI: ESLint and TypeScript checks are now blocking — continue-on-error
removed. Tests already were. Required pre-existing 'any' types in
api-handler.ts to be properly justified with eslint-disable comments
(Next.js variadic handler signature; the bound type still constrains
return to Promise<Response>).
|
||
|
|
0c0c345e82 |
i18n: migrate from German-first to English-first default locale
Switch defaultLocale from "de" to "en" and make English the code-level language throughout. All ~145 hardcoded German strings translated: - API error messages (52 routes) now return English - Zod validation messages in English - Classification/analytics functions return English categories - Notification type labels in English - Component strings use i18n t() calls (heatmap, mood chart, medication form, intake history, password strength, insights card) - ~50 new i18n keys in both de.json and en.json Route migration: - /stimmung → /mood, /zielwerte → /targets (new pages) - 301 redirects in proxy.ts for legacy URLs - Sidebar + bottom nav updated Bump version to 1.0.1. |
||
|
|
38309932f3 |
Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure, pulse, mood tracking, medication compliance, Withings integration, doctor report PDF export, and Dracula-themed UI. |