fix(oura): ask for the spo2 and heart_health scopes, explain a missing scope calmly

Oura's current authorization server enforces scopes per collection.
daily_spo2 needs spo2; vO2_max, daily_cardiovascular_age and
daily_resilience need heart_health. The authorize request now asks for
both.

A grant issued before keeps answering 401 on those four collections
while every other one succeeds. Such a 401 next to a success is now
recorded as a scope gap rather than a failure: it no longer counts
toward the reconnect threshold, it no longer forces a token refresh once
known, and the Oura card asks for one reconnect instead. The Withings
card names the readings the account's API plan withholds.
This commit is contained in:
Marc-André Bombeck committed 2026-10-14 19:01:02 +02:00
1 parent 39b0cabb43
commit 044f9008ba
29 files changed
+824 -101

No files matched your search

+14
View File
@@ -34,6 +34,8 @@ import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { fitbitCredentialsSchema } from "@/lib/validations/fitbit";
import { googleHealthCredentialsSchema } from "@/lib/validations/google-health";
import { WITHINGS_PLAN_FEATURES } from "@/lib/withings/plan-features";
import { OURA_SCOPE_GATED_COLLECTIONS } from "@/lib/oura/scope";
import { ouraCredentialsSchema } from "@/lib/validations/oura";
import { polarCredentialsSchema } from "@/lib/validations/polar";
import { stravaCredentialsSchema } from "@/lib/validations/strava";
@@ -925,12 +927,24 @@ const integrationEnvelopeEntry = z
backfillCompleted: z.boolean().nullable().optional(),
scope: z.string().nullable().optional().describe("Withings only."),
hasActivityScope: z.boolean().optional().describe("Withings only."),
planUnavailable: z
.array(z.enum(WITHINGS_PLAN_FEATURES))
.optional()
.describe(
"Withings only. The readings this connection's Withings API plan withholds (status 277 since the 2026-10-12 plan split), deduplicated and in display order. Empty when the plan refuses nothing. The sync skips these and keeps importing everything else; the list refreshes when the weekly re-probe asks again.",
),
needsReauth: z.boolean().optional().describe("Google Health only."),
available: z.boolean().optional().describe("Oura / Polar / Strava only."),
hasOwnCredentials: z
.boolean()
.optional()
.describe("Oura / Polar / Strava only."),
scopeReconnect: z
.array(z.enum(OURA_SCOPE_GATED_COLLECTIONS))
.optional()
.describe(
"Oura only. Collections the stored grant does not cover: they answered 401 while sibling collections succeeded with the same token. Not a failure and not counted toward the reconnect threshold; one reconnect asks for the missing scopes. Empty when the grant covers everything HealthLog reads.",
),
hasToken: z.boolean().optional().describe("Nightscout only."),
allowPrivateHost: z.boolean().optional().describe("Nightscout only."),
})