@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/email-otp.js
CHANGED
|
@@ -1,218 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* mailslurp — managed SaaS (existing, no self-hosting)
|
|
4
|
-
* imap — any real IMAP mailbox (Gmail, Outlook, custom domain catch-all)
|
|
5
|
-
* mailpit — self-hosted SMTP trap (dev/staging only)
|
|
6
|
-
*
|
|
7
|
-
* Use mailslurp or imap for production apps.
|
|
8
|
-
* Use mailpit only when you can point the app's SMTP at the trap server.
|
|
9
|
-
*/
|
|
10
|
-
const POLL_INTERVAL_MS = 2_000;
|
|
11
|
-
const MAX_WAIT_MS = 60_000;
|
|
12
|
-
/** Extract OTP code from email body text. Prefers 6-digit, falls back to 4-8 digit. */
|
|
1
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
2
|
+
const MAX_WAIT_MS = 6e4;
|
|
13
3
|
function extractOTP(text) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (magic)
|
|
22
|
-
return magic[1];
|
|
23
|
-
return null;
|
|
4
|
+
const m6 = text.match(/\b(\d{6})\b/);
|
|
5
|
+
if (m6) return m6[1];
|
|
6
|
+
const m48 = text.match(/\b(\d{4,8})\b/);
|
|
7
|
+
if (m48) return m48[1];
|
|
8
|
+
const magic = text.match(/\b([A-Z0-9]{6,10})\b/);
|
|
9
|
+
if (magic) return magic[1];
|
|
10
|
+
return null;
|
|
24
11
|
}
|
|
25
|
-
/** Extract a magic-link URL from email body text. Matches /verify, /magic, /auth/callback, ?token= etc. */
|
|
26
12
|
function extractMagicLink(text) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return u.toString();
|
|
37
|
-
}
|
|
38
|
-
catch { /* skip malformed URLs */ }
|
|
13
|
+
const magicPath = /\/(verify|magic|login|auth|confirm|sign-in)[/?#]/i;
|
|
14
|
+
const magicQuery = /[?&](token|magic_token|login_token|auth_token|key)=/i;
|
|
15
|
+
const urlMatches = text.match(/https:\/\/[^\s<>"')\]]+/g);
|
|
16
|
+
if (!urlMatches) return null;
|
|
17
|
+
for (const raw of urlMatches) {
|
|
18
|
+
try {
|
|
19
|
+
const u = new URL(raw.replace(/[>'")\].,]+$/, ""));
|
|
20
|
+
if (magicPath.test(u.pathname) || magicQuery.test(u.search)) return u.toString();
|
|
21
|
+
} catch {
|
|
39
22
|
}
|
|
40
|
-
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
41
25
|
}
|
|
42
26
|
function parseEmailContent(text) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return { otp };
|
|
49
|
-
return {};
|
|
27
|
+
const magicLink = extractMagicLink(text);
|
|
28
|
+
if (magicLink) return { magicLink };
|
|
29
|
+
const otp = extractOTP(text);
|
|
30
|
+
if (otp) return { otp };
|
|
31
|
+
return {};
|
|
50
32
|
}
|
|
51
|
-
|
|
52
|
-
const MAILSLURP_BASE = 'https://api.mailslurp.com';
|
|
33
|
+
const MAILSLURP_BASE = "https://api.mailslurp.com";
|
|
53
34
|
async function waitForContentMailSlurp(apiKey, inboxId, since) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
catch { /* retry */ }
|
|
35
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
36
|
+
while (Date.now() < deadline) {
|
|
37
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
38
|
+
try {
|
|
39
|
+
const listRes = await fetch(
|
|
40
|
+
`${MAILSLURP_BASE}/inboxes/${inboxId}/emails?sort=DESC&limit=5&since=${encodeURIComponent(since)}`,
|
|
41
|
+
{ headers: { "x-api-key": apiKey } }
|
|
42
|
+
);
|
|
43
|
+
if (!listRes.ok) continue;
|
|
44
|
+
const emails = await listRes.json();
|
|
45
|
+
if (!emails.length) continue;
|
|
46
|
+
const bodyRes = await fetch(`${MAILSLURP_BASE}/emails/${emails[0].id}`, { headers: { "x-api-key": apiKey } });
|
|
47
|
+
if (!bodyRes.ok) continue;
|
|
48
|
+
const email = await bodyRes.json();
|
|
49
|
+
const content = parseEmailContent((email.body ?? "") + " " + (email.subject ?? ""));
|
|
50
|
+
if (content.otp || content.magicLink) return content;
|
|
51
|
+
} catch {
|
|
73
52
|
}
|
|
74
|
-
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
75
55
|
}
|
|
76
56
|
async function getInboxEmailMailSlurp(apiKey, inboxId) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}`, { headers: { "x-api-key": apiKey } });
|
|
59
|
+
if (!res.ok) return null;
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
return data.emailAddress ?? null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
87
65
|
}
|
|
88
|
-
// ── Mailpit (SMTP trap REST API) ──────────────────────────────────────────────
|
|
89
66
|
async function waitForContentMailpit(baseUrl, since) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const full = parseEmailContent((body.Text?.Body ?? '') + ' ' + (body.Subject ?? ''));
|
|
110
|
-
if (full.otp || full.magicLink)
|
|
111
|
-
return full;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch { /* retry */ }
|
|
67
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
68
|
+
const sinceMs = since.getTime();
|
|
69
|
+
while (Date.now() < deadline) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`${baseUrl}/api/v1/messages?limit=10`);
|
|
73
|
+
if (!res.ok) continue;
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
const recent = (data.messages ?? []).filter((m) => new Date(m.Created).getTime() > sinceMs);
|
|
76
|
+
for (const msg of recent) {
|
|
77
|
+
const snippet = parseEmailContent(msg.Snippet + " " + msg.Subject);
|
|
78
|
+
if (snippet.otp || snippet.magicLink) return snippet;
|
|
79
|
+
const bodyRes = await fetch(`${baseUrl}/api/v1/message/${msg.ID}`);
|
|
80
|
+
if (!bodyRes.ok) continue;
|
|
81
|
+
const body = await bodyRes.json();
|
|
82
|
+
const full = parseEmailContent((body.Text?.Body ?? "") + " " + (body.Subject ?? ""));
|
|
83
|
+
if (full.otp || full.magicLink) return full;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
115
86
|
}
|
|
116
|
-
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
117
89
|
}
|
|
118
|
-
// ── IMAP (any mailbox) ────────────────────────────────────────────────────────
|
|
119
90
|
async function waitForContentImap(cfg, since) {
|
|
120
|
-
|
|
91
|
+
let ImapFlow;
|
|
92
|
+
try {
|
|
93
|
+
const mod = await import("imapflow");
|
|
94
|
+
ImapFlow = mod.ImapFlow;
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error("IMAP provider requires imapflow package. Add it to dependencies.");
|
|
97
|
+
}
|
|
98
|
+
const client = new ImapFlow({
|
|
99
|
+
host: cfg.host,
|
|
100
|
+
port: cfg.port,
|
|
101
|
+
secure: cfg.tls,
|
|
102
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
103
|
+
logger: false
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await client.connect();
|
|
107
|
+
const lock = await client.getMailboxLock("INBOX");
|
|
121
108
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
logger: false,
|
|
134
|
-
});
|
|
135
|
-
try {
|
|
136
|
-
await client.connect();
|
|
137
|
-
const lock = await client.getMailboxLock('INBOX');
|
|
138
|
-
try {
|
|
139
|
-
const deadline = Date.now() + MAX_WAIT_MS;
|
|
140
|
-
while (Date.now() < deadline) {
|
|
141
|
-
const uids = await client.search({ seen: false, since });
|
|
142
|
-
if (uids.length) {
|
|
143
|
-
for await (const msg of client.fetch(uids.slice(-5), { source: true })) {
|
|
144
|
-
const { simpleParser } = await import('mailparser');
|
|
145
|
-
const parsed = await simpleParser(msg.source);
|
|
146
|
-
const text = (parsed.text ?? '') + ' ' + (parsed.subject ?? '');
|
|
147
|
-
const content = parseEmailContent(text);
|
|
148
|
-
if (content.otp || content.magicLink)
|
|
149
|
-
return content;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
lock.release();
|
|
109
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
const uids = await client.search({ seen: false, since });
|
|
112
|
+
if (uids.length) {
|
|
113
|
+
for await (const msg of client.fetch(uids.slice(-5), { source: true })) {
|
|
114
|
+
const { simpleParser } = await import("mailparser");
|
|
115
|
+
const parsed = await simpleParser(msg.source);
|
|
116
|
+
const text = (parsed.text ?? "") + " " + (parsed.subject ?? "");
|
|
117
|
+
const content = parseEmailContent(text);
|
|
118
|
+
if (content.otp || content.magicLink) return content;
|
|
119
|
+
}
|
|
158
120
|
}
|
|
121
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
} finally {
|
|
125
|
+
lock.release();
|
|
159
126
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
127
|
+
} finally {
|
|
128
|
+
await client.logout().catch(() => {
|
|
129
|
+
});
|
|
130
|
+
}
|
|
163
131
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
pass: config.imapPass,
|
|
188
|
-
tls: config.imapTls !== false,
|
|
189
|
-
}, sinceDate);
|
|
190
|
-
default:
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
132
|
+
async function waitForEmailContent(config, since) {
|
|
133
|
+
const sinceStr = typeof since === "string" ? since : since.toISOString();
|
|
134
|
+
const sinceDate = typeof since === "string" ? new Date(since) : since;
|
|
135
|
+
switch (config.provider) {
|
|
136
|
+
case "mailslurp":
|
|
137
|
+
if (!config.apiKey || !config.inboxId) throw new Error("mailslurp requires apiKey + inboxId");
|
|
138
|
+
return waitForContentMailSlurp(config.apiKey, config.inboxId, sinceStr);
|
|
139
|
+
case "mailpit":
|
|
140
|
+
return waitForContentMailpit(config.mailpitBaseUrl ?? "http://mailpit:8025", sinceDate);
|
|
141
|
+
case "imap":
|
|
142
|
+
if (!config.imapHost || !config.imapUser || !config.imapPass) {
|
|
143
|
+
throw new Error("imap requires imapHost, imapUser, imapPass");
|
|
144
|
+
}
|
|
145
|
+
return waitForContentImap({
|
|
146
|
+
host: config.imapHost,
|
|
147
|
+
port: config.imapPort ?? 993,
|
|
148
|
+
user: config.imapUser,
|
|
149
|
+
pass: config.imapPass,
|
|
150
|
+
tls: config.imapTls !== false
|
|
151
|
+
}, sinceDate);
|
|
152
|
+
default:
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
193
155
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
*/
|
|
198
|
-
export async function waitForEmailOTP(config, since) {
|
|
199
|
-
const content = await waitForEmailContent(config, since);
|
|
200
|
-
return content?.otp ?? null;
|
|
156
|
+
async function waitForEmailOTP(config, since) {
|
|
157
|
+
const content = await waitForEmailContent(config, since);
|
|
158
|
+
return content?.otp ?? null;
|
|
201
159
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
if (configOrApiKey.provider === 'imap') {
|
|
215
|
-
return configOrApiKey.imapUser ?? null;
|
|
216
|
-
}
|
|
217
|
-
return null;
|
|
160
|
+
async function getInboxEmail(configOrApiKey, inboxId) {
|
|
161
|
+
if (typeof configOrApiKey === "string") {
|
|
162
|
+
return getInboxEmailMailSlurp(configOrApiKey, inboxId ?? "");
|
|
163
|
+
}
|
|
164
|
+
if (configOrApiKey.provider === "mailslurp" && configOrApiKey.apiKey && configOrApiKey.inboxId) {
|
|
165
|
+
return getInboxEmailMailSlurp(configOrApiKey.apiKey, configOrApiKey.inboxId);
|
|
166
|
+
}
|
|
167
|
+
if (configOrApiKey.provider === "imap") {
|
|
168
|
+
return configOrApiKey.imapUser ?? null;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
218
171
|
}
|
|
172
|
+
export {
|
|
173
|
+
getInboxEmail,
|
|
174
|
+
waitForEmailContent,
|
|
175
|
+
waitForEmailOTP
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=email-otp.js.map
|
package/dist/engine-router.js
CHANGED
|
@@ -1,105 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
export class RenderingTypePredictor {
|
|
11
|
-
cache = new Map();
|
|
12
|
-
/** Return cached decision if available, undefined if unknown. */
|
|
13
|
-
getCached(url) {
|
|
14
|
-
try {
|
|
15
|
-
return this.cache.get(new URL(url).hostname);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
1
|
+
class RenderingTypePredictor {
|
|
2
|
+
cache = /* @__PURE__ */ new Map();
|
|
3
|
+
/** Return cached decision if available, undefined if unknown. */
|
|
4
|
+
getCached(url) {
|
|
5
|
+
try {
|
|
6
|
+
return this.cache.get(new URL(url).hostname);
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
20
9
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const tinyBody = bodyLen < 3000 && wordCount < 50;
|
|
41
|
-
if ((hasSpaRoot && wordCount < 100) || (tinyBody && !hasNoscriptFallback)) {
|
|
42
|
-
decision = 'browser';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
catch { /* default http */ }
|
|
47
|
-
try {
|
|
48
|
-
this.cache.set(new URL(url).hostname, decision);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Analyze an HTTP response body to predict if JS rendering is required.
|
|
13
|
+
* Returns 'browser' if JS-heavy signals detected, 'http' otherwise.
|
|
14
|
+
*/
|
|
15
|
+
predict(url, htmlBody, statusCode) {
|
|
16
|
+
let decision = "http";
|
|
17
|
+
try {
|
|
18
|
+
if (statusCode >= 400) {
|
|
19
|
+
decision = "browser";
|
|
20
|
+
} else {
|
|
21
|
+
const bodyLen = htmlBody.length;
|
|
22
|
+
const textOnly = htmlBody.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
23
|
+
const wordCount = textOnly.split(/\s+/).filter((w) => w.length > 2).length;
|
|
24
|
+
const hasSpaRoot = /<div[^>]*\s+id=["'](?:root|app|__nuxt|__next|app-root)["'][^>]*>\s*<\/div>/i.test(htmlBody) || /<app-root[^>]*>\s*<\/app-root>/i.test(htmlBody);
|
|
25
|
+
const hasNoscriptFallback = /<noscript[^>]*>[\s\S]{100,}/i.test(htmlBody);
|
|
26
|
+
const tinyBody = bodyLen < 3e3 && wordCount < 50;
|
|
27
|
+
if (hasSpaRoot && wordCount < 100 || tinyBody && !hasNoscriptFallback) {
|
|
28
|
+
decision = "browser";
|
|
49
29
|
}
|
|
50
|
-
|
|
51
|
-
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
52
32
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.cache.set(new URL(url).hostname, engine);
|
|
57
|
-
}
|
|
58
|
-
catch { /* ignore */ }
|
|
33
|
+
try {
|
|
34
|
+
this.cache.set(new URL(url).hostname, decision);
|
|
35
|
+
} catch {
|
|
59
36
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
httpDomains.push(domain);
|
|
68
|
-
}
|
|
69
|
-
return { cached: this.cache.size, browserDomains, httpDomains };
|
|
37
|
+
return decision;
|
|
38
|
+
}
|
|
39
|
+
/** Force a domain to use a specific engine (e.g. after observing crawl errors). */
|
|
40
|
+
set(url, engine) {
|
|
41
|
+
try {
|
|
42
|
+
this.cache.set(new URL(url).hostname, engine);
|
|
43
|
+
} catch {
|
|
70
44
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (appType === 'android' || appType === 'ios' || appType === 'windows' || appType === 'macos') {
|
|
79
|
-
return { engines: ['native'], reasons: [`appType=${input.appType}`], confidence: 0.95 };
|
|
45
|
+
}
|
|
46
|
+
stats() {
|
|
47
|
+
const browserDomains = [];
|
|
48
|
+
const httpDomains = [];
|
|
49
|
+
for (const [domain, engine] of this.cache) {
|
|
50
|
+
if (engine === "browser") browserDomains.push(domain);
|
|
51
|
+
else httpDomains.push(domain);
|
|
80
52
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
53
|
+
return { cached: this.cache.size, browserDomains, httpDomains };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const renderingPredictor = new RenderingTypePredictor();
|
|
57
|
+
function chooseCrawlEngines(input) {
|
|
58
|
+
const engines = [];
|
|
59
|
+
const reasons = [];
|
|
60
|
+
const appType = input.appType?.toLowerCase();
|
|
61
|
+
if (appType === "android" || appType === "ios" || appType === "windows" || appType === "macos") {
|
|
62
|
+
return { engines: ["native"], reasons: [`appType=${input.appType}`], confidence: 0.95 };
|
|
63
|
+
}
|
|
64
|
+
if (input.hasStorageState || input.hasAuthToken) {
|
|
65
|
+
engines.push("recorded-session");
|
|
66
|
+
reasons.push("session/token auth available");
|
|
67
|
+
}
|
|
68
|
+
engines.push(input.hasProxy ? "browser-stealth" : "browser");
|
|
69
|
+
reasons.push(input.hasProxy ? "proxy configured; prefer stealth browser path" : "default browser path");
|
|
70
|
+
if (input.hasCredentials || input.profile.industry !== "unknown") {
|
|
71
|
+
engines.push("stagehand");
|
|
72
|
+
reasons.push(input.hasCredentials ? "credentials available for AI login/navigation" : `domain profile=${input.profile.industry}`);
|
|
73
|
+
}
|
|
74
|
+
const hardUi = input.profile.industry === "legacy-enterprise" || /captcha|blocked|canvas|iframe|bot/i.test(input.previousFailureReason ?? "");
|
|
75
|
+
if (input.hasSkyvern && hardUi) {
|
|
76
|
+
engines.push("vision");
|
|
77
|
+
reasons.push("hard UI or previous block detected; vision fallback enabled");
|
|
78
|
+
}
|
|
79
|
+
if (engines.length === 1) {
|
|
80
|
+
engines.unshift("http");
|
|
81
|
+
reasons.push("HTTP/static extraction can be attempted before browser escalation");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
engines: Array.from(new Set(engines)),
|
|
85
|
+
reasons,
|
|
86
|
+
confidence: input.profile.confidence
|
|
87
|
+
};
|
|
105
88
|
}
|
|
89
|
+
export {
|
|
90
|
+
RenderingTypePredictor,
|
|
91
|
+
chooseCrawlEngines,
|
|
92
|
+
renderingPredictor
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=engine-router.js.map
|