Files
HealthLog/src/lib/withings/oauth-state.ts
T
Marc-André Bombeck 66a7f8b6fa chore(release): v1.4.47 — audit-backlog closure + drag-to-reorder + Coach disable + OAuth nonce ledger
Eight changes: W0 idempotency test pin, W1 legacy column drop, W2 Textarea primitive, W3 Coach disable toggle, W4 dnd-kit drag-to-reorder, W5 onboarding 24h gate, W6 Withings OAuth nonce ledger, W7+W8 legacy_form_total drop + Coach onLine pre-check. 6-axis QA reconciled. Migrations 0076/0077/0078 required pre-image. Tests 5093 → ~5170.
2026-05-22 09:18:52 +02:00

49 lines
2.0 KiB
TypeScript

/**
* v1.4.47 W6 — shared constants + mint helper for the Withings OAuth
* state nonce ledger.
*
* The legacy implementation (v1.4.42-) built the state as
* `${user.id}:${random16}` and persisted it on both the URL state
* param and the `withings_state` cookie. The cookie is httpOnly +
* Secure + sameSite:lax so XSS exfiltration is closed, but the user
* id still leaks into request logs / network captures (the v1.4.43
* security audit L-1 finding). The new shape decouples the two: the
* nonce is random + opaque, and a short-lived `WithingsOAuthState`
* row carries the `(nonce → userId)` mapping.
*
* The helpers live on their own module so the connect route, the
* callback route, and the cleanup cron all read the same source of
* truth — and the unit tests can pin the contract without importing
* either route file.
*/
import { randomBytes } from "node:crypto";
/**
* Cookie name carrying the in-flight state nonce. Same string as the
* legacy `withings_state` cookie so a deploy mid-flight doesn't
* orphan a user's open Withings tab — the cookie shape is what
* changed (nonce-only instead of `${userId}:${nonce}`), not the
* cookie name.
*/
export const WITHINGS_OAUTH_STATE_COOKIE = "withings_state" as const;
/**
* 10-minute TTL on the state row. Matches the cookie `maxAge`. Long
* enough to cover a user typing their Withings credentials and
* approving the consent prompt; short enough that an abandoned
* handshake doesn't leave a row stranded for the daily cleanup sweep
* to pick up hours later.
*/
export const WITHINGS_OAUTH_STATE_TTL_MS = 10 * 60 * 1000;
/**
* Mint a fresh state nonce. 16 random bytes → 22 base64url chars
* (Node's `randomBytes(16).toString("base64url")` returns exactly 22
* characters without padding for any 16-byte input). 128 bits of
* entropy — well past the CSRF threshold and aligned with the OAuth
* 2.0 §10.10 recommendation of "at least 128 bits".
*/
export function mintWithingsOAuthStateNonce(): string {
return randomBytes(16).toString("base64url");
}