Connect mints the PKCE pair, stashes the verifier on the OAuth-state row, and
sends the S256 challenge to the authorize endpoint. Callback reads the verifier
back and presents it on the token exchange, refusing cleanly when it is absent.
The connection-test probe hits the classic /1/user/-/profile.json endpoint.
Snapshot the incremental watermark once per sync cycle and thread the same
lower bound to every resource (metrics, activity, sleep, workout), stamping
lastSyncedAt once at the end. Previously each resource read lastSyncedAt then
stamped now() independently, so the first resource advanced the watermark and
later resources only fetched the last overlap window — silently dropping the
gap after an outage longer than the overlap. The full/backfill path is
unaffected (it passes no lower bound).
Reject an OAuth callback whose token response carries no refresh_token instead
of persisting an empty string. An empty refresh token bricks every future
token refresh; the callback now errors out cleanly so the user re-consents
rather than landing a dead connection.
Model the INTERVAL data types (steps/distance/calories/floors daily totals,
sleep and exercise sessions) on Google Health's interval.start_time /
interval.civil_start_time anchor instead of a sample_time / bare date. The
incremental filter for sleep/exercise previously targeted
sample_time.physical_time, which 400s/empties for interval types and stalls
incremental sync. Daily-summary metrics and spot readings keep their existing
date / sample anchors. Shape mismatches still fail safe (return []/null).
Mark the Google Health (Fitbit & Pixel) settings card as experimental with a
badge and a short note that data coverage is still being verified, in all six
locales.
(cherry picked from commit 3f116802149eebe9144729e2779f7f4405f248f2)
Land the OAuth + credentials half of the Fitbit / Google Health provider,
cloning the WHOOP provider structure and adapting it to the Google identity
flow.
- Client OAuth half (src/lib/fitbit/client.ts): Google authorize-URL builder
(access_type=offline + prompt=consent), authorization-code exchange, token
refresh, and a profile fetch for the connection's external user id. Client
credentials ride in an HTTP Basic-auth header on the token endpoint; the
four launch Restricted read scopes are pinned in FITBIT_OAUTH_SCOPE. All
egress goes through safeFetch.
- Non-rotating refresh tokens: getValidToken refreshes at expiry-minus-five-
minutes, persists the new access token + expiry, and overwrites the stored
refresh token ONLY when the response carries a fresh one — Google keeps the
refresh token stable, unlike WHOOP.
- BYO credentials route (PUT/GET/DELETE /api/fitbit/credentials): per-user
client id/secret stored encrypted on the User columns, built field-by-field
under requireAuth.
- OAuth state + connect + callback routes: a random base64url nonce backed by
a 10-minute FitbitOAuthState ledger row carries the (nonce -> userId)
mapping; the callback validates the cookie/URL state with timingSafeEqual,
consumes the row atomically (replay/expired/cross-user reason tags),
exchanges the code, upserts the encrypted FitbitConnection, clears prior
reauth state, and enqueues the self-converging history backfill.
- Response classifier mirrors WHOOP's HTTP-status buckets; Google signals a
revoked grant with a 401, so there is no invalid_grant body special-case.
Tests cover the authorize URL, Basic-auth token exchange, the no-rotation
refresh branch (token preserved when omitted, overwritten when present), the
classifier status table, and the OAuth-state nonce mint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>