@detiq/crawler 1.1.8 → 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.
Files changed (82) hide show
  1. package/dist/accessibility-auditor.js +74 -54
  2. package/dist/ai-login.js +249 -227
  3. package/dist/auth-token-rotator.js +67 -57
  4. package/dist/auth.js +336 -299
  5. package/dist/browser-extension.js +142 -104
  6. package/dist/captcha-solver.js +125 -98
  7. package/dist/color-contrast.js +89 -77
  8. package/dist/content-chunker.js +126 -126
  9. package/dist/content-dedup.js +71 -54
  10. package/dist/cookie-consent.js +83 -68
  11. package/dist/cookie-jar.js +60 -53
  12. package/dist/crawler.js +5345 -4486
  13. package/dist/custom-js-injector.js +61 -45
  14. package/dist/dark-mode-capture.js +81 -67
  15. package/dist/dead-link-detector.js +55 -47
  16. package/dist/element-hash.js +12 -8
  17. package/dist/email-otp.js +195 -154
  18. package/dist/engine-router.js +98 -87
  19. package/dist/export/bulk-export.js +55 -70
  20. package/dist/export/vector-export.js +105 -77
  21. package/dist/extractors/advanced.js +266 -249
  22. package/dist/extractors/artifact-envelope.js +58 -59
  23. package/dist/failure-analysis-agent.js +33 -37
  24. package/dist/feature-flag-detector.js +66 -51
  25. package/dist/form-detector.js +75 -55
  26. package/dist/frontier.js +101 -76
  27. package/dist/gap-pass.js +89 -68
  28. package/dist/graphql-detector.js +72 -62
  29. package/dist/http-engine.js +330 -282
  30. package/dist/incremental-diff.js +87 -54
  31. package/dist/index.js +28 -67
  32. package/dist/input-fuzzer.js +97 -93
  33. package/dist/interactive-probe.js +827 -715
  34. package/dist/job-queue.js +2169 -2151
  35. package/dist/js-coverage.js +56 -52
  36. package/dist/js-router-detector.js +117 -86
  37. package/dist/language-detector.js +63 -51
  38. package/dist/link-classifier.js +50 -32
  39. package/dist/live-view.js +108 -91
  40. package/dist/locator-extractor.js +187 -151
  41. package/dist/logger.js +7 -9
  42. package/dist/markdown-extract.js +61 -48
  43. package/dist/native-crawler.js +946 -780
  44. package/dist/network-observer.js +156 -135
  45. package/dist/network-throttle.js +40 -29
  46. package/dist/openapi-generator.js +92 -77
  47. package/dist/pdf-extractor.js +90 -72
  48. package/dist/performance-metrics.js +102 -80
  49. package/dist/phash.js +144 -125
  50. package/dist/pii-scrubber.js +68 -65
  51. package/dist/playwright-recorder.js +58 -70
  52. package/dist/policies/crawl-safety.js +34 -31
  53. package/dist/profiles/domain-profile.js +125 -130
  54. package/dist/proxy.js +172 -146
  55. package/dist/rate-limiter.js +65 -58
  56. package/dist/redirect-tracker.js +64 -60
  57. package/dist/redis-frontier.js +136 -116
  58. package/dist/redis-queue.js +144 -157
  59. package/dist/response-cache.js +140 -127
  60. package/dist/responsive-detector.js +46 -42
  61. package/dist/retry-policy.js +50 -46
  62. package/dist/robots-txt.js +83 -69
  63. package/dist/screen-reader.js +70 -45
  64. package/dist/screenshot-masker.js +77 -59
  65. package/dist/seo-checker.js +107 -88
  66. package/dist/session-capture.js +212 -192
  67. package/dist/session-export.js +133 -107
  68. package/dist/sitemap-writer.js +41 -38
  69. package/dist/sitemap.js +202 -141
  70. package/dist/skyvern.js +148 -123
  71. package/dist/sqs-queue.js +42 -42
  72. package/dist/sso-token.js +90 -74
  73. package/dist/stealth/human-behavior.js +173 -156
  74. package/dist/telemetry.js +82 -68
  75. package/dist/test-executor.js +335 -309
  76. package/dist/tls-fingerprint.js +88 -77
  77. package/dist/url-pattern-completer.js +90 -71
  78. package/dist/visual-diff-verdict.js +38 -23
  79. package/dist/webhook-notifier.js +69 -71
  80. package/dist/websocket-detector.js +68 -56
  81. package/dist/worker.js +38 -29
  82. package/package.json +1 -1
