| 1 | import { existsSync } from "node:fs"; |
| 2 | import { |
| 3 | applyBrowserSessionPolicy, |
| 4 | clearBrowserCache, |
| 5 | clearBrowserData, |
| 6 | type BrowserControlStore, |
| 7 | type BrowserSession, |
| 8 | } from "./browserControl.js"; |
| 9 | import { |
| 10 | ChromeImportError, |
| 11 | defaultRunCommand, |
| 12 | findChromeProfiles, |
| 13 | importChromeCookies, |
| 14 | chromeProfileRoot, |
| 15 | type ChromeImportDeps, |
| 16 | type CookieSink, |
| 17 | } from "./chromeImport.js"; |
| 18 | import type { Logger } from "./log.js"; |
| 19 | import type { ChromeImportOutcome, BrowserControlState } from "../shared/ipc.js"; |
| 20 | |
| 21 | export interface BrowserControlHostDeps { |
| 22 | store: BrowserControlStore; |
| 23 | sharedSession(): BrowserSession & { cookies: CookieSink }; |
| 24 | log: Logger; |
| 25 | platform: NodeJS.Platform; |
| 26 | home: string; |
| 27 | env: NodeJS.ProcessEnv; |
| 28 | run?(command: string, args: string[]): Promise<string>; |
| 29 | list(path: string): string[]; |
| 30 | onControlEnabled(enabled: boolean): void; |
| 31 | } |
| 32 | |
| 33 | // The renderer-facing surface: settings state plus the one-shot actions that |
| 34 | // only make sense against the browser partition. |
| 35 | export interface BrowserControlApi { |
| 36 | state(): BrowserControlState; |
| 37 | setControlEnabled(enabled: boolean): Promise<BrowserControlState>; |
| 38 | setIgnoreCertificateErrors(enabled: boolean): Promise<BrowserControlState>; |
| 39 | clearCache(): Promise<void>; |
| 40 | clearAllData(): Promise<void>; |
| 41 | importChromeLogin(): Promise<ChromeImportOutcome>; |
| 42 | } |
| 43 | |
| 44 | export class BrowserControlHost implements BrowserControlApi { |
| 45 | private readonly sessions = new Map<string, BrowserSession>(); |
| 46 | |
| 47 | constructor(private readonly deps: BrowserControlHostDeps) {} |
| 48 | |
| 49 | state(): BrowserControlState { |
| 50 | return this.deps.store.current; |
| 51 | } |
| 52 | |
| 53 | // Every guest partition is the built-in browser, so the certificate policy |
| 54 | // follows each one as it appears; the app window never goes through here. |
| 55 | trackSession(partition: string, session: BrowserSession): void { |
| 56 | this.sessions.set(partition, session); |
| 57 | applyBrowserSessionPolicy(session, this.deps.store.current.ignoreCertificateErrors); |
| 58 | } |
| 59 | |
| 60 | async setControlEnabled(enabled: boolean): Promise<BrowserControlState> { |
| 61 | const state = await this.deps.store.setControlEnabled(enabled); |
| 62 | this.deps.log.info(`browser control enabled=${state.controlEnabled}`); |
| 63 | this.deps.onControlEnabled(state.controlEnabled); |
| 64 | return state; |
| 65 | } |
| 66 | |
| 67 | async setIgnoreCertificateErrors(enabled: boolean): Promise<BrowserControlState> { |
| 68 | const state = await this.deps.store.setIgnoreCertificateErrors(enabled); |
| 69 | for (const session of this.sessions.values()) applyBrowserSessionPolicy(session, state.ignoreCertificateErrors); |
| 70 | this.deps.log.info(`browser certificate verification relaxed=${state.ignoreCertificateErrors}`); |
| 71 | return state; |
| 72 | } |
| 73 | |
| 74 | async clearCache(): Promise<void> { |
| 75 | await clearBrowserCache(this.deps.sharedSession()); |
| 76 | this.deps.log.info("browser cache cleared"); |
| 77 | } |
| 78 | |
| 79 | async clearAllData(): Promise<void> { |
| 80 | await clearBrowserData(this.deps.sharedSession()); |
| 81 | this.deps.log.info("browser data cleared"); |
| 82 | } |
| 83 | |
| 84 | async importChromeLogin(): Promise<ChromeImportOutcome> { |
| 85 | const root = chromeProfileRoot(this.deps.platform, this.deps.home, this.deps.env); |
| 86 | if (!existsSync(root)) { |
| 87 | this.deps.log.warn(`chrome import: no Chrome data at ${root}`); |
| 88 | return { ok: false, reason: "chrome-missing" }; |
| 89 | } |
| 90 | const profiles = findChromeProfiles(root, this.deps.list); |
| 91 | if (profiles.length === 0) { |
| 92 | this.deps.log.warn(`chrome import: no profile with cookies under ${root}`); |
| 93 | return { ok: false, reason: "profile-not-found" }; |
| 94 | } |
| 95 | const deps: ChromeImportDeps = { |
| 96 | platform: this.deps.platform, |
| 97 | home: this.deps.home, |
| 98 | env: this.deps.env, |
| 99 | cookies: this.deps.sharedSession().cookies, |
| 100 | run: this.deps.run ?? defaultRunCommand, |
| 101 | log: this.deps.log, |
| 102 | }; |
| 103 | try { |
| 104 | const summary = await importChromeCookies(deps, root, profiles); |
| 105 | return { ok: true, ...summary }; |
| 106 | } catch (error) { |
| 107 | const reason = error instanceof ChromeImportError ? error.code : "cookies-unreadable"; |
| 108 | this.deps.log.warn(`chrome import failed (${reason}): ${error instanceof Error ? error.message : String(error)}`); |
| 109 | return { ok: false, reason }; |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 |