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.
@@ -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/promises";
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
- attemptedManagedImport = false;
706
+ attemptedBrowserImport = false;
701
707
  async init(options = {}) {
702
708
  if (this.page) {
703
709
  return this.page;
704
710
  }
705
- await this.maybeImportManagedBrowserSession();
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
- async maybeImportManagedBrowserSession() {
807
- if (this.attemptedManagedImport) {
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.attemptedManagedImport = true;
811
- if (await hasPersistedSessionArtifacts()) {
822
+ this.attemptedBrowserImport = true;
823
+ if (await hasBlockedBrowserImport()) {
812
824
  return;
813
825
  }
814
- await importManagedBrowserSessionIfAvailable().catch(() => { });
826
+ await importBrowserSessionIfAvailable().catch(() => { });
815
827
  }
816
828
  }
817
829
  const session = new DoorDashDirectSession();
818
- export async function checkAuthDirect() {
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
- export async function bootstrapAuthSession() {
841
- const page = await session.init({ headed: true });
842
- console.error("A Chromium window is open for DoorDash session bootstrap.");
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
- message: auth.isLoggedIn
859
- ? "DoorDash session saved for direct API use."
860
- : "DoorDash session state saved, but the consumer still appears to be logged out or guest-only.",
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
- export function isDoorDashUrl(value) {
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
- export function hasDoorDashCookies(cookies) {
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 selectManagedBrowserImportMode(input) {
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 importManagedBrowserSessionIfAvailable() {
1566
- for (const cdpUrl of await getManagedBrowserCdpCandidates()) {
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 reusablePage = pages.find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
1580
- const importMode = selectManagedBrowserImportMode({
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 (reusablePage) {
1588
- const consumerData = await fetchConsumerViaPage(reusablePage).catch(() => null);
1589
- const consumer = consumerData?.consumer ?? null;
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
- if (hasDoorDashCookies(cookies)) {
1596
- await saveContextState(context, cookies);
1597
- return true;
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("managedBrowserConsumerImport", raw.status, raw.text);
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 getManagedBrowserCdpCandidates() {
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
- for (const value of [
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.trim().replace(/\/$/, ""));
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
- for (const value of await readOpenClawBrowserConfigCandidates()) {
1643
- candidates.add(value.replace(/\/$/, ""));
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
- candidates.add("http://127.0.0.1:18800");
1646
- return [...candidates];
1913
+ return false;
1647
1914
  }
1648
- async function readOpenClawBrowserConfigCandidates() {
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
- const pushCandidate = (value) => {
1655
- const object = asObject(value);
1656
- if (typeof object.cdpUrl === "string" && object.cdpUrl.trim()) {
1657
- candidates.push(object.cdpUrl.trim());
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");