swagger-digest 0.1.1 → 0.2.0
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/LICENSE +2 -2
- package/README.md +135 -1
- package/dist/chunk-2NORV6KM.js +143 -0
- package/dist/{chunk-UZQM2SHL.js → chunk-U7OLLL6J.js} +1313 -517
- package/dist/cli.js +90 -8
- package/dist/index.d.ts +107 -5
- package/dist/index.js +9 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +16 -0
- package/package.json +14 -13
|
@@ -1,6 +1,505 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
function readPackageVersion(moduleUrl) {
|
|
4
|
+
const require2 = createRequire(moduleUrl);
|
|
5
|
+
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
6
|
+
try {
|
|
7
|
+
const pkg = require2(candidate);
|
|
8
|
+
if (typeof pkg?.version === "string" && pkg.version) return pkg.version;
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return "unknown";
|
|
13
|
+
}
|
|
14
|
+
var VERSION = readPackageVersion(import.meta.url);
|
|
15
|
+
var USER_AGENT = `swagger-digest/${VERSION}`;
|
|
16
|
+
|
|
17
|
+
// src/transformer/utils.ts
|
|
18
|
+
function isObject(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function entries(value) {
|
|
22
|
+
return isObject(value) ? Object.entries(value) : [];
|
|
23
|
+
}
|
|
24
|
+
function cleanText(value) {
|
|
25
|
+
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
26
|
+
}
|
|
27
|
+
function markdownCell(value) {
|
|
28
|
+
return cleanText(value).replace(/\|/g, "\\|").replace(/`/g, "\\`") || "\u2014";
|
|
29
|
+
}
|
|
30
|
+
function safeSourceUrl(value) {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(value);
|
|
33
|
+
url.username = "";
|
|
34
|
+
url.password = "";
|
|
35
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
36
|
+
if (/(token|secret|password|signature|credential|api[-_]?key|authorization|auth)/i.test(key)) {
|
|
37
|
+
url.searchParams.set(key, "[redacted]");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return url.toString();
|
|
41
|
+
} catch {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function safeComment(value) {
|
|
46
|
+
return cleanText(value).replace(/\*\//g, "* /").replace(/[\r\n]+/g, " ");
|
|
47
|
+
}
|
|
48
|
+
function propertyKey(value) {
|
|
49
|
+
return /^[$A-Z_a-z][$\w]*$/.test(value) ? value : JSON.stringify(value);
|
|
50
|
+
}
|
|
51
|
+
function toTypeScriptIdentifier(value) {
|
|
52
|
+
let identifier = value.replace(/[^\p{L}\p{N}_$]+/gu, "_").replace(/^([^\p{L}_$])/u, "_$1");
|
|
53
|
+
if (!identifier) identifier = "Unnamed";
|
|
54
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
55
|
+
"abstract",
|
|
56
|
+
"any",
|
|
57
|
+
"as",
|
|
58
|
+
"asserts",
|
|
59
|
+
"async",
|
|
60
|
+
"await",
|
|
61
|
+
"boolean",
|
|
62
|
+
"break",
|
|
63
|
+
"case",
|
|
64
|
+
"catch",
|
|
65
|
+
"class",
|
|
66
|
+
"const",
|
|
67
|
+
"constructor",
|
|
68
|
+
"continue",
|
|
69
|
+
"debugger",
|
|
70
|
+
"declare",
|
|
71
|
+
"default",
|
|
72
|
+
"delete",
|
|
73
|
+
"do",
|
|
74
|
+
"else",
|
|
75
|
+
"enum",
|
|
76
|
+
"export",
|
|
77
|
+
"extends",
|
|
78
|
+
"false",
|
|
79
|
+
"finally",
|
|
80
|
+
"for",
|
|
81
|
+
"from",
|
|
82
|
+
"function",
|
|
83
|
+
"get",
|
|
84
|
+
"if",
|
|
85
|
+
"implements",
|
|
86
|
+
"import",
|
|
87
|
+
"in",
|
|
88
|
+
"infer",
|
|
89
|
+
"instanceof",
|
|
90
|
+
"interface",
|
|
91
|
+
"is",
|
|
92
|
+
"keyof",
|
|
93
|
+
"let",
|
|
94
|
+
"module",
|
|
95
|
+
"namespace",
|
|
96
|
+
"never",
|
|
97
|
+
"new",
|
|
98
|
+
"null",
|
|
99
|
+
"number",
|
|
100
|
+
"object",
|
|
101
|
+
"of",
|
|
102
|
+
"package",
|
|
103
|
+
"private",
|
|
104
|
+
"protected",
|
|
105
|
+
"public",
|
|
106
|
+
"readonly",
|
|
107
|
+
"require",
|
|
108
|
+
"return",
|
|
109
|
+
"set",
|
|
110
|
+
"static",
|
|
111
|
+
"string",
|
|
112
|
+
"super",
|
|
113
|
+
"switch",
|
|
114
|
+
"symbol",
|
|
115
|
+
"this",
|
|
116
|
+
"throw",
|
|
117
|
+
"true",
|
|
118
|
+
"try",
|
|
119
|
+
"type",
|
|
120
|
+
"typeof",
|
|
121
|
+
"undefined",
|
|
122
|
+
"unique",
|
|
123
|
+
"unknown",
|
|
124
|
+
"var",
|
|
125
|
+
"void",
|
|
126
|
+
"while",
|
|
127
|
+
"with",
|
|
128
|
+
"yield"
|
|
129
|
+
]);
|
|
130
|
+
if (reserved.has(identifier)) identifier = `${identifier}Model`;
|
|
131
|
+
return identifier;
|
|
132
|
+
}
|
|
133
|
+
|
|
1
134
|
// src/fetcher.ts
|
|
2
135
|
import { load } from "cheerio";
|
|
136
|
+
import { parse as parseYaml2 } from "yaml";
|
|
137
|
+
|
|
138
|
+
// src/errors.ts
|
|
139
|
+
var FetchError = class extends Error {
|
|
140
|
+
kind;
|
|
141
|
+
constructor(kind, message) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.name = "FetchError";
|
|
144
|
+
this.kind = kind;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var FetchSizeError = class extends FetchError {
|
|
148
|
+
constructor(message) {
|
|
149
|
+
super("size", message);
|
|
150
|
+
this.name = "FetchSizeError";
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var BlockedNetworkError = class extends FetchError {
|
|
154
|
+
constructor(message) {
|
|
155
|
+
super("blocked", message);
|
|
156
|
+
this.name = "BlockedNetworkError";
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var FetchNetworkError = class extends FetchError {
|
|
160
|
+
constructor(message) {
|
|
161
|
+
super("network", message);
|
|
162
|
+
this.name = "FetchNetworkError";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var FetchHttpError = class extends FetchError {
|
|
166
|
+
status;
|
|
167
|
+
constructor(message, status) {
|
|
168
|
+
super("http", message);
|
|
169
|
+
this.name = "FetchHttpError";
|
|
170
|
+
this.status = status;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function isFailFastFetchError(error) {
|
|
174
|
+
return error instanceof FetchError && (error.kind === "size" || error.kind === "blocked" || error.kind === "network");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/hardened-fetch.ts
|
|
178
|
+
import { lookup } from "dns/promises";
|
|
179
|
+
import { isIP } from "net";
|
|
180
|
+
var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
181
|
+
var DEFAULT_MAX_REDIRECTS = 5;
|
|
182
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
183
|
+
function isPrivateAddress(address) {
|
|
184
|
+
const family = isIP(address);
|
|
185
|
+
if (family === 4) return isPrivateIPv4(address);
|
|
186
|
+
if (family === 6) {
|
|
187
|
+
const normalized = address.toLowerCase();
|
|
188
|
+
const mapped = normalized.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
189
|
+
if (mapped?.[1]) return isPrivateIPv4(mapped[1]);
|
|
190
|
+
if (normalized === "::1" || normalized === "::") return true;
|
|
191
|
+
if (/^f[cd][0-9a-f]{2}(?::|$)/.test(normalized)) return true;
|
|
192
|
+
if (/^fe[89ab][0-9a-f](?::|$)/.test(normalized)) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
function isPrivateIPv4(address) {
|
|
198
|
+
const parts = address.split(".").map(Number);
|
|
199
|
+
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part))) return true;
|
|
200
|
+
const [a, b] = parts;
|
|
201
|
+
if (a === 0) return true;
|
|
202
|
+
if (a === 10) return true;
|
|
203
|
+
if (a === 127) return true;
|
|
204
|
+
if (a === 169 && b === 254) return true;
|
|
205
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
206
|
+
if (a === 192 && b === 168) return true;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
async function assertPublicHost(hostname) {
|
|
210
|
+
if (isIP(hostname) !== 0) {
|
|
211
|
+
if (isPrivateAddress(hostname)) {
|
|
212
|
+
throw new BlockedNetworkError(
|
|
213
|
+
`Blocked request to private network address (host resolved to a loopback/private/metadata address). Enable this only for hosts you trust; use it against internal corporate APIs without this option.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
let addresses;
|
|
219
|
+
try {
|
|
220
|
+
addresses = await lookup(hostname, { all: true, verbatim: true });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new FetchNetworkError(
|
|
223
|
+
`DNS resolution failed for ${hostname}: ${error instanceof Error ? error.message : String(error)}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (!addresses.length) {
|
|
227
|
+
throw new FetchNetworkError(`DNS resolution returned no addresses for ${hostname}`);
|
|
228
|
+
}
|
|
229
|
+
for (const { address } of addresses) {
|
|
230
|
+
if (isPrivateAddress(address)) {
|
|
231
|
+
throw new BlockedNetworkError(
|
|
232
|
+
`Blocked request to ${hostname}: it resolves to a private/loopback/metadata address while blockPrivateNetworks is enabled.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function normalizeHeaders(headers) {
|
|
238
|
+
return Object.fromEntries(Object.entries(headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
|
|
239
|
+
}
|
|
240
|
+
async function fetchWithLimits(url, options) {
|
|
241
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
242
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
243
|
+
const defaultHeaders = normalizeHeaders({
|
|
244
|
+
accept: "application/json, application/yaml, text/yaml, text/plain, text/html;q=0.8, */*;q=0.5",
|
|
245
|
+
"user-agent": USER_AGENT
|
|
246
|
+
});
|
|
247
|
+
const userHeaders = normalizeHeaders(options.headers ?? {});
|
|
248
|
+
return await follow(url, defaultHeaders, userHeaders, options, maxBytes, maxRedirects);
|
|
249
|
+
}
|
|
250
|
+
async function follow(url, defaultHeaders, userHeaders, options, maxBytes, redirectsLeft) {
|
|
251
|
+
let target;
|
|
252
|
+
try {
|
|
253
|
+
target = new URL(url);
|
|
254
|
+
} catch {
|
|
255
|
+
throw new FetchNetworkError(`Invalid URL: ${url}`);
|
|
256
|
+
}
|
|
257
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
258
|
+
throw new FetchNetworkError(`Only HTTP and HTTPS URLs are supported: ${url}`);
|
|
259
|
+
}
|
|
260
|
+
if (options.blockPrivateNetworks) {
|
|
261
|
+
await assertPublicHost(target.hostname);
|
|
262
|
+
}
|
|
263
|
+
const sameOrigin = !options.allowedHeaderOrigin || !options.headers || target.origin === options.allowedHeaderOrigin;
|
|
264
|
+
if (!sameOrigin && options.allowedHeaderOrigin && options.headers && Object.keys(options.headers).length > 0) {
|
|
265
|
+
options.onCrossOriginHeadersDropped?.(target.origin);
|
|
266
|
+
}
|
|
267
|
+
const requestHeaders = sameOrigin ? { ...defaultHeaders, ...userHeaders } : defaultHeaders;
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
const timeout = AbortSignal.timeout(options.timeoutMs);
|
|
270
|
+
timeout.addEventListener("abort", () => controller.abort(), { once: true });
|
|
271
|
+
let response;
|
|
272
|
+
try {
|
|
273
|
+
response = await fetch(target, {
|
|
274
|
+
headers: requestHeaders,
|
|
275
|
+
redirect: "manual",
|
|
276
|
+
signal: controller.signal
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error instanceof FetchError) throw error;
|
|
280
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
281
|
+
throw new FetchNetworkError(`Request to ${target} failed: ${describeNetworkError(reason)}`);
|
|
282
|
+
}
|
|
283
|
+
if (REDIRECT_STATUSES.has(response.status)) {
|
|
284
|
+
const location = response.headers.get("location");
|
|
285
|
+
if (!location) {
|
|
286
|
+
throw new FetchNetworkError(`Redirect response without a Location header from ${target}`);
|
|
287
|
+
}
|
|
288
|
+
if (redirectsLeft <= 0) {
|
|
289
|
+
throw new FetchNetworkError(`Too many redirects (limit ${options.maxRedirects ?? 5}) while fetching ${url}`);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
await response.arrayBuffer();
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
const next = new URL(response.headers.get("location") ?? "", target);
|
|
296
|
+
return follow(next.toString(), defaultHeaders, userHeaders, options, maxBytes, redirectsLeft - 1);
|
|
297
|
+
}
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
await discardBodySafely(response);
|
|
300
|
+
throw new FetchHttpError(`HTTP ${response.status} ${response.statusText} for ${url}`, response.status);
|
|
301
|
+
}
|
|
302
|
+
const contentLength = response.headers.get("content-length");
|
|
303
|
+
if (contentLength !== null) {
|
|
304
|
+
const declared = Number(contentLength);
|
|
305
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
306
|
+
await discardBodySafely(response);
|
|
307
|
+
throw new FetchSizeError(
|
|
308
|
+
`Response from ${target} is ${declared} bytes, exceeding the ${maxBytes} byte limit (content-length). Increase --max-bytes if the spec is genuinely large.`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const text = await readBodyWithCap(response, target.toString(), maxBytes);
|
|
313
|
+
return {
|
|
314
|
+
text,
|
|
315
|
+
contentType: response.headers.get("content-type") ?? "",
|
|
316
|
+
status: response.status,
|
|
317
|
+
url: target.toString()
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function describeNetworkError(reason) {
|
|
321
|
+
if (/timeouterror|timed out|aborted/i.test(reason)) return "request timed out";
|
|
322
|
+
if (/enotfound|getaddrinfo/i.test(reason)) return "host not found (DNS)";
|
|
323
|
+
if (/econnrefused/i.test(reason)) return "connection refused";
|
|
324
|
+
return reason;
|
|
325
|
+
}
|
|
326
|
+
async function readBodyWithCap(response, url, maxBytes) {
|
|
327
|
+
const body = response.body;
|
|
328
|
+
if (!body) return "";
|
|
329
|
+
const chunks = [];
|
|
330
|
+
let total = 0;
|
|
331
|
+
try {
|
|
332
|
+
for await (const chunk of body) {
|
|
333
|
+
chunks.push(chunk);
|
|
334
|
+
total += chunk.byteLength;
|
|
335
|
+
if (total > maxBytes) {
|
|
336
|
+
try {
|
|
337
|
+
await body.cancel();
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
throw new FetchSizeError(
|
|
341
|
+
`Response body from ${url} exceeded the ${maxBytes} byte limit while streaming (content-length was ${response.headers.get("content-length") ?? "not declared"}). Use --max-bytes to raise the cap if the spec is genuinely large.`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof FetchSizeError) throw error;
|
|
347
|
+
throw new FetchNetworkError(`Failed to read response body from ${url}: ${describeNetworkError(String(error))}`);
|
|
348
|
+
}
|
|
349
|
+
const decoder = new TextDecoder("utf-8");
|
|
350
|
+
const parts = [];
|
|
351
|
+
for (const chunk of chunks) parts.push(decoder.decode(chunk, { stream: true }));
|
|
352
|
+
parts.push(decoder.decode());
|
|
353
|
+
return parts.join("");
|
|
354
|
+
}
|
|
355
|
+
async function discardBodySafely(response) {
|
|
356
|
+
try {
|
|
357
|
+
await response.arrayBuffer();
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/bundler.ts
|
|
3
363
|
import { parse as parseYaml } from "yaml";
|
|
364
|
+
var MAX_EXTERNAL_DOCUMENTS = 5;
|
|
365
|
+
async function bundleExternalRefs(spec, sourceUrl, fetchDocument) {
|
|
366
|
+
if (!sourceUrl) return spec;
|
|
367
|
+
const externalRefs = collectExternalRefs(spec);
|
|
368
|
+
if (externalRefs.size === 0) return spec;
|
|
369
|
+
const schemaBucket = openSchemaBucket(spec);
|
|
370
|
+
const usedNames = new Set(Object.keys(schemaBucket));
|
|
371
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
372
|
+
let fetched = 0;
|
|
373
|
+
for (const ref of externalRefs) {
|
|
374
|
+
if (fetched >= MAX_EXTERNAL_DOCUMENTS) break;
|
|
375
|
+
const { docUrl, pointer } = splitExternalRef(ref, sourceUrl);
|
|
376
|
+
if (!docUrl) continue;
|
|
377
|
+
try {
|
|
378
|
+
const text = await fetchDocument(docUrl);
|
|
379
|
+
const parsed = parseDocumentText(text);
|
|
380
|
+
if (parsed === void 0) continue;
|
|
381
|
+
fetched += 1;
|
|
382
|
+
const target = resolvePointer(parsed, pointer);
|
|
383
|
+
if (target === void 0) continue;
|
|
384
|
+
const localName = uniqueNameFor(localNameFor(docUrl, pointer), usedNames);
|
|
385
|
+
schemaBucket[localName] = target;
|
|
386
|
+
mapping.set(ref, localName);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (mapping.size === 0) return spec;
|
|
391
|
+
const rewritten = rewriteRefs(spec, mapping);
|
|
392
|
+
return isObject(rewritten) ? rewritten : spec;
|
|
393
|
+
}
|
|
394
|
+
function collectExternalRefs(node) {
|
|
395
|
+
const found = /* @__PURE__ */ new Set();
|
|
396
|
+
const walk = (value) => {
|
|
397
|
+
if (Array.isArray(value)) {
|
|
398
|
+
for (const item of value) walk(item);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (!isObject(value)) return;
|
|
402
|
+
for (const [key, refValue] of Object.entries(value)) {
|
|
403
|
+
if (key === "$ref" && typeof refValue === "string" && !refValue.startsWith("#")) {
|
|
404
|
+
found.add(refValue);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (key === "example" || key === "examples" || key === "externalDocs") continue;
|
|
408
|
+
walk(refValue);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
walk(node);
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
function rewriteRefs(node, mapping) {
|
|
415
|
+
if (Array.isArray(node)) return node.map((item) => rewriteRefs(item, mapping));
|
|
416
|
+
if (!isObject(node)) return node;
|
|
417
|
+
const result = {};
|
|
418
|
+
for (const [key, value] of Object.entries(node)) {
|
|
419
|
+
if (key === "$ref" && typeof value === "string") {
|
|
420
|
+
const localName = mapping.get(value);
|
|
421
|
+
if (localName === void 0) {
|
|
422
|
+
result[key] = value;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
result[key] = `#/${bucketPath(bucketStyle(value))}/${localName}`;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
result[key] = rewriteRefs(value, mapping);
|
|
429
|
+
}
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
function bucketStyle(ref) {
|
|
433
|
+
return ref.includes("/components/") ? "openapi" : "swagger";
|
|
434
|
+
}
|
|
435
|
+
function bucketPath(style) {
|
|
436
|
+
return style === "openapi" ? "components/schemas" : "definitions";
|
|
437
|
+
}
|
|
438
|
+
function openSchemaBucket(spec) {
|
|
439
|
+
if (isObject(spec.components)) {
|
|
440
|
+
if (!isObject(spec.components.schemas)) spec.components.schemas = {};
|
|
441
|
+
return spec.components.schemas;
|
|
442
|
+
}
|
|
443
|
+
if (!isObject(spec.definitions)) spec.definitions = {};
|
|
444
|
+
return spec.definitions;
|
|
445
|
+
}
|
|
446
|
+
function splitExternalRef(ref, baseUrl) {
|
|
447
|
+
const hashIndex = ref.indexOf("#");
|
|
448
|
+
if (hashIndex < 0) return { docUrl: void 0, pointer: "" };
|
|
449
|
+
const docPart = ref.slice(0, hashIndex);
|
|
450
|
+
const pointer = ref.slice(hashIndex + 1);
|
|
451
|
+
try {
|
|
452
|
+
return { docUrl: new URL(docPart, baseUrl).toString(), pointer };
|
|
453
|
+
} catch {
|
|
454
|
+
return { docUrl: void 0, pointer };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function parseDocumentText(text) {
|
|
458
|
+
try {
|
|
459
|
+
return JSON.parse(text);
|
|
460
|
+
} catch {
|
|
461
|
+
try {
|
|
462
|
+
return parseYaml(text);
|
|
463
|
+
} catch {
|
|
464
|
+
return void 0;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function resolvePointer(root, pointer) {
|
|
469
|
+
if (!pointer.startsWith("/")) return void 0;
|
|
470
|
+
let current = root;
|
|
471
|
+
for (const rawPart of pointer.slice(1).split("/")) {
|
|
472
|
+
const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
473
|
+
if (!isObject(current)) return void 0;
|
|
474
|
+
current = current[part];
|
|
475
|
+
}
|
|
476
|
+
return current;
|
|
477
|
+
}
|
|
478
|
+
function localNameFor(docUrl, pointer) {
|
|
479
|
+
try {
|
|
480
|
+
const url = new URL(docUrl);
|
|
481
|
+
const file = url.pathname.split("/").pop() ?? "external";
|
|
482
|
+
const stem = file.replace(/\.(json|ya?ml)$/i, "").replace(/[^A-Za-z0-9_$]+/g, "_") || "external";
|
|
483
|
+
const segments = pointer.split("/").filter(Boolean);
|
|
484
|
+
const pointerName = segments.length > 0 ? segments[segments.length - 1] : void 0;
|
|
485
|
+
const cleanedPointer = pointerName?.replace(/[^A-Za-z0-9_$]+/g, "_");
|
|
486
|
+
return [stem, cleanedPointer].filter(Boolean).join("_") || "external";
|
|
487
|
+
} catch {
|
|
488
|
+
return "external";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function uniqueNameFor(base, used) {
|
|
492
|
+
let candidate = base;
|
|
493
|
+
let suffix = 2;
|
|
494
|
+
while (used.has(candidate)) {
|
|
495
|
+
candidate = `${candidate}${suffix}`;
|
|
496
|
+
suffix += 1;
|
|
497
|
+
}
|
|
498
|
+
used.add(candidate);
|
|
499
|
+
return candidate;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/fetcher.ts
|
|
4
503
|
var SPEC_FILE_EXTENSION = /\.(?:json|ya?ml)$/i;
|
|
5
504
|
var FALLBACK_PATHS = ["/v3/api-docs", "/swagger/v1/swagger.json", "/swagger.json", "/openapi.json", "/api-docs"];
|
|
6
505
|
function isRecord(value) {
|
|
@@ -14,7 +513,7 @@ function isOpenApiSpec(value) {
|
|
|
14
513
|
return hasVersion && hasInfo && hasPaths;
|
|
15
514
|
}
|
|
16
515
|
function parseSpecText(text, source = "response") {
|
|
17
|
-
const parsers = [() => JSON.parse(text), () =>
|
|
516
|
+
const parsers = [() => JSON.parse(text), () => parseYaml2(text)];
|
|
18
517
|
const errors = [];
|
|
19
518
|
for (const parser of parsers) {
|
|
20
519
|
try {
|
|
@@ -30,62 +529,164 @@ function parseSpecText(text, source = "response") {
|
|
|
30
529
|
function isDirectSpecUrl(url) {
|
|
31
530
|
return SPEC_FILE_EXTENSION.test(url.pathname);
|
|
32
531
|
}
|
|
33
|
-
async function fetchText(url,
|
|
34
|
-
const response = await
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
532
|
+
async function fetchText(url, ctx) {
|
|
533
|
+
const response = await fetchWithLimits(url, {
|
|
534
|
+
timeoutMs: ctx.timeoutMs,
|
|
535
|
+
maxBytes: ctx.maxBytes,
|
|
536
|
+
maxRedirects: ctx.maxRedirects,
|
|
537
|
+
blockPrivateNetworks: ctx.blockPrivateNetworks,
|
|
538
|
+
headers: ctx.headers,
|
|
539
|
+
allowedHeaderOrigin: ctx.allowedHeaderOrigin,
|
|
540
|
+
onCrossOriginHeadersDropped: ctx.onCrossOriginHeadersDropped
|
|
41
541
|
});
|
|
42
|
-
if (!response.ok) {
|
|
43
|
-
throw new Error(`HTTP ${response.status} ${response.statusText} for ${url}`);
|
|
44
|
-
}
|
|
45
542
|
return {
|
|
46
|
-
text:
|
|
47
|
-
contentType: response.
|
|
543
|
+
text: response.text,
|
|
544
|
+
contentType: response.contentType
|
|
48
545
|
};
|
|
49
546
|
}
|
|
50
547
|
function extractScripts(html) {
|
|
51
548
|
const $ = load(html);
|
|
52
549
|
const inline = [];
|
|
53
550
|
const external = [];
|
|
54
|
-
$("script").
|
|
551
|
+
for (const element of $("script").toArray()) {
|
|
55
552
|
const body = $(element).html();
|
|
56
553
|
if (body?.trim()) inline.push(body);
|
|
57
554
|
const src = $(element).attr("src");
|
|
58
555
|
if (src?.trim()) external.push(src.trim());
|
|
59
|
-
}
|
|
556
|
+
}
|
|
60
557
|
return { inline, external };
|
|
61
558
|
}
|
|
62
559
|
function extractSpecUrlFromScripts(scripts, pageUrl) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
560
|
+
return extractSpecUrlsFromScripts(scripts, pageUrl)[0];
|
|
561
|
+
}
|
|
562
|
+
function extractSpecUrlsFromScripts(scripts, pageUrl) {
|
|
563
|
+
const found = [];
|
|
564
|
+
const pushCandidate = (candidate) => {
|
|
565
|
+
const trimmed = candidate.trim();
|
|
566
|
+
if (!trimmed || trimmed.startsWith("data:")) return;
|
|
69
567
|
try {
|
|
70
|
-
|
|
568
|
+
const resolved = new URL(trimmed, pageUrl).toString();
|
|
569
|
+
if (!found.includes(resolved)) found.push(resolved);
|
|
71
570
|
} catch {
|
|
72
571
|
}
|
|
572
|
+
};
|
|
573
|
+
for (const script of scripts) {
|
|
574
|
+
const urlsArray = findObjectLiteralArrayAfterKey(script, "urls");
|
|
575
|
+
if (urlsArray) {
|
|
576
|
+
const entryPattern = /\burl\s*:\s*(["'])(.*?)\1/gs;
|
|
577
|
+
for (const match of urlsArray.matchAll(entryPattern)) {
|
|
578
|
+
pushCandidate(match[2] ?? "");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (!urlsArray) {
|
|
582
|
+
const urlPattern = /\burl\s*:\s*(["'])(.*?)\1/s;
|
|
583
|
+
const match = urlPattern.exec(script);
|
|
584
|
+
if (match?.[2]) pushCandidate(match[2]);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
589
|
+
function findObjectLiteralArrayAfterKey(source, key) {
|
|
590
|
+
const keyPattern = new RegExp(`\\b${key}\\s*:`);
|
|
591
|
+
const match = keyPattern.exec(source);
|
|
592
|
+
if (!match) return void 0;
|
|
593
|
+
let index = match.index + match[0].length;
|
|
594
|
+
while (/\s/.test(source[index] ?? "")) index += 1;
|
|
595
|
+
if (source[index] !== "[") return void 0;
|
|
596
|
+
const start = index;
|
|
597
|
+
let depth = 0;
|
|
598
|
+
let quote;
|
|
599
|
+
let escaped = false;
|
|
600
|
+
let lineComment = false;
|
|
601
|
+
let blockComment = false;
|
|
602
|
+
for (; index < source.length; index += 1) {
|
|
603
|
+
const char = source[index] ?? "";
|
|
604
|
+
const next = source[index + 1] ?? "";
|
|
605
|
+
if (lineComment) {
|
|
606
|
+
if (char === "\n") lineComment = false;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (blockComment) {
|
|
610
|
+
if (char === "*" && next === "/") {
|
|
611
|
+
blockComment = false;
|
|
612
|
+
index += 1;
|
|
613
|
+
}
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (quote) {
|
|
617
|
+
if (escaped) {
|
|
618
|
+
escaped = false;
|
|
619
|
+
} else if (char === "\\") {
|
|
620
|
+
escaped = true;
|
|
621
|
+
} else if (char === quote) {
|
|
622
|
+
quote = void 0;
|
|
623
|
+
}
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (char === "/" && next === "/") {
|
|
627
|
+
lineComment = true;
|
|
628
|
+
index += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (char === "/" && next === "*") {
|
|
632
|
+
blockComment = true;
|
|
633
|
+
index += 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
637
|
+
quote = char;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (char === "[") depth += 1;
|
|
641
|
+
if (char === "]") {
|
|
642
|
+
depth -= 1;
|
|
643
|
+
if (depth === 0) return source.slice(start, index + 1);
|
|
644
|
+
}
|
|
73
645
|
}
|
|
74
646
|
return void 0;
|
|
75
647
|
}
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
if (fromScript) return fromScript;
|
|
648
|
+
function extractSpecUrls(scripts, pageUrl) {
|
|
649
|
+
const fromScripts = extractSpecUrlsFromScripts(scripts, pageUrl);
|
|
79
650
|
const queryUrl = pageUrl.searchParams.get("url");
|
|
80
651
|
if (queryUrl) {
|
|
81
652
|
try {
|
|
82
|
-
|
|
653
|
+
const resolved = new URL(queryUrl, pageUrl).toString();
|
|
654
|
+
if (!fromScripts.includes(resolved)) fromScripts.push(resolved);
|
|
83
655
|
} catch {
|
|
84
656
|
}
|
|
85
657
|
}
|
|
658
|
+
return fromScripts;
|
|
659
|
+
}
|
|
660
|
+
async function probeSpecUrls(candidates, ctx, recordFailure, onMultiple) {
|
|
661
|
+
if (candidates.length === 0) return void 0;
|
|
662
|
+
if (candidates.length > 1) {
|
|
663
|
+
onMultiple?.(candidates[0] ?? "", candidates);
|
|
664
|
+
}
|
|
665
|
+
const batchSize = 4;
|
|
666
|
+
for (let start = 0; start < candidates.length; start += batchSize) {
|
|
667
|
+
const batch = candidates.slice(start, start + batchSize);
|
|
668
|
+
const attempts = await Promise.all(
|
|
669
|
+
batch.map(async (candidate) => {
|
|
670
|
+
try {
|
|
671
|
+
return { candidate, spec: await tryFetchSpec(candidate, ctx) };
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return { candidate, error };
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
);
|
|
677
|
+
const winner = attempts.find(
|
|
678
|
+
(attempt) => attempt.spec !== void 0
|
|
679
|
+
);
|
|
680
|
+
if (winner) {
|
|
681
|
+
return { spec: winner.spec, sourceUrl: winner.candidate, retrievalMethod: "html-config" };
|
|
682
|
+
}
|
|
683
|
+
for (const attempt of attempts) {
|
|
684
|
+
recordFailure("Configured spec", attempt.error, attempt.candidate);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
86
687
|
return void 0;
|
|
87
688
|
}
|
|
88
|
-
async function fetchExternalConfigScripts(srcs, pageUrl,
|
|
689
|
+
async function fetchExternalConfigScripts(srcs, pageUrl, ctx, errors) {
|
|
89
690
|
const candidates = srcs.map((src) => {
|
|
90
691
|
try {
|
|
91
692
|
return new URL(src, pageUrl);
|
|
@@ -100,7 +701,7 @@ async function fetchExternalConfigScripts(srcs, pageUrl, timeoutMs, errors) {
|
|
|
100
701
|
const scripts = [];
|
|
101
702
|
for (const candidate of candidates.slice(0, 10)) {
|
|
102
703
|
try {
|
|
103
|
-
const response = await fetchText(candidate.toString(),
|
|
704
|
+
const response = await fetchText(candidate.toString(), ctx);
|
|
104
705
|
scripts.push(response.text);
|
|
105
706
|
if (extractSpecUrlFromScripts(scripts, pageUrl) || scripts.some((script) => findObjectLiteralAfterKey(script, "spec"))) {
|
|
106
707
|
break;
|
|
@@ -195,7 +796,7 @@ function findObjectLiteralAfterKey(source, key) {
|
|
|
195
796
|
function parseInlineObjectLiteral(literal) {
|
|
196
797
|
const candidates = [
|
|
197
798
|
() => JSON.parse(literal),
|
|
198
|
-
() =>
|
|
799
|
+
() => parseYaml2(literal),
|
|
199
800
|
// Swagger config often uses JS object syntax with trailing commas.
|
|
200
801
|
() => JSON.parse(literal.replace(/,\s*([}\]])/g, "$1"))
|
|
201
802
|
];
|
|
@@ -217,11 +818,26 @@ function extractInlineSpec(scripts) {
|
|
|
217
818
|
}
|
|
218
819
|
return void 0;
|
|
219
820
|
}
|
|
220
|
-
async function tryFetchSpec(url,
|
|
221
|
-
const response = await fetchText(url,
|
|
222
|
-
|
|
821
|
+
async function tryFetchSpec(url, ctx) {
|
|
822
|
+
const response = await fetchText(url, ctx);
|
|
823
|
+
const parsed = parseSpecText(response.text, url);
|
|
824
|
+
try {
|
|
825
|
+
return await bundleExternalRefs(parsed, url, async (docUrl) => (await fetchText(docUrl, ctx)).text);
|
|
826
|
+
} catch {
|
|
827
|
+
return parsed;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function headersForBrowserRequest(requestUrl, headers, allowedOrigin) {
|
|
831
|
+
if (!headers || Object.keys(headers).length === 0) return void 0;
|
|
832
|
+
let requestOrigin;
|
|
833
|
+
try {
|
|
834
|
+
requestOrigin = new URL(requestUrl).origin;
|
|
835
|
+
} catch {
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
return requestOrigin === allowedOrigin ? headers : void 0;
|
|
223
839
|
}
|
|
224
|
-
async function tryBrowserFallback(pageUrl) {
|
|
840
|
+
async function tryBrowserFallback(pageUrl, headers) {
|
|
225
841
|
let playwright;
|
|
226
842
|
try {
|
|
227
843
|
const optionalModuleName = "playwright";
|
|
@@ -234,6 +850,20 @@ async function tryBrowserFallback(pageUrl) {
|
|
|
234
850
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
235
851
|
try {
|
|
236
852
|
const page = await browser.newPage();
|
|
853
|
+
const allowedOrigin = new URL(pageUrl).origin;
|
|
854
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
855
|
+
const browserHeaders = Object.fromEntries(
|
|
856
|
+
Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
|
|
857
|
+
);
|
|
858
|
+
await page.route("**/*", async (route) => {
|
|
859
|
+
const extra = headersForBrowserRequest(route.request().url(), browserHeaders, allowedOrigin);
|
|
860
|
+
if (!extra) {
|
|
861
|
+
await route.continue();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
await route.continue({ headers: { ...route.request().headers(), ...extra } });
|
|
865
|
+
});
|
|
866
|
+
}
|
|
237
867
|
await page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
238
868
|
await page.waitForFunction(
|
|
239
869
|
() => {
|
|
@@ -241,7 +871,8 @@ async function tryBrowserFallback(pageUrl) {
|
|
|
241
871
|
try {
|
|
242
872
|
const candidate = w.ui?.spec?.();
|
|
243
873
|
const document = candidate && typeof candidate.toJSON === "function" ? candidate.toJSON() : candidate;
|
|
244
|
-
|
|
874
|
+
const doc = document;
|
|
875
|
+
return Boolean(doc?.openapi || doc?.swagger || w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
|
|
245
876
|
} catch {
|
|
246
877
|
return Boolean(w.swaggerDoc?.openapi || w.swaggerDoc?.swagger);
|
|
247
878
|
}
|
|
@@ -286,18 +917,42 @@ async function fetchOpenApiSpec(input, options = {}) {
|
|
|
286
917
|
throw new Error("Only HTTP and HTTPS URLs are supported.");
|
|
287
918
|
}
|
|
288
919
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
920
|
+
const ctx = {
|
|
921
|
+
timeoutMs,
|
|
922
|
+
maxBytes: options.maxBytes,
|
|
923
|
+
maxRedirects: options.maxRedirects,
|
|
924
|
+
blockPrivateNetworks: options.blockPrivateNetworks,
|
|
925
|
+
allowedHeaderOrigin: pageUrl.origin,
|
|
926
|
+
headers: options.headers,
|
|
927
|
+
onCrossOriginHeadersDropped: (origin) => options.warn?.(
|
|
928
|
+
`Custom headers were withheld: ${origin} is a different origin from the URL you supplied. Only tool defaults were sent.`
|
|
929
|
+
)
|
|
930
|
+
};
|
|
289
931
|
const errors = [];
|
|
932
|
+
const recordFailure = (label, error, subject) => {
|
|
933
|
+
if (isFailFastFetchError(error)) throw error;
|
|
934
|
+
const prefix = subject ? `${label} (${subject}):` : `${label}:`;
|
|
935
|
+
errors.push(`${prefix} ${error instanceof Error ? error.message : String(error)}`);
|
|
936
|
+
};
|
|
290
937
|
if (isDirectSpecUrl(pageUrl)) {
|
|
291
938
|
try {
|
|
292
|
-
const spec = await tryFetchSpec(pageUrl.toString(),
|
|
939
|
+
const spec = await tryFetchSpec(pageUrl.toString(), ctx);
|
|
293
940
|
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
|
|
294
941
|
} catch (error) {
|
|
295
|
-
|
|
942
|
+
recordFailure("Direct spec", error);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (options.specUrl) {
|
|
946
|
+
try {
|
|
947
|
+
const spec = await tryFetchSpec(options.specUrl, ctx);
|
|
948
|
+
return { spec, sourceUrl: options.specUrl, retrievalMethod: "direct-spec" };
|
|
949
|
+
} catch (error) {
|
|
950
|
+
recordFailure("Specified spec (--spec)", error, options.specUrl);
|
|
296
951
|
}
|
|
297
952
|
}
|
|
298
953
|
let html;
|
|
299
954
|
try {
|
|
300
|
-
const response = await fetchText(pageUrl.toString(),
|
|
955
|
+
const response = await fetchText(pageUrl.toString(), ctx);
|
|
301
956
|
try {
|
|
302
957
|
const spec = parseSpecText(response.text, pageUrl.toString());
|
|
303
958
|
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "direct-spec" };
|
|
@@ -305,35 +960,29 @@ async function fetchOpenApiSpec(input, options = {}) {
|
|
|
305
960
|
html = response.text;
|
|
306
961
|
}
|
|
307
962
|
} catch (error) {
|
|
308
|
-
|
|
963
|
+
recordFailure("Swagger UI page", error);
|
|
309
964
|
}
|
|
310
965
|
if (html !== void 0) {
|
|
311
966
|
const pageScripts = extractScripts(html);
|
|
312
|
-
|
|
313
|
-
let specUrl = extractSpecUrl(scripts, pageUrl);
|
|
967
|
+
const scripts = pageScripts.inline;
|
|
314
968
|
const inlineSpec = extractInlineSpec(scripts);
|
|
315
969
|
if (inlineSpec) {
|
|
316
970
|
return { spec: inlineSpec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
|
|
317
971
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
const externalScripts = await fetchExternalConfigScripts(pageScripts.external, pageUrl, timeoutMs, errors);
|
|
327
|
-
scripts = [...scripts, ...externalScripts];
|
|
328
|
-
specUrl = extractSpecUrl(scripts, pageUrl);
|
|
329
|
-
if (specUrl && specUrl !== pageUrl.searchParams.get("url")) {
|
|
330
|
-
try {
|
|
331
|
-
const spec = await tryFetchSpec(specUrl, timeoutMs);
|
|
332
|
-
return { spec, sourceUrl: specUrl, retrievalMethod: "html-config" };
|
|
333
|
-
} catch (error) {
|
|
334
|
-
errors.push(`Configured spec (${specUrl}): ${error instanceof Error ? error.message : String(error)}`);
|
|
972
|
+
const noteMultiple = (picked, all) => {
|
|
973
|
+
if (all.length > 1) {
|
|
974
|
+
options.warn?.(
|
|
975
|
+
`Multiple spec definitions found (${String(all.length)}); using ${picked}. Others: ${all.filter((url) => url !== picked).join(", ")}.`
|
|
976
|
+
);
|
|
335
977
|
}
|
|
336
|
-
}
|
|
978
|
+
};
|
|
979
|
+
const fromInline = await probeSpecUrls(extractSpecUrls(scripts, pageUrl), ctx, recordFailure, noteMultiple);
|
|
980
|
+
if (fromInline) return fromInline;
|
|
981
|
+
const externalScripts = await fetchExternalConfigScripts(pageScripts.external, pageUrl, ctx, errors);
|
|
982
|
+
const inlineUrls = extractSpecUrls(scripts, pageUrl);
|
|
983
|
+
const externalUrls = extractSpecUrls(externalScripts, pageUrl).filter((url) => !inlineUrls.includes(url));
|
|
984
|
+
const fromExternal = await probeSpecUrls([...inlineUrls, ...externalUrls], ctx, recordFailure, noteMultiple);
|
|
985
|
+
if (fromExternal) return fromExternal;
|
|
337
986
|
for (const script of scripts) {
|
|
338
987
|
const literal = findObjectLiteralAfterKey(script, "spec");
|
|
339
988
|
if (!literal) continue;
|
|
@@ -341,17 +990,33 @@ async function fetchOpenApiSpec(input, options = {}) {
|
|
|
341
990
|
if (spec) return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "inline-spec" };
|
|
342
991
|
}
|
|
343
992
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
993
|
+
const probeCtx = { ...ctx, timeoutMs: options.probeTimeoutMs ?? 5e3 };
|
|
994
|
+
const candidates = getFallbackUrls(pageUrl);
|
|
995
|
+
const probeBatchSize = 4;
|
|
996
|
+
for (let start = 0; start < candidates.length; start += probeBatchSize) {
|
|
997
|
+
const batch = candidates.slice(start, start + probeBatchSize);
|
|
998
|
+
const attempts = await Promise.all(
|
|
999
|
+
batch.map(async (candidate) => {
|
|
1000
|
+
try {
|
|
1001
|
+
return { candidate, spec: await tryFetchSpec(candidate, probeCtx) };
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
return { candidate, error };
|
|
1004
|
+
}
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
const winner = attempts.find(
|
|
1008
|
+
(attempt) => attempt.spec !== void 0
|
|
1009
|
+
);
|
|
1010
|
+
if (winner) {
|
|
1011
|
+
return { spec: winner.spec, sourceUrl: winner.candidate, retrievalMethod: "fallback-path" };
|
|
1012
|
+
}
|
|
1013
|
+
for (const attempt of attempts) {
|
|
1014
|
+
recordFailure(attempt.candidate, attempt.error);
|
|
350
1015
|
}
|
|
351
1016
|
}
|
|
352
1017
|
if (options.useBrowserFallback !== false) {
|
|
353
1018
|
try {
|
|
354
|
-
const spec = await tryBrowserFallback(pageUrl.toString());
|
|
1019
|
+
const spec = await tryBrowserFallback(pageUrl.toString(), ctx.headers);
|
|
355
1020
|
return { spec, sourceUrl: pageUrl.toString(), retrievalMethod: "headless-browser" };
|
|
356
1021
|
} catch (error) {
|
|
357
1022
|
errors.push(`Headless browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -366,171 +1031,147 @@ Check that the URL is reachable from this terminal and points to a Swagger UI pa
|
|
|
366
1031
|
);
|
|
367
1032
|
}
|
|
368
1033
|
|
|
369
|
-
// src/
|
|
370
|
-
var
|
|
371
|
-
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return typeof value === "string" ? value.trim().replace(/\s+/g, " ") : "";
|
|
395
|
-
}
|
|
396
|
-
function markdownCell(value) {
|
|
397
|
-
return cleanText(value).replace(/\|/g, "\\|").replace(/`/g, "\\`") || "\u2014";
|
|
398
|
-
}
|
|
399
|
-
function safeSourceUrl(value) {
|
|
400
|
-
try {
|
|
401
|
-
const url = new URL(value);
|
|
402
|
-
url.username = "";
|
|
403
|
-
url.password = "";
|
|
404
|
-
for (const key of [...url.searchParams.keys()]) {
|
|
405
|
-
if (/(token|secret|password|signature|credential|api[-_]?key|authorization|auth)/i.test(key)) {
|
|
406
|
-
url.searchParams.set(key, "[redacted]");
|
|
1034
|
+
// src/tokens.ts
|
|
1035
|
+
var MODEL_CONFIGS = {
|
|
1036
|
+
claude: { name: "Claude (Sonnet / Opus)", contextWindow: 2e5, ratio: 1.08 },
|
|
1037
|
+
openai: { name: "OpenAI (GPT-4o / o-series)", contextWindow: 128e3, ratio: 0.95 },
|
|
1038
|
+
gemini: { name: "Google Gemini (Pro / Flash)", contextWindow: 1e6, ratio: 0.9 },
|
|
1039
|
+
deepseek: { name: "DeepSeek (V3 / R1)", contextWindow: 128e3, ratio: 0.97 },
|
|
1040
|
+
llama: { name: "Meta Llama (3.3 / 4)", contextWindow: 128e3, ratio: 0.98 }
|
|
1041
|
+
};
|
|
1042
|
+
function estimateTokens(text) {
|
|
1043
|
+
const characters = text.length;
|
|
1044
|
+
if (characters === 0) {
|
|
1045
|
+
const zeroModel = ({ name, contextWindow }) => ({
|
|
1046
|
+
name,
|
|
1047
|
+
tokens: 0,
|
|
1048
|
+
contextWindow,
|
|
1049
|
+
contextPercent: 0
|
|
1050
|
+
});
|
|
1051
|
+
return {
|
|
1052
|
+
characters: 0,
|
|
1053
|
+
models: {
|
|
1054
|
+
claude: zeroModel(MODEL_CONFIGS.claude),
|
|
1055
|
+
openai: zeroModel(MODEL_CONFIGS.openai),
|
|
1056
|
+
gemini: zeroModel(MODEL_CONFIGS.gemini),
|
|
1057
|
+
deepseek: zeroModel(MODEL_CONFIGS.deepseek),
|
|
1058
|
+
llama: zeroModel(MODEL_CONFIGS.llama)
|
|
407
1059
|
}
|
|
408
|
-
}
|
|
409
|
-
return url.toString();
|
|
410
|
-
} catch {
|
|
411
|
-
return value;
|
|
1060
|
+
};
|
|
412
1061
|
}
|
|
1062
|
+
const wordCount = (text.match(/\b[\w$]+\b/gu) || []).length;
|
|
1063
|
+
const syntaxSymbols = (text.match(/[{}[\]()<>=:;,."`|/-]/g) || []).length;
|
|
1064
|
+
const whitespaces = (text.match(/\s+/g) || []).length;
|
|
1065
|
+
const baseTokens = wordCount * 1.18 + syntaxSymbols * 0.42 + whitespaces * 0.05;
|
|
1066
|
+
const calculate = (name, ratio, contextWindow) => {
|
|
1067
|
+
const tokens = Math.max(1, Math.round(baseTokens * ratio));
|
|
1068
|
+
const contextPercent = Number((tokens / contextWindow * 100).toFixed(1));
|
|
1069
|
+
return { name, tokens, contextWindow, contextPercent };
|
|
1070
|
+
};
|
|
1071
|
+
return {
|
|
1072
|
+
characters,
|
|
1073
|
+
models: {
|
|
1074
|
+
claude: calculate(MODEL_CONFIGS.claude.name, MODEL_CONFIGS.claude.ratio, MODEL_CONFIGS.claude.contextWindow),
|
|
1075
|
+
openai: calculate(MODEL_CONFIGS.openai.name, MODEL_CONFIGS.openai.ratio, MODEL_CONFIGS.openai.contextWindow),
|
|
1076
|
+
gemini: calculate(MODEL_CONFIGS.gemini.name, MODEL_CONFIGS.gemini.ratio, MODEL_CONFIGS.gemini.contextWindow),
|
|
1077
|
+
deepseek: calculate(
|
|
1078
|
+
MODEL_CONFIGS.deepseek.name,
|
|
1079
|
+
MODEL_CONFIGS.deepseek.ratio,
|
|
1080
|
+
MODEL_CONFIGS.deepseek.contextWindow
|
|
1081
|
+
),
|
|
1082
|
+
llama: calculate(MODEL_CONFIGS.llama.name, MODEL_CONFIGS.llama.ratio, MODEL_CONFIGS.llama.contextWindow)
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
413
1085
|
}
|
|
414
|
-
|
|
415
|
-
|
|
1086
|
+
|
|
1087
|
+
// src/transformer/security.ts
|
|
1088
|
+
function describeSecurityScheme(name, scheme) {
|
|
1089
|
+
const type = cleanText(scheme.type).toLowerCase();
|
|
1090
|
+
const label = cleanText(scheme.description);
|
|
1091
|
+
let details = name;
|
|
1092
|
+
if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "bearer")
|
|
1093
|
+
details += " (Bearer token via Authorization header)";
|
|
1094
|
+
else if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "basic")
|
|
1095
|
+
details += " (HTTP Basic via Authorization header)";
|
|
1096
|
+
else if (type === "apikey" || type === "apiKey")
|
|
1097
|
+
details += ` (API key in ${cleanText(scheme.in) || "header"}: ${cleanText(scheme.name) || "key"})`;
|
|
1098
|
+
else if (type === "oauth2") {
|
|
1099
|
+
const flows = entries(scheme.flows).map(([flowName]) => flowName).join(", ") || cleanText(scheme.flow);
|
|
1100
|
+
details += ` (OAuth2${flows ? `; ${flows}` : ""})`;
|
|
1101
|
+
} else if (type === "openidconnect" || type === "openIdConnect") details += " (OpenID Connect)";
|
|
1102
|
+
else if (type === "basic") details += " (HTTP Basic)";
|
|
1103
|
+
else if (type) details += ` (${type})`;
|
|
1104
|
+
if (label) details += ` \u2014 ${label}`;
|
|
1105
|
+
return details;
|
|
416
1106
|
}
|
|
417
|
-
function
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const metadata = [];
|
|
421
|
-
if (cleanText(schema.description)) metadata.push(cleanText(schema.description));
|
|
422
|
-
if (cleanText(schema.format)) metadata.push(`format: ${cleanText(schema.format)}`);
|
|
423
|
-
const constraints = [
|
|
424
|
-
["default", "default"],
|
|
425
|
-
["pattern", "pattern"],
|
|
426
|
-
["minLength", "minLength"],
|
|
427
|
-
["maxLength", "maxLength"],
|
|
428
|
-
["minimum", "minimum"],
|
|
429
|
-
["maximum", "maximum"],
|
|
430
|
-
["exclusiveMinimum", "exclusiveMinimum"],
|
|
431
|
-
["exclusiveMaximum", "exclusiveMaximum"],
|
|
432
|
-
["minItems", "minItems"],
|
|
433
|
-
["maxItems", "maxItems"],
|
|
434
|
-
["uniqueItems", "uniqueItems"],
|
|
435
|
-
["multipleOf", "multipleOf"]
|
|
436
|
-
];
|
|
437
|
-
for (const [property, label] of constraints) {
|
|
438
|
-
const value = schema[property];
|
|
439
|
-
if (value === void 0) continue;
|
|
440
|
-
if (property === "default" && (value === null || value === "")) continue;
|
|
441
|
-
metadata.push(
|
|
442
|
-
`${label}: ${property === "default" ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value)}`
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
if (schema.readOnly === true) metadata.push("read-only");
|
|
446
|
-
if (schema.writeOnly === true) metadata.push("write-only");
|
|
447
|
-
if (schema.deprecated === true) metadata.push("deprecated");
|
|
448
|
-
return [...new Set(metadata)];
|
|
1107
|
+
function getSecuritySchemes(spec) {
|
|
1108
|
+
const dictionary = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
|
|
1109
|
+
return entries(dictionary).filter((entry) => isObject(entry[1]));
|
|
449
1110
|
}
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
if (!
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
"
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
"
|
|
461
|
-
|
|
462
|
-
"case",
|
|
463
|
-
"catch",
|
|
464
|
-
"class",
|
|
465
|
-
"const",
|
|
466
|
-
"constructor",
|
|
467
|
-
"continue",
|
|
468
|
-
"debugger",
|
|
469
|
-
"declare",
|
|
470
|
-
"default",
|
|
471
|
-
"delete",
|
|
472
|
-
"do",
|
|
473
|
-
"else",
|
|
474
|
-
"enum",
|
|
475
|
-
"export",
|
|
476
|
-
"extends",
|
|
477
|
-
"false",
|
|
478
|
-
"finally",
|
|
479
|
-
"for",
|
|
480
|
-
"from",
|
|
481
|
-
"function",
|
|
482
|
-
"get",
|
|
483
|
-
"if",
|
|
484
|
-
"implements",
|
|
485
|
-
"import",
|
|
486
|
-
"in",
|
|
487
|
-
"infer",
|
|
488
|
-
"instanceof",
|
|
489
|
-
"interface",
|
|
490
|
-
"is",
|
|
491
|
-
"keyof",
|
|
492
|
-
"let",
|
|
493
|
-
"module",
|
|
494
|
-
"namespace",
|
|
495
|
-
"never",
|
|
496
|
-
"new",
|
|
497
|
-
"null",
|
|
498
|
-
"number",
|
|
499
|
-
"object",
|
|
500
|
-
"of",
|
|
501
|
-
"package",
|
|
502
|
-
"private",
|
|
503
|
-
"protected",
|
|
504
|
-
"public",
|
|
505
|
-
"readonly",
|
|
506
|
-
"require",
|
|
507
|
-
"return",
|
|
508
|
-
"set",
|
|
509
|
-
"static",
|
|
510
|
-
"string",
|
|
511
|
-
"super",
|
|
512
|
-
"switch",
|
|
513
|
-
"symbol",
|
|
514
|
-
"this",
|
|
515
|
-
"throw",
|
|
516
|
-
"true",
|
|
517
|
-
"try",
|
|
518
|
-
"type",
|
|
519
|
-
"typeof",
|
|
520
|
-
"undefined",
|
|
521
|
-
"unique",
|
|
522
|
-
"unknown",
|
|
523
|
-
"var",
|
|
524
|
-
"void",
|
|
525
|
-
"while",
|
|
526
|
-
"with",
|
|
527
|
-
"yield"
|
|
528
|
-
]);
|
|
529
|
-
if (reserved.has(identifier)) identifier = `${identifier}Model`;
|
|
530
|
-
return identifier;
|
|
1111
|
+
function securityRequirement(spec, operation) {
|
|
1112
|
+
const security = operation.security !== void 0 ? operation.security : spec.security;
|
|
1113
|
+
if (!Array.isArray(security) || security.length === 0) return "None explicitly required";
|
|
1114
|
+
const allSchemes = new Map(getSecuritySchemes(spec));
|
|
1115
|
+
return security.map((requirement) => {
|
|
1116
|
+
if (!isObject(requirement)) return "";
|
|
1117
|
+
const names = Object.entries(requirement).map(([name, scopes]) => {
|
|
1118
|
+
const label = allSchemes.has(name) ? name : `${name} (scheme not defined)`;
|
|
1119
|
+
return Array.isArray(scopes) && scopes.length ? `${label} [scopes: ${scopes.join(", ")}]` : label;
|
|
1120
|
+
});
|
|
1121
|
+
return names.length ? names.join(" + ") : "No auth";
|
|
1122
|
+
}).filter(Boolean).join(" OR ");
|
|
531
1123
|
}
|
|
532
|
-
function
|
|
533
|
-
|
|
1124
|
+
function authOverview(spec) {
|
|
1125
|
+
const schemes = getSecuritySchemes(spec);
|
|
1126
|
+
if (!schemes.length) return ["- **Authentication:** No security schemes declared."];
|
|
1127
|
+
const globalSecurity = securityRequirement(spec, {});
|
|
1128
|
+
return [
|
|
1129
|
+
`- **Authentication required globally:** ${globalSecurity}`,
|
|
1130
|
+
"- **Available schemes:**",
|
|
1131
|
+
...schemes.map(([name, scheme]) => ` - ${describeSecurityScheme(name, scheme)}`)
|
|
1132
|
+
];
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/transformer/render-style.ts
|
|
1136
|
+
var FULL_STYLE = { includeExamples: true, collapsed: false };
|
|
1137
|
+
function shortenDescription(text, limit) {
|
|
1138
|
+
if (limit === void 0 || text.length <= limit) return text;
|
|
1139
|
+
return `${text.slice(0, limit).trimEnd()}\u2026`;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/transformer/schema-type.ts
|
|
1143
|
+
function schemaMetadata(schemaValue) {
|
|
1144
|
+
if (!isObject(schemaValue)) return [];
|
|
1145
|
+
const schema = schemaValue;
|
|
1146
|
+
const metadata = [];
|
|
1147
|
+
if (cleanText(schema.description)) metadata.push(cleanText(schema.description));
|
|
1148
|
+
if (cleanText(schema.format)) metadata.push(`format: ${cleanText(schema.format)}`);
|
|
1149
|
+
const constraints = [
|
|
1150
|
+
["default", "default"],
|
|
1151
|
+
["pattern", "pattern"],
|
|
1152
|
+
["minLength", "minLength"],
|
|
1153
|
+
["maxLength", "maxLength"],
|
|
1154
|
+
["minimum", "minimum"],
|
|
1155
|
+
["maximum", "maximum"],
|
|
1156
|
+
["exclusiveMinimum", "exclusiveMinimum"],
|
|
1157
|
+
["exclusiveMaximum", "exclusiveMaximum"],
|
|
1158
|
+
["minItems", "minItems"],
|
|
1159
|
+
["maxItems", "maxItems"],
|
|
1160
|
+
["uniqueItems", "uniqueItems"],
|
|
1161
|
+
["multipleOf", "multipleOf"]
|
|
1162
|
+
];
|
|
1163
|
+
for (const [property, label] of constraints) {
|
|
1164
|
+
const value = schema[property];
|
|
1165
|
+
if (value === void 0) continue;
|
|
1166
|
+
if (property === "default" && (value === null || value === "")) continue;
|
|
1167
|
+
metadata.push(
|
|
1168
|
+
`${label}: ${property === "default" ? JSON.stringify(value) : typeof value === "string" ? value : JSON.stringify(value)}`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
if (schema.readOnly === true) metadata.push("read-only");
|
|
1172
|
+
if (schema.writeOnly === true) metadata.push("write-only");
|
|
1173
|
+
if (schema.deprecated === true) metadata.push("deprecated");
|
|
1174
|
+
return [...new Set(metadata)];
|
|
534
1175
|
}
|
|
535
1176
|
function refName(ref) {
|
|
536
1177
|
const parts = ref.split("/");
|
|
@@ -635,80 +1276,7 @@ function getSchemaDictionary(spec) {
|
|
|
635
1276
|
const schemas = spec.components?.schemas ?? spec.definitions ?? {};
|
|
636
1277
|
return isObject(schemas) ? schemas : {};
|
|
637
1278
|
}
|
|
638
|
-
function
|
|
639
|
-
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
640
|
-
seen.add(value);
|
|
641
|
-
if (Array.isArray(value)) {
|
|
642
|
-
for (const item of value) collectSchemaRefs(item, output, root, seen, followedRefs);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
const object = value;
|
|
646
|
-
if (typeof object.$ref === "string") {
|
|
647
|
-
if (isSchemaRef(object.$ref)) {
|
|
648
|
-
const name = refName(object.$ref);
|
|
649
|
-
if (name) output.add(name);
|
|
650
|
-
} else if (root && object.$ref.startsWith("#/") && !followedRefs.has(object.$ref)) {
|
|
651
|
-
followedRefs.add(object.$ref);
|
|
652
|
-
const target = resolvePointer(root, object.$ref);
|
|
653
|
-
if (target !== void 0) collectSchemaRefs(target, output, root, seen, followedRefs);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
for (const [key, child] of Object.entries(object)) {
|
|
657
|
-
if (key !== "example" && key !== "examples" && key !== "externalDocs")
|
|
658
|
-
collectSchemaRefs(child, output, root, seen, followedRefs);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
function referencedSchemas(spec) {
|
|
662
|
-
const dictionary = getSchemaDictionary(spec);
|
|
663
|
-
const knownByName = /* @__PURE__ */ new Map();
|
|
664
|
-
for (const [name, schema] of Object.entries(dictionary)) {
|
|
665
|
-
knownByName.set(toTypeScriptIdentifier(name), [name, schema]);
|
|
666
|
-
}
|
|
667
|
-
const refs = /* @__PURE__ */ new Set();
|
|
668
|
-
collectSchemaRefs(spec.paths ?? {}, refs, spec);
|
|
669
|
-
const selected = /* @__PURE__ */ new Set();
|
|
670
|
-
const queue = [...refs];
|
|
671
|
-
while (queue.length) {
|
|
672
|
-
const name = queue.shift();
|
|
673
|
-
if (!name || selected.has(name)) continue;
|
|
674
|
-
selected.add(name);
|
|
675
|
-
const model = knownByName.get(name);
|
|
676
|
-
if (!model) continue;
|
|
677
|
-
const childRefs = /* @__PURE__ */ new Set();
|
|
678
|
-
collectSchemaRefs(model[1], childRefs, spec);
|
|
679
|
-
for (const child of childRefs) if (!selected.has(child)) queue.push(child);
|
|
680
|
-
}
|
|
681
|
-
return [...selected].map((name) => knownByName.get(name)).filter((entry) => Boolean(entry));
|
|
682
|
-
}
|
|
683
|
-
function modelDeclaration(name, schema) {
|
|
684
|
-
const identifier = toTypeScriptIdentifier(name);
|
|
685
|
-
const description = safeComment(schema.description);
|
|
686
|
-
const comment = description ? `/** ${description} */
|
|
687
|
-
` : "";
|
|
688
|
-
if (schema.$ref) return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
689
|
-
if (Array.isArray(schema.enum) || schema.const !== void 0 || schema.oneOf || schema.anyOf || schema.allOf) {
|
|
690
|
-
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
691
|
-
}
|
|
692
|
-
const kind = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
|
|
693
|
-
if (kind === "object" || schema.properties || schema.additionalProperties !== void 0) {
|
|
694
|
-
const properties = entries(schema.properties);
|
|
695
|
-
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
696
|
-
const fields = properties.map(([field, value]) => {
|
|
697
|
-
const metadata = schemaMetadata(value).map(safeComment).filter(Boolean).join("; ");
|
|
698
|
-
const doc = metadata ? ` /** ${metadata} */
|
|
699
|
-
` : "";
|
|
700
|
-
return `${doc} ${propertyKey(field)}${required.has(field) ? "" : "?"}: ${typeExpression(value)};`;
|
|
701
|
-
});
|
|
702
|
-
if (schema.additionalProperties === true) fields.push(" [key: string]: unknown;");
|
|
703
|
-
else if (isObject(schema.additionalProperties))
|
|
704
|
-
fields.push(` [key: string]: ${typeExpression(schema.additionalProperties)};`);
|
|
705
|
-
return `${comment}export interface ${identifier} {
|
|
706
|
-
${fields.join("\n")}
|
|
707
|
-
}`;
|
|
708
|
-
}
|
|
709
|
-
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
710
|
-
}
|
|
711
|
-
function resolvePointer(root, ref) {
|
|
1279
|
+
function resolvePointer2(root, ref) {
|
|
712
1280
|
if (!ref.startsWith("#/")) return void 0;
|
|
713
1281
|
let current = root;
|
|
714
1282
|
for (const rawPart of ref.slice(2).split("/")) {
|
|
@@ -723,7 +1291,7 @@ function resolveReferenceObject(spec, value, seen = /* @__PURE__ */ new Set()) {
|
|
|
723
1291
|
if (typeof value.$ref !== "string") return value;
|
|
724
1292
|
const ref = value.$ref;
|
|
725
1293
|
if (seen.has(ref)) return {};
|
|
726
|
-
const target =
|
|
1294
|
+
const target = resolvePointer2(spec, ref);
|
|
727
1295
|
if (!isObject(target)) return value;
|
|
728
1296
|
const nextSeen = new Set(seen).add(ref);
|
|
729
1297
|
return {
|
|
@@ -731,112 +1299,8 @@ function resolveReferenceObject(spec, value, seen = /* @__PURE__ */ new Set()) {
|
|
|
731
1299
|
...Object.fromEntries(Object.entries(value).filter(([key]) => key !== "$ref"))
|
|
732
1300
|
};
|
|
733
1301
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const paths = isObject(spec.paths) ? spec.paths : {};
|
|
737
|
-
for (const [path2, pathItemValue] of Object.entries(paths)) {
|
|
738
|
-
if (!isObject(pathItemValue)) continue;
|
|
739
|
-
const pathParameters = Array.isArray(pathItemValue.parameters) ? pathItemValue.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
740
|
-
for (const method of HTTP_METHODS) {
|
|
741
|
-
const operationValue = pathItemValue[method];
|
|
742
|
-
if (!isObject(operationValue)) continue;
|
|
743
|
-
const firstTag = Array.isArray(operationValue.tags) && operationValue.tags.length ? String(operationValue.tags[0]) : "General";
|
|
744
|
-
result.push({ method, path: path2, operation: operationValue, pathParameters, tag: firstTag });
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
return result;
|
|
748
|
-
}
|
|
749
|
-
function getServers(spec) {
|
|
750
|
-
if (Array.isArray(spec.servers) && spec.servers.length) {
|
|
751
|
-
return spec.servers.map((server) => cleanText(server?.url)).filter(Boolean);
|
|
752
|
-
}
|
|
753
|
-
if (isObject(spec)) {
|
|
754
|
-
const host = cleanText(spec.host);
|
|
755
|
-
if (host) {
|
|
756
|
-
const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
|
|
757
|
-
const basePath = cleanText(spec.basePath) || "/";
|
|
758
|
-
return schemes.map(
|
|
759
|
-
(scheme) => `${scheme}://${host}${basePath.startsWith("/") ? basePath : `/${basePath}`}`
|
|
760
|
-
);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return [];
|
|
764
|
-
}
|
|
765
|
-
function describeSecurityScheme(name, scheme) {
|
|
766
|
-
const type = cleanText(scheme.type).toLowerCase();
|
|
767
|
-
const label = cleanText(scheme.description);
|
|
768
|
-
let details = name;
|
|
769
|
-
if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "bearer")
|
|
770
|
-
details += " (Bearer token via Authorization header)";
|
|
771
|
-
else if (type === "http" && cleanText(scheme.scheme).toLowerCase() === "basic")
|
|
772
|
-
details += " (HTTP Basic via Authorization header)";
|
|
773
|
-
else if (type === "apikey" || type === "apiKey")
|
|
774
|
-
details += ` (API key in ${cleanText(scheme.in) || "header"}: ${cleanText(scheme.name) || "key"})`;
|
|
775
|
-
else if (type === "oauth2") {
|
|
776
|
-
const flows = entries(scheme.flows).map(([flowName]) => flowName).join(", ") || cleanText(scheme.flow);
|
|
777
|
-
details += ` (OAuth2${flows ? `; ${flows}` : ""})`;
|
|
778
|
-
} else if (type === "openidconnect" || type === "openIdConnect") details += " (OpenID Connect)";
|
|
779
|
-
else if (type === "basic") details += " (HTTP Basic)";
|
|
780
|
-
else if (type) details += ` (${type})`;
|
|
781
|
-
if (label) details += ` \u2014 ${label}`;
|
|
782
|
-
return details;
|
|
783
|
-
}
|
|
784
|
-
function getSecuritySchemes(spec) {
|
|
785
|
-
const dictionary = spec.components?.securitySchemes ?? spec.securityDefinitions ?? {};
|
|
786
|
-
return entries(dictionary).filter((entry) => isObject(entry[1]));
|
|
787
|
-
}
|
|
788
|
-
function securityRequirement(spec, operation) {
|
|
789
|
-
const security = operation.security !== void 0 ? operation.security : spec.security;
|
|
790
|
-
if (!Array.isArray(security) || security.length === 0) return "None explicitly required";
|
|
791
|
-
const allSchemes = new Map(getSecuritySchemes(spec));
|
|
792
|
-
return security.map((requirement) => {
|
|
793
|
-
if (!isObject(requirement)) return "";
|
|
794
|
-
const names = Object.entries(requirement).map(([name, scopes]) => {
|
|
795
|
-
const label = allSchemes.has(name) ? name : `${name} (scheme not defined)`;
|
|
796
|
-
return Array.isArray(scopes) && scopes.length ? `${label} [scopes: ${scopes.join(", ")}]` : label;
|
|
797
|
-
});
|
|
798
|
-
return names.length ? names.join(" + ") : "No auth";
|
|
799
|
-
}).filter(Boolean).join(" OR ");
|
|
800
|
-
}
|
|
801
|
-
function mergedParameters(spec, endpoint) {
|
|
802
|
-
const operationParameters = Array.isArray(endpoint.operation.parameters) ? endpoint.operation.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
803
|
-
const byIdentity = /* @__PURE__ */ new Map();
|
|
804
|
-
for (const parameter of [...endpoint.pathParameters, ...operationParameters]) {
|
|
805
|
-
if (parameter.in === "body") continue;
|
|
806
|
-
const key = `${cleanText(parameter.in)}:${cleanText(parameter.name)}`;
|
|
807
|
-
if (key !== ":") byIdentity.set(key, parameter);
|
|
808
|
-
}
|
|
809
|
-
return [...byIdentity.values()];
|
|
810
|
-
}
|
|
811
|
-
function parameterType(parameter) {
|
|
812
|
-
if (isObject(parameter.schema)) return typeExpression(parameter.schema);
|
|
813
|
-
if (parameter.type === "array") return `${typeExpression(parameter.items)}[]`;
|
|
814
|
-
if (parameter.type) return typeExpression({ type: parameter.type, format: parameter.format, enum: parameter.enum });
|
|
815
|
-
return "unknown";
|
|
816
|
-
}
|
|
817
|
-
function parameterDisplayType(parameter) {
|
|
818
|
-
return parameterType(parameter).replace(/\/\*\s*([^*]+?)\s*\*\//g, "($1)");
|
|
819
|
-
}
|
|
820
|
-
function parameterDefault(parameter) {
|
|
821
|
-
if (parameter.default !== void 0) return parameter.default;
|
|
822
|
-
if (isObject(parameter.schema)) return parameter.schema.default;
|
|
823
|
-
return void 0;
|
|
824
|
-
}
|
|
825
|
-
function isMeaningfulDefault(value) {
|
|
826
|
-
return value !== void 0 && value !== null && value !== "";
|
|
827
|
-
}
|
|
828
|
-
function parameterList(parameters) {
|
|
829
|
-
if (!parameters.length) return "_None._";
|
|
830
|
-
return parameters.map((parameter) => {
|
|
831
|
-
const required = parameter.required === true || parameter.in === "path";
|
|
832
|
-
const type = parameterDisplayType(parameter);
|
|
833
|
-
const defaultValue = parameterDefault(parameter);
|
|
834
|
-
const defaultSuffix = isMeaningfulDefault(defaultValue) ? ` (default: ${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : String(defaultValue)})` : "";
|
|
835
|
-
const description = cleanText(parameter.description);
|
|
836
|
-
const details = description ? ` \u2014 ${description}` : "";
|
|
837
|
-
return `- \`${cleanText(parameter.name)}\` (\`${cleanText(parameter.in)}\`${required ? ", required" : ""}) \xB7 \`${type}\`${defaultSuffix}${details}`;
|
|
838
|
-
}).join("\n");
|
|
839
|
-
}
|
|
1302
|
+
|
|
1303
|
+
// src/transformer/examples.ts
|
|
840
1304
|
function chooseMediaType(content) {
|
|
841
1305
|
const mediaTypes = Object.keys(content);
|
|
842
1306
|
return mediaTypes.find((type) => type.toLowerCase() === "application/json") ?? mediaTypes.find((type) => type.toLowerCase().includes("+json")) ?? mediaTypes[0];
|
|
@@ -849,7 +1313,7 @@ function firstDeclaredExample(spec, schemaValue, seen = /* @__PURE__ */ new Set(
|
|
|
849
1313
|
let schema = schemaValue;
|
|
850
1314
|
if (typeof schema.$ref === "string") {
|
|
851
1315
|
if (seen.has(schema.$ref)) return void 0;
|
|
852
|
-
const target =
|
|
1316
|
+
const target = resolvePointer2(spec, schema.$ref);
|
|
853
1317
|
if (!isObject(target)) return void 0;
|
|
854
1318
|
const nextSeen = new Set(seen).add(schema.$ref);
|
|
855
1319
|
schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
|
|
@@ -866,7 +1330,7 @@ function generateSchemaExample(spec, schemaValue, depth = 0, seenRefs = /* @__PU
|
|
|
866
1330
|
let nextSeen = seenRefs;
|
|
867
1331
|
if (typeof schema.$ref === "string") {
|
|
868
1332
|
if (seenRefs.has(schema.$ref)) return null;
|
|
869
|
-
const target =
|
|
1333
|
+
const target = resolvePointer2(spec, schema.$ref);
|
|
870
1334
|
if (!isObject(target)) return void 0;
|
|
871
1335
|
nextSeen = new Set(seenRefs).add(schema.$ref);
|
|
872
1336
|
schema = { ...target, ...Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref")) };
|
|
@@ -959,12 +1423,63 @@ function payloadExample(spec, mediaValue, schemaValue, legacyExamples) {
|
|
|
959
1423
|
return { value: legacyExamples[mediaType], origin: "declared" };
|
|
960
1424
|
}
|
|
961
1425
|
}
|
|
962
|
-
const declaredSchemaExample = firstDeclaredExample(spec, schemaValue);
|
|
963
|
-
if (declaredSchemaExample !== void 0) return { value: declaredSchemaExample, origin: "declared" };
|
|
964
|
-
const generated = generateSchemaExample(spec, schemaValue);
|
|
965
|
-
return generated === void 0 ? void 0 : { value: generated, origin: "schema-derived" };
|
|
1426
|
+
const declaredSchemaExample = firstDeclaredExample(spec, schemaValue);
|
|
1427
|
+
if (declaredSchemaExample !== void 0) return { value: declaredSchemaExample, origin: "declared" };
|
|
1428
|
+
const generated = generateSchemaExample(spec, schemaValue);
|
|
1429
|
+
return generated === void 0 ? void 0 : { value: generated, origin: "schema-derived" };
|
|
1430
|
+
}
|
|
1431
|
+
function formattedExample(value) {
|
|
1432
|
+
try {
|
|
1433
|
+
return JSON.stringify(value, null, 2) ?? "null";
|
|
1434
|
+
} catch {
|
|
1435
|
+
return "null";
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/transformer/parameters.ts
|
|
1440
|
+
function mergedParameters(spec, endpoint) {
|
|
1441
|
+
const operationParameters = Array.isArray(endpoint.operation.parameters) ? endpoint.operation.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
1442
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
1443
|
+
for (const parameter of [...endpoint.pathParameters, ...operationParameters]) {
|
|
1444
|
+
if (parameter.in === "body") continue;
|
|
1445
|
+
const key = `${cleanText(parameter.in)}:${cleanText(parameter.name)}`;
|
|
1446
|
+
if (key !== ":") byIdentity.set(key, parameter);
|
|
1447
|
+
}
|
|
1448
|
+
return [...byIdentity.values()];
|
|
1449
|
+
}
|
|
1450
|
+
function parameterType(parameter) {
|
|
1451
|
+
if (isObject(parameter.schema)) return typeExpression(parameter.schema);
|
|
1452
|
+
if (parameter.type === "array") return `${typeExpression(parameter.items)}[]`;
|
|
1453
|
+
if (parameter.type) return typeExpression({ type: parameter.type, format: parameter.format, enum: parameter.enum });
|
|
1454
|
+
return "unknown";
|
|
1455
|
+
}
|
|
1456
|
+
function parameterDisplayType(parameter) {
|
|
1457
|
+
return parameterType(parameter).replace(/\/\*\s*([^*]+?)\s*\*\//g, "($1)");
|
|
1458
|
+
}
|
|
1459
|
+
function parameterDefault(parameter) {
|
|
1460
|
+
if (parameter.default !== void 0) return parameter.default;
|
|
1461
|
+
if (isObject(parameter.schema)) return parameter.schema.default;
|
|
1462
|
+
return void 0;
|
|
1463
|
+
}
|
|
1464
|
+
function isMeaningfulDefault(value) {
|
|
1465
|
+
return value !== void 0 && value !== null && value !== "";
|
|
1466
|
+
}
|
|
1467
|
+
function parameterList(parameters, style = FULL_STYLE) {
|
|
1468
|
+
if (!parameters.length) return "_None._";
|
|
1469
|
+
if (style.collapsed) {
|
|
1470
|
+
return `- _${parameters.length} parameter${parameters.length === 1 ? "" : "s"} (detail collapsed for token budget)._`;
|
|
1471
|
+
}
|
|
1472
|
+
return parameters.map((parameter) => {
|
|
1473
|
+
const required = parameter.required === true || parameter.in === "path";
|
|
1474
|
+
const type = parameterDisplayType(parameter);
|
|
1475
|
+
const defaultValue = parameterDefault(parameter);
|
|
1476
|
+
const defaultSuffix = isMeaningfulDefault(defaultValue) ? ` (default: ${typeof defaultValue === "string" ? JSON.stringify(defaultValue) : String(defaultValue)})` : "";
|
|
1477
|
+
const description = shortenDescription(cleanText(parameter.description), style.descriptionLimit);
|
|
1478
|
+
const details = description ? ` \u2014 ${description}` : "";
|
|
1479
|
+
return `- \`${cleanText(parameter.name)}\` (\`${cleanText(parameter.in)}\`${required ? ", required" : ""}) \xB7 \`${type}\`${defaultSuffix}${details}`;
|
|
1480
|
+
}).join("\n");
|
|
966
1481
|
}
|
|
967
|
-
function requestPayloadDescription(spec, operation) {
|
|
1482
|
+
function requestPayloadDescription(spec, operation, style = FULL_STYLE) {
|
|
968
1483
|
if (isObject(operation.requestBody) || operation.requestBody?.$ref) {
|
|
969
1484
|
const body = resolveReferenceObject(spec, operation.requestBody);
|
|
970
1485
|
const content = isObject(body.content) ? body.content : {};
|
|
@@ -974,7 +1489,7 @@ function requestPayloadDescription(spec, operation) {
|
|
|
974
1489
|
return {
|
|
975
1490
|
type: schema ? typeExpression(schema) : "unknown",
|
|
976
1491
|
mediaType: mediaType || "unknown media type",
|
|
977
|
-
example: media || schema ? payloadExample(spec, media, schema) : void 0,
|
|
1492
|
+
example: style.includeExamples && (media || schema) ? payloadExample(spec, media, schema) : void 0,
|
|
978
1493
|
required: body.required === true
|
|
979
1494
|
};
|
|
980
1495
|
}
|
|
@@ -985,13 +1500,31 @@ function requestPayloadDescription(spec, operation) {
|
|
|
985
1500
|
return {
|
|
986
1501
|
type: typeExpression(bodyParameter.schema),
|
|
987
1502
|
mediaType: consumes[0] ?? "application/json",
|
|
988
|
-
example: payloadExample(spec, void 0, bodyParameter.schema),
|
|
1503
|
+
example: style.includeExamples ? payloadExample(spec, void 0, bodyParameter.schema) : void 0,
|
|
989
1504
|
required: bodyParameter.required === true
|
|
990
1505
|
};
|
|
991
1506
|
}
|
|
992
1507
|
return void 0;
|
|
993
1508
|
}
|
|
994
|
-
|
|
1509
|
+
|
|
1510
|
+
// src/transformer/responses.ts
|
|
1511
|
+
var COMMON_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1512
|
+
"400",
|
|
1513
|
+
"401",
|
|
1514
|
+
"403",
|
|
1515
|
+
"404",
|
|
1516
|
+
"405",
|
|
1517
|
+
"409",
|
|
1518
|
+
"415",
|
|
1519
|
+
"422",
|
|
1520
|
+
"429",
|
|
1521
|
+
"500",
|
|
1522
|
+
"501",
|
|
1523
|
+
"502",
|
|
1524
|
+
"503",
|
|
1525
|
+
"default"
|
|
1526
|
+
]);
|
|
1527
|
+
function responseSchema(spec, responseValue, style = FULL_STYLE) {
|
|
995
1528
|
const response = resolveReferenceObject(spec, responseValue);
|
|
996
1529
|
if (isObject(response.content)) {
|
|
997
1530
|
const mediaType = chooseMediaType(response.content);
|
|
@@ -1000,30 +1533,26 @@ function responseSchema(spec, responseValue) {
|
|
|
1000
1533
|
return {
|
|
1001
1534
|
type: schema ? typeExpression(schema) : void 0,
|
|
1002
1535
|
mediaType,
|
|
1003
|
-
example: schema || media ? payloadExample(spec, media, schema, response.examples) : void 0
|
|
1536
|
+
example: style.includeExamples && (schema || media) ? payloadExample(spec, media, schema, response.examples) : void 0
|
|
1004
1537
|
};
|
|
1005
1538
|
}
|
|
1006
1539
|
if (isObject(response.schema)) {
|
|
1007
1540
|
return {
|
|
1008
1541
|
type: typeExpression(response.schema),
|
|
1009
1542
|
mediaType: "application/json",
|
|
1010
|
-
example: payloadExample(spec, void 0, response.schema, response.examples)
|
|
1543
|
+
example: style.includeExamples ? payloadExample(spec, void 0, response.schema, response.examples) : void 0
|
|
1011
1544
|
};
|
|
1012
1545
|
}
|
|
1013
1546
|
return {};
|
|
1014
1547
|
}
|
|
1015
|
-
function
|
|
1016
|
-
try {
|
|
1017
|
-
return JSON.stringify(value, null, 2) ?? "null";
|
|
1018
|
-
} catch {
|
|
1019
|
-
return "null";
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
function responseBlock(spec, code, responseValue) {
|
|
1548
|
+
function responseBlock(spec, code, responseValue, style = FULL_STYLE) {
|
|
1023
1549
|
const response = resolveReferenceObject(spec, responseValue);
|
|
1024
|
-
const description = cleanText(response.description);
|
|
1025
|
-
const payload = responseSchema(spec, responseValue);
|
|
1550
|
+
const description = style.collapsed ? "" : shortenDescription(cleanText(response.description), style.descriptionLimit);
|
|
1551
|
+
const payload = responseSchema(spec, responseValue, style);
|
|
1026
1552
|
const suffix = payload.type ? ` \u2014 \`${payload.type}\`${payload.mediaType ? ` (${payload.mediaType})` : ""}` : " \u2014 no body schema declared";
|
|
1553
|
+
if (style.collapsed) {
|
|
1554
|
+
return [`- **${markdownCell(code)}**${suffix}`].join("\n");
|
|
1555
|
+
}
|
|
1027
1556
|
const lines = [`- **${markdownCell(code)}${description ? ` ${description}` : ""}**${suffix}`];
|
|
1028
1557
|
if (payload.example?.origin === "declared") {
|
|
1029
1558
|
const label = payload.example.name ? ` \xB7 ${payload.example.name}` : "";
|
|
@@ -1088,7 +1617,7 @@ function collapsibleErrorEntry(spec, code, responseValue) {
|
|
|
1088
1617
|
if (schemaValue === void 0) return `\`${code}\``;
|
|
1089
1618
|
return `\`${code}\` (\`${typeExpression(schemaValue)}\`)`;
|
|
1090
1619
|
}
|
|
1091
|
-
function responseSection(spec, operation) {
|
|
1620
|
+
function responseSection(spec, operation, style = FULL_STYLE) {
|
|
1092
1621
|
const responses = entries(operation.responses).filter(([, value]) => isObject(value));
|
|
1093
1622
|
if (!responses.length) return "_No responses declared._";
|
|
1094
1623
|
const success = responses.filter(([code]) => /^2\d\d$/.test(code));
|
|
@@ -1096,42 +1625,96 @@ function responseSection(spec, operation) {
|
|
|
1096
1625
|
const errors = responses.filter(
|
|
1097
1626
|
([code]) => (COMMON_ERROR_CODES.has(code) || /^([45])\d\d$/.test(code)) && !success.some(([successCode]) => successCode === code)
|
|
1098
1627
|
);
|
|
1628
|
+
if (style.collapsed) {
|
|
1629
|
+
const collapsedLines = success.map(([code, response]) => responseBlock(spec, code, response, style));
|
|
1630
|
+
collapsedLines.push(...errors.map(([code, responseValue]) => responseBlock(spec, code, responseValue, style)));
|
|
1631
|
+
return collapsedLines.join("\n");
|
|
1632
|
+
}
|
|
1099
1633
|
const detailedErrors = [];
|
|
1100
1634
|
const collapsedErrors = [];
|
|
1101
1635
|
for (const [code, responseValue] of errors) {
|
|
1102
1636
|
const entry = collapsibleErrorEntry(spec, code, responseValue);
|
|
1103
|
-
if (entry === void 0) detailedErrors.push(responseBlock(spec, code, responseValue));
|
|
1637
|
+
if (entry === void 0) detailedErrors.push(responseBlock(spec, code, responseValue, style));
|
|
1104
1638
|
else collapsedErrors.push(entry);
|
|
1105
1639
|
}
|
|
1106
1640
|
const primarySuccess = success[0];
|
|
1107
|
-
const lines = primarySuccess ? [responseBlock(spec, primarySuccess[0], primarySuccess[1])] : [];
|
|
1641
|
+
const lines = primarySuccess ? [responseBlock(spec, primarySuccess[0], primarySuccess[1], style)] : [];
|
|
1108
1642
|
lines.push(...detailedErrors);
|
|
1109
1643
|
if (collapsedErrors.length) lines.push(`- **Errors:** ${collapsedErrors.join(", ")}`);
|
|
1110
1644
|
if (success.length > 1) {
|
|
1111
|
-
const others = success.slice(1).map(([code, response]) => responseBlock(spec, code, response));
|
|
1645
|
+
const others = success.slice(1).map(([code, response]) => responseBlock(spec, code, response, style));
|
|
1112
1646
|
lines.push("", "<details><summary>Other successful responses</summary>", "", ...others, "", "</details>");
|
|
1113
1647
|
}
|
|
1114
1648
|
if (!lines.length) {
|
|
1115
|
-
return responses.map(([code, response]) => responseBlock(spec, code, response)).join("\n\n");
|
|
1649
|
+
return responses.map(([code, response]) => responseBlock(spec, code, response, style)).join("\n\n");
|
|
1116
1650
|
}
|
|
1117
1651
|
return lines.join("\n");
|
|
1118
1652
|
}
|
|
1653
|
+
|
|
1654
|
+
// src/transformer/endpoints.ts
|
|
1655
|
+
var HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
1656
|
+
function getEndpoints(spec) {
|
|
1657
|
+
const result = [];
|
|
1658
|
+
const paths = isObject(spec.paths) ? spec.paths : {};
|
|
1659
|
+
for (const [path2, pathItemValue] of Object.entries(paths)) {
|
|
1660
|
+
if (!isObject(pathItemValue)) continue;
|
|
1661
|
+
const pathParameters = Array.isArray(pathItemValue.parameters) ? pathItemValue.parameters.map((parameter) => resolveReferenceObject(spec, parameter)) : [];
|
|
1662
|
+
for (const method of HTTP_METHODS) {
|
|
1663
|
+
const operationValue = pathItemValue[method];
|
|
1664
|
+
if (!isObject(operationValue)) continue;
|
|
1665
|
+
const firstTag = Array.isArray(operationValue.tags) && operationValue.tags.length ? String(operationValue.tags[0]) : "General";
|
|
1666
|
+
result.push({ method, path: path2, operation: operationValue, pathParameters, tag: firstTag });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return result;
|
|
1670
|
+
}
|
|
1671
|
+
function getServers(spec) {
|
|
1672
|
+
if (Array.isArray(spec.servers) && spec.servers.length) {
|
|
1673
|
+
return spec.servers.map((server) => cleanText(isObject(server) ? server.url : void 0)).filter(Boolean);
|
|
1674
|
+
}
|
|
1675
|
+
if (isObject(spec)) {
|
|
1676
|
+
const host = cleanText(spec.host);
|
|
1677
|
+
if (host) {
|
|
1678
|
+
const schemes = Array.isArray(spec.schemes) && spec.schemes.length ? spec.schemes : ["https"];
|
|
1679
|
+
const basePath = cleanText(spec.basePath) || "/";
|
|
1680
|
+
return schemes.map(
|
|
1681
|
+
(scheme) => `${scheme}://${host}${basePath.startsWith("/") ? basePath : `/${basePath}`}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return [];
|
|
1686
|
+
}
|
|
1687
|
+
function collectTags(spec, endpoints) {
|
|
1688
|
+
const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => cleanText(isObject(tag) ? tag.name : void 0)).filter(Boolean) : [];
|
|
1689
|
+
const encountered = [...new Set(endpoints.map((endpoint) => endpoint.tag))];
|
|
1690
|
+
return [
|
|
1691
|
+
...declared.filter((tag) => encountered.includes(tag)),
|
|
1692
|
+
...encountered.filter((tag) => !declared.includes(tag))
|
|
1693
|
+
];
|
|
1694
|
+
}
|
|
1695
|
+
function endpointTypeBase(method, path2, operation) {
|
|
1696
|
+
if (cleanText(operation.operationId)) return toTypeScriptIdentifier(cleanText(operation.operationId));
|
|
1697
|
+
const pathName = path2.replace(/[{}]/g, "").split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1698
|
+
return toTypeScriptIdentifier(`${method.charAt(0).toUpperCase()}${method.slice(1)}${pathName}`);
|
|
1699
|
+
}
|
|
1119
1700
|
function shortOperationDescription(operation) {
|
|
1120
1701
|
return markdownCell(operation.summary || operation.operationId || operation.description || "\u2014");
|
|
1121
1702
|
}
|
|
1122
|
-
function endpointMarkdown(spec, endpoint) {
|
|
1703
|
+
function endpointMarkdown(spec, endpoint, style = FULL_STYLE) {
|
|
1123
1704
|
const { operation, method, path: path2 } = endpoint;
|
|
1124
1705
|
const heading = `#### \`${method.toUpperCase()} ${path2}\``;
|
|
1125
1706
|
const summary = cleanText(operation.summary);
|
|
1126
1707
|
const description = cleanText(operation.description);
|
|
1127
1708
|
const lines = [heading];
|
|
1128
|
-
if (summary) lines.push("", summary);
|
|
1129
|
-
if (description && description !== summary)
|
|
1709
|
+
if (summary) lines.push("", shortenDescription(summary, style.descriptionLimit));
|
|
1710
|
+
if (description && description !== summary) {
|
|
1711
|
+
lines.push("", shortenDescription(description, style.descriptionLimit));
|
|
1712
|
+
}
|
|
1130
1713
|
lines.push("", `**Authentication:** ${securityRequirement(spec, operation)}`);
|
|
1131
1714
|
const parameters = mergedParameters(spec, endpoint);
|
|
1132
|
-
const request = requestPayloadDescription(spec, operation);
|
|
1715
|
+
const request = requestPayloadDescription(spec, operation, style);
|
|
1133
1716
|
lines.push("", "**Parameters**");
|
|
1134
|
-
lines.push(parameterList(parameters));
|
|
1717
|
+
lines.push(parameterList(parameters, style));
|
|
1135
1718
|
if (request) {
|
|
1136
1719
|
lines.push(
|
|
1137
1720
|
"",
|
|
@@ -1149,31 +1732,83 @@ function endpointMarkdown(spec, endpoint) {
|
|
|
1149
1732
|
);
|
|
1150
1733
|
}
|
|
1151
1734
|
}
|
|
1152
|
-
lines.push("", "**Responses**", responseSection(spec, operation));
|
|
1735
|
+
lines.push("", "**Responses**", responseSection(spec, operation, style));
|
|
1153
1736
|
return lines.join("\n");
|
|
1154
1737
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1738
|
+
|
|
1739
|
+
// src/transformer/models.ts
|
|
1740
|
+
function collectSchemaRefs(value, output, root, seen = /* @__PURE__ */ new Set(), followedRefs = /* @__PURE__ */ new Set()) {
|
|
1741
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
1742
|
+
seen.add(value);
|
|
1743
|
+
if (Array.isArray(value)) {
|
|
1744
|
+
for (const item of value) collectSchemaRefs(item, output, root, seen, followedRefs);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const object = value;
|
|
1748
|
+
if (typeof object.$ref === "string") {
|
|
1749
|
+
if (isSchemaRef(object.$ref)) {
|
|
1750
|
+
const name = refName(object.$ref);
|
|
1751
|
+
if (name) output.add(name);
|
|
1752
|
+
} else if (root && object.$ref.startsWith("#/") && !followedRefs.has(object.$ref)) {
|
|
1753
|
+
followedRefs.add(object.$ref);
|
|
1754
|
+
const target = resolvePointer2(root, object.$ref);
|
|
1755
|
+
if (target !== void 0) collectSchemaRefs(target, output, root, seen, followedRefs);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
for (const [key, child] of Object.entries(object)) {
|
|
1759
|
+
if (key !== "example" && key !== "examples" && key !== "externalDocs")
|
|
1760
|
+
collectSchemaRefs(child, output, root, seen, followedRefs);
|
|
1761
|
+
}
|
|
1164
1762
|
}
|
|
1165
|
-
function
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1763
|
+
function referencedSchemas(spec) {
|
|
1764
|
+
const dictionary = getSchemaDictionary(spec);
|
|
1765
|
+
const knownByName = /* @__PURE__ */ new Map();
|
|
1766
|
+
for (const [name, schema] of Object.entries(dictionary)) {
|
|
1767
|
+
knownByName.set(toTypeScriptIdentifier(name), [name, schema]);
|
|
1768
|
+
}
|
|
1769
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1770
|
+
collectSchemaRefs(spec.paths ?? {}, refs, spec);
|
|
1771
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1772
|
+
const queue = [...refs];
|
|
1773
|
+
while (queue.length) {
|
|
1774
|
+
const name = queue.shift();
|
|
1775
|
+
if (!name || selected.has(name)) continue;
|
|
1776
|
+
selected.add(name);
|
|
1777
|
+
const model = knownByName.get(name);
|
|
1778
|
+
if (!model) continue;
|
|
1779
|
+
const childRefs = /* @__PURE__ */ new Set();
|
|
1780
|
+
collectSchemaRefs(model[1], childRefs, spec);
|
|
1781
|
+
for (const child of childRefs) if (!selected.has(child)) queue.push(child);
|
|
1782
|
+
}
|
|
1783
|
+
return [...selected].map((name) => knownByName.get(name)).filter((entry) => Boolean(entry));
|
|
1172
1784
|
}
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1785
|
+
function modelDeclaration(name, schema) {
|
|
1786
|
+
const identifier = toTypeScriptIdentifier(name);
|
|
1787
|
+
const description = safeComment(schema.description);
|
|
1788
|
+
const comment = description ? `/** ${description} */
|
|
1789
|
+
` : "";
|
|
1790
|
+
if (schema.$ref) return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
1791
|
+
if (Array.isArray(schema.enum) || schema.const !== void 0 || schema.oneOf || schema.anyOf || schema.allOf) {
|
|
1792
|
+
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
1793
|
+
}
|
|
1794
|
+
const kind = Array.isArray(schema.type) ? schema.type.find((item) => item !== "null") : schema.type;
|
|
1795
|
+
if (kind === "object" || schema.properties || schema.additionalProperties !== void 0) {
|
|
1796
|
+
const properties = entries(schema.properties);
|
|
1797
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1798
|
+
const fields = properties.map(([field, value]) => {
|
|
1799
|
+
const metadata = schemaMetadata(value).map(safeComment).filter(Boolean).join("; ");
|
|
1800
|
+
const doc = metadata ? ` /** ${metadata} */
|
|
1801
|
+
` : "";
|
|
1802
|
+
return `${doc} ${propertyKey(field)}${required.has(field) ? "" : "?"}: ${typeExpression(value)};`;
|
|
1803
|
+
});
|
|
1804
|
+
if (schema.additionalProperties === true) fields.push(" [key: string]: unknown;");
|
|
1805
|
+
else if (isObject(schema.additionalProperties))
|
|
1806
|
+
fields.push(` [key: string]: ${typeExpression(schema.additionalProperties)};`);
|
|
1807
|
+
return `${comment}export interface ${identifier} {
|
|
1808
|
+
${fields.join("\n")}
|
|
1809
|
+
}`;
|
|
1810
|
+
}
|
|
1811
|
+
return `${comment}export type ${identifier} = ${typeExpression(schema)};`;
|
|
1177
1812
|
}
|
|
1178
1813
|
function normalizeInlinePayloadSchemas(spec) {
|
|
1179
1814
|
const document = structuredClone(spec);
|
|
@@ -1267,26 +1902,123 @@ function normalizeInlinePayloadSchemas(spec) {
|
|
|
1267
1902
|
}
|
|
1268
1903
|
return document;
|
|
1269
1904
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1905
|
+
|
|
1906
|
+
// src/transformer/index.ts
|
|
1907
|
+
var HTTP_METHODS2 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
1908
|
+
function tagsOfEndpoint(endpoint) {
|
|
1909
|
+
const tags = endpoint.operation.tags;
|
|
1910
|
+
return Array.isArray(tags) && tags.length ? tags.map(String) : ["General"];
|
|
1911
|
+
}
|
|
1912
|
+
function filterSpec(spec, endpoints, options, warnings) {
|
|
1913
|
+
const includeTags = options.includeTags ?? [];
|
|
1914
|
+
const excludeTags = options.excludeTags ?? [];
|
|
1915
|
+
const hasFilter = includeTags.length > 0 || excludeTags.length > 0 || options.pathPrefix !== void 0;
|
|
1916
|
+
if (!hasFilter) return spec;
|
|
1917
|
+
const declared = Array.isArray(spec.tags) ? spec.tags.map((tag) => isObject(tag) && typeof tag.name === "string" ? tag.name : "").filter(Boolean) : [];
|
|
1918
|
+
const available = [.../* @__PURE__ */ new Set([...declared, ...endpoints.map((endpoint) => endpoint.tag)])];
|
|
1919
|
+
const unknownTags = [
|
|
1920
|
+
...includeTags.filter((tag) => !available.includes(tag)),
|
|
1921
|
+
...excludeTags.filter((tag) => !available.includes(tag))
|
|
1922
|
+
];
|
|
1923
|
+
if (unknownTags.length > 0) {
|
|
1924
|
+
warnings.push(
|
|
1925
|
+
`Unknown tag(s): ${[...new Set(unknownTags)].join(", ")}. Available tags: ${available.join(", ") || "none"}.`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
const keep = (endpoint) => {
|
|
1929
|
+
if (options.pathPrefix && !endpoint.path.startsWith(options.pathPrefix)) return false;
|
|
1930
|
+
const tags = tagsOfEndpoint(endpoint);
|
|
1931
|
+
if (includeTags.length > 0 && !tags.some((tag) => includeTags.includes(tag))) return false;
|
|
1932
|
+
if (excludeTags.length > 0 && tags.some((tag) => excludeTags.includes(tag))) return false;
|
|
1933
|
+
return true;
|
|
1934
|
+
};
|
|
1935
|
+
const paths = {};
|
|
1936
|
+
for (const endpoint of endpoints.filter(keep)) {
|
|
1937
|
+
const source = isObject(spec.paths?.[endpoint.path]) ? spec.paths[endpoint.path] : {};
|
|
1938
|
+
const item = paths[endpoint.path] ?? { ...source };
|
|
1939
|
+
for (const method of HTTP_METHODS2) delete item[method];
|
|
1940
|
+
item[endpoint.method] = source[endpoint.method];
|
|
1941
|
+
paths[endpoint.path] = item;
|
|
1942
|
+
}
|
|
1943
|
+
return { ...spec, paths };
|
|
1944
|
+
}
|
|
1945
|
+
function buildTrimStages(_endpoints, tags) {
|
|
1946
|
+
const stages = [];
|
|
1947
|
+
const emptyTags = /* @__PURE__ */ new Set();
|
|
1948
|
+
stages.push({
|
|
1949
|
+
includeExamples: true,
|
|
1950
|
+
collapsedTags: emptyTags,
|
|
1951
|
+
indexOnly: false
|
|
1952
|
+
});
|
|
1953
|
+
stages.push({
|
|
1954
|
+
includeExamples: false,
|
|
1955
|
+
collapsedTags: emptyTags,
|
|
1956
|
+
indexOnly: false,
|
|
1957
|
+
action: "removed declared example payloads"
|
|
1958
|
+
});
|
|
1959
|
+
stages.push({
|
|
1960
|
+
includeExamples: false,
|
|
1961
|
+
descriptionLimit: 200,
|
|
1962
|
+
collapsedTags: emptyTags,
|
|
1963
|
+
indexOnly: false,
|
|
1964
|
+
action: "shortened long descriptions"
|
|
1965
|
+
});
|
|
1966
|
+
const collapsedSoFar = /* @__PURE__ */ new Set();
|
|
1967
|
+
for (const tag of [...tags].reverse()) {
|
|
1968
|
+
collapsedSoFar.add(tag);
|
|
1969
|
+
stages.push({
|
|
1970
|
+
includeExamples: false,
|
|
1971
|
+
descriptionLimit: 200,
|
|
1972
|
+
collapsedTags: new Set(collapsedSoFar),
|
|
1973
|
+
indexOnly: false,
|
|
1974
|
+
action: `collapsed parameter and response detail for tag "${tag}"`
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
stages.push({
|
|
1978
|
+
includeExamples: false,
|
|
1979
|
+
descriptionLimit: 200,
|
|
1980
|
+
collapsedTags: new Set(tags),
|
|
1981
|
+
indexOnly: true,
|
|
1982
|
+
action: "kept only the Quick index entries for the remaining tags"
|
|
1983
|
+
});
|
|
1984
|
+
return stages;
|
|
1985
|
+
}
|
|
1986
|
+
function renderDigest(spec, sourceUrl, endpoints, tags, stage) {
|
|
1273
1987
|
const info = isObject(spec.info) ? spec.info : {};
|
|
1274
1988
|
const title = cleanText(info.title) || "API";
|
|
1275
1989
|
const version = cleanText(info.version) || "unspecified";
|
|
1276
1990
|
const openApiVersion = cleanText(spec.openapi) || (spec.swagger ? `Swagger ${spec.swagger}` : "OpenAPI");
|
|
1277
1991
|
const servers = getServers(spec);
|
|
1278
|
-
const endpoints = getEndpoints(spec);
|
|
1279
|
-
const tags = collectTags(spec, endpoints);
|
|
1280
1992
|
const indexRows = endpoints.map(
|
|
1281
1993
|
(endpoint) => `| **${endpoint.method.toUpperCase()}** | \`${markdownCell(endpoint.path)}\` | ${shortOperationDescription(endpoint.operation)} | ${markdownCell(Array.isArray(endpoint.operation.tags) ? endpoint.operation.tags.join(", ") : endpoint.tag)} |`
|
|
1282
1994
|
);
|
|
1995
|
+
const detailEndpoints = stage.indexOnly ? [] : endpoints.filter((endpoint) => !stage.collapsedTags.has(endpoint.tag));
|
|
1283
1996
|
const groupedEndpoints = tags.map((tag) => {
|
|
1284
1997
|
const members = endpoints.filter((endpoint) => endpoint.tag === tag);
|
|
1285
1998
|
if (!members.length) return "";
|
|
1286
|
-
|
|
1999
|
+
const rendered = members.map(
|
|
2000
|
+
(endpoint) => endpointMarkdown(spec, endpoint, {
|
|
2001
|
+
includeExamples: stage.includeExamples,
|
|
2002
|
+
descriptionLimit: stage.descriptionLimit,
|
|
2003
|
+
collapsed: stage.collapsedTags.has(endpoint.tag)
|
|
2004
|
+
})
|
|
2005
|
+
);
|
|
2006
|
+
return [`### ${tag}`, ...rendered].join("\n\n");
|
|
1287
2007
|
}).filter(Boolean);
|
|
1288
|
-
|
|
1289
|
-
|
|
2008
|
+
let modelSpec = spec;
|
|
2009
|
+
if (detailEndpoints.length !== endpoints.length) {
|
|
2010
|
+
const scopedPaths = {};
|
|
2011
|
+
for (const endpoint of detailEndpoints) {
|
|
2012
|
+
const source = isObject(spec.paths?.[endpoint.path]) ? spec.paths[endpoint.path] : {};
|
|
2013
|
+
const item = scopedPaths[endpoint.path] ?? { ...source };
|
|
2014
|
+
for (const method of HTTP_METHODS2) delete item[method];
|
|
2015
|
+
item[endpoint.method] = source[endpoint.method];
|
|
2016
|
+
scopedPaths[endpoint.path] = item;
|
|
2017
|
+
}
|
|
2018
|
+
modelSpec = { ...spec, paths: scopedPaths };
|
|
2019
|
+
}
|
|
2020
|
+
const models = referencedSchemas(modelSpec);
|
|
2021
|
+
const modelSection = !stage.indexOnly && models.length ? [
|
|
1290
2022
|
"## Data types",
|
|
1291
2023
|
"",
|
|
1292
2024
|
"```typescript",
|
|
@@ -1309,62 +2041,122 @@ function transformOpenApiToMarkdown(spec, sourceUrl) {
|
|
|
1309
2041
|
"",
|
|
1310
2042
|
"| Method | Path | Summary | Tags |",
|
|
1311
2043
|
"|---|---|---|---|",
|
|
1312
|
-
...indexRows.length ? indexRows : ["| \u2014 | \u2014 | No operations declared | \u2014 |"]
|
|
1313
|
-
"",
|
|
1314
|
-
"## Endpoints",
|
|
1315
|
-
"",
|
|
1316
|
-
...groupedEndpoints.length ? groupedEndpoints : ["_No endpoints declared._"],
|
|
1317
|
-
...modelSection ? ["", modelSection] : []
|
|
2044
|
+
...indexRows.length ? indexRows : ["| \u2014 | \u2014 | No operations declared | \u2014 |"]
|
|
1318
2045
|
];
|
|
2046
|
+
if (!stage.indexOnly) {
|
|
2047
|
+
sections.push(
|
|
2048
|
+
"",
|
|
2049
|
+
"## Endpoints",
|
|
2050
|
+
"",
|
|
2051
|
+
...groupedEndpoints.length ? groupedEndpoints : ["_No endpoints declared._"]
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
if (modelSection) {
|
|
2055
|
+
sections.push("", modelSection);
|
|
2056
|
+
}
|
|
1319
2057
|
return `${sections.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
|
|
1320
2058
|
`;
|
|
1321
2059
|
}
|
|
2060
|
+
function appendTrimNote(markdown, actions, budget) {
|
|
2061
|
+
return `${markdown}
|
|
2062
|
+
## Trimmed
|
|
1322
2063
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
llama: zeroModel("Meta Llama (3.3 / 4)", 128e3)
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
2064
|
+
> _Trimmed to fit the ~${budget.toLocaleString()}-token budget: ${actions.join("; ")}._
|
|
2065
|
+
`;
|
|
2066
|
+
}
|
|
2067
|
+
function transformOpenApiToMarkdown(spec, sourceUrl, options = {}) {
|
|
2068
|
+
const warnings = [];
|
|
2069
|
+
const filteredSpec = filterSpec(spec, getEndpoints(spec), options, warnings);
|
|
2070
|
+
for (const message of warnings) options.onWarning?.(message);
|
|
2071
|
+
const normalizedSpec = normalizeInlinePayloadSchemas(filteredSpec);
|
|
2072
|
+
const endpoints = getEndpoints(normalizedSpec);
|
|
2073
|
+
const tags = collectTags(normalizedSpec, endpoints);
|
|
2074
|
+
const fullStage = {
|
|
2075
|
+
includeExamples: true,
|
|
2076
|
+
collapsedTags: /* @__PURE__ */ new Set(),
|
|
2077
|
+
indexOnly: false
|
|
2078
|
+
};
|
|
2079
|
+
if (options.maxTokens === void 0) {
|
|
2080
|
+
return renderDigest(normalizedSpec, sourceUrl, endpoints, tags, fullStage);
|
|
1343
2081
|
}
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
const baseTokens = wordCount * 1.18 + syntaxSymbols * 0.42 + whitespaces * 0.05;
|
|
1348
|
-
const calculate = (name, ratio, contextWindow) => {
|
|
1349
|
-
const tokens = Math.max(1, Math.round(baseTokens * ratio));
|
|
1350
|
-
const contextPercent = Number((tokens / contextWindow * 100).toFixed(1));
|
|
1351
|
-
return { name, tokens, contextWindow, contextPercent };
|
|
2082
|
+
const worst = (markdown2) => {
|
|
2083
|
+
const report = estimateTokens(markdown2);
|
|
2084
|
+
return Math.max(...Object.values(report.models).map((model) => model.tokens));
|
|
1352
2085
|
};
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2086
|
+
const fits = (markdown2) => worst(markdown2) <= options.maxTokens;
|
|
2087
|
+
const full = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, fullStage);
|
|
2088
|
+
if (fits(full)) return full;
|
|
2089
|
+
const stages = buildTrimStages(endpoints, tags);
|
|
2090
|
+
for (let i = 1; i < stages.length; i += 1) {
|
|
2091
|
+
const stage = stages[i];
|
|
2092
|
+
const markdown2 = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, stage);
|
|
2093
|
+
if (fits(markdown2)) {
|
|
2094
|
+
return appendTrimNote(
|
|
2095
|
+
markdown2,
|
|
2096
|
+
stages.slice(1, i + 1).map((entry) => entry.action).filter((action) => Boolean(action)),
|
|
2097
|
+
options.maxTokens
|
|
2098
|
+
);
|
|
1366
2099
|
}
|
|
1367
|
-
}
|
|
2100
|
+
}
|
|
2101
|
+
const last = stages[stages.length - 1];
|
|
2102
|
+
const markdown = renderDigest(normalizedSpec, sourceUrl, endpoints, tags, last);
|
|
2103
|
+
const allActions = stages.slice(1).map((stage) => stage.action).filter((action) => Boolean(action));
|
|
2104
|
+
return appendTrimNote(markdown, allActions, options.maxTokens);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/headers.ts
|
|
2108
|
+
var HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
2109
|
+
function validateHeaderName(name) {
|
|
2110
|
+
if (!name) throw new Error('Header name must not be empty. Expected "Name: value".');
|
|
2111
|
+
if (/[\r\n]/.test(name)) {
|
|
2112
|
+
throw new Error(`Invalid header name: header names must not contain CR/LF characters.`);
|
|
2113
|
+
}
|
|
2114
|
+
if (!HEADER_NAME_PATTERN.test(name)) {
|
|
2115
|
+
throw new Error(`Invalid header name "${name}": use a valid HTTP header token (letters, digits and -_).`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function validateHeaderValue(name, value) {
|
|
2119
|
+
if (/[\r\n]/.test(value)) {
|
|
2120
|
+
throw new Error(`Invalid value for header "${name}": values must not contain CR/LF.`);
|
|
2121
|
+
}
|
|
2122
|
+
if (/\0/.test(value)) {
|
|
2123
|
+
throw new Error(`Invalid value for header "${name}": values must not contain NUL bytes.`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function parseHeaderArgument(raw) {
|
|
2127
|
+
const separator = raw.indexOf(":");
|
|
2128
|
+
if (separator === -1) {
|
|
2129
|
+
if (/[\r\n]/.test(raw)) {
|
|
2130
|
+
throw new Error(`Invalid header: names and values must not contain CR/LF.`);
|
|
2131
|
+
}
|
|
2132
|
+
throw new Error(`Invalid header \u2014 expected "Name: value" format (for example "Authorization: Bearer <token>").`);
|
|
2133
|
+
}
|
|
2134
|
+
const name = raw.slice(0, separator).trim();
|
|
2135
|
+
const value = raw.slice(separator + 1).trim();
|
|
2136
|
+
validateHeaderName(name);
|
|
2137
|
+
validateHeaderValue(name, value);
|
|
2138
|
+
return { [name.toLowerCase()]: value };
|
|
2139
|
+
}
|
|
2140
|
+
function parseHeaderPairs(raw) {
|
|
2141
|
+
const result = {};
|
|
2142
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
2143
|
+
if (!line.trim()) continue;
|
|
2144
|
+
Object.assign(result, parseHeaderArgument(line));
|
|
2145
|
+
}
|
|
2146
|
+
return result;
|
|
2147
|
+
}
|
|
2148
|
+
function mergeHeaders(...sources) {
|
|
2149
|
+
const result = {};
|
|
2150
|
+
for (const source of sources) {
|
|
2151
|
+
if (!source) continue;
|
|
2152
|
+
for (const [name, value] of Object.entries(source)) {
|
|
2153
|
+
if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) {
|
|
2154
|
+
throw new Error(`Invalid header "${name}": CR/LF is not allowed in header names or values.`);
|
|
2155
|
+
}
|
|
2156
|
+
result[name.toLowerCase()] = value;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return result;
|
|
1368
2160
|
}
|
|
1369
2161
|
|
|
1370
2162
|
// src/writer.ts
|
|
@@ -1398,12 +2190,16 @@ async function writeDigest(options) {
|
|
|
1398
2190
|
}
|
|
1399
2191
|
|
|
1400
2192
|
export {
|
|
2193
|
+
VERSION,
|
|
2194
|
+
toTypeScriptIdentifier,
|
|
1401
2195
|
isOpenApiSpec,
|
|
1402
2196
|
parseSpecText,
|
|
1403
2197
|
fetchOpenApiSpec,
|
|
1404
|
-
toTypeScriptIdentifier,
|
|
1405
|
-
transformOpenApiToMarkdown,
|
|
1406
2198
|
estimateTokens,
|
|
2199
|
+
transformOpenApiToMarkdown,
|
|
2200
|
+
parseHeaderArgument,
|
|
2201
|
+
parseHeaderPairs,
|
|
2202
|
+
mergeHeaders,
|
|
1407
2203
|
defaultOutputDirectory,
|
|
1408
2204
|
defaultFilename,
|
|
1409
2205
|
resolveOutputDirectory,
|