← back to Govarbitrage

src/lib/session.ts

43 lines

import { SignJWT, jwtVerify } from "jose";

// JWT session tokens (HS256) signed with AUTH_SECRET. Pure-JS (jose / Web
// Crypto) so this module works in BOTH the Edge middleware and Node handlers.

export const SESSION_COOKIE = "ga_session";
export const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 days

export interface SessionPayload {
  sub: string; // user id
  email: string;
  role: string; // ADMIN | ANALYST | VIEWER
}

function secret(): Uint8Array {
  const s = process.env.AUTH_SECRET;
  // The dev fallback is in the repo — a production deploy that forgot to set
  // AUTH_SECRET would mint forge-trivial sessions. Refuse to run instead.
  if (!s && process.env.NODE_ENV === "production") {
    throw new Error("AUTH_SECRET must be set in production — refusing to sign sessions with the dev fallback.");
  }
  return new TextEncoder().encode(s || "dev-only-change-me");
}

export async function createSession(payload: SessionPayload): Promise<string> {
  return new SignJWT({ email: payload.email, role: payload.role })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject(payload.sub)
    .setIssuedAt()
    .setExpirationTime(`${SESSION_MAX_AGE}s`)
    .sign(secret());
}

export async function verifySession(token: string): Promise<SessionPayload | null> {
  try {
    const { payload } = await jwtVerify(token, secret());
    if (!payload.sub || !payload.email) return null;
    return { sub: payload.sub, email: payload.email as string, role: (payload.role as string) ?? "VIEWER" };
  } catch {
    return null;
  }
}