@detiq/crawler 1.1.7 → 1.1.9
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 +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
package/dist/gap-pass.js
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
/**
|
|
2
|
+
* P6: Gap Pass — LLM-guided second crawl pass
|
|
3
|
+
*
|
|
4
|
+
* After the primary BFS crawl completes, this module:
|
|
5
|
+
* 1. Sends the discovered screen list to an LLM for gap analysis
|
|
6
|
+
* 2. HEAD-validates each suggested URL
|
|
7
|
+
* 3. Returns confirmed gap URLs for targeted re-crawl
|
|
8
|
+
*
|
|
9
|
+
* The caller (crawler.ts) is responsible for actually visiting the confirmed
|
|
10
|
+
* URLs with the existing browser — this module only identifies them.
|
|
11
|
+
*/
|
|
12
|
+
import { logger as rootLogger } from './logger.js';
|
|
13
|
+
const logger = rootLogger.child({ module: 'gap-pass' });
|
|
14
|
+
/**
|
|
15
|
+
* Run LLM gap analysis + HEAD validation.
|
|
16
|
+
* Returns candidates with `confirmedUrl` set for those where HEAD returned 200.
|
|
17
|
+
*/
|
|
18
|
+
export async function runGapAnalysis(screens, appUrl, llmCfg) {
|
|
19
|
+
if (!llmCfg?.apiKey || screens.length < 2)
|
|
20
|
+
return [];
|
|
21
|
+
const origin = (() => { try {
|
|
22
|
+
return new URL(appUrl).origin;
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
catch {
|
|
25
|
+
return '';
|
|
26
|
+
} })();
|
|
27
|
+
if (!origin)
|
|
28
|
+
return [];
|
|
29
|
+
// Cap context — only send url + name, not full element data
|
|
30
|
+
const screenList = screens
|
|
31
|
+
.slice(0, 80)
|
|
32
|
+
.map(s => {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(s.url).pathname + (s.name ? ` (${s.name})` : '');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return s.url;
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.join('\n');
|
|
41
|
+
const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.
|
|
21
42
|
|
|
22
43
|
Discovered screens (${screens.length} total):
|
|
23
44
|
${screenList}
|
|
@@ -28,55 +49,55 @@ Based on these screens and common patterns for this type of web application, ide
|
|
|
28
49
|
|
|
29
50
|
Respond with ONLY valid JSON, no markdown:
|
|
30
51
|
{"gaps":[{"category":"User profile edit","urlPatterns":["/profile/edit","/account/profile"],"confidence":0.8,"rationale":"Profile view found but no edit page"},{"category":"Billing history","urlPatterns":["/settings/billing","/account/billing"],"confidence":0.7,"rationale":"Settings section found but no billing page"}]}`;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
try {
|
|
53
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
54
|
+
const client = new Anthropic({ apiKey: llmCfg.apiKey });
|
|
55
|
+
const response = await client.messages.create({
|
|
56
|
+
model: llmCfg.model ?? 'claude-haiku-4-5-20251001',
|
|
57
|
+
max_tokens: 1024,
|
|
58
|
+
temperature: 0,
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
system: [{ type: 'text', text: 'You are a web application coverage analyst. Respond with valid JSON only.', cache_control: { type: 'ephemeral' } }],
|
|
61
|
+
messages: [{ role: 'user', content: prompt }],
|
|
62
|
+
});
|
|
63
|
+
const text = response.content[0]?.text ?? '';
|
|
64
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
65
|
+
if (!jsonMatch)
|
|
66
|
+
return [];
|
|
67
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
68
|
+
const rawGaps = parsed.gaps ?? [];
|
|
69
|
+
if (!Array.isArray(rawGaps) || rawGaps.length === 0)
|
|
70
|
+
return [];
|
|
71
|
+
logger.info({ gaps: rawGaps.length }, '[P6] Gap analysis: LLM returned candidates');
|
|
72
|
+
// HEAD-validate each URL pattern, take the first 200
|
|
73
|
+
const results = [];
|
|
74
|
+
for (const gap of rawGaps.slice(0, 15)) {
|
|
75
|
+
let confirmedUrl = null;
|
|
76
|
+
let headStatus = null;
|
|
77
|
+
for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {
|
|
78
|
+
const fullUrl = pattern.startsWith('http') ? pattern : `${origin}${pattern.startsWith('/') ? '' : '/'}${pattern}`;
|
|
79
|
+
try {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), 6_000);
|
|
82
|
+
const res = await fetch(fullUrl, { method: 'HEAD', signal: controller.signal, redirect: 'follow' }).catch(() => null);
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
headStatus = res?.status ?? null;
|
|
85
|
+
if (res?.ok) {
|
|
86
|
+
confirmedUrl = fullUrl;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch { /* non-fatal */ }
|
|
91
|
+
}
|
|
92
|
+
results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });
|
|
93
|
+
logger.info({ category: gap.category, confirmedUrl, headStatus }, '[P6] Gap candidate checked');
|
|
66
94
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
const confirmed = results.filter(r => r.confirmedUrl).length;
|
|
96
|
+
logger.info({ total: results.length, confirmed }, '[P6] Gap pass analysis complete');
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
logger.warn({ err: String(err) }, '[P6] Gap analysis failed — non-fatal');
|
|
101
|
+
return [];
|
|
70
102
|
}
|
|
71
|
-
const confirmed = results.filter((r) => r.confirmedUrl).length;
|
|
72
|
-
logger.info({ total: results.length, confirmed }, "[P6] Gap pass analysis complete");
|
|
73
|
-
return results;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
logger.warn({ err: String(err) }, "[P6] Gap analysis failed \u2014 non-fatal");
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
103
|
}
|
|
79
|
-
export {
|
|
80
|
-
runGapAnalysis
|
|
81
|
-
};
|
|
82
|
-
//# sourceMappingURL=gap-pass.js.map
|
package/dist/graphql-detector.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL detection + introspection.
|
|
3
|
+
*
|
|
4
|
+
* During crawl, if a GraphQL endpoint is discovered (via network observer or
|
|
5
|
+
* known-URL heuristics), fire a minimal introspection query to get the schema.
|
|
6
|
+
* The schema is stored in the crawl artifact for use during test generation.
|
|
7
|
+
*/
|
|
1
8
|
const INTROSPECTION_QUERY = `
|
|
2
9
|
query IntrospectionQuery {
|
|
3
10
|
__schema {
|
|
@@ -15,72 +22,75 @@ const INTROSPECTION_QUERY = `
|
|
|
15
22
|
}
|
|
16
23
|
`;
|
|
17
24
|
function flattenType(t) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
if (!t)
|
|
26
|
+
return 'unknown';
|
|
27
|
+
if (t.kind === 'NON_NULL' || t.kind === 'LIST')
|
|
28
|
+
return flattenType(t.ofType);
|
|
29
|
+
return t.name ?? 'unknown';
|
|
21
30
|
}
|
|
22
31
|
function parseSchema(raw) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async function introspectGraphQL(endpoint, opts = {}) {
|
|
39
|
-
const { headers = {}, timeoutMs = 1e4, cookies } = opts;
|
|
40
|
-
const t0 = Date.now();
|
|
41
|
-
try {
|
|
42
|
-
const reqHeaders = {
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
"Accept": "application/json",
|
|
45
|
-
...headers
|
|
32
|
+
const s = raw?.__schema ?? raw?.data?.__schema ?? raw;
|
|
33
|
+
const types = (s?.types ?? [])
|
|
34
|
+
.filter((t) => !t.name?.startsWith('__'))
|
|
35
|
+
.map((t) => ({
|
|
36
|
+
name: t.name,
|
|
37
|
+
kind: t.kind,
|
|
38
|
+
fields: (t.fields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
|
|
39
|
+
inputFields: (t.inputFields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
|
|
40
|
+
enumValues: (t.enumValues ?? []).map((e) => e.name),
|
|
41
|
+
}));
|
|
42
|
+
return {
|
|
43
|
+
queryType: s?.queryType?.name,
|
|
44
|
+
mutationType: s?.mutationType?.name,
|
|
45
|
+
subscriptionType: s?.subscriptionType?.name,
|
|
46
|
+
types,
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
}
|
|
49
|
+
export async function introspectGraphQL(endpoint, opts = {}) {
|
|
50
|
+
const { headers = {}, timeoutMs = 10_000, cookies } = opts;
|
|
51
|
+
const t0 = Date.now();
|
|
52
|
+
try {
|
|
53
|
+
const reqHeaders = {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'Accept': 'application/json',
|
|
56
|
+
...headers,
|
|
57
|
+
};
|
|
58
|
+
if (cookies)
|
|
59
|
+
reqHeaders['Cookie'] = cookies;
|
|
60
|
+
const res = await fetch(endpoint, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: reqHeaders,
|
|
63
|
+
body: JSON.stringify({ query: INTROSPECTION_QUERY }),
|
|
64
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
return { endpoint, schema: null, error: `HTTP ${res.status}`, durationMs: Date.now() - t0 };
|
|
68
|
+
}
|
|
69
|
+
const json = await res.json();
|
|
70
|
+
if (json?.errors?.length) {
|
|
71
|
+
const msg = json.errors[0]?.message ?? 'introspection disabled';
|
|
72
|
+
return { endpoint, schema: null, error: msg, durationMs: Date.now() - t0 };
|
|
73
|
+
}
|
|
74
|
+
return { endpoint, schema: parseSchema(json?.data ?? json), durationMs: Date.now() - t0 };
|
|
56
75
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const msg = json.errors[0]?.message ?? "introspection disabled";
|
|
60
|
-
return { endpoint, schema: null, error: msg, durationMs: Date.now() - t0 };
|
|
76
|
+
catch (e) {
|
|
77
|
+
return { endpoint, schema: null, error: e.message, durationMs: Date.now() - t0 };
|
|
61
78
|
}
|
|
62
|
-
return { endpoint, schema: parseSchema(json?.data ?? json), durationMs: Date.now() - t0 };
|
|
63
|
-
} catch (e) {
|
|
64
|
-
return { endpoint, schema: null, error: e.message, durationMs: Date.now() - t0 };
|
|
65
|
-
}
|
|
66
79
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
/** Guess GraphQL endpoint URLs from a known app URL. */
|
|
81
|
+
export function guessGraphQLEndpoints(appUrl) {
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(appUrl);
|
|
84
|
+
const base = `${u.protocol}//${u.host}`;
|
|
85
|
+
return [
|
|
86
|
+
`${base}/graphql`,
|
|
87
|
+
`${base}/api/graphql`,
|
|
88
|
+
`${base}/v1/graphql`,
|
|
89
|
+
`${base}/gql`,
|
|
90
|
+
`${base}/query`,
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
|
-
export {
|
|
83
|
-
guessGraphQLEndpoints,
|
|
84
|
-
introspectGraphQL
|
|
85
|
-
};
|
|
86
|
-
//# sourceMappingURL=graphql-detector.js.map
|