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>