import { randomBytes, timingSafeEqual } from 'node:crypto' import { createClient } from 'redis' export const SESSION_COOKIE = '__Host-remora-session' export class SessionStore { constructor(config, client = createClient({ url: config.sessionUrl })) { this.config = config this.client = client } async connect() { this.client.on('error', () => undefined) await this.client.connect() } get ready() { return this.client.isReady } async create(value) { const identifier = randomValue(32) await this.client.set(this.key(identifier), JSON.stringify(value), { EX: this.config.sessionTtlSeconds, }) return identifier } async read(identifier) { if (!validIdentifier(identifier)) return null const encoded = await this.client.get(this.key(identifier)) if (encoded === null) return null try { return JSON.parse(encoded) } catch { await this.destroy(identifier) return null } } async replace(identifier, value) { const replacement = await this.create(value) await this.destroy(identifier) return replacement } async destroy(identifier) { if (validIdentifier(identifier)) await this.client.del(this.key(identifier)) } key(identifier) { return `${this.config.sessionNamespace}${identifier}` } } export function sessionIdentifier(request) { const cookies = String(request.headers.cookie || '').split(';') for (const cookie of cookies) { const separator = cookie.indexOf('=') if (separator === -1) continue if (cookie.slice(0, separator).trim() === SESSION_COOKIE) return cookie.slice(separator + 1).trim() } return null } export function sessionCookie(identifier, config) { const secure = config.secureCookie ? '; Secure' : '' return `${SESSION_COOKIE}=${identifier}; Path=/; HttpOnly${secure}; SameSite=Strict` } export function expiredSessionCookie(config) { return `${sessionCookie('', config)}; Max-Age=0` } export function randomValue(bytes = 24) { return randomBytes(bytes).toString('base64url') } export function equalSecret(left, right) { const a = Buffer.from(String(left)) const b = Buffer.from(String(right)) return a.length === b.length && timingSafeEqual(a, b) } function validIdentifier(value) { return typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value) }