Files
HealthLog/src/lib/whoop/credentials.ts
T
Marc-André Bombeck 2258cbb489 feat(whoop): WHOOP v2 API client, response classifier, credential resolution
Add the WHOOP integration data layer on copy-of-Withings rails:

- client.ts: hand-rolled fetch over safeFetch (no SDK) — OAuth authorize
  URL / code exchange / rotating-refresh, the four v2 collection fetchers
  (recovery, sleep, cycle, workout) with next_token pagination, single
  body-measurement + profile reads, and the field->Measurement mappers
  (RECOVERY_SCORE/HRV_RMSSD/RESTING_HEART_RATE/OXYGEN_SATURATION/
  SKIN_TEMPERATURE, per-stage SLEEP_DURATION ms->min, SLEEP_NEED/SLEEP_*,
  DAY_STRAIN/ENERGY_EXPENDITURE_KJ, source=WHOOP). Day strain maps to
  DAY_STRAIN and RMSSD to HRV_RMSSD so the device-native values never
  share a bucket with the COMPUTED STRAIN_SCORE / SDNN proxies.
- response-classifier.ts: HTTP-status-driven success/transient/
  reauth_required/persistent verdicts + WhoopApiError, mirroring the
  Withings reason-tag classifier.
- credentials.ts: per-user BYO-key resolution, decrypting the client
  id/secret from the User columns.
- oauth-state.ts: opaque-nonce mint helper + cookie/TTL constants.
- mapping.md: field->Measurement reference, kept in sync with the mappers.

Unit tests cover the classifier, pagination, every fetcher, the mappers
(ms->min, kJ->kcal factor, stage mapping, unscored-record handling), and
credential resolution against mocked WHOOP JSON. No routes / OAuth flow /
sync workers / migration yet.
2026-06-04 01:05:06 +02:00

38 lines
1.1 KiB
TypeScript

/**
* Helper to retrieve per-user WHOOP OAuth credentials from the database.
* Mirrors `src/lib/withings/credentials.ts`.
*
* WHOOP ships per-user BYO-keys: each self-hoster registers their own WHOOP
* dev app and pastes the client id/secret into Settings, stored encrypted on
* `User` (the per-app authorized-user cap makes a single shared app
* unworkable for a multi-operator product).
*/
import { prisma } from "@/lib/db";
import { decrypt } from "@/lib/crypto";
import type { WhoopCredentials } from "./client";
/**
* Fetch and decrypt the user's WHOOP API credentials.
* Returns null if the user has not configured credentials.
*/
export async function getUserWhoopCredentials(
userId: string,
): Promise<WhoopCredentials | null> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
whoopClientIdEncrypted: true,
whoopClientSecretEncrypted: true,
},
});
if (!user?.whoopClientIdEncrypted || !user?.whoopClientSecretEncrypted) {
return null;
}
return {
clientId: decrypt(user.whoopClientIdEncrypted),
clientSecret: decrypt(user.whoopClientSecretEncrypted),
};
}