@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/tls-fingerprint.js
CHANGED
|
@@ -1,92 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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 => {
|
|
44
|
-
|
|
25
|
+
${internalUrls.map((u) => {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(u).pathname + new URL(u).search;
|
|
28
|
+
} catch {
|
|
29
|
+
return u;
|
|
45
30
|
}
|
|
46
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
package/dist/webhook-notifier.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|