doordash-cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -5
- package/dist/cli.js +4 -3
- package/dist/cli.test.js +6 -4
- package/dist/direct-api.d.ts +18 -1
- package/dist/direct-api.js +242 -28
- package/dist/direct-api.test.js +224 -8
- package/docs/examples.md +2 -2
- package/docs/install.md +5 -5
- package/man/dd-cli.1 +16 -8
- 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.1](https://github.com/LatencyTDH/doordash-cli/compare/v0.4.0...v0.4.1) (2026-04-10)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* bound auth-check and restore login completion flow ([#39](https://github.com/LatencyTDH/doordash-cli/issues/39)) ([5166944](https://github.com/LatencyTDH/doordash-cli/commit/51669444dc124e39ece719624c997ab9f46acd93))
|
|
17
|
+
|
|
12
18
|
## [0.4.0](https://github.com/LatencyTDH/doordash-cli/compare/v0.3.3...v0.4.0) (2026-04-10)
|
|
13
19
|
|
|
14
20
|
### Features
|
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
|
|
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.
|
|
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 signed-in browser session. If
|
|
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.
|
|
74
74
|
|
|
75
|
-
`auth-check` reports whether the saved state appears logged in and can quietly import a discoverable 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 a discoverable attachable signed-in browser session unless `logout` disabled that auto-reuse.
|
|
76
76
|
|
|
77
|
-
`logout` clears persisted cookies and stored browser state, then keeps
|
|
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,
|
|
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.
|
|
80
80
|
|
|
81
81
|
## Command surface
|
|
82
82
|
|
package/dist/cli.js
CHANGED
|
@@ -42,10 +42,11 @@ 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
|
|
45
|
+
" - login reuses saved local auth when possible, otherwise imports an attachable signed-in browser session or opens a temporary Chromium login window.",
|
|
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.",
|
|
46
47
|
" - login exits non-zero if authentication is still not established.",
|
|
47
|
-
" - auth-check reports saved-session status and can quietly reuse/import
|
|
48
|
-
" - logout clears saved session files and keeps
|
|
48
|
+
" - auth-check reports saved-session status and can quietly reuse/import an attachable signed-in browser session unless logout disabled that auto-reuse.",
|
|
49
|
+
" - logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt.",
|
|
49
50
|
" - configurable items require explicit --options-json selections.",
|
|
50
51
|
" - unsupported option trees fail closed.",
|
|
51
52
|
"",
|
package/dist/cli.test.js
CHANGED
|
@@ -113,10 +113,11 @@ 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
|
|
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\./);
|
|
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\./);
|
|
117
118
|
assert.match(result.stdout, /login exits non-zero if authentication is still not established\./);
|
|
118
|
-
assert.match(result.stdout, /auth-check reports saved-session status and can quietly reuse\/import
|
|
119
|
-
assert.match(result.stdout, /logout clears saved session files and keeps
|
|
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\./);
|
|
120
|
+
assert.match(result.stdout, /logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt\./);
|
|
120
121
|
assert.match(result.stdout, /Out-of-scope commands remain intentionally unsupported/);
|
|
121
122
|
assert.doesNotMatch(result.stdout, /auth-bootstrap/);
|
|
122
123
|
assert.doesNotMatch(result.stdout, /auth-clear/);
|
|
@@ -130,7 +131,8 @@ test("repository ships man pages for the supported lowercase command names", ()
|
|
|
130
131
|
assert.match(readFileSync(ddManPath, "utf8"), /\.B login/);
|
|
131
132
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-bootstrap/);
|
|
132
133
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-clear/);
|
|
133
|
-
assert.match(readFileSync(ddManPath, "utf8"), /
|
|
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);
|
|
134
136
|
assert.match(readFileSync(ddManPath, "utf8"), /temporary Chromium.*window/i);
|
|
135
137
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /Dd-cli/);
|
|
136
138
|
assert.equal(readFileSync(aliasManPath, "utf8").trim(), ".so man1/dd-cli.1");
|
package/dist/direct-api.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export type AuthBootstrapResult = (AuthResult & {
|
|
|
24
24
|
isLoggedIn: false;
|
|
25
25
|
message: string;
|
|
26
26
|
});
|
|
27
|
+
type ManagedBrowserLoginResult = {
|
|
28
|
+
status: "completed";
|
|
29
|
+
completion: "automatic" | "manual";
|
|
30
|
+
auth: AuthResult;
|
|
31
|
+
} | {
|
|
32
|
+
status: "timed-out";
|
|
33
|
+
auth: AuthResult;
|
|
34
|
+
} | {
|
|
35
|
+
status: "launch-failed";
|
|
36
|
+
};
|
|
27
37
|
export type SearchRestaurantResult = {
|
|
28
38
|
id: string;
|
|
29
39
|
name: string;
|
|
@@ -469,6 +479,7 @@ type BootstrapAuthSessionDeps = {
|
|
|
469
479
|
markBrowserImportAttempted: () => void;
|
|
470
480
|
getAttachedBrowserCdpCandidates: () => Promise<string[]>;
|
|
471
481
|
getReachableCdpCandidates: (candidates: string[]) => Promise<string[]>;
|
|
482
|
+
describeDesktopBrowserReuseGap: () => Promise<string | null>;
|
|
472
483
|
openUrlInAttachedBrowser: (input: {
|
|
473
484
|
cdpUrl: string;
|
|
474
485
|
targetUrl: string;
|
|
@@ -482,7 +493,9 @@ type BootstrapAuthSessionDeps = {
|
|
|
482
493
|
targetUrl: string;
|
|
483
494
|
timeoutMs: number;
|
|
484
495
|
pollIntervalMs: number;
|
|
485
|
-
|
|
496
|
+
log: (message: string) => void;
|
|
497
|
+
}) => Promise<ManagedBrowserLoginResult>;
|
|
498
|
+
canPromptForManagedBrowserConfirmation: () => boolean;
|
|
486
499
|
checkAuthDirect: () => Promise<AuthResult>;
|
|
487
500
|
log: (message: string) => void;
|
|
488
501
|
};
|
|
@@ -562,6 +575,10 @@ export declare function resolveSystemBrowserOpenCommand(targetUrl: string, targe
|
|
|
562
575
|
command: string;
|
|
563
576
|
args: string[];
|
|
564
577
|
} | null;
|
|
578
|
+
export declare function summarizeDesktopBrowserReuseGap(input: {
|
|
579
|
+
processCommands: readonly string[];
|
|
580
|
+
hasAnyDevToolsActivePort: boolean;
|
|
581
|
+
}): string | null;
|
|
565
582
|
export declare function parseSearchRestaurants(body: unknown[]): SearchRestaurantResult[];
|
|
566
583
|
export declare function parseSearchRestaurantRow(entry: unknown): SearchRestaurantResult | null;
|
|
567
584
|
export declare function parseExistingOrderLifecycleStatus(orderRoot: unknown): ExistingOrderLifecycleStatus;
|
package/dist/direct-api.js
CHANGED
|
@@ -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,6 +10,8 @@ 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";
|
|
13
16
|
const GRAPHQL_HEADERS = {
|
|
14
17
|
accept: "*/*",
|
|
@@ -864,6 +867,42 @@ function buildAuthBootstrapFailure(auth, message) {
|
|
|
864
867
|
message,
|
|
865
868
|
};
|
|
866
869
|
}
|
|
870
|
+
function canPromptForManagedBrowserConfirmation() {
|
|
871
|
+
return Boolean(process.stdin.isTTY);
|
|
872
|
+
}
|
|
873
|
+
function createManagedBrowserManualConfirmationHandle() {
|
|
874
|
+
if (!canPromptForManagedBrowserConfirmation()) {
|
|
875
|
+
return {
|
|
876
|
+
isEnabled: false,
|
|
877
|
+
consumeRequested: () => false,
|
|
878
|
+
close: () => { },
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const rl = createInterface({
|
|
882
|
+
input: process.stdin,
|
|
883
|
+
output: process.stderr,
|
|
884
|
+
terminal: true,
|
|
885
|
+
});
|
|
886
|
+
let requested = false;
|
|
887
|
+
const onLine = () => {
|
|
888
|
+
requested = true;
|
|
889
|
+
};
|
|
890
|
+
rl.on("line", onLine);
|
|
891
|
+
return {
|
|
892
|
+
isEnabled: true,
|
|
893
|
+
consumeRequested: () => {
|
|
894
|
+
if (!requested) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
requested = false;
|
|
898
|
+
return true;
|
|
899
|
+
},
|
|
900
|
+
close: () => {
|
|
901
|
+
rl.off("line", onLine);
|
|
902
|
+
rl.close();
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
}
|
|
867
906
|
export async function checkAuthDirect() {
|
|
868
907
|
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
869
908
|
return buildAuthResult(data.consumer ?? null);
|
|
@@ -891,11 +930,11 @@ export async function bootstrapAuthSessionWithDeps(deps) {
|
|
|
891
930
|
});
|
|
892
931
|
const openedDefaultBrowser = openedAttachedBrowser ? false : await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
893
932
|
deps.log(openedAttachedBrowser
|
|
894
|
-
? `Opened DoorDash in the
|
|
933
|
+
? `Opened DoorDash in the attachable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
|
|
895
934
|
: openedDefaultBrowser
|
|
896
|
-
? `Found
|
|
897
|
-
: `Detected
|
|
898
|
-
deps.log(`Detected ${reachableCandidates.length}
|
|
935
|
+
? `Found an attachable browser session, but couldn't drive it directly, so I opened DoorDash in your default browser: ${AUTH_BOOTSTRAP_URL}`
|
|
936
|
+
: `Detected an attachable browser session, but couldn't open DoorDash automatically. Open this URL in that watched browser to continue: ${AUTH_BOOTSTRAP_URL}`);
|
|
937
|
+
deps.log(`Detected ${reachableCandidates.length} attachable browser session(s). Finish the sign-in in that browser window and I'll import it automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
|
|
899
938
|
const importedAfterWait = await deps.waitForAttachedBrowserSessionImport({
|
|
900
939
|
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
901
940
|
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
@@ -903,29 +942,43 @@ export async function bootstrapAuthSessionWithDeps(deps) {
|
|
|
903
942
|
const auth = await deps.checkAuthDirect();
|
|
904
943
|
if (importedAfterWait) {
|
|
905
944
|
return auth.isLoggedIn
|
|
906
|
-
? buildAuthBootstrapSuccess(auth, "Opened DoorDash in
|
|
945
|
+
? buildAuthBootstrapSuccess(auth, "Opened DoorDash in an attachable browser session, detected the signed-in consumer state, and saved it for direct API use.")
|
|
907
946
|
: buildAuthBootstrapFailure(auth, "Detected browser session state in the watched browser, but the consumer still appears logged out or guest-only.");
|
|
908
947
|
}
|
|
909
948
|
return buildAuthBootstrapFailure(auth, openedAttachedBrowser || openedDefaultBrowser
|
|
910
|
-
? `Opened DoorDash and watched
|
|
911
|
-
: `Watched
|
|
912
|
-
}
|
|
913
|
-
|
|
949
|
+
? `Opened DoorDash and watched attachable browser sessions for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no signed-in DoorDash session was imported. Finish the login in that watched browser and rerun dd-cli login.`
|
|
950
|
+
: `Watched attachable browser sessions for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds without importing a signed-in DoorDash session. Open ${AUTH_BOOTSTRAP_URL} manually in the watched browser, finish signing in, then rerun dd-cli login.`);
|
|
951
|
+
}
|
|
952
|
+
const desktopBrowserReuseGap = await deps.describeDesktopBrowserReuseGap().catch(() => null);
|
|
953
|
+
if (desktopBrowserReuseGap) {
|
|
954
|
+
deps.log(desktopBrowserReuseGap);
|
|
955
|
+
}
|
|
956
|
+
const manualManagedConfirmationAvailable = deps.canPromptForManagedBrowserConfirmation();
|
|
957
|
+
deps.log("I couldn't find an attachable browser session I can reuse, so I'm opening a temporary Chromium login window that the CLI can watch directly.");
|
|
958
|
+
deps.log(manualManagedConfirmationAvailable
|
|
959
|
+
? `Finish signing in in that window. I'll keep checking automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds. If the page already shows you're signed in and the CLI still hasn't finished, press Enter here to force an immediate recheck.`
|
|
960
|
+
: `Finish signing in in that window. I'll keep checking automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
|
|
914
961
|
const managedAuth = await deps.waitForManagedBrowserLogin({
|
|
915
962
|
targetUrl: AUTH_BOOTSTRAP_URL,
|
|
916
963
|
timeoutMs: AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
917
964
|
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
965
|
+
log: deps.log,
|
|
918
966
|
});
|
|
919
|
-
if (managedAuth) {
|
|
920
|
-
return managedAuth.
|
|
921
|
-
?
|
|
922
|
-
:
|
|
967
|
+
if (managedAuth.status === "completed") {
|
|
968
|
+
return buildAuthBootstrapSuccess(managedAuth.auth, managedAuth.completion === "manual"
|
|
969
|
+
? "Opened a temporary Chromium login window. After you pressed Enter to confirm the browser login was complete, the CLI rechecked the signed-in session there and saved it for direct API use."
|
|
970
|
+
: "Opened a temporary Chromium login window, detected the signed-in session there automatically, and saved it for direct API use.");
|
|
971
|
+
}
|
|
972
|
+
if (managedAuth.status === "timed-out") {
|
|
973
|
+
return buildAuthBootstrapFailure(managedAuth.auth, manualManagedConfirmationAvailable
|
|
974
|
+
? `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but I still couldn't prove an authenticated DoorDash session. If the browser already looks signed in, press Enter sooner next time to force an immediate recheck, or rerun dd-cli login.`
|
|
975
|
+
: `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no authenticated DoorDash session was established.`);
|
|
923
976
|
}
|
|
924
977
|
const openedBrowser = await deps.openUrlInDefaultBrowser(AUTH_BOOTSTRAP_URL);
|
|
925
978
|
deps.log(openedBrowser
|
|
926
979
|
? `I couldn't launch the temporary Chromium login window, so I opened DoorDash in your default browser instead: ${AUTH_BOOTSTRAP_URL}`
|
|
927
980
|
: `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
|
|
981
|
+
deps.log("This environment still isn't exposing an attachable browser session the CLI can import, so I won't keep you waiting for the full login timeout. Once you've exposed an attachable browser session, rerun `dd-cli login`.");
|
|
929
982
|
const importedAfterGrace = await deps.waitForAttachedBrowserSessionImport({
|
|
930
983
|
timeoutMs: AUTH_BOOTSTRAP_NO_DISCOVERY_GRACE_MS,
|
|
931
984
|
pollIntervalMs: AUTH_BOOTSTRAP_POLL_INTERVAL_MS,
|
|
@@ -933,12 +986,12 @@ export async function bootstrapAuthSessionWithDeps(deps) {
|
|
|
933
986
|
const auth = await deps.checkAuthDirect();
|
|
934
987
|
if (importedAfterGrace) {
|
|
935
988
|
return auth.isLoggedIn
|
|
936
|
-
? buildAuthBootstrapSuccess(auth, "
|
|
989
|
+
? buildAuthBootstrapSuccess(auth, "An attachable browser session appeared a few seconds later and was saved for direct API use.")
|
|
937
990
|
: buildAuthBootstrapFailure(auth, "Detected browser session state after opening the browser, but the consumer still appears logged out or guest-only.");
|
|
938
991
|
}
|
|
939
992
|
return buildAuthBootstrapFailure(auth, openedBrowser
|
|
940
|
-
? "Opened DoorDash in your default browser, but this environment still isn't exposing
|
|
941
|
-
: "Couldn't open a watchable browser automatically, and this environment still isn't exposing
|
|
993
|
+
? "Opened DoorDash in your default browser, but this environment still isn't exposing an attachable browser session the CLI can import. Finish signing in there, make an attachable browser session discoverable, then rerun `dd-cli login`."
|
|
994
|
+
: "Couldn't open a watchable browser automatically, and this environment still isn't exposing an attachable browser session the CLI can import. Open the DoorDash home page manually, make an attachable browser session discoverable, then rerun `dd-cli login`.");
|
|
942
995
|
}
|
|
943
996
|
export async function bootstrapAuthSession() {
|
|
944
997
|
return bootstrapAuthSessionWithDeps({
|
|
@@ -948,10 +1001,12 @@ export async function bootstrapAuthSession() {
|
|
|
948
1001
|
markBrowserImportAttempted: () => session.markBrowserImportAttempted(),
|
|
949
1002
|
getAttachedBrowserCdpCandidates,
|
|
950
1003
|
getReachableCdpCandidates,
|
|
1004
|
+
describeDesktopBrowserReuseGap,
|
|
951
1005
|
openUrlInAttachedBrowser,
|
|
952
1006
|
openUrlInDefaultBrowser,
|
|
953
1007
|
waitForAttachedBrowserSessionImport,
|
|
954
1008
|
waitForManagedBrowserLogin,
|
|
1009
|
+
canPromptForManagedBrowserConfirmation,
|
|
955
1010
|
checkAuthDirect,
|
|
956
1011
|
log: (message) => console.error(message),
|
|
957
1012
|
});
|
|
@@ -1671,7 +1726,7 @@ async function importBrowserSessionFromCdpCandidates(candidates) {
|
|
|
1671
1726
|
let browser = null;
|
|
1672
1727
|
let tempPage = null;
|
|
1673
1728
|
try {
|
|
1674
|
-
browser = await chromium.connectOverCDP(cdpUrl);
|
|
1729
|
+
browser = await chromium.connectOverCDP(cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
|
|
1675
1730
|
const context = browser.contexts()[0];
|
|
1676
1731
|
if (!context) {
|
|
1677
1732
|
continue;
|
|
@@ -1731,6 +1786,26 @@ async function fetchConsumerViaPage(page) {
|
|
|
1731
1786
|
});
|
|
1732
1787
|
return parseGraphQlResponse("attachedBrowserConsumerImport", raw.status, raw.text);
|
|
1733
1788
|
}
|
|
1789
|
+
async function readLiveManagedBrowserAuth(page) {
|
|
1790
|
+
if (!page || page.isClosed()) {
|
|
1791
|
+
return buildAuthResult(null);
|
|
1792
|
+
}
|
|
1793
|
+
const consumerData = await fetchConsumerViaPage(page).catch(() => null);
|
|
1794
|
+
return buildAuthResult(consumerData?.consumer ?? null);
|
|
1795
|
+
}
|
|
1796
|
+
async function confirmManagedBrowserAuth(context, page) {
|
|
1797
|
+
const liveAuth = await readLiveManagedBrowserAuth(page);
|
|
1798
|
+
if (liveAuth.isLoggedIn) {
|
|
1799
|
+
if (context) {
|
|
1800
|
+
await saveContextState(context).catch(() => { });
|
|
1801
|
+
}
|
|
1802
|
+
return liveAuth;
|
|
1803
|
+
}
|
|
1804
|
+
if (context) {
|
|
1805
|
+
await saveContextState(context).catch(() => { });
|
|
1806
|
+
}
|
|
1807
|
+
return (await getPersistedAuthDirect().catch(() => null)) ?? liveAuth;
|
|
1808
|
+
}
|
|
1734
1809
|
async function saveContextState(context, cookies = null) {
|
|
1735
1810
|
const storageStatePath = getStorageStatePath();
|
|
1736
1811
|
await ensureConfigDir();
|
|
@@ -1869,7 +1944,7 @@ async function openUrlInDefaultBrowser(targetUrl) {
|
|
|
1869
1944
|
async function openUrlInAttachedBrowser(input) {
|
|
1870
1945
|
let browser = null;
|
|
1871
1946
|
try {
|
|
1872
|
-
browser = await chromium.connectOverCDP(input.cdpUrl);
|
|
1947
|
+
browser = await chromium.connectOverCDP(input.cdpUrl, { timeout: ATTACHED_BROWSER_CDP_CONNECT_TIMEOUT_MS });
|
|
1873
1948
|
const context = browser.contexts()[0];
|
|
1874
1949
|
if (!context) {
|
|
1875
1950
|
return false;
|
|
@@ -1893,6 +1968,7 @@ async function waitForManagedBrowserLogin(input) {
|
|
|
1893
1968
|
let browser = null;
|
|
1894
1969
|
let context = null;
|
|
1895
1970
|
let page = null;
|
|
1971
|
+
const manualConfirmation = createManagedBrowserManualConfirmationHandle();
|
|
1896
1972
|
try {
|
|
1897
1973
|
browser = await chromium.launch({
|
|
1898
1974
|
headless: false,
|
|
@@ -1917,10 +1993,25 @@ async function waitForManagedBrowserLogin(input) {
|
|
|
1917
1993
|
await page.bringToFront().catch(() => { });
|
|
1918
1994
|
const deadline = Date.now() + input.timeoutMs;
|
|
1919
1995
|
while (Date.now() <= deadline) {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
return
|
|
1996
|
+
const liveAuth = await readLiveManagedBrowserAuth(page);
|
|
1997
|
+
if (liveAuth.isLoggedIn) {
|
|
1998
|
+
await saveContextState(context).catch(() => { });
|
|
1999
|
+
return {
|
|
2000
|
+
status: "completed",
|
|
2001
|
+
completion: "automatic",
|
|
2002
|
+
auth: liveAuth,
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
if (manualConfirmation.consumeRequested()) {
|
|
2006
|
+
const confirmedAuth = await confirmManagedBrowserAuth(context, page);
|
|
2007
|
+
if (confirmedAuth.isLoggedIn) {
|
|
2008
|
+
return {
|
|
2009
|
+
status: "completed",
|
|
2010
|
+
completion: "manual",
|
|
2011
|
+
auth: confirmedAuth,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
input.log("Still not seeing an authenticated DoorDash session yet. Finish signing in in the opened browser window, then press Enter again or keep waiting for automatic detection.");
|
|
1924
2015
|
}
|
|
1925
2016
|
if (page.isClosed()) {
|
|
1926
2017
|
break;
|
|
@@ -1930,16 +2021,27 @@ async function waitForManagedBrowserLogin(input) {
|
|
|
1930
2021
|
}
|
|
1931
2022
|
await wait(input.pollIntervalMs);
|
|
1932
2023
|
}
|
|
1933
|
-
await
|
|
1934
|
-
|
|
2024
|
+
const finalAuth = await confirmManagedBrowserAuth(context, page);
|
|
2025
|
+
if (finalAuth.isLoggedIn) {
|
|
2026
|
+
return {
|
|
2027
|
+
status: "completed",
|
|
2028
|
+
completion: "automatic",
|
|
2029
|
+
auth: finalAuth,
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
status: "timed-out",
|
|
2034
|
+
auth: finalAuth,
|
|
2035
|
+
};
|
|
1935
2036
|
}
|
|
1936
2037
|
catch (error) {
|
|
1937
2038
|
if (isPlaywrightBrowserInstallMissingError(error)) {
|
|
1938
|
-
return
|
|
2039
|
+
return { status: "launch-failed" };
|
|
1939
2040
|
}
|
|
1940
|
-
return
|
|
2041
|
+
return { status: "launch-failed" };
|
|
1941
2042
|
}
|
|
1942
2043
|
finally {
|
|
2044
|
+
manualConfirmation.close();
|
|
1943
2045
|
await page?.close().catch(() => { });
|
|
1944
2046
|
await context?.close().catch(() => { });
|
|
1945
2047
|
await browser?.close().catch(() => { });
|
|
@@ -1952,6 +2054,40 @@ function isPlaywrightBrowserInstallMissingError(error) {
|
|
|
1952
2054
|
const message = error.message.toLowerCase();
|
|
1953
2055
|
return message.includes("executable doesn't exist") || message.includes("please run the following command") || message.includes("playwright install");
|
|
1954
2056
|
}
|
|
2057
|
+
const KNOWN_DESKTOP_BROWSERS = [
|
|
2058
|
+
{
|
|
2059
|
+
label: "Brave",
|
|
2060
|
+
processMatchers: [/\bbrave-browser(?:-stable)?\b/i, /\/brave(?:$|\s)/i],
|
|
2061
|
+
devToolsActivePortPaths: [
|
|
2062
|
+
join(homedir(), ".config", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
|
|
2063
|
+
join(homedir(), "Library", "Application Support", "BraveSoftware", "Brave-Browser", "DevToolsActivePort"),
|
|
2064
|
+
],
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
label: "Google Chrome",
|
|
2068
|
+
processMatchers: [/\bgoogle-chrome(?:-stable|-beta|-unstable)?\b/i, /\/google-chrome(?:$|\s)/i],
|
|
2069
|
+
devToolsActivePortPaths: [
|
|
2070
|
+
join(homedir(), ".config", "google-chrome", "DevToolsActivePort"),
|
|
2071
|
+
join(homedir(), "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
|
|
2072
|
+
],
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
label: "Microsoft Edge",
|
|
2076
|
+
processMatchers: [/\bmicrosoft-edge(?:-stable|-beta|-dev)?\b/i, /\/microsoft-edge(?:$|\s)/i],
|
|
2077
|
+
devToolsActivePortPaths: [
|
|
2078
|
+
join(homedir(), ".config", "microsoft-edge", "DevToolsActivePort"),
|
|
2079
|
+
join(homedir(), "Library", "Application Support", "Microsoft Edge", "DevToolsActivePort"),
|
|
2080
|
+
],
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
label: "Chromium",
|
|
2084
|
+
processMatchers: [/\bchromium-browser\b/i, /\bchromium\b/i],
|
|
2085
|
+
devToolsActivePortPaths: [
|
|
2086
|
+
join(homedir(), ".config", "chromium", "DevToolsActivePort"),
|
|
2087
|
+
join(homedir(), "Library", "Application Support", "Chromium", "DevToolsActivePort"),
|
|
2088
|
+
],
|
|
2089
|
+
},
|
|
2090
|
+
];
|
|
1955
2091
|
function normalizeCdpCandidate(value) {
|
|
1956
2092
|
return value.trim().replace(/\/$/, "");
|
|
1957
2093
|
}
|
|
@@ -2001,6 +2137,82 @@ function wait(ms) {
|
|
|
2001
2137
|
setTimeout(resolve, ms);
|
|
2002
2138
|
});
|
|
2003
2139
|
}
|
|
2140
|
+
function detectRunningDesktopBrowser(processCommands) {
|
|
2141
|
+
const filteredCommands = processCommands
|
|
2142
|
+
.map((command) => command.trim())
|
|
2143
|
+
.filter((command) => command.length > 0)
|
|
2144
|
+
.filter((command) => !/chrome-devtools-mcp/i.test(command));
|
|
2145
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2146
|
+
const preferredMatch = filteredCommands.some((command) => !/--type=|crashpad|zygote/i.test(command) && browser.processMatchers.some((matcher) => matcher.test(command)));
|
|
2147
|
+
if (preferredMatch) {
|
|
2148
|
+
return browser;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2152
|
+
if (filteredCommands.some((command) => browser.processMatchers.some((matcher) => matcher.test(command)))) {
|
|
2153
|
+
return browser;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return null;
|
|
2157
|
+
}
|
|
2158
|
+
function hasRemoteDebuggingSignal(processCommands) {
|
|
2159
|
+
return processCommands.some((command) => /--remote-debugging-(?:port|pipe)(?:=|\b)/i.test(command));
|
|
2160
|
+
}
|
|
2161
|
+
export function summarizeDesktopBrowserReuseGap(input) {
|
|
2162
|
+
const browser = detectRunningDesktopBrowser(input.processCommands);
|
|
2163
|
+
if (!browser) {
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
if (hasRemoteDebuggingSignal(input.processCommands) || input.hasAnyDevToolsActivePort) {
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
return `I can see ${browser.label} is already running on this desktop, but it is not exposing an attachable browser automation session right now. A normal open browser window is not automatically reusable; dd-cli can only import a browser session it can actually attach to.`;
|
|
2170
|
+
}
|
|
2171
|
+
async function captureCommandStdout(command, args) {
|
|
2172
|
+
return await new Promise((resolve, reject) => {
|
|
2173
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2174
|
+
const stdout = [];
|
|
2175
|
+
child.once("error", reject);
|
|
2176
|
+
child.stdout?.on("data", (chunk) => {
|
|
2177
|
+
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2178
|
+
});
|
|
2179
|
+
child.once("close", (code) => {
|
|
2180
|
+
if (code === 0) {
|
|
2181
|
+
resolve(Buffer.concat(stdout).toString("utf8"));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
async function listProcessCommands(targetPlatform = process.platform) {
|
|
2189
|
+
if (targetPlatform === "win32") {
|
|
2190
|
+
return [];
|
|
2191
|
+
}
|
|
2192
|
+
const args = targetPlatform === "darwin" ? ["-axo", "command="] : ["-eo", "command="];
|
|
2193
|
+
const stdout = await captureCommandStdout("ps", args).catch(() => "");
|
|
2194
|
+
return stdout
|
|
2195
|
+
.split(/\r?\n/)
|
|
2196
|
+
.map((command) => command.trim())
|
|
2197
|
+
.filter((command) => command.length > 0);
|
|
2198
|
+
}
|
|
2199
|
+
async function hasAnyKnownDevToolsActivePort() {
|
|
2200
|
+
for (const browser of KNOWN_DESKTOP_BROWSERS) {
|
|
2201
|
+
for (const path of browser.devToolsActivePortPaths) {
|
|
2202
|
+
if ((await readFile(path, "utf8").catch(() => null)) !== null) {
|
|
2203
|
+
return true;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return false;
|
|
2208
|
+
}
|
|
2209
|
+
async function describeDesktopBrowserReuseGap() {
|
|
2210
|
+
const processCommands = await listProcessCommands();
|
|
2211
|
+
return summarizeDesktopBrowserReuseGap({
|
|
2212
|
+
processCommands,
|
|
2213
|
+
hasAnyDevToolsActivePort: await hasAnyKnownDevToolsActivePort(),
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2004
2216
|
async function readOpenClawBrowserConfigCandidates(input) {
|
|
2005
2217
|
try {
|
|
2006
2218
|
const raw = await readFile(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
|
|
@@ -2021,7 +2233,9 @@ async function readOpenClawBrowserConfigCandidates(input) {
|
|
|
2021
2233
|
}
|
|
2022
2234
|
async function isCdpEndpointReachable(cdpUrl) {
|
|
2023
2235
|
try {
|
|
2024
|
-
const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version
|
|
2236
|
+
const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version`, {
|
|
2237
|
+
signal: AbortSignal.timeout(ATTACHED_BROWSER_CDP_REACHABILITY_TIMEOUT_MS),
|
|
2238
|
+
});
|
|
2025
2239
|
return response.ok;
|
|
2026
2240
|
}
|
|
2027
2241
|
catch {
|
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, } from "./direct-api.js";
|
|
3
|
+
import { bootstrapAuthSessionWithDeps, buildAddConsumerAddressPayload, buildAddToCartPayload, buildUpdateCartPayload, extractExistingOrdersFromApolloCache, normalizeItemName, parseExistingOrderLifecycleStatus, parseExistingOrdersResponse, parseOptionSelectionsJson, parseSearchRestaurantRow, resolveAttachedBrowserCdpCandidates, resolveAvailableAddressMatch, resolveSystemBrowserOpenCommand, selectAttachedBrowserImportMode, summarizeDesktopBrowserReuseGap, } from "./direct-api.js";
|
|
4
4
|
function configurableItemDetail() {
|
|
5
5
|
return {
|
|
6
6
|
success: true,
|
|
@@ -181,6 +181,29 @@ 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", () => {
|
|
185
|
+
const message = summarizeDesktopBrowserReuseGap({
|
|
186
|
+
processCommands: [
|
|
187
|
+
"/bin/bash /usr/bin/brave-browser-stable",
|
|
188
|
+
"/opt/brave.com/brave/brave",
|
|
189
|
+
"/opt/brave.com/brave/brave --type=renderer",
|
|
190
|
+
],
|
|
191
|
+
hasAnyDevToolsActivePort: false,
|
|
192
|
+
});
|
|
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);
|
|
196
|
+
});
|
|
197
|
+
test("summarizeDesktopBrowserReuseGap stays quiet once the browser exposes attach signals", () => {
|
|
198
|
+
assert.equal(summarizeDesktopBrowserReuseGap({
|
|
199
|
+
processCommands: ["/bin/bash /usr/bin/brave-browser-stable --remote-debugging-port=9222"],
|
|
200
|
+
hasAnyDevToolsActivePort: false,
|
|
201
|
+
}), null);
|
|
202
|
+
assert.equal(summarizeDesktopBrowserReuseGap({
|
|
203
|
+
processCommands: ["/bin/bash /usr/bin/brave-browser-stable"],
|
|
204
|
+
hasAnyDevToolsActivePort: true,
|
|
205
|
+
}), null);
|
|
206
|
+
});
|
|
184
207
|
test("selectAttachedBrowserImportMode treats an authenticated browser with DoorDash cookies as an immediate import candidate", () => {
|
|
185
208
|
assert.equal(selectAttachedBrowserImportMode({
|
|
186
209
|
pageUrls: ["https://github.com/LatencyTDH/doordash-cli/pulls"],
|
|
@@ -229,6 +252,7 @@ test("bootstrapAuthSessionWithDeps returns immediately when saved local auth is
|
|
|
229
252
|
getReachableCdpCandidates: async () => {
|
|
230
253
|
throw new Error("should not probe reachability when saved auth is already valid");
|
|
231
254
|
},
|
|
255
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
232
256
|
openUrlInAttachedBrowser: async () => {
|
|
233
257
|
throw new Error("should not try to open an attached browser when saved auth is already valid");
|
|
234
258
|
},
|
|
@@ -243,6 +267,7 @@ test("bootstrapAuthSessionWithDeps returns immediately when saved local auth is
|
|
|
243
267
|
waitForManagedBrowserLogin: async () => {
|
|
244
268
|
throw new Error("should not launch a managed browser when saved auth is already valid");
|
|
245
269
|
},
|
|
270
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
246
271
|
checkAuthDirect: async () => {
|
|
247
272
|
throw new Error("should not re-check auth through the live session when saved auth is already valid");
|
|
248
273
|
},
|
|
@@ -288,6 +313,7 @@ test("bootstrapAuthSessionWithDeps returns immediately when an attached browser
|
|
|
288
313
|
getReachableCdpCandidates: async () => {
|
|
289
314
|
throw new Error("should not probe reachability on immediate browser-session import");
|
|
290
315
|
},
|
|
316
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
291
317
|
openUrlInAttachedBrowser: async () => {
|
|
292
318
|
throw new Error("should not open a browser when immediate browser-session import succeeded");
|
|
293
319
|
},
|
|
@@ -302,6 +328,7 @@ test("bootstrapAuthSessionWithDeps returns immediately when an attached browser
|
|
|
302
328
|
waitForManagedBrowserLogin: async () => {
|
|
303
329
|
throw new Error("should not launch a managed browser when immediate browser-session import succeeded");
|
|
304
330
|
},
|
|
331
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
305
332
|
checkAuthDirect: async () => auth,
|
|
306
333
|
log: (message) => {
|
|
307
334
|
logs.push(message);
|
|
@@ -343,6 +370,7 @@ test("bootstrapAuthSessionWithDeps opens a watchable attached browser session be
|
|
|
343
370
|
reachableCalls += 1;
|
|
344
371
|
return candidates;
|
|
345
372
|
},
|
|
373
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
346
374
|
openUrlInAttachedBrowser: async () => {
|
|
347
375
|
openAttachedCalls += 1;
|
|
348
376
|
return true;
|
|
@@ -359,6 +387,7 @@ test("bootstrapAuthSessionWithDeps opens a watchable attached browser session be
|
|
|
359
387
|
waitForManagedBrowserLogin: async () => {
|
|
360
388
|
throw new Error("should not launch a managed browser when an attached browser is reachable");
|
|
361
389
|
},
|
|
390
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
362
391
|
checkAuthDirect: async () => auth,
|
|
363
392
|
log: (message) => {
|
|
364
393
|
logs.push(message);
|
|
@@ -369,11 +398,11 @@ test("bootstrapAuthSessionWithDeps opens a watchable attached browser session be
|
|
|
369
398
|
assert.equal(openDefaultCalls, 0);
|
|
370
399
|
assert.equal(waitCalls, 1);
|
|
371
400
|
assert.equal(waitTimeoutMs, 180_000);
|
|
372
|
-
assert.match(logs.join("\n"), /Opened DoorDash in the
|
|
373
|
-
assert.match(logs.join("\n"), /Detected 1
|
|
401
|
+
assert.match(logs.join("\n"), /Opened DoorDash in the attachable browser session I'm watching/);
|
|
402
|
+
assert.match(logs.join("\n"), /Detected 1 attachable browser session/);
|
|
374
403
|
assert.match(result.message, /saved it for direct API use/);
|
|
375
404
|
});
|
|
376
|
-
test("bootstrapAuthSessionWithDeps falls back to a managed browser login window
|
|
405
|
+
test("bootstrapAuthSessionWithDeps falls back to a managed browser login window and auto-completes when it can prove login", async () => {
|
|
377
406
|
const auth = {
|
|
378
407
|
success: true,
|
|
379
408
|
isLoggedIn: true,
|
|
@@ -396,6 +425,7 @@ test("bootstrapAuthSessionWithDeps falls back to a managed browser login window
|
|
|
396
425
|
markBrowserImportAttempted: () => { },
|
|
397
426
|
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
398
427
|
getReachableCdpCandidates: async () => [],
|
|
428
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
399
429
|
openUrlInAttachedBrowser: async () => false,
|
|
400
430
|
openUrlInDefaultBrowser: async () => true,
|
|
401
431
|
waitForAttachedBrowserSessionImport: async () => {
|
|
@@ -404,8 +434,13 @@ test("bootstrapAuthSessionWithDeps falls back to a managed browser login window
|
|
|
404
434
|
},
|
|
405
435
|
waitForManagedBrowserLogin: async () => {
|
|
406
436
|
managedCalls += 1;
|
|
407
|
-
return
|
|
437
|
+
return {
|
|
438
|
+
status: "completed",
|
|
439
|
+
completion: "automatic",
|
|
440
|
+
auth,
|
|
441
|
+
};
|
|
408
442
|
},
|
|
443
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
409
444
|
checkAuthDirect: async () => auth,
|
|
410
445
|
log: (message) => {
|
|
411
446
|
logs.push(message);
|
|
@@ -414,10 +449,143 @@ test("bootstrapAuthSessionWithDeps falls back to a managed browser login window
|
|
|
414
449
|
assert.equal(managedCalls, 1);
|
|
415
450
|
assert.equal(attachedWaitCalls, 0);
|
|
416
451
|
assert.match(logs.join("\n"), /temporary Chromium login window/);
|
|
417
|
-
assert.match(
|
|
452
|
+
assert.match(logs.join("\n"), /press Enter here to force an immediate recheck/i);
|
|
453
|
+
assert.match(result.message, /detected the signed-in session there automatically/i);
|
|
454
|
+
assert.equal(result.success, true);
|
|
455
|
+
assert.equal(result.isLoggedIn, true);
|
|
456
|
+
});
|
|
457
|
+
test("bootstrapAuthSessionWithDeps logs why an already-open desktop browser still is not reusable", async () => {
|
|
458
|
+
const auth = {
|
|
459
|
+
success: true,
|
|
460
|
+
isLoggedIn: true,
|
|
461
|
+
email: "user@example.com",
|
|
462
|
+
firstName: "Test",
|
|
463
|
+
lastName: "User",
|
|
464
|
+
consumerId: "consumer-1",
|
|
465
|
+
marketId: "market-1",
|
|
466
|
+
defaultAddress: null,
|
|
467
|
+
cookiesPath: "/tmp/cookies.json",
|
|
468
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
469
|
+
};
|
|
470
|
+
const logs = [];
|
|
471
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
472
|
+
clearBlockedBrowserImport: async () => { },
|
|
473
|
+
checkPersistedAuth: async () => null,
|
|
474
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
475
|
+
markBrowserImportAttempted: () => { },
|
|
476
|
+
getAttachedBrowserCdpCandidates: async () => [],
|
|
477
|
+
getReachableCdpCandidates: async () => [],
|
|
478
|
+
describeDesktopBrowserReuseGap: async () => "I can see Brave is already running on this desktop, but it is not exposing an attachable browser automation session right now.",
|
|
479
|
+
openUrlInAttachedBrowser: async () => false,
|
|
480
|
+
openUrlInDefaultBrowser: async () => true,
|
|
481
|
+
waitForAttachedBrowserSessionImport: async () => false,
|
|
482
|
+
waitForManagedBrowserLogin: async () => ({
|
|
483
|
+
status: "completed",
|
|
484
|
+
completion: "automatic",
|
|
485
|
+
auth,
|
|
486
|
+
}),
|
|
487
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
488
|
+
checkAuthDirect: async () => auth,
|
|
489
|
+
log: (message) => {
|
|
490
|
+
logs.push(message);
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
assert.match(logs.join("\n"), /Brave is already running on this desktop/i);
|
|
494
|
+
assert.match(logs.join("\n"), /couldn't find an attachable browser session I can reuse/i);
|
|
418
495
|
assert.equal(result.success, true);
|
|
419
496
|
assert.equal(result.isLoggedIn, true);
|
|
420
497
|
});
|
|
498
|
+
test("bootstrapAuthSessionWithDeps restores an explicit Enter-style completion path for the managed browser fallback", async () => {
|
|
499
|
+
const auth = {
|
|
500
|
+
success: true,
|
|
501
|
+
isLoggedIn: true,
|
|
502
|
+
email: "user@example.com",
|
|
503
|
+
firstName: "Test",
|
|
504
|
+
lastName: "User",
|
|
505
|
+
consumerId: "consumer-1",
|
|
506
|
+
marketId: "market-1",
|
|
507
|
+
defaultAddress: null,
|
|
508
|
+
cookiesPath: "/tmp/cookies.json",
|
|
509
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
510
|
+
};
|
|
511
|
+
const logs = [];
|
|
512
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
513
|
+
clearBlockedBrowserImport: async () => { },
|
|
514
|
+
checkPersistedAuth: async () => null,
|
|
515
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
516
|
+
markBrowserImportAttempted: () => { },
|
|
517
|
+
getAttachedBrowserCdpCandidates: async () => [],
|
|
518
|
+
getReachableCdpCandidates: async () => [],
|
|
519
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
520
|
+
openUrlInAttachedBrowser: async () => false,
|
|
521
|
+
openUrlInDefaultBrowser: async () => true,
|
|
522
|
+
waitForAttachedBrowserSessionImport: async () => false,
|
|
523
|
+
waitForManagedBrowserLogin: async () => ({
|
|
524
|
+
status: "completed",
|
|
525
|
+
completion: "manual",
|
|
526
|
+
auth,
|
|
527
|
+
}),
|
|
528
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
529
|
+
checkAuthDirect: async () => auth,
|
|
530
|
+
log: (message) => {
|
|
531
|
+
logs.push(message);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
assert.match(logs.join("\n"), /press Enter here to force an immediate recheck/i);
|
|
535
|
+
assert.match(result.message, /After you pressed Enter to confirm the browser login was complete/i);
|
|
536
|
+
assert.equal(result.success, true);
|
|
537
|
+
assert.equal(result.isLoggedIn, true);
|
|
538
|
+
});
|
|
539
|
+
test("bootstrapAuthSessionWithDeps returns a bounded failure instead of a dead-end when managed browser auto-detection cannot prove login", async () => {
|
|
540
|
+
const auth = {
|
|
541
|
+
success: true,
|
|
542
|
+
isLoggedIn: false,
|
|
543
|
+
email: null,
|
|
544
|
+
firstName: null,
|
|
545
|
+
lastName: null,
|
|
546
|
+
consumerId: null,
|
|
547
|
+
marketId: null,
|
|
548
|
+
defaultAddress: null,
|
|
549
|
+
cookiesPath: "/tmp/cookies.json",
|
|
550
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
551
|
+
};
|
|
552
|
+
let attachedWaitCalls = 0;
|
|
553
|
+
let attachedWaitTimeoutMs = 0;
|
|
554
|
+
const logs = [];
|
|
555
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
556
|
+
clearBlockedBrowserImport: async () => { },
|
|
557
|
+
checkPersistedAuth: async () => null,
|
|
558
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
559
|
+
markBrowserImportAttempted: () => { },
|
|
560
|
+
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
561
|
+
getReachableCdpCandidates: async () => [],
|
|
562
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
563
|
+
openUrlInAttachedBrowser: async () => false,
|
|
564
|
+
openUrlInDefaultBrowser: async () => true,
|
|
565
|
+
waitForAttachedBrowserSessionImport: async (input) => {
|
|
566
|
+
attachedWaitCalls += 1;
|
|
567
|
+
attachedWaitTimeoutMs = input.timeoutMs;
|
|
568
|
+
return false;
|
|
569
|
+
},
|
|
570
|
+
waitForManagedBrowserLogin: async () => ({
|
|
571
|
+
status: "timed-out",
|
|
572
|
+
auth,
|
|
573
|
+
}),
|
|
574
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
575
|
+
checkAuthDirect: async () => auth,
|
|
576
|
+
log: (message) => {
|
|
577
|
+
logs.push(message);
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
assert.equal(attachedWaitCalls, 0);
|
|
581
|
+
assert.equal(attachedWaitTimeoutMs, 0);
|
|
582
|
+
assert.match(logs.join("\n"), /temporary Chromium login window/i);
|
|
583
|
+
assert.match(logs.join("\n"), /press Enter here to force an immediate recheck/i);
|
|
584
|
+
assert.equal(result.success, false);
|
|
585
|
+
assert.equal(result.isLoggedIn, false);
|
|
586
|
+
assert.match(result.message, /couldn't prove an authenticated DoorDash session/i);
|
|
587
|
+
assert.match(result.message, /press Enter sooner next time/i);
|
|
588
|
+
});
|
|
421
589
|
test("bootstrapAuthSessionWithDeps falls back to quick troubleshooting guidance when the managed browser login window cannot launch", async () => {
|
|
422
590
|
const auth = {
|
|
423
591
|
success: true,
|
|
@@ -441,6 +609,7 @@ test("bootstrapAuthSessionWithDeps falls back to quick troubleshooting guidance
|
|
|
441
609
|
markBrowserImportAttempted: () => { },
|
|
442
610
|
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
443
611
|
getReachableCdpCandidates: async () => [],
|
|
612
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
444
613
|
openUrlInAttachedBrowser: async () => false,
|
|
445
614
|
openUrlInDefaultBrowser: async () => true,
|
|
446
615
|
waitForAttachedBrowserSessionImport: async (input) => {
|
|
@@ -448,7 +617,8 @@ test("bootstrapAuthSessionWithDeps falls back to quick troubleshooting guidance
|
|
|
448
617
|
attachedWaitTimeoutMs = input.timeoutMs;
|
|
449
618
|
return false;
|
|
450
619
|
},
|
|
451
|
-
waitForManagedBrowserLogin: async () =>
|
|
620
|
+
waitForManagedBrowserLogin: async () => ({ status: "launch-failed" }),
|
|
621
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
452
622
|
checkAuthDirect: async () => auth,
|
|
453
623
|
log: (message) => {
|
|
454
624
|
logs.push(message);
|
|
@@ -460,7 +630,53 @@ test("bootstrapAuthSessionWithDeps falls back to quick troubleshooting guidance
|
|
|
460
630
|
assert.match(logs.join("\n"), /won't keep you waiting for the full login timeout/i);
|
|
461
631
|
assert.equal(result.success, false);
|
|
462
632
|
assert.equal(result.isLoggedIn, false);
|
|
463
|
-
assert.match(result.message, /still isn't exposing
|
|
633
|
+
assert.match(result.message, /still isn't exposing an attachable browser session/);
|
|
634
|
+
});
|
|
635
|
+
test("bootstrapAuthSessionWithDeps clears the logout block before an explicit login reuses an attached browser session", async () => {
|
|
636
|
+
const auth = {
|
|
637
|
+
success: true,
|
|
638
|
+
isLoggedIn: true,
|
|
639
|
+
email: "user@example.com",
|
|
640
|
+
firstName: "Test",
|
|
641
|
+
lastName: "User",
|
|
642
|
+
consumerId: "consumer-1",
|
|
643
|
+
marketId: "market-1",
|
|
644
|
+
defaultAddress: null,
|
|
645
|
+
cookiesPath: "/tmp/cookies.json",
|
|
646
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
647
|
+
};
|
|
648
|
+
let blocked = true;
|
|
649
|
+
let clearCalls = 0;
|
|
650
|
+
let importCalls = 0;
|
|
651
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
652
|
+
clearBlockedBrowserImport: async () => {
|
|
653
|
+
clearCalls += 1;
|
|
654
|
+
blocked = false;
|
|
655
|
+
},
|
|
656
|
+
checkPersistedAuth: async () => null,
|
|
657
|
+
importBrowserSessionIfAvailable: async () => {
|
|
658
|
+
importCalls += 1;
|
|
659
|
+
return blocked === false;
|
|
660
|
+
},
|
|
661
|
+
markBrowserImportAttempted: () => { },
|
|
662
|
+
getAttachedBrowserCdpCandidates: async () => [],
|
|
663
|
+
getReachableCdpCandidates: async () => [],
|
|
664
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
665
|
+
openUrlInAttachedBrowser: async () => false,
|
|
666
|
+
openUrlInDefaultBrowser: async () => false,
|
|
667
|
+
waitForAttachedBrowserSessionImport: async () => false,
|
|
668
|
+
waitForManagedBrowserLogin: async () => {
|
|
669
|
+
throw new Error("should not launch a managed browser when explicit login can immediately reuse an attached browser session");
|
|
670
|
+
},
|
|
671
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
672
|
+
checkAuthDirect: async () => auth,
|
|
673
|
+
log: () => { },
|
|
674
|
+
});
|
|
675
|
+
assert.equal(clearCalls, 1);
|
|
676
|
+
assert.equal(importCalls, 1);
|
|
677
|
+
assert.equal(result.success, true);
|
|
678
|
+
assert.equal(result.isLoggedIn, true);
|
|
679
|
+
assert.match(result.message, /Imported an existing signed-in browser session/);
|
|
464
680
|
});
|
|
465
681
|
test("parseOptionSelectionsJson parses structured recursive option selections", () => {
|
|
466
682
|
assert.deepEqual(parseOptionSelectionsJson('[{"groupId":"703393388","optionId":"4716032529"},{"groupId":"recommended_option_546935995","optionId":"546936011","children":[{"groupId":"780057412","optionId":"4702669757","quantity":2}]}]'), [
|
package/docs/examples.md
CHANGED
|
@@ -26,13 +26,13 @@ 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 signed-in browser session,
|
|
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:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
doordash-cli login
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Reset saved session state when you want a clean logged-out start. This also disables
|
|
35
|
+
Reset saved session state when you want a clean logged-out start. This also disables passive browser-session reuse until your next explicit `doordash-cli login`:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
doordash-cli logout
|
package/docs/install.md
CHANGED
|
@@ -60,14 +60,14 @@ 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 signed-in browser session. If
|
|
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.
|
|
64
64
|
|
|
65
|
-
`doordash-cli auth-check` can also quietly import a discoverable signed-in browser session unless `doordash-cli logout` disabled that auto-reuse.
|
|
65
|
+
`doordash-cli auth-check` can also quietly import a discoverable attachable signed-in browser session unless `doordash-cli logout` disabled that auto-reuse.
|
|
66
66
|
|
|
67
|
-
`doordash-cli logout` clears persisted cookies and stored browser state, then keeps
|
|
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
|
|
|
69
69
|
## Browser-session troubleshooting
|
|
70
70
|
|
|
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.
|
|
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
|
|
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.
|
package/man/dd-cli.1
CHANGED
|
@@ -35,20 +35,25 @@ 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 browser session when
|
|
39
|
-
available, unless
|
|
38
|
+
quietly reuse or import an already-signed-in attachable browser session when
|
|
39
|
+
one is available, unless
|
|
40
40
|
.B logout
|
|
41
41
|
explicitly disabled that auto-reuse.
|
|
42
42
|
.TP
|
|
43
43
|
.B login
|
|
44
44
|
Reuse saved local auth when possible. Otherwise try to import a discoverable
|
|
45
|
-
signed-in browser session
|
|
46
|
-
|
|
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,
|
|
47
52
|
.B login
|
|
48
53
|
exits non-zero.
|
|
49
54
|
.TP
|
|
50
55
|
.B logout
|
|
51
|
-
Delete stored session material used by this CLI and disable
|
|
56
|
+
Delete stored session material used by this CLI and disable passive
|
|
52
57
|
browser-session reuse until the next explicit
|
|
53
58
|
.BR login .
|
|
54
59
|
.TP
|
|
@@ -205,14 +210,17 @@ time, but the saved state is reused across runs unless cleared with
|
|
|
205
210
|
.BR logout .
|
|
206
211
|
After
|
|
207
212
|
.BR logout ,
|
|
208
|
-
|
|
213
|
+
passive browser-session reuse stays disabled until the next explicit
|
|
209
214
|
.BR login .
|
|
210
215
|
.SH ENVIRONMENT
|
|
211
216
|
.PP
|
|
212
217
|
In the common case you should not need to configure browser plumbing manually.
|
|
213
218
|
For attached-browser reuse, the CLI can probe compatible CDP URLs/ports and a
|
|
214
|
-
few localhost defaults.
|
|
215
|
-
|
|
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
|
|
216
224
|
.I docs/install.md
|
|
217
225
|
for setup and troubleshooting.
|
|
218
226
|
.SH EXIT STATUS
|
package/package.json
CHANGED