From c322276e0a303ba9dc09ee337e6b26d33c9a2717 Mon Sep 17 00:00:00 2001 From: Theo Tarr Date: Sun, 24 May 2026 10:51:02 -0400 Subject: [PATCH 1/4] Add managed Chromium auth fallback --- src/cli.ts | 16 + src/core/config.ts | 6 + .../core/auth/browser-resolver.test.ts | 57 ++- src/platforms/core/auth/browser-resolver.ts | 21 +- src/platforms/core/auth/chromium-worker.ts | 39 +- .../core/auth/managed-browser.test.ts | 166 ++++++++ src/platforms/core/auth/managed-browser.ts | 368 ++++++++++++++++++ 7 files changed, 658 insertions(+), 15 deletions(-) create mode 100644 src/platforms/core/auth/managed-browser.test.ts create mode 100644 src/platforms/core/auth/managed-browser.ts diff --git a/src/cli.ts b/src/cli.ts index 8328e37f..5c411569 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -29,6 +29,10 @@ import { installMacOSApp, resolveInstalledAppPath, } from "./macos/install.js"; +import { + getManagedChromiumStatus, + installManagedChromiumBrowser, +} from "./platforms/core/auth/managed-browser.js"; import { IntegrationAuthService } from "./platforms/core/auth/service.js"; import { getIntegrationSummary, @@ -142,6 +146,7 @@ Usage: cued login-item enable|disable|status cued onboarding complete|snapshot|status [--refresh-managed] [--refresh-permissions] cued telemetry status|enable|disable|smoke + cued browser status|install cued skill install-global|status cued permissions doctor|status|request [--all|--contacts|--full-disk-access] cued sql @@ -777,6 +782,17 @@ async function main(): Promise { default: throw new Error("Usage: cued skill install-global | status"); } + case "browser": + switch (subcommand) { + case "status": + printJson(getManagedChromiumStatus()); + return; + case "install": + printJson(await installManagedChromiumBrowser()); + return; + default: + throw new Error("Usage: cued browser status | install"); + } case "telemetry": { const db = openCuedDatabase(); try { diff --git a/src/core/config.ts b/src/core/config.ts index cee6be2b..2e59c1e0 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -34,6 +34,7 @@ export const CUED_MENU_BAR_STATUS_PATH = join(CUED_HOME, "menu-bar-status.json") export const CUED_LOG_DIR = join(CUED_HOME, "logs"); export const CUED_DAEMON_LOG_PATH = join(CUED_LOG_DIR, "daemon.log"); export const CUED_BROWSER_DIR = join(CUED_HOME, "browser"); +export const CUED_MANAGED_BROWSER_DIR = join(CUED_HOME, "browsers"); export const CUED_INTEGRATIONS_DIR = join(CUED_HOME, "integrations"); export const CUED_SIGNAL_DIR = join(CUED_INTEGRATIONS_DIR, "signal"); export const CUED_WHATSAPP_DIR = join(CUED_INTEGRATIONS_DIR, "whatsapp"); @@ -59,6 +60,10 @@ export function ensureCuedDirs(): void { mkdirSync(CUED_BROWSER_DIR, { recursive: true, mode: 0o700 }); } + if (!existsSync(CUED_MANAGED_BROWSER_DIR)) { + mkdirSync(CUED_MANAGED_BROWSER_DIR, { recursive: true, mode: 0o700 }); + } + if (!existsSync(CUED_INTEGRATIONS_DIR)) { mkdirSync(CUED_INTEGRATIONS_DIR, { recursive: true, mode: 0o700 }); } @@ -102,6 +107,7 @@ export function ensureCuedDirs(): void { // Best-effort hardening for the local data dir. chmodSync(CUED_HOME, 0o700); chmodSync(CUED_BROWSER_DIR, 0o700); + chmodSync(CUED_MANAGED_BROWSER_DIR, 0o700); chmodSync(CUED_INTEGRATIONS_DIR, 0o700); chmodSync(CUED_SIGNAL_DIR, 0o700); chmodSync(CUED_WHATSAPP_DIR, 0o700); diff --git a/src/platforms/core/auth/browser-resolver.test.ts b/src/platforms/core/auth/browser-resolver.test.ts index b4557e67..89f5f526 100644 --- a/src/platforms/core/auth/browser-resolver.test.ts +++ b/src/platforms/core/auth/browser-resolver.test.ts @@ -1,12 +1,18 @@ -import { describe, expect, it } from "vitest"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import { type BrowserCandidate, formatMissingChromiumAuthBrowserError, + listAvailableChromiumAuthBrowsers, listInstalledChromiumAuthBrowsers, resolveChromiumAuthBrowser, } from "./browser-resolver.js"; +import { installManagedChromiumBrowser } from "./managed-browser.js"; describe("resolveChromiumAuthBrowser", () => { + const tempDirs: string[] = []; const candidates: BrowserCandidate[] = [ { name: "Missing", @@ -20,6 +26,38 @@ describe("resolveChromiumAuthBrowser", () => { }, ]; + afterEach(() => { + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } + }); + + function tempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; + } + + async function installFakeManagedBrowser(root: string): Promise { + return installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + installChromium: async ({ destinationRoot }) => { + const executablePath = join( + destinationRoot, + "chromium", + "Chromium.app", + "Contents", + "MacOS", + "Chromium", + ); + mkdirSync(dirname(executablePath), { recursive: true }); + writeFileSync(executablePath, ""); + }, + resolveExecutablePath: async ({ browserRoot }) => + join(browserRoot, "chromium", "Chromium.app", "Contents", "MacOS", "Chromium"), + }); + } + it("uses an explicit executable path override", () => { expect( resolveChromiumAuthBrowser( @@ -57,8 +95,23 @@ describe("resolveChromiumAuthBrowser", () => { ]); }); + it("appends an installed managed Chromium after local browsers", async () => { + const managedRoot = tempDir("cued-managed-browser-resolver-"); + const managedBrowser = await installFakeManagedBrowser(managedRoot); + + expect( + listAvailableChromiumAuthBrowsers({ CUED_MANAGED_BROWSER_DIR: managedRoot }, [ + candidates[0]!, + candidates[1]!, + ]), + ).toEqual([candidates[1], managedBrowser]); + }); + it("returns null when no candidate exists", () => { - expect(resolveChromiumAuthBrowser({}, [candidates[0]!])).toBeNull(); + const managedRoot = tempDir("cued-managed-browser-missing-"); + expect( + resolveChromiumAuthBrowser({ CUED_MANAGED_BROWSER_DIR: managedRoot }, [candidates[0]!]), + ).toBeNull(); expect(formatMissingChromiumAuthBrowserError()).toContain("No supported browser"); }); }); diff --git a/src/platforms/core/auth/browser-resolver.ts b/src/platforms/core/auth/browser-resolver.ts index 9950f9de..897a561b 100644 --- a/src/platforms/core/auth/browser-resolver.ts +++ b/src/platforms/core/auth/browser-resolver.ts @@ -1,6 +1,7 @@ import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { getManagedChromiumBrowser } from "./managed-browser.js"; export interface BrowserCandidate { name: string; @@ -60,16 +61,30 @@ export function listInstalledChromiumAuthBrowsers( return candidates.filter((candidate) => existsSync(candidate.executablePath)); } +export function listAvailableChromiumAuthBrowsers( + env: NodeJS.ProcessEnv = process.env, + candidates: BrowserCandidate[] = MACOS_BROWSER_CANDIDATES, +): BrowserCandidate[] { + const installed = listInstalledChromiumAuthBrowsers(env, candidates); + let managed: BrowserCandidate | null = null; + try { + managed = getManagedChromiumBrowser(env); + } catch { + managed = null; + } + return managed ? [...installed, managed] : installed; +} + export function resolveChromiumAuthBrowser( env: NodeJS.ProcessEnv = process.env, candidates: BrowserCandidate[] = MACOS_BROWSER_CANDIDATES, ): BrowserCandidate | null { - return listInstalledChromiumAuthBrowsers(env, candidates)[0] ?? null; + return listAvailableChromiumAuthBrowsers(env, candidates)[0] ?? null; } export function formatMissingChromiumAuthBrowserError(): string { return [ - "No supported browser was found for Cued browser authentication.", - "Install Google Chrome, Chromium, Microsoft Edge, or Brave Browser and try again.", + "No supported browser was available for Cued browser authentication.", + "Install Google Chrome, Chromium, Microsoft Edge, or Brave Browser, or run `cued browser install` to install Cued's managed Chromium.", ].join(" "); } diff --git a/src/platforms/core/auth/chromium-worker.ts b/src/platforms/core/auth/chromium-worker.ts index b96dcf7d..5096c190 100644 --- a/src/platforms/core/auth/chromium-worker.ts +++ b/src/platforms/core/auth/chromium-worker.ts @@ -5,8 +5,9 @@ import { cuedAuthKeychainService } from "../../../core/identity.js"; import { type BrowserCandidate, formatMissingChromiumAuthBrowserError, - listInstalledChromiumAuthBrowsers, + listAvailableChromiumAuthBrowsers, } from "./browser-resolver.js"; +import { installManagedChromiumBrowser } from "./managed-browser.js"; declare const localStorage: { getItem(key: string): string | null; @@ -467,6 +468,18 @@ function getFakeResult(args: WorkerArgs): WorkerResult | null { }; } +async function launchBrowserCandidate( + browser: BrowserCandidate, + profileDir: string, +): Promise { + return chromium.launchPersistentContext(profileDir, { + headless: false, + executablePath: browser.executablePath, + args: ["--disable-blink-features=AutomationControlled"], + viewport: { width: 1280, height: 900 }, + }); +} + async function run(): Promise { const args = parseArgs(process.argv.slice(2)); const fake = getFakeResult(args); @@ -476,9 +489,9 @@ async function run(): Promise { } mkdirSync(args.profileDir, { recursive: true }); - const browsers = listInstalledChromiumAuthBrowsers(); + const browsers = listAvailableChromiumAuthBrowsers(); if (browsers.length === 0) { - throw new Error(formatMissingChromiumAuthBrowserError()); + browsers.push(await installManagedChromiumBrowser()); } let context: BrowserContext | null = null; @@ -487,12 +500,7 @@ async function run(): Promise { try { for (const candidate of browsers) { try { - context = await chromium.launchPersistentContext(args.profileDir, { - headless: false, - executablePath: candidate.executablePath, - args: ["--disable-blink-features=AutomationControlled"], - viewport: { width: 1280, height: 900 }, - }); + context = await launchBrowserCandidate(candidate, args.profileDir); browser = candidate; break; } catch (error) { @@ -503,7 +511,18 @@ async function run(): Promise { } if (!context || !browser) { - throw new Error(`Could not launch a supported browser. ${launchErrors.join(" ")}`); + const managedBrowser = await installManagedChromiumBrowser({ force: true }); + try { + context = await launchBrowserCandidate(managedBrowser, args.profileDir); + browser = managedBrowser; + } catch (error) { + launchErrors.push( + `${managedBrowser.name}: ${error instanceof Error ? error.message : String(error)}`, + ); + throw new Error( + `${formatMissingChromiumAuthBrowserError()} ${launchErrors.join(" ")}`.trim(), + ); + } } let page = firstOpenPage(context) ?? (await context.newPage()); diff --git a/src/platforms/core/auth/managed-browser.test.ts b/src/platforms/core/auth/managed-browser.test.ts new file mode 100644 index 00000000..77539602 --- /dev/null +++ b/src/platforms/core/auth/managed-browser.test.ts @@ -0,0 +1,166 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + getManagedChromiumBrowser, + getManagedChromiumStatus, + installManagedChromiumBrowser, +} from "./managed-browser.js"; + +describe("managed Chromium browser", () => { + const tempDirs: string[] = []; + + afterEach(() => { + while (tempDirs.length > 0) { + rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } + }); + + function tempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; + } + + async function installFakeBrowser(root: string, executableName = "Chromium") { + let installCalls = 0; + const browser = await installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + installChromium: async ({ destinationRoot }) => { + installCalls += 1; + const executablePath = join( + destinationRoot, + "chromium", + "Chromium.app", + "Contents", + "MacOS", + executableName, + ); + mkdirSync(dirname(executablePath), { recursive: true }); + writeFileSync(executablePath, ""); + }, + resolveExecutablePath: async ({ browserRoot }) => + join(browserRoot, "chromium", "Chromium.app", "Contents", "MacOS", executableName), + }); + return { browser, installCalls }; + } + + it("reports the pinned Playwright Chromium spec before install", () => { + const root = tempDir("cued-managed-browser-status-"); + const status = getManagedChromiumStatus({ CUED_MANAGED_BROWSER_DIR: root }); + + expect(status.installed).toBe(false); + expect(status.root).toContain(root); + expect(status.spec.playwrightVersion).toMatch(/^\d+\.\d+\.\d+/); + expect(status.spec.revision).toMatch(/^\d+$/); + }); + + it("installs into the managed root and writes metadata", async () => { + const root = tempDir("cued-managed-browser-install-"); + const { browser, installCalls } = await installFakeBrowser(root); + const status = getManagedChromiumStatus({ CUED_MANAGED_BROWSER_DIR: root }); + + expect(installCalls).toBe(1); + expect(browser.name).toBe("Managed Chromium"); + expect(browser.appBundleIdentifier).toBe("com.google.chrome.for.testing"); + expect(browser.executablePath).toContain(status.root); + expect(existsSync(browser.executablePath)).toBe(true); + expect(status.installed).toBe(true); + expect(status.metadata?.executablePath).toBe(browser.executablePath); + expect(readFileSync(join(status.root, "metadata.json"), "utf8")).toContain( + '"provider": "playwright"', + ); + }); + + it("reuses a valid managed browser without reinstalling", async () => { + const root = tempDir("cued-managed-browser-reuse-"); + const first = await installFakeBrowser(root); + let installCalls = 0; + const second = await installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + installChromium: async () => { + installCalls += 1; + }, + resolveExecutablePath: async () => { + throw new Error("should not resolve on reuse"); + }, + }); + + expect(second).toEqual(first.browser); + expect(installCalls).toBe(0); + expect(getManagedChromiumBrowser({ CUED_MANAGED_BROWSER_DIR: root })).toEqual(first.browser); + }); + + it("forces reinstall when the managed browser should be refreshed", async () => { + const root = tempDir("cued-managed-browser-force-"); + const first = await installFakeBrowser(root, "OldChromium"); + const second = await installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + force: true, + installChromium: async ({ destinationRoot }) => { + const executablePath = join( + destinationRoot, + "chromium", + "Chromium.app", + "Contents", + "MacOS", + "NewChromium", + ); + mkdirSync(dirname(executablePath), { recursive: true }); + writeFileSync(executablePath, ""); + }, + resolveExecutablePath: async ({ browserRoot }) => + join(browserRoot, "chromium", "Chromium.app", "Contents", "MacOS", "NewChromium"), + }); + + expect(second.executablePath).not.toBe(first.browser.executablePath); + expect(existsSync(second.executablePath)).toBe(true); + expect(existsSync(first.browser.executablePath)).toBe(false); + }); + + it("keeps the existing managed browser when a forced replacement fails", async () => { + const root = tempDir("cued-managed-browser-force-failure-"); + const first = await installFakeBrowser(root, "StableChromium"); + + await expect( + installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + force: true, + installChromium: async ({ destinationRoot }) => { + mkdirSync(destinationRoot, { recursive: true }); + }, + resolveExecutablePath: async ({ browserRoot }) => + join(browserRoot, "chromium", "Chromium.app", "Contents", "MacOS", "BrokenChromium"), + }), + ).rejects.toThrow("Managed Chromium executable was not found"); + + expect(existsSync(first.browser.executablePath)).toBe(true); + expect(getManagedChromiumBrowser({ CUED_MANAGED_BROWSER_DIR: root })).toEqual(first.browser); + }); + + it("does not trust metadata pointing outside the managed browser root", async () => { + const root = tempDir("cued-managed-browser-outside-"); + const outsideExecutable = join(root, "outside-chromium"); + writeFileSync(outsideExecutable, ""); + + await installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + installChromium: async ({ destinationRoot }) => { + const executablePath = join( + destinationRoot, + "chromium", + "Chromium.app", + "Contents", + "MacOS", + "Chromium", + ); + mkdirSync(dirname(executablePath), { recursive: true }); + writeFileSync(executablePath, ""); + }, + resolveExecutablePath: async () => outsideExecutable, + }).catch(() => null); + + expect(getManagedChromiumBrowser({ CUED_MANAGED_BROWSER_DIR: root })).toBeNull(); + }); +}); diff --git a/src/platforms/core/auth/managed-browser.ts b/src/platforms/core/auth/managed-browser.ts new file mode 100644 index 00000000..13de4e97 --- /dev/null +++ b/src/platforms/core/auth/managed-browser.ts @@ -0,0 +1,368 @@ +import { execFile } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { promisify } from "node:util"; +import { CUED_MANAGED_BROWSER_DIR } from "../../../core/config.js"; +import type { BrowserCandidate } from "./browser-resolver.js"; + +const execFileAsync = promisify(execFile); +const require = createRequire(import.meta.url); +const MANAGED_BROWSER_NAME = "Managed Chromium"; +const MANAGED_BROWSER_BUNDLE_ID = "com.google.chrome.for.testing"; +const METADATA_FILE = "metadata.json"; +const INSTALL_LOCK_STALE_MS = 60 * 60_000; +const INSTALL_LOCK_RETRY_MS = 250; +const INSTALL_LOCK_TIMEOUT_MS = 5 * 60_000; + +interface PlaywrightChromiumSpec { + playwrightVersion: string; + revision: string; + browserVersion: string | null; + packageRoot: string; + cliPath: string; +} + +interface ManagedBrowserMetadata { + provider: "playwright"; + browser: "chromium"; + playwrightVersion: string; + revision: string; + browserVersion: string | null; + platform: string; + executablePath: string; + installedAt: number; +} + +interface InstallOptions { + env?: NodeJS.ProcessEnv; + force?: boolean; + installChromium?: (input: { + destinationRoot: string; + env: NodeJS.ProcessEnv; + spec: PlaywrightChromiumSpec; + }) => Promise; + resolveExecutablePath?: (input: { + browserRoot: string; + env: NodeJS.ProcessEnv; + spec: PlaywrightChromiumSpec; + }) => Promise; +} + +function managedBrowserRoot(env: NodeJS.ProcessEnv = process.env): string { + return env.CUED_MANAGED_BROWSER_DIR?.trim() || CUED_MANAGED_BROWSER_DIR; +} + +function currentPlatform(): string { + return `mac-${process.arch === "arm64" ? "arm64" : "x64"}`; +} + +function readPlaywrightChromiumSpec(): PlaywrightChromiumSpec { + const packagePath = require.resolve("playwright/package.json"); + const packageRoot = dirname(packagePath); + const playwrightVersion = (JSON.parse(readFileSync(packagePath, "utf8")) as { version: string }) + .version; + const corePackageRoot = dirname(require.resolve("playwright-core/package.json")); + const browsers = JSON.parse(readFileSync(join(corePackageRoot, "browsers.json"), "utf8")) as { + browsers: Array<{ name: string; revision: string; browserVersion?: string }>; + }; + const chromium = browsers.browsers.find((browser) => browser.name === "chromium"); + if (!chromium) { + throw new Error("Playwright Chromium metadata was not found"); + } + + return { + playwrightVersion, + revision: chromium.revision, + browserVersion: chromium.browserVersion ?? null, + packageRoot, + cliPath: join(packageRoot, "cli.js"), + }; +} + +function versionRoot(root: string, spec: PlaywrightChromiumSpec): string { + return join(root, `playwright-${spec.playwrightVersion}`); +} + +function metadataPath(root: string, spec: PlaywrightChromiumSpec): string { + return join(versionRoot(root, spec), METADATA_FILE); +} + +function isPathInside(parent: string, child: string): boolean { + const parentPath = resolve(parent); + const childPath = resolve(child); + const childRelativePath = relative(parentPath, childPath); + return ( + childRelativePath === "" || + (!childRelativePath.startsWith("..") && !isAbsolute(childRelativePath)) + ); +} + +function sleep(ms: number): Promise { + return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); +} + +function lockPath(targetRoot: string): string { + return `${targetRoot}.lock`; +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function acquireInstallLock(targetRoot: string): Promise<() => void> { + const path = lockPath(targetRoot); + const deadline = Date.now() + INSTALL_LOCK_TIMEOUT_MS; + + while (true) { + try { + mkdirSync(path, { mode: 0o700 }); + writeFileSync( + join(path, "owner.json"), + JSON.stringify({ pid: process.pid, createdAt: Date.now() }), + { + mode: 0o600, + }, + ); + return () => { + rmSync(path, { recursive: true, force: true }); + }; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EEXIST") { + throw error; + } + + try { + const owner = JSON.parse(readFileSync(join(path, "owner.json"), "utf8")) as { + pid?: number; + createdAt?: number; + }; + if (typeof owner.pid === "number" && isProcessAlive(owner.pid)) { + if (Date.now() >= deadline) { + throw new Error("Timed out waiting for managed Chromium install lock"); + } + await sleep(INSTALL_LOCK_RETRY_MS); + continue; + } + if ( + typeof owner.createdAt === "number" && + Date.now() - owner.createdAt > INSTALL_LOCK_STALE_MS + ) { + rmSync(path, { recursive: true, force: true }); + continue; + } + } catch { + rmSync(path, { recursive: true, force: true }); + continue; + } + + if (Date.now() >= deadline) { + throw new Error("Timed out waiting for managed Chromium install lock"); + } + await sleep(INSTALL_LOCK_RETRY_MS); + } + } +} + +function readValidMetadata( + root = managedBrowserRoot(), + spec = readPlaywrightChromiumSpec(), +): ManagedBrowserMetadata | null { + const path = metadataPath(root, spec); + if (!existsSync(path)) { + return null; + } + + try { + const metadata = JSON.parse(readFileSync(path, "utf8")) as ManagedBrowserMetadata; + if ( + metadata.provider !== "playwright" || + metadata.browser !== "chromium" || + metadata.playwrightVersion !== spec.playwrightVersion || + metadata.revision !== spec.revision || + metadata.platform !== currentPlatform() || + !isPathInside(versionRoot(root, spec), metadata.executablePath) || + !existsSync(metadata.executablePath) + ) { + return null; + } + return metadata; + } catch { + return null; + } +} + +async function defaultInstallChromium(input: { + destinationRoot: string; + env: NodeJS.ProcessEnv; + spec: PlaywrightChromiumSpec; +}): Promise { + await execFileAsync(process.execPath, [input.spec.cliPath, "install", "chromium"], { + env: { + ...input.env, + PLAYWRIGHT_BROWSERS_PATH: input.destinationRoot, + PLAYWRIGHT_SKIP_BROWSER_GC: "1", + }, + maxBuffer: 10 * 1024 * 1024, + }); +} + +async function defaultResolveExecutablePath(input: { + browserRoot: string; + env: NodeJS.ProcessEnv; + spec: PlaywrightChromiumSpec; +}): Promise { + const script = ` +const { createRequire } = require("node:module"); +const requireFromPlaywright = createRequire(${JSON.stringify(join(input.spec.packageRoot, "package.json"))}); +const { chromium } = requireFromPlaywright("playwright"); +process.stdout.write(chromium.executablePath()); +`; + const { stdout } = await execFileAsync(process.execPath, ["-e", script], { + env: { + ...input.env, + PLAYWRIGHT_BROWSERS_PATH: input.browserRoot, + }, + maxBuffer: 1024 * 1024, + }); + return stdout.trim(); +} + +function promoteManagedBrowserInstall(tmpRoot: string, targetRoot: string): void { + const backupRoot = `${targetRoot}.previous-${process.pid}-${Date.now()}`; + let hasBackup = false; + + try { + if (existsSync(targetRoot)) { + renameSync(targetRoot, backupRoot); + hasBackup = true; + } + renameSync(tmpRoot, targetRoot); + if (hasBackup) { + rmSync(backupRoot, { recursive: true, force: true }); + } + } catch (error) { + rmSync(targetRoot, { recursive: true, force: true }); + if (hasBackup && existsSync(backupRoot)) { + renameSync(backupRoot, targetRoot); + } + throw error; + } +} + +export function getManagedChromiumStatus(env: NodeJS.ProcessEnv = process.env): { + installed: boolean; + root: string; + metadata: ManagedBrowserMetadata | null; + spec: Pick; +} { + const root = managedBrowserRoot(env); + const spec = readPlaywrightChromiumSpec(); + const metadata = readValidMetadata(root, spec); + return { + installed: Boolean(metadata), + root: versionRoot(root, spec), + metadata, + spec: { + playwrightVersion: spec.playwrightVersion, + revision: spec.revision, + browserVersion: spec.browserVersion, + }, + }; +} + +export function getManagedChromiumBrowser( + env: NodeJS.ProcessEnv = process.env, +): BrowserCandidate | null { + const metadata = readValidMetadata(managedBrowserRoot(env)); + if (!metadata) { + return null; + } + return { + name: MANAGED_BROWSER_NAME, + executablePath: metadata.executablePath, + appBundleIdentifier: MANAGED_BROWSER_BUNDLE_ID, + }; +} + +export async function installManagedChromiumBrowser( + options: InstallOptions = {}, +): Promise { + const env = options.env ?? process.env; + const root = managedBrowserRoot(env); + const spec = readPlaywrightChromiumSpec(); + const existing = readValidMetadata(root, spec); + if (existing && !options.force) { + return { + name: MANAGED_BROWSER_NAME, + executablePath: existing.executablePath, + appBundleIdentifier: MANAGED_BROWSER_BUNDLE_ID, + }; + } + + const targetRoot = versionRoot(root, spec); + const tmpRoot = `${targetRoot}.install-${process.pid}-${Date.now()}`; + rmSync(tmpRoot, { recursive: true, force: true }); + mkdirSync(dirname(targetRoot), { recursive: true, mode: 0o700 }); + const releaseLock = await acquireInstallLock(targetRoot); + + try { + const lockedExisting = readValidMetadata(root, spec); + if (lockedExisting && !options.force) { + return { + name: MANAGED_BROWSER_NAME, + executablePath: lockedExisting.executablePath, + appBundleIdentifier: MANAGED_BROWSER_BUNDLE_ID, + }; + } + + await (options.installChromium ?? defaultInstallChromium)({ + destinationRoot: tmpRoot, + env, + spec, + }); + const tempExecutablePath = await ( + options.resolveExecutablePath ?? defaultResolveExecutablePath + )({ + browserRoot: tmpRoot, + env, + spec, + }); + if (!isPathInside(tmpRoot, tempExecutablePath) || !existsSync(tempExecutablePath)) { + throw new Error(`Managed Chromium executable was not found at ${tempExecutablePath}`); + } + + const executablePath = join( + targetRoot, + relative(resolve(tmpRoot), resolve(tempExecutablePath)), + ); + + const metadata: ManagedBrowserMetadata = { + provider: "playwright", + browser: "chromium", + playwrightVersion: spec.playwrightVersion, + revision: spec.revision, + browserVersion: spec.browserVersion, + platform: currentPlatform(), + executablePath, + installedAt: Date.now(), + }; + writeFileSync(join(tmpRoot, METADATA_FILE), JSON.stringify(metadata, null, 2), { + mode: 0o600, + }); + promoteManagedBrowserInstall(tmpRoot, targetRoot); + return { + name: MANAGED_BROWSER_NAME, + executablePath, + appBundleIdentifier: MANAGED_BROWSER_BUNDLE_ID, + }; + } finally { + rmSync(tmpRoot, { recursive: true, force: true }); + releaseLock(); + } +} From 530d5eeaad6695d7e73f2736b6bdacd3a092044a Mon Sep 17 00:00:00 2001 From: Theo Tarr Date: Sun, 24 May 2026 11:07:01 -0400 Subject: [PATCH 2/4] Resolve Playwright core from Playwright package --- src/platforms/core/auth/managed-browser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platforms/core/auth/managed-browser.ts b/src/platforms/core/auth/managed-browser.ts index 13de4e97..a3329e4c 100644 --- a/src/platforms/core/auth/managed-browser.ts +++ b/src/platforms/core/auth/managed-browser.ts @@ -60,9 +60,10 @@ function currentPlatform(): string { function readPlaywrightChromiumSpec(): PlaywrightChromiumSpec { const packagePath = require.resolve("playwright/package.json"); const packageRoot = dirname(packagePath); + const requireFromPlaywright = createRequire(packagePath); const playwrightVersion = (JSON.parse(readFileSync(packagePath, "utf8")) as { version: string }) .version; - const corePackageRoot = dirname(require.resolve("playwright-core/package.json")); + const corePackageRoot = dirname(requireFromPlaywright.resolve("playwright-core/package.json")); const browsers = JSON.parse(readFileSync(join(corePackageRoot, "browsers.json"), "utf8")) as { browsers: Array<{ name: string; revision: string; browserVersion?: string }>; }; From a68b03f2acf02689bca3fc9c9fdae820a81d9511 Mon Sep 17 00:00:00 2001 From: Theo Tarr Date: Sun, 24 May 2026 11:13:35 -0400 Subject: [PATCH 3/4] Harden managed Chromium install locking --- .../core/auth/managed-browser.test.ts | 44 ++++++++++++++ src/platforms/core/auth/managed-browser.ts | 59 +++++++++++-------- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/src/platforms/core/auth/managed-browser.test.ts b/src/platforms/core/auth/managed-browser.test.ts index 77539602..70ff39ec 100644 --- a/src/platforms/core/auth/managed-browser.test.ts +++ b/src/platforms/core/auth/managed-browser.test.ts @@ -92,6 +92,50 @@ describe("managed Chromium browser", () => { expect(getManagedChromiumBrowser({ CUED_MANAGED_BROWSER_DIR: root })).toEqual(first.browser); }); + it("serializes concurrent managed browser installs", async () => { + const root = tempDir("cued-managed-browser-concurrent-"); + let installCalls = 0; + let releaseFirstInstall!: () => void; + let markFirstInstallStarted!: () => void; + const started = new Promise((resolveStarted) => { + markFirstInstallStarted = resolveStarted; + }); + const release = new Promise((resolveRelease) => { + releaseFirstInstall = resolveRelease; + }); + + const install = () => + installManagedChromiumBrowser({ + env: { CUED_MANAGED_BROWSER_DIR: root }, + installChromium: async ({ destinationRoot }) => { + installCalls += 1; + markFirstInstallStarted(); + await release; + const executablePath = join( + destinationRoot, + "chromium", + "Chromium.app", + "Contents", + "MacOS", + "Chromium", + ); + mkdirSync(dirname(executablePath), { recursive: true }); + writeFileSync(executablePath, ""); + }, + resolveExecutablePath: async ({ browserRoot }) => + join(browserRoot, "chromium", "Chromium.app", "Contents", "MacOS", "Chromium"), + }); + + const first = install(); + await started; + const second = install(); + releaseFirstInstall(); + + const [firstResult, secondResult] = await Promise.all([first, second]); + expect(secondResult).toEqual(firstResult); + expect(installCalls).toBe(1); + }); + it("forces reinstall when the managed browser should be refreshed", async () => { const root = tempDir("cued-managed-browser-force-"); const first = await installFakeBrowser(root, "OldChromium"); diff --git a/src/platforms/core/auth/managed-browser.ts b/src/platforms/core/auth/managed-browser.ts index a3329e4c..7eb5757b 100644 --- a/src/platforms/core/auth/managed-browser.ts +++ b/src/platforms/core/auth/managed-browser.ts @@ -116,6 +116,17 @@ function isProcessAlive(pid: number): boolean { } } +function readLockOwner(path: string): { pid?: number; createdAt?: number } | null { + try { + return JSON.parse(readFileSync(join(path, "owner.json"), "utf8")) as { + pid?: number; + createdAt?: number; + }; + } catch { + return null; + } +} + async function acquireInstallLock(targetRoot: string): Promise<() => void> { const path = lockPath(targetRoot); const deadline = Date.now() + INSTALL_LOCK_TIMEOUT_MS; @@ -138,26 +149,22 @@ async function acquireInstallLock(targetRoot: string): Promise<() => void> { throw error; } - try { - const owner = JSON.parse(readFileSync(join(path, "owner.json"), "utf8")) as { - pid?: number; - createdAt?: number; - }; - if (typeof owner.pid === "number" && isProcessAlive(owner.pid)) { - if (Date.now() >= deadline) { - throw new Error("Timed out waiting for managed Chromium install lock"); - } - await sleep(INSTALL_LOCK_RETRY_MS); - continue; - } - if ( - typeof owner.createdAt === "number" && - Date.now() - owner.createdAt > INSTALL_LOCK_STALE_MS - ) { - rmSync(path, { recursive: true, force: true }); - continue; + const owner = readLockOwner(path); + if (!owner) { + rmSync(path, { recursive: true, force: true }); + continue; + } + if (typeof owner.pid === "number" && isProcessAlive(owner.pid)) { + if (Date.now() >= deadline) { + throw new Error("Timed out waiting for managed Chromium install lock"); } - } catch { + await sleep(INSTALL_LOCK_RETRY_MS); + continue; + } + if ( + typeof owner.createdAt === "number" && + Date.now() - owner.createdAt > INSTALL_LOCK_STALE_MS + ) { rmSync(path, { recursive: true, force: true }); continue; } @@ -236,20 +243,24 @@ process.stdout.write(chromium.executablePath()); function promoteManagedBrowserInstall(tmpRoot: string, targetRoot: string): void { const backupRoot = `${targetRoot}.previous-${process.pid}-${Date.now()}`; - let hasBackup = false; + let targetMovedToBackup = false; + let replacementPromoted = false; try { if (existsSync(targetRoot)) { renameSync(targetRoot, backupRoot); - hasBackup = true; + targetMovedToBackup = true; } renameSync(tmpRoot, targetRoot); - if (hasBackup) { + replacementPromoted = true; + if (targetMovedToBackup) { rmSync(backupRoot, { recursive: true, force: true }); } } catch (error) { - rmSync(targetRoot, { recursive: true, force: true }); - if (hasBackup && existsSync(backupRoot)) { + if (replacementPromoted) { + rmSync(targetRoot, { recursive: true, force: true }); + } + if (targetMovedToBackup && existsSync(backupRoot)) { renameSync(backupRoot, targetRoot); } throw error; From 95375e7b193fd2f4fcffad883b40e383350630a2 Mon Sep 17 00:00:00 2001 From: Theo Tarr Date: Sun, 24 May 2026 11:59:20 -0400 Subject: [PATCH 4/4] Default chromium auth launch targets --- src/platforms/core/auth/chromium-worker.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/platforms/core/auth/chromium-worker.ts b/src/platforms/core/auth/chromium-worker.ts index 5096c190..bb38e009 100644 --- a/src/platforms/core/auth/chromium-worker.ts +++ b/src/platforms/core/auth/chromium-worker.ts @@ -50,12 +50,18 @@ const REQUIRED_ARGS = new Set([ "--launch-target", ]); +const DEFAULT_LAUNCH_TARGETS: Record = { + slack: "https://slack.com/signin", + linkedin: "https://www.linkedin.com/login", + discord: "https://discord.com/login", +}; + function parseArgs(argv: string[]): WorkerArgs { const values = new Map(); for (let index = 0; index < argv.length; index += 2) { const key = argv[index]; const value = argv[index + 1]; - if (!REQUIRED_ARGS.has(key) || !value) { + if (!REQUIRED_ARGS.has(key) || value == null) { throw new Error(`Invalid argument sequence at ${key ?? "end"}`); } values.set(key, value); @@ -71,7 +77,7 @@ function parseArgs(argv: string[]): WorkerArgs { accountKey: values.get("--account-key")!, sessionId: values.get("--session-id")!, profileDir: values.get("--profile-dir")!, - launchTarget: values.get("--launch-target")!, + launchTarget: values.get("--launch-target") || DEFAULT_LAUNCH_TARGETS[platform], }; }