mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
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.
49 lines
2.0 KiB
TypeScript
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");
|
|
}
|