@detiq/crawler 1.1.9 → 1.1.11
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/dist/accessibility-auditor.js +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
- package/src/job-queue.ts +136 -0
package/dist/session-capture.js
CHANGED
|
@@ -1,59 +1,47 @@
|
|
|
1
|
-
import { chromium } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import { login } from
|
|
4
|
-
import { generateTOTP } from
|
|
5
|
-
const activeSessions = new Map();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
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;
|
|
28
22
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return JSON.stringify(state);
|
|
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);
|
|
39
32
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
activeSessions.delete(captureId);
|
|
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);
|
|
48
40
|
}
|
|
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
|
-
*/
|
|
54
41
|
async function exploreAppAfterLogin(page) {
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
const sleep = (ms) => page.waitForTimeout(ms).catch(() => {
|
|
43
|
+
});
|
|
44
|
+
const isVisible = `
|
|
57
45
|
function isVisible(el) {
|
|
58
46
|
const s = window.getComputedStyle(el);
|
|
59
47
|
if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) < 0.1) return false;
|
|
@@ -61,11 +49,10 @@ async function exploreAppAfterLogin(page) {
|
|
|
61
49
|
return r.width > 4 && r.height > 4;
|
|
62
50
|
}
|
|
63
51
|
`;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await page.evaluate(new Function(`
|
|
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(`
|
|
69
56
|
${isVisible}
|
|
70
57
|
const inBadCtx = el => {
|
|
71
58
|
let p = el.parentElement;
|
|
@@ -87,10 +74,10 @@ async function exploreAppAfterLogin(page) {
|
|
|
87
74
|
if ((el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') && isVisible(el) && !inBadCtx(el)) targets.add(el);
|
|
88
75
|
});
|
|
89
76
|
targets.forEach(el => { try { el.click(); } catch {} });
|
|
90
|
-
`)).catch(() => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
`)).catch(() => {
|
|
78
|
+
});
|
|
79
|
+
await sleep(600);
|
|
80
|
+
const navLinks = await page.evaluate(new Function(`
|
|
94
81
|
${isVisible}
|
|
95
82
|
const links = new Set();
|
|
96
83
|
const navEls = document.querySelectorAll('nav a, [role="navigation"] a, aside a, header a');
|
|
@@ -105,44 +92,39 @@ async function exploreAppAfterLogin(page) {
|
|
|
105
92
|
});
|
|
106
93
|
return [...links].slice(0, 10); // cap at 10 nav pages
|
|
107
94
|
`)).catch(() => []);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 ───────────────────────────
|
|
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);
|
|
128
104
|
await interactWithCurrentPage(page, isVisible, sleep);
|
|
129
|
-
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
130
107
|
}
|
|
131
|
-
|
|
132
|
-
|
|
108
|
+
if (navLinks.length > 0) {
|
|
109
|
+
await page.goto(startUrl, { waitUntil: "networkidle", timeout: 15e3 }).catch(() => {
|
|
110
|
+
});
|
|
111
|
+
await sleep(400);
|
|
133
112
|
}
|
|
113
|
+
await interactWithCurrentPage(page, isVisible, sleep);
|
|
114
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
134
118
|
}
|
|
135
|
-
/** Interacts with tabs, carousels, accordions, and scrolls on the current page. */
|
|
136
119
|
async function interactWithCurrentPage(page, isVisible, sleep) {
|
|
137
|
-
|
|
138
|
-
await page.evaluate(new Function(`
|
|
120
|
+
await page.evaluate(new Function(`
|
|
139
121
|
${isVisible}
|
|
140
122
|
const tabs = [...document.querySelectorAll('[role="tab"]')].filter(isVisible);
|
|
141
123
|
tabs.forEach(tab => { try { tab.click(); } catch {} });
|
|
142
|
-
`)).catch(() => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
124
|
+
`)).catch(() => {
|
|
125
|
+
});
|
|
126
|
+
await sleep(400);
|
|
127
|
+
await page.evaluate(new Function(`
|
|
146
128
|
${isVisible}
|
|
147
129
|
const nextBtns = [...document.querySelectorAll(
|
|
148
130
|
'button[aria-label*="next" i], button[aria-label*="forward" i], button[aria-label*="right" i], ' +
|
|
@@ -152,10 +134,10 @@ async function interactWithCurrentPage(page, isVisible, sleep) {
|
|
|
152
134
|
for (let i = 0; i < 3; i++) {
|
|
153
135
|
nextBtns.forEach(btn => { try { btn.click(); } catch {} });
|
|
154
136
|
}
|
|
155
|
-
`)).catch(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
137
|
+
`)).catch(() => {
|
|
138
|
+
});
|
|
139
|
+
await sleep(400);
|
|
140
|
+
await page.evaluate(new Function(`
|
|
159
141
|
${isVisible}
|
|
160
142
|
document.querySelectorAll(
|
|
161
143
|
'[data-state="closed"] button, [aria-expanded="false"], ' +
|
|
@@ -163,127 +145,125 @@ async function interactWithCurrentPage(page, isVisible, sleep) {
|
|
|
163
145
|
).forEach(el => {
|
|
164
146
|
if (isVisible(el)) { try { el.click(); } catch {} }
|
|
165
147
|
});
|
|
166
|
-
`)).catch(() => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
160
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
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;
|
|
183
172
|
try {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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(() => { });
|
|
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 };
|
|
249
217
|
}
|
|
218
|
+
}
|
|
219
|
+
loggedIn = true;
|
|
250
220
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 };
|
|
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
|
+
}
|
|
285
228
|
}
|
|
286
|
-
|
|
287
|
-
|
|
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;
|
|
288
243
|
}
|
|
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");
|
|
256
|
+
}
|
|
257
|
+
return { storageState: JSON.stringify(state), cookies, localStorage, finalUrl };
|
|
258
|
+
} finally {
|
|
259
|
+
await browser.close().catch(() => {
|
|
260
|
+
});
|
|
261
|
+
}
|
|
289
262
|
}
|
|
263
|
+
export {
|
|
264
|
+
cancelSessionCapture,
|
|
265
|
+
completeSessionCapture,
|
|
266
|
+
startSessionCapture,
|
|
267
|
+
validateAndCaptureSession
|
|
268
|
+
};
|
|
269
|
+
//# sourceMappingURL=session-capture.js.map
|