@detiq/crawler 1.1.8 → 1.1.9

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.
Files changed (82) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.js +266 -249
  22. package/dist/extractors/artifact-envelope.js +58 -59
  23. package/dist/failure-analysis-agent.js +33 -37
  24. package/dist/feature-flag-detector.js +66 -51
  25. package/dist/form-detector.js +75 -55
  26. package/dist/frontier.js +101 -76
  27. package/dist/gap-pass.js +89 -68
  28. package/dist/graphql-detector.js +72 -62
  29. package/dist/http-engine.js +330 -282
  30. package/dist/incremental-diff.js +87 -54
  31. package/dist/index.js +28 -67
  32. package/dist/input-fuzzer.js +97 -93
  33. package/dist/interactive-probe.js +827 -715
  34. package/dist/job-queue.js +2169 -2151
  35. package/dist/js-coverage.js +56 -52
  36. package/dist/js-router-detector.js +117 -86
  37. package/dist/language-detector.js +63 -51
  38. package/dist/link-classifier.js +50 -32
  39. package/dist/live-view.js +108 -91
  40. package/dist/locator-extractor.js +187 -151
  41. package/dist/logger.js +7 -9
  42. package/dist/markdown-extract.js +61 -48
  43. package/dist/native-crawler.js +946 -780
  44. package/dist/network-observer.js +156 -135
  45. package/dist/network-throttle.js +40 -29
  46. package/dist/openapi-generator.js +92 -77
  47. package/dist/pdf-extractor.js +90 -72
  48. package/dist/performance-metrics.js +102 -80
  49. package/dist/phash.js +144 -125
  50. package/dist/pii-scrubber.js +68 -65
  51. package/dist/playwright-recorder.js +58 -70
  52. package/dist/policies/crawl-safety.js +34 -31
  53. package/dist/profiles/domain-profile.js +125 -130
  54. package/dist/proxy.js +172 -146
  55. package/dist/rate-limiter.js +65 -58
  56. package/dist/redirect-tracker.js +64 -60
  57. package/dist/redis-frontier.js +136 -116
  58. package/dist/redis-queue.js +144 -157
  59. package/dist/response-cache.js +140 -127
  60. package/dist/responsive-detector.js +46 -42
  61. package/dist/retry-policy.js +50 -46
  62. package/dist/robots-txt.js +83 -69
  63. package/dist/screen-reader.js +70 -45
  64. package/dist/screenshot-masker.js +77 -59
  65. package/dist/seo-checker.js +107 -88
  66. package/dist/session-capture.js +212 -192
  67. package/dist/session-export.js +133 -107
  68. package/dist/sitemap-writer.js +41 -38
  69. package/dist/sitemap.js +202 -141
  70. package/dist/skyvern.js +148 -123
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +90 -74
  73. package/dist/stealth/human-behavior.js +173 -156
  74. package/dist/telemetry.js +82 -68
  75. package/dist/test-executor.js +335 -309
  76. package/dist/tls-fingerprint.js +88 -77
  77. package/dist/url-pattern-completer.js +90 -71
  78. package/dist/visual-diff-verdict.js +38 -23
  79. package/dist/webhook-notifier.js +69 -71
  80. package/dist/websocket-detector.js +68 -56
  81. package/dist/worker.js +38 -29
  82. package/package.json +1 -1
