@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/network-observer.js
CHANGED
|
@@ -1,170 +1,149 @@
|
|
|
1
|
-
import { redactJson } from
|
|
1
|
+
import { redactJson } from "./policies/crawl-safety.js";
|
|
2
2
|
const API_PATTERN = /\/(api|graphql|gql|rest|v\d+)\//i;
|
|
3
3
|
const ASSET_EXT = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map|json\.map)(\?|$)/i;
|
|
4
4
|
function normalizeApiUrl(url) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.replace(/\/[a-f0-9-]{8,}(?=\/|$)/gi, '/{id}')
|
|
8
|
-
.replace(/\/c[a-z0-9]{20,}(?=\/|$)/g, '/{id}')
|
|
9
|
-
.replace(/\/\d+(?=\/|$)/g, '/{id}');
|
|
5
|
+
const u = new URL(url);
|
|
6
|
+
return u.pathname.replace(/\/[a-f0-9-]{8,}(?=\/|$)/gi, "/{id}").replace(/\/c[a-z0-9]{20,}(?=\/|$)/g, "/{id}").replace(/\/\d+(?=\/|$)/g, "/{id}");
|
|
10
7
|
}
|
|
11
8
|
function inferShape(value) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return typeof value;
|
|
9
|
+
if (Array.isArray(value)) return value.length > 0 ? [inferShape(value[0])] : [];
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [key, item] of Object.entries(value).slice(0, 40)) out[key] = inferShape(item);
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
return typeof value;
|
|
21
16
|
}
|
|
22
|
-
/** P4: Extract page-URL candidates from a JSON response body string. */
|
|
23
17
|
function extractPageUrls(jsonStr) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return [...found];
|
|
18
|
+
const PAGE_URL = /"(?:url|href|path|link|route|to|src|redirect)"\s*:\s*"(\/[\w\-\/\.~@%]+)"/g;
|
|
19
|
+
const ASSET = /\.(png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|css|js|map|json)$/i;
|
|
20
|
+
const API_PATH = /^\/(api|v\d+|graphql|gql|rest|rpc|static|_next|assets)\//i;
|
|
21
|
+
const found = /* @__PURE__ */ new Set();
|
|
22
|
+
let m;
|
|
23
|
+
PAGE_URL.lastIndex = 0;
|
|
24
|
+
while ((m = PAGE_URL.exec(jsonStr)) !== null) {
|
|
25
|
+
const u = m[1];
|
|
26
|
+
if (u && u.length > 1 && u.length < 120 && !ASSET.test(u) && !API_PATH.test(u)) found.add(u);
|
|
27
|
+
}
|
|
28
|
+
return [...found];
|
|
36
29
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
30
|
+
function attachNetworkObserver(context, sink, onPageUrlDiscovered) {
|
|
31
|
+
const seen = /* @__PURE__ */ new Map();
|
|
32
|
+
const requestTimes = /* @__PURE__ */ new Map();
|
|
33
|
+
context.on("request", (request) => {
|
|
34
|
+
try {
|
|
35
|
+
const url = request.url();
|
|
36
|
+
const rtype = request.resourceType();
|
|
37
|
+
if (rtype !== "xhr" && rtype !== "fetch" || !API_PATTERN.test(url) || ASSET_EXT.test(url)) return;
|
|
38
|
+
const method = request.method();
|
|
39
|
+
const normalized = normalizeApiUrl(url);
|
|
40
|
+
const key = `${method}:${normalized}`;
|
|
41
|
+
requestTimes.set(url, Date.now());
|
|
42
|
+
if (!seen.has(key)) {
|
|
43
|
+
const row = { method, urlPattern: normalized, requestKind: rtype };
|
|
44
|
+
seen.set(key, row);
|
|
45
|
+
sink.push(row);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
context.on("response", async (response) => {
|
|
51
|
+
try {
|
|
52
|
+
const request = response.request();
|
|
53
|
+
const url = request.url();
|
|
54
|
+
const rtype = request.resourceType();
|
|
55
|
+
if (rtype !== "xhr" && rtype !== "fetch" || !API_PATTERN.test(url) || ASSET_EXT.test(url)) return;
|
|
56
|
+
const normalized = normalizeApiUrl(url);
|
|
57
|
+
const key = `${request.method()}:${normalized}`;
|
|
58
|
+
const durationMs = Date.now() - (requestTimes.get(url) ?? Date.now());
|
|
59
|
+
let row = seen.get(key);
|
|
60
|
+
if (!row) {
|
|
61
|
+
row = { method: request.method(), urlPattern: normalized, requestKind: rtype };
|
|
62
|
+
seen.set(key, row);
|
|
63
|
+
sink.push(row);
|
|
64
|
+
}
|
|
65
|
+
row.statusCode = response.status();
|
|
66
|
+
row.durationMs = durationMs;
|
|
67
|
+
const contentType = response.headers()["content-type"] ?? "";
|
|
68
|
+
if (contentType.includes("application/json")) {
|
|
69
|
+
const body = await response.json().catch(() => void 0);
|
|
70
|
+
if (body !== void 0) {
|
|
71
|
+
row.responseShape = redactJson(inferShape(body));
|
|
72
|
+
if (onPageUrlDiscovered) {
|
|
73
|
+
for (const u of extractPageUrls(JSON.stringify(body))) onPageUrlDiscovered(u);
|
|
74
|
+
}
|
|
58
75
|
}
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
}
|
|
77
|
+
if (response.status() >= 400) {
|
|
61
78
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if ((rtype !== 'xhr' && rtype !== 'fetch') || !API_PATTERN.test(url) || ASSET_EXT.test(url))
|
|
66
|
-
return;
|
|
67
|
-
const normalized = normalizeApiUrl(url);
|
|
68
|
-
const key = `${request.method()}:${normalized}`;
|
|
69
|
-
const durationMs = Date.now() - (requestTimes.get(url) ?? Date.now());
|
|
70
|
-
let row = seen.get(key);
|
|
71
|
-
if (!row) {
|
|
72
|
-
row = { method: request.method(), urlPattern: normalized, requestKind: rtype };
|
|
73
|
-
seen.set(key, row);
|
|
74
|
-
sink.push(row);
|
|
75
|
-
}
|
|
76
|
-
row.statusCode = response.status();
|
|
77
|
-
row.durationMs = durationMs;
|
|
78
|
-
const contentType = response.headers()['content-type'] ?? '';
|
|
79
|
-
if (contentType.includes('application/json')) {
|
|
80
|
-
const body = await response.json().catch(() => undefined);
|
|
81
|
-
if (body !== undefined) {
|
|
82
|
-
row.responseShape = redactJson(inferShape(body));
|
|
83
|
-
// P4: scan JSON body for page-URL candidates (non-API paths)
|
|
84
|
-
if (onPageUrlDiscovered) {
|
|
85
|
-
for (const u of extractPageUrls(JSON.stringify(body)))
|
|
86
|
-
onPageUrlDiscovered(u);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (response.status() >= 400) {
|
|
91
|
-
try {
|
|
92
|
-
const errText = await response.text();
|
|
93
|
-
row.errorBody = errText.slice(0, 300);
|
|
94
|
-
}
|
|
95
|
-
catch { /* non-fatal */ }
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// non-fatal
|
|
79
|
+
const errText = await response.text();
|
|
80
|
+
row.errorBody = errText.slice(0, 300);
|
|
81
|
+
} catch {
|
|
100
82
|
}
|
|
101
|
-
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
});
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
88
|
+
function attachHarCapture(cdpSession, pageTitle = "Page") {
|
|
89
|
+
const entries = [];
|
|
90
|
+
const pending = /* @__PURE__ */ new Map();
|
|
91
|
+
const pageId = `page_${String(Date.now())}`;
|
|
92
|
+
const pageStarted = (/* @__PURE__ */ new Date()).toISOString();
|
|
93
|
+
const onRequest = (evt) => {
|
|
94
|
+
const { requestId, request } = evt ?? {};
|
|
95
|
+
if (!requestId || !request) return;
|
|
96
|
+
const headers = {};
|
|
97
|
+
for (const h of request.headers ?? []) if (h.name) headers[String(h.name).toLowerCase()] = String(h.value ?? "");
|
|
98
|
+
pending.set(requestId, { method: request.method ?? "GET", url: request.url ?? "", headers, startMs: Date.now() });
|
|
99
|
+
};
|
|
100
|
+
const onResponse = (evt) => {
|
|
101
|
+
const { requestId, response } = evt ?? {};
|
|
102
|
+
if (!requestId || !response) return;
|
|
103
|
+
const req = pending.get(requestId);
|
|
104
|
+
if (!req) return;
|
|
105
|
+
pending.delete(requestId);
|
|
106
|
+
const durationMs = Date.now() - req.startMs;
|
|
107
|
+
const responseHeaders = {};
|
|
108
|
+
for (const h of response.headers ?? []) if (h.name) responseHeaders[String(h.name).toLowerCase()] = String(h.value ?? "");
|
|
109
|
+
entries.push({
|
|
110
|
+
startedAt: new Date(req.startMs).toISOString(),
|
|
111
|
+
method: req.method,
|
|
112
|
+
url: req.url,
|
|
113
|
+
status: response.status ?? 0,
|
|
114
|
+
statusText: response.statusText ?? "",
|
|
115
|
+
requestHeaders: req.headers,
|
|
116
|
+
responseHeaders,
|
|
117
|
+
contentType: responseHeaders["content-type"] ?? "",
|
|
118
|
+
bodySize: response.encodedDataLength ?? -1,
|
|
119
|
+
durationMs,
|
|
120
|
+
timings: { send: 0, wait: Math.round(durationMs * 0.9), receive: Math.round(durationMs * 0.1) }
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
if (cdpSession?.on) {
|
|
124
|
+
cdpSession.on("Network.requestWillBeSent", onRequest);
|
|
125
|
+
cdpSession.on("Network.responseReceived", onResponse);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
getHar() {
|
|
129
|
+
return {
|
|
130
|
+
version: "1.2",
|
|
131
|
+
creator: { name: "ZeTa-Crawler", version: "1.0" },
|
|
132
|
+
pages: [{ id: pageId, title: pageTitle, startedAt: pageStarted }],
|
|
133
|
+
entries: [...entries]
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
detach() {
|
|
137
|
+
if (cdpSession?.off) {
|
|
138
|
+
cdpSession.off("Network.requestWillBeSent", onRequest);
|
|
139
|
+
cdpSession.off("Network.responseReceived", onResponse);
|
|
140
|
+
}
|
|
141
|
+
pending.clear();
|
|
152
142
|
}
|
|
153
|
-
|
|
154
|
-
getHar() {
|
|
155
|
-
return {
|
|
156
|
-
version: '1.2',
|
|
157
|
-
creator: { name: 'ZeTa-Crawler', version: '1.0' },
|
|
158
|
-
pages: [{ id: pageId, title: pageTitle, startedAt: pageStarted }],
|
|
159
|
-
entries: [...entries],
|
|
160
|
-
};
|
|
161
|
-
},
|
|
162
|
-
detach() {
|
|
163
|
-
if (cdpSession?.off) {
|
|
164
|
-
cdpSession.off('Network.requestWillBeSent', onRequest);
|
|
165
|
-
cdpSession.off('Network.responseReceived', onResponse);
|
|
166
|
-
}
|
|
167
|
-
pending.clear();
|
|
168
|
-
},
|
|
169
|
-
};
|
|
143
|
+
};
|
|
170
144
|
}
|
|
145
|
+
export {
|
|
146
|
+
attachHarCapture,
|
|
147
|
+
attachNetworkObserver
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=network-observer.js.map
|
package/dist/network-throttle.js
CHANGED
|
@@ -1,45 +1,34 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Network throttle simulation via CDP.
|
|
3
|
-
*
|
|
4
|
-
* Simulates different network conditions during crawl to test how the app
|
|
5
|
-
* behaves under slow connections. Useful for detecting loading states, error
|
|
6
|
-
* handling, and performance degradation on poor networks.
|
|
7
|
-
*
|
|
8
|
-
* Conditions mirror Chrome DevTools presets:
|
|
9
|
-
* - slow3g: ~500kbps download, high latency
|
|
10
|
-
* - fast3g: ~1.6Mbps download, medium latency
|
|
11
|
-
* - 4g: ~9Mbps download, low latency
|
|
12
|
-
* - offline: no connectivity
|
|
13
|
-
* - none: no throttle (default)
|
|
14
|
-
*/
|
|
15
1
|
const PRESETS = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
none: { offline: false, downloadThroughput: -1, uploadThroughput: -1, latency: 0 },
|
|
3
|
+
slow3g: { offline: false, downloadThroughput: 500 * 1024 / 8, uploadThroughput: 500 * 1024 / 8, latency: 2e3 },
|
|
4
|
+
fast3g: { offline: false, downloadThroughput: 1.6 * 1024 * 1024 / 8, uploadThroughput: 750 * 1024 / 8, latency: 562.5 },
|
|
5
|
+
"4g": { offline: false, downloadThroughput: 9 * 1024 * 1024 / 8, uploadThroughput: 2.67 * 1024 * 1024 / 8, latency: 85 },
|
|
6
|
+
offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 }
|
|
21
7
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
catch { /* non-fatal — CDP may not be available */ }
|
|
8
|
+
async function applyNetworkThrottle(cdpSession, preset) {
|
|
9
|
+
if (!cdpSession?.send) return;
|
|
10
|
+
const condition = PRESETS[preset];
|
|
11
|
+
try {
|
|
12
|
+
await cdpSession.send("Network.enable");
|
|
13
|
+
await cdpSession.send("Network.emulateNetworkConditions", condition);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
31
16
|
}
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
async function clearNetworkThrottle(cdpSession) {
|
|
18
|
+
await applyNetworkThrottle(cdpSession, "none");
|
|
34
19
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await clearNetworkThrottle(cdpSession).catch(() => { });
|
|
44
|
-
}
|
|
20
|
+
async function withNetworkThrottle(cdpSession, preset, fn, restoreAfter = true) {
|
|
21
|
+
await applyNetworkThrottle(cdpSession, preset);
|
|
22
|
+
try {
|
|
23
|
+
return await fn();
|
|
24
|
+
} finally {
|
|
25
|
+
if (restoreAfter) await clearNetworkThrottle(cdpSession).catch(() => {
|
|
26
|
+
});
|
|
27
|
+
}
|
|
45
28
|
}
|
|
29
|
+
export {
|
|
30
|
+
applyNetworkThrottle,
|
|
31
|
+
clearNetworkThrottle,
|
|
32
|
+
withNetworkThrottle
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=network-throttle.js.map
|
|
@@ -1,104 +1,89 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAPI 3.0 spec generator from observed network traffic.
|
|
3
|
-
*
|
|
4
|
-
* Takes the ApiEndpointObservationInput[] collected by the network observer
|
|
5
|
-
* during crawl and generates a minimal but valid OpenAPI 3.0 spec.
|
|
6
|
-
*
|
|
7
|
-
* The spec captures: endpoints, methods, observed response shapes, status codes.
|
|
8
|
-
* It does NOT capture request bodies (we'd need to intercept requests too) — that's
|
|
9
|
-
* marked as a TODO. But it's enough for generating smoke-test suites.
|
|
10
|
-
*/
|
|
11
1
|
function extractPathParams(urlPattern) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
const params = [];
|
|
3
|
+
const matches = urlPattern.matchAll(/\{(\w+)\}/g);
|
|
4
|
+
for (const m of matches) {
|
|
5
|
+
params.push({ name: m[1], in: "path", required: true, schema: { type: "string" } });
|
|
6
|
+
}
|
|
7
|
+
return params;
|
|
18
8
|
}
|
|
19
9
|
function methodSummary(method, path) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
const resource = path.split("/").filter((s) => !s.startsWith("{") && s.length > 0).pop() ?? "resource";
|
|
11
|
+
const summaries = {
|
|
12
|
+
GET: `Get ${resource}`,
|
|
13
|
+
POST: `Create ${resource}`,
|
|
14
|
+
PUT: `Update ${resource}`,
|
|
15
|
+
PATCH: `Partially update ${resource}`,
|
|
16
|
+
DELETE: `Delete ${resource}`
|
|
17
|
+
};
|
|
18
|
+
return summaries[method.toUpperCase()] ?? `${method} ${resource}`;
|
|
29
19
|
}
|
|
30
20
|
function inferTag(path) {
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
const parts = path.split("/").filter((s) => s.length > 0 && !s.startsWith("{"));
|
|
22
|
+
return parts[0] ?? "default";
|
|
33
23
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
function generateOpenApiSpec(observations, opts = {}) {
|
|
25
|
+
const { title = "ZeTa Discovered API", baseUrl = "/", version = "1.0.0" } = opts;
|
|
26
|
+
const spec = {
|
|
27
|
+
openapi: "3.0.3",
|
|
28
|
+
info: { title, version, description: "Auto-generated by ZeTa crawler from observed network traffic." },
|
|
29
|
+
servers: [{ url: baseUrl }],
|
|
30
|
+
paths: {}
|
|
31
|
+
};
|
|
32
|
+
for (const obs of observations) {
|
|
33
|
+
const path = obs.urlPattern;
|
|
34
|
+
const method = obs.method.toLowerCase();
|
|
35
|
+
if (!spec.paths[path]) spec.paths[path] = {};
|
|
36
|
+
const params = extractPathParams(path);
|
|
37
|
+
const statusCode = obs.statusCode ?? 200;
|
|
38
|
+
const responses = {
|
|
39
|
+
[String(statusCode)]: {
|
|
40
|
+
description: statusCode < 400 ? "Successful response" : "Error response",
|
|
41
|
+
content: obs.responseShape ? { "application/json": { schema: shapeToJsonSchema(obs.responseShape) } } : void 0
|
|
42
|
+
}
|
|
41
43
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
description: statusCode < 400 ? 'Successful response' : 'Error response',
|
|
52
|
-
content: obs.responseShape
|
|
53
|
-
? { 'application/json': { schema: shapeToJsonSchema(obs.responseShape) } }
|
|
54
|
-
: undefined,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
const op = {
|
|
58
|
-
summary: methodSummary(obs.method, path),
|
|
59
|
-
parameters: params.length > 0 ? params : undefined,
|
|
60
|
-
responses,
|
|
61
|
-
tags: [inferTag(path)],
|
|
62
|
-
};
|
|
63
|
-
spec.paths[path][method] = op;
|
|
64
|
-
}
|
|
65
|
-
return spec;
|
|
44
|
+
const op = {
|
|
45
|
+
summary: methodSummary(obs.method, path),
|
|
46
|
+
parameters: params.length > 0 ? params : void 0,
|
|
47
|
+
responses,
|
|
48
|
+
tags: [inferTag(path)]
|
|
49
|
+
};
|
|
50
|
+
spec.paths[path][method] = op;
|
|
51
|
+
}
|
|
52
|
+
return spec;
|
|
66
53
|
}
|
|
67
54
|
function shapeToJsonSchema(shape) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return { type: String(shape) };
|
|
55
|
+
if (Array.isArray(shape)) return { type: "array", items: shape.length > 0 ? shapeToJsonSchema(shape[0]) : {} };
|
|
56
|
+
if (shape && typeof shape === "object") {
|
|
57
|
+
const props = {};
|
|
58
|
+
for (const [k, v] of Object.entries(shape)) props[k] = shapeToJsonSchema(v);
|
|
59
|
+
return { type: "object", properties: props };
|
|
60
|
+
}
|
|
61
|
+
return { type: String(shape) };
|
|
77
62
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const vStr = toYaml(v, indent + 1);
|
|
96
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
97
|
-
? `\n${pad}${k}:${vStr}`
|
|
98
|
-
: `\n${pad}${k}: ${vStr.trimStart()}`;
|
|
99
|
-
}).join('');
|
|
100
|
-
}
|
|
101
|
-
return String(val);
|
|
63
|
+
function specToYaml(spec) {
|
|
64
|
+
function toYaml(val, indent = 0) {
|
|
65
|
+
const pad = " ".repeat(indent);
|
|
66
|
+
if (val === null || val === void 0) return "null";
|
|
67
|
+
if (typeof val === "string") return val.includes("\n") || val.includes(":") ? `"${val.replace(/"/g, '\\"')}"` : val;
|
|
68
|
+
if (typeof val === "number" || typeof val === "boolean") return String(val);
|
|
69
|
+
if (Array.isArray(val)) return val.map((v) => `
|
|
70
|
+
${pad}- ${toYaml(v, indent + 1).trimStart()}`).join("") || "[]";
|
|
71
|
+
if (typeof val === "object") {
|
|
72
|
+
const entries = Object.entries(val);
|
|
73
|
+
if (entries.length === 0) return "{}";
|
|
74
|
+
return entries.map(([k, v]) => {
|
|
75
|
+
const vStr = toYaml(v, indent + 1);
|
|
76
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? `
|
|
77
|
+
${pad}${k}:${vStr}` : `
|
|
78
|
+
${pad}${k}: ${vStr.trimStart()}`;
|
|
79
|
+
}).join("");
|
|
102
80
|
}
|
|
103
|
-
return
|
|
81
|
+
return String(val);
|
|
82
|
+
}
|
|
83
|
+
return `openapi: "${spec.openapi}"${toYaml(spec, 0)}`;
|
|
104
84
|
}
|
|
85
|
+
export {
|
|
86
|
+
generateOpenApiSpec,
|
|
87
|
+
specToYaml
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=openapi-generator.js.map
|