import { chromium } from 'playwright' import { mkdirSync, existsSync, readdirSync, readFileSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) const PROFILE_DIR = resolve(__dirname, '../sessions/chrome-profile') const SNAPSHOTS_DIR = resolve(__dirname, '../sessions/snapshots') const INJECT_SRC = resolve(__dirname, './inject.browser.js') const PUBLIC_BASE = process.env.BEACON_PUBLIC_URL || `http://localhost:${process.env.BEACON_PORT || 3700}` // Inject script with beacon base URL substituted in function buildInjectScript() { return readFileSync(INJECT_SRC, 'utf8').replaceAll('__BEACON_BASE__', PUBLIC_BASE) } // Wire auto-inject onto a context — fires on every new page that opens function attachInject(context) { const script = buildInjectScript() // addInitScript runs before any page JS — functions are available immediately context.addInitScript(script).catch(() => {}) } const IS_MAC = process.platform === 'darwin' const IS_HF = !!process.env.SPACE_ID // HuggingFace injects this let _context = null let _browser = null // only set in non-persistent launch export async function getContext() { if (_context) return _context mkdirSync(PROFILE_DIR, { recursive: true }) mkdirSync(SNAPSHOTS_DIR, { recursive: true }) // ── Mac path ────────────────────────────────────────────────────────────── if (IS_MAC && !IS_HF) { // 1. CDP bridge — rides your already-running Chrome (zero copy) // Enable with: open -a "Google Chrome" --args --remote-debugging-port=9222 try { const browser = await chromium.connectOverCDP('http://localhost:9222', { timeout: 2000 }) const contexts = browser.contexts() _context = contexts[0] ?? await browser.newContext() attachInject(_context) console.log('[beacon] CDP bridge — live Chrome session active') return _context } catch {} // 2. Chrome profile clone (npm run sync-profile) const hasProfile = existsSync(`${PROFILE_DIR}/Cookies`) _context = await chromium.launchPersistentContext(PROFILE_DIR, { channel: 'chrome', headless: true, viewport: { width: 1280, height: 900 }, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', args: ['--no-sandbox', '--disable-blink-features=AutomationControlled'], ignoreDefaultArgs: ['--enable-automation'], }) attachInject(_context) console.log(hasProfile ? '[beacon] Chrome profile loaded — all sessions active' : '[beacon] Fresh profile — run: npm run sync-profile') return _context } // ── Linux / HuggingFace path ────────────────────────────────────────────── // No local Chrome. Load auth from committed session snapshots instead. const storageState = mergeSnapshots(SNAPSHOTS_DIR) _browser = await chromium.launch({ headless: true, args: [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled', ], }) _context = await _browser.newContext({ storageState: storageState || undefined, viewport: { width: 1280, height: 900 }, userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', ignoreHTTPSErrors: false, }) attachInject(_context) const snapshotCount = storageState?.cookies?.length ?? 0 console.log(snapshotCount > 0 ? `[beacon] Loaded ${snapshotCount} cookies from session snapshots` : '[beacon] No snapshots found — browsing anonymously') return _context } // Merge all snapshot JSON files into a single storageState object function mergeSnapshots(dir) { const files = readdirSync(dir).filter(f => f.endsWith('.json')) if (!files.length) return null const cookies = [] const origins = [] for (const file of files) { try { const state = JSON.parse(readFileSync(`${dir}/${file}`, 'utf8')) if (state.cookies) cookies.push(...state.cookies) if (state.origins) origins.push(...state.origins) } catch {} } return { cookies, origins } } export async function closeContext() { if (_context) { await _context.close(); _context = null } if (_browser) { await _browser.close(); _browser = null } }