/** * Cookie jar — persist and restore Playwright storageState cookies between * crawl segments and restarts. Enables continuous auth across session boundaries * (e.g. segment 1 logs in, segment 2–N inherit the session without re-login). * * Storage: JSON file per project in the crawl cache dir. * Format: Playwright's storageState shape (cookies + origins). */ import * as fs from 'fs/promises'; import * as path from 'path'; export interface CookieJarEntry { projectId: string; tenantId: string; storageState: string; // JSON-stringified Playwright StorageState savedAt: string; // ISO timestamp domain: string; // hostname for quick lookup } function jarPath(cacheDir: string, projectId: string): string { return path.join(cacheDir, `cookie-jar-${projectId}.json`); } export async function saveCookieJar( cacheDir: string, projectId: string, tenantId: string, storageState: string, domain: string, ): Promise { await fs.mkdir(cacheDir, { recursive: true }); const entry: CookieJarEntry = { projectId, tenantId, storageState, savedAt: new Date().toISOString(), domain, }; await fs.writeFile(jarPath(cacheDir, projectId), JSON.stringify(entry, null, 2), 'utf8'); } export async function loadCookieJar( cacheDir: string, projectId: string, maxAgeMs = 8 * 60 * 60 * 1000, // 8 hours ): Promise { try { const raw = await fs.readFile(jarPath(cacheDir, projectId), 'utf8'); const entry: CookieJarEntry = JSON.parse(raw); const age = Date.now() - new Date(entry.savedAt).getTime(); if (age > maxAgeMs) return null; // expired return entry.storageState; } catch { return null; } } export async function clearCookieJar(cacheDir: string, projectId: string): Promise { try { await fs.unlink(jarPath(cacheDir, projectId)); } catch { /* already gone */ } } /** Merge two storageState JSON strings — second takes priority for same-domain cookies. */ export function mergeStorageStates(base: string, overlay: string): string { try { const a = JSON.parse(base); const b = JSON.parse(overlay); // Cookies: overlay replaces same-name+domain+path cookies from base const cookies = [...(a.cookies ?? [])]; for (const ck of (b.cookies ?? [])) { const idx = cookies.findIndex( (c: any) => c.name === ck.name && c.domain === ck.domain && c.path === ck.path, ); if (idx >= 0) cookies[idx] = ck; else cookies.push(ck); } // Origins: merge origins, overlay wins on same-origin const originsMap = new Map(); for (const o of [...(a.origins ?? []), ...(b.origins ?? [])]) { originsMap.set(o.origin, o); } return JSON.stringify({ cookies, origins: [...originsMap.values()] }); } catch { return overlay || base; } }