@@ -1,149 +1,170 @@
1
- import { redactJson } from "./policies/crawl-safety.js";
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
- 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}");
5
+ const u = new URL(url);
6
+ return u.pathname
7
+ .replace(/\/[a-f0-9-]{8,}(?=\/|$)/gi, '/{id}')
8
+ .replace(/\/c[a-z0-9]{20,}(?=\/|$)/g, '/{id}')
9
+ .replace(/\/\d+(?=\/|$)/g, '/{id}');
7
10
  }
8
11
  function inferShape(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;
12
+ if (Array.isArray(value))
13
+ return value.length > 0 ? [inferShape(value[0])] : [];
14
+ if (value && typeof value === 'object') {
15
+ const out = {};
16
+ for (const [key, item] of Object.entries(value).slice(0, 40))
17
+ out[key] = inferShape(item);
18
+ return out;
19
+ }
20
+ return typeof value;
16
21
  }
22
+ /** P4: Extract page-URL candidates from a JSON response body string. */
17
23
  function extractPageUrls(jsonStr) {
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];
29
- }
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 {
24
+ const PAGE_URL = /"(?:url|href|path|link|route|to|src|redirect)"\s*:\s*"(\/[\w\-\/\.~@%]+)"/g;
25
+ const ASSET = /\.(png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|css|js|map|json)$/i;
26
+ const API_PATH = /^\/(api|v\d+|graphql|gql|rest|rpc|static|_next|assets)\//i;
27
+ const found = new Set();
28
+ let m;
29
+ PAGE_URL.lastIndex = 0;
30
+ while ((m = PAGE_URL.exec(jsonStr)) !== null) {
31
+ const u = m[1];
32
+ if (u && u.length > 1 && u.length < 120 && !ASSET.test(u) && !API_PATH.test(u))
33
+ found.add(u);
48
34
  }
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
- }
35
+ return [...found];
36
+ }
37
+ export function attachNetworkObserver(context, sink, onPageUrlDiscovered) {
38
+ const seen = new Map();
39
+ const requestTimes = new Map();
40
+ context.on('request', (request) => {
41
+ try {
42
+ const url = request.url();
43
+ const rtype = request.resourceType();
44
+ if ((rtype !== 'xhr' && rtype !== 'fetch') || !API_PATTERN.test(url) || ASSET_EXT.test(url))
45
+ return;
46
+ const method = request.method();
47
+ const normalized = normalizeApiUrl(url);
48
+ const key = `${method}:${normalized}`;
49
+ requestTimes.set(url, Date.now());
50
+ if (!seen.has(key)) {
51
+ const row = { method, urlPattern: normalized, requestKind: rtype };
52
+ seen.set(key, row);
53
+ sink.push(row);
54
+ }
55
+ }
56
+ catch {
57
+ // non-fatal
75
58
  }
76
- }
77
- if (response.status() >= 400) {
59
+ });
60
+ context.on('response', async (response) => {
78
61
  try {
79
- const errText = await response.text();
80
- row.errorBody = errText.slice(0, 300);
81
- } catch {
62
+ const request = response.request();
63
+ const url = request.url();
64
+ const rtype = request.resourceType();
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
82
100
  }
83
- }
84
- } catch {
85
- }
86
- });
87
- }
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
101
  });
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();
102
+ }
103
+ /**
104
+ * Full HAR capture via CDP — intercepts ALL network traffic, not just API endpoints.
105
+ * Returns a controller with getHar() and detach().
106
+ */
107
+ export function attachHarCapture(cdpSession, pageTitle = 'Page') {
108
+ const entries = [];
109
+ const pending = new Map();
110
+ const pageId = `page_${String(Date.now())}`;
111
+ const pageStarted = new Date().toISOString();
112
+ const onRequest = (evt) => {
113
+ const { requestId, request } = evt ?? {};
114
+ if (!requestId || !request)
115
+ return;
116
+ const headers = {};
117
+ for (const h of (request.headers ?? []))
118
+ if (h.name)
119
+ headers[String(h.name).toLowerCase()] = String(h.value ?? '');
120
+ pending.set(requestId, { method: request.method ?? 'GET', url: request.url ?? '', headers, startMs: Date.now() });
121
+ };
122
+ const onResponse = (evt) => {
123
+ const { requestId, response } = evt ?? {};
124
+ if (!requestId || !response)
125
+ return;
126
+ const req = pending.get(requestId);
127
+ if (!req)
128
+ return;
129
+ pending.delete(requestId);
130
+ const durationMs = Date.now() - req.startMs;
131
+ const responseHeaders = {};
132
+ for (const h of (response.headers ?? []))
133
+ if (h.name)
134
+ responseHeaders[String(h.name).toLowerCase()] = String(h.value ?? '');
135
+ entries.push({
136
+ startedAt: new Date(req.startMs).toISOString(),
137
+ method: req.method,
138
+ url: req.url,
139
+ status: response.status ?? 0,
140
+ statusText: response.statusText ?? '',
141
+ requestHeaders: req.headers,
142
+ responseHeaders,
143
+ contentType: responseHeaders['content-type'] ?? '',
144
+ bodySize: response.encodedDataLength ?? -1,
145
+ durationMs,
146
+ timings: { send: 0, wait: Math.round(durationMs * 0.9), receive: Math.round(durationMs * 0.1) },
147
+ });
148
+ };
149
+ if (cdpSession?.on) {
150
+ cdpSession.on('Network.requestWillBeSent', onRequest);
151
+ cdpSession.on('Network.responseReceived', onResponse);
142
152
  }
143
- };
153
+ return {
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
+ };
144
170
  }