@@ -1,47 +1,59 @@
1
- import { chromium } from "playwright";
2
- import { randomBytes } from "node:crypto";
3
- import { login } from "./auth.js";
4
- import { generateTOTP } from "./job-queue.js";
5
- const activeSessions = /* @__PURE__ */ new Map();
6
- async function startSessionCapture(appUrl) {
7
- const inDocker = process.env.PLAYWRIGHT_IN_DOCKER === "true";
8
- const noDisplay = process.platform === "linux" && !process.env.DISPLAY;
9
- if (inDocker || noDisplay) {
10
- throw new Error("HEADLESS_ENV");
11
- }
12
- const id = randomBytes(16).toString("hex");
13
- const browser = await chromium.launch({ headless: false });
14
- const context = await browser.newContext();
15
- const page = await context.newPage();
16
- await page.goto(appUrl, { waitUntil: "domcontentloaded" }).catch(() => {
17
- });
18
- const timer = setTimeout(() => cancelSessionCapture(id).catch(() => {
19
- }), 10 * 60 * 1e3);
20
- activeSessions.set(id, { browser, context, timer });
21
- return id;
1
+ import { chromium } from 'playwright';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { login } from './auth.js';
4
+ import { generateTOTP } from './job-queue.js';
5
+ const activeSessions = new Map();
6
+ /**
7
+ * Opens a headed (visible) browser at appUrl and returns a captureId.
8
+ * The user logs in manually, then calls completeSessionCapture() to harvest storageState.
9
+ * Sessions expire after 10 minutes if not completed.
10
+ *
11
+ * Throws 'HEADLESS_ENV' if running in Docker/Linux without a display server.
12
+ * The API route surfaces this as a 503 so the frontend can switch to manual JSON paste.
13
+ */
14
+ export async function startSessionCapture(appUrl) {
15
+ const inDocker = process.env.PLAYWRIGHT_IN_DOCKER === 'true';
16
+ const noDisplay = process.platform === 'linux' && !process.env.DISPLAY;
17
+ if (inDocker || noDisplay) {
18
+ throw new Error('HEADLESS_ENV');
19
+ }
20
+ const id = randomBytes(16).toString('hex');
21
+ const browser = await chromium.launch({ headless: false });
22
+ const context = await browser.newContext();
23
+ const page = await context.newPage();
24
+ await page.goto(appUrl, { waitUntil: 'domcontentloaded' }).catch(() => { });
25
+ const timer = setTimeout(() => cancelSessionCapture(id).catch(() => { }), 10 * 60 * 1000);
26
+ activeSessions.set(id, { browser, context, timer });
27
+ return id;
22
28
  }
23
- async function completeSessionCapture(captureId) {
24
- const session = activeSessions.get(captureId);
25
- if (!session) throw new Error("Capture session not found or expired.");
26
- clearTimeout(session.timer);
27
- const state = await session.context.storageState();
28
- await session.browser.close().catch(() => {
29
- });
30
- activeSessions.delete(captureId);
31
- return JSON.stringify(state);
29
+ /** Captures storageState from the open browser, closes it, and returns the JSON string. */
30
+ export async function completeSessionCapture(captureId) {
31
+ const session = activeSessions.get(captureId);
32
+ if (!session)
33
+ throw new Error('Capture session not found or expired.');
34
+ clearTimeout(session.timer);
35
+ const state = await session.context.storageState();
36
+ await session.browser.close().catch(() => { });
37
+ activeSessions.delete(captureId);
38
+ return JSON.stringify(state);
32
39
  }
33
- async function cancelSessionCapture(captureId) {
34
- const session = activeSessions.get(captureId);
35
- if (!session) return;
36
- clearTimeout(session.timer);
37
- await session.browser.close().catch(() => {
38
- });
39
- activeSessions.delete(captureId);
40
+ /** Closes the browser without capturing — called on Cancel. */
41
+ export async function cancelSessionCapture(captureId) {
42
+ const session = activeSessions.get(captureId);
43
+ if (!session)
44
+ return;
45
+ clearTimeout(session.timer);
46
+ await session.browser.close().catch(() => { });
47
+ activeSessions.delete(captureId);
40
48
  }
49
+ /**
50
+ * Post-login exploration: navigates nav links, cycles tabs, advances carousels,
51
+ * opens accordions, scrolls — so the browser accumulates full session state before capture.
52
+ * All best-effort: never throws, never fails the capture.
53
+ */
41
54
  async function exploreAppAfterLogin(page) {
42
- const sleep = (ms) => page.waitForTimeout(ms).catch(() => {
43
- });
44
- const isVisible = `
55
+ const sleep = (ms) => page.waitForTimeout(ms).catch(() => { });
56
+ const isVisible = `
45
57
  function isVisible(el) {
46
58
  const s = window.getComputedStyle(el);
47
59
  if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) < 0.1) return false;
@@ -49,10 +61,11 @@ async function exploreAppAfterLogin(page) {
49
61
  return r.width > 4 && r.height > 4;
50
62
  }
51
63
  `;
52
- try {
53
- const origin = await page.evaluate(() => location.origin).catch(() => "");
54
- const startUrl = await page.evaluate(() => location.href).catch(() => "");
55
- await page.evaluate(new Function(`
64
+ try {
65
+ const origin = await page.evaluate(() => location.origin).catch(() => '');
66
+ const startUrl = await page.evaluate(() => location.href).catch(() => '');
67
+ // ── 1. Expand collapsed nav menus / hamburgers ──────────────────────────
68
+ await page.evaluate(new Function(`
56
69
  ${isVisible}
57
70
  const inBadCtx = el => {
58
71
  let p = el.parentElement;
@@ -74,10 +87,10 @@ async function exploreAppAfterLogin(page) {
74
87
  if ((el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') && isVisible(el) && !inBadCtx(el)) targets.add(el);
75
88
  });
76
89
  targets.forEach(el => { try { el.click(); } catch {} });
77
- `)).catch(() => {
78
- });
79
- await sleep(600);
80
- const navLinks = await page.evaluate(new Function(`
90
+ `)).catch(() => { });
91
+ await sleep(600);
92
+ // ── 2. Collect nav links from the expanded DOM ──────────────────────────
93
+ const navLinks = await page.evaluate(new Function(`
81
94
  ${isVisible}
82
95
  const links = new Set();
83
96
  const navEls = document.querySelectorAll('nav a, [role="navigation"] a, aside a, header a');
@@ -92,39 +105,44 @@ async function exploreAppAfterLogin(page) {
92
105
  });
93
106
  return [...links].slice(0, 10); // cap at 10 nav pages
94
107
  `)).catch(() => []);
95
- await page.keyboard?.press("Escape").catch(() => {
96
- });
97
- await sleep(200);
98
- for (const link of navLinks) {
99
- try {
100
- if (link === startUrl) continue;
101
- await page.goto(link, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
102
- });
103
- await sleep(400);
108
+ // Dismiss opened menus before navigating
109
+ await page.keyboard?.press('Escape').catch(() => { });
110
+ await sleep(200);
111
+ // ── 3. Visit each nav link, interact, return ────────────────────────────
112
+ for (const link of navLinks) {
113
+ try {
114
+ if (link === startUrl)
115
+ continue;
116
+ await page.goto(link, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => { });
117
+ await sleep(400);
118
+ await interactWithCurrentPage(page, isVisible, sleep);
119
+ }
120
+ catch { /* skip broken page */ }
121
+ }
122
+ // Return to start page
123
+ if (navLinks.length > 0) {
124
+ await page.goto(startUrl, { waitUntil: 'networkidle', timeout: 15_000 }).catch(() => { });
125
+ await sleep(400);
126
+ }
127
+ // ── 4. Final interaction pass on landing page ───────────────────────────
104
128
  await interactWithCurrentPage(page, isVisible, sleep);
105
- } catch {
106
- }
129
+ await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => { });
107
130
  }
108
- if (navLinks.length > 0) {
109
- await page.goto(startUrl, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
110
- });
111
- await sleep(400);
131
+ catch {
132
+ // Exploration is best-effort
112
133
  }
113
- await interactWithCurrentPage(page, isVisible, sleep);
114
- await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
115
- });
116
- } catch {
117
- }
118
134
  }
