@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
@@ -1,92 +1,81 @@
1
- /**
2
- * TLS fingerprint spoofing — Chrome JA3 cipher suite ordering.
3
- *
4
- * Node.js TLS doesn't expose JA3 hash control directly, but ordering
5
- * cipher suites to match Chrome's TLS ClientHello reduces fingerprint
6
- * detectability vs the default OpenSSL order.
7
- *
8
- * For full JA3 bypass, pair with the browser-stealth engine (Camoufox).
9
- */
10
- import { Agent as HttpsAgent } from 'node:https';
11
- import { connect as tlsConnect } from 'node:tls';
12
- // Chrome 120+ TLS 1.3 + 1.2 cipher suite order
13
- export const CHROME_CIPHERS = [
14
- 'TLS_AES_128_GCM_SHA256',
15
- 'TLS_AES_256_GCM_SHA384',
16
- 'TLS_CHACHA20_POLY1305_SHA256',
17
- 'ECDHE-ECDSA-AES128-GCM-SHA256',
18
- 'ECDHE-RSA-AES128-GCM-SHA256',
19
- 'ECDHE-ECDSA-AES256-GCM-SHA384',
20
- 'ECDHE-RSA-AES256-GCM-SHA384',
21
- 'ECDHE-ECDSA-CHACHA20-POLY1305',
22
- 'ECDHE-RSA-CHACHA20-POLY1305',
23
- 'ECDHE-RSA-AES128-SHA',
24
- 'ECDHE-RSA-AES256-SHA',
25
- 'AES128-GCM-SHA256',
26
- 'AES256-GCM-SHA384',
27
- 'AES128-SHA',
28
- 'AES256-SHA',
29
- ].join(':');
30
- // Chrome's ECDH curve preference order
31
- export const CHROME_CURVES = 'X25519:P-256:P-384';
32
- /**
33
- * Returns an HTTPS agent configured with Chrome-like TLS settings.
34
- * Drop-in replacement for the default agent in fetch/got/axios.
35
- */
36
- export function createChromeTlsAgent(opts = {}) {
37
- return new HttpsAgent({
38
- ciphers: CHROME_CIPHERS,
39
- ecdhCurve: CHROME_CURVES,
40
- minVersion: 'TLSv1.2',
41
- maxVersion: 'TLSv1.3',
42
- rejectUnauthorized: opts.rejectUnauthorized ?? true,
43
- keepAlive: opts.keepAlive ?? true,
44
- keepAliveMsecs: opts.keepAliveMsecs ?? 10_000,
45
- maxSockets: opts.maxSockets ?? 50,
46
- });
1
+ import { Agent as HttpsAgent } from "node:https";
2
+ import { connect as tlsConnect } from "node:tls";
3
+ const CHROME_CIPHERS = [
4
+ "TLS_AES_128_GCM_SHA256",
5
+ "TLS_AES_256_GCM_SHA384",
6
+ "TLS_CHACHA20_POLY1305_SHA256",
7
+ "ECDHE-ECDSA-AES128-GCM-SHA256",
8
+ "ECDHE-RSA-AES128-GCM-SHA256",
9
+ "ECDHE-ECDSA-AES256-GCM-SHA384",
10
+ "ECDHE-RSA-AES256-GCM-SHA384",
11
+ "ECDHE-ECDSA-CHACHA20-POLY1305",
12
+ "ECDHE-RSA-CHACHA20-POLY1305",
13
+ "ECDHE-RSA-AES128-SHA",
14
+ "ECDHE-RSA-AES256-SHA",
15
+ "AES128-GCM-SHA256",
16
+ "AES256-GCM-SHA384",
17
+ "AES128-SHA",
18
+ "AES256-SHA"
19
+ ].join(":");
20
+ const CHROME_CURVES = "X25519:P-256:P-384";
21
+ function createChromeTlsAgent(opts = {}) {
22
+ return new HttpsAgent({
23
+ ciphers: CHROME_CIPHERS,
24
+ ecdhCurve: CHROME_CURVES,
25
+ minVersion: "TLSv1.2",
26
+ maxVersion: "TLSv1.3",
27
+ rejectUnauthorized: opts.rejectUnauthorized ?? true,
28
+ keepAlive: opts.keepAlive ?? true,
29
+ keepAliveMsecs: opts.keepAliveMsecs ?? 1e4,
30
+ maxSockets: opts.maxSockets ?? 50
31
+ });
47
32
  }
48
- /**
49
- * Probe a host and return its JA3-relevant TLS parameters.
50
- * Useful for verifying that the fingerprint changed vs default.
51
- */
52
- export async function probeTlsFingerprint(host, port = 443) {
53
- return new Promise((resolve, reject) => {
54
- const socket = tlsConnect({
55
- host,
56
- port,
57
- servername: host,
58
- ciphers: CHROME_CIPHERS,
59
- ecdhCurve: CHROME_CURVES,
60
- minVersion: 'TLSv1.2',
61
- rejectUnauthorized: false,
62
- }, () => {
63
- const cert = socket.getPeerCertificate();
64
- resolve({
65
- protocol: socket.getProtocol() ?? 'unknown',
66
- cipher: socket.getCipher()?.name ?? 'unknown',
67
- authorized: socket.authorized,
68
- subjectCN: cert?.subject?.CN?.toString() ?? null,
69
- });
70
- socket.destroy();
71
- });
72
- socket.on('error', reject);
73
- setTimeout(() => { socket.destroy(); reject(new Error('TLS probe timeout')); }, 5_000);
33
+ async function probeTlsFingerprint(host, port = 443) {
34
+ return new Promise((resolve, reject) => {
35
+ const socket = tlsConnect({
36
+ host,
37
+ port,
38
+ servername: host,
39
+ ciphers: CHROME_CIPHERS,
40
+ ecdhCurve: CHROME_CURVES,
41
+ minVersion: "TLSv1.2",
42
+ rejectUnauthorized: false
43
+ }, () => {
44
+ const cert = socket.getPeerCertificate();
45
+ resolve({
46
+ protocol: socket.getProtocol() ?? "unknown",
47
+ cipher: socket.getCipher()?.name ?? "unknown",
48
+ authorized: socket.authorized,
49
+ subjectCN: cert?.subject?.CN?.toString() ?? null
50
+ });
51
+ socket.destroy();
74
52
  });
53
+ socket.on("error", reject);
54
+ setTimeout(() => {
55
+ socket.destroy();
56
+ reject(new Error("TLS probe timeout"));
57
+ }, 5e3);
58
+ });
75
59
  }
76
- /**
77
- * Node.js fetch dispatcher options with Chrome TLS agent.
78
- * Pass as the `dispatcher` option to undici or as `agent` to node-fetch.
79
- */
80
- export const chromeTlsAgent = createChromeTlsAgent();
81
- export const CHROME_TLS_HEADERS = {
82
- 'Accept-Encoding': 'gzip, deflate, br, zstd',
83
- 'Accept-Language': 'en-US,en;q=0.9',
84
- 'sec-ch-ua': '"Google Chrome";v="120", "Chromium";v="120", "Not-A.Brand";v="99"',
85
- 'sec-ch-ua-mobile': '?0',
86
- 'sec-ch-ua-platform': '"Windows"',
87
- 'Sec-Fetch-Dest': 'document',
88
- 'Sec-Fetch-Mode': 'navigate',
89
- 'Sec-Fetch-Site': 'none',
90
- 'Sec-Fetch-User': '?1',
91
- 'Upgrade-Insecure-Requests': '1',
60
+ const chromeTlsAgent = createChromeTlsAgent();
61
+ const CHROME_TLS_HEADERS = {
62
+ "Accept-Encoding": "gzip, deflate, br, zstd",
63
+ "Accept-Language": "en-US,en;q=0.9",
64
+ "sec-ch-ua": '"Google Chrome";v="120", "Chromium";v="120", "Not-A.Brand";v="99"',
65
+ "sec-ch-ua-mobile": "?0",
66
+ "sec-ch-ua-platform": '"Windows"',
67
+ "Sec-Fetch-Dest": "document",
68
+ "Sec-Fetch-Mode": "navigate",
69
+ "Sec-Fetch-Site": "none",
70
+ "Sec-Fetch-User": "?1",
71
+ "Upgrade-Insecure-Requests": "1"
72
+ };
73
+ export {
74
+ CHROME_CIPHERS,
75
+ CHROME_CURVES,
76
+ CHROME_TLS_HEADERS,
77
+ chromeTlsAgent,
78
+ createChromeTlsAgent,
79
+ probeTlsFingerprint
92
80
  };
81
+ //# sourceMappingURL=tls-fingerprint.js.map
@@ -1,107 +1,88 @@
1
- /**
2
- * P1: URL Pattern LLM Completion
3
- *
4
- * After BFS crawl completes, runs a single LLM call to predict URLs that exist
5
- * but were never linked from within the app. Validates each candidate with
6
- * HTTP HEAD before emitting as crawl evidence (URL_CANDIDATES).
7
- *
8
- * Cost: ~1 LLM call + N HEAD requests. Takes ~5-30 seconds depending on
9
- * candidate count. Does NOT launch a browser.
10
- */
11
- import { logger as rootLogger } from './logger.js';
12
- const logger = rootLogger.child({ module: 'url-pattern-completer' });
13
- /**
14
- * Returns validated URL candidates (HEAD=200) predicted by LLM from
15
- * the pattern of already-discovered URLs.
16
- */
17
- export async function runUrlPatternCompletion(discoveredUrls, appUrl, llmCfg) {
18
- if (!llmCfg?.apiKey || discoveredUrls.length < 2)
19
- return [];
20
- const origin = (() => { try {
21
- return new URL(appUrl).origin;
1
+ import { logger as rootLogger } from "./logger.js";
2
+ const logger = rootLogger.child({ module: "url-pattern-completer" });
3
+ async function runUrlPatternCompletion(discoveredUrls, appUrl, llmCfg) {
4
+ if (!llmCfg?.apiKey || discoveredUrls.length < 2) return [];
5
+ const origin = (() => {
6
+ try {
7
+ return new URL(appUrl).origin;
8
+ } catch {
9
+ return "";
22
10
  }
23
- catch {
24
- return '';
25
- } })();
26
- if (!origin)
27
- return [];
28
- // Only send internal URLs (same origin, no assets)
29
- const ASSET_EXT = /\.(png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|eot|css|js|map|json|xml|txt|pdf)$/i;
30
- const internalUrls = discoveredUrls
31
- .filter(u => { try {
32
- return new URL(u).origin === origin && !ASSET_EXT.test(u);
11
+ })();
12
+ if (!origin) return [];
13
+ const ASSET_EXT = /\.(png|jpg|jpeg|gif|svg|ico|webp|woff|woff2|ttf|eot|css|js|map|json|xml|txt|pdf)$/i;
14
+ const internalUrls = discoveredUrls.filter((u) => {
15
+ try {
16
+ return new URL(u).origin === origin && !ASSET_EXT.test(u);
17
+ } catch {
18
+ return false;
33
19
  }
34
- catch {
35
- return false;
36
- } })
37
- .slice(0, 60); // cap context size
38
- if (internalUrls.length < 2)
39
- return [];
40
- const prompt = `You are analyzing a web application's URL structure to find screens that exist but weren't discovered during automated crawling.
20
+ }).slice(0, 60);
21
+ if (internalUrls.length < 2) return [];
22
+ const prompt = `You are analyzing a web application's URL structure to find screens that exist but weren't discovered during automated crawling.
41
23
 
42
24
  Discovered URLs:
43
- ${internalUrls.map(u => { try {
44
- return new URL(u).pathname + new URL(u).search;
25
+ ${internalUrls.map((u) => {
26
+ try {
27
+ return new URL(u).pathname + new URL(u).search;
28
+ } catch {
29
+ return u;
45
30
  }
46
- catch {
47
- return u;
48
- } }).join('\n')}
31
+ }).join("\n")}
49
32
 
50
- Based on these URL patterns, suggest up to 15 additional URLs that likely exist in this application but weren't found — typically settings subpages, admin panels, profile variants, paginated views, or feature-gated routes.
33
+ Based on these URL patterns, suggest up to 15 additional URLs that likely exist in this application but weren't found \u2014 typically settings subpages, admin panels, profile variants, paginated views, or feature-gated routes.
51
34
 
52
35
  Rules:
53
36
  - Only suggest URLs on the same domain
54
37
  - Use relative paths (starting with /)
55
38
  - Skip generic guesses (/login, /register, /home) unless the pattern strongly implies them
56
39
  - Focus on patterns YOU SEE in the discovered URLs (e.g. if /settings/profile exists, try /settings/security, /settings/billing)
57
- - Confidence 0.0–1.0: how certain you are the page exists
40
+ - Confidence 0.0\u20131.0: how certain you are the page exists
58
41
 
59
42
  Respond with ONLY valid JSON, no explanation:
60
43
  {"candidates":[{"path":"/settings/billing","confidence":0.85,"reason":"settings subpage pattern from /settings/profile"},{"path":"/admin/users","confidence":0.6,"reason":"admin prefix implied by /admin/dashboard"}]}`;
61
- try {
62
- const Anthropic = (await import('@anthropic-ai/sdk')).default;
63
- const client = new Anthropic({ apiKey: llmCfg.apiKey });
64
- const response = await client.messages.create({
65
- model: llmCfg.model ?? 'claude-haiku-4-5-20251001',
66
- max_tokens: 1024,
67
- temperature: 0,
68
- // @ts-ignore: cache_control supported in beta
69
- system: [{ type: 'text', text: 'You are a URL pattern analyst. Always respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],
70
- messages: [{ role: 'user', content: prompt }],
71
- });
72
- const text = response.content[0]?.text ?? '';
73
- const jsonMatch = text.match(/\{[\s\S]*\}/);
74
- if (!jsonMatch)
75
- return [];
76
- const parsed = JSON.parse(jsonMatch[0]);
77
- const rawCandidates = parsed.candidates ?? [];
78
- if (!Array.isArray(rawCandidates) || rawCandidates.length === 0)
79
- return [];
80
- // HEAD-validate each candidate
81
- const results = [];
82
- for (const c of rawCandidates.slice(0, 20)) {
83
- if (!c.path || typeof c.path !== 'string')
84
- continue;
85
- const fullUrl = c.path.startsWith('http') ? c.path : `${origin}${c.path.startsWith('/') ? '' : '/'}${c.path}`;
86
- try {
87
- const controller = new AbortController();
88
- const timeout = setTimeout(() => controller.abort(), 6_000);
89
- const res = await fetch(fullUrl, { method: 'HEAD', signal: controller.signal, redirect: 'follow' }).catch(() => null);
90
- clearTimeout(timeout);
91
- const status = res?.ok ? 'hit' : 'miss';
92
- results.push({ url: fullUrl, confidence: c.confidence ?? 0.5, reason: c.reason ?? '', status });
93
- logger.info({ url: fullUrl, httpStatus: res?.status ?? 'timeout', status }, '[url-completer] candidate checked');
94
- }
95
- catch {
96
- results.push({ url: fullUrl, confidence: c.confidence ?? 0.5, reason: c.reason ?? '', status: 'error' });
97
- }
98
- }
99
- const hits = results.filter(r => r.status === 'hit');
100
- logger.info({ total: results.length, hits: hits.length }, '[url-completer] pattern completion done');
101
- return results;
102
- }
103
- catch (err) {
104
- logger.warn({ err: String(err) }, '[url-completer] LLM call failed — skipping pattern completion');
105
- return [];
44
+ try {
45
+ const Anthropic = (await import("@anthropic-ai/sdk")).default;
46
+ const client = new Anthropic({ apiKey: llmCfg.apiKey });
47
+ const response = await client.messages.create({
48
+ model: llmCfg.model ?? "claude-haiku-4-5-20251001",
49
+ max_tokens: 1024,
50
+ temperature: 0,
51
+ // @ts-ignore: cache_control supported in beta
52
+ system: [{ type: "text", text: "You are a URL pattern analyst. Always respond with valid JSON only.", cache_control: { type: "ephemeral" } }],
53
+ messages: [{ role: "user", content: prompt }]
54
+ });
55
+ const text = response.content[0]?.text ?? "";
56
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
57
+ if (!jsonMatch) return [];
58
+ const parsed = JSON.parse(jsonMatch[0]);
59
+ const rawCandidates = parsed.candidates ?? [];
60
+ if (!Array.isArray(rawCandidates) || rawCandidates.length === 0) return [];
61
+ const results = [];
62
+ for (const c of rawCandidates.slice(0, 20)) {
63
+ if (!c.path || typeof c.path !== "string") continue;
64
+ const fullUrl = c.path.startsWith("http") ? c.path : `${origin}${c.path.startsWith("/") ? "" : "/"}${c.path}`;
65
+ try {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), 6e3);
68
+ const res = await fetch(fullUrl, { method: "HEAD", signal: controller.signal, redirect: "follow" }).catch(() => null);
69
+ clearTimeout(timeout);
70
+ const status = res?.ok ? "hit" : "miss";
71
+ results.push({ url: fullUrl, confidence: c.confidence ?? 0.5, reason: c.reason ?? "", status });
72
+ logger.info({ url: fullUrl, httpStatus: res?.status ?? "timeout", status }, "[url-completer] candidate checked");
73
+ } catch {
74
+ results.push({ url: fullUrl, confidence: c.confidence ?? 0.5, reason: c.reason ?? "", status: "error" });
75
+ }
106
76
  }
77
+ const hits = results.filter((r) => r.status === "hit");
78
+ logger.info({ total: results.length, hits: hits.length }, "[url-completer] pattern completion done");
79
+ return results;
80
+ } catch (err) {
81
+ logger.warn({ err: String(err) }, "[url-completer] LLM call failed \u2014 skipping pattern completion");
82
+ return [];
83
+ }
107
84
  }
85
+ export {
86
+ runUrlPatternCompletion
87
+ };
88
+ //# sourceMappingURL=url-pattern-completer.js.map
@@ -1,40 +1,25 @@
1
- /**
2
- * Pure visual-diff verdict logic, split out of crawler.ts so it can be unit-tested
3
- * without pulling in crawler.ts's full import graph (Playwright, Stagehand, and
4
- * transitively @detiq/database, which instantiates a PrismaClient at module load and
5
- * so requires a live DATABASE_URL just to import the module).
6
- */
7
- /** Cheap, always-on first-pass verdict from perceptual hash distance + layout/element diffs. */
8
- export function pHashVerdict(pHashDistance, changeLevel, changes) {
9
- const changeType = changeLevel === 'IDENTICAL' ? 'NONE'
10
- : changeLevel === 'MINOR' ? 'MINOR'
11
- : changeLevel === 'MODERATE' ? 'SIGNIFICANT'
12
- : 'CRITICAL';
13
- const recommendation = pHashDistance === 0 ? 'APPROVE'
14
- : pHashDistance < 5 ? 'APPROVE'
15
- : pHashDistance < 20 ? 'REVIEW'
16
- : 'FLAG';
17
- return { changeType, recommendation, changes };
1
+ function pHashVerdict(pHashDistance, changeLevel, changes) {
2
+ const changeType = changeLevel === "IDENTICAL" ? "NONE" : changeLevel === "MINOR" ? "MINOR" : changeLevel === "MODERATE" ? "SIGNIFICANT" : "CRITICAL";
3
+ const recommendation = pHashDistance === 0 ? "APPROVE" : pHashDistance < 5 ? "APPROVE" : pHashDistance < 20 ? "REVIEW" : "FLAG";
4
+ return { changeType, recommendation, changes };
18
5
  }
19
- /** Replaces a pHash-only verdict with the AI's semantic judgment once it's available,
20
- * and drafts a bug report for anything still CRITICAL/SIGNIFICANT after AI review
21
- * (pHash flags plenty of anti-aliasing/font-rendering noise the AI correctly discards
22
- * as NONE/MINOR — only what survives AI review is worth a draft). */
23
- export function mergeAiVisualVerdict(pHashResult, aiResult, screenName) {
24
- if (!aiResult.changeType)
25
- return pHashResult;
26
- const changeType = aiResult.changeType;
27
- const recommendation = aiResult.recommendation;
28
- const changes = Array.isArray(aiResult.changes) && aiResult.changes.length > 0 ? aiResult.changes : pHashResult.changes;
29
- const aiSummary = aiResult.summary ?? undefined;
30
- const bugReportDraft = (changeType === 'CRITICAL' || changeType === 'SIGNIFICANT')
31
- ? {
32
- title: `Visual regression on "${screenName}"`,
33
- severity: changeType === 'CRITICAL' ? 'High' : 'Medium',
34
- steps: ['Navigate to the screen', 'Compare current view to baseline screenshot'],
35
- expected: 'Screen appearance should match the stored baseline.',
36
- actual: aiSummary || `Screen has changed (diff score: ${aiResult.diffScore.toFixed(1)}%)`,
37
- }
38
- : undefined;
39
- return { changeType, recommendation, changes, bugReportDraft, aiSummary };
6
+ function mergeAiVisualVerdict(pHashResult, aiResult, screenName) {
7
+ if (!aiResult.changeType) return pHashResult;
8
+ const changeType = aiResult.changeType;
9
+ const recommendation = aiResult.recommendation;
10
+ const changes = Array.isArray(aiResult.changes) && aiResult.changes.length > 0 ? aiResult.changes : pHashResult.changes;
11
+ const aiSummary = aiResult.summary ?? void 0;
12
+ const bugReportDraft = changeType === "CRITICAL" || changeType === "SIGNIFICANT" ? {
13
+ title: `Visual regression on "${screenName}"`,
14
+ severity: changeType === "CRITICAL" ? "High" : "Medium",
15
+ steps: ["Navigate to the screen", "Compare current view to baseline screenshot"],
16
+ expected: "Screen appearance should match the stored baseline.",
17
+ actual: aiSummary || `Screen has changed (diff score: ${aiResult.diffScore.toFixed(1)}%)`
18
+ } : void 0;
19
+ return { changeType, recommendation, changes, bugReportDraft, aiSummary };
40
20
  }
21
+ export {
22
+ mergeAiVisualVerdict,
23
+ pHashVerdict
24
+ };
25
+ //# sourceMappingURL=visual-diff-verdict.js.map
@@ -1,74 +1,76 @@
1
- /**
2
- * Webhook notifier — fire HTTP POST callbacks on crawl lifecycle events.
3
- *
4
- * Supports: crawl.started, crawl.progress, crawl.completed, crawl.failed
5
- * Payload format compatible with Slack, Discord, and generic REST webhooks.
6
- * Retries 3x on failure with exponential backoff.
7
- */
8
1
  async function hmacSign(secret, payload) {
9
- const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
10
- const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
11
- return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
2
+ const key = await crypto.subtle.importKey(
3
+ "raw",
4
+ new TextEncoder().encode(secret),
5
+ { name: "HMAC", hash: "SHA-256" },
6
+ false,
7
+ ["sign"]
8
+ );
9
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
10
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
12
11
  }
13
- export class WebhookNotifier {
14
- opts;
15
- constructor(opts) {
16
- this.opts = {
17
- url: opts.url,
18
- secret: opts.secret,
19
- headers: opts.headers ?? {},
20
- timeoutMs: opts.timeoutMs ?? 10_000,
21
- maxRetries: opts.maxRetries ?? 3,
22
- };
12
+ class WebhookNotifier {
13
+ opts;
14
+ constructor(opts) {
15
+ this.opts = {
16
+ url: opts.url,
17
+ secret: opts.secret,
18
+ headers: opts.headers ?? {},
19
+ timeoutMs: opts.timeoutMs ?? 1e4,
20
+ maxRetries: opts.maxRetries ?? 3
21
+ };
22
+ }
23
+ async notify(event, jobId, projectId, tenantId, data = {}) {
24
+ const payload = {
25
+ event,
26
+ jobId,
27
+ projectId,
28
+ tenantId,
29
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
30
+ data
31
+ };
32
+ const body = JSON.stringify(payload);
33
+ const headers = {
34
+ "Content-Type": "application/json",
35
+ "User-Agent": "ZeTa-Crawler/1.0",
36
+ ...this.opts.headers
37
+ };
38
+ if (this.opts.secret) {
39
+ headers["X-ZeTa-Signature"] = `sha256=${await hmacSign(this.opts.secret, body)}`;
23
40
  }
24
- async notify(event, jobId, projectId, tenantId, data = {}) {
25
- const payload = {
26
- event,
27
- jobId,
28
- projectId,
29
- tenantId,
30
- timestamp: new Date().toISOString(),
31
- data,
32
- };
33
- const body = JSON.stringify(payload);
34
- const headers = {
35
- 'Content-Type': 'application/json',
36
- 'User-Agent': 'ZeTa-Crawler/1.0',
37
- ...this.opts.headers,
38
- };
39
- if (this.opts.secret) {
40
- headers['X-ZeTa-Signature'] = `sha256=${await hmacSign(this.opts.secret, body)}`;
41
- }
42
- let lastErr;
43
- for (let attempt = 0; attempt <= this.opts.maxRetries; attempt++) {
44
- try {
45
- const res = await fetch(this.opts.url, {
46
- method: 'POST',
47
- headers,
48
- body,
49
- signal: AbortSignal.timeout(this.opts.timeoutMs),
50
- });
51
- if (res.ok)
52
- return;
53
- lastErr = new Error(`Webhook HTTP ${res.status}`);
54
- }
55
- catch (e) {
56
- lastErr = e;
57
- }
58
- if (attempt < this.opts.maxRetries) {
59
- await new Promise(r => setTimeout(r, 1_000 * Math.pow(2, attempt)));
60
- }
61
- }
62
- console.error(`[webhook] Failed to deliver ${event} after ${this.opts.maxRetries + 1} attempts:`, lastErr?.message);
63
- }
64
- /** Slack-compatible message format */
65
- static slackPayload(event, data) {
66
- const emoji = event === 'crawl.completed' ? '✅' : event === 'crawl.failed' ? '❌' : '🔄';
67
- return {
68
- text: `${emoji} *ZeTa Crawl Event: ${event}*`,
69
- blocks: [
70
- { type: 'section', text: { type: 'mrkdwn', text: `${emoji} *${event}*\n${Object.entries(data).map(([k, v]) => `• *${k}*: ${v}`).join('\n')}` } },
71
- ],
72
- };
41
+ let lastErr;
42
+ for (let attempt = 0; attempt <= this.opts.maxRetries; attempt++) {
43
+ try {
44
+ const res = await fetch(this.opts.url, {
45
+ method: "POST",
46
+ headers,
47
+ body,
48
+ signal: AbortSignal.timeout(this.opts.timeoutMs)
49
+ });
50
+ if (res.ok) return;
51
+ lastErr = new Error(`Webhook HTTP ${res.status}`);
52
+ } catch (e) {
53
+ lastErr = e;
54
+ }
55
+ if (attempt < this.opts.maxRetries) {
56
+ await new Promise((r) => setTimeout(r, 1e3 * Math.pow(2, attempt)));
57
+ }
73
58
  }
59
+ console.error(`[webhook] Failed to deliver ${event} after ${this.opts.maxRetries + 1} attempts:`, lastErr?.message);
60
+ }
61
+ /** Slack-compatible message format */
62
+ static slackPayload(event, data) {
63
+ const emoji = event === "crawl.completed" ? "\u2705" : event === "crawl.failed" ? "\u274C" : "\u{1F504}";
64
+ return {
65
+ text: `${emoji} *ZeTa Crawl Event: ${event}*`,
66
+ blocks: [
67
+ { type: "section", text: { type: "mrkdwn", text: `${emoji} *${event}*
68
+ ${Object.entries(data).map(([k, v]) => `\u2022 *${k}*: ${v}`).join("\n")}` } }
69
+ ]
70
+ };
71
+ }
74
72
  }
73
+ export {
74
+ WebhookNotifier
75
+ };
76
+ //# sourceMappingURL=webhook-notifier.js.map