feat(fitbit): add the Fitbit connection data model and FITBIT measurement source

Lay the data-model foundation for the Fitbit/Pixel integration over the
Google Health API, mirroring the WHOOP provider. No OAuth, sync, or UI yet.

- Prisma: FitbitConnection (1:1 per user, encrypted access/refresh tokens)
  and FitbitOAuthState models, two encrypted BYO-credential columns on User,
  and FITBIT appended to the MeasurementSource enum. Migration
  0117_v1120_fitbit_integration is additive only — one enum value plus two
  tables and two nullable columns, no new MeasurementType (every launch
  metric maps onto an existing type).
- Wire the two new encrypted columns and the FitbitConnection token columns
  into scripts/rotate-encryption-key.ts.
- FITBIT touchpoints: the read measurementSourceEnum (not the writable set),
  the DEFAULT_SOURCE_PRIORITY and workout-source ladders, the sources-section
  label map, the IntegrationKey union plus its display name, and a
  fitbit()/fitbitStatus() query-key pair. The source-rank SQL and read-tier
  collapse pick FITBIT up from the ladders with no code change.
- Add the FITBIT source label to all six locale bundles and regenerate the
  OpenAPI contract so the source enum carries FITBIT for the iOS decoder.
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 21:02:10 +02:00
1 parent 045da70133
commit 86b2774567
18 files changed
+368 -20

No files matched your search

+24
View File
@@ -102,6 +102,8 @@ async function main() {
withingsClientSecretEncrypted: true,
whoopClientIdEncrypted: true,
whoopClientSecretEncrypted: true,
fitbitClientIdEncrypted: true,
fitbitClientSecretEncrypted: true,
},
});
@@ -119,6 +121,8 @@ async function main() {
"withingsClientSecretEncrypted",
"whoopClientIdEncrypted",
"whoopClientSecretEncrypted",
"fitbitClientIdEncrypted",
"fitbitClientSecretEncrypted",
];
for (const field of userFields) {
const r = await rotateField(
@@ -176,6 +180,26 @@ async function main() {
results.push(r);
}
// ───── FitbitConnection table (accessToken / refreshToken) ─────
const fitbit = await prisma.fitbitConnection.findMany({
select: { id: true, accessToken: true, refreshToken: true },
});
for (const field of ["accessToken", "refreshToken"] as const) {
const r = await rotateField(
"FitbitConnection",
field,
fitbit,
(f) => f[field],
async (id, ciphertext) => {
await prisma.fitbitConnection.update({
where: { id },
data: { [field]: ciphertext } as Record<string, string>,
});
},
);
results.push(r);
}
// ───── AppSettings table (singleton typically) ─────
const settings = await prisma.appSettings.findMany({
select: {