Files
gridpilot.gg/packages/infrastructure/adapters/automation/SessionCookieStore.ts

195 lines
5.9 KiB
TypeScript

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<string, unknown>): 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<StorageState | null> {
try {
const content = await fs.readFile(this.storagePath, 'utf-8');
return JSON.parse(content) as StorageState;
} catch {
return null;
}
}
async write(state: StorageState): Promise<void> {
await fs.writeFile(this.storagePath, JSON.stringify(state, null, 2), 'utf-8');
}
async delete(): Promise<void> {
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;
}
}