275 lines · 9.9 KB
import { env, waitUntil } from "cloudflare:workers";
import { deleteCookie, getCookie, getRequestHeader, setCookie } from "@tanstack/react-start/server";
import { all, first, run } from "./db";
import { randomId, randomToken, sha256Hex } from "./crypto";
import type { UserRow, Viewer } from "~/lib/types";
import { noteSignIn } from "./notify/sign-in";

const SESSION_COOKIE_BASE = "gitfor_session";
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);

/** `__Host-` (Secure, Path=/, no Domain) everywhere browsers accept Secure cookies; a plain-http dev host gets neither. */
export function cookieSecure(): boolean {
  const url = new URL(env.PUBLIC_URL);
  return url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname);
}

export function cookieName(base: string): string {
  return cookieSecure() ? `__Host-${base}` : base;
}

/** Attributes for hand-built Set-Cookie headers. */
export function cookieAttributes(maxAge: number): string {
  return `Path=/; HttpOnly;${cookieSecure() ? " Secure;" : ""} SameSite=Lax; Max-Age=${maxAge}`;
}
/** Absolute lifetime of a browser session; the cookie carries the same Max-Age. */
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30;
/** Sliding idle timeout: a session unused for this long is dead even if the absolute TTL has not passed (AUTH-A12). */
const SESSION_IDLE_MS = 14 * 24 * 60 * 60 * 1000;
/** `last_seen_at` is written at most this often so ordinary browsing does not cost a D1 write per request. */
const SESSION_TOUCH_MS = 5 * 60 * 1000;
const MAX_USER_AGENT = 200;
export const PAT_PREFIX = "gitfor_pat";

type SessionRow = {
  id: string;
  user_id: string;
  ip: string | null;
  user_agent: string | null;
  created_at: number;
  last_seen_at: number;