@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/frontier.js
CHANGED
|
@@ -1,108 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configurable crawl traversal strategy — matches crawl4AI's BFS/DFS/
|
|
3
|
-
* Best-First deep-crawl strategies (ZeTa previously had a single hardcoded
|
|
4
|
-
* FIFO queue, i.e. always BFS).
|
|
5
|
-
*
|
|
6
|
-
* Array-like on purpose (`length`, `shift()`, `push()`, `filter()`) so it
|
|
7
|
-
* drops into crawler.ts's existing queue call sites with a rename, not a
|
|
8
|
-
* rewrite — BFS's `shift()` behavior is byte-for-byte the same as the plain
|
|
9
|
-
* array it replaces, so BFS (the default) has zero behavior change.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Best-First scoring heuristic: shallower paths score higher (breadth-like
|
|
13
|
-
* bias toward top-level sections before deep sub-pages), boosted further by
|
|
14
|
-
* any user-supplied keyword appearing in the URL. Simple and transparent by
|
|
15
|
-
* design — crawl4AI's own default scorer is a similarly simple keyword-
|
|
16
|
-
* relevance heuristic, not a learned model.
|
|
17
|
-
*/
|
|
18
1
|
function scoreUrl(url, keywords) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return score;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return 0;
|
|
2
|
+
try {
|
|
3
|
+
const u = new URL(url);
|
|
4
|
+
const segments = u.pathname.split("/").filter(Boolean);
|
|
5
|
+
let score = -segments.length;
|
|
6
|
+
if (keywords.length > 0) {
|
|
7
|
+
const lower = url.toLowerCase();
|
|
8
|
+
for (const kw of keywords) {
|
|
9
|
+
if (kw && lower.includes(kw)) score += 10;
|
|
10
|
+
}
|
|
34
11
|
}
|
|
12
|
+
return score;
|
|
13
|
+
} catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
35
16
|
}
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.items.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return undefined;
|
|
77
|
-
switch (this.strategy) {
|
|
78
|
-
case 'DFS':
|
|
79
|
-
return this.items.pop().url;
|
|
80
|
-
case 'BEST_FIRST': {
|
|
81
|
-
let bestIdx = 0;
|
|
82
|
-
for (let i = 1; i < this.items.length; i++) {
|
|
83
|
-
if (this.items[i].score > this.items[bestIdx].score)
|
|
84
|
-
bestIdx = i;
|
|
85
|
-
}
|
|
86
|
-
return this.items.splice(bestIdx, 1)[0].url;
|
|
87
|
-
}
|
|
88
|
-
case 'BFS':
|
|
89
|
-
default:
|
|
90
|
-
return this.items.shift().url;
|
|
17
|
+
class CrawlFrontier {
|
|
18
|
+
items = [];
|
|
19
|
+
strategy;
|
|
20
|
+
keywords;
|
|
21
|
+
urlFilter;
|
|
22
|
+
constructor(strategy, seedUrls, keywords = [], urlFilter) {
|
|
23
|
+
this.strategy = strategy;
|
|
24
|
+
this.keywords = keywords.map((k) => k.toLowerCase()).filter(Boolean);
|
|
25
|
+
this.urlFilter = urlFilter ? (() => {
|
|
26
|
+
try {
|
|
27
|
+
return new RegExp(urlFilter, "i");
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
})() : null;
|
|
32
|
+
for (const url of seedUrls) this.push(url);
|
|
33
|
+
}
|
|
34
|
+
get length() {
|
|
35
|
+
return this.items.length;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* P3b: sparse-parent boost — if the parent page had fewer than 5 interactive
|
|
39
|
+
* elements (thin/canvas page), its child URLs score +1 in BEST_FIRST mode so
|
|
40
|
+
* the frontier revisits sparse branches before deep-linked content.
|
|
41
|
+
*/
|
|
42
|
+
push(url, pushOpts) {
|
|
43
|
+
if (this.urlFilter && !this.urlFilter.test(url)) return;
|
|
44
|
+
const sparseBoost = this.strategy === "BEST_FIRST" && pushOpts?.parentElements !== void 0 && pushOpts.parentElements < 5 ? 1 : 0;
|
|
45
|
+
this.items.push({ url, score: this.strategy === "BEST_FIRST" ? scoreUrl(url, this.keywords) + sparseBoost : 0 });
|
|
46
|
+
}
|
|
47
|
+
/** Dequeues the next URL per this frontier's strategy. undefined if empty. */
|
|
48
|
+
shift() {
|
|
49
|
+
if (this.items.length === 0) return void 0;
|
|
50
|
+
switch (this.strategy) {
|
|
51
|
+
case "DFS":
|
|
52
|
+
return this.items.pop().url;
|
|
53
|
+
case "BEST_FIRST": {
|
|
54
|
+
let bestIdx = 0;
|
|
55
|
+
for (let i = 1; i < this.items.length; i++) {
|
|
56
|
+
if (this.items[i].score > this.items[bestIdx].score) bestIdx = i;
|
|
91
57
|
}
|
|
58
|
+
return this.items.splice(bestIdx, 1)[0].url;
|
|
59
|
+
}
|
|
60
|
+
case "BFS":
|
|
61
|
+
default:
|
|
62
|
+
return this.items.shift().url;
|
|
92
63
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
64
|
+
}
|
|
65
|
+
filter(predicate) {
|
|
66
|
+
return this.items.filter((e) => predicate(e.url)).map((e) => e.url);
|
|
67
|
+
}
|
|
68
|
+
/** Remove queued URLs not matching the regex pattern. Returns count removed. */
|
|
69
|
+
filterByPattern(pattern) {
|
|
70
|
+
try {
|
|
71
|
+
const re = new RegExp(pattern, "i");
|
|
72
|
+
const before = this.items.length;
|
|
73
|
+
this.items = this.items.filter((e) => re.test(e.url));
|
|
74
|
+
return before - this.items.length;
|
|
75
|
+
} catch {
|
|
76
|
+
return 0;
|
|
107
77
|
}
|
|
78
|
+
}
|
|
108
79
|
}
|
|
80
|
+
export {
|
|
81
|
+
CrawlFrontier
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=frontier.js.map
|
package/dist/gap-pass.js
CHANGED
|
@@ -1,44 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
1
|
+
import { logger as rootLogger } from "./logger.js";
|
|
2
|
+
const logger = rootLogger.child({ module: "gap-pass" });
|
|
3
|
+
async function runGapAnalysis(screens, appUrl, llmCfg) {
|
|
4
|
+
if (!llmCfg?.apiKey || screens.length < 2) return [];
|
|
5
|
+
const origin = (() => {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(appUrl).origin;
|
|
8
|
+
} catch {
|
|
9
|
+
return "";
|
|
23
10
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
11
|
+
})();
|
|
12
|
+
if (!origin) return [];
|
|
13
|
+
const screenList = screens.slice(0, 80).map((s) => {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(s.url).pathname + (s.name ? ` (${s.name})` : "");
|
|
16
|
+
} catch {
|
|
17
|
+
return s.url;
|
|
18
|
+
}
|
|
19
|
+
}).join("\n");
|
|
20
|
+
const prompt = `You are analyzing the screen map of a web application to find structural coverage gaps.
|
|
42
21
|
|
|
43
22
|
Discovered screens (${screens.length} total):
|
|
44
23
|
${screenList}
|
|
@@ -49,55 +28,55 @@ Based on these screens and common patterns for this type of web application, ide
|
|
|
49
28
|
|
|
50
29
|
Respond with ONLY valid JSON, no markdown:
|
|
51
30
|
{"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"}]}`;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
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');
|
|
31
|
+
try {
|
|
32
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
33
|
+
const client = new Anthropic({ apiKey: llmCfg.apiKey });
|
|
34
|
+
const response = await client.messages.create({
|
|
35
|
+
model: llmCfg.model ?? "claude-haiku-4-5-20251001",
|
|
36
|
+
max_tokens: 1024,
|
|
37
|
+
temperature: 0,
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
system: [{ type: "text", text: "You are a web application coverage analyst. Respond with valid JSON only.", cache_control: { type: "ephemeral" } }],
|
|
40
|
+
messages: [{ role: "user", content: prompt }]
|
|
41
|
+
});
|
|
42
|
+
const text = response.content[0]?.text ?? "";
|
|
43
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
44
|
+
if (!jsonMatch) return [];
|
|
45
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
46
|
+
const rawGaps = parsed.gaps ?? [];
|
|
47
|
+
if (!Array.isArray(rawGaps) || rawGaps.length === 0) return [];
|
|
48
|
+
logger.info({ gaps: rawGaps.length }, "[P6] Gap analysis: LLM returned candidates");
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const gap of rawGaps.slice(0, 15)) {
|
|
51
|
+
let confirmedUrl = null;
|
|
52
|
+
let headStatus = null;
|
|
53
|
+
for (const pattern of (gap.urlPatterns ?? []).slice(0, 4)) {
|
|
54
|
+
const fullUrl = pattern.startsWith("http") ? pattern : `${origin}${pattern.startsWith("/") ? "" : "/"}${pattern}`;
|
|
55
|
+
try {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeout = setTimeout(() => controller.abort(), 6e3);
|
|
58
|
+
const res = await fetch(fullUrl, { method: "HEAD", signal: controller.signal, redirect: "follow" }).catch(() => null);
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
headStatus = res?.status ?? null;
|
|
61
|
+
if (res?.ok) {
|
|
62
|
+
confirmedUrl = fullUrl;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
94
66
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
logger.warn({ err: String(err) }, '[P6] Gap analysis failed — non-fatal');
|
|
101
|
-
return [];
|
|
67
|
+
}
|
|
68
|
+
results.push({ category: gap.category, urlPatterns: gap.urlPatterns, confidence: gap.confidence, rationale: gap.rationale, confirmedUrl, headStatus });
|
|
69
|
+
logger.info({ category: gap.category, confirmedUrl, headStatus }, "[P6] Gap candidate checked");
|
|
102
70
|
}
|
|
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
|
+
}
|
|
103
78
|
}
|
|
79
|
+
export {
|
|
80
|
+
runGapAnalysis
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=gap-pass.js.map
|
package/dist/graphql-detector.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
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
|
-
*/
|
|
8
1
|
const INTROSPECTION_QUERY = `
|
|
9
2
|
query IntrospectionQuery {
|
|
10
3
|
__schema {
|
|
@@ -22,75 +15,72 @@ const INTROSPECTION_QUERY = `
|
|
|
22
15
|
}
|
|
23
16
|
`;
|
|
24
17
|
function flattenType(t) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return flattenType(t.ofType);
|
|
29
|
-
return t.name ?? 'unknown';
|
|
18
|
+
if (!t) return "unknown";
|
|
19
|
+
if (t.kind === "NON_NULL" || t.kind === "LIST") return flattenType(t.ofType);
|
|
20
|
+
return t.name ?? "unknown";
|
|
30
21
|
}
|
|
31
22
|
function parseSchema(raw) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
types,
|
|
47
|
-
};
|
|
23
|
+
const s = raw?.__schema ?? raw?.data?.__schema ?? raw;
|
|
24
|
+
const types = (s?.types ?? []).filter((t) => !t.name?.startsWith("__")).map((t) => ({
|
|
25
|
+
name: t.name,
|
|
26
|
+
kind: t.kind,
|
|
27
|
+
fields: (t.fields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
|
|
28
|
+
inputFields: (t.inputFields ?? []).map((f) => ({ name: f.name, type: flattenType(f.type) })),
|
|
29
|
+
enumValues: (t.enumValues ?? []).map((e) => e.name)
|
|
30
|
+
}));
|
|
31
|
+
return {
|
|
32
|
+
queryType: s?.queryType?.name,
|
|
33
|
+
mutationType: s?.mutationType?.name,
|
|
34
|
+
subscriptionType: s?.subscriptionType?.name,
|
|
35
|
+
types
|
|
36
|
+
};
|
|
48
37
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 };
|
|
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
|
|
46
|
+
};
|
|
47
|
+
if (cookies) reqHeaders["Cookie"] = cookies;
|
|
48
|
+
const res = await fetch(endpoint, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: reqHeaders,
|
|
51
|
+
body: JSON.stringify({ query: INTROSPECTION_QUERY }),
|
|
52
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
return { endpoint, schema: null, error: `HTTP ${res.status}`, durationMs: Date.now() - t0 };
|
|
75
56
|
}
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
const json = await res.json();
|
|
58
|
+
if (json?.errors?.length) {
|
|
59
|
+
const msg = json.errors[0]?.message ?? "introspection disabled";
|
|
60
|
+
return { endpoint, schema: null, error: msg, durationMs: Date.now() - t0 };
|
|
78
61
|
}
|
|
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
|
+
}
|
|
79
66
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
67
|
+
function guessGraphQLEndpoints(appUrl) {
|
|
68
|
+
try {
|
|
69
|
+
const u = new URL(appUrl);
|
|
70
|
+
const base = `${u.protocol}//${u.host}`;
|
|
71
|
+
return [
|
|
72
|
+
`${base}/graphql`,
|
|
73
|
+
`${base}/api/graphql`,
|
|
74
|
+
`${base}/v1/graphql`,
|
|
75
|
+
`${base}/gql`,
|
|
76
|
+
`${base}/query`
|
|
77
|
+
];
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
96
81
|
}
|
|
82
|
+
export {
|
|
83
|
+
guessGraphQLEndpoints,
|
|
84
|
+
introspectGraphQL
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=graphql-detector.js.map
|