@trusty-squire/mcp 1.1.13 → 1.1.14-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +19 -11
  2. package/dist/bot/browser-process-owner.d.ts +75 -0
  3. package/dist/bot/browser-process-owner.d.ts.map +1 -0
  4. package/dist/bot/browser-process-owner.js +931 -0
  5. package/dist/bot/browser-process-owner.js.map +1 -0
  6. package/dist/bot/browser-process-runtime.d.ts +159 -0
  7. package/dist/bot/browser-process-runtime.d.ts.map +1 -0
  8. package/dist/bot/browser-process-runtime.js +905 -0
  9. package/dist/bot/browser-process-runtime.js.map +1 -0
  10. package/dist/bot/browser.d.ts +45 -194
  11. package/dist/bot/browser.d.ts.map +1 -1
  12. package/dist/bot/browser.js +666 -2358
  13. package/dist/bot/browser.js.map +1 -1
  14. package/dist/bot/compact-observation-v2.d.ts +9 -0
  15. package/dist/bot/compact-observation-v2.d.ts.map +1 -1
  16. package/dist/bot/compact-observation-v2.js +141 -82
  17. package/dist/bot/compact-observation-v2.js.map +1 -1
  18. package/dist/bot/identity-runtime.d.ts +56 -0
  19. package/dist/bot/identity-runtime.d.ts.map +1 -0
  20. package/dist/bot/identity-runtime.js +141 -0
  21. package/dist/bot/identity-runtime.js.map +1 -0
  22. package/dist/bot/owned-pages.d.ts +16 -0
  23. package/dist/bot/owned-pages.d.ts.map +1 -0
  24. package/dist/bot/owned-pages.js +58 -0
  25. package/dist/bot/owned-pages.js.map +1 -0
  26. package/dist/bot/page-driver.d.ts +35 -0
  27. package/dist/bot/page-driver.d.ts.map +1 -0
  28. package/dist/bot/page-driver.js +302 -0
  29. package/dist/bot/page-driver.js.map +1 -0
  30. package/dist/bot/provision-session.d.ts +4 -0
  31. package/dist/bot/provision-session.d.ts.map +1 -1
  32. package/dist/bot/provision-session.js +122 -29
  33. package/dist/bot/provision-session.js.map +1 -1
  34. package/dist/bot/session/lifecycle.d.ts.map +1 -1
  35. package/dist/bot/session/lifecycle.js +218 -14
  36. package/dist/bot/session/lifecycle.js.map +1 -1
  37. package/dist/bot/session/multisession-flag.d.ts +2 -0
  38. package/dist/bot/session/multisession-flag.d.ts.map +1 -0
  39. package/dist/bot/session/multisession-flag.js +23 -0
  40. package/dist/bot/session/multisession-flag.js.map +1 -0
  41. package/dist/tools/provision-drive.d.ts +12 -12
  42. package/package.json +1 -1
