@redocly/cli 0.0.0-snapshot.1783058823 → 0.0.0-snapshot.1783680915
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/THIRD_PARTY_NOTICES +4 -3
- package/lib/chunks/2NDA36XW.js +525 -0
- package/lib/chunks/3GLLMTG6.js +2788 -0
- package/lib/chunks/{BRJTJNID.js → 3P5DPDSQ.js} +3 -3
- package/lib/chunks/{XLXDKMB3.js → 4DAHBJVB.js} +4 -2
- package/lib/chunks/{EOUEGR5D.js → 4FB5ZIPP.js} +1 -1
- package/lib/chunks/{6TSWXV6K.js → 6UZHD7QP.js} +5 -10
- package/lib/chunks/CM3BN6AQ.js +571 -0
- package/lib/chunks/CVULHLVL.js +129 -0
- package/lib/chunks/{XBUQB53C.js → DZWV43TY.js} +1 -1
- package/lib/chunks/{76SYIULE.js → E3EMQ3A4.js} +9 -5
- package/lib/chunks/HX6M2L3N.js +17 -0
- package/lib/chunks/{GX6BFPR3.js → JJQ56OW7.js} +7 -4
- package/lib/chunks/{J2A7KQMF.js → K2LTHSMQ.js} +742 -462
- package/lib/chunks/{VNOK7MXC.js → O7PYIAHJ.js} +5 -3
- package/lib/chunks/OR35UHGS.js +20272 -0
- package/lib/chunks/TGT4OEBL.js +681 -0
- package/lib/chunks/UUU33DK3.js +125 -0
- package/lib/chunks/{YKWOWQR4.js → XB6C62FW.js} +100 -771
- package/lib/index.js +158 -14
- package/package.json +1 -1
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -648,10 +648,10 @@ Packages:
|
|
|
648
648
|
@redocly/ajv@8.18.3 — Copyright (c) 2015-2021 Evgeny Poberezkin
|
|
649
649
|
@redocly/cli-otel@0.3.1 — Copyright 2025 Redocly Inc, all rights reserved.
|
|
650
650
|
@redocly/config@0.22.0 — Copyright 2024 Redocly Inc.
|
|
651
|
-
@redocly/config@0.
|
|
651
|
+
@redocly/config@0.50.1 — Copyright 2024 Redocly Inc.
|
|
652
652
|
@redocly/openapi-core@1.34.16
|
|
653
|
-
@redocly/openapi-core@2.
|
|
654
|
-
@redocly/respect-core@2.
|
|
653
|
+
@redocly/openapi-core@2.38.0
|
|
654
|
+
@redocly/respect-core@2.38.0
|
|
655
655
|
agent-base@7.1.4 — Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
|
656
656
|
ajv-formats@3.0.1 — Copyright (c) 2020 Evgeny Poberezkin
|
|
657
657
|
ajv@8.20.0 — Copyright (c) 2015-2021 Evgeny Poberezkin
|
|
@@ -673,6 +673,7 @@ Packages:
|
|
|
673
673
|
fast-deep-equal@3.1.3 — Copyright (c) 2017 Evgeny Poberezkin
|
|
674
674
|
fast-safe-stringify@2.1.1 — Copyright (c) 2016 David Mark Clements
|
|
675
675
|
foreach@2.0.6 — Copyright (c) 2013 Manuel Stofer
|
|
676
|
+
graphql@16.14.2 — Copyright (c) GraphQL Contributors
|
|
676
677
|
handlebars@4.7.9 — Copyright (C) 2011-2019 by Yehuda Katz
|
|
677
678
|
has-flag@4.0.0
|
|
678
679
|
https-proxy-agent@7.0.6 — Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
2
|
+
const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
ValidationSession,
|
|
5
|
+
createNormalizedExchange,
|
|
6
|
+
isJsonMime,
|
|
7
|
+
loadOpenApiIndex,
|
|
8
|
+
normalizeFsPath,
|
|
9
|
+
parseCsv,
|
|
10
|
+
renderReport
|
|
11
|
+
} from "./3GLLMTG6.js";
|
|
12
|
+
import {
|
|
13
|
+
AbortFlowError,
|
|
14
|
+
exitWithError
|
|
15
|
+
} from "./HX6M2L3N.js";
|
|
16
|
+
import {
|
|
17
|
+
require_undici
|
|
18
|
+
} from "./XB6C62FW.js";
|
|
19
|
+
import {
|
|
20
|
+
isPlainObject,
|
|
21
|
+
logger
|
|
22
|
+
} from "./K2LTHSMQ.js";
|
|
23
|
+
import "./OR35UHGS.js";
|
|
24
|
+
import {
|
|
25
|
+
__toESM
|
|
26
|
+
} from "./5ILQMFXK.js";
|
|
27
|
+
|
|
28
|
+
// src/commands/proxy/har-writer.ts
|
|
29
|
+
import { createReadStream, createWriteStream, existsSync } from "node:fs";
|
|
30
|
+
import { appendFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { createInterface } from "node:readline";
|
|
33
|
+
var HarWriter = class {
|
|
34
|
+
outputPath;
|
|
35
|
+
entriesPath;
|
|
36
|
+
creatorVersion;
|
|
37
|
+
writeChain = Promise.resolve();
|
|
38
|
+
directoryEnsured = false;
|
|
39
|
+
started = false;
|
|
40
|
+
count = 0;
|
|
41
|
+
constructor(outputPath, creatorVersion) {
|
|
42
|
+
this.outputPath = path.resolve(process.cwd(), outputPath);
|
|
43
|
+
this.entriesPath = `${this.outputPath}.entries.tmp`;
|
|
44
|
+
this.creatorVersion = creatorVersion;
|
|
45
|
+
}
|
|
46
|
+
get entryCount() {
|
|
47
|
+
return this.count;
|
|
48
|
+
}
|
|
49
|
+
add(entry) {
|
|
50
|
+
const line = `${JSON.stringify(entry)}
|
|
51
|
+
`;
|
|
52
|
+
const append = this.writeChain.catch(() => void 0).then(() => this.append(line)).then(() => {
|
|
53
|
+
this.count += 1;
|
|
54
|
+
});
|
|
55
|
+
this.writeChain = append.catch(() => void 0);
|
|
56
|
+
return append;
|
|
57
|
+
}
|
|
58
|
+
async finalize() {
|
|
59
|
+
await this.writeChain.catch(() => void 0);
|
|
60
|
+
await this.ensureDirectory();
|
|
61
|
+
await this.writeDocument();
|
|
62
|
+
await rm(this.entriesPath, { force: true });
|
|
63
|
+
}
|
|
64
|
+
async append(line) {
|
|
65
|
+
await this.ensureDirectory();
|
|
66
|
+
if (!this.started) {
|
|
67
|
+
await writeFile(this.entriesPath, line, "utf8");
|
|
68
|
+
this.started = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await appendFile(this.entriesPath, line, "utf8");
|
|
72
|
+
}
|
|
73
|
+
async ensureDirectory() {
|
|
74
|
+
if (this.directoryEnsured) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await mkdir(path.dirname(this.outputPath), { recursive: true });
|
|
78
|
+
this.directoryEnsured = true;
|
|
79
|
+
}
|
|
80
|
+
async writeDocument() {
|
|
81
|
+
const stream = createWriteStream(this.outputPath, { encoding: "utf8" });
|
|
82
|
+
const write = (chunk) => new Promise((resolve, reject) => {
|
|
83
|
+
stream.write(chunk, (error) => error ? reject(error) : resolve());
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
const creator = JSON.stringify({ name: "redocly-cli proxy", version: this.creatorVersion });
|
|
87
|
+
await write(
|
|
88
|
+
`{
|
|
89
|
+
"log": {
|
|
90
|
+
"version": "1.2",
|
|
91
|
+
"creator": ${creator},
|
|
92
|
+
"entries": [`
|
|
93
|
+
);
|
|
94
|
+
let written = 0;
|
|
95
|
+
if (existsSync(this.entriesPath)) {
|
|
96
|
+
const lines = createInterface({
|
|
97
|
+
input: createReadStream(this.entriesPath, { encoding: "utf8" }),
|
|
98
|
+
crlfDelay: Infinity
|
|
99
|
+
});
|
|
100
|
+
for await (const line of lines) {
|
|
101
|
+
if (!line.trim()) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
await write(`${written === 0 ? "\n" : ",\n"} ${line}`);
|
|
105
|
+
written += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await write(written === 0 ? "]\n }\n}\n" : "\n ]\n }\n}\n");
|
|
109
|
+
} finally {
|
|
110
|
+
await new Promise((resolve, reject) => {
|
|
111
|
+
stream.on("error", reject);
|
|
112
|
+
stream.end(() => resolve());
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/commands/proxy/server.ts
|
|
119
|
+
var import_undici = __toESM(require_undici(), 1);
|
|
120
|
+
import {
|
|
121
|
+
createServer,
|
|
122
|
+
STATUS_CODES
|
|
123
|
+
} from "node:http";
|
|
124
|
+
var HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH"];
|
|
125
|
+
function toHttpMethod(value) {
|
|
126
|
+
const upper = (value ?? "GET").toUpperCase();
|
|
127
|
+
return HTTP_METHODS.find((method) => method === upper);
|
|
128
|
+
}
|
|
129
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
130
|
+
"connection",
|
|
131
|
+
"keep-alive",
|
|
132
|
+
"proxy-authenticate",
|
|
133
|
+
"proxy-authorization",
|
|
134
|
+
"te",
|
|
135
|
+
"trailer",
|
|
136
|
+
"transfer-encoding",
|
|
137
|
+
"upgrade"
|
|
138
|
+
]);
|
|
139
|
+
function readRequestBody(req) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
143
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
144
|
+
req.on("error", reject);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function toForwardRequestHeaders(req) {
|
|
148
|
+
const headers = {};
|
|
149
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
150
|
+
if (value === void 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const lower = name.toLowerCase();
|
|
154
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || lower === "host" || lower === "content-length") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (lower === "accept-encoding") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
headers[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
161
|
+
}
|
|
162
|
+
return headers;
|
|
163
|
+
}
|
|
164
|
+
function toClientResponseHeaders(headers) {
|
|
165
|
+
const result = {};
|
|
166
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
167
|
+
if (value === void 0 || HOP_BY_HOP_HEADERS.has(name.toLowerCase())) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
result[name] = value;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
function rawHeadersToHarHeaders(rawHeaders) {
|
|
175
|
+
const headers = [];
|
|
176
|
+
for (let index = 0; index < rawHeaders.length - 1; index += 2) {
|
|
177
|
+
headers.push({ name: rawHeaders[index], value: rawHeaders[index + 1] });
|
|
178
|
+
}
|
|
179
|
+
return headers;
|
|
180
|
+
}
|
|
181
|
+
function responseHeadersToHarHeaders(headers) {
|
|
182
|
+
const result = [];
|
|
183
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
184
|
+
if (value === void 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
for (const item of value) {
|
|
189
|
+
result.push({ name, value: item });
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
result.push({ name, value });
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
function toQueryString(url) {
|
|
198
|
+
const query = [];
|
|
199
|
+
for (const [name, value] of url.searchParams.entries()) {
|
|
200
|
+
query.push({ name, value });
|
|
201
|
+
}
|
|
202
|
+
return query;
|
|
203
|
+
}
|
|
204
|
+
function parseCookieHeader(cookieHeader) {
|
|
205
|
+
if (!cookieHeader) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
const cookies = [];
|
|
209
|
+
for (const pair of cookieHeader.split(";")) {
|
|
210
|
+
const separatorIndex = pair.indexOf("=");
|
|
211
|
+
if (separatorIndex === -1) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const name = pair.slice(0, separatorIndex).trim();
|
|
215
|
+
const value = pair.slice(separatorIndex + 1).trim();
|
|
216
|
+
if (name) {
|
|
217
|
+
cookies.push({ name, value });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return cookies;
|
|
221
|
+
}
|
|
222
|
+
function isTextualContentType(contentType) {
|
|
223
|
+
if (!contentType) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (isJsonMime(contentType)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
230
|
+
return mime.startsWith("text/") || mime === "application/xml" || mime === "application/x-www-form-urlencoded" || mime.endsWith("+xml");
|
|
231
|
+
}
|
|
232
|
+
function buildBodyPayload(body, contentType) {
|
|
233
|
+
if (body.length === 0) {
|
|
234
|
+
return { size: 0 };
|
|
235
|
+
}
|
|
236
|
+
if (isTextualContentType(contentType)) {
|
|
237
|
+
return { text: body.toString("utf8"), size: body.length };
|
|
238
|
+
}
|
|
239
|
+
return { text: body.toString("base64"), encoding: "base64", size: body.length };
|
|
240
|
+
}
|
|
241
|
+
function buildPostData(body, contentType) {
|
|
242
|
+
if (body.length === 0) {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
const payload = buildBodyPayload(body, contentType);
|
|
246
|
+
return {
|
|
247
|
+
mimeType: contentType ?? "application/octet-stream",
|
|
248
|
+
text: payload.text ?? "",
|
|
249
|
+
...payload.encoding ? { encoding: payload.encoding } : {}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function startProxyServer(options) {
|
|
253
|
+
const targetUrl = new URL(options.target);
|
|
254
|
+
let exchangeIndex = 0;
|
|
255
|
+
const nextExchangeIndex = () => exchangeIndex++;
|
|
256
|
+
const server = createServer((req, res) => {
|
|
257
|
+
void handleRequest(req, res, targetUrl, options, nextExchangeIndex);
|
|
258
|
+
});
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
server.once("error", reject);
|
|
261
|
+
server.listen(options.port, options.host, () => {
|
|
262
|
+
server.removeListener("error", reject);
|
|
263
|
+
const address = server.address();
|
|
264
|
+
const boundPort = isPlainObject(address) ? address.port : options.port;
|
|
265
|
+
resolve({
|
|
266
|
+
url: `http://${options.host}:${boundPort}`,
|
|
267
|
+
close: () => new Promise((resolveClose, rejectClose) => {
|
|
268
|
+
server.close((error) => error ? rejectClose(error) : resolveClose());
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function buildForwardUrl(requestUrl, targetUrl) {
|
|
275
|
+
const requested = new URL(requestUrl, targetUrl);
|
|
276
|
+
const basePath = targetUrl.pathname.endsWith("/") ? targetUrl.pathname.slice(0, -1) : targetUrl.pathname;
|
|
277
|
+
const requestPath = requested.pathname;
|
|
278
|
+
const alreadyPrefixed = requestPath === basePath || requestPath.startsWith(`${basePath}/`);
|
|
279
|
+
const forwardUrl = new URL(targetUrl.toString());
|
|
280
|
+
forwardUrl.pathname = basePath && !alreadyPrefixed ? `${basePath}${requestPath}` : requestPath;
|
|
281
|
+
forwardUrl.search = requested.search;
|
|
282
|
+
return forwardUrl;
|
|
283
|
+
}
|
|
284
|
+
async function handleRequest(req, res, targetUrl, options, nextExchangeIndex) {
|
|
285
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
286
|
+
const method = toHttpMethod(req.method);
|
|
287
|
+
if (!method) {
|
|
288
|
+
res.writeHead(501, { "content-type": "text/plain" });
|
|
289
|
+
res.end(`Unsupported HTTP method: ${req.method}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const forwardUrl = buildForwardUrl(req.url ?? "/", targetUrl);
|
|
293
|
+
let captured = null;
|
|
294
|
+
try {
|
|
295
|
+
const requestBody = await readRequestBody(req);
|
|
296
|
+
const hasBody = method !== "GET" && method !== "HEAD" && requestBody.length > 0;
|
|
297
|
+
const upstream = await (0, import_undici.request)(forwardUrl, {
|
|
298
|
+
method,
|
|
299
|
+
headers: toForwardRequestHeaders(req),
|
|
300
|
+
body: hasBody ? requestBody : void 0
|
|
301
|
+
});
|
|
302
|
+
const responseBody = Buffer.from(await upstream.body.arrayBuffer());
|
|
303
|
+
res.writeHead(upstream.statusCode, toClientResponseHeaders(upstream.headers));
|
|
304
|
+
res.end(responseBody);
|
|
305
|
+
const elapsedMs = Date.now() - startedAt.getTime();
|
|
306
|
+
captured = buildCapturedExchange({
|
|
307
|
+
index: nextExchangeIndex(),
|
|
308
|
+
method,
|
|
309
|
+
forwardUrl,
|
|
310
|
+
req,
|
|
311
|
+
requestBody,
|
|
312
|
+
statusCode: upstream.statusCode,
|
|
313
|
+
responseHeaders: upstream.headers,
|
|
314
|
+
responseBody,
|
|
315
|
+
startedAt,
|
|
316
|
+
elapsedMs
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
options.onError(error);
|
|
320
|
+
if (!res.writableEnded) {
|
|
321
|
+
if (!res.headersSent) {
|
|
322
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
323
|
+
}
|
|
324
|
+
res.end(`Proxy error: ${error.message}`);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (captured) {
|
|
329
|
+
try {
|
|
330
|
+
await options.onExchange(captured);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
options.onError(error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function buildCapturedExchange(params) {
|
|
337
|
+
const requestContentType = singleHeader(params.req.headers["content-type"]);
|
|
338
|
+
const responseContentType = singleHeader(params.responseHeaders["content-type"]);
|
|
339
|
+
const exchange = createNormalizedExchange(
|
|
340
|
+
{
|
|
341
|
+
method: params.method,
|
|
342
|
+
url: params.forwardUrl.toString(),
|
|
343
|
+
requestHeaders: params.req.headers,
|
|
344
|
+
requestBody: params.requestBody,
|
|
345
|
+
requestContentType,
|
|
346
|
+
responseStatus: params.statusCode,
|
|
347
|
+
responseHeaders: params.responseHeaders,
|
|
348
|
+
responseBody: params.responseBody,
|
|
349
|
+
responseContentType,
|
|
350
|
+
startedAt: params.startedAt.toISOString()
|
|
351
|
+
},
|
|
352
|
+
params.index,
|
|
353
|
+
"(proxy)"
|
|
354
|
+
);
|
|
355
|
+
if (!exchange) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const responsePayload = buildBodyPayload(params.responseBody, responseContentType);
|
|
359
|
+
const harEntry = {
|
|
360
|
+
startedDateTime: params.startedAt.toISOString(),
|
|
361
|
+
time: params.elapsedMs,
|
|
362
|
+
request: {
|
|
363
|
+
method: params.method,
|
|
364
|
+
url: params.forwardUrl.toString(),
|
|
365
|
+
httpVersion: `HTTP/${params.req.httpVersion}`,
|
|
366
|
+
cookies: parseCookieHeader(singleHeader(params.req.headers.cookie)),
|
|
367
|
+
headers: rawHeadersToHarHeaders(params.req.rawHeaders),
|
|
368
|
+
queryString: toQueryString(params.forwardUrl),
|
|
369
|
+
postData: buildPostData(params.requestBody, requestContentType),
|
|
370
|
+
headersSize: -1,
|
|
371
|
+
bodySize: params.requestBody.length
|
|
372
|
+
},
|
|
373
|
+
response: {
|
|
374
|
+
status: params.statusCode,
|
|
375
|
+
statusText: STATUS_CODES[params.statusCode] ?? "",
|
|
376
|
+
httpVersion: "HTTP/1.1",
|
|
377
|
+
cookies: [],
|
|
378
|
+
headers: responseHeadersToHarHeaders(params.responseHeaders),
|
|
379
|
+
content: {
|
|
380
|
+
size: responsePayload.size,
|
|
381
|
+
mimeType: responseContentType ?? "application/octet-stream",
|
|
382
|
+
...responsePayload.text !== void 0 ? { text: responsePayload.text } : {},
|
|
383
|
+
...responsePayload.encoding ? { encoding: responsePayload.encoding } : {}
|
|
384
|
+
},
|
|
385
|
+
redirectURL: singleHeader(params.responseHeaders.location) ?? "",
|
|
386
|
+
headersSize: -1,
|
|
387
|
+
bodySize: params.responseBody.length
|
|
388
|
+
},
|
|
389
|
+
cache: {},
|
|
390
|
+
timings: { send: 0, wait: params.elapsedMs, receive: 0 }
|
|
391
|
+
};
|
|
392
|
+
return { exchange, harEntry };
|
|
393
|
+
}
|
|
394
|
+
function singleHeader(value) {
|
|
395
|
+
if (Array.isArray(value)) {
|
|
396
|
+
return value[0];
|
|
397
|
+
}
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/commands/proxy/index.ts
|
|
402
|
+
var USE_COLOR = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0;
|
|
403
|
+
function severityIcon(severity) {
|
|
404
|
+
if (severity === "error") return "\u2716";
|
|
405
|
+
if (severity === "warning") return "\u25B2";
|
|
406
|
+
return "\u25CF";
|
|
407
|
+
}
|
|
408
|
+
function formatLiveFinding(finding) {
|
|
409
|
+
const status = finding.status !== void 0 ? ` (${finding.status})` : "";
|
|
410
|
+
const operation = finding.operationId ? ` ${finding.operationId}` : "";
|
|
411
|
+
return `${severityIcon(finding.severity)} ${finding.severity.toUpperCase()} ${finding.method} ${finding.path}${status}${operation} \u2192 [${finding.ruleId}] ${finding.message}`;
|
|
412
|
+
}
|
|
413
|
+
function waitForShutdownSignal() {
|
|
414
|
+
return new Promise((resolve) => {
|
|
415
|
+
process.once("SIGINT", () => resolve());
|
|
416
|
+
process.once("SIGTERM", () => resolve());
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async function handleProxy({ argv, config, version }) {
|
|
420
|
+
const targetInput = /^[a-z][a-z0-9+.-]*:\/\//i.test(argv.target) ? argv.target : `http://${argv.target}`;
|
|
421
|
+
let target;
|
|
422
|
+
try {
|
|
423
|
+
target = new URL(targetInput);
|
|
424
|
+
} catch {
|
|
425
|
+
return exitWithError(`Invalid --target URL: ${argv.target}`);
|
|
426
|
+
}
|
|
427
|
+
const harPath = normalizeFsPath(argv.har);
|
|
428
|
+
const harWriter = new HarWriter(harPath, version);
|
|
429
|
+
let session = null;
|
|
430
|
+
if (argv.api) {
|
|
431
|
+
const specPath = normalizeFsPath(argv.api);
|
|
432
|
+
const openApiIndex = await loadOpenApiIndex(specPath, config);
|
|
433
|
+
if (openApiIndex.loadedOperations === 0) {
|
|
434
|
+
return exitWithError(`No OpenAPI operations were loaded from: ${specPath}`);
|
|
435
|
+
}
|
|
436
|
+
session = ValidationSession.create({
|
|
437
|
+
openApiIndex,
|
|
438
|
+
matchMode: argv["match-mode"],
|
|
439
|
+
ignoreCookies: argv["ignore-cookies"],
|
|
440
|
+
previewFindingsLimit: argv["max-findings"],
|
|
441
|
+
activeRules: argv.rules ? parseCsv(argv.rules) : void 0
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
let exchangeQueue = Promise.resolve();
|
|
445
|
+
let server;
|
|
446
|
+
try {
|
|
447
|
+
server = await startProxyServer({
|
|
448
|
+
target: target.toString(),
|
|
449
|
+
port: argv.port,
|
|
450
|
+
host: argv.host,
|
|
451
|
+
onExchange: ({ exchange, harEntry }) => {
|
|
452
|
+
const task = exchangeQueue.then(async () => {
|
|
453
|
+
try {
|
|
454
|
+
await harWriter.add(harEntry);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
logger.error(
|
|
457
|
+
`Failed to write HAR entry, skipping validation for this exchange: ${error.message}
|
|
458
|
+
`
|
|
459
|
+
);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!session) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const findings2 = await session.process(exchange);
|
|
466
|
+
for (const finding of findings2) {
|
|
467
|
+
logger.info(`${formatLiveFinding(finding)}
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
exchangeQueue = task.catch(() => void 0);
|
|
472
|
+
return task;
|
|
473
|
+
},
|
|
474
|
+
onError: (error) => {
|
|
475
|
+
logger.error(`Proxy request failed: ${error.message}
|
|
476
|
+
`);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return exitWithError(`Failed to start proxy server: ${error.message}`);
|
|
481
|
+
}
|
|
482
|
+
logger.info(`Proxy listening on ${server.url} \u2192 forwarding to ${target.toString()}
|
|
483
|
+
`);
|
|
484
|
+
logger.info(`Recording traffic to ${harPath}
|
|
485
|
+
`);
|
|
486
|
+
if (session) {
|
|
487
|
+
logger.info(`Validating live traffic against ${normalizeFsPath(argv.api)}
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
logger.info("Press Ctrl+C to stop.\n");
|
|
491
|
+
await waitForShutdownSignal();
|
|
492
|
+
logger.info("\nShutting down proxy\u2026\n");
|
|
493
|
+
await server.close();
|
|
494
|
+
await exchangeQueue;
|
|
495
|
+
await harWriter.finalize();
|
|
496
|
+
logger.info(`Captured ${harWriter.entryCount} exchange(s) to ${harPath}
|
|
497
|
+
`);
|
|
498
|
+
if (!session) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const { runId, summary, findings } = session.finalize();
|
|
502
|
+
const result = {
|
|
503
|
+
runId,
|
|
504
|
+
summary,
|
|
505
|
+
findings,
|
|
506
|
+
meta: {
|
|
507
|
+
specSource: normalizeFsPath(argv.api),
|
|
508
|
+
trafficPath: harPath,
|
|
509
|
+
format: "har",
|
|
510
|
+
matchMode: argv["match-mode"]
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const report = renderReport(result, {
|
|
514
|
+
format: argv["report-format"],
|
|
515
|
+
color: USE_COLOR && argv["report-format"] === "pretty",
|
|
516
|
+
maxFindings: argv["max-findings"]
|
|
517
|
+
});
|
|
518
|
+
logger.output(report);
|
|
519
|
+
if (summary.findingsBySeverity.error > 0) {
|
|
520
|
+
throw new AbortFlowError("Drift detected.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
export {
|
|
524
|
+
handleProxy
|
|
525
|
+
};
|