working companion prototype
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user