doordash-cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir } from "node:os";
5
+ import { createInterface } from "node:readline";
5
6
  import { chromium } from "playwright";
6
7
  import { getBrowserImportBlockPath, getCookiesPath, getStorageStatePath } from "./session-storage.js";
7
8
  const BASE_URL = "https://www.doordash.com";
@@ -9,7 +10,168 @@ const AUTH_BOOTSTRAP_URL = `${BASE_URL}/home`;
9
10
  const AUTH_BOOTSTRAP_TIMEOUT_MS = 180_000;
10
11
  const AUTH_BOOTSTRAP_POLL_INTERVAL_MS = 2_000;
11
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;
12
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";
16
+ const LINUX_CHROMIUM_COOKIE_IMPORT_SCRIPT = String.raw `
17
+ import json
18
+ import os
19
+ import sqlite3
20
+ import hashlib
21
+ import sys
22
+
23
+ try:
24
+ import secretstorage
25
+ from Crypto.Cipher import AES
26
+ except Exception as exc:
27
+ raise SystemExit(f"missing python support for Chromium cookie import: {exc}")
28
+
29
+ browser_label = os.environ.get("DD_BROWSER_LABEL", "Chromium")
30
+ user_data_dir = os.environ.get("DD_BROWSER_USER_DATA_DIR", "")
31
+ safe_storage_application = os.environ.get("DD_BROWSER_SAFE_STORAGE_APP", "")
32
+
33
+ if not user_data_dir or not safe_storage_application:
34
+ raise SystemExit("missing browser metadata")
35
+
36
+
37
+ def chromium_utc_to_unix_seconds(value):
38
+ if not value:
39
+ return -1
40
+ return max(-1, (int(value) / 1000000) - 11644473600)
41
+
42
+
43
+ def samesite_to_playwright(value):
44
+ mapping = {
45
+ -1: "Lax",
46
+ 0: "None",
47
+ 1: "Lax",
48
+ 2: "Strict",
49
+ }
50
+ try:
51
+ return mapping.get(int(value), "Lax")
52
+ except Exception:
53
+ return "Lax"
54
+
55
+
56
+ def get_safe_storage_secret(application):
57
+ bus = secretstorage.dbus_init()
58
+ for collection in secretstorage.get_all_collections(bus):
59
+ for item in collection.get_all_items():
60
+ attrs = item.get_attributes()
61
+ if attrs.get("xdg:schema") == "chrome_libsecret_os_crypt_password_v2" and attrs.get("application") == application:
62
+ secret = item.get_secret()
63
+ return secret.decode("utf-8") if isinstance(secret, bytes) else str(secret)
64
+ return None
65
+
66
+
67
+ def decrypt_cookie_value(encrypted_value, host_key, secret):
68
+ payload = encrypted_value[3:] if encrypted_value.startswith((b"v10", b"v11")) else encrypted_value
69
+ key = hashlib.pbkdf2_hmac("sha1", secret.encode("utf-8"), b"saltysalt", 1, dklen=16)
70
+ decrypted = AES.new(key, AES.MODE_CBC, b" " * 16).decrypt(payload)
71
+ pad = decrypted[-1]
72
+ if isinstance(pad, str):
73
+ pad = ord(pad)
74
+ if 1 <= pad <= 16:
75
+ decrypted = decrypted[:-pad]
76
+ host_hash = hashlib.sha256(host_key.encode("utf-8")).digest()
77
+ if decrypted.startswith(host_hash):
78
+ decrypted = decrypted[len(host_hash):]
79
+ return decrypted.decode("utf-8")
80
+
81
+
82
+ def profile_names_for_user_data_dir(root):
83
+ local_state_path = os.path.join(root, "Local State")
84
+ entries = []
85
+ try:
86
+ with open(local_state_path, "r", encoding="utf-8") as handle:
87
+ info_cache = ((json.load(handle).get("profile") or {}).get("info_cache") or {})
88
+ for profile_name, info in info_cache.items():
89
+ if not isinstance(profile_name, str) or not profile_name.strip():
90
+ continue
91
+ active_time = 0.0
92
+ if isinstance(info, dict):
93
+ try:
94
+ active_time = float(info.get("active_time") or 0)
95
+ except Exception:
96
+ active_time = 0.0
97
+ entries.append((0 if profile_name == "Default" else 1, -active_time, profile_name))
98
+ except Exception:
99
+ pass
100
+
101
+ entries.sort()
102
+ names = [profile_name for _, _, profile_name in entries]
103
+ if "Default" not in names:
104
+ names.append("Default")
105
+ return names
106
+
107
+
108
+ safe_storage_secret = get_safe_storage_secret(safe_storage_application)
109
+ if not safe_storage_secret:
110
+ print("[]")
111
+ raise SystemExit(0)
112
+
113
+ imports = []
114
+ for profile_name in profile_names_for_user_data_dir(user_data_dir):
115
+ cookies_db_path = os.path.join(user_data_dir, profile_name, "Cookies")
116
+ if not os.path.exists(cookies_db_path):
117
+ continue
118
+
119
+ connection = None
120
+ try:
121
+ connection = sqlite3.connect(f"file:{cookies_db_path}?mode=ro", uri=True)
122
+ cursor = connection.cursor()
123
+ rows = cursor.execute(
124
+ """
125
+ select host_key, name, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite
126
+ from cookies
127
+ where host_key like '%doordash%'
128
+ order by host_key, name
129
+ """
130
+ ).fetchall()
131
+ except Exception:
132
+ if connection is not None:
133
+ connection.close()
134
+ continue
135
+
136
+ cookies = []
137
+ for host_key, name, encrypted_value, path, expires_utc, is_secure, is_httponly, samesite in rows:
138
+ try:
139
+ decrypted_value = decrypt_cookie_value(encrypted_value, host_key, safe_storage_secret)
140
+ except Exception:
141
+ continue
142
+ cookies.append({
143
+ "name": name,
144
+ "value": decrypted_value,
145
+ "domain": host_key,
146
+ "path": path or "/",
147
+ "expires": chromium_utc_to_unix_seconds(expires_utc),
148
+ "httpOnly": bool(is_httponly),
149
+ "secure": bool(is_secure),
150
+ "sameSite": samesite_to_playwright(samesite),
151
+ })
152
+
153
+ connection.close()
154
+ if cookies:
155
+ imports.append({
156
+ "browserLabel": browser_label,
157
+ "profileName": profile_name,
158
+ "cookies": cookies,
159
+ })
160
+
161
+ print(json.dumps(imports))
162
+ `;
163
+ const LINUX_CHROMIUM_COOKIE_IMPORT_BROWSERS = [
164
+ {
165
+ browserLabel: "Brave",
166
+ safeStorageApplication: "brave",
167
+ userDataDir: join(homedir(), ".config", "BraveSoftware", "Brave-Browser"),
168
+ },
169
+ {
170
+ browserLabel: "Google Chrome",
171
+ safeStorageApplication: "chrome",
172
+ userDataDir: join(homedir(), ".config", "google-chrome"),
173
+ },
174
+ ];
13
175
  const GRAPHQL_HEADERS = {
14
176
  accept: "*/*",
15
177
  "content-type": "application/json",
@@ -864,6 +1026,42 @@ function buildAuthBootstrapFailure(auth, message) {
864
1026
  message,
865
1027
  };
866
1028
  }
