import * as fs from 'fs/promises'; import * as path from 'path'; import { AuthenticationState } from '../../../domain/value-objects/AuthenticationState'; import type { ILogger } from '../../../application/ports/ILogger'; interface Cookie { name: string; value: string; domain: string; expires: number; } interface StorageState { cookies: Cookie[]; origins: unknown[]; } /** * Known iRacing session cookie names to look for. * These are the primary authentication indicators. */ const IRACING_SESSION_COOKIES = [ 'irsso_members', 'authtoken_members', 'irsso', 'authtoken', ]; /** * iRacing domain patterns to match cookies against. */ const IRACING_DOMAINS = [ 'iracing.com', '.iracing.com', 'members.iracing.com', ]; const EXPIRY_BUFFER_SECONDS = 300; export class SessionCookieStore { private readonly storagePath: string; private logger?: ILogger; constructor(userDataDir: string, logger?: ILogger) { this.storagePath = path.join(userDataDir, 'session-state.json'); this.logger = logger; } private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record): void { if (this.logger) { if (level === 'error') { this.logger.error(message, undefined, context); } else { this.logger[level](message, context); } } } getPath(): string { return this.storagePath; } async read(): Promise { try { const content = await fs.readFile(this.storagePath, 'utf-8'); return JSON.parse(content) as StorageState; } catch { return null; } } async write(state: StorageState): Promise { await fs.writeFile(this.storagePath, JSON.stringify(state, null, 2), 'utf-8'); } async delete(): Promise { try { await fs.unlink(this.storagePath); } catch { // File may not exist, ignore } } /** * Validate cookies and determine authentication state. * * Looks for iRacing session cookies by checking: * 1. Domain matches iRacing patterns * 2. Cookie name matches known session cookie names OR * 3. Any cookie from members.iracing.com domain (fallback) */ validateCookies(cookies: Cookie[]): AuthenticationState { // Log all cookies for debugging this.log('debug', 'Validating cookies', { totalCookies: cookies.length, cookieNames: cookies.map(c => ({ name: c.name, domain: c.domain })) }); // Filter cookies from iRacing domains const iracingDomainCookies = cookies.filter(c => IRACING_DOMAINS.some(domain => c.domain === domain || c.domain.endsWith(domain) ) ); this.log('debug', 'iRacing domain cookies found', { count: iracingDomainCookies.length, cookies: iracingDomainCookies.map(c => ({ name: c.name, domain: c.domain, expires: c.expires, expiresDate: new Date(c.expires * 1000).toISOString() })) }); // Look for known session cookies first const knownSessionCookies = iracingDomainCookies.filter(c => IRACING_SESSION_COOKIES.some(name => c.name.toLowerCase() === name.toLowerCase() || c.name.toLowerCase().includes(name.toLowerCase()) ) ); // If no known session cookies, check for any auth-like cookies from members domain const authCookies = knownSessionCookies.length > 0 ? knownSessionCookies : iracingDomainCookies.filter(c => c.domain.includes('members') && (c.name.toLowerCase().includes('auth') || c.name.toLowerCase().includes('sso') || c.name.toLowerCase().includes('session') || c.name.toLowerCase().includes('token')) ); this.log('debug', 'Authentication cookies identified', { knownSessionCookiesCount: knownSessionCookies.length, authCookiesCount: authCookies.length, cookies: authCookies.map(c => ({ name: c.name, domain: c.domain })) }); if (authCookies.length === 0) { // Last resort: if we have ANY cookies from members.iracing.com, consider it potentially valid const membersCookies = iracingDomainCookies.filter(c => c.domain.includes('members.iracing.com') || c.domain === '.iracing.com' ); if (membersCookies.length > 0) { this.log('info', 'No known auth cookies found, but members domain cookies exist', { count: membersCookies.length, cookies: membersCookies.map(c => c.name) }); // Check expiry on any of these cookies const now = Math.floor(Date.now() / 1000); const hasValidCookie = membersCookies.some(c => c.expires === -1 || c.expires === 0 || c.expires > (now + EXPIRY_BUFFER_SECONDS) ); return hasValidCookie ? AuthenticationState.AUTHENTICATED : AuthenticationState.EXPIRED; } this.log('info', 'No iRacing authentication cookies found'); return AuthenticationState.UNKNOWN; } // Check if any auth cookie is still valid (not expired) const now = Math.floor(Date.now() / 1000); const validCookies = authCookies.filter(c => { // Handle session cookies (expires = -1 or 0) and persistent cookies const isSession = c.expires === -1 || c.expires === 0; const isNotExpired = c.expires > (now + EXPIRY_BUFFER_SECONDS); return isSession || isNotExpired; }); this.log('debug', 'Cookie expiry check', { now, validCookiesCount: validCookies.length, cookies: authCookies.map(c => ({ name: c.name, expires: c.expires, isValid: c.expires === -1 || c.expires === 0 || c.expires > (now + EXPIRY_BUFFER_SECONDS) })) }); if (validCookies.length > 0) { this.log('info', 'Valid iRacing session cookies found', { count: validCookies.length }); return AuthenticationState.AUTHENTICATED; } this.log('info', 'iRacing session cookies found but all expired'); return AuthenticationState.EXPIRED; } }