mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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>
16 lines
607 B
TypeScript
16 lines
607 B
TypeScript
import { z } from "zod/v4";
|
|
|
|
/**
|
|
* Per-user Fitbit / Google Health BYO-key credentials. Each self-hoster
|
|
* registers their own Google Cloud OAuth client (the Restricted-scope brand
|
|
* verification + CASA assessment is per-OAuth-client, so a single shared app is
|
|
* unworkable for a multi-operator product) and pastes the client id/secret into
|
|
* Settings. Stored encrypted on `User`.
|
|
*/
|
|
export const fitbitCredentialsSchema = z.object({
|
|
clientId: z.string().min(1).max(200),
|
|
clientSecret: z.string().min(1).max(200),
|
|
});
|
|
|
|
export type FitbitCredentialsInput = z.infer<typeof fitbitCredentialsSchema>;
|