145
- export {
146
- attachHarCapture,
147
- attachNetworkObserver
148
- };
149
- //# sourceMappingURL=network-observer.js.map
@@ -1,34 +1,45 @@
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
+ */
1
15
  const PRESETS = {
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 }
16
+ none: { offline: false, downloadThroughput: -1, uploadThroughput: -1, latency: 0 },
17
+ slow3g: { offline: false, downloadThroughput: 500 * 1024 / 8, uploadThroughput: 500 * 1024 / 8, latency: 2_000 },
18
+ fast3g: { offline: false, downloadThroughput: 1.6 * 1024 * 1024 / 8, uploadThroughput: 750 * 1024 / 8, latency: 562.5 },
19
+ '4g': { offline: false, downloadThroughput: 9 * 1024 * 1024 / 8, uploadThroughput: 2.67 * 1024 * 1024 / 8, latency: 85 },
20
+ offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 },
7
21
  };
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
- }
22
+ export async function applyNetworkThrottle(cdpSession, preset) {
23
+ if (!cdpSession?.send)
24
+ return;
25
+ const condition = PRESETS[preset];
26
+ try {
27
+ await cdpSession.send('Network.enable');
28
+ await cdpSession.send('Network.emulateNetworkConditions', condition);
29
+ }
30
+ catch { /* non-fatal — CDP may not be available */ }
16
31
  }
17
- async function clearNetworkThrottle(cdpSession) {
18
- await applyNetworkThrottle(cdpSession, "none");
32
+ export async function clearNetworkThrottle(cdpSession) {
33
+ await applyNetworkThrottle(cdpSession, 'none');
19
34
  }
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
- }
35
+ /** Apply throttle, run fn, restore. Returns fn's result. */
36
+ export async function withNetworkThrottle(cdpSession, preset, fn, restoreAfter = true) {
37
+ await applyNetworkThrottle(cdpSession, preset);
38
+ try {
39
+ return await fn();
40
+ }
41
+ finally {
42
+ if (restoreAfter)
43
+ await clearNetworkThrottle(cdpSession).catch(() => { });
44
+ }
28
45
  }
