doordash-cli 0.3.3 → 0.4.1
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/CHANGELOG.md +12 -0
- package/README.md +14 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +17 -8
- package/dist/cli.test.js +43 -3
- package/dist/direct-api.d.ts +55 -4
- package/dist/direct-api.js +655 -75
- package/dist/direct-api.test.js +529 -14
- package/dist/session-storage.d.ts +1 -0
- package/dist/session-storage.js +4 -0
- package/dist/session-storage.test.js +2 -1
- package/docs/examples.md +3 -3
- package/docs/install.md +15 -5
- package/man/dd-cli.1 +34 -14
- package/package.json +1 -1
package/dist/direct-api.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { createInterface } from "node:readline
|
|
5
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
6
|
import { chromium } from "playwright";
|
|
7
|
-
import { getCookiesPath, getStorageStatePath } from "./session-storage.js";
|
|
7
|
+
import { getBrowserImportBlockPath, getCookiesPath, getStorageStatePath } from "./session-storage.js";
|
|
8
8
|
const BASE_URL = "https://www.doordash.com";
|
|
9
|
+
const AUTH_BOOTSTRAP_URL = `${BASE_URL}/home`;
|
|
10
|
+
const AUTH_BOOTSTRAP_TIMEOUT_MS = 180_000;
|
|
11
|
+
const AUTH_BOOTSTRAP_POLL_INTERVAL_MS = 2_000;
|
|
12
|
+
const AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS = 10_000;
|
|
13
|
+
const ATTACHED_BROWSER_CDP_REACHABILITY_TIMEOUT_MS = 2_000;
|
|
14
|
+
const ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS = 5_000;
|
|
9
15
|
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
|
|
10
16
|
const GRAPHQL_HEADERS = {
|
|
11
17
|
accept: "*/*",
|
|
@@ -697,12 +703,12 @@ class DoorDashDirectSession {
|
|
|
697
703
|
browser = null;
|
|
698
704
|
context = null;
|
|
699
705
|
page = null;
|
|
700
|
-
|
|
706
|
+
attemptedBrowserImport = false;
|
|
701
707
|
async init(options = {}) {
|
|
702
708
|
if (this.page) {
|
|
703
709
|
return this.page;
|
|
704
710
|
}
|
|
705
|
-
await this.
|
|
711
|
+
await this.maybeImportBrowserSession();
|
|
706
712
|
const storageStatePath = getStorageStatePath();
|
|
707
713
|
this.browser = await chromium.launch({
|
|
708
714
|
headless: options.headed ? false : true,
|
|
@@ -803,21 +809,25 @@ class DoorDashDirectSession {
|
|
|
803
809
|
}
|
|
804
810
|
throw new Error(`DoorDash request failed for ${input.url}`);
|
|
805
811
|
}
|
|
806
|
-
|
|
807
|
-
|
|
812
|
+
markBrowserImportAttempted() {
|
|
813
|
+
this.attemptedBrowserImport = true;
|
|
814
|
+
}
|
|
815
|
+
resetBrowserImportAttempted() {
|
|
816
|
+
this.attemptedBrowserImport = false;
|
|
817
|
+
}
|
|
818
|
+
async maybeImportBrowserSession() {
|
|
819
|
+
if (this.attemptedBrowserImport) {
|
|
808
820
|
return;
|
|
809
821
|
}
|
|
810
|
-
this.
|
|
811
|
-
if (await
|
|
822
|
+
this.attemptedBrowserImport = true;
|
|
823
|
+
if (await hasBlockedBrowserImport()) {
|
|
812
824
|
return;
|
|
813
825
|
}
|
|
814
|
-
await
|
|
826
|
+
await importBrowserSessionIfAvailable().catch(() => { });
|
|
815
827
|
}
|
|
816
828
|
}
|
|
817
829
|
const session = new DoorDashDirectSession();
|
|
818
|
-
|
|
819
|
-
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
820
|
-
const consumer = data.consumer ?? null;
|
|
830
|
+
function buildAuthResult(consumer) {
|
|
821
831
|
return {
|
|
822
832
|
success: true,
|
|
823
833
|
isLoggedIn: Boolean(consumer && consumer.isGuest === false),
|
|
@@ -837,36 +847,179 @@ export async function checkAuthDirect() {
|
|
|
837
847
|
storageStatePath: getStorageStatePath(),
|
|
838
848
|
};
|
|
839
849
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
console.error("1) Sign in if needed.");
|
|
844
|
-
console.error("2) Confirm your delivery address if needed.");
|
|
845
|
-
console.error("3) Return here and press Enter to save the session for direct API use.");
|
|
846
|
-
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
847
|
-
const rl = createInterface({ input, output });
|
|
848
|
-
try {
|
|
849
|
-
await rl.question("");
|
|
850
|
+
function buildAuthBootstrapSuccess(auth, message) {
|
|
851
|
+
if (!auth.isLoggedIn) {
|
|
852
|
+
return buildAuthBootstrapFailure(auth, message);
|
|
850
853
|
}
|
|
851
|
-
finally {
|
|
852
|
-
rl.close();
|
|
853
|
-
}
|
|
854
|
-
await session.saveState();
|
|
855
|
-
const auth = await checkAuthDirect();
|
|
856
854
|
return {
|
|
857
855
|
...auth,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
856
|
+
success: true,
|
|
857
|
+
isLoggedIn: true,
|
|
858
|
+
message,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function buildAuthBootstrapFailure(auth, message) {
|
|
862
|
+
const base = auth ?? buildAuthResult(null);
|
|
863
|
+
return {
|
|
864
|
+
...base,
|
|
865
|
+
success: false,
|
|
866
|
+
isLoggedIn: false,
|
|
867
|
+
message,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function canPromptForManagedBrowserConfirmation() {
|
|
871
|
+
return Boolean(process.stdin.isTTY);
|
|
872
|
+
}
|
|
873
|
+
function createManagedBrowserManualConfirmationHandle() {
|
|
874
|
+
if (!canPromptForManagedBrowserConfirmation()) {
|
|
875
|
+
return {
|
|
876
|
+
isEnabled: false,
|
|
877
|
+
consumeRequested: () => false,
|
|
878
|
+
close: () => { },
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const rl = createInterface({
|
|
882
|
+
input: process.stdin,
|
|
883
|
+
output: process.stderr,
|
|
884
|
+
terminal: true,
|
|
885
|
+
});
|
|
886
|
+
let requested = false;
|
|
887
|
+
const onLine = () => {
|
|
888
|
+
requested = true;
|
|
889
|
+
};
|
|
890
|
+
rl.on("line", onLine);
|
|
891
|
+
return {
|
|
892
|
+
isEnabled: true,
|
|
893
|
+
consumeRequested: () => {
|
|
894
|
+
if (!requested) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
requested = false;
|
|
898
|
+
return true;
|
|
899
|
+
},
|
|
900
|
+
close: () => {
|
|
901
|
+
rl.off("line", onLine);
|
|
902
|
+
rl.close();
|
|
903
|
+
},
|
|
861
904
|
};
|
|
862
905
|
}
|
|
906
|
+
export async function checkAuthDirect() {
|
|
907
|
+
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
908
|
+
return buildAuthResult(data.consumer ?? null);
|
|
909
|
+
}
|
|
910
|
+
export async function bootstrapAuthSessionWithDeps(deps) {
|
|
911
|
+
await deps.clearBlockedBrowserImport().catch(() => { });
|
|
912
|
+
const persistedAuth = await deps.checkPersistedAuth().catch(() => null);
|
|
913
|
+
if (persistedAuth?.isLoggedIn) {
|
|
914
|
+
return buildAuthBootstrapSuccess(persistedAuth, "Already signed in with saved local DoorDash session state. No browser interaction was needed.");
|
|
915
|
+
}
|
|
916
|
+
const imported = await deps.importBrowserSessionIfAvailable().catch(() => false);
|
|
917
|
+
deps.markBrowserImportAttempted();
|
|
918
|
+
if (imported) {
|
|
919
|
+
const auth = await deps.checkAuthDirect();
|
|
920
|
+
return auth.isLoggedIn
|
|
921
|
+
? buildAuthBootstrapSuccess(auth, "Imported an existing signed-in browser session and saved it for direct API use.")
|
|
922
|
+
: buildAuthBootstrapFailure(auth, "Imported browser session state, but the consumer still appears to be logged out or guest-only.");
|
|
923
|
+
}
|
|
924
|
+
const attachedCandidates = await deps.getAttachedBrowserCdpCandidates();
|
|
925
|
+
const reachableCandidates = await deps.getReachableCdpCandidates(attachedCandidates);
|
|
926
|
+
if (reachableCandidates.length > 0) {
|
|
927
|
+
const openedAttachedBrowser = await deps.openUrlInAttachedBrowser({
|
|
928
|
+
cdpUrl: reachableCandidates[0],
|
|
929
|
+
targetUrl: AUTH_BOOTSTRAP_URL,
|
|
930
|
+
});
|
|
931
|
+
const openedDefaultBrowser = openedAttachedBrowser ? false : await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
932
|
+
deps.log(openedAttachedBrowser
|
|
933
|
+
? `Opened DoorDash in the attachable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
|
|
934
|
+
: openedDefaultBrowser
|
|
935
|
+
? `Found an attachable browser session, but couldn't drive it directly, so I opened DoorDash in your default browser: ${AUTH_BOOTSTRAP_URL}`
|
|
936
|
+
: `Detected an attachable browser session, but couldn't open DoorDash automatically. Open this URL in that watched browser to continue: ${AUTH_BOOTSTRAP_URL}`);
|
|
937
|
+
deps.log(`Detected ${reachableCandidates.length} attachable browser session(s). Finish the sign-in in that browser window and I'll import it automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
|
|
938
|
+
const importedAfterWait = await deps.waitForAttachedBrowserSessionImport({
|
|
939
|
+
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
940
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
941
|
+
});
|
|
942
|
+
const auth = await deps.checkAuthDirect();
|
|
943
|
+
if (importedAfterWait) {
|
|
944
|
+
return auth.isLoggedIn
|
|
945
|
+
? buildAuthBootstrapSuccess(auth, "Opened DoorDash in an attachable browser session, detected the signed-in consumer state, and saved it for direct API use.")
|
|
946
|
+
: buildAuthBootstrapFailure(auth, "Detected browser session state in the watched browser, but the consumer still appears logged out or guest-only.");
|
|
947
|
+
}
|
|
948
|
+
return buildAuthBootstrapFailure(auth, openedAttachedBrowser || openedDefaultBrowser
|
|
949
|
+
? `Opened DoorDash and watched attachable browser sessions for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no signed-in DoorDash session was imported. Finish the login in that watched browser and rerun dd-cli login.`
|
|
950
|
+
: `Watched attachable browser sessions for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds without importing a signed-in DoorDash session. Open ${AUTH_BOOTSTRAP_URL} manually in the watched browser, finish signing in, then rerun dd-cli login.`);
|
|
951
|
+
}
|
|
952
|
+
const desktopBrowserReuseGap = await deps.describeDesktopBrowserReuseGap().catch(() => null);
|
|
953
|
+
if (desktopBrowserReuseGap) {
|
|
954
|
+
deps.log(desktopBrowserReuseGap);
|
|
955
|
+
}
|
|
956
|
+
const manualManagedConfirmationAvailable = deps.canPromptForManagedBrowserConfirmation();
|
|
957
|
+
deps.log("I couldn't find an attachable browser session I can reuse, so I'm opening a temporary Chromium login window that the CLI can watch directly.");
|
|
958
|
+
deps.log(manualManagedConfirmationAvailable
|
|
959
|
+
? `Finish signing in in that window. I'll keep checking automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds. If the page already shows you're signed in and the CLI still hasn't finished, press Enter here to force an immediate recheck.`
|
|
960
|
+
: `Finish signing in in that window. I'll keep checking automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
|
|
961
|
+
const managedAuth = await deps.waitForManagedBrowserLogin({
|
|
962
|
+
targetUrl: AUTH_BOOTSTRAP_URL,
|
|
963
|
+
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
964
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
965
|
+
log: deps.log,
|
|
966
|
+
});
|
|
967
|
+
if (managedAuth.status === "completed") {
|
|
968
|
+
return buildAuthBootstrapSuccess(managedAuth.auth, managedAuth.completion === "manual"
|
|
969
|
+
? "Opened a temporary Chromium login window. After you pressed Enter to confirm the browser login was complete, the CLI rechecked the signed-in session there and saved it for direct API use."
|
|
970
|
+
: "Opened a temporary Chromium login window, detected the signed-in session there automatically, and saved it for direct API use.");
|
|
971
|
+
}
|
|
972
|
+
if (managedAuth.status === "timed-out") {
|
|
973
|
+
return buildAuthBootstrapFailure(managedAuth.auth, manualManagedConfirmationAvailable
|
|
974
|
+
? `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but I still couldn't prove an authenticated DoorDash session. If the browser already looks signed in, press Enter sooner next time to force an immediate recheck, or rerun dd-cli login.`
|
|
975
|
+
: `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no authenticated DoorDash session was established.`);
|
|
976
|
+
}
|
|
977
|
+
const openedBrowser = await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
978
|
+
deps.log(openedBrowser
|
|
979
|
+
? `I couldn't launch the temporary Chromium login window, so I opened DoorDash in your default browser instead: ${AUTH_BOOTSTRAP_URL}`
|
|
980
|
+
: `Couldn't open the temporary Chromium login window or your default browser automatically. Open this URL to continue: ${AUTH_BOOTSTRAP_URL}`);
|
|
981
|
+
deps.log("This environment still isn't exposing an attachable browser session the CLI can import, so I won't keep you waiting for the full login timeout. Once you've exposed an attachable browser session, rerun `dd-cli login`.");
|
|
982
|
+
const importedAfterGrace = await deps.waitForAttachedBrowserSessionImport({
|
|
983
|
+
timeoutMs: AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS,
|
|
984
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
985
|
+
});
|
|
986
|
+
const auth = await deps.checkAuthDirect();
|
|
987
|
+
if (importedAfterGrace) {
|
|
988
|
+
return auth.isLoggedIn
|
|
989
|
+
? buildAuthBootstrapSuccess(auth, "An attachable browser session appeared a few seconds later and was saved for direct API use.")
|
|
990
|
+
: buildAuthBootstrapFailure(auth, "Detected browser session state after opening the browser, but the consumer still appears logged out or guest-only.");
|
|
991
|
+
}
|
|
992
|
+
return buildAuthBootstrapFailure(auth, openedBrowser
|
|
993
|
+
? "Opened DoorDash in your default browser, but this environment still isn't exposing an attachable browser session the CLI can import. Finish signing in there, make an attachable browser session discoverable, then rerun `dd-cli login`."
|
|
994
|
+
: "Couldn't open a watchable browser automatically, and this environment still isn't exposing an attachable browser session the CLI can import. Open the DoorDash home page manually, make an attachable browser session discoverable, then rerun `dd-cli login`.");
|
|
995
|
+
}
|
|
996
|
+
export async function bootstrapAuthSession() {
|
|
997
|
+
return bootstrapAuthSessionWithDeps({
|
|
998
|
+
clearBlockedBrowserImport,
|
|
999
|
+
checkPersistedAuth: getPersistedAuthDirect,
|
|
1000
|
+
importBrowserSessionIfAvailable,
|
|
1001
|
+
markBrowserImportAttempted: () => session.markBrowserImportAttempted(),
|
|
1002
|
+
getAttachedBrowserCdpCandidates,
|
|
1003
|
+
getReachableCdpCandidates,
|
|
1004
|
+
describeDesktopBrowserReuseGap,
|
|
1005
|
+
openUrlInAttachedBrowser,
|
|
1006
|
+
openUrlInDefaultBrowser,
|
|
1007
|
+
waitForAttachedBrowserSessionImport,
|
|
1008
|
+
waitForManagedBrowserLogin,
|
|
1009
|
+
canPromptForManagedBrowserConfirmation,
|
|
1010
|
+
checkAuthDirect,
|
|
1011
|
+
log: (message) => console.error(message),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
863
1014
|
export async function clearStoredSession() {
|
|
864
1015
|
await session.close();
|
|
1016
|
+
session.resetBrowserImportAttempted();
|
|
865
1017
|
await rm(getCookiesPath(), { force: true }).catch(() => { });
|
|
866
1018
|
await rm(getStorageStatePath(), { force: true }).catch(() => { });
|
|
1019
|
+
await blockBrowserImport();
|
|
867
1020
|
return {
|
|
868
1021
|
success: true,
|
|
869
|
-
message: "DoorDash cookies and stored browser session state cleared.",
|
|
1022
|
+
message: "DoorDash cookies and stored browser session state cleared. Automatic browser-session reuse is disabled until the next `dd-cli login`.",
|
|
870
1023
|
cookiesPath: getCookiesPath(),
|
|
871
1024
|
storageStatePath: getStorageStatePath(),
|
|
872
1025
|
};
|
|
@@ -1538,7 +1691,7 @@ function normalizeAddressText(value) {
|
|
|
1538
1691
|
.replace(/[.,]/g, "")
|
|
1539
1692
|
.replace(/\s+/g, " ");
|
|
1540
1693
|
}
|
|
1541
|
-
|
|
1694
|
+
function isDoorDashUrl(value) {
|
|
1542
1695
|
try {
|
|
1543
1696
|
const url = new URL(value);
|
|
1544
1697
|
return url.hostname === "doordash.com" || url.hostname.endsWith(".doordash.com");
|
|
@@ -1547,13 +1700,13 @@ export function isDoorDashUrl(value) {
|
|
|
1547
1700
|
return false;
|
|
1548
1701
|
}
|
|
1549
1702
|
}
|
|
1550
|
-
|
|
1703
|
+
function hasDoorDashCookies(cookies) {
|
|
1551
1704
|
return cookies.some((cookie) => {
|
|
1552
1705
|
const domain = cookie.domain.trim().replace(/^\./, "").toLowerCase();
|
|
1553
1706
|
return domain === "doordash.com" || domain.endsWith(".doordash.com");
|
|
1554
1707
|
});
|
|
1555
1708
|
}
|
|
1556
|
-
export function
|
|
1709
|
+
export function selectAttachedBrowserImportMode(input) {
|
|
1557
1710
|
if (input.pageUrls.some((url) => isDoorDashUrl(url))) {
|
|
1558
1711
|
return "page";
|
|
1559
1712
|
}
|
|
@@ -1562,45 +1715,57 @@ export function selectManagedBrowserImportMode(input) {
|
|
|
1562
1715
|
}
|
|
1563
1716
|
return "skip";
|
|
1564
1717
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
|
|
1718
|
+
async function importBrowserSessionIfAvailable() {
|
|
1719
|
+
return await importBrowserSessionFromCdpCandidates(await getAttachedBrowserCdpCandidates());
|
|
1720
|
+
}
|
|
1721
|
+
async function importBrowserSessionFromCdpCandidates(candidates) {
|
|
1722
|
+
for (const cdpUrl of candidates) {
|
|
1567
1723
|
if (!(await isCdpEndpointReachable(cdpUrl))) {
|
|
1568
1724
|
continue;
|
|
1569
1725
|
}
|
|
1570
1726
|
let browser = null;
|
|
1727
|
+
let tempPage = null;
|
|
1571
1728
|
try {
|
|
1572
|
-
browser = await chromium.connectOverCDP(cdpUrl);
|
|
1729
|
+
browser = await chromium.connectOverCDP(cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
|
|
1573
1730
|
const context = browser.contexts()[0];
|
|
1574
1731
|
if (!context) {
|
|
1575
1732
|
continue;
|
|
1576
1733
|
}
|
|
1577
1734
|
const cookies = await context.cookies();
|
|
1578
1735
|
const pages = context.pages();
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
pageUrls: reusablePage ? [reusablePage.url()] : [],
|
|
1736
|
+
const importMode = selectAttachedBrowserImportMode({
|
|
1737
|
+
pageUrls: pages.map((candidate) => candidate.url()),
|
|
1582
1738
|
cookies,
|
|
1583
1739
|
});
|
|
1584
1740
|
if (importMode === "skip") {
|
|
1585
1741
|
continue;
|
|
1586
1742
|
}
|
|
1587
|
-
if (
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (consumer?.isGuest === false) {
|
|
1591
|
-
await saveContextState(context, cookies);
|
|
1743
|
+
if (importMode === "cookies") {
|
|
1744
|
+
await saveContextState(context, cookies);
|
|
1745
|
+
if (await validatePersistedDirectSessionArtifacts()) {
|
|
1592
1746
|
return true;
|
|
1593
1747
|
}
|
|
1594
1748
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1749
|
+
let page = pages.find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
|
|
1750
|
+
if (!page) {
|
|
1751
|
+
tempPage = await context.newPage();
|
|
1752
|
+
page = tempPage;
|
|
1753
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1754
|
+
await page.waitForTimeout(1_000);
|
|
1598
1755
|
}
|
|
1756
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1757
|
+
const consumer = consumerData?.consumer ?? null;
|
|
1758
|
+
if (!consumer || consumer.isGuest !== false) {
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
await saveContextState(context, cookies);
|
|
1762
|
+
return true;
|
|
1599
1763
|
}
|
|
1600
1764
|
catch {
|
|
1601
1765
|
continue;
|
|
1602
1766
|
}
|
|
1603
1767
|
finally {
|
|
1768
|
+
await tempPage?.close().catch(() => { });
|
|
1604
1769
|
await browser?.close().catch(() => { });
|
|
1605
1770
|
}
|
|
1606
1771
|
}
|
|
@@ -1619,7 +1784,27 @@ async function fetchConsumerViaPage(page) {
|
|
|
1619
1784
|
headers: GRAPHQL_HEADERS,
|
|
1620
1785
|
url: `${BASE_URL}/graphql/consumer?operation=consumer`,
|
|
1621
1786
|
});
|
|
1622
|
-
return parseGraphQlResponse("
|
|
1787
|
+
return parseGraphQlResponse("attachedBrowserConsumerImport", raw.status, raw.text);
|
|
1788
|
+
}
|
|
1789
|
+
async function readLiveManagedBrowserAuth(page) {
|
|
1790
|
+
if (!page || page.isClosed()) {
|
|
1791
|
+
return buildAuthResult(null);
|
|
1792
|
+
}
|
|
1793
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1794
|
+
return buildAuthResult(consumerData?.consumer ?? null);
|
|
1795
|
+
}
|
|
1796
|
+
async function confirmManagedBrowserAuth(context, page) {
|
|
1797
|
+
const liveAuth = await readLiveManagedBrowserAuth(page);
|
|
1798
|
+
if (liveAuth.isLoggedIn) {
|
|
1799
|
+
if (context) {
|
|
1800
|
+
await saveContextState(context).catch(() => { });
|
|
1801
|
+
}
|
|
1802
|
+
return liveAuth;
|
|
1803
|
+
}
|
|
1804
|
+
if (context) {
|
|
1805
|
+
await saveContextState(context).catch(() => { });
|
|
1806
|
+
}
|
|
1807
|
+
return (await getPersistedAuthDirect().catch(() => null)) ?? liveAuth;
|
|
1623
1808
|
}
|
|
1624
1809
|
async function saveContextState(context, cookies = null) {
|
|
1625
1810
|
const storageStatePath = getStorageStatePath();
|
|
@@ -1628,41 +1813,418 @@ async function saveContextState(context, cookies = null) {
|
|
|
1628
1813
|
const resolvedCookies = cookies ?? (await context.cookies());
|
|
1629
1814
|
await writeFile(getCookiesPath(), JSON.stringify(resolvedCookies, null, 2));
|
|
1630
1815
|
}
|
|
1631
|
-
async function
|
|
1816
|
+
async function getPersistedAuthDirect() {
|
|
1817
|
+
if (!(await hasPersistedSessionArtifacts())) {
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
let browser = null;
|
|
1821
|
+
let context = null;
|
|
1822
|
+
let page = null;
|
|
1823
|
+
try {
|
|
1824
|
+
browser = await chromium.launch({
|
|
1825
|
+
headless: true,
|
|
1826
|
+
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-setuid-sandbox"],
|
|
1827
|
+
});
|
|
1828
|
+
const storageStatePath = getStorageStatePath();
|
|
1829
|
+
const hasStorage = await hasStorageState();
|
|
1830
|
+
context = await browser.newContext({
|
|
1831
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
1832
|
+
locale: "en-US",
|
|
1833
|
+
viewport: { width: 1280, height: 900 },
|
|
1834
|
+
...(hasStorage ? { storageState: storageStatePath } : {}),
|
|
1835
|
+
});
|
|
1836
|
+
if (!hasStorage) {
|
|
1837
|
+
const cookies = await readStoredCookies();
|
|
1838
|
+
if (cookies.length === 0) {
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
await context.addCookies(cookies);
|
|
1842
|
+
}
|
|
1843
|
+
page = await context.newPage();
|
|
1844
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1845
|
+
await page.waitForTimeout(1_000);
|
|
1846
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1847
|
+
return buildAuthResult(consumerData?.consumer ?? null);
|
|
1848
|
+
}
|
|
1849
|
+
catch {
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
finally {
|
|
1853
|
+
await page?.close().catch(() => { });
|
|
1854
|
+
await context?.close().catch(() => { });
|
|
1855
|
+
await browser?.close().catch(() => { });
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async function validatePersistedDirectSessionArtifacts() {
|
|
1859
|
+
const auth = await getPersistedAuthDirect();
|
|
1860
|
+
return auth?.isLoggedIn === true;
|
|
1861
|
+
}
|
|
1862
|
+
export function resolveAttachedBrowserCdpCandidates(env, configCandidates = []) {
|
|
1632
1863
|
const candidates = new Set();
|
|
1633
|
-
|
|
1634
|
-
process.env.DOORDASH_MANAGED_BROWSER_CDP_URL,
|
|
1635
|
-
process.env.OPENCLAW_BROWSER_CDP_URL,
|
|
1636
|
-
process.env.OPENCLAW_OPENCLAW_CDP_URL,
|
|
1637
|
-
]) {
|
|
1864
|
+
const addCandidate = (value) => {
|
|
1638
1865
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
1639
|
-
candidates.add(value
|
|
1866
|
+
candidates.add(normalizeCdpCandidate(value));
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
addCdpCandidatesFromList(candidates, env.DOORDASH_ATTACHED_BROWSER_CDP_URLS);
|
|
1870
|
+
addCdpCandidatesFromList(candidates, env.DOORDASH_BROWSER_CDP_URLS);
|
|
1871
|
+
addCandidate(env.DOORDASH_ATTACHED_BROWSER_CDP_URL);
|
|
1872
|
+
addCandidate(env.DOORDASH_BROWSER_CDP_URL);
|
|
1873
|
+
addCdpPortCandidatesFromList(candidates, env.DOORDASH_BROWSER_CDP_PORTS);
|
|
1874
|
+
const portCandidate = parseCdpPortCandidate(env.DOORDASH_BROWSER_CDP_PORT);
|
|
1875
|
+
if (portCandidate) {
|
|
1876
|
+
candidates.add(portCandidate);
|
|
1877
|
+
}
|
|
1878
|
+
for (const compatibilityValue of [env.DOORDASH_MANAGED_BROWSER_CDP_URL, env.OPENCLAW_BROWSER_CDP_URL, env.OPENCLAW_OPENCLAW_CDP_URL]) {
|
|
1879
|
+
addCandidate(compatibilityValue);
|
|
1880
|
+
}
|
|
1881
|
+
for (const value of configCandidates) {
|
|
1882
|
+
addCandidate(value);
|
|
1883
|
+
}
|
|
1884
|
+
addCandidate("http://127.0.0.1:18792");
|
|
1885
|
+
addCandidate("http://127.0.0.1:18800");
|
|
1886
|
+
addCandidate("http://127.0.0.1:9222");
|
|
1887
|
+
return [...candidates];
|
|
1888
|
+
}
|
|
1889
|
+
async function getAttachedBrowserCdpCandidates() {
|
|
1890
|
+
const configCandidates = await readOpenClawBrowserConfigCandidates({ profileNames: ["user", "chrome", "openclaw"] });
|
|
1891
|
+
return resolveAttachedBrowserCdpCandidates(process.env, configCandidates);
|
|
1892
|
+
}
|
|
1893
|
+
async function getReachableCdpCandidates(candidates) {
|
|
1894
|
+
const reachable = [];
|
|
1895
|
+
for (const cdpUrl of candidates) {
|
|
1896
|
+
if (await isCdpEndpointReachable(cdpUrl)) {
|
|
1897
|
+
reachable.push(cdpUrl);
|
|
1640
1898
|
}
|
|
1641
1899
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1900
|
+
return reachable;
|
|
1901
|
+
}
|
|
1902
|
+
async function waitForAttachedBrowserSessionImport(input) {
|
|
1903
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1904
|
+
while (Date.now() <= deadline) {
|
|
1905
|
+
if (await importBrowserSessionIfAvailable().catch(() => false)) {
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
if (Date.now() >= deadline) {
|
|
1909
|
+
break;
|
|
1910
|
+
}
|
|
1911
|
+
await wait(input.pollIntervalMs);
|
|
1644
1912
|
}
|
|
1645
|
-
|
|
1646
|
-
return [...candidates];
|
|
1913
|
+
return false;
|
|
1647
1914
|
}
|
|
1648
|
-
|
|
1915
|
+
export function resolveSystemBrowserOpenCommand(targetUrl, targetPlatform = process.platform) {
|
|
1916
|
+
if (targetPlatform === "darwin") {
|
|
1917
|
+
return { command: "open", args: [targetUrl] };
|
|
1918
|
+
}
|
|
1919
|
+
if (targetPlatform === "win32") {
|
|
1920
|
+
return { command: "cmd", args: ["/c", "start", "", targetUrl] };
|
|
1921
|
+
}
|
|
1922
|
+
if (["linux", "freebsd", "openbsd", "netbsd", "sunos", "android"].includes(targetPlatform)) {
|
|
1923
|
+
return { command: "xdg-open", args: [targetUrl] };
|
|
1924
|
+
}
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
async function openUrlInDefaultBrowser(targetUrl) {
|
|
1928
|
+
const command = resolveSystemBrowserOpenCommand(targetUrl);
|
|
1929
|
+
if (!command) {
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
return await new Promise((resolve) => {
|
|
1933
|
+
const child = spawn(command.command, command.args, {
|
|
1934
|
+
detached: process.platform !== "win32",
|
|
1935
|
+
stdio: "ignore",
|
|
1936
|
+
});
|
|
1937
|
+
child.once("error", () => resolve(false));
|
|
1938
|
+
child.once("spawn", () => {
|
|
1939
|
+
child.unref();
|
|
1940
|
+
resolve(true);
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
async function openUrlInAttachedBrowser(input) {
|
|
1945
|
+
let browser = null;
|
|
1946
|
+
try {
|
|
1947
|
+
browser = await chromium.connectOverCDP(input.cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
|
|
1948
|
+
const context = browser.contexts()[0];
|
|
1949
|
+
if (!context) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
let page = context.pages().find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
|
|
1953
|
+
if (!page) {
|
|
1954
|
+
page = await context.newPage();
|
|
1955
|
+
}
|
|
1956
|
+
await page.goto(input.targetUrl, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1957
|
+
await page.bringToFront().catch(() => { });
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
return false;
|
|
1962
|
+
}
|
|
1963
|
+
finally {
|
|
1964
|
+
await browser?.close().catch(() => { });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function waitForManagedBrowserLogin(input) {
|
|
1968
|
+
let browser = null;
|
|
1969
|
+
let context = null;
|
|
1970
|
+
let page = null;
|
|
1971
|
+
const manualConfirmation = createManagedBrowserManualConfirmationHandle();
|
|
1972
|
+
try {
|
|
1973
|
+
browser = await chromium.launch({
|
|
1974
|
+
headless: false,
|
|
1975
|
+
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-setuid-sandbox"],
|
|
1976
|
+
});
|
|
1977
|
+
const storageStatePath = getStorageStatePath();
|
|
1978
|
+
const hasStorage = await hasStorageState();
|
|
1979
|
+
context = await browser.newContext({
|
|
1980
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
1981
|
+
locale: "en-US",
|
|
1982
|
+
viewport: { width: 1280, height: 900 },
|
|
1983
|
+
...(hasStorage ? { storageState: storageStatePath } : {}),
|
|
1984
|
+
});
|
|
1985
|
+
if (!hasStorage) {
|
|
1986
|
+
const cookies = await readStoredCookies();
|
|
1987
|
+
if (cookies.length > 0) {
|
|
1988
|
+
await context.addCookies(cookies);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
page = await context.newPage();
|
|
1992
|
+
await page.goto(input.targetUrl, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1993
|
+
await page.bringToFront().catch(() => { });
|
|
1994
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1995
|
+
while (Date.now() <= deadline) {
|
|
1996
|
+
const liveAuth = await readLiveManagedBrowserAuth(page);
|
|
1997
|
+
if (liveAuth.isLoggedIn) {
|
|
1998
|
+
await saveContextState(context).catch(() => { });
|
|
1999
|
+
return {
|
|
2000
|
+
status: "completed",
|
|
2001
|
+
completion: "automatic",
|
|
2002
|
+
auth: liveAuth,
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
if (manualConfirmation.consumeRequested()) {
|
|
2006
|
+
const confirmedAuth = await confirmManagedBrowserAuth(context, page);
|
|
2007
|
+
if (confirmedAuth.isLoggedIn) {
|
|
2008
|
+
return {
|
|
2009
|
+
status: "completed",
|
|
2010
|
+
completion: "manual",
|
|
2011
|
+
auth: confirmedAuth,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
input.log("Still not seeing an authenticated DoorDash session yet. Finish signing in in the opened browser window, then press Enter again or keep waiting for automatic detection.");
|
|
2015
|
+
}
|
|
2016
|
+
if (page.isClosed()) {
|
|
2017
|
+
break;
|
|
2018
|
+
}
|
|
2019
|
+
if (Date.now() >= deadline) {
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
2022
|
+
await wait(input.pollIntervalMs);
|
|
2023
|
+
}
|
|
2024
|
+
const finalAuth = await confirmManagedBrowserAuth(context, page);
|
|
2025
|
+
if (finalAuth.isLoggedIn) {
|
|
2026
|
+
return {
|
|
2027
|
+
status: "completed",
|
|
2028
|
+
completion: "automatic",
|
|
2029
|
+
auth: finalAuth,
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
status: "timed-out",
|
|
2034
|
+
auth: finalAuth,
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
catch (error) {
|
|
2038
|
+
if (isPlaywrightBrowserInstallMissingError(error)) {
|
|
2039
|
+
return { status: "launch-failed" };
|
|
2040
|
+
}
|
|
2041
|
+
return { status: "launch-failed" };
|
|
2042
|
+
}
|
|
2043
|
+
finally {
|
|
2044
|
+
manualConfirmation.close();
|
|
2045
|
+
await page?.close().catch(() => { });
|
|
2046
|
+
await context?.close().catch(() => { });
|
|
2047
|
+
await browser?.close().catch(() => { });
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
function isPlaywrightBrowserInstallMissingError(error) {
|
|
2051
|
+
if (!(error instanceof Error)) {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
const message = error.message.toLowerCase();
|
|
2055
|
+
return message.includes("executable doesn't exist") || message.includes("please run the following command") || message.includes("playwright install");
|
|
2056
|
+
}
|
|
2057
|
+
const KNOWN_DESKTOP_BROWSERS = [
|
|
2058
|
+
{
|
|
2059
|
+
label: "Brave",
|
|
2060
|
+
processMatchers: [/\bbrave-browser(?:-stable)?\b/i, /\/brave(?:$|\s)/i],
|
|
2061
|
+
devToolsActivePortPaths: [
|
|
2062
|
+
join(homedir(), ".config", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
|
|
2063
|
+
join(homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
|
|
2064
|
+
],
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
label: "Google Chrome",
|
|
2068
|
+
processMatchers: [/\bgoogle-chrome(?:-stable|-beta|-unstable)?\b/i, /\/google-chrome(?:$|\s)/i],
|
|
2069
|
+
devToolsActivePortPaths: [
|
|
2070
|
+
join(homedir(), ".config", "google-chrome", "DevToolsActivePort"),
|
|
2071
|
+
join(homedir(), "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
|
|
2072
|
+
],
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
label: "Microsoft Edge",
|
|
2076
|
+
processMatchers: [/\bmicrosoft-edge(?:-stable|-beta|-dev)?\b/i, /\/microsoft-edge(?:$|\s)/i],
|
|
2077
|
+
devToolsActivePortPaths: [
|
|
2078
|
+
join(homedir(), ".config", "microsoft-edge", "DevToolsActivePort"),
|
|
2079
|
+
join(homedir(), "Library", "Application Support", "Microsoft Edge", "DevToolsActivePort"),
|
|
2080
|
+
],
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
label: "Chromium",
|
|
2084
|
+
processMatchers: [/\bchromium-browser\b/i, /\bchromium\b/i],
|
|
2085
|
+
devToolsActivePortPaths: [
|
|
2086
|
+
join(homedir(), ".config", "chromium", "DevToolsActivePort"),
|
|
2087
|
+
join(homedir(), "Library", "Application Support", "Chromium", "DevToolsActivePort"),
|
|
2088
|
+
],
|
|
2089
|
+
},
|
|
2090
|
+
];
|
|
2091
|
+
function normalizeCdpCandidate(value) {
|
|
2092
|
+
return value.trim().replace(/\/$/, "");
|
|
2093
|
+
}
|
|
2094
|
+
function addCdpCandidatesFromList(candidates, value) {
|
|
2095
|
+
if (!value) {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
for (const entry of value.split(/[,\n]/)) {
|
|
2099
|
+
const trimmed = entry.trim();
|
|
2100
|
+
if (trimmed) {
|
|
2101
|
+
candidates.add(normalizeCdpCandidate(trimmed));
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
function addCdpPortCandidatesFromList(candidates, value) {
|
|
2106
|
+
if (!value) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
for (const entry of value.split(/[,\n]/)) {
|
|
2110
|
+
const portCandidate = parseCdpPortCandidate(entry.trim());
|
|
2111
|
+
if (portCandidate) {
|
|
2112
|
+
candidates.add(portCandidate);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function parseCdpPortCandidate(value) {
|
|
2117
|
+
if (!value) {
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
const parsed = Number.parseInt(value, 10);
|
|
2121
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
return `http://127.0.0.1:${parsed}`;
|
|
2125
|
+
}
|
|
2126
|
+
function appendBrowserConfigCandidate(candidates, value) {
|
|
2127
|
+
const object = asObject(value);
|
|
2128
|
+
if (typeof object.cdpUrl === "string" && object.cdpUrl.trim()) {
|
|
2129
|
+
candidates.push(normalizeCdpCandidate(object.cdpUrl));
|
|
2130
|
+
}
|
|
2131
|
+
else if (typeof object.cdpPort === "number" && Number.isInteger(object.cdpPort)) {
|
|
2132
|
+
candidates.push(`http://127.0.0.1:${object.cdpPort}`);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function wait(ms) {
|
|
2136
|
+
return new Promise((resolve) => {
|
|
2137
|
+
setTimeout(resolve, ms);
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
function detectRunningDesktopBrowser(processCommands) {
|
|
2141
|
+
const filteredCommands = processCommands
|
|
2142
|
+
.map((command) => command.trim())
|
|
2143
|
+
.filter((command) => command.length > 0)
|
|
2144
|
+
.filter((command) => !/chrome-devtools-mcp/i.test(command));
|
|
2145
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2146
|
+
const preferredMatch = filteredCommands.some((command) => !/--type=|crashpad|zygote/i.test(command) && browser.processMatchers.some((matcher) => matcher.test(command)));
|
|
2147
|
+
if (preferredMatch) {
|
|
2148
|
+
return browser;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2152
|
+
if (filteredCommands.some((command) => browser.processMatchers.some((matcher) => matcher.test(command)))) {
|
|
2153
|
+
return browser;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
function hasRemoteDebuggingSignal(processCommands) {
|
|
2159
|
+
return processCommands.some((command) => /--remote-debugging-(?:port|pipe)(?:=|\b)/i.test(command));
|
|
2160
|
+
}
|
|
2161
|
+
export function summarizeDesktopBrowserReuseGap(input) {
|
|
2162
|
+
const browser = detectRunningDesktopBrowser(input.processCommands);
|
|
2163
|
+
if (!browser) {
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
if (hasRemoteDebuggingSignal(input.processCommands) || input.hasAnyDevToolsActivePort) {
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
return `I can see ${browser.label} is already running on this desktop, but it is not exposing an attachable browser automation session right now. A normal open browser window is not automatically reusable; dd-cli can only import a browser session it can actually attach to.`;
|
|
2170
|
+
}
|
|
2171
|
+
async function captureCommandStdout(command, args) {
|
|
2172
|
+
return await new Promise((resolve, reject) => {
|
|
2173
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2174
|
+
const stdout = [];
|
|
2175
|
+
child.once("error", reject);
|
|
2176
|
+
child.stdout?.on("data", (chunk) => {
|
|
2177
|
+
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2178
|
+
});
|
|
2179
|
+
child.once("close", (code) => {
|
|
2180
|
+
if (code === 0) {
|
|
2181
|
+
resolve(Buffer.concat(stdout).toString("utf8"));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
async function listProcessCommands(targetPlatform = process.platform) {
|
|
2189
|
+
if (targetPlatform === "win32") {
|
|
2190
|
+
return [];
|
|
2191
|
+
}
|
|
2192
|
+
const args = targetPlatform === "darwin" ? ["-axo", "command="] : ["-eo", "command="];
|
|
2193
|
+
const stdout = await captureCommandStdout("ps", args).catch(() => "");
|
|
2194
|
+
return stdout
|
|
2195
|
+
.split(/\r?\n/)
|
|
2196
|
+
.map((command) => command.trim())
|
|
2197
|
+
.filter((command) => command.length > 0);
|
|
2198
|
+
}
|
|
2199
|
+
async function hasAnyKnownDevToolsActivePort() {
|
|
2200
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2201
|
+
for (const path of browser.devToolsActivePortPaths) {
|
|
2202
|
+
if ((await readFile(path, "utf8").catch(() => null)) !== null) {
|
|
2203
|
+
return true;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
async function describeDesktopBrowserReuseGap() {
|
|
2210
|
+
const processCommands = await listProcessCommands();
|
|
2211
|
+
return summarizeDesktopBrowserReuseGap({
|
|
2212
|
+
processCommands,
|
|
2213
|
+
hasAnyDevToolsActivePort: await hasAnyKnownDevToolsActivePort(),
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
async function readOpenClawBrowserConfigCandidates(input) {
|
|
1649
2217
|
try {
|
|
1650
2218
|
const raw = await readFile(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
|
|
1651
2219
|
const parsed = safeJsonParse(raw);
|
|
1652
2220
|
const browserConfig = asObject(parsed?.browser);
|
|
2221
|
+
const profiles = asObject(browserConfig.profiles);
|
|
1653
2222
|
const candidates = [];
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
else if (typeof object.cdpPort === "number" && Number.isInteger(object.cdpPort)) {
|
|
1660
|
-
candidates.push(`http://127.0.0.1:${object.cdpPort}`);
|
|
1661
|
-
}
|
|
1662
|
-
};
|
|
1663
|
-
pushCandidate(browserConfig);
|
|
1664
|
-
pushCandidate(browserConfig.openclaw);
|
|
1665
|
-
pushCandidate(asObject(browserConfig.profiles).openclaw);
|
|
2223
|
+
appendBrowserConfigCandidate(candidates, browserConfig);
|
|
2224
|
+
appendBrowserConfigCandidate(candidates, browserConfig.openclaw);
|
|
2225
|
+
for (const profileName of input.profileNames) {
|
|
2226
|
+
appendBrowserConfigCandidate(candidates, profiles[profileName]);
|
|
2227
|
+
}
|
|
1666
2228
|
return dedupeBy(candidates, (value) => value);
|
|
1667
2229
|
}
|
|
1668
2230
|
catch {
|
|
@@ -1671,7 +2233,9 @@ async function readOpenClawBrowserConfigCandidates() {
|
|
|
1671
2233
|
}
|
|
1672
2234
|
async function isCdpEndpointReachable(cdpUrl) {
|
|
1673
2235
|
try {
|
|
1674
|
-
const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version
|
|
2236
|
+
const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version`, {
|
|
2237
|
+
signal: AbortSignal.timeout(ATTACHED_BROWSER_CDP_REACHABILITY_TIMEOUT_MS),
|
|
2238
|
+
});
|
|
1675
2239
|
return response.ok;
|
|
1676
2240
|
}
|
|
1677
2241
|
catch {
|
|
@@ -2306,6 +2870,22 @@ function truncate(value, length) {
|
|
|
2306
2870
|
async function ensureConfigDir() {
|
|
2307
2871
|
await mkdir(dirname(getCookiesPath()), { recursive: true });
|
|
2308
2872
|
}
|
|
2873
|
+
async function hasBlockedBrowserImport() {
|
|
2874
|
+
try {
|
|
2875
|
+
await readFile(getBrowserImportBlockPath(), "utf8");
|
|
2876
|
+
return true;
|
|
2877
|
+
}
|
|
2878
|
+
catch {
|
|
2879
|
+
return false;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
async function blockBrowserImport() {
|
|
2883
|
+
await ensureConfigDir();
|
|
2884
|
+
await writeFile(getBrowserImportBlockPath(), "logged-out\n");
|
|
2885
|
+
}
|
|
2886
|
+
async function clearBlockedBrowserImport() {
|
|
2887
|
+
await rm(getBrowserImportBlockPath(), { force: true }).catch(() => { });
|
|
2888
|
+
}
|
|
2309
2889
|
async function hasStorageState() {
|
|
2310
2890
|
try {
|
|
2311
2891
|
await readFile(getStorageStatePath(), "utf8");
|