@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.
Files changed (83) hide show
  1. package/dist/accessibility-auditor.js +54 -74
  2. package/dist/ai-login.js +227 -249
  3. package/dist/auth-token-rotator.js +57 -67
  4. package/dist/auth.js +299 -336
  5. package/dist/browser-extension.js +104 -142
  6. package/dist/captcha-solver.js +98 -125
  7. package/dist/color-contrast.js +77 -89
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +54 -71
  10. package/dist/cookie-consent.js +68 -83
  11. package/dist/cookie-jar.js +53 -60
  12. package/dist/crawler.js +4494 -5353
  13. package/dist/custom-js-injector.js +45 -61
  14. package/dist/dark-mode-capture.js +67 -81
  15. package/dist/dead-link-detector.js +47 -55
  16. package/dist/element-hash.js +8 -12
  17. package/dist/email-otp.js +154 -195
  18. package/dist/engine-router.js +87 -98
  19. package/dist/export/bulk-export.js +70 -55
  20. package/dist/export/vector-export.js +77 -105
  21. package/dist/extractors/advanced.js +249 -266
  22. package/dist/extractors/artifact-envelope.js +59 -58
  23. package/dist/failure-analysis-agent.js +37 -33
  24. package/dist/feature-flag-detector.js +51 -66
  25. package/dist/form-detector.js +55 -75
  26. package/dist/frontier.js +76 -101
  27. package/dist/gap-pass.js +68 -89
  28. package/dist/graphql-detector.js +62 -72
  29. package/dist/http-engine.js +282 -330
  30. package/dist/incremental-diff.js +54 -87
  31. package/dist/index.js +67 -28
  32. package/dist/input-fuzzer.js +93 -97
  33. package/dist/interactive-probe.js +715 -827
  34. package/dist/job-queue.js +2151 -2169
  35. package/dist/js-coverage.js +52 -56
  36. package/dist/js-router-detector.js +86 -117
  37. package/dist/language-detector.js +51 -63
  38. package/dist/link-classifier.js +32 -50
  39. package/dist/live-view.js +91 -108
  40. package/dist/locator-extractor.js +151 -187
  41. package/dist/logger.js +9 -7
  42. package/dist/markdown-extract.js +48 -61
  43. package/dist/native-crawler.js +780 -946
  44. package/dist/network-observer.js +135 -156
  45. package/dist/network-throttle.js +29 -40
  46. package/dist/openapi-generator.js +77 -92
  47. package/dist/pdf-extractor.js +72 -90
  48. package/dist/performance-metrics.js +80 -102
  49. package/dist/phash.js +125 -144
  50. package/dist/pii-scrubber.js +65 -68
  51. package/dist/playwright-recorder.js +70 -58
  52. package/dist/policies/crawl-safety.js +31 -34
  53. package/dist/profiles/domain-profile.js +130 -125
  54. package/dist/proxy.js +146 -172
  55. package/dist/rate-limiter.js +58 -65
  56. package/dist/redirect-tracker.js +60 -64
  57. package/dist/redis-frontier.js +116 -136
  58. package/dist/redis-queue.js +157 -144
  59. package/dist/response-cache.js +127 -140
  60. package/dist/responsive-detector.js +42 -46
  61. package/dist/retry-policy.js +46 -50
  62. package/dist/robots-txt.js +69 -83
  63. package/dist/screen-reader.js +45 -70
  64. package/dist/screenshot-masker.js +59 -77
  65. package/dist/seo-checker.js +88 -107
  66. package/dist/session-capture.js +192 -212
  67. package/dist/session-export.js +107 -133
  68. package/dist/sitemap-writer.js +38 -41
  69. package/dist/sitemap.js +141 -202
  70. package/dist/skyvern.js +123 -148
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +74 -90
  73. package/dist/stealth/human-behavior.js +156 -173
  74. package/dist/telemetry.js +68 -82
  75. package/dist/test-executor.js +309 -335
  76. package/dist/tls-fingerprint.js +77 -88
  77. package/dist/url-pattern-completer.js +71 -90
  78. package/dist/visual-diff-verdict.js +23 -38
  79. package/dist/webhook-notifier.js +71 -69
  80. package/dist/websocket-detector.js +56 -68
  81. package/dist/worker.js +29 -38
  82. package/package.json +1 -1
  83. package/src/job-queue.ts +136 -0