29
- export {
30
- applyNetworkThrottle,
31
- clearNetworkThrottle,
32
- withNetworkThrottle
33
- };
34
- //# sourceMappingURL=network-throttle.js.map
@@ -1,89 +1,104 @@
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
+ */
1
11
  function extractPathParams(urlPattern) {
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;
12
+ const params = [];
13
+ const matches = urlPattern.matchAll(/\{(\w+)\}/g);
14
+ for (const m of matches) {
15
+ params.push({ name: m[1], in: 'path', required: true, schema: { type: 'string' } });
16
+ }
17
+ return params;
8
18
  }
9
19
  function methodSummary(method, path) {
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}`;
20
+ const resource = path.split('/').filter(s => !s.startsWith('{') && s.length > 0).pop() ?? 'resource';
21
+ const summaries = {
22
+ GET: `Get ${resource}`,
23
+ POST: `Create ${resource}`,
24
+ PUT: `Update ${resource}`,
25
+ PATCH: `Partially update ${resource}`,
26
+ DELETE: `Delete ${resource}`,
27
+ };
28
+ return summaries[method.toUpperCase()] ?? `${method} ${resource}`;
19
29
  }
20
30
  function inferTag(path) {
21
- const parts = path.split("/").filter((s) => s.length > 0 && !s.startsWith("{"));
22
- return parts[0] ?? "default";
31
+ const parts = path.split('/').filter(s => s.length > 0 && !s.startsWith('{'));
32
+ return parts[0] ?? 'default';
23
33
  }
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
- }
43
- };
44
- const op = {
45
- summary: methodSummary(obs.method, path),
46
- parameters: params.length > 0 ? params : void 0,
47
- responses,
48
- tags: [inferTag(path)]
34
+ export function generateOpenApiSpec(observations, opts = {}) {
35
+ const { title = 'ZeTa Discovered API', baseUrl = '/', version = '1.0.0' } = opts;
36
+ const spec = {
37
+ openapi: '3.0.3',
38
+ info: { title, version, description: 'Auto-generated by ZeTa crawler from observed network traffic.' },
39
+ servers: [{ url: baseUrl }],
40
+ paths: {},
49
41
  };
50
- spec.paths[path][method] = op;
51
- }
52
- return spec;
42
+ for (const obs of observations) {
43
+ const path = obs.urlPattern;
44
+ const method = obs.method.toLowerCase();
45
+ if (!spec.paths[path])
46
+ spec.paths[path] = {};
47
+ const params = extractPathParams(path);
48
+ const statusCode = obs.statusCode ?? 200;
49
+ const responses = {
50
+ [String(statusCode)]: {
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;
53
66
  }
54
67
  function shapeToJsonSchema(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) };
68
+ if (Array.isArray(shape))
69
+ return { type: 'array', items: shape.length > 0 ? shapeToJsonSchema(shape[0]) : {} };
70
+ if (shape && typeof shape === 'object') {
71
+ const props = {};
72
+ for (const [k, v] of Object.entries(shape))
73
+ props[k] = shapeToJsonSchema(v);
74
+ return { type: 'object', properties: props };
75
+ }
76
+ return { type: String(shape) };
62
77
  }
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("");
78
+ export function specToYaml(spec) {
79
+ // Minimal YAML serializer — avoids yaml dep
80
+ function toYaml(val, indent = 0) {
81
+ const pad = ' '.repeat(indent);
82
+ if (val === null || val === undefined)
83
+ return 'null';
84
+ if (typeof val === 'string')
85
+ return val.includes('\n') || val.includes(':') ? `"${val.replace(/"/g, '\\"')}"` : val;
86
+ if (typeof val === 'number' || typeof val === 'boolean')
87
+ return String(val);
88
+ if (Array.isArray(val))
89
+ return val.map(v => `\n${pad}- ${toYaml(v, indent + 1).trimStart()}`).join('') || '[]';
90
+ if (typeof val === 'object') {
91
+ const entries = Object.entries(val);
92
+ if (entries.length === 0)
93
+ return '{}';
94
+ return entries.map(([k, v]) => {
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);
80
102
  }
81
- return String(val);
82
- }
83
- return `openapi: "${spec.openapi}"${toYaml(spec, 0)}`;
103
+ return `openapi: "${spec.openapi}"${toYaml(spec, 0)}`;
84
104
  }
85
- export {
86
- generateOpenApiSpec,
87
- specToYaml
88
- };
89
- //# sourceMappingURL=openapi-generator.js.map