135
+ /** Interacts with tabs, carousels, accordions, and scrolls on the current page. */
119
136
  async function interactWithCurrentPage(page, isVisible, sleep) {
120
- await page.evaluate(new Function(`
137
+ // Cycle through tabs
138
+ await page.evaluate(new Function(`
121
139
  ${isVisible}
122
140
  const tabs = [...document.querySelectorAll('[role="tab"]')].filter(isVisible);
123
141
  tabs.forEach(tab => { try { tab.click(); } catch {} });
124
- `)).catch(() => {
125
- });
126
- await sleep(400);
127
- await page.evaluate(new Function(`
142
+ `)).catch(() => { });
143
+ await sleep(400);
144
+ // Advance carousels (next buttons)
145
+ await page.evaluate(new Function(`
128
146
  ${isVisible}
129
147
  const nextBtns = [...document.querySelectorAll(
130
148
  'button[aria-label*="next" i], button[aria-label*="forward" i], button[aria-label*="right" i], ' +
@@ -134,10 +152,10 @@ async function interactWithCurrentPage(page, isVisible, sleep) {
134
152
  for (let i = 0; i < 3; i++) {
135
153
  nextBtns.forEach(btn => { try { btn.click(); } catch {} });
136
154
  }
137
- `)).catch(() => {
138
- });
139
- await sleep(400);
140
- await page.evaluate(new Function(`
155
+ `)).catch(() => { });
156
+ await sleep(400);
157
+ // Open closed accordions / disclosure panels
158
+ await page.evaluate(new Function(`
141
159
  ${isVisible}
142
160
  document.querySelectorAll(
143
161
  '[data-state="closed"] button, [aria-expanded="false"], ' +
@@ -145,125 +163,127 @@ async function interactWithCurrentPage(page, isVisible, sleep) {
145
163
  ).forEach(el => {
146
164
  if (isVisible(el)) { try { el.click(); } catch {} }
147
165
  });
148
- `)).catch(() => {
149
- });
150
- await sleep(300);
151
- await page.evaluate(() => {
152
- window.scrollTo({ top: document.body.scrollHeight, behavior: "instant" });
153
- }).catch(() => {
154
- });
155
- await sleep(300);
156
- await page.evaluate(() => {
157
- window.scrollTo({ top: 0, behavior: "instant" });
158
- }).catch(() => {
159
- });
166
+ `)).catch(() => { });
167
+ await sleep(300);
168
+ // Scroll to bottom then back to top
169
+ await page.evaluate(() => { window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' }); }).catch(() => { });
170
+ await sleep(300);
171
+ await page.evaluate(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }).catch(() => { });
160
172
  }
161
- async function validateAndCaptureSession(appUrl, creds, llmConfig, captchaOpts) {
162
- const browser = await chromium.launch({ headless: true });
163
- try {
164
- const contextOpts = {};
165
- if (creds.httpBasicUsername && creds.httpBasicPassword) {
166
- contextOpts.httpCredentials = { username: creds.httpBasicUsername, password: creds.httpBasicPassword };
167
- }
168
- const context = await browser.newContext(contextOpts);
169
- const page = await context.newPage();
170
- const getOneTimeCode = creds.totpSecret ? async () => generateTOTP(creds.totpSecret) : void 0;
171
- let loggedIn;
173
+ /**
174
+ * Headless login + session capture — works on Docker/cloud (no display needed).
175
+ * Tries DOM-based login first (fast, free). If it fails and llmConfig is provided,
176
+ * falls back to Stagehand AI-guided login (handles modals, multi-step, marketing pages).
177
+ *
178
+ * Returns { storageState, cookies, localStorage } on success.
179
+ * Throws a human-readable error on failure.
180
+ */
181
+ export async function validateAndCaptureSession(appUrl, creds, llmConfig, captchaOpts) {
182
+ const browser = await chromium.launch({ headless: true });
172
183
  try {
173
- loggedIn = await login(page, appUrl, {
174
- username: creds.username,
175
- password: creds.password,
176
- getOneTimeCode
177
- });
178
- } catch (loginErr) {
179
- const shotBuf = await page.screenshot({ type: "jpeg", quality: 60, fullPage: false }).catch(() => null);
180
- throw Object.assign(loginErr, {
181
- failureScreenshot: shotBuf ? shotBuf.toString("base64") : null
182
- });
183
- }
184
- if (!loggedIn && llmConfig) {
185
- let sh = null;
186
- try {
187
- const { Stagehand } = await import("@browserbasehq/stagehand");
188
- const { loginWithStagehand } = await import("./ai-login.js");
189
- sh = new Stagehand({
190
- env: "LOCAL",
191
- model: { modelName: llmConfig.modelName, apiKey: llmConfig.apiKey },
192
- serverCache: false,
193
- verbose: 0,
194
- localBrowserLaunchOptions: { headless: true }
195
- });
196
- await sh.init();
197
- const shPage = await sh.resolvePage();
198
- await shPage.goto(appUrl, { waitUntil: "networkidle", timeout: 3e4 }).catch(() => {
199
- });
200
- const shLoggedIn = await loginWithStagehand(
201
- sh,
202
- { username: creds.username, password: creds.password, getOneTimeCode },
203
- appUrl,
204
- captchaOpts
205
- );
206
- if (shLoggedIn) {
207
- const shCtx = sh.context ?? sh.browserContext;
208
- if (shCtx) {
209
- const shState = await shCtx.storageState();
210
- const shCookies = shState.cookies?.length ?? 0;
211
- const shLocalStorage = shState.origins?.reduce((s, o) => s + (o.localStorage?.length ?? 0), 0) ?? 0;
212
- if (shCookies > 0 || shLocalStorage > 0) {
213
- await sh.close().catch(() => {
214
- });
215
- sh = null;
216
- return { storageState: JSON.stringify(shState), cookies: shCookies, localStorage: shLocalStorage };
184
+ const contextOpts = {};
185
+ if (creds.httpBasicUsername && creds.httpBasicPassword) {
186
+ contextOpts.httpCredentials = { username: creds.httpBasicUsername, password: creds.httpBasicPassword };
187
+ }
188
+ const context = await browser.newContext(contextOpts);
189
+ const page = await context.newPage();
190
+ const getOneTimeCode = creds.totpSecret
191
+ ? async () => generateTOTP(creds.totpSecret)
192
+ : undefined;
193
+ let loggedIn;
194
+ try {
195
+ loggedIn = await login(page, appUrl, {
196
+ username: creds.username,
197
+ password: creds.password,
198
+ getOneTimeCode,
199
+ });
200
+ }
201
+ catch (loginErr) {
202
+ // login() throws when it can read a real error message from the page
203
+ const shotBuf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false }).catch(() => null);
204
+ throw Object.assign(loginErr, {
205
+ failureScreenshot: shotBuf ? shotBuf.toString('base64') : null,
206
+ });
207
+ }
208
+ // Stagehand AI fallback when DOM login fails and LLM config is available
209
+ if (!loggedIn && llmConfig) {
210
+ let sh = null;
211
+ try {
212
+ const { Stagehand } = await import('@browserbasehq/stagehand');
213
+ const { loginWithStagehand } = await import('./ai-login.js');
214
+ sh = new Stagehand({
215
+ env: 'LOCAL',
216
+ model: { modelName: llmConfig.modelName, apiKey: llmConfig.apiKey },
217
+ serverCache: false,
218
+ verbose: 0,
219
+ localBrowserLaunchOptions: { headless: true },
220
+ });
221
+ await sh.init();
222
+ const shPage = await sh.resolvePage();
223
+ await shPage.goto(appUrl, { waitUntil: 'networkidle', timeout: 30_000 }).catch(() => { });
224
+ const shLoggedIn = await loginWithStagehand(sh, { username: creds.username, password: creds.password, getOneTimeCode }, appUrl, captchaOpts);
225
+ if (shLoggedIn) {
226
+ const shCtx = sh.context ?? sh.browserContext;
227
+ if (shCtx) {
228
+ const shState = await shCtx.storageState();
229
+ const shCookies = shState.cookies?.length ?? 0;
230
+ const shLocalStorage = shState.origins?.reduce((s, o) => s + (o.localStorage?.length ?? 0), 0) ?? 0;
231
+ if (shCookies > 0 || shLocalStorage > 0) {
232
+ await sh.close().catch(() => { });
233
+ sh = null;
234
+ return { storageState: JSON.stringify(shState), cookies: shCookies, localStorage: shLocalStorage };
235
+ }
236
+ }
237
+ loggedIn = true; // Stagehand succeeded but we'll capture from the plain browser context below
238
+ }
239
+ }
240
+ catch (shErr) {
241
+ const msg = String(shErr?.message ?? shErr);
242
+ if (msg.includes('CAPTCHA') || shErr?.isSSOError)
243
+ throw shErr;
244
+ // Stagehand failed silently — fall through to "credentials rejected"
245
+ }
246
+ finally {
247
+ if (sh)
248
+ await sh.close().catch(() => { });
217
249
  }
218
- }
219
- loggedIn = true;
220
250
  }
221
- } catch (shErr) {
222
- const msg = String(shErr?.message ?? shErr);
223
- if (msg.includes("CAPTCHA") || shErr?.isSSOError) throw shErr;
224
- } finally {
225
- if (sh) await sh.close().catch(() => {
226
- });
227
- }
228
- }
229
- if (!loggedIn) {
230
- const shotBuf = await page.screenshot({ type: "jpeg", quality: 60, fullPage: false }).catch(() => null);
231
- const challenge = await page.evaluate(() => {
232
- const text = (document.body?.innerText ?? "").toLowerCase();
233
- const hasCloudflare = /verify you are human|checking your browser|cloudflare|cf-challenge|turnstile/.test(text) || !!document.querySelector('iframe[src*="challenges.cloudflare.com"], .cf-turnstile, [data-sitekey]');
234
- const hasCaptcha = hasCloudflare || /captcha|recaptcha|hcaptcha/.test(text) || !!document.querySelector('iframe[src*="recaptcha"], iframe[src*="hcaptcha"], #captcha, .captcha, [id*="captcha"], [class*="captcha"]');
235
- return { hasCloudflare, hasCaptcha };
236
- }).catch(() => ({ hasCloudflare: false, hasCaptcha: false }));
237
- const message = challenge.hasCloudflare ? "Cloudflare human verification blocked headless validation. This cannot be bypassed reliably in code; use Live Browser or manual session capture, configure a CAPTCHA solver, or allowlist the crawler IP." : challenge.hasCaptcha ? "A CAPTCHA blocked headless validation. Configure CAPTCHA Auto-Solve or use Live Browser/manual session capture." : "Login failed \u2014 credentials rejected or the login flow requires a manual/SSO step. Check credentials, saved login instructions, or use Live Browser session capture.";
238
- const err = Object.assign(new Error(message), {
239
- failureScreenshot: shotBuf ? shotBuf.toString("base64") : null,
240
- reason: challenge.hasCloudflare ? "CLOUDFLARE_CHALLENGE" : challenge.hasCaptcha ? "CAPTCHA_CHALLENGE" : "LOGIN_NOT_CONFIRMED"
241
- });
242
- throw err;
251
+ if (!loggedIn) {
252
+ const shotBuf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false }).catch(() => null);
253
+ const challenge = await page.evaluate(() => {
254
+ const text = (document.body?.innerText ?? '').toLowerCase();
255
+ const hasCloudflare = /verify you are human|checking your browser|cloudflare|cf-challenge|turnstile/.test(text) ||
256
+ !!document.querySelector('iframe[src*="challenges.cloudflare.com"], .cf-turnstile, [data-sitekey]');
257
+ const hasCaptcha = hasCloudflare || /captcha|recaptcha|hcaptcha/.test(text) ||
258
+ !!document.querySelector('iframe[src*="recaptcha"], iframe[src*="hcaptcha"], #captcha, .captcha, [id*="captcha"], [class*="captcha"]');
259
+ return { hasCloudflare, hasCaptcha };
260
+ }).catch(() => ({ hasCloudflare: false, hasCaptcha: false }));
261
+ const message = challenge.hasCloudflare
262
+ ? 'Cloudflare human verification blocked headless validation. This cannot be bypassed reliably in code; use Live Browser or manual session capture, configure a CAPTCHA solver, or allowlist the crawler IP.'
263
+ : challenge.hasCaptcha
264
+ ? 'A CAPTCHA blocked headless validation. Configure CAPTCHA Auto-Solve or use Live Browser/manual session capture.'
265
+ : 'Login failed — credentials rejected or the login flow requires a manual/SSO step. Check credentials, saved login instructions, or use Live Browser session capture.';
266
+ const err = Object.assign(new Error(message), {
267
+ failureScreenshot: shotBuf ? shotBuf.toString('base64') : null,
268
+ reason: challenge.hasCloudflare ? 'CLOUDFLARE_CHALLENGE' : challenge.hasCaptcha ? 'CAPTCHA_CHALLENGE' : 'LOGIN_NOT_CONFIRMED',
269
+ });
270
+ throw err;
271
+ }
272
+ // Wait for post-login navigation to settle before capturing session
273
+ await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => { });
274
+ // Capture post-login landing URL before exploration navigates away
275
+ const finalUrl = page.url();
276
+ // Post-login exploration: navigate tabs, expand menus, scroll to trigger lazy-loaded state
277
+ await exploreAppAfterLogin(page);
278
+ const state = await context.storageState();
279
+ const cookies = state.cookies?.length ?? 0;
280
+ const localStorage = state.origins?.reduce((sum, o) => sum + (o.localStorage?.length ?? 0), 0) ?? 0;
281
+ if (cookies === 0 && localStorage === 0) {
282
+ throw new Error('Login appeared to succeed but no session data was captured — try manual session capture instead');
283
+ }
284
+ return { storageState: JSON.stringify(state), cookies, localStorage, finalUrl };
243
285
  }
244
- await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
245
- });
246
- const finalUrl = page.url();
247
- await exploreAppAfterLogin(page);
248
- const state = await context.storageState();
249
- const cookies = state.cookies?.length ?? 0;
250
- const localStorage = state.origins?.reduce(
251
- (sum, o) => sum + (o.localStorage?.length ?? 0),
252
- 0
253
- ) ?? 0;
254
- if (cookies === 0 && localStorage === 0) {
255
- throw new Error("Login appeared to succeed but no session data was captured \u2014 try manual session capture instead");
286
+ finally {
287
+ await browser.close().catch(() => { });
256
288
  }
257
- return { storageState: JSON.stringify(state), cookies, localStorage, finalUrl };
258
- } finally {
259
- await browser.close().catch(() => {
260
- });
261
- }
262
289
  }
263
- export {
264
- cancelSessionCapture,
265
- completeSessionCapture,
266
- startSessionCapture,
267
- validateAndCaptureSession
268
- };
269
- //# sourceMappingURL=session-capture.js.map