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 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 a discoverable signed-in browser session when possible, and otherwise opens a temporary login window.
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 neither is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
73
+ `login` reuses saved local auth when it is still valid. Otherwise it 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 automatic browser-session reuse disabled until you explicitly run `dd-cli login` again.
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, finish signing in there and let the CLI save the session. If you expect reuse from another browser, make sure it exposes a compatible CDP endpoint, then rerun `dd-cli login`.
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 a signed-in browser session or opens a temporary Chromium login window.",
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 a signed-in browser session unless logout disabled that auto-reuse.",
48
- " - logout clears saved session files and keeps automatic browser-session reuse off until the next login.",
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 a signed-in browser session or opens a temporary Chromium login window\./);
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 a signed-in browser session unless logout disabled that auto-reuse\./);
119
- assert.match(result.stdout, /logout clears saved session files and keeps automatic browser-session reuse off until the next login\./);
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"), /automatic\s+browser-session reuse stays disabled until the next explicit/i);
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");
@@ -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
- }) => Promise<AuthResult | null>;
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;
@@ -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 reusable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
933
+ ? `Opened DoorDash in the attachable browser session I'm watching: ${AUTH_BOOTSTRAP_URL}`
895
934
  : openedDefaultBrowser
896
- ? `Found a reusable browser connection, but couldn't drive it directly, so I opened DoorDash in your default browser: ${AUTH_BOOTSTRAP_URL}`
897
- : `Detected a reusable browser connection, but couldn't open DoorDash automatically. Open this URL in that watched browser to continue: ${AUTH_BOOTSTRAP_URL}`);
898
- deps.log(`Detected ${reachableCandidates.length} reusable browser connection(s). Finish the sign-in in that browser window and I'll import it automatically for up to ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds.`);
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 a reusable browser session, detected the signed-in consumer state, and saved it for direct API use.")
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 reusable browser connections for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no signed-in DoorDash session was imported. Finish the login in that watched browser and rerun dd-cli login.`
911
- : `Watched reusable browser connections for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds without importing a signed-in DoorDash session. Open ${AUTH_BOOTSTRAP_URL} manually in the watched browser, finish signing in, then rerun dd-cli login.`);
912
- }
913
- deps.log("I couldn't find a reusable browser connection, so I'm opening a temporary Chromium login window that the CLI can watch directly.");
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.isLoggedIn
921
- ? buildAuthBootstrapSuccess(managedAuth, "Opened a temporary Chromium login window, detected the signed-in session there, and saved it for direct API use.")
922
- : buildAuthBootstrapFailure(managedAuth, `Opened a temporary Chromium login window and watched for ${Math.round(AUTH_BOOTSTRAP_TIMEOUT_MS / 1000)} seconds, but no authenticated DoorDash session was established.`);
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 a reusable browser session the CLI can import, so I won't keep you waiting for the full login timeout. Once you've exposed a compatible browser session, rerun `dd-cli login`.");
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, "A reusable browser session appeared a few seconds later and was saved for direct API use.")
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 a reusable browser session the CLI can import. Finish signing in there, make a compatible browser session discoverable, then rerun `dd-cli login`."
941
- : "Couldn't open a watchable browser automatically, and this environment still isn't exposing a reusable browser session the CLI can import. Open the DoorDash home page manually, make a compatible browser session discoverable, then rerun `dd-cli login`.");
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
- await saveContextState(context).catch(() => { });
1921
- const auth = await getPersistedAuthDirect();
1922
- if (auth?.isLoggedIn) {
1923
- return auth;
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 saveContextState(context).catch(() => { });
1934
- return (await getPersistedAuthDirect()) ?? buildAuthResult(null);
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 null;
2039
+ return { status: "launch-failed" };
1939
2040
  }
1940
- return null;
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 {
@@ -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 reusable browser session I'm watching/);
373
- assert.match(logs.join("\n"), /Detected 1 reusable browser connection/);
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 when no reusable browser connection is discoverable", async () => {
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 auth;
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(result.message, /temporary Chromium login window/);
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 () => null,
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 a reusable browser session/);
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, or opens a temporary Chromium login window and saves the session there:
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 automatic browser-session reuse until your next explicit `doordash-cli login`:
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 neither is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
63
+ `doordash-cli login` reuses saved local auth when it is still valid. Otherwise it 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 automatic browser-session reuse disabled until you explicitly run `doordash-cli login` again.
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 a compatible CDP endpoint, then rerun `doordash-cli login`.
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 one is
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, or open a temporary Chromium login window and save
46
- that session there. If authentication is still not established,
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 automatic
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
- automatic browser-session reuse stays disabled until the next explicit
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. If no reusable browser connection is discoverable,
215
- login falls back to a temporary Chromium window it can watch directly. See
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doordash-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Cart-safe DoorDash CLI with direct API support for browse, read-only existing-order, and cart workflows.",
5
5
  "type": "module",
6
6
  "main": "dist/lib.js",