1029
+ function canPromptForManagedBrowserConfirmation() {
1030
+ return Boolean(process.stdin.isTTY);
1031
+ }
1032
+ function createManagedBrowserManualConfirmationHandle() {
1033
+ if (!canPromptForManagedBrowserConfirmation()) {
1034
+ return {
1035
+ isEnabled: false,
1036
+ consumeRequested: () => false,
1037
+ close: () => { },
1038
+ };
1039
+ }
1040
+ const rl = createInterface({
1041
+ input: process.stdin,
1042
+ output: process.stderr,
1043
+ terminal: true,
1044
+ });
1045
+ let requested = false;
1046
+ const onLine = () => {
1047
+ requested = true;
1048
+ };
1049
+ rl.on("line", onLine);
1050
+ return {
1051
+ isEnabled: true,
1052
+ consumeRequested: () => {
1053
+ if (!requested) {
1054
+ return false;
1055
+ }
1056
+ requested = false;
1057
+ return true;
1058
+ },
1059
+ close: () => {
1060
+ rl.off("line", onLine);
1061
+ rl.close();
1062
+ },
1063
+ };
1064
+ }
867
1065
  export async function checkAuthDirect() {
868
1066
  const data = await session.graphql("consumer", CONSUMER_QUERY, {});
869
1067
  return buildAuthResult(data.consumer ?? null);
@@ -891,11 +1089,11 @@ export async function bootstrapAuthSessionWithDeps(deps) {
891
1089
  });
