/** * Stateless session: a signed JWT stored in an httpOnly cookie. The payload is * small (the user's DB id) — everything else is looked up from the database. */ import { SignJWT, jwtVerify } from "jose"; import { cookies } from "next/headers"; import { serverEnv } from "../env"; const COOKIE_NAME = "ttsa_session"; const MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days type SessionPayload = { uid: number }; function secretKey(): Uint8Array { return new TextEncoder().encode(serverEnv.sessionSecret()); } /** * Cookie SameSite/Secure attributes. In the embeddable Space (a cross-site * iframe) cookies must be SameSite=None; Secure to be sent at all; locally we * use Lax over http. Exported so the OAuth-state cookie matches. */ export function cookieSecurity(): { sameSite: "none" | "lax"; secure: boolean; } { return serverEnv.isEmbedded() ? { sameSite: "none", secure: true } : { sameSite: "lax", secure: process.env.NODE_ENV === "production" }; } export async function createSession(userId: number): Promise { const token = await new SignJWT({ uid: userId }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(`${MAX_AGE_SECONDS}s`) .sign(secretKey()); const store = await cookies(); store.set(COOKIE_NAME, token, { httpOnly: true, ...cookieSecurity(), path: "/", maxAge: MAX_AGE_SECONDS, }); } export async function readSession(): Promise { const store = await cookies(); const token = store.get(COOKIE_NAME)?.value; if (!token) return null; try { const { payload } = await jwtVerify(token, secretKey()); return typeof payload.uid === "number" ? payload.uid : null; } catch { return null; } } export async function destroySession(): Promise { const store = await cookies(); // Overwrite with an expired cookie carrying the SAME attributes it was set // with. A bare delete(name) doesn't reliably clear a SameSite=None; Secure // cookie (as used in the HF iframe) — the browser needs a matching Set-Cookie. store.set(COOKIE_NAME, "", { httpOnly: true, ...cookieSecurity(), path: "/", maxAge: 0, expires: new Date(0), }); store.delete(COOKIE_NAME); }