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 +6 -0
- package/README.md +4 -4
- package/dist/cli.js +2 -2
- package/dist/cli.test.js +4 -4
- package/dist/direct-api.d.ts +2 -0
- package/dist/direct-api.js +297 -35
- package/dist/direct-api.test.js +10 -4
- package/docs/examples.md +1 -1
- package/docs/install.md +3 -3
- package/man/dd-cli.1 +17 -16
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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"), /
|
|
136
|
-
assert.match(readFileSync(ddManPath, "utf8"), /temporary
|
|
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
|
});
|
package/dist/direct-api.d.ts
CHANGED
|
@@ -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;
|
package/dist/direct-api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
let page = null;
|
|
2016
|
+
await session.close().catch(() => { });
|
|
2017
|
+
session.markBrowserImportAttempted();
|
|
1823
2018
|
try {
|
|
1824
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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");
|
package/dist/direct-api.test.js
CHANGED
|
@@ -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
|
|
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 ?? "", /
|
|
195
|
-
assert.match(message ?? "", /
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
39
|
-
one is available,
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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