@trusty-squire/mcp 1.0.47 → 1.0.48-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.
- package/dist/bot/browser.d.ts +33 -0
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +180 -0
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/google-login.d.ts +5 -1
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +243 -103
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/operator-recipe.d.ts +24 -24
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +28 -23
- package/dist/install/cli.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +12 -12
- package/dist/tools/store-credential.d.ts +4 -4
- package/dist/tools/use-credential.d.ts +12 -12
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ export interface LoginResult {
|
|
|
5
5
|
detail?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function contextHasProviderSession(context: BrowserContext, provider: OAuthProviderId): Promise<boolean>;
|
|
8
|
+
export declare function profileHasProviderCookies(profileDir: string, provider: OAuthProviderId): boolean;
|
|
8
9
|
export declare function detectActiveProviderSessions(profileDir?: string): Promise<OAuthProviderId[]>;
|
|
9
10
|
export type GoogleAuthState = "consent" | "needs_login" | "challenge" | "not_google";
|
|
10
11
|
export declare function classifyGoogleAuthState(url: string, bodyText: string): GoogleAuthState;
|
|
@@ -26,6 +27,9 @@ export interface RunInBotChromeOpts {
|
|
|
26
27
|
onSuccess?: (context: BrowserContext) => Promise<void>;
|
|
27
28
|
apiBaseUrl?: string;
|
|
28
29
|
heartbeatMessage?: string | (() => string);
|
|
30
|
+
plainProfileLogin?: boolean;
|
|
31
|
+
plainPollUntilDone?: (profileDir: string) => Promise<boolean>;
|
|
32
|
+
plainOnSuccess?: (profileDir: string) => Promise<void>;
|
|
29
33
|
}
|
|
30
34
|
export declare function runInBotChrome(opts: RunInBotChromeOpts): Promise<{
|
|
31
35
|
status: "completed" | "preflight_satisfied" | "timeout";
|
|
@@ -41,7 +45,7 @@ export declare function ensureOAuthSession(opts?: {
|
|
|
41
45
|
export declare function extractGoogleAccountEmail(pageText: string): string | null;
|
|
42
46
|
export declare function openInstallConfirmInBotChrome(opts: {
|
|
43
47
|
confirmUrl: string;
|
|
44
|
-
pollUntilClaimed: (
|
|
48
|
+
pollUntilClaimed: (profileDir: string) => Promise<boolean>;
|
|
45
49
|
profileDir?: string;
|
|
46
50
|
timeoutMinutes?: number;
|
|
47
51
|
apiBaseUrl?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-login.d.ts","sourceRoot":"","sources":["../../src/bot/google-login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"google-login.d.ts","sourceRoot":"","sources":["../../src/bot/google-login.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsG5D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,eAAe,GACxB,OAAO,CAAC,OAAO,CAAC,CAElB;AAiBD,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,eAAe,GACxB,OAAO,CAmBT;AA0CD,wBAAsB,4BAA4B,CAChD,UAAU,GAAE,MAA2B,GACtC,OAAO,CAAC,eAAe,EAAE,CAAC,CAqC5B;AAUD,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,aAAa,GACb,WAAW,GACX,YAAY,CAAC;AAEjB,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,eAAe,CAwCtF;AA+BD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CA2BlE;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAEjE;AAaD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAiB/D;AAUD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBpE;AAGD,wBAAgB,UAAU,IAAI,OAAO,CAsBpC;AAWD,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAe9C;AAmBD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGjD;AAqBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAoB9D;AA2JD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IAGjB,aAAa,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAG7D,WAAW,EAAE,MAAM,CAAC;IAIpB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAO1D,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAKvD,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;IAS3C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAG5B,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAG9D,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC;IAAE,MAAM,EAAE,WAAW,GAAG,qBAAqB,GAAG,SAAS,CAAA;CAAE,CAAC,CAsBtE;AA2ZD,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC7B,gBAAgB,GAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CACsD,GAC7F,OAAO,CAAC,OAAO,CAAC,CAelB;AAQD,wBAAsB,kBAAkB,CAAC,IAAI,CAAC,EAAE;IAC9C,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IAQpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,WAAW,CAAC,CA8DvB;AAYD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQzE;AASD,wBAAsB,6BAA6B,CAAC,IAAI,EAAE;IACxD,UAAU,EAAE,MAAM,CAAC;IAOnB,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,CAAC;CAC5C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiDxE"}
|
package/dist/bot/google-login.js
CHANGED
|
@@ -32,7 +32,8 @@ import boxen from "boxen";
|
|
|
32
32
|
import chalk from "chalk";
|
|
33
33
|
import { shortenVncUrl } from "../api-client.js";
|
|
34
34
|
import { CHROME_PROFILE_DIR, launchWithProfileGate, ProfileBusyError, waitForProfileFree } from "./profile.js";
|
|
35
|
-
import {
|
|
35
|
+
import { launchPlainLoginBrowser, launchSelfManagedLoginContext, resolveChannelBinary, selfLaunchEnabled, } from "./browser.js";
|
|
36
|
+
import { markProviderLoggedIn } from "./login-state.js";
|
|
36
37
|
import { randomBytes } from "node:crypto";
|
|
37
38
|
const require = createRequire(import.meta.url);
|
|
38
39
|
// Route the INTERACTIVE login through the same proxy the bot uses, so the
|
|
@@ -123,6 +124,43 @@ async function hasProviderSession(context, target) {
|
|
|
123
124
|
export async function contextHasProviderSession(context, provider) {
|
|
124
125
|
return hasProviderSession(context, LOGIN_TARGETS[provider]);
|
|
125
126
|
}
|
|
127
|
+
// Provider session cookie NAMES long/specific enough to detect by substring in
|
|
128
|
+
// the raw on-disk Cookies DB. Bare short names (Google's "SID") are omitted —
|
|
129
|
+
// they substring-collide with other cookie names.
|
|
130
|
+
const PROVIDER_COOKIE_MARKERS = {
|
|
131
|
+
google: ["__Secure-1PSID", "SAPISID"],
|
|
132
|
+
github: ["user_session"],
|
|
133
|
+
};
|
|
134
|
+
// Read a PLAIN login browser's provider session straight off disk. The connect
|
|
135
|
+
// claim's login browser never attaches CDP (see launchPlainLoginBrowser — a
|
|
136
|
+
// CDP attach fails Google's OAuth "secure browser" check), so there is no live
|
|
137
|
+
// context to query. Chrome stores cookie NAMES as plaintext in the Cookies
|
|
138
|
+
// SQLite b-tree pages, so a raw byte search reliably answers "did this
|
|
139
|
+
// session's cookie get written" — dependency-free and Node-version-agnostic
|
|
140
|
+
// (engines >=20; node:sqlite is 22.5+). Presence-only; never decrypts values.
|
|
141
|
+
export function profileHasProviderCookies(profileDir, provider) {
|
|
142
|
+
const markers = PROVIDER_COOKIE_MARKERS[provider];
|
|
143
|
+
// <profile>/Default/Cookies is the norm; a profile that never opened a window
|
|
144
|
+
// may only have <profile>/Cookies. Include the WAL sidecar — a just-written
|
|
145
|
+
// cookie may not be checkpointed into the main file yet.
|
|
146
|
+
const bases = [join(profileDir, "Default", "Cookies"), join(profileDir, "Cookies")];
|
|
147
|
+
for (const base of bases) {
|
|
148
|
+
for (const path of [base, `${base}-wal`]) {
|
|
149
|
+
if (!existsSync(path))
|
|
150
|
+
continue;
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
text = readFileSync(path).toString("latin1");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (markers.some((m) => text.includes(m)))
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
126
164
|
// VALIDATE a session instead of just spotting a cookie. A provider session that
|
|
127
165
|
// expired server-side often leaves its `user_session` cookie sitting in the
|
|
128
166
|
// profile, so name-presence false-positives ("the GitHub marker lies"). Here we
|
|
@@ -625,6 +663,45 @@ export async function runInBotChrome(opts) {
|
|
|
625
663
|
return await runHeadlessChrome(opts);
|
|
626
664
|
}
|
|
627
665
|
async function runDisplayedChrome(opts) {
|
|
666
|
+
// PLAIN-BROWSER path (connect claim): launch plain Chrome, never attach CDP,
|
|
667
|
+
// detect completion off the API + on-disk cookie store. See
|
|
668
|
+
// launchPlainLoginBrowser / RunInBotChromeOpts.plainProfileLogin.
|
|
669
|
+
if (opts.plainProfileLogin === true) {
|
|
670
|
+
if (opts.plainPollUntilDone === undefined) {
|
|
671
|
+
throw new Error("plainProfileLogin set without plainPollUntilDone");
|
|
672
|
+
}
|
|
673
|
+
const binary = resolveChannelBinary("chrome");
|
|
674
|
+
if (binary === null) {
|
|
675
|
+
throw new Error("no Chrome binary found for the plain login browser");
|
|
676
|
+
}
|
|
677
|
+
const proxyOpt = loginProxyOption();
|
|
678
|
+
const browser = await launchPlainLoginBrowser({
|
|
679
|
+
binary,
|
|
680
|
+
profileDir: opts.profileDir,
|
|
681
|
+
url: opts.url,
|
|
682
|
+
window: { width: 1280, height: 800 },
|
|
683
|
+
env: process.env,
|
|
684
|
+
// Self-launch/--proxy-server can't carry proxy auth — drop a credentialed
|
|
685
|
+
// proxy (direct). Connect from the box is the point anyway.
|
|
686
|
+
proxyServer: proxyOpt !== undefined && proxyOpt.password === undefined ? proxyOpt.server : null,
|
|
687
|
+
extraArgs: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
688
|
+
});
|
|
689
|
+
try {
|
|
690
|
+
console.error(`\n[login] A Chrome window has opened. ${opts.bannerLabel}\n`);
|
|
691
|
+
console.error(`[connect-debug] launcher=plain_display chrome=${binary}`);
|
|
692
|
+
const ok = await pollUntil(opts.deadline, () => opts.plainPollUntilDone(opts.profileDir), opts.heartbeatMessage);
|
|
693
|
+
if (ok && opts.plainOnSuccess !== undefined) {
|
|
694
|
+
try {
|
|
695
|
+
await opts.plainOnSuccess(opts.profileDir);
|
|
696
|
+
}
|
|
697
|
+
catch { /* swallow */ }
|
|
698
|
+
}
|
|
699
|
+
return { status: ok ? "completed" : "timeout" };
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
await browser.teardown();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
628
705
|
const chromium = resolveChromium();
|
|
629
706
|
const context = await launchWithProfileGate(opts.profileDir, () => chromium.launchPersistentContext(opts.profileDir, {
|
|
630
707
|
channel: "chrome",
|
|
@@ -696,6 +773,11 @@ async function runHeadlessChrome(opts) {
|
|
|
696
773
|
// Playwright handle, closed via context.close(). Tracked here so the
|
|
697
774
|
// signal handler can release the profile lock before exiting.
|
|
698
775
|
let activeContext;
|
|
776
|
+
// Teardown for the login browser — for the self-launch path this ALSO kills
|
|
777
|
+
// the spawned Chrome child (a bare context.close() over CDP leaves it
|
|
778
|
+
// running), for the persistent fallback it's just context.close(). Tracked
|
|
779
|
+
// so the signal handler can release the profile lock before exiting.
|
|
780
|
+
let activeTeardown;
|
|
699
781
|
// Ensure nothing is orphaned if the process dies mid-flow. `exit`
|
|
700
782
|
// covers a normal return; SIGTERM/SIGINT cover an interrupted run —
|
|
701
783
|
// once a signal listener is registered the default terminate is
|
|
@@ -706,13 +788,13 @@ async function runHeadlessChrome(opts) {
|
|
|
706
788
|
teardown(rig);
|
|
707
789
|
process.exit(130);
|
|
708
790
|
};
|
|
709
|
-
if (
|
|
710
|
-
//
|
|
711
|
-
// cap the wait: a wedged Chrome
|
|
712
|
-
// indefinitely, and the rig MUST still be torn
|
|
713
|
-
// wins (clean
|
|
791
|
+
if (activeTeardown !== undefined) {
|
|
792
|
+
// Tear down the browser to release the persistent-profile lock (and
|
|
793
|
+
// kill the self-launched Chrome) — but cap the wait: a wedged Chrome
|
|
794
|
+
// under Xvfb can hang indefinitely, and the rig MUST still be torn
|
|
795
|
+
// down. Whichever wins (clean teardown, or the 3s cap), `finish` runs.
|
|
714
796
|
const capped = new Promise((r) => setTimeout(r, 3000));
|
|
715
|
-
Promise.race([
|
|
797
|
+
Promise.race([activeTeardown().catch(() => undefined), capped]).then(finish, finish);
|
|
716
798
|
}
|
|
717
799
|
else {
|
|
718
800
|
finish();
|
|
@@ -726,41 +808,115 @@ async function runHeadlessChrome(opts) {
|
|
|
726
808
|
rig.procs.push(spawnBg("Xvfb", [display, "-screen", "0", `${HEADLESS_W}x${HEADLESS_H}x24`, "-ac"]));
|
|
727
809
|
await new Promise((r) => setTimeout(r, 1500));
|
|
728
810
|
// 2. Chrome on that display, persistent profile, window filling the display.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
811
|
+
// Self-launch + connectOverCDP is the STATE.md 2026-06-12 launcher fix:
|
|
812
|
+
// a launchPersistentContext-driven Chrome carries the launch-time
|
|
813
|
+
// instrumentation anti-bot (Cloudflare Turnstile, and — the bug this
|
|
814
|
+
// ports the fix for — Google's sign-in device-prompt) reads as a bot,
|
|
815
|
+
// while a self-launched Chrome attached over CDP does not. The signup
|
|
816
|
+
// path (BrowserController) already migrated; the connect login was the
|
|
817
|
+
// last consumer of the detectable launcher. Fall back to the old
|
|
818
|
+
// launcher when self-launch is opted out (BOT_SELF_LAUNCH=0), Chrome
|
|
819
|
+
// isn't resolvable as a real binary, or a credentialed proxy is set
|
|
820
|
+
// (self-launch's --proxy-server can't carry proxy auth).
|
|
821
|
+
const proxyOpt = loginProxyOption();
|
|
822
|
+
const chromeBinary = resolveChannelBinary("chrome");
|
|
823
|
+
const useSelfLaunch = selfLaunchEnabled() &&
|
|
824
|
+
chromeBinary !== null &&
|
|
825
|
+
(proxyOpt === undefined || proxyOpt.password === undefined);
|
|
826
|
+
const sharedChromeArgs = [
|
|
827
|
+
"--disable-blink-features=AutomationControlled",
|
|
828
|
+
// --test-type suppresses the "unsupported command-line flag:
|
|
829
|
+
// --no-sandbox" yellow infobar that otherwise eats the top strip of the
|
|
830
|
+
// phone-shaped framebuffer once handed to noVNC. Standard automation
|
|
831
|
+
// flag; doesn't affect OAuth/cookies.
|
|
832
|
+
"--test-type",
|
|
833
|
+
"--no-sandbox",
|
|
834
|
+
"--disable-dev-shm-usage",
|
|
835
|
+
];
|
|
836
|
+
// PLAIN-BROWSER mode (connect claim): NO CDP at all — a CDP-attached Chrome
|
|
837
|
+
// fails Google's OAuth "secure browser" check (STATE.md 2026-07-20, fully
|
|
838
|
+
// bisected). `context` stays undefined; completion is read via
|
|
839
|
+
// plainPollUntilDone (API + on-disk cookies), never a live context.
|
|
840
|
+
const plain = opts.plainProfileLogin === true;
|
|
841
|
+
let context;
|
|
842
|
+
let teardownContext;
|
|
843
|
+
let launcherMode;
|
|
844
|
+
if (plain) {
|
|
845
|
+
if (opts.plainPollUntilDone === undefined) {
|
|
846
|
+
throw new Error("plainProfileLogin set without plainPollUntilDone");
|
|
847
|
+
}
|
|
848
|
+
if (chromeBinary === null) {
|
|
849
|
+
throw new Error("no Chrome binary found for the plain login browser");
|
|
850
|
+
}
|
|
851
|
+
launcherMode = "plain";
|
|
852
|
+
const browser = await launchPlainLoginBrowser({
|
|
853
|
+
binary: chromeBinary,
|
|
854
|
+
profileDir: opts.profileDir,
|
|
855
|
+
url: opts.url,
|
|
856
|
+
window: { width: HEADLESS_W, height: HEADLESS_H },
|
|
857
|
+
env: { ...process.env, DISPLAY: display },
|
|
858
|
+
proxyServer: proxyOpt !== undefined && proxyOpt.password === undefined ? proxyOpt.server : null,
|
|
859
|
+
extraArgs: sharedChromeArgs,
|
|
860
|
+
});
|
|
861
|
+
teardownContext = browser.teardown;
|
|
862
|
+
}
|
|
863
|
+
else if (useSelfLaunch && chromeBinary !== null) {
|
|
864
|
+
launcherMode = "self_launch";
|
|
865
|
+
const launched = await launchSelfManagedLoginContext({
|
|
866
|
+
binary: chromeBinary,
|
|
867
|
+
profileDir: opts.profileDir,
|
|
868
|
+
// App mode (--app) strips tabs/URL bar so the install page gets the
|
|
869
|
+
// full phone-shaped framebuffer under noVNC.
|
|
870
|
+
initialUrl: opts.url,
|
|
871
|
+
appMode: true,
|
|
872
|
+
window: { width: HEADLESS_W, height: HEADLESS_H },
|
|
873
|
+
env: { ...process.env, DISPLAY: display },
|
|
874
|
+
proxyServer: proxyOpt?.server ?? null,
|
|
875
|
+
extraArgs: sharedChromeArgs,
|
|
876
|
+
});
|
|
877
|
+
context = launched.context;
|
|
878
|
+
teardownContext = launched.teardown;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
launcherMode = "persistent";
|
|
882
|
+
const chromium = resolveChromium();
|
|
883
|
+
const persistent = await launchWithProfileGate(opts.profileDir, () => chromium.launchPersistentContext(opts.profileDir, {
|
|
884
|
+
channel: "chrome",
|
|
885
|
+
headless: false,
|
|
886
|
+
viewport: null, // use the real window size
|
|
887
|
+
env: { ...process.env, DISPLAY: display },
|
|
888
|
+
// Drop --enable-automation: kills the "controlled by automated test
|
|
889
|
+
// software" infobar and the matching automation fingerprint.
|
|
890
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
891
|
+
...(proxyOpt !== undefined ? { proxy: proxyOpt } : {}),
|
|
892
|
+
args: [
|
|
893
|
+
`--window-position=0,0`,
|
|
894
|
+
`--window-size=${HEADLESS_W},${HEADLESS_H}`,
|
|
895
|
+
`--app=${opts.url}`,
|
|
896
|
+
...sharedChromeArgs,
|
|
897
|
+
],
|
|
898
|
+
}));
|
|
899
|
+
context = persistent;
|
|
900
|
+
teardownContext = async () => {
|
|
901
|
+
await persistent.close().catch(() => undefined);
|
|
902
|
+
};
|
|
903
|
+
}
|
|
757
904
|
activeContext = context;
|
|
905
|
+
activeTeardown = teardownContext;
|
|
906
|
+
// Trace which launcher ran — survives the noVNC collapsing (stderr). The
|
|
907
|
+
// self-launch port is the fix under test for the Google device-prompt
|
|
908
|
+
// rejection; this line is how we confirm which path a real run took.
|
|
909
|
+
console.error(`[connect-debug] launcher=${launcherMode} chrome=${chromeBinary ?? "<none>"} display=${display}`);
|
|
758
910
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
911
|
+
// CDP path only: preflight + drive the first page to the URL. The plain
|
|
912
|
+
// path has no context — plain Chrome's --app already opened opts.url.
|
|
913
|
+
if (context !== undefined) {
|
|
914
|
+
if (opts.preflight !== undefined && await opts.preflight(context)) {
|
|
915
|
+
return { status: "preflight_satisfied" };
|
|
916
|
+
}
|
|
917
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
918
|
+
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
761
919
|
}
|
|
762
|
-
const page = context.pages()[0] ?? (await context.newPage());
|
|
763
|
-
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
764
920
|
// 3. x11vnc on the display — localhost-only, password-gated, -noshm
|
|
765
921
|
// (the box's X server lacks the shared-memory extension).
|
|
766
922
|
// G14 — pass the password via a 0600 file, NOT `-passwd <plain>`
|
|
@@ -840,20 +996,36 @@ async function runHeadlessChrome(opts) {
|
|
|
840
996
|
// set — opening a second context after teardown is racy and
|
|
841
997
|
// silently fails on profile lock), then tear the whole stack
|
|
842
998
|
// down.
|
|
843
|
-
const ok = await pollUntil(opts.deadline, () =>
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
999
|
+
const ok = await pollUntil(opts.deadline, () => context !== undefined
|
|
1000
|
+
? opts.pollUntilDone(context)
|
|
1001
|
+
: opts.plainPollUntilDone(opts.profileDir), opts.heartbeatMessage);
|
|
1002
|
+
if (ok) {
|
|
1003
|
+
if (context !== undefined) {
|
|
1004
|
+
if (opts.onSuccess !== undefined) {
|
|
1005
|
+
try {
|
|
1006
|
+
await opts.onSuccess(context);
|
|
1007
|
+
}
|
|
1008
|
+
catch { /* swallow */ }
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
else if (opts.plainOnSuccess !== undefined) {
|
|
1012
|
+
try {
|
|
1013
|
+
await opts.plainOnSuccess(opts.profileDir);
|
|
1014
|
+
}
|
|
1015
|
+
catch { /* swallow */ }
|
|
847
1016
|
}
|
|
848
|
-
catch { /* swallow */ }
|
|
849
1017
|
}
|
|
850
|
-
await
|
|
1018
|
+
await teardownContext();
|
|
851
1019
|
return { status: ok ? "completed" : "timeout" };
|
|
852
1020
|
}
|
|
853
1021
|
finally {
|
|
854
|
-
|
|
855
|
-
//
|
|
1022
|
+
// Idempotent (self-launch teardown guards with a `torn` flag; the
|
|
1023
|
+
// persistent fallback's close() is .catch-wrapped), so the success-path
|
|
1024
|
+
// call above and this finally can both fire safely.
|
|
1025
|
+
await teardownContext();
|
|
1026
|
+
// Torn down — the signal handler must not double-tear it.
|
|
856
1027
|
activeContext = undefined;
|
|
1028
|
+
activeTeardown = undefined;
|
|
857
1029
|
}
|
|
858
1030
|
}
|
|
859
1031
|
finally {
|
|
@@ -964,7 +1136,10 @@ export async function ensureOAuthSession(opts) {
|
|
|
964
1136
|
// "Google Account: Ada Lovelace (ada@example.com)" on every Google surface, so
|
|
965
1137
|
// prefer that anchored match; fall back to the first email-shaped token (the
|
|
966
1138
|
// myaccount.google.com page renders the address prominently). Pure + exported
|
|
967
|
-
// for unit tests
|
|
1139
|
+
// for unit tests. (Eager capture-at-login was removed with the plain-login
|
|
1140
|
+
// switch — it needed a live CDP context to scrape myaccount.google.com, which
|
|
1141
|
+
// the plain connect browser no longer has; provision scrapes the email per-run
|
|
1142
|
+
// when the marker is unset, so this is now the only consumer besides tests.)
|
|
968
1143
|
const EMAIL_TOKEN_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i;
|
|
969
1144
|
export function extractGoogleAccountEmail(pageText) {
|
|
970
1145
|
const chip = /Google Account:[^()]*\(([^)]+)\)/i.exec(pageText);
|
|
@@ -976,29 +1151,6 @@ export function extractGoogleAccountEmail(pageText) {
|
|
|
976
1151
|
const any = EMAIL_TOKEN_RE.exec(pageText);
|
|
977
1152
|
return any !== null ? any[0].trim() : null;
|
|
978
1153
|
}
|
|
979
|
-
// Capture-at-login: with the profile's live Google session, read the account's
|
|
980
|
-
// email once and persist it to the profile marker so provision can fill it as
|
|
981
|
-
// the signup email (user-owned accounts) without a per-run scrape. Best-effort:
|
|
982
|
-
// any failure leaves the marker unset and provision proceeds without a pre-known
|
|
983
|
-
// email. Only meaningful for google (the inbox identity); github has no inbox.
|
|
984
|
-
async function captureGoogleEmail(context, profileDir) {
|
|
985
|
-
let page = null;
|
|
986
|
-
try {
|
|
987
|
-
page = await context.newPage();
|
|
988
|
-
await page.goto("https://myaccount.google.com/", { waitUntil: "domcontentloaded", timeout: 15_000 });
|
|
989
|
-
const text = await page.evaluate(() => document.body.innerText).catch(() => "");
|
|
990
|
-
const email = extractGoogleAccountEmail(text);
|
|
991
|
-
if (email !== null)
|
|
992
|
-
recordProviderEmail("google", email, profileDir);
|
|
993
|
-
}
|
|
994
|
-
catch {
|
|
995
|
-
/* best-effort — no pre-known email this run */
|
|
996
|
-
}
|
|
997
|
-
finally {
|
|
998
|
-
if (page !== null)
|
|
999
|
-
await page.close().catch(() => undefined);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
1154
|
// Public entry for the install flow: opens the trustysquire /install
|
|
1003
1155
|
// confirm URL in the bot's persistent Chrome profile, runs the
|
|
1004
1156
|
// user-supplied check until the install is claimed (or the deadline
|
|
@@ -1017,45 +1169,33 @@ export async function openInstallConfirmInBotChrome(opts) {
|
|
|
1017
1169
|
deadline,
|
|
1018
1170
|
bannerLabel: `You'll see a Chrome window with the Trusty Squire install page. ` +
|
|
1019
1171
|
`Sign in there to connect this machine — you only sign in once.`,
|
|
1020
|
-
|
|
1172
|
+
// PLAIN browser — no CDP. Google's OAuth flow (which this confirm page
|
|
1173
|
+
// initiates) rejects a CDP-attached Chrome (STATE.md 2026-07-20). The
|
|
1174
|
+
// context-taking pollUntilDone is never invoked in this mode; supply a
|
|
1175
|
+
// stub to satisfy the (CDP-path) type.
|
|
1176
|
+
plainProfileLogin: true,
|
|
1177
|
+
pollUntilDone: () => Promise.resolve(false),
|
|
1178
|
+
plainPollUntilDone: (dir) => opts.pollUntilClaimed(dir),
|
|
1021
1179
|
...(opts.apiBaseUrl !== undefined ? { apiBaseUrl: opts.apiBaseUrl } : {}),
|
|
1022
1180
|
...(opts.heartbeatMessage !== undefined
|
|
1023
1181
|
? { heartbeatMessage: opts.heartbeatMessage }
|
|
1024
1182
|
: {}),
|
|
1025
|
-
// The user's sign-in inside this Chrome leaves a provider session
|
|
1026
|
-
//
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
|
|
1030
|
-
// right after teardown is racy and silently fails on profile-
|
|
1031
|
-
// lock contention, which would leave the marker file empty and
|
|
1032
|
-
// make the signup bot fall back to manual on every subsequent
|
|
1033
|
-
// OAuth-only service.
|
|
1034
|
-
onSuccess: async (context) => {
|
|
1183
|
+
// The user's sign-in inside this Chrome leaves a provider session in the
|
|
1184
|
+
// persistent profile. We don't know WHICH provider they used, so probe
|
|
1185
|
+
// both cookie sets (from the on-disk store — no live context in plain
|
|
1186
|
+
// mode) and mark whichever seeded.
|
|
1187
|
+
plainOnSuccess: async (dir) => {
|
|
1035
1188
|
for (const provider of ["google", "github"]) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
// `.google.com` domain so visible at www.google.com, but
|
|
1039
|
-
// some get set on accounts.google.com specifically. Checking
|
|
1040
|
-
// both catches the OAuth-redirect case (the user came
|
|
1041
|
-
// through accounts.google.com, never visited www.google.com).
|
|
1042
|
-
const origins = [target.cookieOrigin, "https://accounts.google.com"];
|
|
1043
|
-
let hit = false;
|
|
1044
|
-
for (const origin of origins) {
|
|
1045
|
-
const cookies = await context.cookies(origin);
|
|
1046
|
-
if (cookies.some((c) => target.cookies.includes(c.name))) {
|
|
1047
|
-
hit = true;
|
|
1048
|
-
break;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (hit) {
|
|
1052
|
-
markProviderLoggedIn(provider, profileDir);
|
|
1053
|
-
// Capture-at-login: store the Google email now (the one moment it's
|
|
1054
|
-
// certain) so provision fills it as the user-owned signup email.
|
|
1055
|
-
if (provider === "google")
|
|
1056
|
-
await captureGoogleEmail(context, profileDir);
|
|
1189
|
+
if (profileHasProviderCookies(dir, provider)) {
|
|
1190
|
+
markProviderLoggedIn(provider, dir);
|
|
1057
1191
|
}
|
|
1058
1192
|
}
|
|
1193
|
+
// NB: eager Google-email capture (captureGoogleEmail) needed a live
|
|
1194
|
+
// context to scrape myaccount.google.com; the plain login path has
|
|
1195
|
+
// none. It was only an optimization ("provision proceeds without a
|
|
1196
|
+
// pre-known email" otherwise) — provision scrapes the email per-run
|
|
1197
|
+
// when unset, so dropping eager capture is safe and keeping CDP off
|
|
1198
|
+
// the OAuth login is the whole point of the plain path.
|
|
1059
1199
|
},
|
|
1060
1200
|
});
|
|
1061
1201
|
if (result.status === "completed") {
|