/** * TASK-0134 — the single source of truth for refresh-cookie attributes * (spec: "Cookie flag centralization — Secure/HttpOnly/SameSite/Path in one * place (single source for the cookie-flags test)"). * * The API sets exactly ONE cookie — the rotating opaque refresh token * (`factory_refresh`). Every `Set-Cookie` for it flows through * {@link buildRefreshCookieHeader}, so the cookie-flags suite * (tests/security/auth-paths.test.ts) asserts the flags in exactly one * place, and a future second cookie cannot drift from the policy: * * - `Path=/` — the refresh cookie must be present on /auth/refresh. * - `HttpOnly` — JS never reads it (the access token lives in * memory, not a cookie). * - `SameSite=Lax` — recorded decision (2026-10-03, TASK-0134): Lax for * the app cookie. The ONLY cookie-authenticated * route is `POST /auth/refresh` (the 401-retry); * Lax allows it from a same-site top-level context * while blocking cross-site form posts. `Strict` * was evaluated and rejected: it breaks the * deep-link → /login → app flow where the login * navigation is a cross-site top-level GET that * must NOT carry the (stale) cookie into a * surprise refresh. A separate CSRF-token cookie * is not needed because the web app authenticates * with a Bearer access token (never a cookie) — * recorded in audit-report.md (finding F-107). * - `Secure` — in production only (dev is plain HTTP on :4000; * Secure on plain HTTP would drop the cookie and * break the dev loop). * - `Max-Age` — from `REFRESH_TOKEN_TTL`; 0 clears. */ import { REFRESH_COOKIE } from "../constants.js"; export interface CookieHeaderOptions { /** `null` clears the cookie (empty value + Max-Age=0). */ maxAgeSeconds: number; /** Emit the `Secure` flag (production only). */ secure: boolean; } /** * Build the full `Set-Cookie` header value for the refresh cookie. The flag * ORDER is part of the e2e contract (tests/e2e/global-setup.ts parses this * exact shape) — do not reorder. */ export function buildRefreshCookieHeader( token: string | null, opts: CookieHeaderOptions ): string { const value = token === null ? "" : encodeURIComponent(token); const flags = ["Path=/", "HttpOnly", "SameSite=Lax"]; if (opts.secure) flags.push("Secure"); flags.push(`Max-Age=${token === null ? 0 : Math.max(0, Math.floor(opts.maxAgeSeconds))}`); return `${REFRESH_COOKIE}=${value}; ${flags.join("; ")}`; }