doordash-cli 0.3.3 → 0.4.0
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 +6 -0
- package/README.md +14 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +16 -8
- package/dist/cli.test.js +41 -3
- package/dist/direct-api.d.ts +39 -5
- package/dist/direct-api.js +439 -73
- package/dist/direct-api.test.js +313 -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 +26 -14
- package/package.json +1 -1
package/dist/direct-api.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
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/promises";
|
|
5
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
6
5
|
import { chromium } from "playwright";
|
|
7
|
-
import { getCookiesPath, getStorageStatePath } from "./session-storage.js";
|
|
6
|
+
import { getBrowserImportBlockPath, getCookiesPath, getStorageStatePath } from "./session-storage.js";
|
|
8
7
|
const BASE_URL = "https://www.doordash.com";
|
|
8
|
+
const AUTH_BOOTSTRAP_URL = `${BASE_URL}/home`;
|
|
9
|
+
const AUTH_BOOTSTRAP_TIMEOUT_MS = 180_000;
|
|
10
|
+
const AUTH_BOOTSTRAP_POLL_INTERVAL_MS = 2_000;
|
|
11
|
+
const AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS = 10_000;
|
|
9
12
|
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
13
|
const GRAPHQL_HEADERS = {
|
|
11
14
|
accept: "*/*",
|
|
@@ -697,12 +700,12 @@ class DoorDashDirectSession {
|
|
|
697
700
|
browser = null;
|
|
698
701
|
context = null;
|
|
699
702
|
page = null;
|
|
700
|
-
|
|
703
|
+
attemptedBrowserImport = false;
|
|
701
704
|
async init(options = {}) {
|
|
702
705
|
if (this.page) {
|
|
703
706
|
return this.page;
|
|
704
707
|
}
|
|
705
|
-
await this.
|
|
708
|
+
await this.maybeImportBrowserSession();
|
|
706
709
|
const storageStatePath = getStorageStatePath();
|
|
707
710
|
this.browser = await chromium.launch({
|
|
708
711
|
headless: options.headed ? false : true,
|
|
@@ -803,21 +806,25 @@ class DoorDashDirectSession {
|
|
|
803
806
|
}
|
|
804
807
|
throw new Error(`DoorDash request failed for ${input.url}`);
|
|
805
808
|
}
|
|
806
|
-
|
|
807
|
-
|
|
809
|
+
markBrowserImportAttempted() {
|
|
810
|
+
this.attemptedBrowserImport = true;
|
|
811
|
+
}
|
|
812
|
+
resetBrowserImportAttempted() {
|
|
813
|
+
this.attemptedBrowserImport = false;
|
|
814
|
+
}
|
|
815
|
+
async maybeImportBrowserSession() {
|
|
816
|
+
if (this.attemptedBrowserImport) {
|
|
808
817
|
return;
|
|
809
818
|
}
|
|
810
|
-
this.
|
|
811
|
-
if (await
|
|
819
|
+
this.attemptedBrowserImport = true;
|
|
820
|
+
if (await hasBlockedBrowserImport()) {
|
|
812
821
|
return;
|
|
813
822
|
}
|
|
814
|
-
await
|
|
823
|
+
await importBrowserSessionIfAvailable().catch(() => { });
|
|
815
824
|
}
|
|
816
825
|
}
|
|
817
826
|
const session = new DoorDashDirectSession();
|
|
818
|
-
|
|
819
|
-
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
820
|
-
const consumer = data.consumer ?? null;
|
|
827
|
+
function buildAuthResult(consumer) {
|
|
821
828
|
return {
|
|
822
829
|
success: true,
|
|
823
830
|
isLoggedIn: Boolean(consumer && consumer.isGuest === false),
|
|
@@ -837,36 +844,127 @@ export async function checkAuthDirect() {
|
|
|
837
844
|
storageStatePath: getStorageStatePath(),
|
|
838
845
|
};
|
|
839
846
|
}
|
|
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
|
-
}
|
|
851
|
-
finally {
|
|
852
|
-
rl.close();
|
|
847
|
+
function buildAuthBootstrapSuccess(auth, message) {
|
|
848
|
+
if (!auth.isLoggedIn) {
|
|
849
|
+
return buildAuthBootstrapFailure(auth, message);
|
|
853
850
|
}
|
|
854
|
-
await session.saveState();
|
|
855
|
-
const auth = await checkAuthDirect();
|
|
856
851
|
return {
|
|
857
852
|
...auth,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
853
|
+
success: true,
|
|
854
|
+
isLoggedIn: true,
|
|
855
|
+
message,
|
|
861
856
|
};
|
|
862
857
|
}
|
|
858
|
+
function buildAuthBootstrapFailure(auth, message) {
|
|
859
|
+
const base = auth ?? buildAuthResult(null);
|
|
860
|
+
return {
|
|
861
|
+
...base,
|
|
862
|
+
success: false,
|
|
863
|
+
isLoggedIn: false,
|
|
864
|
+
message,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
export async function checkAuthDirect() {
|
|
868
|
+
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
869
|
+
return buildAuthResult(data.consumer ?? null);
|
|
870
|
+
}
|
|
871
|
+
export async function bootstrapAuthSessionWithDeps(deps) {
|
|
872
|
+
await deps.clearBlockedBrowserImport().catch(() => { });
|
|
873
|
+
const persistedAuth = await deps.checkPersistedAuth().catch(() => null);
|
|
874
|
+
if (persistedAuth?.isLoggedIn) {
|
|
875
|
+
return buildAuthBootstrapSuccess(persistedAuth, "Already signed in with saved local DoorDash session state. No browser interaction was needed.");
|
|
876
|
+
}
|
|
877
|
+
const imported = await deps.importBrowserSessionIfAvailable().catch(() => false);
|
|
878
|
+
deps.markBrowserImportAttempted();
|
|
879
|
+
if (imported) {
|
|
880
|
+
const auth = await deps.checkAuthDirect();
|
|
881
|
+
return auth.isLoggedIn
|
|
882
|
+
? buildAuthBootstrapSuccess(auth, "Imported an existing signed-in browser session and saved it for direct API use.")
|
|
883
|
+
: buildAuthBootstrapFailure(auth, "Imported browser session state, but the consumer still appears to be logged out or guest-only.");
|
|
884
|
+
}
|
|
885
|
+
const attachedCandidates = await deps.getAttachedBrowserCdpCandidates();
|
|
886
|
+
const reachableCandidates = await deps.getReachableCdpCandidates(attachedCandidates);
|
|
887
|
+
if (reachableCandidates.length > 0) {
|
|
888
|
+
const openedAttachedBrowser = await deps.openUrlInAttachedBrowser({
|
|
889
|
+
cdpUrl: reachableCandidates[0],
|
|
890
|
+
targetUrl: AUTH_BOOTSTRAP_URL,
|
|
891
|
+
});
|
|
892
|
+
const openedDefaultBrowser = openedAttachedBrowser ? false : await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
893
|
+
deps.log(openedAttachedBrowser
|
|
894
|
+
? `Opened DoorDash in the reusable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
|
|
895
|
+
: openedDefaultBrowser
|
|
896
|
+
? `Found a reusable browser connection, but couldn't drive it directly, so I opened DoorDash in your default browser: ${AUTH_BOOTSTRAP_URL}`
|
|
897
|
+
: `Detected a reusable browser connection, but couldn't open DoorDash automatically. Open this URL in that watched browser to continue: ${AUTH_BOOTSTRAP_URL}`);
|
|
898
|
+
deps.log(`Detected ${reachableCandidates.length} reusable browser connection(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.`);
|
|
899
|
+
const importedAfterWait = await deps.waitForAttachedBrowserSessionImport({
|
|
900
|
+
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
901
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
902
|
+
});
|
|
903
|
+
const auth = await deps.checkAuthDirect();
|
|
904
|
+
if (importedAfterWait) {
|
|
905
|
+
return auth.isLoggedIn
|
|
906
|
+
? buildAuthBootstrapSuccess(auth, "Opened DoorDash in a reusable browser session, detected the signed-in consumer state, and saved it for direct API use.")
|
|
907
|
+
: buildAuthBootstrapFailure(auth, "Detected browser session state in the watched browser, but the consumer still appears logged out or guest-only.");
|
|
908
|
+
}
|
|
909
|
+
return buildAuthBootstrapFailure(auth, openedAttachedBrowser || openedDefaultBrowser
|
|
910
|
+
? `Opened DoorDash and watched reusable browser connections 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.`
|
|
911
|
+
: `Watched reusable browser connections 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.`);
|
|
912
|
+
}
|
|
913
|
+
deps.log("I couldn't find a reusable browser connection, so I'm opening a temporary Chromium login window that the CLI can watch directly.");
|
|
914
|
+
const managedAuth = await deps.waitForManagedBrowserLogin({
|
|
915
|
+
targetUrl: AUTH_BOOTSTRAP_URL,
|
|
916
|
+
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
917
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
918
|
+
});
|
|
919
|
+
if (managedAuth) {
|
|
920
|
+
return managedAuth.isLoggedIn
|
|
921
|
+
? buildAuthBootstrapSuccess(managedAuth, "Opened a temporary Chromium login window, detected the signed-in session there, and saved it for direct API use.")
|
|
922
|
+
: buildAuthBootstrapFailure(managedAuth, `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no authenticated DoorDash session was established.`);
|
|
923
|
+
}
|
|
924
|
+
const openedBrowser = await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
925
|
+
deps.log(openedBrowser
|
|
926
|
+
? `I couldn't launch the temporary Chromium login window, so I opened DoorDash in your default browser instead: ${AUTH_BOOTSTRAP_URL}`
|
|
927
|
+
: `Couldn't open the temporary Chromium login window or your default browser automatically. Open this URL to continue: ${AUTH_BOOTSTRAP_URL}`);
|
|
928
|
+
deps.log("This environment still isn't exposing a reusable browser session the CLI can import, so I won't keep you waiting for the full login timeout. Once you've exposed a compatible browser session, rerun `dd-cli login`.");
|
|
929
|
+
const importedAfterGrace = await deps.waitForAttachedBrowserSessionImport({
|
|
930
|
+
timeoutMs: AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS,
|
|
931
|
+
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
932
|
+
});
|
|
933
|
+
const auth = await deps.checkAuthDirect();
|
|
934
|
+
if (importedAfterGrace) {
|
|
935
|
+
return auth.isLoggedIn
|
|
936
|
+
? buildAuthBootstrapSuccess(auth, "A reusable browser session appeared a few seconds later and was saved for direct API use.")
|
|
937
|
+
: buildAuthBootstrapFailure(auth, "Detected browser session state after opening the browser, but the consumer still appears logged out or guest-only.");
|
|
938
|
+
}
|
|
939
|
+
return buildAuthBootstrapFailure(auth, openedBrowser
|
|
940
|
+
? "Opened DoorDash in your default browser, but this environment still isn't exposing a reusable browser session the CLI can import. Finish signing in there, make a compatible browser session discoverable, then rerun `dd-cli login`."
|
|
941
|
+
: "Couldn't open a watchable browser automatically, and this environment still isn't exposing a reusable browser session the CLI can import. Open the DoorDash home page manually, make a compatible browser session discoverable, then rerun `dd-cli login`.");
|
|
942
|
+
}
|
|
943
|
+
export async function bootstrapAuthSession() {
|
|
944
|
+
return bootstrapAuthSessionWithDeps({
|
|
945
|
+
clearBlockedBrowserImport,
|
|
946
|
+
checkPersistedAuth: getPersistedAuthDirect,
|
|
947
|
+
importBrowserSessionIfAvailable,
|
|
948
|
+
markBrowserImportAttempted: () => session.markBrowserImportAttempted(),
|
|
949
|
+
getAttachedBrowserCdpCandidates,
|
|
950
|
+
getReachableCdpCandidates,
|
|
951
|
+
openUrlInAttachedBrowser,
|
|
952
|
+
openUrlInDefaultBrowser,
|
|
953
|
+
waitForAttachedBrowserSessionImport,
|
|
954
|
+
waitForManagedBrowserLogin,
|
|
955
|
+
checkAuthDirect,
|
|
956
|
+
log: (message) => console.error(message),
|
|
957
|
+
});
|
|
958
|
+
}
|
|
863
959
|
export async function clearStoredSession() {
|
|
864
960
|
await session.close();
|
|
961
|
+
session.resetBrowserImportAttempted();
|
|
865
962
|
await rm(getCookiesPath(), { force: true }).catch(() => { });
|
|
866
963
|
await rm(getStorageStatePath(), { force: true }).catch(() => { });
|
|
964
|
+
await blockBrowserImport();
|
|
867
965
|
return {
|
|
868
966
|
success: true,
|
|
869
|
-
message: "DoorDash cookies and stored browser session state cleared.",
|
|
967
|
+
message: "DoorDash cookies and stored browser session state cleared. Automatic browser-session reuse is disabled until the next `dd-cli login`.",
|
|
870
968
|
cookiesPath: getCookiesPath(),
|
|
871
969
|
storageStatePath: getStorageStatePath(),
|
|
872
970
|
};
|
|
@@ -1538,7 +1636,7 @@ function normalizeAddressText(value) {
|
|
|
1538
1636
|
.replace(/[.,]/g, "")
|
|
1539
1637
|
.replace(/\s+/g, " ");
|
|
1540
1638
|
}
|
|
1541
|
-
|
|
1639
|
+
function isDoorDashUrl(value) {
|
|
1542
1640
|
try {
|
|
1543
1641
|
const url = new URL(value);
|
|
1544
1642
|
return url.hostname === "doordash.com" || url.hostname.endsWith(".doordash.com");
|
|
@@ -1547,13 +1645,13 @@ export function isDoorDashUrl(value) {
|
|
|
1547
1645
|
return false;
|
|
1548
1646
|
}
|
|
1549
1647
|
}
|
|
1550
|
-
|
|
1648
|
+
function hasDoorDashCookies(cookies) {
|
|
1551
1649
|
return cookies.some((cookie) => {
|
|
1552
1650
|
const domain = cookie.domain.trim().replace(/^\./, "").toLowerCase();
|
|
1553
1651
|
return domain === "doordash.com" || domain.endsWith(".doordash.com");
|
|
1554
1652
|
});
|
|
1555
1653
|
}
|
|
1556
|
-
export function
|
|
1654
|
+
export function selectAttachedBrowserImportMode(input) {
|
|
1557
1655
|
if (input.pageUrls.some((url) => isDoorDashUrl(url))) {
|
|
1558
1656
|
return "page";
|
|
1559
1657
|
}
|
|
@@ -1562,12 +1660,16 @@ export function selectManagedBrowserImportMode(input) {
|
|
|
1562
1660
|
}
|
|
1563
1661
|
return "skip";
|
|
1564
1662
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
|
|
1663
|
+
async function importBrowserSessionIfAvailable() {
|
|
1664
|
+
return await importBrowserSessionFromCdpCandidates(await getAttachedBrowserCdpCandidates());
|
|
1665
|
+
}
|
|
1666
|
+
async function importBrowserSessionFromCdpCandidates(candidates) {
|
|
1667
|
+
for (const cdpUrl of candidates) {
|
|
1567
1668
|
if (!(await isCdpEndpointReachable(cdpUrl))) {
|
|
1568
1669
|
continue;
|
|
1569
1670
|
}
|
|
1570
1671
|
let browser = null;
|
|
1672
|
+
let tempPage = null;
|
|
1571
1673
|
try {
|
|
1572
1674
|
browser = await chromium.connectOverCDP(cdpUrl);
|
|
1573
1675
|
const context = browser.contexts()[0];
|
|
@@ -1576,31 +1678,39 @@ async function importManagedBrowserSessionIfAvailable() {
|
|
|
1576
1678
|
}
|
|
1577
1679
|
const cookies = await context.cookies();
|
|
1578
1680
|
const pages = context.pages();
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
pageUrls: reusablePage ? [reusablePage.url()] : [],
|
|
1681
|
+
const importMode = selectAttachedBrowserImportMode({
|
|
1682
|
+
pageUrls: pages.map((candidate) => candidate.url()),
|
|
1582
1683
|
cookies,
|
|
1583
1684
|
});
|
|
1584
1685
|
if (importMode === "skip") {
|
|
1585
1686
|
continue;
|
|
1586
1687
|
}
|
|
1587
|
-
if (
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (consumer?.isGuest === false) {
|
|
1591
|
-
await saveContextState(context, cookies);
|
|
1688
|
+
if (importMode === "cookies") {
|
|
1689
|
+
await saveContextState(context, cookies);
|
|
1690
|
+
if (await validatePersistedDirectSessionArtifacts()) {
|
|
1592
1691
|
return true;
|
|
1593
1692
|
}
|
|
1594
1693
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1694
|
+
let page = pages.find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
|
|
1695
|
+
if (!page) {
|
|
1696
|
+
tempPage = await context.newPage();
|
|
1697
|
+
page = tempPage;
|
|
1698
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1699
|
+
await page.waitForTimeout(1_000);
|
|
1598
1700
|
}
|
|
1701
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1702
|
+
const consumer = consumerData?.consumer ?? null;
|
|
1703
|
+
if (!consumer || consumer.isGuest !== false) {
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
await saveContextState(context, cookies);
|
|
1707
|
+
return true;
|
|
1599
1708
|
}
|
|
1600
1709
|
catch {
|
|
1601
1710
|
continue;
|
|
1602
1711
|
}
|
|
1603
1712
|
finally {
|
|
1713
|
+
await tempPage?.close().catch(() => { });
|
|
1604
1714
|
await browser?.close().catch(() => { });
|
|
1605
1715
|
}
|
|
1606
1716
|
}
|
|
@@ -1619,7 +1729,7 @@ async function fetchConsumerViaPage(page) {
|
|
|
1619
1729
|
headers: GRAPHQL_HEADERS,
|
|
1620
1730
|
url: `${BASE_URL}/graphql/consumer?operation=consumer`,
|
|
1621
1731
|
});
|
|
1622
|
-
return parseGraphQlResponse("
|
|
1732
|
+
return parseGraphQlResponse("attachedBrowserConsumerImport", raw.status, raw.text);
|
|
1623
1733
|
}
|
|
1624
1734
|
async function saveContextState(context, cookies = null) {
|
|
1625
1735
|
const storageStatePath = getStorageStatePath();
|
|
@@ -1628,41 +1738,281 @@ async function saveContextState(context, cookies = null) {
|
|
|
1628
1738
|
const resolvedCookies = cookies ?? (await context.cookies());
|
|
1629
1739
|
await writeFile(getCookiesPath(), JSON.stringify(resolvedCookies, null, 2));
|
|
1630
1740
|
}
|
|
1631
|
-
async function
|
|
1741
|
+
async function getPersistedAuthDirect() {
|
|
1742
|
+
if (!(await hasPersistedSessionArtifacts())) {
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
let browser = null;
|
|
1746
|
+
let context = null;
|
|
1747
|
+
let page = null;
|
|
1748
|
+
try {
|
|
1749
|
+
browser = await chromium.launch({
|
|
1750
|
+
headless: true,
|
|
1751
|
+
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-setuid-sandbox"],
|
|
1752
|
+
});
|
|
1753
|
+
const storageStatePath = getStorageStatePath();
|
|
1754
|
+
const hasStorage = await hasStorageState();
|
|
1755
|
+
context = await browser.newContext({
|
|
1756
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
1757
|
+
locale: "en-US",
|
|
1758
|
+
viewport: { width: 1280, height: 900 },
|
|
1759
|
+
...(hasStorage ? { storageState: storageStatePath } : {}),
|
|
1760
|
+
});
|
|
1761
|
+
if (!hasStorage) {
|
|
1762
|
+
const cookies = await readStoredCookies();
|
|
1763
|
+
if (cookies.length === 0) {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
await context.addCookies(cookies);
|
|
1767
|
+
}
|
|
1768
|
+
page = await context.newPage();
|
|
1769
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1770
|
+
await page.waitForTimeout(1_000);
|
|
1771
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1772
|
+
return buildAuthResult(consumerData?.consumer ?? null);
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
finally {
|
|
1778
|
+
await page?.close().catch(() => { });
|
|
1779
|
+
await context?.close().catch(() => { });
|
|
1780
|
+
await browser?.close().catch(() => { });
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async function validatePersistedDirectSessionArtifacts() {
|
|
1784
|
+
const auth = await getPersistedAuthDirect();
|
|
1785
|
+
return auth?.isLoggedIn === true;
|
|
1786
|
+
}
|
|
1787
|
+
export function resolveAttachedBrowserCdpCandidates(env, configCandidates = []) {
|
|
1632
1788
|
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
|
-
]) {
|
|
1789
|
+
const addCandidate = (value) => {
|
|
1638
1790
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
1639
|
-
candidates.add(value
|
|
1791
|
+
candidates.add(normalizeCdpCandidate(value));
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1794
|
+
addCdpCandidatesFromList(candidates, env.DOORDASH_ATTACHED_BROWSER_CDP_URLS);
|
|
1795
|
+
addCdpCandidatesFromList(candidates, env.DOORDASH_BROWSER_CDP_URLS);
|
|
1796
|
+
addCandidate(env.DOORDASH_ATTACHED_BROWSER_CDP_URL);
|
|
1797
|
+
addCandidate(env.DOORDASH_BROWSER_CDP_URL);
|
|
1798
|
+
addCdpPortCandidatesFromList(candidates, env.DOORDASH_BROWSER_CDP_PORTS);
|
|
1799
|
+
const portCandidate = parseCdpPortCandidate(env.DOORDASH_BROWSER_CDP_PORT);
|
|
1800
|
+
if (portCandidate) {
|
|
1801
|
+
candidates.add(portCandidate);
|
|
1802
|
+
}
|
|
1803
|
+
for (const compatibilityValue of [env.DOORDASH_MANAGED_BROWSER_CDP_URL, env.OPENCLAW_BROWSER_CDP_URL, env.OPENCLAW_OPENCLAW_CDP_URL]) {
|
|
1804
|
+
addCandidate(compatibilityValue);
|
|
1805
|
+
}
|
|
1806
|
+
for (const value of configCandidates) {
|
|
1807
|
+
addCandidate(value);
|
|
1808
|
+
}
|
|
1809
|
+
addCandidate("http://127.0.0.1:18792");
|
|
1810
|
+
addCandidate("http://127.0.0.1:18800");
|
|
1811
|
+
addCandidate("http://127.0.0.1:9222");
|
|
1812
|
+
return [...candidates];
|
|
1813
|
+
}
|
|
1814
|
+
async function getAttachedBrowserCdpCandidates() {
|
|
1815
|
+
const configCandidates = await readOpenClawBrowserConfigCandidates({ profileNames: ["user", "chrome", "openclaw"] });
|
|
1816
|
+
return resolveAttachedBrowserCdpCandidates(process.env, configCandidates);
|
|
1817
|
+
}
|
|
1818
|
+
async function getReachableCdpCandidates(candidates) {
|
|
1819
|
+
const reachable = [];
|
|
1820
|
+
for (const cdpUrl of candidates) {
|
|
1821
|
+
if (await isCdpEndpointReachable(cdpUrl)) {
|
|
1822
|
+
reachable.push(cdpUrl);
|
|
1640
1823
|
}
|
|
1641
1824
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1825
|
+
return reachable;
|
|
1826
|
+
}
|
|
1827
|
+
async function waitForAttachedBrowserSessionImport(input) {
|
|
1828
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1829
|
+
while (Date.now() <= deadline) {
|
|
1830
|
+
if (await importBrowserSessionIfAvailable().catch(() => false)) {
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
if (Date.now() >= deadline) {
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
await wait(input.pollIntervalMs);
|
|
1837
|
+
}
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
export function resolveSystemBrowserOpenCommand(targetUrl, targetPlatform = process.platform) {
|
|
1841
|
+
if (targetPlatform === "darwin") {
|
|
1842
|
+
return { command: "open", args: [targetUrl] };
|
|
1843
|
+
}
|
|
1844
|
+
if (targetPlatform === "win32") {
|
|
1845
|
+
return { command: "cmd", args: ["/c", "start", "", targetUrl] };
|
|
1846
|
+
}
|
|
1847
|
+
if (["linux", "freebsd", "openbsd", "netbsd", "sunos", "android"].includes(targetPlatform)) {
|
|
1848
|
+
return { command: "xdg-open", args: [targetUrl] };
|
|
1849
|
+
}
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
async function openUrlInDefaultBrowser(targetUrl) {
|
|
1853
|
+
const command = resolveSystemBrowserOpenCommand(targetUrl);
|
|
1854
|
+
if (!command) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
return await new Promise((resolve) => {
|
|
1858
|
+
const child = spawn(command.command, command.args, {
|
|
1859
|
+
detached: process.platform !== "win32",
|
|
1860
|
+
stdio: "ignore",
|
|
1861
|
+
});
|
|
1862
|
+
child.once("error", () => resolve(false));
|
|
1863
|
+
child.once("spawn", () => {
|
|
1864
|
+
child.unref();
|
|
1865
|
+
resolve(true);
|
|
1866
|
+
});
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
async function openUrlInAttachedBrowser(input) {
|
|
1870
|
+
let browser = null;
|
|
1871
|
+
try {
|
|
1872
|
+
browser = await chromium.connectOverCDP(input.cdpUrl);
|
|
1873
|
+
const context = browser.contexts()[0];
|
|
1874
|
+
if (!context) {
|
|
1875
|
+
return false;
|
|
1876
|
+
}
|
|
1877
|
+
let page = context.pages().find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
|
|
1878
|
+
if (!page) {
|
|
1879
|
+
page = await context.newPage();
|
|
1880
|
+
}
|
|
1881
|
+
await page.goto(input.targetUrl, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1882
|
+
await page.bringToFront().catch(() => { });
|
|
1883
|
+
return true;
|
|
1884
|
+
}
|
|
1885
|
+
catch {
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
finally {
|
|
1889
|
+
await browser?.close().catch(() => { });
|
|
1644
1890
|
}
|
|
1645
|
-
candidates.add("http://127.0.0.1:18800");
|
|
1646
|
-
return [...candidates];
|
|
1647
1891
|
}
|
|
1648
|
-
async function
|
|
1892
|
+
async function waitForManagedBrowserLogin(input) {
|
|
1893
|
+
let browser = null;
|
|
1894
|
+
let context = null;
|
|
1895
|
+
let page = null;
|
|
1896
|
+
try {
|
|
1897
|
+
browser = await chromium.launch({
|
|
1898
|
+
headless: false,
|
|
1899
|
+
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-setuid-sandbox"],
|
|
1900
|
+
});
|
|
1901
|
+
const storageStatePath = getStorageStatePath();
|
|
1902
|
+
const hasStorage = await hasStorageState();
|
|
1903
|
+
context = await browser.newContext({
|
|
1904
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
1905
|
+
locale: "en-US",
|
|
1906
|
+
viewport: { width: 1280, height: 900 },
|
|
1907
|
+
...(hasStorage ? { storageState: storageStatePath } : {}),
|
|
1908
|
+
});
|
|
1909
|
+
if (!hasStorage) {
|
|
1910
|
+
const cookies = await readStoredCookies();
|
|
1911
|
+
if (cookies.length > 0) {
|
|
1912
|
+
await context.addCookies(cookies);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
page = await context.newPage();
|
|
1916
|
+
await page.goto(input.targetUrl, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1917
|
+
await page.bringToFront().catch(() => { });
|
|
1918
|
+
const deadline = Date.now() + input.timeoutMs;
|
|
1919
|
+
while (Date.now() <= deadline) {
|
|
1920
|
+
await saveContextState(context).catch(() => { });
|
|
1921
|
+
const auth = await getPersistedAuthDirect();
|
|
1922
|
+
if (auth?.isLoggedIn) {
|
|
1923
|
+
return auth;
|
|
1924
|
+
}
|
|
1925
|
+
if (page.isClosed()) {
|
|
1926
|
+
break;
|
|
1927
|
+
}
|
|
1928
|
+
if (Date.now() >= deadline) {
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
await wait(input.pollIntervalMs);
|
|
1932
|
+
}
|
|
1933
|
+
await saveContextState(context).catch(() => { });
|
|
1934
|
+
return (await getPersistedAuthDirect()) ?? buildAuthResult(null);
|
|
1935
|
+
}
|
|
1936
|
+
catch (error) {
|
|
1937
|
+
if (isPlaywrightBrowserInstallMissingError(error)) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
finally {
|
|
1943
|
+
await page?.close().catch(() => { });
|
|
1944
|
+
await context?.close().catch(() => { });
|
|
1945
|
+
await browser?.close().catch(() => { });
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
function isPlaywrightBrowserInstallMissingError(error) {
|
|
1949
|
+
if (!(error instanceof Error)) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
const message = error.message.toLowerCase();
|
|
1953
|
+
return message.includes("executable doesn't exist") || message.includes("please run the following command") || message.includes("playwright install");
|
|
1954
|
+
}
|
|
1955
|
+
function normalizeCdpCandidate(value) {
|
|
1956
|
+
return value.trim().replace(/\/$/, "");
|
|
1957
|
+
}
|
|
1958
|
+
function addCdpCandidatesFromList(candidates, value) {
|
|
1959
|
+
if (!value) {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
for (const entry of value.split(/[,\n]/)) {
|
|
1963
|
+
const trimmed = entry.trim();
|
|
1964
|
+
if (trimmed) {
|
|
1965
|
+
candidates.add(normalizeCdpCandidate(trimmed));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function addCdpPortCandidatesFromList(candidates, value) {
|
|
1970
|
+
if (!value) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
for (const entry of value.split(/[,\n]/)) {
|
|
1974
|
+
const portCandidate = parseCdpPortCandidate(entry.trim());
|
|
1975
|
+
if (portCandidate) {
|
|
1976
|
+
candidates.add(portCandidate);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function parseCdpPortCandidate(value) {
|
|
1981
|
+
if (!value) {
|
|
1982
|
+
return null;
|
|
1983
|
+
}
|
|
1984
|
+
const parsed = Number.parseInt(value, 10);
|
|
1985
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
return `http://127.0.0.1:${parsed}`;
|
|
1989
|
+
}
|
|
1990
|
+
function appendBrowserConfigCandidate(candidates, value) {
|
|
1991
|
+
const object = asObject(value);
|
|
1992
|
+
if (typeof object.cdpUrl === "string" && object.cdpUrl.trim()) {
|
|
1993
|
+
candidates.push(normalizeCdpCandidate(object.cdpUrl));
|
|
1994
|
+
}
|
|
1995
|
+
else if (typeof object.cdpPort === "number" && Number.isInteger(object.cdpPort)) {
|
|
1996
|
+
candidates.push(`http://127.0.0.1:${object.cdpPort}`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function wait(ms) {
|
|
2000
|
+
return new Promise((resolve) => {
|
|
2001
|
+
setTimeout(resolve, ms);
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
async function readOpenClawBrowserConfigCandidates(input) {
|
|
1649
2005
|
try {
|
|
1650
2006
|
const raw = await readFile(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
|
|
1651
2007
|
const parsed = safeJsonParse(raw);
|
|
1652
2008
|
const browserConfig = asObject(parsed?.browser);
|
|
2009
|
+
const profiles = asObject(browserConfig.profiles);
|
|
1653
2010
|
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);
|
|
2011
|
+
appendBrowserConfigCandidate(candidates, browserConfig);
|
|
2012
|
+
appendBrowserConfigCandidate(candidates, browserConfig.openclaw);
|
|
2013
|
+
for (const profileName of input.profileNames) {
|
|
2014
|
+
appendBrowserConfigCandidate(candidates, profiles[profileName]);
|
|
2015
|
+
}
|
|
1666
2016
|
return dedupeBy(candidates, (value) => value);
|
|
1667
2017
|
}
|
|
1668
2018
|
catch {
|
|
@@ -2306,6 +2656,22 @@ function truncate(value, length) {
|
|
|
2306
2656
|
async function ensureConfigDir() {
|
|
2307
2657
|
await mkdir(dirname(getCookiesPath()), { recursive: true });
|
|
2308
2658
|
}
|
|
2659
|
+
async function hasBlockedBrowserImport() {
|
|
2660
|
+
try {
|
|
2661
|
+
await readFile(getBrowserImportBlockPath(), "utf8");
|
|
2662
|
+
return true;
|
|
2663
|
+
}
|
|
2664
|
+
catch {
|
|
2665
|
+
return false;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async function blockBrowserImport() {
|
|
2669
|
+
await ensureConfigDir();
|
|
2670
|
+
await writeFile(getBrowserImportBlockPath(), "logged-out\n");
|
|
2671
|
+
}
|
|
2672
|
+
async function clearBlockedBrowserImport() {
|
|
2673
|
+
await rm(getBrowserImportBlockPath(), { force: true }).catch(() => { });
|
|
2674
|
+
}
|
|
2309
2675
|
async function hasStorageState() {
|
|
2310
2676
|
try {
|
|
2311
2677
|
await readFile(getStorageStatePath(), "utf8");
|