2026-03-29 00:32:24 -04:00

75 lines
2.0 KiB
TypeScript

import { cookies } from "next/headers";
import { createHmac } from "crypto";
const SECRET = "sec-cybert-label-session-key";
const SESSION_COOKIE = "session";
const SESSION_MAX_AGE_MS = 8 * 60 * 60 * 1000; // 8 hours
interface SessionPayload {
annotatorId: string;
createdAt: number;
}
function sign(payload: string): string {
return createHmac("sha256", SECRET).update(payload).digest("hex");
}
function encodeSession(payload: SessionPayload): string {
const json = JSON.stringify(payload);
const b64 = Buffer.from(json).toString("base64");
const signature = sign(b64);
return `${b64}.${signature}`;
}
export function verifyAndDecode(raw: string): SessionPayload | null {
const dotIndex = raw.lastIndexOf(".");
if (dotIndex === -1) return null;
const b64 = raw.slice(0, dotIndex);
const signature = raw.slice(dotIndex + 1);
const expected = sign(b64);
if (signature !== expected) return null;
try {
const json = Buffer.from(b64, "base64").toString("utf-8");
const payload = JSON.parse(json) as SessionPayload;
if (!payload.annotatorId || !payload.createdAt) return null;
const age = Date.now() - payload.createdAt;
if (age > SESSION_MAX_AGE_MS) return null;
return payload;
} catch {
return null;
}
}
export async function createSession(annotatorId: string): Promise<void> {
const cookieStore = await cookies();
const value = encodeSession({ annotatorId, createdAt: Date.now() });
cookieStore.set(SESSION_COOKIE, value, {
httpOnly: true,
sameSite: "lax",
path: "/",
maxAge: SESSION_MAX_AGE_MS / 1000,
});
}
export async function getSession(): Promise<{ annotatorId: string } | null> {
const cookieStore = await cookies();
const raw = cookieStore.get(SESSION_COOKIE)?.value;
if (!raw) return null;
const payload = verifyAndDecode(raw);
if (!payload) return null;
return { annotatorId: payload.annotatorId };
}
export async function destroySession(): Promise<void> {
const cookieStore = await cookies();
cookieStore.delete(SESSION_COOKIE);
}