892
1090
  const openedDefaultBrowser = openedAttachedBrowser ? false : await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
893
1091
  deps.log(openedAttachedBrowser
894
- ? `Opened DoorDash in the reusable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
1092
+ ? `Opened DoorDash in the attachable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
895
1093
  : 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.`);
1094
+ ? `Found an attachable browser session, but couldn't drive it directly, so I opened DoorDash in your default browser: ${AUTH_BOOTSTRAP_URL}`
1095
+ : `Detected an attachable browser session, but couldn't open DoorDash automatically. Open this URL in that watched browser to continue: ${AUTH_BOOTSTRAP_URL}`);
1096
+ 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.`);
899
1097
  const importedAfterWait = await deps.waitForAttachedBrowserSessionImport({
900
1098
  timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
901
1099
  pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
@@ -903,29 +1101,43 @@ export async function bootstrapAuthSessionWithDeps(deps) {
903
1101
  const auth = await deps.checkAuthDirect();
904
1102
  if (importedAfterWait) {
905
1103
  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.")
1104
+ ? buildAuthBootstrapSuccess(auth, "Opened DoorDash in an attachable browser session, detected the signed-in consumer state, and saved it for direct API use.")
907
1105
  : buildAuthBootstrapFailure(auth, "Detected browser session state in the watched browser, but the consumer still appears logged out or guest-only.");
908
1106
  }
909
1107
  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.");
1108
+ ? `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.`
1109
+ : `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.`);
1110
+ }
1111
+ const desktopBrowserReuseGap = await deps.describeDesktopBrowserReuseGap().catch(() => null);
1112
+ if (desktopBrowserReuseGap) {
1113
+ deps.log(desktopBrowserReuseGap);
1114
+ }
1115
+ const manualManagedConfirmationAvailable = deps.canPromptForManagedBrowserConfirmation();
1116
+ 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.");
1117
+ deps.log(manualManagedConfirmationAvailable
1118
+ ? `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.`
1119
+ : `Finish signing in in that window. I'll keep checking automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
914
1120
  const managedAuth = await deps.waitForManagedBrowserLogin({
915
1121
  targetUrl: AUTH_BOOTSTRAP_URL,
916
1122
  timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
917
1123
  pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
1124
+ log: deps.log,
918
1125
  });
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.`);
1126
+ if (managedAuth.status === "completed") {
1127
+ return buildAuthBootstrapSuccess(managedAuth.auth, managedAuth.completion === "manual"
1128
+ ? "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."
1129
+ : "Opened a temporary Chromium login window, detected the signed-in session there automatically, and saved it for direct API use.");
1130
+ }
1131
+ if (managedAuth.status === "timed-out") {
1132
+ return buildAuthBootstrapFailure(managedAuth.auth, manualManagedConfirmationAvailable
1133
+ ? `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.`
1134
+ : `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
1135
  }
924
1136
  const openedBrowser = await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
925
1137
  deps.log(openedBrowser
926
1138
  ? `I couldn't launch the temporary Chromium login window, so I opened DoorDash in your default browser instead: ${AUTH_BOOTSTRAP_URL}`
927
1139
  : `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`.");
1140
+ 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`.");
929
1141
  const importedAfterGrace = await deps.waitForAttachedBrowserSessionImport({
930
1142
  timeoutMs: AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS,
931
1143
  pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
@@ -933,12 +1145,12 @@ export async function bootstrapAuthSessionWithDeps(deps) {
933
1145
  const auth = await deps.checkAuthDirect();
934
1146
  if (importedAfterGrace) {
935
1147
  return auth.isLoggedIn
936
- ? buildAuthBootstrapSuccess(auth, "A reusable browser session appeared a few seconds later and was saved for direct API use.")
1148
+ ? buildAuthBootstrapSuccess(auth, "An attachable browser session appeared a few seconds later and was saved for direct API use.")
937
1149
  : buildAuthBootstrapFailure(auth, "Detected browser session state after opening the browser, but the consumer still appears logged out or guest-only.");
938
1150
  }
939
1151
  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`.");
1152
+ ? "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`."
1153
+ : "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`.");
942
1154
  }
