mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
The shipped Fitbit integration spoke the Google Health API, whose Restricted scopes need brand verification plus an annual CASA assessment before leaving testing mode — a wall most self-hosters cannot clear. Retarget the transport to the classic api.fitbit.com Web API: a self-hoster registers an app at dev.fitbit.com in minutes, no CASA. Forks only the transport; the upsert/dedup/rollup tail, source-priority, backfill, poll worker, and status ledger are reused unchanged. - OAuth: www.fitbit.com/oauth2/authorize + api.fitbit.com/oauth2/token, PKCE (S256), classic self-serve scopes. - Refresh tokens rotate (one-time use): persist the new token every refresh, with a defensive keep-existing guard on a malformed reply. - Per-endpoint date-range fetchers chunked to the API range caps; the backfill horizon and poll concurrency are tuned for the 150 req/h-per-user budget. - Verified classic-shape extractors for weight, body fat, SpO2, HRV (dailyRmssd), resting HR, respiratory rate, steps, distance, active calories, floors, VO2 max (range midpoint), per-segment sleep stages, and workouts. - Skips intraday HR (needs a per-app grant; daily resting HR covers it) and skin temperature (a baseline delta with no honest absolute slot).
37 lines
1.5 KiB
TypeScript
37 lines
1.5 KiB
TypeScript
/**
|
|
* Shared constants + mint helper for the Fitbit OAuth state nonce ledger.
|
|
* Mirrors `src/lib/whoop/oauth-state.ts`. (v1.20.0 — the PKCE `code_verifier`
|
|
* lives on the `FitbitOAuthState` row; the connect route mints it via the
|
|
* client's `generatePkcePair`.)
|
|
*
|
|
* The nonce is random + opaque; a short-lived `FitbitOAuthState` row carries
|
|
* the `(nonce → userId)` mapping so the user id never leaks into request logs
|
|
* or network captures via the OAuth `state` param. The connect route, the
|
|
* callback route, and the cleanup cron all read these constants so the
|
|
* contract has one source of truth and the unit tests can pin it without
|
|
* importing either route file.
|
|
*/
|
|
import { randomBytes } from "node:crypto";
|
|
|
|
/**
|
|
* Cookie name carrying the in-flight state nonce. httpOnly + Secure +
|
|
* sameSite:lax at the connect route (mirrors the WHOOP cookie shape).
|
|
*/
|
|
export const FITBIT_OAUTH_STATE_COOKIE = "fitbit_state" as const;
|
|
|
|
/**
|
|
* 10-minute TTL on the state row. Matches the cookie `maxAge`. Long enough to
|
|
* cover a user approving the Fitbit consent prompt; short enough that an
|
|
* abandoned handshake doesn't strand a row for hours.
|
|
*/
|
|
export const FITBIT_OAUTH_STATE_TTL_MS = 10 * 60 * 1000;
|
|
|
|
/**
|
|
* Mint a fresh state nonce. 16 random bytes → 22 base64url chars without
|
|
* padding — 128 bits of entropy, aligned with the OAuth 2.0 §10.10
|
|
* "at least 128 bits" recommendation.
|
|
*/
|
|
export function mintFitbitOAuthStateNonce(): string {
|
|
return randomBytes(16).toString("base64url");
|
|
}
|