doordash-cli 0.3.3 → 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 +12 -0
- package/README.md +14 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +17 -8
- package/dist/cli.test.js +43 -3
- package/dist/direct-api.d.ts +55 -4
- package/dist/direct-api.js +655 -75
- package/dist/direct-api.test.js +529 -14
- package/dist/session-storage.d.ts +1 -0
- package/dist/session-storage.js +4 -0
- package/dist/session-storage.test.js +2 -1
- package/docs/examples.md +3 -3
- package/docs/install.md +15 -5
- package/man/dd-cli.1 +34 -14
- package/package.json +1 -1
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 { buildAddConsumerAddressPayload, buildAddToCartPayload, buildUpdateCartPayload, extractExistingOrdersFromApolloCache,
|
|
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,
|
|
@@ -144,25 +144,540 @@ test("parseSearchRestaurantRow extracts restaurant metadata from facet rows", ()
|
|
|
144
144
|
url: "https://www.doordash.com/store/24633898/?pickup=false",
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
-
test("
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
test("resolveAttachedBrowserCdpCandidates prioritizes explicit envs, compatibility envs, config, and defaults", () => {
|
|
148
|
+
const env = {
|
|
149
|
+
DOORDASH_BROWSER_CDP_URLS: "http://127.0.0.1:9555/, http://127.0.0.1:9556",
|
|
150
|
+
DOORDASH_ATTACHED_BROWSER_CDP_URL: "http://127.0.0.1:9666/",
|
|
151
|
+
DOORDASH_BROWSER_CDP_PORTS: "9333, 9334",
|
|
152
|
+
DOORDASH_BROWSER_CDP_PORT: "9444",
|
|
153
|
+
OPENCLAW_BROWSER_CDP_URL: "http://127.0.0.1:18888/",
|
|
154
|
+
};
|
|
155
|
+
const candidates = resolveAttachedBrowserCdpCandidates(env, ["http://127.0.0.1:9777"]);
|
|
156
|
+
assert.deepEqual(candidates.slice(0, 7), [
|
|
157
|
+
"http://127.0.0.1:9555",
|
|
158
|
+
"http://127.0.0.1:9556",
|
|
159
|
+
"http://127.0.0.1:9666",
|
|
160
|
+
"http://127.0.0.1:9333",
|
|
161
|
+
"http://127.0.0.1:9334",
|
|
162
|
+
"http://127.0.0.1:9444",
|
|
163
|
+
"http://127.0.0.1:18888",
|
|
164
|
+
]);
|
|
165
|
+
assert.ok(candidates.includes("http://127.0.0.1:9777"));
|
|
166
|
+
assert.ok(candidates.includes("http://127.0.0.1:18792"));
|
|
167
|
+
assert.ok(candidates.includes("http://127.0.0.1:18800"));
|
|
168
|
+
assert.ok(candidates.includes("http://127.0.0.1:9222"));
|
|
151
169
|
});
|
|
152
|
-
test("
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
test("resolveSystemBrowserOpenCommand stays generic across operating systems", () => {
|
|
171
|
+
assert.deepEqual(resolveSystemBrowserOpenCommand("https://www.doordash.com/home", "darwin"), {
|
|
172
|
+
command: "open",
|
|
173
|
+
args: ["https://www.doordash.com/home"],
|
|
174
|
+
});
|
|
175
|
+
assert.deepEqual(resolveSystemBrowserOpenCommand("https://www.doordash.com/home", "linux"), {
|
|
176
|
+
command: "xdg-open",
|
|
177
|
+
args: ["https://www.doordash.com/home"],
|
|
178
|
+
});
|
|
179
|
+
assert.deepEqual(resolveSystemBrowserOpenCommand("https://www.doordash.com/home", "win32"), {
|
|
180
|
+
command: "cmd",
|
|
181
|
+
args: ["/c", "start", "", "https://www.doordash.com/home"],
|
|
182
|
+
});
|
|
158
183
|
});
|
|
159
|
-
test("
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
});
|
|
207
|
+
test("selectAttachedBrowserImportMode treats an authenticated browser with DoorDash cookies as an immediate import candidate", () => {
|
|
208
|
+
assert.equal(selectAttachedBrowserImportMode({
|
|
209
|
+
pageUrls: ["https://github.com/LatencyTDH/doordash-cli/pulls"],
|
|
210
|
+
cookies: [{ domain: ".doordash.com" }],
|
|
211
|
+
}), "cookies");
|
|
212
|
+
assert.equal(selectAttachedBrowserImportMode({
|
|
213
|
+
pageUrls: ["https://www.doordash.com/home"],
|
|
214
|
+
cookies: [{ domain: ".github.com" }],
|
|
215
|
+
}), "page");
|
|
216
|
+
assert.equal(selectAttachedBrowserImportMode({
|
|
162
217
|
pageUrls: ["https://github.com/LatencyTDH/doordash-cli"],
|
|
163
218
|
cookies: [{ domain: ".github.com" }],
|
|
164
219
|
}), "skip");
|
|
165
220
|
});
|
|
221
|
+
test("bootstrapAuthSessionWithDeps returns immediately when saved local auth is already valid", async () => {
|
|
222
|
+
const auth = {
|
|
223
|
+
success: true,
|
|
224
|
+
isLoggedIn: true,
|
|
225
|
+
email: "user@example.com",
|
|
226
|
+
firstName: "Test",
|
|
227
|
+
lastName: "User",
|
|
228
|
+
consumerId: "consumer-1",
|
|
229
|
+
marketId: "market-1",
|
|
230
|
+
defaultAddress: null,
|
|
231
|
+
cookiesPath: "/tmp/cookies.json",
|
|
232
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
233
|
+
};
|
|
234
|
+
let importCalls = 0;
|
|
235
|
+
let openCalls = 0;
|
|
236
|
+
let waitCalls = 0;
|
|
237
|
+
let markCalls = 0;
|
|
238
|
+
const logs = [];
|
|
239
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
240
|
+
clearBlockedBrowserImport: async () => { },
|
|
241
|
+
checkPersistedAuth: async () => auth,
|
|
242
|
+
importBrowserSessionIfAvailable: async () => {
|
|
243
|
+
importCalls += 1;
|
|
244
|
+
return true;
|
|
245
|
+
},
|
|
246
|
+
markBrowserImportAttempted: () => {
|
|
247
|
+
markCalls += 1;
|
|
248
|
+
},
|
|
249
|
+
getAttachedBrowserCdpCandidates: async () => {
|
|
250
|
+
throw new Error("should not inspect candidates when saved auth is already valid");
|
|
251
|
+
},
|
|
252
|
+
getReachableCdpCandidates: async () => {
|
|
253
|
+
throw new Error("should not probe reachability when saved auth is already valid");
|
|
254
|
+
},
|
|
255
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
256
|
+
openUrlInAttachedBrowser: async () => {
|
|
257
|
+
throw new Error("should not try to open an attached browser when saved auth is already valid");
|
|
258
|
+
},
|
|
259
|
+
openUrlInDefaultBrowser: async () => {
|
|
260
|
+
openCalls += 1;
|
|
261
|
+
return true;
|
|
262
|
+
},
|
|
263
|
+
waitForAttachedBrowserSessionImport: async () => {
|
|
264
|
+
waitCalls += 1;
|
|
265
|
+
return true;
|
|
266
|
+
},
|
|
267
|
+
waitForManagedBrowserLogin: async () => {
|
|
268
|
+
throw new Error("should not launch a managed browser when saved auth is already valid");
|
|
269
|
+
},
|
|
270
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
271
|
+
checkAuthDirect: async () => {
|
|
272
|
+
throw new Error("should not re-check auth through the live session when saved auth is already valid");
|
|
273
|
+
},
|
|
274
|
+
log: (message) => {
|
|
275
|
+
logs.push(message);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
assert.equal(importCalls, 0);
|
|
279
|
+
assert.equal(markCalls, 0);
|
|
280
|
+
assert.equal(openCalls, 0);
|
|
281
|
+
assert.equal(waitCalls, 0);
|
|
282
|
+
assert.equal(logs.length, 0);
|
|
283
|
+
assert.equal(result.isLoggedIn, true);
|
|
284
|
+
assert.match(result.message, /Already signed in with saved local DoorDash session state/);
|
|
285
|
+
});
|
|
286
|
+
test("bootstrapAuthSessionWithDeps returns immediately when an attached browser session is already authenticated", async () => {
|
|
287
|
+
const auth = {
|
|
288
|
+
success: true,
|
|
289
|
+
isLoggedIn: true,
|
|
290
|
+
email: "user@example.com",
|
|
291
|
+
firstName: "Test",
|
|
292
|
+
lastName: "User",
|
|
293
|
+
consumerId: "consumer-1",
|
|
294
|
+
marketId: "market-1",
|
|
295
|
+
defaultAddress: null,
|
|
296
|
+
cookiesPath: "/tmp/cookies.json",
|
|
297
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
298
|
+
};
|
|
299
|
+
let openCalls = 0;
|
|
300
|
+
let waitCalls = 0;
|
|
301
|
+
let markCalls = 0;
|
|
302
|
+
const logs = [];
|
|
303
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
304
|
+
clearBlockedBrowserImport: async () => { },
|
|
305
|
+
checkPersistedAuth: async () => null,
|
|
306
|
+
importBrowserSessionIfAvailable: async () => true,
|
|
307
|
+
markBrowserImportAttempted: () => {
|
|
308
|
+
markCalls += 1;
|
|
309
|
+
},
|
|
310
|
+
getAttachedBrowserCdpCandidates: async () => {
|
|
311
|
+
throw new Error("should not inspect candidates on immediate browser-session import");
|
|
312
|
+
},
|
|
313
|
+
getReachableCdpCandidates: async () => {
|
|
314
|
+
throw new Error("should not probe reachability on immediate browser-session import");
|
|
315
|
+
},
|
|
316
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
317
|
+
openUrlInAttachedBrowser: async () => {
|
|
318
|
+
throw new Error("should not open a browser when immediate browser-session import succeeded");
|
|
319
|
+
},
|
|
320
|
+
openUrlInDefaultBrowser: async () => {
|
|
321
|
+
openCalls += 1;
|
|
322
|
+
return true;
|
|
323
|
+
},
|
|
324
|
+
waitForAttachedBrowserSessionImport: async () => {
|
|
325
|
+
waitCalls += 1;
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
waitForManagedBrowserLogin: async () => {
|
|
329
|
+
throw new Error("should not launch a managed browser when immediate browser-session import succeeded");
|
|
330
|
+
},
|
|
331
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
332
|
+
checkAuthDirect: async () => auth,
|
|
333
|
+
log: (message) => {
|
|
334
|
+
logs.push(message);
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
assert.equal(markCalls, 1);
|
|
338
|
+
assert.equal(openCalls, 0);
|
|
339
|
+
assert.equal(waitCalls, 0);
|
|
340
|
+
assert.equal(logs.length, 0);
|
|
341
|
+
assert.equal(result.isLoggedIn, true);
|
|
342
|
+
assert.match(result.message, /Imported an existing signed-in browser session/);
|
|
343
|
+
});
|
|
344
|
+
test("bootstrapAuthSessionWithDeps opens a watchable attached browser session before entering the full wait path", async () => {
|
|
345
|
+
const auth = {
|
|
346
|
+
success: true,
|
|
347
|
+
isLoggedIn: true,
|
|
348
|
+
email: "user@example.com",
|
|
349
|
+
firstName: "Test",
|
|
350
|
+
lastName: "User",
|
|
351
|
+
consumerId: "consumer-1",
|
|
352
|
+
marketId: "market-1",
|
|
353
|
+
defaultAddress: null,
|
|
354
|
+
cookiesPath: "/tmp/cookies.json",
|
|
355
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
356
|
+
};
|
|
357
|
+
let openAttachedCalls = 0;
|
|
358
|
+
let openDefaultCalls = 0;
|
|
359
|
+
let waitCalls = 0;
|
|
360
|
+
let reachableCalls = 0;
|
|
361
|
+
let waitTimeoutMs = 0;
|
|
362
|
+
const logs = [];
|
|
363
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
364
|
+
clearBlockedBrowserImport: async () => { },
|
|
365
|
+
checkPersistedAuth: async () => null,
|
|
366
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
367
|
+
markBrowserImportAttempted: () => { },
|
|
368
|
+
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
369
|
+
getReachableCdpCandidates: async (candidates) => {
|
|
370
|
+
reachableCalls += 1;
|
|
371
|
+
return candidates;
|
|
372
|
+
},
|
|
373
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
374
|
+
openUrlInAttachedBrowser: async () => {
|
|
375
|
+
openAttachedCalls += 1;
|
|
376
|
+
return true;
|
|
377
|
+
},
|
|
378
|
+
openUrlInDefaultBrowser: async () => {
|
|
379
|
+
openDefaultCalls += 1;
|
|
380
|
+
return true;
|
|
381
|
+
},
|
|
382
|
+
waitForAttachedBrowserSessionImport: async (input) => {
|
|
383
|
+
waitCalls += 1;
|
|
384
|
+
waitTimeoutMs = input.timeoutMs;
|
|
385
|
+
return true;
|
|
386
|
+
},
|
|
387
|
+
waitForManagedBrowserLogin: async () => {
|
|
388
|
+
throw new Error("should not launch a managed browser when an attached browser is reachable");
|
|
389
|
+
},
|
|
390
|
+
canPromptForManagedBrowserConfirmation: () => false,
|
|
391
|
+
checkAuthDirect: async () => auth,
|
|
392
|
+
log: (message) => {
|
|
393
|
+
logs.push(message);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
assert.equal(reachableCalls, 1);
|
|
397
|
+
assert.equal(openAttachedCalls, 1);
|
|
398
|
+
assert.equal(openDefaultCalls, 0);
|
|
399
|
+
assert.equal(waitCalls, 1);
|
|
400
|
+
assert.equal(waitTimeoutMs, 180_000);
|
|
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/);
|
|
403
|
+
assert.match(result.message, /saved it for direct API use/);
|
|
404
|
+
});
|
|
405
|
+
test("bootstrapAuthSessionWithDeps falls back to a managed browser login window and auto-completes when it can prove login", async () => {
|
|
406
|
+
const auth = {
|
|
407
|
+
success: true,
|
|
408
|
+
isLoggedIn: true,
|
|
409
|
+
email: "user@example.com",
|
|
410
|
+
firstName: "Test",
|
|
411
|
+
lastName: "User",
|
|
412
|
+
consumerId: "consumer-1",
|
|
413
|
+
marketId: "market-1",
|
|
414
|
+
defaultAddress: null,
|
|
415
|
+
cookiesPath: "/tmp/cookies.json",
|
|
416
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
417
|
+
};
|
|
418
|
+
let managedCalls = 0;
|
|
419
|
+
let attachedWaitCalls = 0;
|
|
420
|
+
const logs = [];
|
|
421
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
422
|
+
clearBlockedBrowserImport: async () => { },
|
|
423
|
+
checkPersistedAuth: async () => null,
|
|
424
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
425
|
+
markBrowserImportAttempted: () => { },
|
|
426
|
+
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
427
|
+
getReachableCdpCandidates: async () => [],
|
|
428
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
429
|
+
openUrlInAttachedBrowser: async () => false,
|
|
430
|
+
openUrlInDefaultBrowser: async () => true,
|
|
431
|
+
waitForAttachedBrowserSessionImport: async () => {
|
|
432
|
+
attachedWaitCalls += 1;
|
|
433
|
+
return false;
|
|
434
|
+
},
|
|
435
|
+
waitForManagedBrowserLogin: async () => {
|
|
436
|
+
managedCalls += 1;
|
|
437
|
+
return {
|
|
438
|
+
status: "completed",
|
|
439
|
+
completion: "automatic",
|
|
440
|
+
auth,
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
444
|
+
checkAuthDirect: async () => auth,
|
|
445
|
+
log: (message) => {
|
|
446
|
+
logs.push(message);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
assert.equal(managedCalls, 1);
|
|
450
|
+
assert.equal(attachedWaitCalls, 0);
|
|
451
|
+
assert.match(logs.join("\n"), /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);
|
|
495
|
+
assert.equal(result.success, true);
|
|
496
|
+
assert.equal(result.isLoggedIn, true);
|
|
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
|
+
});
|
|
589
|
+
test("bootstrapAuthSessionWithDeps falls back to quick troubleshooting guidance when the managed browser login window cannot launch", async () => {
|
|
590
|
+
const auth = {
|
|
591
|
+
success: true,
|
|
592
|
+
isLoggedIn: false,
|
|
593
|
+
email: null,
|
|
594
|
+
firstName: null,
|
|
595
|
+
lastName: null,
|
|
596
|
+
consumerId: null,
|
|
597
|
+
marketId: null,
|
|
598
|
+
defaultAddress: null,
|
|
599
|
+
cookiesPath: "/tmp/cookies.json",
|
|
600
|
+
storageStatePath: "/tmp/storage-state.json",
|
|
601
|
+
};
|
|
602
|
+
let attachedWaitCalls = 0;
|
|
603
|
+
let attachedWaitTimeoutMs = 0;
|
|
604
|
+
const logs = [];
|
|
605
|
+
const result = await bootstrapAuthSessionWithDeps({
|
|
606
|
+
clearBlockedBrowserImport: async () => { },
|
|
607
|
+
checkPersistedAuth: async () => null,
|
|
608
|
+
importBrowserSessionIfAvailable: async () => false,
|
|
609
|
+
markBrowserImportAttempted: () => { },
|
|
610
|
+
getAttachedBrowserCdpCandidates: async () => ["http://127.0.0.1:9222"],
|
|
611
|
+
getReachableCdpCandidates: async () => [],
|
|
612
|
+
describeDesktopBrowserReuseGap: async () => null,
|
|
613
|
+
openUrlInAttachedBrowser: async () => false,
|
|
614
|
+
openUrlInDefaultBrowser: async () => true,
|
|
615
|
+
waitForAttachedBrowserSessionImport: async (input) => {
|
|
616
|
+
attachedWaitCalls += 1;
|
|
617
|
+
attachedWaitTimeoutMs = input.timeoutMs;
|
|
618
|
+
return false;
|
|
619
|
+
},
|
|
620
|
+
waitForManagedBrowserLogin: async () => ({ status: "launch-failed" }),
|
|
621
|
+
canPromptForManagedBrowserConfirmation: () => true,
|
|
622
|
+
checkAuthDirect: async () => auth,
|
|
623
|
+
log: (message) => {
|
|
624
|
+
logs.push(message);
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
assert.equal(attachedWaitCalls, 1);
|
|
628
|
+
assert.equal(attachedWaitTimeoutMs, 10_000);
|
|
629
|
+
assert.match(logs.join("\n"), /couldn't launch the temporary Chromium login window/i);
|
|
630
|
+
assert.match(logs.join("\n"), /won't keep you waiting for the full login timeout/i);
|
|
631
|
+
assert.equal(result.success, false);
|
|
632
|
+
assert.equal(result.isLoggedIn, false);
|
|
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/);
|
|
680
|
+
});
|
|
166
681
|
test("parseOptionSelectionsJson parses structured recursive option selections", () => {
|
|
167
682
|
assert.deepEqual(parseOptionSelectionsJson('[{"groupId":"703393388","optionId":"4716032529"},{"groupId":"recommended_option_546935995","optionId":"546936011","children":[{"groupId":"780057412","optionId":"4702669757","quantity":2}]}]'), [
|
|
168
683
|
{ groupId: "703393388", optionId: "4716032529" },
|
package/dist/session-storage.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
const SESSION_CONFIG_DIR = join(homedir(), ".config", "striderlabs-mcp-doordash");
|
|
6
6
|
const COOKIES_FILE = join(SESSION_CONFIG_DIR, "cookies.json");
|
|
7
7
|
const STORAGE_STATE_FILE = join(SESSION_CONFIG_DIR, "storage-state.json");
|
|
8
|
+
const BROWSER_IMPORT_BLOCK_FILE = join(SESSION_CONFIG_DIR, "browser-import-blocked");
|
|
8
9
|
export function getSessionConfigDir() {
|
|
9
10
|
return SESSION_CONFIG_DIR;
|
|
10
11
|
}
|
|
@@ -14,3 +15,6 @@ export function getCookiesPath() {
|
|
|
14
15
|
export function getStorageStatePath() {
|
|
15
16
|
return STORAGE_STATE_FILE;
|
|
16
17
|
}
|
|
18
|
+
export function getBrowserImportBlockPath() {
|
|
19
|
+
return BROWSER_IMPORT_BLOCK_FILE;
|
|
20
|
+
}
|
|
@@ -2,10 +2,11 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { getCookiesPath, getSessionConfigDir, getStorageStatePath } from "./session-storage.js";
|
|
5
|
+
import { getBrowserImportBlockPath, getCookiesPath, getSessionConfigDir, getStorageStatePath } from "./session-storage.js";
|
|
6
6
|
test("session storage paths stay compatible with the historical StriderLabs location", () => {
|
|
7
7
|
const configDir = join(homedir(), ".config", "striderlabs-mcp-doordash");
|
|
8
8
|
assert.equal(getSessionConfigDir(), configDir);
|
|
9
9
|
assert.equal(getCookiesPath(), join(configDir, "cookies.json"));
|
|
10
10
|
assert.equal(getStorageStatePath(), join(configDir, "storage-state.json"));
|
|
11
|
+
assert.equal(getBrowserImportBlockPath(), join(configDir, "browser-import-blocked"));
|
|
11
12
|
});
|
package/docs/examples.md
CHANGED
|
@@ -14,7 +14,7 @@ All commands print JSON.
|
|
|
14
14
|
|
|
15
15
|
## Session setup
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
If your environment does not already have the bundled Playwright runtime installed, install it once:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
doordash-cli install-browser
|
|
@@ -26,13 +26,13 @@ Check whether you already have reusable session state:
|
|
|
26
26
|
doordash-cli auth-check
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
If
|
|
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 start
|
|
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
|
@@ -31,9 +31,9 @@ npm run cli -- --help
|
|
|
31
31
|
|
|
32
32
|
If you stay in checkout mode, replace `doordash-cli` with `npm run cli --` in the examples below.
|
|
33
33
|
|
|
34
|
-
## Install the
|
|
34
|
+
## Install the bundled runtime if needed
|
|
35
35
|
|
|
36
|
-
If
|
|
36
|
+
If your environment does not already have Playwright's bundled Chromium runtime installed, install it once:
|
|
37
37
|
|
|
38
38
|
### Global or linked install
|
|
39
39
|
|
|
@@ -47,6 +47,8 @@ doordash-cli install-browser
|
|
|
47
47
|
npm run install:browser
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
That runtime is used when the CLI needs a local browser, including the temporary login window fallback.
|
|
51
|
+
|
|
50
52
|
## First run
|
|
51
53
|
|
|
52
54
|
```bash
|
|
@@ -56,8 +58,16 @@ doordash-cli set-address --address "350 5th Ave, New York, NY 10118"
|
|
|
56
58
|
doordash-cli search --query sushi
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
##
|
|
61
|
+
## Login and session reuse
|
|
62
|
+
|
|
63
|
+
`doordash-cli login` reuses saved local auth when it is still valid. Otherwise it tries to import a discoverable attachable signed-in browser session. A merely-open Chrome/Brave window is not automatically reusable unless the CLI can actually attach to it. If no attachable session is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
|
|
64
|
+
|
|
65
|
+
`doordash-cli auth-check` can also quietly import a discoverable attachable signed-in browser session unless `doordash-cli logout` disabled that auto-reuse.
|
|
66
|
+
|
|
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
|
+
|
|
69
|
+
## Browser-session troubleshooting
|
|
60
70
|
|
|
61
|
-
|
|
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.
|
|
62
72
|
|
|
63
|
-
If
|
|
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.
|