Files
HealthLog/src/lib/fitbit
Marc-André Bombeck 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)
2026-06-05 03:01:36 +02:00
..