mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Two connect-in-app enhancements for the native client. return_scheme: GET /api/whoop/connect accepts an optional custom scheme, threaded through the server-side OAuth-state row so it survives the round trip without ever riding in the URL or cookie. When a valid scheme is present, the callback's final redirect targets <scheme>://whoop?whoop= connected|error&reason=... so an in-app web session auto-completes on its custom-scheme match; absent or invalid, the web settings redirect is unchanged. Schemes validate against a strict allowlist (custom app scheme only, ^[a-z][a-z0-9.+-]*$, http/https/javascript/data/file/vbscript rejected, pinned to dev.healthlog.app). A rejected scheme falls back to the web redirect and never reflects an arbitrary value. Bearer connect ticket: a purely Bearer-authenticated client holds no web session cookie, so it cannot start the handshake through the cookie-gated connect route. POST /api/whoop/connect/ticket (Bearer) mints a one-time, ~60s, opaque ticket bound to the user; only its HMAC-SHA256 hash is stored (the raw value never persists). GET /api/whoop/connect?ticket=<opaque> accepts the ticket in lieu of a cookie, consumes it atomically (single-use, reuse rejected), sets the nonce cookie, and 302s to WHOOP exactly as the cookie path. Expired, consumed, or invalid tickets return a typed 401. The two combine. The daily WHOOP OAuth-state cleanup also sweeps expired ticket rows. The ticket query param is added to the egress redaction denylist so the opaque value never reaches logs or error reports. Migration 0124 adds whoop_connect_tickets (id, user_id FK cascade, token_hash unique, expires_at, consumed_at, created_at) and a nullable return_scheme column on whoop_oauth_states. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 221b362011d52dda314139e0be7cc195c47d016e)
56 lines
2.7 KiB
SQL
56 lines
2.7 KiB
SQL
-- v1.12.2 — WHOOP connect-in-app enhancements (two iOS-requested asks).
|
|
--
|
|
-- 1. `whoop_oauth_states` += `return_scheme` (nullable). The native client may
|
|
-- pass `?return_scheme=<custom-scheme>` to `GET /api/whoop/connect`; the
|
|
-- connect route validates it against a strict allowlist and stores it on the
|
|
-- in-flight state row so it survives the OAuth round-trip server-side (never
|
|
-- in the URL or cookie). The callback reads it off the consumed row to send
|
|
-- its FINAL redirect to `<scheme>://whoop?whoop=connected|error&reason=…`
|
|
-- instead of the web settings URL. Null = unchanged web redirect.
|
|
--
|
|
-- 2. `whoop_connect_tickets` — a one-time, Bearer-mintable connect ticket so a
|
|
-- purely Bearer-authenticated native client (no web-session cookie) can
|
|
-- start the WHOOP handshake. The client mints a ticket via an authenticated
|
|
-- `POST /api/whoop/connect/ticket`, then opens
|
|
-- `GET /api/whoop/connect?ticket=<opaque>` in an in-app web session. Only
|
|
-- the HMAC-SHA256 hash of the opaque ticket is stored (`token_hash`, keyed
|
|
-- by `API_TOKEN_HMAC_KEY`); the raw value never persists. The ticket is
|
|
-- single-use (`consumed_at` stamped atomically on first use) and short-lived
|
|
-- (~60s `expires_at`). Mirrors `whoop_oauth_states` in shape + lifecycle.
|
|
--
|
|
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
|
|
--
|
|
-- Reversibility: the column drops with `DROP COLUMN IF EXISTS "return_scheme"`,
|
|
-- the table with `DROP TABLE IF EXISTS "whoop_connect_tickets"`.
|
|
|
|
-- ── 1. whoop_oauth_states — native return-scheme carrier ──────────────
|
|
ALTER TABLE "whoop_oauth_states"
|
|
ADD COLUMN IF NOT EXISTS "return_scheme" TEXT;
|
|
|
|
-- ── 2. whoop_connect_tickets — one-time Bearer-startable connect ticket ─
|
|
CREATE TABLE IF NOT EXISTS "whoop_connect_tickets" (
|
|
"id" TEXT NOT NULL,
|
|
"user_id" TEXT NOT NULL,
|
|
"token_hash" TEXT NOT NULL,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"expires_at" TIMESTAMP(3) NOT NULL,
|
|
"consumed_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "whoop_connect_tickets_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "whoop_connect_tickets_token_hash_key"
|
|
ON "whoop_connect_tickets" ("token_hash");
|
|
|
|
CREATE INDEX IF NOT EXISTS "whoop_connect_tickets_expires_at_idx"
|
|
ON "whoop_connect_tickets" ("expires_at");
|
|
|
|
DO $$ BEGIN
|
|
ALTER TABLE "whoop_connect_tickets"
|
|
ADD CONSTRAINT "whoop_connect_tickets_user_id_fkey"
|
|
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN NULL;
|
|
END $$;
|