import crypto from 'crypto' const JWT_SECRET = process.env.JWT_SECRET || 'change-me-same-across-all-sites' const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || '' export interface SharedSession { username: string displayName: string iat: number exp: number } function base64url(str: string): string { return Buffer.from(str).toString('base64url') } export function signSharedJwt( payload: { username: string; displayName: string }, expiresIn: number = 7 * 24 * 60 * 60 ): string { const header = { alg: 'HS256', typ: 'JWT' } const now = Math.floor(Date.now() / 1000) const body = { ...payload, iat: now, exp: now + expiresIn } const segments = [base64url(JSON.stringify(header)), base64url(JSON.stringify(body))] const signingInput = segments.join('.') segments.push( crypto.createHmac('sha256', JWT_SECRET).update(signingInput).digest('base64url') ) return segments.join('.') } export function verifySharedJwt(token: string): SharedSession | null { try { const parts = token.split('.') if (parts.length !== 3) return null const signingInput = parts.slice(0, 2).join('.') const expectedSig = crypto.createHmac('sha256', JWT_SECRET) .update(signingInput).digest('base64url') if (parts[2] !== expectedSig) return null const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString()) if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null return { username: payload.username, displayName: payload.displayName, iat: payload.iat, exp: payload.exp, } } catch { return null } } export function sharedCookieConfig(maxAge: number = 7 * 24 * 60 * 60) { return { name: 'tlyq_session', httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' as const, domain: COOKIE_DOMAIN, path: '/', maxAge, } }