import { SignJWT, jwtVerify } from "jose"; import { config } from "../config.js"; const secret = new TextEncoder().encode(config.JWT_SECRET); const ISSUER = "stoat-mbot"; export interface SessionClaims { sub: string; username: string; /** "session" for the panel cookie, "link" for one-time links from chat. */ typ: "session" | "link"; /** Present on chat links: the server the link was issued for. */ srv?: string; } export async function signSession(userId: string, username: string): Promise { return new SignJWT({ username, typ: "session" }) .setProtectedHeader({ alg: "HS256" }) .setSubject(userId) .setIssuer(ISSUER) .setIssuedAt() .setExpirationTime(`${config.SESSION_TTL_HOURS}h`) .sign(secret); } export async function signPanelLink( userId: string, username: string, serverId: string, ): Promise { return new SignJWT({ username, typ: "link", srv: serverId }) .setProtectedHeader({ alg: "HS256" }) .setSubject(userId) .setIssuer(ISSUER) .setIssuedAt() .setExpirationTime("10m") .sign(secret); } export async function verifyToken(token: string): Promise { try { const { payload } = await jwtVerify(token, secret, { issuer: ISSUER }); if (!payload.sub || (payload["typ"] !== "session" && payload["typ"] !== "link")) return null; return { sub: payload.sub, username: String(payload["username"] ?? "unknown"), typ: payload["typ"] as "session" | "link", srv: typeof payload["srv"] === "string" ? payload["srv"] : undefined, }; } catch { return null; } }