Files
HealthLog/src/lib/google-health/credentials.ts
T
Marc-André Bombeck fb17a4e9c6 feat(google-health): add the Google Health API transport
Add the transport that reads a user's device data from the Google Health
API (health.googleapis.com/v4): Google OAuth with PKCE and offline
access, the paginated dataPoints reader with a daily-rollup fallback for
the types that lack a list method, the metric/activity/sleep/workout
mappers onto the shared measurement shape, and a response classifier
that separates retryable errors, hard rejects, and the re-consent case.

Access tokens live an hour and refresh tokens are reused, not rotated. A
refresh that comes back invalid_grant raises a distinct reauth-required
signal so the app can prompt a fresh consent rather than fail silently —
the periodic re-consent a Testing-mode Google client needs. The four
core readonly scopes ship by default; the Pixel Watch ECG and
irregular-rhythm scopes stay behind an opt-in flag.

Only the transport is new: the mapped-measurement output, the
upsert/dedup/rollup tail, and the source ladders are the shared ones.
2026-07-03 14:13:16 +02:00

42 lines
1.3 KiB
TypeScript

/**
* Helper to retrieve per-user Google Health OAuth credentials from the database.
* Mirrors `src/lib/whoop/credentials.ts`.
*
* Google Health ships per-user BYO-keys: each self-hoster registers their own
* Google Cloud OAuth client and pastes the client id/secret into Settings,
* stored encrypted on `User` (the Restricted-scope verification + annual CASA
* assessment is per-OAuth-client, so a single shared app is unworkable for a
* multi-operator product).
*/
import { prisma } from "@/lib/db";
import { decrypt } from "@/lib/crypto";
import type { GoogleHealthCredentials } from "./client";
/**
* Fetch and decrypt the user's Google Health API credentials.
* Returns null if the user has not configured credentials.
*/
export async function getUserGoogleHealthCredentials(
userId: string,
): Promise<GoogleHealthCredentials | null> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
googleHealthClientIdEncrypted: true,
googleHealthClientSecretEncrypted: true,
},
});
if (
!user?.googleHealthClientIdEncrypted ||
!user?.googleHealthClientSecretEncrypted
) {
return null;
}
return {
clientId: decrypt(user.googleHealthClientIdEncrypted),
clientSecret: decrypt(user.googleHealthClientSecretEncrypted),
};
}