@redocly/cli 2.37.0 → 2.39.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/THIRD_PARTY_NOTICES +1 -1
- package/lib/chunks/{IVW4MILQ.js → 2LEVMD2A.js} +3 -3
- package/lib/chunks/{YCTAWG2U.js → 4FB5ZIPP.js} +1 -1
- package/lib/chunks/BA5AZJUM.js +681 -0
- package/lib/chunks/BNHQNMUG.js +525 -0
- package/lib/chunks/{LJUXYC4S.js → C3PXBQU5.js} +1 -1
- package/lib/chunks/C5HID4LT.js +571 -0
- package/lib/chunks/{CPNIO4J3.js → KYZEVOA2.js} +1679 -1400
- package/lib/chunks/{FRVYIHIR.js → OVY6O6WL.js} +5 -3
- package/lib/chunks/{4UQ4YHWW.js → OYZMAAZV.js} +5 -10
- package/lib/chunks/U4V3W7MN.js +17 -0
- package/lib/chunks/UUU33DK3.js +125 -0
- package/lib/chunks/{M7TYKZDD.js → V6CVT3XE.js} +6 -4
- package/lib/chunks/{QFYLVVG2.js → V74PIVJZ.js} +8 -5
- package/lib/chunks/{ZHITMP73.js → W5R2GYDZ.js} +3 -2
- package/lib/chunks/WST4KAJO.js +2788 -0
- package/lib/chunks/{U3LS66ZD.js → XB6C62FW.js} +100 -771
- package/lib/index.js +157 -14
- package/package.json +1 -1
|
@@ -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 "./WST4KAJO.js";
|
|
12
|
+
import {
|
|
13
|
+
AbortFlowError,
|
|
14
|
+
exitWithError
|
|
15
|
+
} from "./U4V3W7MN.js";
|
|
16
|
+
import {
|
|
17
|
+
require_undici
|
|
18
|
+
} from "./XB6C62FW.js";
|
|
19
|
+
import {
|
|
20
|
+
isPlainObject,
|
|
21
|
+
logger
|
|
22
|
+
} from "./KYZEVOA2.js";
|
|
23
|
+
import "./Z2I5YXYN.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
|
+
};
|