mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Resilience sits behind Oura's `stress` scope, not `heart_health`, so a
connection that granted every scope HealthLog asked for still answered 401
on `daily_resilience`. `src/lib/oura/scope.ts` now maps every collection the
sync reads to the scope the live API enforces and is the one source for the
authorize request, the per-collection permission detection, the reconnect
hint and the settings card. The request asks for `stress` as well.
Oura names a missing scope in its 401 body ("Token is not authorized access
stress scope."), while a dead token reads differently. A collection refusal
now carries that scope: it spends no token refresh, never counts toward the
reconnect threshold and marks only the collections it names. A 403 (a lapsed
membership) is a persistent failure rather than a reconnect. The callback
also records what the token response says was granted, so an unticked
permission shows on the card before the first sync.
A browser can deliver an OAuth callback twice with the same code. Both
requests exchanged it, and Oura answers a reused code by revoking the token
the first exchange issued. Every connect callback (Oura, Polar, Strava,
WHOOP, Withings, Fitbit, Google Health) now claims its state in
`oauth_callback_claims` before the exchange: one request exchanges, the
duplicate waits for it and redirects to the same page without touching any
token. The ledger flows claim inside the transaction that consumes their
state row. A failed exchange settles with its error page, and a new connect
mints a new state.
Oura token refreshes are serialised per user with an advisory lock, so
overlapping syncs no longer spend the same single-use refresh token, and the
write stays conditional so a reconnect's new grant is never overwritten by a
refresh of the old one.
29 lines
1.1 KiB
SQL
29 lines
1.1 KiB
SQL
-- Single-use OAuth callbacks.
|
|
--
|
|
-- A browser can deliver an OAuth callback twice with the same code and state,
|
|
-- a few hundred milliseconds apart. The connect flows that sign their state
|
|
-- (Oura, Polar, Strava) kept no record of it, so both requests exchanged the
|
|
-- code; the second was refused, and Oura answers a reused code by revoking the
|
|
-- token the first exchange had just issued. A row here is the claim on one
|
|
-- callback: the first request inserts it before the exchange, a duplicate
|
|
-- finds it taken and is sent where the first one went. No user id, token or
|
|
-- code is stored; the key is a hash of the provider and the state.
|
|
--
|
|
-- Reversibility:
|
|
-- DROP TABLE IF EXISTS "oauth_callback_claims";
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "oauth_callback_claims" (
|
|
"key" TEXT NOT NULL,
|
|
"provider" TEXT NOT NULL,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"settled_at" TIMESTAMP(3),
|
|
"location" TEXT,
|
|
|
|
CONSTRAINT "oauth_callback_claims_pkey" PRIMARY KEY ("key")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "oauth_callback_claims_expires_at_idx" ON "oauth_callback_claims"("expires_at");
|