import type { Session, User } from 'lucia' import { Buffer } from 'node:buffer' import { AccessToken } from 'livekit-server-sdk' import { SESSION_EXPIRES_IN } from '../auth/lucia.ts' import prisma from '../prisma/client.ts' /** * Re-issue the token slightly before it actually expires, so a client that * caches the /me response for a while still gets a usable token. */ const REFRESH_BEFORE_EXPIRY_SECONDS = 60 * 60 export async function createLivekitToken(user: User, ttlSeconds: number) { const apiKey = process.env.LIVEKIT_API_KEY const apiSecret = process.env.LIVEKIT_API_SECRET if (!apiKey || !apiSecret) throw new Error('LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set') const token = new AccessToken(apiKey, apiSecret, { identity: user.id, name: user.displayName, ttl: ttlSeconds, }) token.addGrant({ room: process.env.LIVEKIT_ROOM || 'chad', roomJoin: true, canPublish: true, canSubscribe: true, canPublishData: true, canUpdateOwnMetadata: true, }) return token.toJwt() } /** Mints the token that gets stored on a session at the moment it is created. */ export function createSessionLivekitToken(user: User) { return createLivekitToken(user, SESSION_EXPIRES_IN.seconds()) } /** * Returns the token stored alongside the session, re-issuing and persisting it * when it is missing (session predates the column) or about to expire (Lucia * extends `expiresAt` on active sessions, outliving the original token). */ export async function getSessionLivekitToken(user: User, session: Session) { if (session.livekitToken && !isExpiringSoon(session.livekitToken)) return session.livekitToken const ttlSeconds = Math.max( Math.ceil((session.expiresAt.getTime() - Date.now()) / 1000), REFRESH_BEFORE_EXPIRY_SECONDS, ) const livekitToken = await createLivekitToken(user, ttlSeconds) await prisma.session.update({ where: { id: session.id }, data: { livekitToken }, }) session.livekitToken = livekitToken return livekitToken } function isExpiringSoon(token: string) { try { const [, payload] = token.split('.') const { exp } = JSON.parse(Buffer.from(payload, 'base64url').toString()) return typeof exp !== 'number' || exp - REFRESH_BEFORE_EXPIRY_SECONDS <= Date.now() / 1000 } catch { return true } }