package/dist/email-otp.js CHANGED
@@ -1,218 +1,177 @@
1
- /**
2
- * Email OTP retrieval — supports 3 providers:
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
- const m6 = text.match(/\b(\d{6})\b/);
15
- if (m6)
16
- return m6[1];
17
- const m48 = text.match(/\b(\d{4,8})\b/);
18
- if (m48)
19
- return m48[1];
20
- const magic = text.match(/\b([A-Z0-9]{6,10})\b/);
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
- const magicPath = /\/(verify|magic|login|auth|confirm|sign-in)[/?#]/i;
28
- const magicQuery = /[?&](token|magic_token|login_token|auth_token|key)=/i;
29
- const urlMatches = text.match(/https:\/\/[^\s<>"')\]]+/g);
30
- if (!urlMatches)
31
- return null;
32
- for (const raw of urlMatches) {
33
- try {
34
- const u = new URL(raw.replace(/[>'")\].,]+$/, ''));
35
- if (magicPath.test(u.pathname) || magicQuery.test(u.search))
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
- return null;
23
+ }
24
+ return null;
41
25
  }
42
26
  function parseEmailContent(text) {
43
- const magicLink = extractMagicLink(text);
44
- if (magicLink)
45
- return { magicLink };
46
- const otp = extractOTP(text);
47
- if (otp)
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
- // ── MailSlurp ────────────────────────────────────────────────────────────────
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
- const deadline = Date.now() + MAX_WAIT_MS;
55
- while (Date.now() < deadline) {
56
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
57
- try {
58
- const listRes = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}/emails?sort=DESC&limit=5&since=${encodeURIComponent(since)}`, { headers: { 'x-api-key': apiKey } });
59
- if (!listRes.ok)
60
- continue;
61
- const emails = await listRes.json();
62
- if (!emails.length)
63
- continue;
64
- const bodyRes = await fetch(`${MAILSLURP_BASE}/emails/${emails[0].id}`, { headers: { 'x-api-key': apiKey } });
65
- if (!bodyRes.ok)
66
- continue;
67
- const email = await bodyRes.json();
68
- const content = parseEmailContent((email.body ?? '') + ' ' + (email.subject ?? ''));
69
- if (content.otp || content.magicLink)
70
- return content;
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
- return null;
53
+ }
54
+ return null;
75
55
  }
76
56
  async function getInboxEmailMailSlurp(apiKey, inboxId) {
77
- try {
78
- const res = await fetch(`${MAILSLURP_BASE}/inboxes/${inboxId}`, { headers: { 'x-api-key': apiKey } });
79
- if (!res.ok)
80
- return null;
81
- const data = await res.json();
82
- return data.emailAddress ?? null;
83
- }
84
- catch {
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
- const deadline = Date.now() + MAX_WAIT_MS;
91
- const sinceMs = since.getTime();
92
- while (Date.now() < deadline) {
93
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
94
- try {
95
- const res = await fetch(`${baseUrl}/api/v1/messages?limit=10`);
96
- if (!res.ok)
97
- continue;
98
- const data = await res.json();
99
- const recent = (data.messages ?? []).filter(m => new Date(m.Created).getTime() > sinceMs);
100
- for (const msg of recent) {
101
- const snippet = parseEmailContent(msg.Snippet + ' ' + msg.Subject);
102
- if (snippet.otp || snippet.magicLink)
103
- return snippet;
104
- // Fetch full body — snippet may truncate the magic link
105
- const bodyRes = await fetch(`${baseUrl}/api/v1/message/${msg.ID}`);
106
- if (!bodyRes.ok)
107
- continue;
108
- const body = await bodyRes.json();
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
- return null;
87
+ }
88
+ return null;
117
89
  }
118
- // ── IMAP (any mailbox) ────────────────────────────────────────────────────────
119
90
  async function waitForContentImap(cfg, since) {
120
- let ImapFlow;
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
- const mod = await import('imapflow');
123
- ImapFlow = mod.ImapFlow;
124
- }
125
- catch {
126
- throw new Error('IMAP provider requires imapflow package. Add it to dependencies.');
127
- }
128
- const client = new ImapFlow({
129
- host: cfg.host,
130
- port: cfg.port,
131
- secure: cfg.tls,
132
- auth: { user: cfg.user, pass: cfg.pass },
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
- finally {
161
- await client.logout().catch(() => { });
162
- }
127
+ } finally {
128
+ await client.logout().catch(() => {
129
+ });
130
+ }
163
131
  }
164
- // ── Public API ────────────────────────────────────────────────────────────────
165
- /**
166
- * Wait for an email and return its OTP code or magic link URL.
167
- * Prefer this over waitForEmailOTP when the app may send passwordless magic links.
168
- */
169
- export async function waitForEmailContent(config, since) {
170
- const sinceStr = typeof since === 'string' ? since : since.toISOString();
171
- const sinceDate = typeof since === 'string' ? new Date(since) : since;
172
- switch (config.provider) {
173
- case 'mailslurp':
174
- if (!config.apiKey || !config.inboxId)
175
- throw new Error('mailslurp requires apiKey + inboxId');
176
- return waitForContentMailSlurp(config.apiKey, config.inboxId, sinceStr);
177
- case 'mailpit':
178
- return waitForContentMailpit(config.mailpitBaseUrl ?? 'http://mailpit:8025', sinceDate);
179
- case 'imap':
180
- if (!config.imapHost || !config.imapUser || !config.imapPass) {
181
- throw new Error('imap requires imapHost, imapUser, imapPass');
182
- }
183
- return waitForContentImap({
184
- host: config.imapHost,
185
- port: config.imapPort ?? 993,
186
- user: config.imapUser,
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
- * Wait for an OTP email and extract the code.
196
- * @deprecated Use waitForEmailContent which also handles magic links.
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
- * Get the email address for a MailSlurp inbox (used to display in UI).
204
- * Returns null for IMAP/Mailpit providers — use imapUser directly.
205
- */
206
- export async function getInboxEmail(configOrApiKey, inboxId) {
207
- // Legacy call: getInboxEmail(apiKey, inboxId)
208
- if (typeof configOrApiKey === 'string') {
209
- return getInboxEmailMailSlurp(configOrApiKey, inboxId ?? '');
210
- }
211
- if (configOrApiKey.provider === 'mailslurp' && configOrApiKey.apiKey && configOrApiKey.inboxId) {
212
- return getInboxEmailMailSlurp(configOrApiKey.apiKey, configOrApiKey.inboxId);
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
@@ -1,105 +1,94 @@
1
- /**
2
- * Per-domain rendering predictor — learns whether a domain requires a full browser
3
- * or can be served by the fast HTTP engine. Implements adaptive crawlee-python pattern.
4
- *
5
- * Strategy:
6
- * 1. On first encounter of a domain, classify the HTTP response body.
7
- * 2. Store the decision in memory (Map). All future URLs on same domain skip re-testing.
8
- * 3. "Requires browser" signals: React/Vue/Angular root with no content, tiny body (<500 chars meaningful text).
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
- * Analyze an HTTP response body to predict if JS rendering is required.
23
- * Returns 'browser' if JS-heavy signals detected, 'http' otherwise.
24
- */
25
- predict(url, htmlBody, statusCode) {
26
- let decision = 'http';
27
- try {
28
- if (statusCode >= 400) {
29
- decision = 'browser'; // let browser handle error pages
30
- }
31
- else {
32
- const bodyLen = htmlBody.length;
33
- // Strip tags to get raw text
34
- const textOnly = htmlBody.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
35
- const wordCount = textOnly.split(/\s+/).filter(w => w.length > 2).length;
36
- // SPA signals: very short text, presence of React/Vue/Angular root markers
37
- const hasSpaRoot = /<div[^>]*\s+id=["'](?:root|app|__nuxt|__next|app-root)["'][^>]*>\s*<\/div>/i.test(htmlBody)
38
- || /<app-root[^>]*>\s*<\/app-root>/i.test(htmlBody);
39
- const hasNoscriptFallback = /<noscript[^>]*>[\s\S]{100,}/i.test(htmlBody);
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
- catch { /* ignore */ }
51
- return decision;
30
+ }
31
+ } catch {
52
32
  }
53
- /** Force a domain to use a specific engine (e.g. after observing crawl errors). */
54
- set(url, engine) {
55
- try {
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
- stats() {
61
- const browserDomains = [];
62
- const httpDomains = [];
63
- for (const [domain, engine] of this.cache) {
64
- if (engine === 'browser')
65
- browserDomains.push(domain);
66
- else
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
- /** Singleton predictor — shared across crawl jobs in the same process. */
73
- export const renderingPredictor = new RenderingTypePredictor();
74
- export function chooseCrawlEngines(input) {
75
- const engines = [];
76
- const reasons = [];
77
- const appType = input.appType?.toLowerCase();
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
- if (input.hasStorageState || input.hasAuthToken) {
82
- engines.push('recorded-session');
83
- reasons.push('session/token auth available');
84
- }
85
- engines.push(input.hasProxy ? 'browser-stealth' : 'browser');
86
- reasons.push(input.hasProxy ? 'proxy configured; prefer stealth browser path' : 'default browser path');
87
- if (input.hasCredentials || input.profile.industry !== 'unknown') {
88
- engines.push('stagehand');
89
- reasons.push(input.hasCredentials ? 'credentials available for AI login/navigation' : `domain profile=${input.profile.industry}`);
90
- }
91
- const hardUi = input.profile.industry === 'legacy-enterprise' || /captcha|blocked|canvas|iframe|bot/i.test(input.previousFailureReason ?? '');
92
- if (input.hasSkyvern && hardUi) {
93
- engines.push('vision');
94
- reasons.push('hard UI or previous block detected; vision fallback enabled');
95
- }
96
- if (engines.length === 1) {
97
- engines.unshift('http');
98
- reasons.push('HTTP/static extraction can be attempted before browser escalation');
99
- }
100
- return {
101
- engines: Array.from(new Set(engines)),
102
- reasons,
103
- confidence: input.profile.confidence,
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