Files
anouma/lib/meeting/token.ts
T
maro 45261a0461 Initial commit: ANOUMA website with Payload CMS, WebRTC meetings, and booking system
- Next.js 16 App Router site with the ANOUMA design system
- Payload CMS (PostgreSQL) for offers, events, posts and page content
- WebRTC video-call system with custom signaling server
- SMTP email reminders and booking-request notifications
- Customer accounts, calendar-based availability, and booking workflow
2026-08-25 16:40:51 +02:00

67 lines
2.0 KiB
TypeScript

import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
export type MeetingRole = "host" | "participant";
export type MeetingTokenPayload = {
eventSlug: string;
participantId: string;
name: string;
role: MeetingRole;
exp: number; // unix ms
};
function secret(): string {
const value = process.env.MEETING_SESSION_SECRET;
if (!value) throw new Error("MEETING_SESSION_SECRET is not set");
return value;
}
function base64url(input: Buffer | string): string {
return Buffer.from(input).toString("base64url");
}
function sign(data: string): string {
return createHmac("sha256", secret()).update(data).digest("base64url");
}
/** Issues a short-lived, self-contained, signed token for one meeting session. */
export function createMeetingToken(args: {
eventSlug: string;
name: string;
role: MeetingRole;
ttlMs?: number;
}): { token: string; participantId: string } {
const participantId = randomUUID();
const payload: MeetingTokenPayload = {
eventSlug: args.eventSlug,
participantId,
name: args.name,
role: args.role,
exp: Date.now() + (args.ttlMs ?? 6 * 60 * 60 * 1000), // 6h default
};
const body = base64url(JSON.stringify(payload));
const signature = sign(body);
return { token: `${body}.${signature}`, participantId };
}
/** Verifies signature + expiry. Returns null if invalid/expired/tampered. */
export function verifyMeetingToken(token: string): MeetingTokenPayload | null {
const parts = token.split(".");
if (parts.length !== 2) return null;
const [body, signature] = parts;
const expected = sign(body);
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
try {
const payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8")) as MeetingTokenPayload;
if (typeof payload.exp !== "number" || payload.exp < Date.now()) return null;
if (!payload.eventSlug || !payload.participantId || !payload.name || !payload.role) return null;
return payload;
} catch {
return null;
}
}