feat(google-health): wire the OAuth routes, sync jobs, and source priority

Add the request surface and the background sync for the Google Health
provider, all built on the shared integration plumbing.

- Routes under /api/google-health: connect (mint the PKCE pair and state
  nonce, redirect to Google), callback (timing-safe CSRF, atomic state
  consume, token exchange, connection upsert, clear the reauth flag,
  enqueue backfill), credentials (encrypt the operator's BYO client id
  and secret field-by-field), disconnect, status, and a rate-limited
  manual sync.
- Jobs: a boot-time backfill, an hourly poll cohort, and a daily
  oauth-state cleanup, registered under the existing dead-queue contract.
- Rank GOOGLE_HEALTH just after Fitbit in the per-metric source ladders;
  it is the successor API for the same devices.
- Add GOOGLE_HEALTH to the readable measurement-source enum but leave it
  out of the writable set — the provider is server-owned, so a client
  cannot attribute a write to it.
- Register the two new encrypted token columns and the two BYO-client
  columns in the encryption-rotation registry and the rotation script.
This commit is contained in:
Marc-André Bombeck committed 2026-07-03 14:13:28 +02:00
1 parent fb17a4e9c6
commit b26a18d35e
15 files changed
+1107 -10

No files matched your search

+5
View File
@@ -55,6 +55,9 @@ export const ENCRYPTED_COLUMNS: readonly EncryptedColumn[] = [
{ model: "User", field: "whoopClientSecretEncrypted", kind: "string" },
{ model: "User", field: "fitbitClientIdEncrypted", kind: "string" },
{ model: "User", field: "fitbitClientSecretEncrypted", kind: "string" },
// v1.27.0 — Google Health (Fitbit / Pixel Watch) BYO OAuth client.
{ model: "User", field: "googleHealthClientIdEncrypted", kind: "string" },
{ model: "User", field: "googleHealthClientSecretEncrypted", kind: "string" },
{ model: "User", field: "nightscoutUrlEncrypted", kind: "string" },
{ model: "User", field: "nightscoutTokenEncrypted", kind: "string" },
{ model: "User", field: "polarAccessTokenEncrypted", kind: "string" },
@@ -82,6 +85,8 @@ export const ENCRYPTED_COLUMNS: readonly EncryptedColumn[] = [
{ model: "WhoopConnection", field: "refreshToken", kind: "string" },
{ model: "FitbitConnection", field: "accessToken", kind: "string" },
{ model: "FitbitConnection", field: "refreshToken", kind: "string" },
{ model: "GoogleHealthConnection", field: "accessToken", kind: "string" },
{ model: "GoogleHealthConnection", field: "refreshToken", kind: "string" },
// ───── AppSettings — operator credentials ─────
{ model: "AppSettings", field: "adminAiKeyEncrypted", kind: "string" },