Files
chad/server/utils/livekit.ts
2026-09-05 14:46:17 +06:00

79 lines
2.3 KiB
TypeScript

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
}
}