@@ -0,0 +1,931 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {} from "node:child_process";
4
+ import { clearStaleSingletonLock, closeProfileWithProof, currentProfileHolderPid, profileProcessIdentity, profileProcessIdentityState, profileProcessMatches, reapProfileHolderIfOwned, } from "./profile.js";
5
+ import { createOperatorBrowserMarker, OPERATOR_BROWSER_MARKER_ENV, operatorBrowserProcessMatchesMarker, startGlobalOperatorBrowserProcessWatchdog, } from "./operator-browser-watchdog.js";
6
+ import { bindOwnerBrowserLaunch, markOwnerBrowserLaunchTerminal, terminateOwnerBrowserLaunch, untrackOwnerBrowserLaunch, } from "./owner-process-reaper.js";
7
+ import { activeStealthProfileValue, canSelfLaunchWithProxy, captureOwnedChromeProcessTreeProof, detectChromiumChannel, DEVTOOLS_ACTIVE_PORT_FILE, getChromium, launchCancellablePersistentContext, OPERATOR_BROWSER_HEADLESS, ownedChromeProcessTreeState, parseEgressGeo, PERSISTENT_CONTEXT_CANCELLATION_SETTLE_MS, persistentProxyOptions, PROFILE_IDENTITY_POLL_MS, PROFILE_IDENTITY_PROOF_TIMEOUT_MS, registerLocalBrowserLaunch, registerSelfManagedChrome, releaseOwnedChromeProcessTree, resolveAttachedProfileChildIdentity, resolveChannelBinary, resolveExplicitProxy, resolvePersistentFallbackIdentity, selfLaunchEnabled, selfManagedChromes, signalOwnedChromeProcessTree, spawnLocalBrowser, terminateTrackedProfileChild, trackOwnedChromeProcessTree, waitForOwnedDevtoolsEndpoint, } from "./browser-process-runtime.js";
8
+ /** Exclusive Chrome custody. Page setup is awaited at the original startup boundary. */
9
+ export class BrowserProcessOwner {
10
+ pages;
11
+ initializePages;
12
+ // A persistent browser context backed by the user's real Chrome profile.
13
+ context = null;
14
+ // Self-launch path (Turnstile-safe; see selfLaunchEnabled). When we spawn
15
+ // Chrome ourselves and attach over CDP, these hold the child process and
16
+ // the connected Browser so close() can tear both down.
17
+ childChrome = null;
18
+ childChromeIdentity = null;
19
+ childChromeProcessGroup = false;
20
+ ownedDisplayRig = null;
21
+ ownedChromeProcessTreeProof = null;
22
+ operatorProcessMarker = null;
23
+ ownerLaunchTracked = false;
24
+ cdpBrowser = null;
25
+ // True once a local browser context launched this session.
26
+ launchedContext = false;
27
+ launchedProfileHolderIdentity = null;
28
+ startPromise = null;
29
+ closePromise = null;
30
+ startCancellationRequested = false;
31
+ startLaunchCommitted = false;
32
+ startSettled = false;
33
+ persistentFallbackLaunchInFlight = false;
34
+ persistentFallbackOwnershipMonitor = null;
35
+ persistentFallbackCancellationState = null;
36
+ resolveStartCancellation = null;
37
+ startCancellation = new Promise((resolveCancellation) => {
38
+ this.resolveStartCancellation = resolveCancellation;
39
+ });
40
+ cancelledStartReaper = null;
41
+ // Records the browser channel that .start() actually launched. Set
42
+ // post-launch so telemetry can surface "this run
43
+ // used real Chrome" vs "this run used bundled Chromium." Useful for
44
+ // separating fingerprint regressions from network regressions when
45
+ // a service starts failing.
46
+ launchedChannel = null;
47
+ // The proxy server this run egressed through, or null for a direct
48
+ // connection. Set by .start(); surfaced via the `proxied` getter —
49
+ // a captcha failure behind a residential proxy is materially
50
+ // different signal from the same failure on a raw datacenter IP.
51
+ proxyServer = null;
52
+ profileDir;
53
+ // Per-launch egress override. null means direct egress. Explicit overrides
54
+ // are never subject to host-network classification.
55
+ proxyOverride;
56
+ // Surfaced in the run trail so operators can distinguish local headed,
57
+ // remote, and headless launches.
58
+ launchedMode = "unknown";
59
+ constructor(opts, pages, initializePages) {
60
+ this.pages = pages;
61
+ this.initializePages = initializePages;
62
+ this.profileDir = opts.profileDir ?? "";
63
+ this.proxyOverride =
64
+ opts.proxyUrl !== undefined && opts.proxyUrl.trim().length > 0 ? opts.proxyUrl.trim() : null;
65
+ }
66
+ operatorBrowserMarker() {
67
+ this.operatorProcessMarker ??= createOperatorBrowserMarker();
68
+ return this.operatorProcessMarker;
69
+ }
70
+ async ownedHeadedBrowserEnvironment() {
71
+ if (this.ownedDisplayRig === null) {
72
+ const { createXvfbDisplayRig, startRemoteLoginDisplay } = await import("./remote-login-display.js");
73
+ const rig = createXvfbDisplayRig();
74
+ this.ownedDisplayRig = rig;
75
+ await startRemoteLoginDisplay(rig);
76
+ }
77
+ const { remoteLoginEnvironment } = await import("./remote-login-display.js");
78
+ const rig = this.ownedDisplayRig;
79
+ if (rig === null)
80
+ throw new Error("headed operator display did not start");
81
+ return remoteLoginEnvironment(rig, process.env);
82
+ }
83
+ async teardownOwnedDisplay() {
84
+ const rig = this.ownedDisplayRig;
85
+ this.ownedDisplayRig = null;
86
+ if (rig === null)
87
+ return;
88
+ const { teardownRemoteLoginRig } = await import("./remote-login-display.js");
89
+ await teardownRemoteLoginRig(rig);
90
+ }
91
+ adoptOwnedChromeProcessTree(identity, processGroup) {
92
+ if (this.ownedChromeProcessTreeProof?.identity.pid === identity.pid &&
93
+ this.ownedChromeProcessTreeProof.identity.start_time === identity.start_time) {
94
+ return this.ownedChromeProcessTreeProof;
95
+ }
96
+ const tracked = selfManagedChromes.get(identity.pid);
97
+ const proof = tracked?.identity.start_time === identity.start_time
98
+ ? tracked.proof
99
+ : trackOwnedChromeProcessTree(identity, processGroup);
100
+ if (proof !== null && this.ownerLaunchTracked) {
101
+ if (!bindOwnerBrowserLaunch(this.operatorBrowserMarker(), proof.identity)) {
102
+ releaseOwnedChromeProcessTree(proof);
103
+ throw new Error("local browser launch identity could not be bound to owner custody");
104
+ }
105
+ }
106
+ if (proof !== null)
107
+ this.ownedChromeProcessTreeProof = proof;
108
+ return proof;
109
+ }
110
+ signalCurrentSelfManagedChrome(identity, signal) {
111
+ return signalOwnedChromeProcessTree(identity, this.childChromeProcessGroup, signal, {
112
+ ...(this.ownedChromeProcessTreeProof === null
113
+ ? {}
114
+ : { proof: this.ownedChromeProcessTreeProof }),
115
+ });
116
+ }
117
+ // Required health gate for a live session browser. BrowserContext alone is not a
118
+ // sufficient signal: a dead CDP transport can leave stale JS objects behind.
119
+ isConnected() {
120
+ const browser = this.cdpBrowser ?? this.context?.browser() ?? null;
121
+ return browser?.isConnected() === true;
122
+ }
123
+ // Which browser channel the most recent .start() actually used.
124
+ // `null` means bundled Chromium; a string like "chrome" means a
125
+ // real installed browser of that channel. Throws if .start() hasn't
126
+ // been called yet — there's no sensible default to return.
127
+ get channel() {
128
+ if (this.context === null) {
129
+ throw new Error("BrowserController.channel read before .start()");
130
+ }
131
+ return this.launchedChannel;
132
+ }
133
+ // The proxy server the most recent .start() routed egress through,
134
+ // or null for a direct connection. Useful telemetry alongside
135
+ // `channel`. Throws if .start() hasn't run — same reason as channel.
136
+ get proxied() {
137
+ if (this.context === null) {
138
+ throw new Error("BrowserController.proxied read before .start()");
139
+ }
140
+ return this.proxyServer;
141
+ }
142
+ // The stealth profile the most recent .start() launched under:
143
+ // "cdp_hardened" when the patchright launcher actually loaded
144
+ // (BOT_CDP_HARDENED set + patchright present), else "baseline". Surfaced
145
+ // for the CaptchaEvent A/B tag. Throws before .start() — same reason
146
+ // as channel/proxied.
147
+ get stealthProfile() {
148
+ if (this.context === null) {
149
+ throw new Error("BrowserController.stealthProfile read before .start()");
150
+ }
151
+ return activeStealthProfileValue();
152
+ }
153
+ get launchMode() {
154
+ return this.launchedMode;
155
+ }
156
+ // Launch Chrome ourselves and attach over CDP — the Turnstile-safe launch
157
+ // (see selfLaunchEnabled for the proof). The profile dir is the SAME shared
158
+ // profile launchPersistentContext would use, so the OAuth session carries
159
+ // over. Options that a default connectOverCDP context can't take at creation
160
+ // are applied differently:
161
+ // • timezone → TZ env on the child (more authentic than a CDP override)
162
+ // • proxy → --proxy-server flag, with credentials applied post-connect
163
+ // • viewport → --window-size (with viewport:null-equivalent: we never set
164
+ // an emulated viewport on the connected context)
165
+ // • locale/geo/permissions → applied post-connect by start()
166
+ async launchSelfManagedContext(params) {
167
+ this.throwIfStartCancelled();
168
+ // Remote-CDP attach: BOT_CDP_ENDPOINT points at a Chrome already running on
169
+ // another host (e.g. a real-GPU Mac), reachable over Tailscale. We do NOT
170
+ // spawn or own the binary — the remote host launched it with its own
171
+ // profile, real GPU, and (residential) egress. Just attach over CDP. This
172
+ // is the real-GPU path: software-WebGL output (llvmpipe) is what
173
+ // hCaptcha-Enterprise-class anti-bot scores, and only real hardware fixes
174
+ // the rendered-pixel fingerprint that JS spoofing can't.
175
+ const remoteEndpoint = (process.env.BOT_CDP_ENDPOINT ?? "").trim();
176
+ if (remoteEndpoint.length > 0) {
177
+ const launcher = getChromium();
178
+ const browser = await launcher.connectOverCDP(remoteEndpoint);
179
+ this.cdpBrowser = browser;
180
+ this.launchedMode = "remote";
181
+ const ctx = browser.contexts()[0];
182
+ if (ctx === undefined) {
183
+ throw new Error(`remote Chrome (BOT_CDP_ENDPOINT=${remoteEndpoint}) exposed no default browser context`);
184
+ }
185
+ return ctx;
186
+ }
187
+ const endpoint = await (async () => {
188
+ this.throwIfStartCancelled();
189
+ clearStaleSingletonLock(this.profileDir);
190
+ rmSync(join(this.profileDir, DEVTOOLS_ACTIVE_PORT_FILE), { force: true });
191
+ const argv = [
192
+ "--remote-debugging-port=0",
193
+ "--remote-debugging-address=127.0.0.1",
194
+ `--user-data-dir=${this.profileDir}`,
195
+ "--no-first-run",
196
+ "--no-default-browser-check",
197
+ "--password-store=basic",
198
+ "--window-position=0,0",
199
+ `--window-size=${params.window.width},${params.window.height}`,
200
+ "--lang=en-US",
201
+ ...params.args,
202
+ ...(params.proxy !== null ? [`--proxy-server=${params.proxy.server}`] : []),
203
+ "about:blank",
204
+ ];
205
+ this.commitProfileLaunch();
206
+ const child = spawnLocalBrowser(params.binary, argv, this.profileDir, {
207
+ env: params.env,
208
+ stdio: ["ignore", "ignore", "pipe"],
209
+ // A dedicated process group gives the session a single, identity-
210
+ // proven teardown target for Chrome plus every renderer/GPU helper.
211
+ detached: process.platform !== "win32",
212
+ marker: this.operatorBrowserMarker(),
213
+ });
214
+ this.childChrome = child;
215
+ this.childChromeProcessGroup = process.platform !== "win32";
216
+ this.childChromeIdentity = registerSelfManagedChrome(child, this.profileDir, this.childChromeProcessGroup);
217
+ if (this.childChromeIdentity !== null) {
218
+ this.adoptOwnedChromeProcessTree(this.childChromeIdentity, this.childChromeProcessGroup);
219
+ }
220
+ let chromeStderr = "";
221
+ let chromeExit = "";
222
+ child.stderr?.on("data", (chunk) => {
223
+ chromeStderr = (chromeStderr + chunk.toString("utf8")).slice(-4_000);
224
+ });
225
+ child.on("exit", (code, signal) => {
226
+ chromeExit = ` exit=${code ?? "null"} signal=${signal ?? "none"}`;
227
+ });
228
+ if (this.startCancellationRequested) {
229
+ await this.cancelSpawnedSelfManagedChrome(child);
230
+ throw new Error("BrowserController start cancelled");
231
+ }
232
+ try {
233
+ const endpoint = await waitForOwnedDevtoolsEndpoint(this.profileDir, 30_000, child);
234
+ this.childChromeIdentity = await resolveAttachedProfileChildIdentity(child, this.profileDir, this.childChromeIdentity, { processGroup: this.childChromeProcessGroup });
235
+ if (process.platform === "linux" && this.childChromeIdentity === null) {
236
+ throw new Error("self-launched Chrome exited before identity was proven");
237
+ }
238
+ if (this.childChromeIdentity !== null) {
239
+ this.adoptOwnedChromeProcessTree(this.childChromeIdentity, this.childChromeProcessGroup);
240
+ }
241
+ return endpoint;
242
+ }
243
+ catch (err) {
244
+ const alive = this.childChromeIdentity !== null &&
245
+ profileProcessMatches(this.childChromeIdentity, this.profileDir);
246
+ this.childChromeIdentity = await terminateTrackedProfileChild(child, this.profileDir, {
247
+ identity: this.childChromeIdentity,
248
+ terminate: (identity, profileDir) => {
249
+ const signalled = signalOwnedChromeProcessTree(identity, this.childChromeProcessGroup, "SIGKILL", {
250
+ ...(this.ownedChromeProcessTreeProof === null
251
+ ? {}
252
+ : { proof: this.ownedChromeProcessTreeProof }),
253
+ });
254
+ reapProfileHolderIfOwned(profileDir, identity);
255
+ return signalled;
256
+ },
257
+ processGroup: this.childChromeProcessGroup,
258
+ });
259
+ this.childChrome = null;
260
+ this.childChromeIdentity = null;
261
+ this.childChromeProcessGroup = false;
262
+ const detail = chromeStderr.trim();
263
+ throw new Error(`${err instanceof Error ? err.message : String(err)}; Chrome pid=${child.pid ?? "unknown"} alive=${alive ? 1 : 0}` +
264
+ `${chromeExit}${detail.length > 0 ? `; Chrome stderr: ${detail}` : ""}`);
265
+ }
266
+ })();
267
+ // Use the patchright launcher's connectOverCDP — it's the exact path the
268
+ // falsification experiment validated (its connect avoids Runtime.enable,
269
+ // which a plain attach would emit). The anti-detection that matters here
270
+ // is the LAUNCH (which we now own), not the connect.
271
+ const launcher = getChromium();
272
+ const browser = await launcher.connectOverCDP(endpoint);
273
+ this.cdpBrowser = browser;
274
+ const ctx = browser.contexts()[0];
275
+ if (ctx === undefined) {
276
+ throw new Error("self-launched Chrome exposed no default browser context");
277
+ }
278
+ return ctx;
279
+ }
280
+ async cancelSpawnedSelfManagedChrome(child) {
281
+ this.childChromeIdentity = await terminateTrackedProfileChild(child, this.profileDir, {
282
+ identity: this.childChromeIdentity,
283
+ terminate: (identity, profileDir) => {
284
+ const signalled = signalOwnedChromeProcessTree(identity, this.childChromeProcessGroup, "SIGKILL", {
285
+ ...(this.ownedChromeProcessTreeProof === null
286
+ ? {}
287
+ : { proof: this.ownedChromeProcessTreeProof }),
288
+ });
289
+ reapProfileHolderIfOwned(profileDir, identity);
290
+ return signalled;
291
+ },
292
+ processGroup: this.childChromeProcessGroup,
293
+ });
294
+ if (this.childChrome === child)
295
+ this.childChrome = null;
296
+ this.childChromeIdentity = null;
297
+ this.childChromeProcessGroup = false;
298
+ }
299
+ async start() {
300
+ if (this.profileDir.length === 0) {
301
+ throw new Error("BrowserController.start requires a per-session profile directory");
302
+ }
303
+ if (this.closePromise !== null)
304
+ throw new Error("BrowserController is already closing");
305
+ this.startPromise ??= this.startOnce();
306
+ await this.startPromise;
307
+ }
308
+ async startOnce() {
309
+ const remoteMode = (process.env.BOT_CDP_ENDPOINT ?? "").trim().length > 0;
310
+ if (!remoteMode)
311
+ startGlobalOperatorBrowserProcessWatchdog();
312
+ try {
313
+ await this.startBrowser();
314
+ if (this.startCancellationRequested) {
315
+ await this.closeBrowser();
316
+ throw new Error("BrowserController start cancelled");
317
+ }
318
+ }
319
+ catch (err) {
320
+ await this.teardownOwnedDisplay().catch(() => undefined);
321
+ if (this.startCancellationRequested && this.persistentFallbackCancellationState === null) {
322
+ await this.closeBrowser().catch(() => undefined);
323
+ }
324
+ throw err;
325
+ }
326
+ finally {
327
+ this.startSettled = true;
328
+ }
329
+ }
330
+ throwIfStartCancelled() {
331
+ if (this.startCancellationRequested)
332
+ throw new Error("BrowserController start cancelled");
333
+ }
334
+ commitProfileLaunch() {
335
+ this.throwIfStartCancelled();
336
+ this.startLaunchCommitted = true;
337
+ }
338
+ async startBrowser() {
339
+ this.throwIfStartCancelled();
340
+ const channel = await detectChromiumChannel();
341
+ this.throwIfStartCancelled();
342
+ this.launchedChannel = channel;
343
+ const proxy = await this.resolveProxy();
344
+ this.throwIfStartCancelled();
345
+ this.proxyServer = proxy?.server ?? null;
346
+ // Stderr so the MCP stdio transport's framing stays clean (the
347
+ // module's existing logging convention).
348
+ console.error(`[operator] launching browser channel=${channel ?? "bundled-chromium"} ` +
349
+ `proxy=${proxy === null ? "direct" : "configured"}`);
350
+ // Remote-CDP mode (BOT_CDP_ENDPOINT): the browser runs on a REMOTE host
351
+ // (e.g. a Mac with a real GPU + residential egress) and we attach over CDP
352
+ // across Tailscale. The remote machine IS a real device, so we spoof
353
+ // NOTHING — no WebGL/device fingerprint patch (a fake-Intel string over a
354
+ // real Apple-GPU output would be its own mismatch tell), no local display, no
355
+ // egress-geo override (the remote host's real timezone + residential IP are
356
+ // authentic). software-WebGL output is exactly what the toughest anti-bot
357
+ // (hCaptcha Enterprise) scores; only real hardware fixes the pixel
358
+ // fingerprint, which is the whole point of this path.
359
+ const remoteMode = (process.env.BOT_CDP_ENDPOINT ?? "").trim().length > 0;
360
+ if (remoteMode) {
361
+ console.error(`[operator] REMOTE-CDP mode — attaching to ${(process.env.BOT_CDP_ENDPOINT ?? "").trim()} ` +
362
+ `(real-host GPU + egress; local fingerprint spoof + display setup disabled)`);
363
+ }
364
+ if (!remoteMode && !this.ownerLaunchTracked) {
365
+ registerLocalBrowserLaunch(this.profileDir, process.env, this.operatorBrowserMarker());
366
+ this.ownerLaunchTracked = true;
367
+ }
368
+ const browserEnv = remoteMode ? process.env : await this.ownedHeadedBrowserEnvironment();
369
+ // T3.1: probe where this run's traffic actually exits so the
370
+ // browser's declared timezone matches its egress IP (a US-timezone
371
+ // browser on a foreign proxy IP is itself an anti-bot signal).
372
+ // Done before the real launch: launchPersistentContext bakes the
373
+ // timezone in at creation, with no way to set it afterward. Skipped in
374
+ // remote mode — the remote host's own clock/IP are the authentic truth.
375
+ const geo = remoteMode ? null : await this.probeEgressGeo(channel, proxy, browserEnv);
376
+ this.throwIfStartCancelled();
377
+ if (geo !== null) {
378
+ console.error(`[operator] egress geo: timezone=${geo.timezoneId}` +
379
+ (geo.geolocation !== undefined
380
+ ? ` loc=${geo.geolocation.latitude},${geo.geolocation.longitude}`
381
+ : ""));
382
+ }
383
+ // Keep the operator browser headed: the browser runs on the operator's
384
+ // Xvfb display, preserving the normal Chrome surface OAuth providers see.
385
+ this.launchedMode = "headed";
386
+ // T3: a PERSISTENT context backed by this operator session's unique
387
+ // profile. launchPersistentContext takes launch + context options in one
388
+ // call.
389
+ // Resolve the launcher first so activeStealthProfile is set before we
390
+ // decide on executablePath below.
391
+ const launcher = getChromium();
392
+ const hardened = activeStealthProfileValue() === "cdp_hardened";
393
+ // Both launchers drive real Chrome via `channel`: baseline through
394
+ // playwright+stealth, hardened through patchright. patchright closes
395
+ // the automation tells at the protocol layer and drives real Chrome
396
+ // directly — so it no longer needs the bundled-chromium pin the old
397
+ // rebrowser fork required (the pin is what crashed the OAuth flow and
398
+ // confounded the A/B). One binary for both arms.
399
+ this.launchedChannel = channel;
400
+ // Launch args shared by BOTH paths (launchPersistentContext and the
401
+ // self-launch). See the per-flag rationale: swiftshader gives a real
402
+ // (software) WebGL context on GPU-less hosts; the others are the
403
+ // standard headless/sandbox flags. The three background-throttling disables
404
+ // are payment correctness controls: a backgrounded CardinalCommerce ACS
405
+ // frame must keep running its timers long enough to finish the issuer's OOB
406
+ // post-approval handshake with Stripe. Keep them paired with bringToFront()
407
+ // before payment submission and in waitForThreeDsResolution(). NOTE we
408
+ // deliberately do NOT include Playwright's automation flags
409
+ // (--enable-automation et al.) — on the self-launch path their ABSENCE is
410
+ // the whole fix.
411
+ const launchArgs = [
412
+ "--disable-blink-features=AutomationControlled",
413
+ "--disable-background-timer-throttling",
414
+ "--disable-backgrounding-occluded-windows",
415
+ "--disable-renderer-backgrounding",
416
+ "--no-sandbox",
417
+ "--disable-dev-shm-usage",
418
+ "--enable-unsafe-swiftshader",
419
+ "--ignore-gpu-blocklist",
420
+ ];
421
+ // F10 clipboard + egress-matched geolocation permission, built once for
422
+ // either path. Typed as string[] (Playwright's grantPermissions /
423
+ // permissions option both accept it).
424
+ const grantedPermissions = [
425
+ ...(geo?.geolocation !== undefined ? ["geolocation"] : []),
426
+ "clipboard-read",
427
+ "clipboard-write",
428
+ ];
429
+ const selfLaunchBinary = selfLaunchEnabled()
430
+ ? (resolveChannelBinary(channel) ?? (channel === null ? launcher.executablePath() : null))
431
+ : null;
432
+ const useSelfLaunch = selfLaunchBinary !== null && existsSync(selfLaunchBinary) && canSelfLaunchWithProxy(proxy);
433
+ let context;
434
+ this.throwIfStartCancelled();
435
+ if (useSelfLaunch && selfLaunchBinary !== null) {
436
+ console.error(`[operator] self-launch + connectOverCDP (Turnstile-safe launch) binary=${selfLaunchBinary}`);
437
+ const window = { width: 1280, height: 1024 };
438
+ const selfEnv = {
439
+ ...browserEnv,
440
+ TZ: geo?.timezoneId ?? "America/New_York",
441
+ [OPERATOR_BROWSER_MARKER_ENV]: this.operatorBrowserMarker(),
442
+ };
443
+ const launch = () => {
444
+ this.throwIfStartCancelled();
445
+ return this.launchSelfManagedContext({
446
+ binary: selfLaunchBinary,
447
+ args: launchArgs,
448
+ proxy,
449
+ env: selfEnv,
450
+ window,
451
+ });
452
+ };
453
+ context = await launch();
454
+ try {
455
+ await context.grantPermissions(grantedPermissions);
456
+ if (geo?.geolocation !== undefined) {
457
+ await context.setGeolocation(geo.geolocation);
458
+ }
459
+ }
460
+ catch (err) {
461
+ console.error(`[operator] post-connect context setup partial: ${err instanceof Error ? err.message : String(err)}`);
462
+ }
463
+ }
464
+ else {
465
+ this.persistentFallbackLaunchInFlight = true;
466
+ this.startPersistentFallbackOwnershipMonitor();
467
+ const cleanupProfileHolder = async () => {
468
+ const proof = await this.waitForPersistentFallbackIdentity();
469
+ if (proof.state === "absent")
470
+ return "closed";
471
+ if (proof.state === "unknown")
472
+ return "unknown";
473
+ const { identity } = proof;
474
+ const treeProof = this.adoptOwnedChromeProcessTree(identity, false);
475
+ signalOwnedChromeProcessTree(identity, false, "SIGKILL", {
476
+ ...(treeProof === null ? {} : { proof: treeProof }),
477
+ });
478
+ return (await this.waitForOwnedProfileExit(identity, treeProof)) ? "closed" : "unknown";
479
+ };
480
+ const cleanupCancelled = async (lateContext) => {
481
+ const proof = await this.waitForPersistentFallbackIdentity().catch(() => ({ state: "unknown" }));
482
+ if (proof.state !== "owned") {
483
+ await lateContext.close().catch(() => undefined);
484
+ return proof.state === "absent" ? "closed" : "unknown";
485
+ }
486
+ const { identity } = proof;
487
+ const treeProof = this.adoptOwnedChromeProcessTree(identity, false);
488
+ const closeState = await closeProfileWithProof({
489
+ profileDir: this.profileDir,
490
+ identity,
491
+ close: () => lateContext.close(),
492
+ forceClose: () => {
493
+ signalOwnedChromeProcessTree(identity, false, "SIGKILL", {
494
+ ...(treeProof === null ? {} : { proof: treeProof }),
495
+ });
496
+ reapProfileHolderIfOwned(this.profileDir, identity);
497
+ },
498
+ ...(treeProof === null
499
+ ? {}
500
+ : { identityState: () => ownedChromeProcessTreeState(treeProof) }),
501
+ });
502
+ if (closeState === "closed")
503
+ return closeState;
504
+ return (await this.waitForOwnedProfileExit(identity, treeProof)) ? "closed" : "unknown";
505
+ };
506
+ const outcome = await (async () => {
507
+ try {
508
+ return await launchCancellablePersistentContext({
509
+ launch: (options) => launcher.launchPersistentContext(this.profileDir, options),
510
+ options: {
511
+ headless: OPERATOR_BROWSER_HEADLESS,
512
+ env: {
513
+ ...browserEnv,
514
+ [OPERATOR_BROWSER_MARKER_ENV]: this.operatorBrowserMarker(),
515
+ },
516
+ ...(channel !== null ? { channel } : {}),
517
+ ...persistentProxyOptions(proxy),
518
+ args: [...launchArgs],
519
+ viewport: null,
520
+ locale: "en-US",
521
+ timezoneId: geo?.timezoneId ?? "America/New_York",
522
+ permissions: grantedPermissions,
523
+ ...(geo?.geolocation !== undefined ? { geolocation: geo.geolocation } : {}),
524
+ },
525
+ cancellation: this.startCancellation,
526
+ cleanupCancelled,
527
+ cleanupRejected: cleanupProfileHolder,
528
+ });
529
+ }
530
+ catch (error) {
531
+ if (!this.startCancellationRequested) {
532
+ this.persistentFallbackLaunchInFlight = false;
533
+ throw error;
534
+ }
535
+ this.persistentFallbackCancellationState = await cleanupProfileHolder().catch(() => "unknown");
536
+ this.persistentFallbackLaunchInFlight = false;
537
+ throw new Error("BrowserController start cancelled");
538
+ }
539
+ })();
540
+ if (outcome.status === "cancelled") {
541
+ this.persistentFallbackCancellationState = outcome.closeState;
542
+ this.persistentFallbackLaunchInFlight = false;
543
+ throw new Error("BrowserController start cancelled");
544
+ }
545
+ context = outcome.value;
546
+ if (this.startCancellationRequested) {
547
+ this.persistentFallbackCancellationState = await cleanupCancelled(context).catch(() => "unknown");
548
+ this.persistentFallbackLaunchInFlight = false;
549
+ throw new Error("BrowserController start cancelled");
550
+ }
551
+ this.context = context;
552
+ this.launchedContext = true;
553
+ this.launchedProfileHolderIdentity = await this.requirePersistentFallbackOwnership(async () => {
554
+ markOwnerBrowserLaunchTerminal(this.operatorBrowserMarker());
555
+ await Promise.race([
556
+ context.close().catch(() => undefined),
557
+ new Promise((resolveWait) => {
558
+ const timer = setTimeout(resolveWait, PERSISTENT_CONTEXT_CANCELLATION_SETTLE_MS);
559
+ timer.unref();
560
+ }),
561
+ ]);
562
+ const markerClosed = await terminateOwnerBrowserLaunch(this.operatorBrowserMarker(), this.profileDir);
563
+ if (markerClosed) {
564
+ untrackOwnerBrowserLaunch(this.operatorBrowserMarker());
565
+ this.ownerLaunchTracked = false;
566
+ }
567
+ this.context = null;
568
+ this.launchedContext = false;
569
+ });
570
+ this.commitProfileLaunch();
571
+ this.persistentFallbackLaunchInFlight = false;
572
+ }
573
+ this.context = context;
574
+ // We own the profile now — close() may reap a leaked Chrome.
575
+ this.launchedContext = true;
576
+ if (!remoteMode) {
577
+ const holderPid = this.childChrome?.pid ?? currentProfileHolderPid(this.profileDir);
578
+ this.launchedProfileHolderIdentity =
579
+ this.childChromeIdentity ??
580
+ (holderPid === null ? null : profileProcessIdentity(holderPid, this.profileDir));
581
+ if (this.launchedProfileHolderIdentity !== null) {
582
+ this.adoptOwnedChromeProcessTree(this.launchedProfileHolderIdentity, this.childChromeIdentity !== null && this.childChromeProcessGroup);
583
+ }
584
+ }
585
+ if (this.startCancellationRequested) {
586
+ await this.closeBrowser();
587
+ throw new Error("BrowserController start cancelled");
588
+ }
589
+ await this.initializePages(context, hardened, remoteMode);
590
+ }
591
+ // Probe the run's actual egress geo by loading ipinfo.io. Launches a
592
+ // throwaway browser: the persistent context isn't up yet, and its
593
+ // timezone has to be known before it is. The throwaway inherits the
594
+ // same channel + proxy so it reports the real egress. Best-effort —
595
+ // any failure returns null and start() keeps a default timezone.
596
+ async probeEgressGeo(channel, proxy, browserEnv) {
597
+ if (proxy === null) {
598
+ try {
599
+ const resp = await fetch("https://ipinfo.io/json", { signal: AbortSignal.timeout(10_000) });
600
+ if (!resp.ok)
601
+ throw new Error(`HTTP ${resp.status}`);
602
+ return parseEgressGeo(await resp.text());
603
+ }
604
+ catch (err) {
605
+ console.error(`[operator] egress geo probe failed — using default ` +
606
+ `timezone: ${err instanceof Error ? err.message : String(err)}`);
607
+ return null;
608
+ }
609
+ }
610
+ let probe;
611
+ try {
612
+ probe = await getChromium().launch({
613
+ headless: OPERATOR_BROWSER_HEADLESS,
614
+ env: browserEnv,
615
+ ...(channel !== null ? { channel } : {}),
616
+ ...(proxy !== null ? { proxy } : {}),
617
+ args: ["--no-sandbox", "--disable-dev-shm-usage"],
618
+ });
619
+ const page = await probe.newPage();
620
+ await page.goto("https://ipinfo.io/json", {
621
+ timeout: 10000,
622
+ waitUntil: "domcontentloaded",
623
+ });
624
+ const body = await page.evaluate(() => document.body.innerText);
625
+ return parseEgressGeo(body);
626
+ }
627
+ catch (err) {
628
+ console.error(`[operator] egress geo probe failed — using default ` +
629
+ `timezone: ${err instanceof Error ? err.message : String(err)}`);
630
+ return null;
631
+ }
632
+ finally {
633
+ if (probe !== undefined)
634
+ await probe.close();
635
+ }
636
+ }
637
+ // Resolve the deliberate per-session egress selection. A session proxy is
638
+ // not an optimization hint: falling back to the host's IP could submit a
639
+ // geo-gated flow from the wrong country, so malformed or unreachable values
640
+ // abort startup rather than silently egressing directly.
641
+ async resolveProxy() {
642
+ if (this.proxyOverride === null)
643
+ return null;
644
+ return resolveExplicitProxy(this.proxyOverride);
645
+ }
646
+ async close(options = {}) {
647
+ if (options.cancelStart === true) {
648
+ this.startCancellationRequested = true;
649
+ this.resolveStartCancellation?.();
650
+ this.resolveStartCancellation = null;
651
+ }
652
+ this.closePromise ??= this.closeAfterStart();
653
+ return await this.closePromise;
654
+ }
655
+ async waitForCancelledStartQuiescence() {
656
+ if (!this.startCancellationRequested)
657
+ return;
658
+ await Promise.allSettled([
659
+ this.startPromise ?? Promise.resolve(),
660
+ this.reapCancelledStartProcess(),
661
+ ]);
662
+ await this.persistentFallbackOwnershipMonitor?.catch(() => undefined);
663
+ }
664
+ async forceCloseOwnedProcessTree() {
665
+ this.startCancellationRequested = true;
666
+ this.resolveStartCancellation?.();
667
+ this.resolveStartCancellation = null;
668
+ const marker = this.operatorBrowserMarker();
669
+ if (this.ownerLaunchTracked)
670
+ markOwnerBrowserLaunchTerminal(marker);
671
+ const proof = this.ownedChromeProcessTreeProof;
672
+ const identity = proof?.identity ?? this.currentOwnedProfileIdentity();
673
+ if (identity !== null) {
674
+ signalOwnedChromeProcessTree(identity, proof?.processGroup ?? false, "SIGKILL", {
675
+ ...(proof === null ? {} : { proof }),
676
+ });
677
+ reapProfileHolderIfOwned(this.profileDir, identity);
678
+ }
679
+ const closed = identity === null
680
+ ? this.startSettled && !this.launchedContext
681
+ : await this.waitForOwnedProfileExit(identity, proof);
682
+ if (closed && proof !== null) {
683
+ releaseOwnedChromeProcessTree(proof);
684
+ const tracked = selfManagedChromes.get(proof.identity.pid);
685
+ if (tracked?.proof === proof)
686
+ selfManagedChromes.delete(proof.identity.pid);
687
+ if (this.ownedChromeProcessTreeProof === proof)
688
+ this.ownedChromeProcessTreeProof = null;
689
+ }
690
+ const markerClosed = !this.ownerLaunchTracked || (await terminateOwnerBrowserLaunch(marker, this.profileDir));
691
+ if (closed && markerClosed && this.ownerLaunchTracked) {
692
+ untrackOwnerBrowserLaunch(marker);
693
+ this.ownerLaunchTracked = false;
694
+ }
695
+ await this.teardownOwnedDisplay().catch(() => undefined);
696
+ return closed && markerClosed ? "closed" : "unknown";
697
+ }
698
+ async closeCancelledStart() {
699
+ void this.reapCancelledStartProcess().catch(() => undefined);
700
+ if (this.persistentFallbackCancellationState !== null) {
701
+ return this.persistentFallbackCancellationState;
702
+ }
703
+ if (!this.startLaunchCommitted) {
704
+ const closeState = await this.closeBrowser();
705
+ return this.startSettled ? closeState : "unknown";
706
+ }
707
+ return await this.closeBrowser();
708
+ }
709
+ async reapCancelledStartProcess() {
710
+ this.cancelledStartReaper ??= this.monitorCancelledStartProcess();
711
+ await this.cancelledStartReaper;
712
+ }
713
+ async monitorCancelledStartProcess() {
714
+ while (!this.startSettled) {
715
+ const identity = this.currentOwnedProfileIdentity();
716
+ if (identity !== null) {
717
+ this.signalCurrentSelfManagedChrome(identity, "SIGKILL");
718
+ reapProfileHolderIfOwned(this.profileDir, identity);
719
+ }
720
+ await new Promise((resolveWait) => {
721
+ const timer = setTimeout(resolveWait, 25);
722
+ timer.unref();
723
+ });
724
+ }
725
+ }
726
+ currentOwnedProfileIdentity() {
727
+ const known = this.ownedChromeProcessTreeProof?.identity ??
728
+ this.childChromeIdentity ??
729
+ this.launchedProfileHolderIdentity;
730
+ if (known !== null)
731
+ return known;
732
+ const holderPid = currentProfileHolderPid(this.profileDir);
733
+ if (holderPid === null)
734
+ return null;
735
+ const identity = profileProcessIdentity(holderPid, this.profileDir);
736
+ if (identity === null)
737
+ return null;
738
+ if (!this.startCancellationRequested)
739
+ return identity;
740
+ return operatorBrowserProcessMatchesMarker(identity.pid, this.operatorBrowserMarker())
741
+ ? identity
742
+ : null;
743
+ }
744
+ async waitForPersistentFallbackIdentity() {
745
+ if (this.ownedChromeProcessTreeProof !== null) {
746
+ return { state: "owned", identity: this.ownedChromeProcessTreeProof.identity };
747
+ }
748
+ const proof = await resolvePersistentFallbackIdentity({ profileDir: this.profileDir });
749
+ if (proof.state === "owned" &&
750
+ this.startCancellationRequested &&
751
+ !operatorBrowserProcessMatchesMarker(proof.identity.pid, this.operatorBrowserMarker())) {
752
+ return { state: "unknown" };
753
+ }
754
+ if (proof.state === "owned")
755
+ this.adoptOwnedChromeProcessTree(proof.identity, false);
756
+ return proof;
757
+ }
758
+ async requirePersistentFallbackOwnership(cleanupUnproven) {
759
+ try {
760
+ const proof = await this.waitForPersistentFallbackIdentity();
761
+ if (proof.state !== "owned" || this.ownedChromeProcessTreeProof === null) {
762
+ throw new Error("persistent browser launch identity could not be bound to owner custody");
763
+ }
764
+ return proof.identity;
765
+ }
766
+ catch (error) {
767
+ await cleanupUnproven().catch(() => undefined);
768
+ this.persistentFallbackLaunchInFlight = false;
769
+ throw error;
770
+ }
771
+ }
772
+ startPersistentFallbackOwnershipMonitor() {
773
+ if (this.persistentFallbackOwnershipMonitor !== null)
774
+ return;
775
+ this.persistentFallbackOwnershipMonitor = (async () => {
776
+ while (this.persistentFallbackLaunchInFlight && this.ownedChromeProcessTreeProof === null) {
777
+ const holderPid = currentProfileHolderPid(this.profileDir);
778
+ const identity = holderPid === null ? null : profileProcessIdentity(holderPid, this.profileDir);
779
+ const controllerOwnsIdentity = identity !== null &&
780
+ (!this.startCancellationRequested ||
781
+ operatorBrowserProcessMatchesMarker(identity.pid, this.operatorBrowserMarker()));
782
+ if (identity !== null && controllerOwnsIdentity) {
783
+ this.launchedProfileHolderIdentity = identity;
784
+ try {
785
+ this.adoptOwnedChromeProcessTree(identity, false);
786
+ }
787
+ catch { }
788
+ return;
789
+ }
790
+ await new Promise((resolveWait) => {
791
+ const timer = setTimeout(resolveWait, PROFILE_IDENTITY_POLL_MS);
792
+ timer.unref();
793
+ });
794
+ }
795
+ })();
796
+ }
797
+ async waitForOwnedProfileExit(identity, existingProof) {
798
+ const deadline = Date.now() + PROFILE_IDENTITY_PROOF_TIMEOUT_MS;
799
+ const proof = existingProof ?? captureOwnedChromeProcessTreeProof(identity, false);
800
+ let state = proof === null
801
+ ? profileProcessIdentityState(identity, this.profileDir)
802
+ : ownedChromeProcessTreeState(proof);
803
+ while (state !== "stale" && Date.now() < deadline) {
804
+ if (proof !== null) {
805
+ signalOwnedChromeProcessTree(identity, false, "SIGKILL", { proof });
806
+ }
807
+ else if (profileProcessMatches(identity, this.profileDir)) {
808
+ signalOwnedChromeProcessTree(identity, false, "SIGKILL");
809
+ }
810
+ await new Promise((resolveWait) => {
811
+ const timer = setTimeout(resolveWait, PROFILE_IDENTITY_POLL_MS);
812
+ timer.unref();
813
+ });
814
+ state =
815
+ proof === null
816
+ ? profileProcessIdentityState(identity, this.profileDir)
817
+ : ownedChromeProcessTreeState(proof);
818
+ }
819
+ if (state !== "stale")
820
+ return false;
821
+ reapProfileHolderIfOwned(this.profileDir, identity);
822
+ return true;
823
+ }
824
+ async closeAfterStart() {
825
+ if (this.startPromise !== null && !this.startSettled) {
826
+ await Promise.race([this.startPromise.catch(() => undefined), this.startCancellation]);
827
+ }
828
+ if (this.startCancellationRequested)
829
+ return await this.closeCancelledStart();
830
+ return await this.closeBrowser();
831
+ }
832
+ async closeBrowser() {
833
+ this.pages.disposeRegistrations();
834
+ if (this.pages.harnessAttachedPage) {
835
+ this.pages.page = null;
836
+ this.pages.primaryPage = null;
837
+ this.pages.oauthProductPage = null;
838
+ this.pages.oauthProviderPage = null;
839
+ this.pages.oauthProviderPageClosed = false;
840
+ this.context = null;
841
+ return "closed";
842
+ }
843
+ const marker = this.operatorBrowserMarker();
844
+ if (this.ownerLaunchTracked)
845
+ markOwnerBrowserLaunchTerminal(marker);
846
+ // Each step is best-effort and independent: a throw closing the page
847
+ // or context must NOT skip the browser reap below, or an un-closed Chrome
848
+ // keeps the profile's
849
+ // SingletonLock held — bricking the next signup + `mcp connect`).
850
+ //
851
+ // EVERY close call is timeout-capped. On a wedged headed Chrome (e.g. a
852
+ // run that crashed mid-captcha-click), BOTH page.close() AND
853
+ // context.close() can hang INDEFINITELY — and an un-capped page.close()
854
+ // blocked the reap below from ever running, so the browser leaked for
855
+ // minutes and bricked the next 3 services (MEASURED 2026-06-09: supabase
856
+ // crash → cockroachdb/weaviate/honeycomb all "profile held"). The cap
857
+ // guarantees we always reach the SIGKILL reap.
858
+ const page = this.pages.page;
859
+ const context = this.context;
860
+ const cdpBrowser = this.cdpBrowser;
861
+ const childIdentity = this.childChromeIdentity;
862
+ const childChromeProcessGroup = this.childChromeProcessGroup;
863
+ const holderIdentity = this.launchedProfileHolderIdentity ?? this.currentOwnedProfileIdentity();
864
+ const identity = this.ownedChromeProcessTreeProof?.identity ?? childIdentity ?? holderIdentity;
865
+ const treeProof = identity === null
866
+ ? null
867
+ : (this.ownedChromeProcessTreeProof ??
868
+ this.adoptOwnedChromeProcessTree(identity, childIdentity !== null ? childChromeProcessGroup : false));
869
+ this.pages.page = null;
870
+ this.pages.primaryPage = null;
871
+ this.pages.oauthProductPage = null;
872
+ this.pages.oauthProviderPage = null;
873
+ this.pages.oauthProviderPageClosed = false;
874
+ this.context = null;
875
+ this.cdpBrowser = null;
876
+ this.childChrome = null;
877
+ this.childChromeIdentity = null;
878
+ this.childChromeProcessGroup = false;
879
+ this.launchedContext = false;
880
+ this.launchedProfileHolderIdentity = null;
881
+ const closeState = await closeProfileWithProof({
882
+ profileDir: this.profileDir,
883
+ identity,
884
+ close: async () => {
885
+ if (identity !== null) {
886
+ signalOwnedChromeProcessTree(identity, treeProof?.processGroup ?? (childIdentity !== null ? childChromeProcessGroup : false), "SIGTERM", { ...(treeProof === null ? {} : { proof: treeProof }) });
887
+ }
888
+ // A process-tree SIGTERM can close the CDP target before Playwright
889
+ // observes it. That is successful teardown, not a reason to skip the
890
+ // proof/reap path or retain a cleanly closed ephemeral profile.
891
+ if (page !== null)
892
+ await page.close().catch(() => undefined);
893
+ if (context !== null)
894
+ await context.close().catch(() => undefined);
895
+ if (cdpBrowser !== null)
896
+ await cdpBrowser.close().catch(() => undefined);
897
+ },
898
+ forceClose: () => {
899
+ if (identity !== null) {
900
+ signalOwnedChromeProcessTree(identity, treeProof?.processGroup ?? (childIdentity !== null ? childChromeProcessGroup : false), "SIGKILL", { ...(treeProof === null ? {} : { proof: treeProof }) });
901
+ }
902
+ reapProfileHolderIfOwned(this.profileDir, identity);
903
+ },
904
+ ...(treeProof === null
905
+ ? {}
906
+ : { identityState: () => ownedChromeProcessTreeState(treeProof) }),
907
+ });
908
+ // Self-launch path: disconnect the CDP browser and SIGKILL the Chrome we
909
+ // spawned. context.close() on a connectOverCDP context only disconnects —
910
+ // it does NOT necessarily exit the browser process, which would leak the
911
+ // SingletonLock and brick the next run (the reap below is the backstop, but
912
+ // killing our own child directly is cleaner and faster).
913
+ if (treeProof !== null && ownedChromeProcessTreeState(treeProof) === "stale") {
914
+ releaseOwnedChromeProcessTree(treeProof);
915
+ const tracked = selfManagedChromes.get(treeProof.identity.pid);
916
+ if (tracked?.proof === treeProof)
917
+ selfManagedChromes.delete(treeProof.identity.pid);
918
+ if (this.ownedChromeProcessTreeProof === treeProof) {
919
+ this.ownedChromeProcessTreeProof = null;
920
+ }
921
+ }
922
+ const markerClosed = !this.ownerLaunchTracked || (await terminateOwnerBrowserLaunch(marker, this.profileDir));
923
+ if (markerClosed && this.ownerLaunchTracked) {
924
+ untrackOwnerBrowserLaunch(marker);
925
+ this.ownerLaunchTracked = false;
926
+ }
927
+ await this.teardownOwnedDisplay().catch(() => undefined);
928
+ return closeState === "closed" && !markerClosed ? "force_closed_unproven" : closeState;
929
+ }
930
+ }
931
+ //# sourceMappingURL=browser-process-owner.js.map