// THE BROWSER THAT OPENS THE X SESSION PROFILE (release 16 slice XL). // // Why this exists: the Connect window used to be Playwright's bundled Chromium // launched with its automation signals on — `--enable-automation` (which is // what draws "Chrome is being controlled by automated test software") and // `navigator.webdriver === true`, which Playwright's debugging pipe turns on by // itself. Google's sign-in refuses such a browser ("this browser or app may not // be secure") and X's own login form stalled in it (2026-10-01). // // So the Connect window is the operator's own browser when one is installed — // ARCHILYZER_X_BROWSER, else the first of chromium / google-chrome / // google-chrome-stable / chrome on PATH, else the bundled build — and every // launch here drops the signals: // - `ignoreDefaultArgs: ["--enable-automation"]` — no automation bar. ONLY // that one: `ignoreDefaultArgs: true` would also drop Playwright's // `--password-store=basic`, and a system Chromium would then encrypt the // profile's cookies with the desktop keyring's key, which the bundled // headless build that refreshes the jar cannot read. // - `--disable-blink-features=AutomationControlled` — navigator.webdriver is // false. Measured: without it, it stays true even with // `--enable-automation` gone (the pipe sets it). // - `--test-type` and, for the headed window, the sandbox on — a system // Chrome draws "You are using an unsupported command-line flag" for the // blink flag above and for Playwright's default `--no-sandbox`. The // sandbox removes the second. `--test-type` is Chromium's internal // test-harness switch: it makes the browser skip its startup bars, the // bad-flags one among them, and sets no automation signal (measured: // navigator.webdriver, window.chrome, the user agent and the plugins are // the same with and without it). ChromeDriver passes // `--test-type=webdriver`; the bare switch is used here. It is not the // documented route — that is the `CommandLineFlagSecurityWarningsEnabled` // policy, which is machine-wide, needs root, and would silence the // operator's everyday browser too. The bundled build draws neither bar. // None of this hides the debugging pipe itself; Google's sign-in may still // refuse an embedded browser, which the Settings section says. // // THE PROFILE REMEMBERS WHO WROTE IT. A connect records its executable in the // profile dir (`archilyzer-browser.json`). The headless refresh keeps the // bundled build — it never logs in, and its headless shell needs no display — // and falls back to the recorded executable when the bundled build cannot open // the profile (a profile written by a newer system browser). Measured // 2026-10-01: a profile written by system Chromium 153 opens in the bundled // headless shell 147 with its cookies readable, so the fallback is a guard, // not the common path. import { accessSync, constants, statSync } from "node:fs"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { writeFileAtomic } from "../lib/jsonFile-server"; export const X_BROWSER_ENV = "ARCHILYZER_X_BROWSER"; // Looked up on PATH in this order when ARCHILYZER_X_BROWSER is unset. export const SYSTEM_CHROMIUM_NAMES: readonly string[] = [ "chromium", "google-chrome", "google-chrome-stable", "chrome", ]; export type XBrowserChoice = | { kind: "system"; executablePath: string; // How it was found: the env override, PATH, or the profile's record. from: "env" | "path" | "recorded"; } | { kind: "bundled" }; export function isExecutableFile(p: string): boolean { try { if (!statSync(p).isFile()) return false; accessSync(p, constants.X_OK); return true; } catch { return false; } } function onPath( name: string, pathEnv: string | undefined, isExecutable: (p: string) => boolean, ): string | undefined { for (const dir of (pathEnv ?? "").split(path.delimiter)) { if (!dir) continue; const candidate = path.join(dir, name); if (isExecutable(candidate)) return candidate; } return undefined; } // The Connect window's browser. Throws only when ARCHILYZER_X_BROWSER names // something that is not an executable: an explicit setting that is wrong is // said, not silently replaced by a different browser. export function findXBrowser( opts: { env?: Record; isExecutable?: (p: string) => boolean; } = {}, ): XBrowserChoice { const env = opts.env ?? process.env; const isExecutable = opts.isExecutable ?? isExecutableFile; const override = env.ARCHILYZER_X_BROWSER?.trim(); if (override) { const resolved = override.includes("/") ? isExecutable(override) ? override : undefined : onPath(override, env.PATH, isExecutable); if (!resolved) { throw new Error( `${X_BROWSER_ENV} names "${override}", which is not an executable file` + (override.includes("/") ? "." : " on PATH."), ); } return { kind: "system", executablePath: resolved, from: "env" }; } for (const name of SYSTEM_CHROMIUM_NAMES) { const hit = onPath(name, env.PATH, isExecutable); if (hit) return { kind: "system", executablePath: hit, from: "path" }; } return { kind: "bundled" }; } // THE LAUNCH OPTIONS, pure. `headless: false` is the Connect window (the // operator logs in); `headless: true` is the refresh and the Playwright // fallback fetcher. `sandbox` turns Chromium's sandbox on — the headed window // asks for it (no unsupported-flag bar) and retries without it when the host // cannot start one; a headless launch keeps Playwright's default. export type XBrowserLaunchOptions = { headless: boolean; executablePath?: string; ignoreDefaultArgs: string[]; args: string[]; chromiumSandbox?: boolean; viewport?: { width: number; height: number }; }; export const X_BROWSER_ARGS: readonly string[] = [ "--disable-blink-features=AutomationControlled", "--test-type", ]; export function buildXBrowserLaunchOptions(opts: { browser: XBrowserChoice; headless: boolean; sandbox?: boolean; }): XBrowserLaunchOptions { const out: XBrowserLaunchOptions = { headless: opts.headless, ignoreDefaultArgs: ["--enable-automation"], args: [...X_BROWSER_ARGS], }; if (opts.browser.kind === "system") { out.executablePath = opts.browser.executablePath; } if (opts.sandbox) out.chromiumSandbox = true; if (!opts.headless) out.viewport = { width: 1280, height: 900 }; return out; } export function describeXBrowser(b: XBrowserChoice, version?: string): string { if (b.kind === "bundled") { return "Playwright's bundled Chromium (no system Chromium or Chrome found)"; } const how = b.from === "env" ? `from ${X_BROWSER_ENV}` : b.from === "path" ? "found on PATH" : "the browser that created the profile"; return `${version ? `${version} at ` : ""}${b.executablePath} (${how})`; } // --- The profile's record of who wrote it --------------------------------- export const X_BROWSER_RECORD = "archilyzer-browser.json"; export type XBrowserRecord = { // The system executable that opened the Connect window; null = the bundled // build. executablePath: string | null; // ` --version`, when it answered. version?: string; recordedAt: string; }; export function xBrowserRecordFile(profileDir: string): string { return path.join(profileDir, X_BROWSER_RECORD); } export async function readXBrowserRecord( profileDir: string, ): Promise { try { const raw = JSON.parse( await readFile(xBrowserRecordFile(profileDir), "utf8"), ) as Record; if ( raw.executablePath !== null && typeof raw.executablePath !== "string" ) { return null; } return { executablePath: raw.executablePath as string | null, ...(typeof raw.version === "string" ? { version: raw.version } : {}), recordedAt: typeof raw.recordedAt === "string" ? raw.recordedAt : "", }; } catch { return null; } } export async function writeXBrowserRecord( profileDir: string, browser: XBrowserChoice, version?: string, ): Promise { const record: XBrowserRecord = { executablePath: browser.kind === "system" ? browser.executablePath : null, ...(version ? { version } : {}), recordedAt: new Date().toISOString(), }; await writeFileAtomic( xBrowserRecordFile(profileDir), JSON.stringify(record, null, 2) + "\n", { mkdir: true }, ); } // The browser the record names, for the refresh's fallback — undefined when it // names the bundled build, nothing, or an executable that is gone. export function recordedXBrowser( record: XBrowserRecord | null, isExecutable: (p: string) => boolean = isExecutableFile, ): XBrowserChoice | undefined { if (!record?.executablePath) return undefined; if (!isExecutable(record.executablePath)) return undefined; return { kind: "system", executablePath: record.executablePath, from: "recorded" }; }