943
1155
  export async function bootstrapAuthSession() {
944
1156
  return bootstrapAuthSessionWithDeps({
@@ -948,10 +1160,12 @@ export async function bootstrapAuthSession() {
948
1160
  markBrowserImportAttempted: () => session.markBrowserImportAttempted(),
949
1161
  getAttachedBrowserCdpCandidates,
950
1162
  getReachableCdpCandidates,
1163
+ describeDesktopBrowserReuseGap,
951
1164
  openUrlInAttachedBrowser,
952
1165
  openUrlInDefaultBrowser,
953
1166
  waitForAttachedBrowserSessionImport,
954
1167
  waitForManagedBrowserLogin,
1168
+ canPromptForManagedBrowserConfirmation,
955
1169
  checkAuthDirect,
956
1170
  log: (message) => console.error(message),
957
1171
  });
@@ -1660,8 +1874,45 @@ export function selectAttachedBrowserImportMode(input) {
1660
1874
  }
1661
1875
  return "skip";
1662
1876
  }
1877
+ export function preferredBrowserSessionImportStrategies(platform) {
1878
+ return platform === "linux"
1879
+ ? ["local-linux-chromium-profile", "attached-browser-cdp"]
1880
+ : ["attached-browser-cdp"];
1881
+ }
1663
1882
  async function importBrowserSessionIfAvailable() {
1664
- return await importBrowserSessionFromCdpCandidates(await getAttachedBrowserCdpCandidates());
1883
+ for (const strategy of preferredBrowserSessionImportStrategies(process.platform)) {
1884
+ if (strategy === "local-linux-chromium-profile") {
1885
+ if (await importBrowserSessionFromLocalChromiumProfiles()) {
1886
+ return true;
1887
+ }
1888
+ continue;
1889
+ }
1890
+ if (await importBrowserSessionFromCdpCandidates(await getAttachedBrowserCdpCandidates())) {
1891
+ return true;
1892
+ }
1893
+ }
1894
+ return false;
1895
+ }
1896
+ async function importBrowserSessionFromLocalChromiumProfiles() {
1897
+ if (process.platform !== "linux") {
1898
+ return false;
1899
+ }
1900
+ const originalArtifacts = await snapshotStoredSessionArtifacts();
1901
+ for (const browser of LINUX_CHROMIUM_COOKIE_IMPORT_BROWSERS) {
1902
+ const profileImports = await readLinuxChromiumCookieImports(browser);
1903
+ for (const profileImport of profileImports) {
1904
+ if (!hasDoorDashCookies(profileImport.cookies)) {
1905
+ continue;
1906
+ }
1907
+ await writeStoredSessionArtifacts(profileImport.cookies);
1908
+ const persistedAuth = await getPersistedAuthDirect();
1909
+ if (persistedAuth?.isLoggedIn) {
1910
+ return true;
1911
+ }
1912
+ await restoreStoredSessionArtifacts(originalArtifacts);
1913
+ }
1914
+ }
1915
+ return false;
1665
1916
  }
1666
1917
  async function importBrowserSessionFromCdpCandidates(candidates) {
1667
1918
  for (const cdpUrl of candidates) {
@@ -1671,7 +1922,7 @@ async function importBrowserSessionFromCdpCandidates(candidates) {
1671
1922
  let browser = null;
1672
1923
  let tempPage = null;
1673
1924
  try {
1674
- browser = await chromium.connectOverCDP(cdpUrl);
1925
+ browser = await chromium.connectOverCDP(cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
1675
1926
  const context = browser.contexts()[0];
1676
1927
  if (!context) {
1677
1928
  continue;
@@ -1731,6 +1982,26 @@ async function fetchConsumerViaPage(page) {
1731
1982
  });
1732
1983
  return parseGraphQlResponse("attachedBrowserConsumerImport", raw.status, raw.text);
1733
1984
  }
1985
+ async function readLiveManagedBrowserAuth(page) {
1986
+ if (!page || page.isClosed()) {
1987
+ return buildAuthResult(null);
1988
+ }
1989
+ const consumerData = await fetchConsumerViaPage(page).catch(() => null);
1990
+ return buildAuthResult(consumerData?.consumer ?? null);
1991
+ }
1992
+ async function confirmManagedBrowserAuth(context, page) {
1993
+ const liveAuth = await readLiveManagedBrowserAuth(page);
1994
+ if (liveAuth.isLoggedIn) {
1995
+ if (context) {
1996
+ await saveContextState(context).catch(() => { });
1997
+ }
1998
+ return liveAuth;
1999
+ }
2000
+ if (context) {
2001
+ await saveContextState(context).catch(() => { });
2002
+ }
2003
+ return (await getPersistedAuthDirect().catch(() => null)) ?? liveAuth;
2004
+ }
1734
2005
  async function saveContextState(context, cookies = null) {
1735
2006
  const storageStatePath = getStorageStatePath();
1736
2007
  await ensureConfigDir();
@@ -1742,42 +2013,16 @@ async function getPersistedAuthDirect() {
1742
2013
  if (!(await hasPersistedSessionArtifacts())) {
1743
2014
  return null;
1744
2015
  }
1745
- let browser = null;
1746
- let context = null;
1747
- let page = null;
2016
+ await session.close().catch(() => { });
2017
+ session.markBrowserImportAttempted();
1748
2018
  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);
2019
+ return await checkAuthDirect();
1773
2020
  }
1774
2021
  catch {
1775
2022
  return null;
1776
2023
  }
1777
2024
  finally {
1778
- await page?.close().catch(() => { });
1779
- await context?.close().catch(() => { });
1780
- await browser?.close().catch(() => { });
2025
+ await session.close().catch(() => { });
1781
2026
  }
1782
2027
  }
1783
2028
  async function validatePersistedDirectSessionArtifacts() {
@@ -1869,7 +2114,7 @@ async function openUrlInDefaultBrowser(targetUrl) {
1869
2114
  async function openUrlInAttachedBrowser(input) {
1870
2115
  let browser = null;
1871
2116
  try {
1872
- browser = await chromium.connectOverCDP(input.cdpUrl);
2117
+ browser = await chromium.connectOverCDP(input.cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
1873
2118
  const context = browser.contexts()[0];
1874
2119
  if (!context) {
1875
2120
  return false;
@@ -1893,6 +2138,7 @@ async function waitForManagedBrowserLogin(input) {
1893
2138
  let browser = null;
1894
2139
  let context = null;
1895
2140
  let page = null;
2141
+ const manualConfirmation = createManagedBrowserManualConfirmationHandle();
1896
2142
  try {
1897
2143
  browser = await chromium.launch({
1898
2144
  headless: false,
@@ -1917,10 +2163,25 @@ async function waitForManagedBrowserLogin(input) {
1917
2163
  await page.bringToFront().catch(() => { });
1918
2164
  const deadline = Date.now() + input.timeoutMs;
1919
2165
  while (Date.now() <= deadline) {
1920
- await saveContextState(context).catch(() => { });
1921
- const auth = await getPersistedAuthDirect();
1922
- if (auth?.isLoggedIn) {
1923
- return auth;
2166
+ const liveAuth = await readLiveManagedBrowserAuth(page);
2167
+ if (liveAuth.isLoggedIn) {
2168
+ await saveContextState(context).catch(() => { });
2169
+ return {
2170
+ status: "completed",
2171
+ completion: "automatic",
2172
+ auth: liveAuth,
2173
+ };
2174
+ }
2175
+ if (manualConfirmation.consumeRequested()) {
2176
+ const confirmedAuth = await confirmManagedBrowserAuth(context, page);
2177
+ if (confirmedAuth.isLoggedIn) {
2178
+ return {
2179
+ status: "completed",
2180
+ completion: "manual",
2181
+ auth: confirmedAuth,
2182
+ };
2183
+ }
2184
+ 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.");
1924
2185
  }
1925
2186
  if (page.isClosed()) {
1926
2187
  break;
@@ -1930,16 +2191,27 @@ async function waitForManagedBrowserLogin(input) {
1930
2191
  }
1931
2192
  await wait(input.pollIntervalMs);
1932
2193
  }
1933
- await saveContextState(context).catch(() => { });
1934
- return (await getPersistedAuthDirect()) ?? buildAuthResult(null);
2194
+ const finalAuth = await confirmManagedBrowserAuth(context, page);
2195
+ if (finalAuth.isLoggedIn) {
2196
+ return {
2197
+ status: "completed",
2198
+ completion: "automatic",
2199
+ auth: finalAuth,
2200
+ };
2201
+ }
2202
+ return {
2203
+ status: "timed-out",
2204
+ auth: finalAuth,
2205
+ };
1935
2206
  }
1936
2207
  catch (error) {
1937
2208
  if (isPlaywrightBrowserInstallMissingError(error)) {
1938
- return null;
2209
+ return { status: "launch-failed" };
1939
2210
  }
1940
- return null;
2211
+ return { status: "launch-failed" };
1941
2212
  }
1942
2213
  finally {
2214
+ manualConfirmation.close();
1943
2215
  await page?.close().catch(() => { });
1944
2216
  await context?.close().catch(() => { });
1945
2217
  await browser?.close().catch(() => { });
@@ -1952,6 +2224,40 @@ function isPlaywrightBrowserInstallMissingError(error) {
1952
2224
  const message = error.message.toLowerCase();
1953
2225
  return message.includes("executable doesn't exist") || message.includes("please run the following command") || message.includes("playwright install");
1954
2226
  }
2227
+ const KNOWN_DESKTOP_BROWSERS = [
2228
+ {
2229
+ label: "Brave",
2230
+ processMatchers: [/\bbrave-browser(?:-stable)?\b/i, /\/brave(?:$|\s)/i],
2231
+ devToolsActivePortPaths: [
2232
+ join(homedir(), ".config", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
2233
+ join(homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
2234
+ ],
2235
+ },
2236
+ {
2237
+ label: "Google Chrome",
2238
+ processMatchers: [/\bgoogle-chrome(?:-stable|-beta|-unstable)?\b/i, /\/google-chrome(?:$|\s)/i],
2239
+ devToolsActivePortPaths: [
2240
+ join(homedir(), ".config", "google-chrome", "DevToolsActivePort"),
2241
+ join(homedir(), "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
2242
+ ],
2243
+ },
2244
+ {
2245
+ label: "Microsoft Edge",
2246
+ processMatchers: [/\bmicrosoft-edge(?:-stable|-beta|-dev)?\b/i, /\/microsoft-edge(?:$|\s)/i],
2247
+ devToolsActivePortPaths: [
2248
+ join(homedir(), ".config", "microsoft-edge", "DevToolsActivePort"),
2249
+ join(homedir(), "Library", "Application Support", "Microsoft Edge", "DevToolsActivePort"),
2250
+ ],
2251
+ },
2252
+ {
2253
+ label: "Chromium",
2254
+ processMatchers: [/\bchromium-browser\b/i, /\bchromium\b/i],
2255
+ devToolsActivePortPaths: [
2256
+ join(homedir(), ".config", "chromium", "DevToolsActivePort"),
2257
+ join(homedir(), "Library", "Application Support", "Chromium", "DevToolsActivePort"),
2258
+ ],
2259
+ },
2260
+ ];
1955
2261
  function normalizeCdpCandidate(value) {
1956
2262
  return value.trim().replace(/\/$/, "");
1957
2263
  }
@@ -2001,6 +2307,91 @@ function wait(ms) {
2001
2307
  setTimeout(resolve, ms);
2002
2308
  });
2003
2309
  }
2310
+ function detectRunningDesktopBrowser(processCommands) {
2311
+ const filteredCommands = processCommands
2312
+ .map((command) => command.trim())
2313
+ .filter((command) => command.length > 0)
2314
+ .filter((command) => !/chrome-devtools-mcp/i.test(command));
2315
+ for (const browser of KNOWN_DESKTOP_BROWSERS) {
2316
+ const preferredMatch = filteredCommands.some((command) => !/--type=|crashpad|zygote/i.test(command) && browser.processMatchers.some((matcher) => matcher.test(command)));
2317
+ if (preferredMatch) {
2318
+ return browser;
2319
+ }
2320
+ }
2321
+ for (const browser of KNOWN_DESKTOP_BROWSERS) {
2322
+ if (filteredCommands.some((command) => browser.processMatchers.some((matcher) => matcher.test(command)))) {
2323
+ return browser;
2324
+ }
2325
+ }
2326
+ return null;
2327
+ }
2328
+ function hasRemoteDebuggingSignal(processCommands) {
2329
+ return processCommands.some((command) => /--remote-debugging-(?:port|pipe)(?:=|\b)/i.test(command));
2330
+ }
2331
+ export function summarizeDesktopBrowserReuseGap(input) {
2332
+ const browser = detectRunningDesktopBrowser(input.processCommands);
2333
+ if (!browser) {
2334
+ return null;
2335
+ }
2336
+ if (hasRemoteDebuggingSignal(input.processCommands) || input.hasAnyDevToolsActivePort) {
2337
+ return null;
2338
+ }
2339
+ return `I can see ${browser.label} is already running on this desktop, but dd-cli still couldn't reuse it automatically. It is not exposing an attachable browser automation session right now, and no importable signed-in DoorDash browser profile state was found.`;
2340
+ }
2341
+ async function captureCommandStdout(command, args, options = {}) {
2342
+ return await new Promise((resolve, reject) => {
2343
+ const child = spawn(command, args, {
2344
+ env: options.env,
2345
+ stdio: ["pipe", "pipe", "pipe"],
2346
+ });
2347
+ const stdout = [];
2348
+ const stderr = [];
2349
+ child.once("error", reject);
2350
+ child.stdout?.on("data", (chunk) => {
2351
+ stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2352
+ });
2353
+ child.stderr?.on("data", (chunk) => {
2354
+ stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2355
+ });
2356
+ child.stdin?.end(options.stdin ?? "");
2357
+ child.once("close", (code) => {
2358
+ if (code === 0) {
2359
+ resolve(Buffer.concat(stdout).toString("utf8"));
2360
+ return;
2361
+ }
2362
+ const stderrText = Buffer.concat(stderr).toString("utf8").trim();
2363
+ reject(new Error(`${command} exited with code ${code ?? "null"}${stderrText ? `: ${stderrText}` : ""}`));
2364
+ });
2365
+ });
2366
+ }
2367
+ async function listProcessCommands(targetPlatform = process.platform) {
2368
+ if (targetPlatform === "win32") {
2369
+ return [];
2370
+ }
2371
+ const args = targetPlatform === "darwin" ? ["-axo", "command="] : ["-eo", "command="];
2372
+ const stdout = await captureCommandStdout("ps", args).catch(() => "");
2373
+ return stdout
2374
+ .split(/\r?\n/)
2375
+ .map((command) => command.trim())
2376
+ .filter((command) => command.length > 0);
2377
+ }
2378
+ async function hasAnyKnownDevToolsActivePort() {
2379
+ for (const browser of KNOWN_DESKTOP_BROWSERS) {
2380
+ for (const path of browser.devToolsActivePortPaths) {
2381
+ if ((await readFile(path, "utf8").catch(() => null)) !== null) {
2382
+ return true;
2383
+ }
2384
+ }
2385
+ }
2386
+ return false;
2387
+ }
2388
+ async function describeDesktopBrowserReuseGap() {
2389
+ const processCommands = await listProcessCommands();
2390
+ return summarizeDesktopBrowserReuseGap({
2391
+ processCommands,
2392
+ hasAnyDevToolsActivePort: await hasAnyKnownDevToolsActivePort(),
2393
+ });
2394
+ }
2004
2395
  async function readOpenClawBrowserConfigCandidates(input) {
2005
2396
  try {
2006
2397
  const raw = await readFile(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
@@ -2021,7 +2412,9 @@ async function readOpenClawBrowserConfigCandidates(input) {
2021
2412
  }
2022
2413
  async function isCdpEndpointReachable(cdpUrl) {
2023
2414
  try {
2024
- const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version`);
2415
+ const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version`, {
2416
+ signal: AbortSignal.timeout(ATTACHED_BROWSER_CDP_REACHABILITY_TIMEOUT_MS),
2417
+ });
2025
2418
  return response.ok;
2026
2419
  }
2027
2420
  catch {
@@ -2656,6 +3049,89 @@ function truncate(value, length) {
2656
3049
  async function ensureConfigDir() {
2657
3050
  await mkdir(dirname(getCookiesPath()), { recursive: true });
2658
3051
  }
3052
+ async function snapshotStoredSessionArtifacts() {
3053
+ const [cookiesRaw, storageStateRaw] = await Promise.all([
3054
+ readFile(getCookiesPath(), "utf8").catch(() => null),
3055
+ readFile(getStorageStatePath(), "utf8").catch(() => null),
3056
+ ]);
3057
+ return { cookiesRaw, storageStateRaw };
3058
+ }
3059
+ async function restoreStoredSessionArtifacts(snapshot) {
3060
+ await ensureConfigDir();
3061
+ if (snapshot.cookiesRaw === null) {
3062
+ await rm(getCookiesPath(), { force: true }).catch(() => { });
3063
+ }
3064
+ else {
3065
+ await writeFile(getCookiesPath(), snapshot.cookiesRaw);
3066
+ }
3067
+ if (snapshot.storageStateRaw === null) {
3068
+ await rm(getStorageStatePath(), { force: true }).catch(() => { });
3069
+ }
3070
+ else {
3071
+ await writeFile(getStorageStatePath(), snapshot.storageStateRaw);
3072
+ }
3073
+ }
3074
+ async function writeStoredSessionArtifacts(cookies) {
3075
+ await ensureConfigDir();
3076
+ await writeFile(getCookiesPath(), JSON.stringify(cookies, null, 2));
3077
+ await writeFile(getStorageStatePath(), JSON.stringify({ cookies, origins: [] }, null, 2));
3078
+ }
3079
+ async function readLinuxChromiumCookieImports(input) {
3080
+ try {
3081
+ const stdout = await captureCommandStdout("python3", ["-c", LINUX_CHROMIUM_COOKIE_IMPORT_SCRIPT], {
3082
+ env: {
3083
+ ...process.env,
3084
+ DD_BROWSER_LABEL: input.browserLabel,
3085
+ DD_BROWSER_SAFE_STORAGE_APP: input.safeStorageApplication,
3086
+ DD_BROWSER_USER_DATA_DIR: input.userDataDir,
3087
+ },
3088
+ });
3089
+ const parsed = JSON.parse(stdout);
3090
+ if (!Array.isArray(parsed)) {
3091
+ return [];
3092
+ }
3093
+ return parsed.flatMap((entry) => {
3094
+ const object = asObject(entry);
3095
+ const browserLabel = typeof object.browserLabel === "string" ? object.browserLabel.trim() : "";
3096
+ const profileName = typeof object.profileName === "string" ? object.profileName.trim() : "";
3097
+ if (!browserLabel || !profileName || !Array.isArray(object.cookies)) {
3098
+ return [];
3099
+ }
3100
+ const cookies = object.cookies.flatMap((cookie) => {
3101
+ const parsedCookie = asObject(cookie);
3102
+ const name = typeof parsedCookie.name === "string" ? parsedCookie.name : "";
3103
+ const value = typeof parsedCookie.value === "string" ? parsedCookie.value : "";
3104
+ const domain = typeof parsedCookie.domain === "string" ? parsedCookie.domain : "";
3105
+ const path = typeof parsedCookie.path === "string" && parsedCookie.path ? parsedCookie.path : "/";
3106
+ const sameSiteRaw = typeof parsedCookie.sameSite === "string" ? parsedCookie.sameSite : "Lax";
3107
+ const sameSite = sameSiteRaw === "Strict" || sameSiteRaw === "None" || sameSiteRaw === "Lax" ? sameSiteRaw : "Lax";
3108
+ const expires = typeof parsedCookie.expires === "number" && Number.isFinite(parsedCookie.expires) ? parsedCookie.expires : -1;
3109
+ if (!name || !domain) {
3110
+ return [];
3111
+ }
3112
+ return [
3113
+ {
3114
+ name,
3115
+ value,
3116
+ domain,
3117
+ path,
3118
+ expires,
3119
+ httpOnly: Boolean(parsedCookie.httpOnly),
3120
+ secure: Boolean(parsedCookie.secure),
3121
+ sameSite,
3122
+ },
3123
+ ];
3124
+ });
3125
+ if (cookies.length === 0) {
3126
+ return [];
3127
+ }
3128
+ return [{ browserLabel, profileName, cookies }];
3129
+ });
3130
+ }
3131
+ catch {
3132
+ return [];
3133
+ }
3134
+ }
2659
3135
  async function hasBlockedBrowserImport() {
2660
3136
  try {
2661
3137
  await readFile(getBrowserImportBlockPath(), "utf8");