doordash-cli 0.3.2 → 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.
@@ -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 } from "@striderlabs/mcp-doordash/dist/auth.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
- attemptedManagedImport = false;
703
+ attemptedBrowserImport = false;
701
704
  async init(options = {}) {
702
705
  if (this.page) {
703
706
  return this.page;
704
707
  }
705
- await this.maybeImportManagedBrowserSession();
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
- async maybeImportManagedBrowserSession() {
807
- if (this.attemptedManagedImport) {
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.attemptedManagedImport = true;
811
- if (await hasPersistedSessionArtifacts()) {
819
+ this.attemptedBrowserImport = true;
820
+ if (await hasBlockedBrowserImport()) {
812
821
  return;
813
822
  }
814
- await importManagedBrowserSessionIfAvailable().catch(() => { });
823
+ await importBrowserSessionIfAvailable().catch(() => { });
815
824
  }
816
825
  }
817
826
  const session = new DoorDashDirectSession();
818
- export async function checkAuthDirect() {
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
- 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
- }
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
- 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.",
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
- export function isDoorDashUrl(value) {
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
- export function hasDoorDashCookies(cookies) {
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 selectManagedBrowserImportMode(input) {
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 importManagedBrowserSessionIfAvailable() {
1566
- for (const cdpUrl of await getManagedBrowserCdpCandidates()) {
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 reusablePage = pages.find((candidate) => isDoorDashUrl(candidate.url())) ?? null;
1580
- const importMode = selectManagedBrowserImportMode({
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 (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);
1688
+ if (importMode === "cookies") {
1689
+ await saveContextState(context, cookies);
1690
+ if (await validatePersistedDirectSessionArtifacts()) {
1592
1691
  return true;
1593
1692
  }
1594
1693
  }
1595
- if (hasDoorDashCookies(cookies)) {
1596
- await saveContextState(context, cookies);
1597
- return true;
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("managedBrowserConsumerImport", raw.status, raw.text);
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 getManagedBrowserCdpCandidates() {
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
- 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
- ]) {
1789
+ const addCandidate = (value) => {
1638
1790
  if (typeof value === "string" && value.trim().length > 0) {
1639
- candidates.add(value.trim().replace(/\/$/, ""));
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
- for (const value of await readOpenClawBrowserConfigCandidates()) {
1643
- candidates.add(value.replace(/\/$/, ""));
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);
1644
1837
  }
1645
- candidates.add("http://127.0.0.1:18800");
1646
- return [...candidates];
1838
+ return false;
1647
1839
  }
1648
- async function readOpenClawBrowserConfigCandidates() {
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(() => { });
1890
+ }
1891
+ }
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
- 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);
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");
@@ -2331,6 +2697,3 @@ async function readStoredCookies() {
2331
2697
  return [];
2332
2698
  }
2333
2699
  }
2334
- export function getStorageStatePath() {
2335
- return join(dirname(getCookiesPath()), "storage-state.json");
2336
- }