doordash-cli 0.4.1 → 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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ All notable changes to `doordash-cli` will be documented in this file.
9
9
 
10
10
  See [docs/releasing.md](docs/releasing.md) for the maintainer release flow.
11
11
 
12
+ ## [0.4.2](https://github.com/LatencyTDH/doordash-cli/compare/v0.4.1...v0.4.2) (2026-04-10)
13
+
14
+ ### Bug Fixes
15
+
16
+ * import signed-in linux browser profile state for login reuse ([#40](https://github.com/LatencyTDH/doordash-cli/issues/40)) ([97feddc](https://github.com/LatencyTDH/doordash-cli/commit/97feddc68ce0ebc882737dfad69d5e908f20d250))
17
+
12
18
  ## [0.4.1](https://github.com/LatencyTDH/doordash-cli/compare/v0.4.0...v0.4.1) (2026-04-10)
13
19
 
14
20
  ### Bug Fixes
package/README.md CHANGED
@@ -9,7 +9,7 @@ It stops before checkout.
9
9
  ## Highlights
10
10
 
11
11
  - **Cart-safe by design** — browse, inspect existing orders, and manage a cart; no checkout, payment, or order mutation.
12
- - **Browser-first login** — `dd-cli login` reuses saved local auth or an attachable signed-in browser session when possible, and otherwise opens a temporary login window.
12
+ - **Browser-first login** — `dd-cli login` reuses saved local auth, then same-machine Linux Brave/Chrome profile state, then attachable signed-in browser sessions when possible, and otherwise opens a temporary login window.
13
13
  - **Direct API first** — auth, discovery, existing-order, and cart commands use DoorDash consumer-web GraphQL/HTTP rather than DOM clicking.
14
14
  - **JSON-friendly** — every command prints structured output.
15
15
  - **Fail-closed** — unsupported commands, flags, or unsafe payload shapes are rejected.
@@ -70,13 +70,13 @@ If you are running from a checkout without `npm link`, replace `doordash-cli` wi
70
70
 
71
71
  ## Login and session reuse
72
72
 
73
- `login` reuses saved local auth when it is still valid. Otherwise it tries to import a discoverable attachable signed-in browser session. A merely-open Chrome/Brave window is not automatically reusable unless the CLI can actually attach to it. If no attachable session is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
73
+ `login` reuses saved local auth when it is still valid. Otherwise it first tries to import signed-in same-machine Linux Brave/Chrome profile state, then falls back to a discoverable attachable signed-in browser session, and finally opens a temporary Chromium login window it can watch directly. If authentication still is not established, `login` exits non-zero.
74
74
 
75
- `auth-check` reports whether the saved state appears logged in and can quietly import a discoverable attachable signed-in browser session unless `logout` disabled that auto-reuse.
75
+ `auth-check` reports whether the saved state appears logged in and can quietly import same-machine Linux Brave/Chrome profile state or a discoverable attachable signed-in browser session unless `logout` disabled that auto-reuse.
76
76
 
77
77
  `logout` clears persisted cookies and stored browser state, then keeps passive browser-session reuse disabled until your next explicit `dd-cli login` attempt.
78
78
 
79
- If `login` opens a temporary Chromium window, the CLI now keeps checking automatically and also tells you that you can press Enter to force an immediate recheck once the page already shows you are signed in. That restores the old effective manual-completion path without giving up automatic completion when it works. If you expect reuse from another browser instead, make sure it exposes an attachable browser automation session the CLI can actually import; a merely-open browser window is not enough today, even if it is already your main browser.
79
+ If `login` opens a temporary Chromium window, the CLI now keeps checking automatically and also tells you that you can press Enter to force an immediate recheck once the page already shows you are signed in. That restores the old effective manual-completion path without giving up automatic completion when it works. On Linux, a signed-in local Brave or Google Chrome profile on the same machine is the preferred browser-reuse path and does not need CDP/remote debugging. If that same-machine profile import is unavailable or not signed in, the next reuse path is an attachable browser automation session.
80
80
 
81
81
  ## Command surface
82
82
 
package/dist/cli.js CHANGED
@@ -42,10 +42,10 @@ export function usage() {
42
42
  " - Installed command names are lowercase only: dd-cli and doordash-cli.",
43
43
  " - install-browser downloads the bundled Playwright Chromium runtime used when the CLI needs a local browser.",
44
44
  " - Manual pages ship with the project: man dd-cli or man doordash-cli.",
45
- " - login reuses saved local auth when possible, otherwise imports an attachable signed-in browser session or opens a temporary Chromium login window.",
45
+ " - login reuses saved local auth when possible, otherwise first tries same-machine Linux Brave/Chrome profile import, then attachable signed-in browser sessions, then a temporary Chromium login window.",
46
46
  " - login auto-detects completion when it can; in the temporary-browser fallback you can also press Enter to force an immediate recheck once the page shows you are signed in.",
47
47
  " - login exits non-zero if authentication is still not established.",
48
- " - auth-check reports saved-session status and can quietly reuse/import an attachable signed-in browser session unless logout disabled that auto-reuse.",
48
+ " - auth-check reports saved-session status and can quietly reuse/import same-machine Linux Brave/Chrome profile state or an attachable signed-in browser session unless logout disabled that auto-reuse.",
49
49
  " - logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt.",
50
50
  " - configurable items require explicit --options-json selections.",
51
51
  " - unsupported option trees fail closed.",
package/dist/cli.test.js CHANGED
@@ -113,10 +113,10 @@ test("help output shows the direct read-only/cart-safe command surface", () => {
113
113
  assert.match(result.stdout, /options-json/);
114
114
  assert.match(result.stdout, /--version, -v/);
115
115
  assert.match(result.stdout, /man dd-cli/);
116
- assert.match(result.stdout, /login reuses saved local auth when possible, otherwise imports an attachable signed-in browser session or opens a temporary Chromium login window\./);
116
+ assert.match(result.stdout, /login reuses saved local auth when possible, otherwise first tries same-machine Linux Brave\/Chrome profile import, then attachable signed-in browser sessions, then a temporary Chromium login window\./);
117
117
  assert.match(result.stdout, /login auto-detects completion when it can; in the temporary-browser fallback you can also press Enter to force an immediate recheck once the page shows you are signed in\./);
118
118
  assert.match(result.stdout, /login exits non-zero if authentication is still not established\./);
119
- assert.match(result.stdout, /auth-check reports saved-session status and can quietly reuse\/import an attachable signed-in browser session unless logout disabled that auto-reuse\./);
119
+ assert.match(result.stdout, /auth-check reports saved-session status and can quietly reuse\/import same-machine Linux Brave\/Chrome profile state or an attachable signed-in browser session unless logout disabled that auto-reuse\./);
120
120
  assert.match(result.stdout, /logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt\./);
121
121
  assert.match(result.stdout, /Out-of-scope commands remain intentionally unsupported/);
122
122
  assert.doesNotMatch(result.stdout, /auth-bootstrap/);
@@ -132,8 +132,8 @@ test("repository ships man pages for the supported lowercase command names", ()
132
132
  assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-bootstrap/);
133
133
  assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-clear/);
134
134
  assert.match(readFileSync(ddManPath, "utf8"), /passive\s+browser-session reuse stays disabled until the next explicit/i);
135
- assert.match(readFileSync(ddManPath, "utf8"), /merely-open Chrome\/Brave window is not\s+automatically reusable/i);
136
- assert.match(readFileSync(ddManPath, "utf8"), /temporary Chromium.*window/i);
135
+ assert.match(readFileSync(ddManPath, "utf8"), /same-machine Linux Brave\/Chrome browser profile/i);
136
+ assert.match(readFileSync(ddManPath, "utf8"), /temporary\s+Chromium\s+window/i);
137
137
  assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /Dd-cli/);
138
138
  assert.equal(readFileSync(aliasManPath, "utf8").trim(), ".so man1/dd-cli.1");
139
139
  });
@@ -570,6 +570,8 @@ export declare function selectAttachedBrowserImportMode(input: {
570
570
  pageUrls: readonly string[];
571
571
  cookies: ReadonlyArray<Pick<Cookie, "domain">>;
572
572
  }): "page" | "cookies" | "skip";
573
+ export type BrowserSessionImportStrategy = "local-linux-chromium-profile" | "attached-browser-cdp";
574
+ export declare function preferredBrowserSessionImportStrategies(platform: NodeJS.Platform): readonly BrowserSessionImportStrategy[];
573
575
  export declare function resolveAttachedBrowserCdpCandidates(env: NodeJS.ProcessEnv, configCandidates?: string[]): string[];
574
576
  export declare function resolveSystemBrowserOpenCommand(targetUrl: string, targetPlatform?: NodeJS.Platform): {
575
577
  command: string;
@@ -13,6 +13,165 @@ const AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS = 10_000;
13
13
  const ATTACHED_BROWSER_CDP_REACHABILITY_TIMEOUT_MS = 2_000;
14
14
  const ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS = 5_000;
15
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
+ ];
16
175
  const GRAPHQL_HEADERS = {
17
176
  accept: "*/*",
18
177
  "content-type": "application/json",
@@ -1715,8 +1874,45 @@ export function selectAttachedBrowserImportMode(input) {
1715
1874
  }
1716
1875
  return "skip";
1717
1876
  }
1877
+ export function preferredBrowserSessionImportStrategies(platform) {
1878
+ return platform === "linux"
1879
+ ? ["local-linux-chromium-profile", "attached-browser-cdp"]
1880
+ : ["attached-browser-cdp"];
1881
+ }
1718
1882
  async function importBrowserSessionIfAvailable() {
1719
- 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;
1720
1916
  }
1721
1917
  async function importBrowserSessionFromCdpCandidates(candidates) {
1722
1918
  for (const cdpUrl of candidates) {
@@ -1817,42 +2013,16 @@ async function getPersistedAuthDirect() {
1817
2013
  if (!(await hasPersistedSessionArtifacts())) {
1818
2014
  return null;
1819
2015
  }
1820
- let browser = null;
1821
- let context = null;
1822
- let page = null;
2016
+ await session.close().catch(() => { });
2017
+ session.markBrowserImportAttempted();
1823
2018
  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);
2019
+ return await checkAuthDirect();
1848
2020
  }
1849
2021
  catch {
1850
2022
  return null;
1851
2023
  }
1852
2024
  finally {
1853
- await page?.close().catch(() => { });
1854
- await context?.close().catch(() => { });
1855
- await browser?.close().catch(() => { });
2025
+ await session.close().catch(() => { });
1856
2026
  }
1857
2027
  }
1858
2028
  async function validatePersistedDirectSessionArtifacts() {
@@ -2166,22 +2336,31 @@ export function summarizeDesktopBrowserReuseGap(input) {
2166
2336
  if (hasRemoteDebuggingSignal(input.processCommands) || input.hasAnyDevToolsActivePort) {
2167
2337
  return null;
2168
2338
  }
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.`;
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.`;
2170
2340
  }
2171
- async function captureCommandStdout(command, args) {
2341
+ async function captureCommandStdout(command, args, options = {}) {
2172
2342
  return await new Promise((resolve, reject) => {
2173
- const child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
2343
+ const child = spawn(command, args, {
2344
+ env: options.env,
2345
+ stdio: ["pipe", "pipe", "pipe"],
2346
+ });
2174
2347
  const stdout = [];
2348
+ const stderr = [];
2175
2349
  child.once("error", reject);
2176
2350
  child.stdout?.on("data", (chunk) => {
2177
2351
  stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2178
2352
  });
2353
+ child.stderr?.on("data", (chunk) => {
2354
+ stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2355
+ });
2356
+ child.stdin?.end(options.stdin ?? "");
2179
2357
  child.once("close", (code) => {
2180
2358
  if (code === 0) {
2181
2359
  resolve(Buffer.concat(stdout).toString("utf8"));
2182
2360
  return;
2183
2361
  }
2184
- reject(new Error(`${command} exited with code ${code ?? "null"}`));
2362
+ const stderrText = Buffer.concat(stderr).toString("utf8").trim();
2363
+ reject(new Error(`${command} exited with code ${code ?? "null"}${stderrText ? `: ${stderrText}` : ""}`));
2185
2364
  });
2186
2365
  });
2187
2366
  }
@@ -2870,6 +3049,89 @@ function truncate(value, length) {
2870
3049
  async function ensureConfigDir() {
2871
3050
  await mkdir(dirname(getCookiesPath()), { recursive: true });
2872
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
+ }
2873
3135
  async function hasBlockedBrowserImport() {
2874
3136
  try {
2875
3137
  await readFile(getBrowserImportBlockPath(), "utf8");
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { bootstrapAuthSessionWithDeps, buildAddConsumerAddressPayload, buildAddToCartPayload, buildUpdateCartPayload, extractExistingOrdersFromApolloCache, normalizeItemName, parseExistingOrderLifecycleStatus, parseExistingOrdersResponse, parseOptionSelectionsJson, parseSearchRestaurantRow, resolveAttachedBrowserCdpCandidates, resolveAvailableAddressMatch, resolveSystemBrowserOpenCommand, selectAttachedBrowserImportMode, summarizeDesktopBrowserReuseGap, } from "./direct-api.js";
3
+ import { bootstrapAuthSessionWithDeps, buildAddConsumerAddressPayload, buildAddToCartPayload, buildUpdateCartPayload, extractExistingOrdersFromApolloCache, normalizeItemName, parseExistingOrderLifecycleStatus, parseExistingOrdersResponse, parseOptionSelectionsJson, parseSearchRestaurantRow, preferredBrowserSessionImportStrategies, resolveAttachedBrowserCdpCandidates, resolveAvailableAddressMatch, resolveSystemBrowserOpenCommand, selectAttachedBrowserImportMode, summarizeDesktopBrowserReuseGap, } from "./direct-api.js";
4
4
  function configurableItemDetail() {
5
5
  return {
6
6
  success: true,
@@ -181,7 +181,7 @@ test("resolveSystemBrowserOpenCommand stays generic across operating systems", (
181
181
  args: ["/c", "start", "", "https://www.doordash.com/home"],
182
182
  });
183
183
  });
184
- test("summarizeDesktopBrowserReuseGap explains why a merely-open Brave window is not reusable", () => {
184
+ test("summarizeDesktopBrowserReuseGap explains why a running Brave session still was not reusable", () => {
185
185
  const message = summarizeDesktopBrowserReuseGap({
186
186
  processCommands: [
187
187
  "/bin/bash /usr/bin/brave-browser-stable",
@@ -191,8 +191,9 @@ test("summarizeDesktopBrowserReuseGap explains why a merely-open Brave window is
191
191
  hasAnyDevToolsActivePort: false,
192
192
  });
193
193
  assert.match(message ?? "", /Brave is already running on this desktop/i);
194
- assert.match(message ?? "", /normal open browser window is not automatically reusable/i);
195
- assert.match(message ?? "", /attach to/i);
194
+ assert.match(message ?? "", /couldn't reuse it automatically/i);
195
+ assert.match(message ?? "", /attachable browser automation session/i);
196
+ assert.match(message ?? "", /no importable signed-in DoorDash browser profile state was found/i);
196
197
  });
197
198
  test("summarizeDesktopBrowserReuseGap stays quiet once the browser exposes attach signals", () => {
198
199
  assert.equal(summarizeDesktopBrowserReuseGap({
@@ -204,6 +205,11 @@ test("summarizeDesktopBrowserReuseGap stays quiet once the browser exposes attac
204
205
  hasAnyDevToolsActivePort: true,
205
206
  }), null);
206
207
  });
208
+ test("preferredBrowserSessionImportStrategies prefers same-machine linux profile imports before CDP attach", () => {
209
+ assert.deepEqual(preferredBrowserSessionImportStrategies("linux"), ["local-linux-chromium-profile", "attached-browser-cdp"]);
210
+ assert.deepEqual(preferredBrowserSessionImportStrategies("darwin"), ["attached-browser-cdp"]);
211
+ assert.deepEqual(preferredBrowserSessionImportStrategies("win32"), ["attached-browser-cdp"]);
212
+ });
207
213
  test("selectAttachedBrowserImportMode treats an authenticated browser with DoorDash cookies as an immediate import candidate", () => {
208
214
  assert.equal(selectAttachedBrowserImportMode({
209
215
  pageUrls: ["https://github.com/LatencyTDH/doordash-cli/pulls"],
package/docs/examples.md CHANGED
@@ -26,7 +26,7 @@ Check whether you already have reusable session state:
26
26
  doordash-cli auth-check
27
27
  ```
28
28
 
29
- If your saved local state is still valid, this exits immediately. Otherwise it tries to reuse a discoverable attachable signed-in browser session. A merely-open Chrome/Brave window is not automatically reusable unless the CLI can actually attach to it, so the fallback is a temporary Chromium login window the CLI can watch directly. In that temporary-browser fallback, the CLI keeps checking automatically and you can also press Enter in the terminal to force an immediate recheck once the page shows you are signed in:
29
+ If your saved local state is still valid, this exits immediately. Otherwise it first tries to reuse same-machine Linux Brave/Chrome profile state, then a discoverable attachable signed-in browser session, and finally falls back to a temporary Chromium login window the CLI can watch directly. In that temporary-browser fallback, the CLI keeps checking automatically and you can also press Enter in the terminal to force an immediate recheck once the page shows you are signed in:
30
30
 
31
31
  ```bash
32
32
  doordash-cli login
package/docs/install.md CHANGED
@@ -60,9 +60,9 @@ doordash-cli search --query sushi
60
60
 
61
61
  ## Login and session reuse
62
62
 
63
- `doordash-cli login` reuses saved local auth when it is still valid. Otherwise it tries to import a discoverable attachable signed-in browser session. A merely-open Chrome/Brave window is not automatically reusable unless the CLI can actually attach to it. If no attachable session is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
63
+ `doordash-cli login` reuses saved local auth when it is still valid. Otherwise it first tries to import signed-in same-machine Linux Brave/Chrome profile state, then falls back to a discoverable attachable signed-in browser session, and finally opens a temporary Chromium login window it can watch directly. If authentication still is not established, `login` exits non-zero.
64
64
 
65
- `doordash-cli auth-check` can also quietly import a discoverable attachable signed-in browser session unless `doordash-cli logout` disabled that auto-reuse.
65
+ `doordash-cli auth-check` can also quietly import same-machine Linux Brave/Chrome profile state or a discoverable attachable signed-in browser session unless `doordash-cli logout` disabled that auto-reuse.
66
66
 
67
67
  `doordash-cli logout` clears persisted cookies and stored browser state, then keeps passive browser-session reuse disabled until your next explicit `doordash-cli login` attempt.
68
68
 
@@ -70,4 +70,4 @@ doordash-cli search --query sushi
70
70
 
71
71
  Normally you should not need to think about browser plumbing. If `doordash-cli login` opens a temporary Chromium window, finish signing in there and let the CLI save the session. The CLI keeps checking automatically, and if the page already shows you are signed in but the command has not finished yet, press Enter in the terminal to force an immediate recheck.
72
72
 
73
- If you expected reuse from another browser instead, make sure that browser exposes an attachable browser automation session the CLI can actually import. A merely-open browser window is not enough today, even if it is already your main browser.
73
+ On Linux, the preferred reuse path is a signed-in local Brave or Google Chrome profile on the same machine, which does not need CDP/remote debugging. If that same-machine profile import is unavailable or not signed in, the next reuse path is an attachable browser automation session.
package/man/dd-cli.1 CHANGED
@@ -35,20 +35,20 @@ local browser, including the temporary login-window fallback.
35
35
  .TP
36
36
  .B auth-check
37
37
  Verify whether the saved session appears authenticated. This command can also
38
- quietly reuse or import an already-signed-in attachable browser session when
39
- one is available, unless
38
+ quietly reuse or import same-machine Linux Brave/Chrome browser profile state
39
+ or an already-signed-in attachable browser session when one is available,
40
+ unless
40
41
  .B logout
41
42
  explicitly disabled that auto-reuse.
42
43
  .TP
43
44
  .B login
44
- Reuse saved local auth when possible. Otherwise try to import a discoverable
45
- attachable signed-in browser session. A merely-open Chrome/Brave window is not
46
- automatically reusable unless the CLI can actually attach to it. If no
47
- attachable session is available, open a temporary Chromium login window and save
48
- that session there. The temporary-browser fallback auto-detects completion when
49
- it can, and also accepts an explicit Enter keypress in the terminal to force an
50
- immediate recheck once the page shows the user is signed in. If authentication
51
- is still not established,
45
+ Reuse saved local auth when possible. Otherwise first try to import signed-in
46
+ same-machine Linux Brave/Chrome browser profile state, then a discoverable
47
+ attachable signed-in browser session, and finally open a temporary Chromium
48
+ login window and save that session there. The temporary-browser fallback
49
+ auto-detects completion when it can, and also accepts an explicit Enter
50
+ keypress in the terminal to force an immediate recheck once the page shows the
51
+ user is signed in. If authentication is still not established,
52
52
  .B login
53
53
  exits non-zero.
54
54
  .TP
@@ -215,12 +215,13 @@ passive browser-session reuse stays disabled until the next explicit
215
215
  .SH ENVIRONMENT
216
216
  .PP
217
217
  In the common case you should not need to configure browser plumbing manually.
218
- For attached-browser reuse, the CLI can probe compatible CDP URLs/ports and a
219
- few localhost defaults. A merely-open browser window is not automatically
220
- reusable unless the CLI can actually attach to it. If no attachable browser
221
- session is discoverable, login falls back to a temporary Chromium window it can
222
- watch directly, with an explicit Enter-to-recheck fallback in the terminal if
223
- automatic detection is not yet convincing. See
218
+ On Linux, the preferred browser-reuse path is a signed-in local Brave or Google
219
+ Chrome profile on the same machine, which does not need CDP/remote debugging.
220
+ For attached-browser reuse, the CLI can also probe compatible CDP URLs/ports
221
+ and a few localhost defaults. If neither same-machine profile import nor an
222
+ attachable browser session is discoverable, login falls back to a temporary
223
+ Chromium window it can watch directly, with an explicit Enter-to-recheck
224
+ fallback in the terminal if automatic detection is not yet convincing. See
224
225
  .I docs/install.md
225
226
  for setup and troubleshooting.
226
227
  .SH EXIT STATUS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doordash-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Cart-safe DoorDash CLI with direct API support for browse, read-only existing-order, and cart workflows.",
5
5
  "type": "module",
6
6
  "main": "dist/lib.js",