@redocly/cli 2.39.0 → 2.41.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.
@@ -1,5 +1,19 @@
1
1
  import { createRequire as __createRequire } from 'node:module';
2
2
  const require = __createRequire(import.meta.url);
3
+ import {
4
+ compileOpenApiPath,
5
+ getPathWithoutTrailingSlash,
6
+ isJsonMime,
7
+ isSyntheticHost,
8
+ listOpenApiFiles,
9
+ normalizeServerPrefix,
10
+ parseHeaderIgnoreList,
11
+ parseUrl,
12
+ pickSchemaByMime,
13
+ resolvePathForServer,
14
+ shouldIgnoreHeaderAsUndocumented,
15
+ splitSetCookieHeader
16
+ } from "./ER45DAHG.js";
3
17
  import {
4
18
  BaseResolver,
5
19
  blue,
@@ -21,7 +35,7 @@ import {
21
35
  require_dist,
22
36
  walkDocument,
23
37
  yellow
24
- } from "./KYZEVOA2.js";
38
+ } from "./YK6T7IHG.js";
25
39
  import {
26
40
  __toESM
27
41
  } from "./5ILQMFXK.js";
@@ -422,278 +436,9 @@ function renderReport(result, options) {
422
436
  }
423
437
  }
424
438
 
425
- // src/commands/drift/utils/files.ts
426
- import { open, readdir, stat } from "node:fs/promises";
427
- import path2 from "node:path";
428
- var SPEC_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
429
- async function listOpenApiFiles(rootDir) {
430
- const output = [];
431
- async function walk(currentDir) {
432
- const entries = await readdir(currentDir, { withFileTypes: true });
433
- for (const entry of entries) {
434
- if (entry.name.startsWith(".")) {
435
- continue;
436
- }
437
- const absolutePath = path2.join(currentDir, entry.name);
438
- if (entry.isDirectory()) {
439
- await walk(absolutePath);
440
- continue;
441
- }
442
- if (entry.isFile() && SPEC_FILE_EXTENSIONS.has(path2.extname(entry.name).toLowerCase())) {
443
- output.push(absolutePath);
444
- }
445
- }
446
- }
447
- await walk(rootDir);
448
- output.sort();
449
- return output;
450
- }
451
- async function readProbe(filePath, maxBytes = 4096) {
452
- const fileHandle = await open(filePath, "r");
453
- try {
454
- const buffer = Buffer.allocUnsafe(maxBytes);
455
- const { bytesRead } = await fileHandle.read(buffer, 0, maxBytes, 0);
456
- return buffer.toString("utf8", 0, bytesRead);
457
- } finally {
458
- await fileHandle.close();
459
- }
460
- }
461
- function normalizeFsPath(value) {
462
- return path2.resolve(process.cwd(), value);
463
- }
464
- async function listFilesRecursively(rootPath) {
465
- const stats = await stat(rootPath);
466
- if (stats.isFile()) {
467
- return [rootPath];
468
- }
469
- if (!stats.isDirectory()) {
470
- return [];
471
- }
472
- const output = [];
473
- async function walk(currentDir) {
474
- const entries = await readdir(currentDir, { withFileTypes: true });
475
- for (const entry of entries) {
476
- if (entry.name.startsWith(".")) {
477
- continue;
478
- }
479
- const absolutePath = path2.join(currentDir, entry.name);
480
- if (entry.isDirectory()) {
481
- await walk(absolutePath);
482
- continue;
483
- }
484
- if (entry.isFile()) {
485
- output.push(absolutePath);
486
- }
487
- }
488
- }
489
- await walk(rootPath);
490
- output.sort();
491
- return output;
492
- }
493
-
494
439
  // src/commands/drift/engine/validation-session.ts
495
440
  import { randomUUID } from "node:crypto";
496
441
 
497
- // src/commands/drift/utils/http.ts
498
- var DUMMY_HOST = "drift.local";
499
- var DUMMY_BASE_URL = `http://${DUMMY_HOST}`;
500
- function isSyntheticHost(host) {
501
- return host === DUMMY_HOST;
502
- }
503
- var IGNORED_UNDOCUMENTED_HEADERS = /* @__PURE__ */ new Set([
504
- "accept",
505
- "accept-charset",
506
- "accept-encoding",
507
- "accept-language",
508
- "authorization",
509
- "baggage",
510
- "cache-control",
511
- "cdn-loop",
512
- "cookie",
513
- "connection",
514
- "content-length",
515
- "content-type",
516
- "dpr",
517
- "dnt",
518
- "downlink",
519
- "ect",
520
- "forwarded",
521
- "host",
522
- "if-match",
523
- "if-modified-since",
524
- "if-none-match",
525
- "if-range",
526
- "if-unmodified-since",
527
- "origin",
528
- "pragma",
529
- "priority",
530
- "range",
531
- "referer",
532
- "sec-fetch-dest",
533
- "sec-fetch-mode",
534
- "sec-fetch-site",
535
- "sec-fetch-user",
536
- "sec-gpc",
537
- "sentry-trace",
538
- "te",
539
- "traceparent",
540
- "tracestate",
541
- "upgrade",
542
- "upgrade-insecure-requests",
543
- "user-agent",
544
- "via",
545
- "x-amzn-trace-id",
546
- "x-client-trace-id",
547
- "x-cloud-trace-context",
548
- "x-correlation-id",
549
- "x-http-method-override",
550
- "x-method-override",
551
- "x-real-ip",
552
- "x-request-id",
553
- "x-forwarded-for",
554
- "x-forwarded-host",
555
- "x-forwarded-port",
556
- "x-forwarded-proto"
557
- ]);
558
- var IGNORED_UNDOCUMENTED_HEADER_PREFIXES = [
559
- "cf-",
560
- "sec-ch-",
561
- "sec-fetch-",
562
- "x-b3-",
563
- "x-envoy-",
564
- "x-forwarded-"
565
- ];
566
- var SET_COOKIE_SEPARATOR = "\n";
567
- function splitSetCookieHeader(value) {
568
- return value.split(SET_COOKIE_SEPARATOR);
569
- }
570
- function appendHeaderValue(result, name, value) {
571
- const key = name.toLowerCase();
572
- const existing = result[key];
573
- if (existing === void 0) {
574
- result[key] = value;
575
- return;
576
- }
577
- result[key] = key === "set-cookie" ? `${existing}${SET_COOKIE_SEPARATOR}${value}` : `${existing},${value}`;
578
- }
579
- function normalizeHeaders(input) {
580
- if (!isPlainObject(input) && !Array.isArray(input)) {
581
- return {};
582
- }
583
- const result = {};
584
- if (Array.isArray(input)) {
585
- for (const item of input) {
586
- if (!isPlainObject(item)) {
587
- continue;
588
- }
589
- const { name, value } = item;
590
- if (typeof name === "string" && value !== void 0) {
591
- appendHeaderValue(result, name, String(value));
592
- }
593
- }
594
- return result;
595
- }
596
- for (const [key, value] of Object.entries(input)) {
597
- if (value === void 0 || value === null) {
598
- continue;
599
- }
600
- if (Array.isArray(value)) {
601
- for (const item of value) {
602
- appendHeaderValue(result, key, String(item));
603
- }
604
- continue;
605
- }
606
- appendHeaderValue(result, key, String(value));
607
- }
608
- return result;
609
- }
610
- function parseUrl(input) {
611
- try {
612
- return new URL(input);
613
- } catch {
614
- return new URL(input, DUMMY_BASE_URL);
615
- }
616
- }
617
- function getPathWithoutTrailingSlash(pathname) {
618
- if (pathname.length > 1 && pathname.endsWith("/")) {
619
- return pathname.slice(0, -1);
620
- }
621
- return pathname;
622
- }
623
- function escapeRegex(value) {
624
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
625
- }
626
- function compileOpenApiPath(pathTemplate) {
627
- const params = [];
628
- let score = 0;
629
- const absolutePath = pathTemplate.startsWith("/") ? pathTemplate : `/${pathTemplate}`;
630
- const normalizedPath = getPathWithoutTrailingSlash(absolutePath);
631
- const regexBody = normalizedPath.split("/").map((segment) => {
632
- if (!segment) {
633
- return "";
634
- }
635
- const paramMatch = segment.match(/^\{([^}]+)\}$/);
636
- if (paramMatch) {
637
- params.push(paramMatch[1]);
638
- return "([^/]+)";
639
- }
640
- score += 2;
641
- return escapeRegex(segment);
642
- }).join("/");
643
- return {
644
- regex: new RegExp(`^${regexBody || "/"}$`),
645
- params,
646
- score
647
- };
648
- }
649
- function parseJsonBodyIfPresent(contentType, bodyText) {
650
- if (!bodyText) {
651
- return void 0;
652
- }
653
- if (!isJsonMime(contentType)) {
654
- return void 0;
655
- }
656
- try {
657
- return JSON.parse(bodyText);
658
- } catch {
659
- return void 0;
660
- }
661
- }
662
- function normalizeContentType(contentType) {
663
- if (!contentType) {
664
- return "";
665
- }
666
- return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
667
- }
668
- function isJsonMime(contentType) {
669
- const mime = normalizeContentType(contentType);
670
- return mime === "application/json" || mime.endsWith("+json");
671
- }
672
- function pickSchemaByMime(contentMap, contentType) {
673
- const requestedMime = normalizeContentType(contentType);
674
- if (!requestedMime) {
675
- return contentMap["application/json"] ?? contentMap["*/*"];
676
- }
677
- if (contentMap[requestedMime] !== void 0) {
678
- return contentMap[requestedMime];
679
- }
680
- const [type, subtype] = requestedMime.split("/");
681
- if (type && subtype) {
682
- const wildcardSubtype = `${type}/*`;
683
- if (contentMap[wildcardSubtype] !== void 0) {
684
- return contentMap[wildcardSubtype];
685
- }
686
- }
687
- return contentMap["*/*"];
688
- }
689
- function shouldIgnoreHeaderAsUndocumented(headerName) {
690
- const normalizedHeaderName = headerName.toLowerCase();
691
- if (normalizedHeaderName.startsWith(":")) {
692
- return true;
693
- }
694
- return IGNORED_UNDOCUMENTED_HEADERS.has(normalizedHeaderName) || IGNORED_UNDOCUMENTED_HEADER_PREFIXES.some((prefix) => normalizedHeaderName.startsWith(prefix));
695
- }
696
-
697
442
  // src/commands/drift/openapi/matcher.ts
698
443
  function toRelativePath(requestPath, server) {
699
444
  const normalizedRequestPath = getPathWithoutTrailingSlash(requestPath || "/") || "/";
@@ -1278,11 +1023,30 @@ function validateParameter(parameter, actualValue, context, findings) {
1278
1023
  });
1279
1024
  }
1280
1025
  }
1026
+ var DEEP_OBJECT_QUERY_KEY_REGEX = /^([^[\]]+)\[([^[\]]+)\]$/;
1027
+ function parseDeepObjectQueryKey(key) {
1028
+ const keyMatch = key.match(DEEP_OBJECT_QUERY_KEY_REGEX);
1029
+ return keyMatch ? { parameterName: keyMatch[1], property: keyMatch[2] } : void 0;
1030
+ }
1031
+ function getDeepObjectParameterValue(parameterName, query) {
1032
+ let objectValue;
1033
+ for (const [key, value] of query) {
1034
+ const deepObjectKey = parseDeepObjectQueryKey(key);
1035
+ if (deepObjectKey?.parameterName === parameterName) {
1036
+ objectValue ??= {};
1037
+ objectValue[deepObjectKey.property] = value;
1038
+ }
1039
+ }
1040
+ return objectValue;
1041
+ }
1281
1042
  function getActualParameterValue(parameter, context, cookies) {
1282
1043
  switch (parameter.in) {
1283
1044
  case "path":
1284
1045
  return context.matchedOperation?.pathParams[parameter.name];
1285
1046
  case "query": {
1047
+ if (parameter.style === "deepObject") {
1048
+ return getDeepObjectParameterValue(parameter.name, context.exchange.request.query);
1049
+ }
1286
1050
  const values = context.exchange.request.query.getAll(parameter.name);
1287
1051
  if (values.length === 0) {
1288
1052
  return void 0;
@@ -1308,29 +1072,38 @@ function createUndocumentedParameterFindings(context, matchedOperation) {
1308
1072
  header: /* @__PURE__ */ new Set(),
1309
1073
  cookie: /* @__PURE__ */ new Set()
1310
1074
  };
1075
+ const deepObjectQueryParams = /* @__PURE__ */ new Set();
1311
1076
  for (const parameter of matchedOperation.operation.requestParameters) {
1312
1077
  if (parameter.in === "header") {
1313
1078
  paramsByLocation.header.add(parameter.name.toLowerCase());
1314
1079
  } else if (parameter.in === "query" || parameter.in === "cookie") {
1315
1080
  paramsByLocation[parameter.in].add(parameter.name);
1081
+ if (parameter.in === "query" && parameter.style === "deepObject") {
1082
+ deepObjectQueryParams.add(parameter.name);
1083
+ }
1316
1084
  }
1317
1085
  }
1318
1086
  for (const name of new Set(context.exchange.request.query.keys())) {
1319
- if (!paramsByLocation.query.has(name)) {
1320
- findings.push({
1321
- ruleId: "schema-consistency",
1322
- severity: "warning",
1323
- category: "documentation",
1324
- message: `Undocumented query parameter in traffic: "${name}"`,
1325
- exchangeIndex: context.exchange.index,
1326
- operationId: matchedOperation.operation.operationId,
1327
- specSource: matchedOperation.operation.specSource,
1328
- target: "request"
1329
- });
1087
+ if (paramsByLocation.query.has(name)) {
1088
+ continue;
1330
1089
  }
1090
+ const deepObjectKey = parseDeepObjectQueryKey(name);
1091
+ if (deepObjectKey && deepObjectQueryParams.has(deepObjectKey.parameterName)) {
1092
+ continue;
1093
+ }
1094
+ findings.push({
1095
+ ruleId: "schema-consistency",
1096
+ severity: "warning",
1097
+ category: "documentation",
1098
+ message: `Undocumented query parameter in traffic: "${name}"`,
1099
+ exchangeIndex: context.exchange.index,
1100
+ operationId: matchedOperation.operation.operationId,
1101
+ specSource: matchedOperation.operation.specSource,
1102
+ target: "request"
1103
+ });
1331
1104
  }
1332
1105
  for (const headerName of Object.keys(context.exchange.request.headers)) {
1333
- if (shouldIgnoreHeaderAsUndocumented(headerName)) {
1106
+ if (shouldIgnoreHeaderAsUndocumented(headerName, context.ignoreHeaders)) {
1334
1107
  continue;
1335
1108
  }
1336
1109
  if (!paramsByLocation.header.has(headerName.toLowerCase())) {
@@ -1348,6 +1121,10 @@ function createUndocumentedParameterFindings(context, matchedOperation) {
1348
1121
  }
1349
1122
  return findings;
1350
1123
  }
1124
+ function isRequestRejectedByServer(context) {
1125
+ const response = context.exchange.response;
1126
+ return response !== void 0 && response.status >= 400 && response.status < 500;
1127
+ }
1351
1128
  function pickResponseSchema(context, matchedOperation) {
1352
1129
  const response = context.exchange.response;
1353
1130
  if (!response) {
@@ -1393,65 +1170,67 @@ var SchemaConsistencyRule = class {
1393
1170
  const findings = [];
1394
1171
  const cookies = parseCookies(context.exchange.request.headers.cookie);
1395
1172
  findings.push(...createUndocumentedParameterFindings(context, matchedOperation));
1396
- for (const parameter of matchedOperation.operation.requestParameters) {
1397
- if (context.ignoreCookies && parameter.in === "cookie") {
1398
- continue;
1173
+ if (!isRequestRejectedByServer(context)) {
1174
+ for (const parameter of matchedOperation.operation.requestParameters) {
1175
+ if (context.ignoreCookies && parameter.in === "cookie") {
1176
+ continue;
1177
+ }
1178
+ const actualValue = getActualParameterValue(parameter, context, cookies);
1179
+ if (parameter.required && (actualValue === void 0 || actualValue === null)) {
1180
+ findings.push({
1181
+ ruleId: this.id,
1182
+ severity: "error",
1183
+ category: "documentation",
1184
+ message: `Missing required ${parameter.in} parameter: "${parameter.name}"`,
1185
+ exchangeIndex: context.exchange.index,
1186
+ operationId: matchedOperation.operation.operationId,
1187
+ specSource: matchedOperation.operation.specSource,
1188
+ target: "request"
1189
+ });
1190
+ continue;
1191
+ }
1192
+ validateParameter(parameter, actualValue, context, findings);
1399
1193
  }
1400
- const actualValue = getActualParameterValue(parameter, context, cookies);
1401
- if (parameter.required && (actualValue === void 0 || actualValue === null)) {
1194
+ const requestContentType = context.exchange.request.contentType;
1195
+ const requestSchema = pickSchemaByMime(
1196
+ matchedOperation.operation.requestBodyContent,
1197
+ requestContentType
1198
+ );
1199
+ const hasRequestBody = hasBodyContent(context.exchange.request.bodyText);
1200
+ if (matchedOperation.operation.requestBodyRequired && !hasRequestBody) {
1402
1201
  findings.push({
1403
1202
  ruleId: this.id,
1404
1203
  severity: "error",
1405
1204
  category: "documentation",
1406
- message: `Missing required ${parameter.in} parameter: "${parameter.name}"`,
1205
+ message: "Missing required request body",
1407
1206
  exchangeIndex: context.exchange.index,
1408
1207
  operationId: matchedOperation.operation.operationId,
1409
1208
  specSource: matchedOperation.operation.specSource,
1410
1209
  target: "request"
1411
1210
  });
1412
- continue;
1413
1211
  }
1414
- validateParameter(parameter, actualValue, context, findings);
1415
- }
1416
- const requestContentType = context.exchange.request.contentType;
1417
- const requestSchema = pickSchemaByMime(
1418
- matchedOperation.operation.requestBodyContent,
1419
- requestContentType
1420
- );
1421
- const hasRequestBody = hasBodyContent(context.exchange.request.bodyText);
1422
- if (matchedOperation.operation.requestBodyRequired && !hasRequestBody) {
1423
- findings.push({
1424
- ruleId: this.id,
1425
- severity: "error",
1426
- category: "documentation",
1427
- message: "Missing required request body",
1428
- exchangeIndex: context.exchange.index,
1429
- operationId: matchedOperation.operation.operationId,
1430
- specSource: matchedOperation.operation.specSource,
1431
- target: "request"
1432
- });
1433
- }
1434
- if (requestSchema && hasRequestBody && isJsonMime(requestContentType)) {
1435
- if (context.exchange.request.bodyJson === void 0) {
1436
- findings.push({
1437
- ruleId: this.id,
1438
- severity: "error",
1439
- category: "schema",
1440
- message: "Request body is not valid JSON for JSON content-type",
1441
- exchangeIndex: context.exchange.index,
1442
- operationId: matchedOperation.operation.operationId,
1443
- specSource: matchedOperation.operation.specSource,
1444
- target: "request"
1445
- });
1446
- } else {
1447
- findings.push(
1448
- ...validateSchemaResult(
1449
- requestSchema,
1450
- context.exchange.request.bodyJson,
1451
- context,
1452
- "request"
1453
- )
1454
- );
1212
+ if (requestSchema && hasRequestBody && isJsonMime(requestContentType)) {
1213
+ if (context.exchange.request.bodyJson === void 0) {
1214
+ findings.push({
1215
+ ruleId: this.id,
1216
+ severity: "error",
1217
+ category: "schema",
1218
+ message: "Request body is not valid JSON for JSON content-type",
1219
+ exchangeIndex: context.exchange.index,
1220
+ operationId: matchedOperation.operation.operationId,
1221
+ specSource: matchedOperation.operation.specSource,
1222
+ target: "request"
1223
+ });
1224
+ } else {
1225
+ findings.push(
1226
+ ...validateSchemaResult(
1227
+ requestSchema,
1228
+ context.exchange.request.bodyJson,
1229
+ context,
1230
+ "request"
1231
+ )
1232
+ );
1233
+ }
1455
1234
  }
1456
1235
  }
1457
1236
  const responseSchema = pickResponseSchema(context, matchedOperation);
@@ -1732,7 +1511,9 @@ function createSecuritySummary(issues) {
1732
1511
  if (optionSummaries.length === 1) {
1733
1512
  return `Authentication check failed. ${optionSummaries[0]}`;
1734
1513
  }
1735
- return `None of the documented authentication options matched. Any one of these options would satisfy the OpenAPI security requirements: ${optionSummaries.join(" | ")}`;
1514
+ return `None of the documented authentication options matched. Any one of these options would satisfy the OpenAPI security requirements: ${optionSummaries.join(
1515
+ " | "
1516
+ )}`;
1736
1517
  }
1737
1518
  function getSensitiveQueryKeys(context) {
1738
1519
  return Array.from(context.exchange.request.query.keys()).filter((key) => {
@@ -1740,8 +1521,21 @@ function getSensitiveQueryKeys(context) {
1740
1521
  return normalizedKey.includes("token") || normalizedKey.includes("apikey") || normalizedKey.includes("api_key") || normalizedKey.includes("access_key");
1741
1522
  });
1742
1523
  }
1524
+ var LOOPBACK_HOSTNAME_PATTERN = /^(?:(?:.+\.)?localhost|127(?:\.\d{1,3}){3}|::1)$/i;
1525
+ function isLoopbackRequest(request) {
1526
+ let hostname;
1527
+ try {
1528
+ hostname = new URL(request.url).hostname;
1529
+ } catch {
1530
+ hostname = request.host?.replace(/:\d+$/, "");
1531
+ }
1532
+ if (!hostname) {
1533
+ return false;
1534
+ }
1535
+ return LOOPBACK_HOSTNAME_PATTERN.test(hostname.replace(/^\[|\]$/g, ""));
1536
+ }
1743
1537
  function shouldFlagInsecureTransport(context) {
1744
- if (context.exchange.request.protocol !== "http:" || !context.exchange.request.protocolKnown) {
1538
+ if (context.exchange.request.protocol !== "http:" || !context.exchange.request.protocolKnown || isLoopbackRequest(context.exchange.request)) {
1745
1539
  return {
1746
1540
  flag: false,
1747
1541
  hasAuthHeader: false,
@@ -1835,9 +1629,9 @@ var UndocumentedEndpointRule = class {
1835
1629
  if (context.matchedOperation) {
1836
1630
  return [];
1837
1631
  }
1838
- const { method, path: path3, host } = context.exchange.request;
1632
+ const { method, path: path2, host } = context.exchange.request;
1839
1633
  const isHostMismatch = context.matchMode === "strict-host" && !context.hostCompatibleWithSpecServers;
1840
- const message = isHostMismatch ? `Undocumented server: ${method} ${path3} was sent to "${host}", which does not match any server in the description` : `Undocumented endpoint: ${method} ${path3}`;
1634
+ const message = isHostMismatch ? `Undocumented server: ${method} ${path2} was sent to "${host}", which does not match any server in the description` : `Undocumented endpoint: ${method} ${path2}`;
1841
1635
  return [
1842
1636
  {
1843
1637
  ruleId: this.id,
@@ -1879,34 +1673,6 @@ function loadRules(activeRuleIds) {
1879
1673
  });
1880
1674
  }
1881
1675
 
1882
- // src/commands/drift/utils/server.ts
1883
- function normalizeServerPrefix(server) {
1884
- const trimmed = server?.replace(/\/+$/, "");
1885
- return trimmed || void 0;
1886
- }
1887
- function stripPrefixFromPath(pathname, prefixPath) {
1888
- if (!prefixPath || prefixPath === "/") {
1889
- return pathname || "/";
1890
- }
1891
- if (pathname === prefixPath) {
1892
- return "/";
1893
- }
1894
- if (!pathname.startsWith(`${prefixPath}/`)) {
1895
- return void 0;
1896
- }
1897
- return pathname.slice(prefixPath.length) || "/";
1898
- }
1899
- function resolvePathForServer(request, server) {
1900
- if (server.startsWith("/")) {
1901
- return stripPrefixFromPath(request.path, server);
1902
- }
1903
- const serverUrl = parseUrl(server.includes("://") ? server : `http://${server}`);
1904
- if (isSyntheticHost(serverUrl.host) || request.host !== void 0 && request.host.toLowerCase() !== serverUrl.host) {
1905
- return void 0;
1906
- }
1907
- return stripPrefixFromPath(request.path, getPathWithoutTrailingSlash(serverUrl.pathname));
1908
- }
1909
-
1910
1676
  // src/commands/drift/engine/schema-validator.ts
1911
1677
  var import__ = __toESM(require__(), 1);
1912
1678
  var import_ajv_formats = __toESM(require_dist(), 1);
@@ -2175,6 +1941,7 @@ var ValidationSession = class _ValidationSession {
2175
1941
  problemKeyStats = /* @__PURE__ */ new Map();
2176
1942
  totalProblemGroups = 0;
2177
1943
  server;
1944
+ ignoreHeaders;
2178
1945
  minSeverityRank;
2179
1946
  specServerHosts;
2180
1947
  hasHostlessSpecServer;
@@ -2182,6 +1949,7 @@ var ValidationSession = class _ValidationSession {
2182
1949
  this.options = options;
2183
1950
  this.rules = rules;
2184
1951
  this.server = normalizeServerPrefix(options.server);
1952
+ this.ignoreHeaders = options.ignoreHeaders ? parseHeaderIgnoreList(options.ignoreHeaders) : void 0;
2185
1953
  const { specServerHosts, hasHostlessSpecServer } = collectSpecServerHosts(options.openApiIndex);
2186
1954
  this.specServerHosts = specServerHosts;
2187
1955
  this.hasHostlessSpecServer = hasHostlessSpecServer;
@@ -2229,6 +1997,7 @@ var ValidationSession = class _ValidationSession {
2229
1997
  matchMode: this.options.matchMode,
2230
1998
  hostCompatibleWithSpecServers: relativePathOverride !== void 0 || hostCompatible,
2231
1999
  ignoreCookies: this.options.ignoreCookies ?? false,
2000
+ ignoreHeaders: this.ignoreHeaders,
2232
2001
  validateSchema: (schema, value, options) => options?.coerce ? this.coercingSchemaValidator.validate(schema, value, options?.target) : this.schemaValidator.validate(schema, value, options?.target)
2233
2002
  });
2234
2003
  const records = [];
@@ -2295,7 +2064,7 @@ var ValidationSession = class _ValidationSession {
2295
2064
  };
2296
2065
 
2297
2066
  // src/commands/drift/openapi/loader.ts
2298
- import { stat as stat2 } from "node:fs/promises";
2067
+ import { stat } from "node:fs/promises";
2299
2068
 
2300
2069
  // src/commands/drift/utils/openapi.ts
2301
2070
  function resolveServerUrl(rawUrl, variables) {
@@ -2351,6 +2120,7 @@ function normalizeParameters(parameters) {
2351
2120
  name: String(entry.name ?? ""),
2352
2121
  in: location,
2353
2122
  required: Boolean(entry.required) || location === "path",
2123
+ style: typeof entry.style === "string" ? entry.style : void 0,
2354
2124
  schema: entry.schema
2355
2125
  });
2356
2126
  }
@@ -2570,7 +2340,7 @@ function finalizeIndex(operationsByMethod, loadedSpecs) {
2570
2340
  return { operationsByMethod, loadedSpecs, loadedOperations };
2571
2341
  }
2572
2342
  async function resolveSpecFiles(specPath) {
2573
- const stats = await stat2(specPath);
2343
+ const stats = await stat(specPath);
2574
2344
  if (stats.isDirectory()) {
2575
2345
  return { specFiles: await listOpenApiFiles(specPath), fromDirectory: true };
2576
2346
  }
@@ -2626,162 +2396,8 @@ function parseCsv(input) {
2626
2396
  return input.split(",").map((value) => value.trim()).filter(Boolean);
2627
2397
  }
2628
2398
 
2629
- // src/commands/drift/log-formats/helpers.ts
2630
- import { createReadStream } from "node:fs";
2631
- import { readFile } from "node:fs/promises";
2632
- import { createInterface } from "node:readline";
2633
- function coerceString(value) {
2634
- if (value === void 0 || value === null) {
2635
- return void 0;
2636
- }
2637
- if (typeof value === "string") {
2638
- return value;
2639
- }
2640
- if (Buffer.isBuffer(value)) {
2641
- return value.toString("utf8");
2642
- }
2643
- if (isPlainObject(value) || Array.isArray(value)) {
2644
- try {
2645
- return JSON.stringify(value);
2646
- } catch {
2647
- return String(value);
2648
- }
2649
- }
2650
- return String(value);
2651
- }
2652
- function coerceNumber(value) {
2653
- if (typeof value === "number" && Number.isFinite(value)) {
2654
- return value;
2655
- }
2656
- if (typeof value === "string" && value.trim() !== "") {
2657
- const parsed = Number(value);
2658
- if (Number.isFinite(parsed)) {
2659
- return parsed;
2660
- }
2661
- }
2662
- return void 0;
2663
- }
2664
- function decodeBody(value, encoding) {
2665
- if (value === void 0 || value === null) {
2666
- return void 0;
2667
- }
2668
- if (encoding === "base64" && typeof value === "string") {
2669
- try {
2670
- return Buffer.from(value, "base64").toString("utf8");
2671
- } catch {
2672
- return value;
2673
- }
2674
- }
2675
- return coerceString(value);
2676
- }
2677
- function createNormalizedExchange(seed, index, source) {
2678
- const method = seed.method?.toUpperCase();
2679
- const url = seed.url;
2680
- if (!method || !url) {
2681
- return null;
2682
- }
2683
- const requestHeaders = normalizeHeaders(seed.requestHeaders);
2684
- let parsedUrl = parseUrl(url);
2685
- if (isSyntheticHost(parsedUrl.host) && requestHeaders.host) {
2686
- try {
2687
- parsedUrl = new URL(
2688
- `${parsedUrl.protocol}//${requestHeaders.host}${parsedUrl.pathname}${parsedUrl.search}`
2689
- );
2690
- } catch {
2691
- parsedUrl = parseUrl(url);
2692
- }
2693
- }
2694
- const requestContentType = seed.requestContentType ?? requestHeaders["content-type"];
2695
- const requestBodyText = decodeBody(seed.requestBody);
2696
- const request = {
2697
- method,
2698
- url: parsedUrl.toString(),
2699
- path: parsedUrl.pathname,
2700
- query: parsedUrl.searchParams,
2701
- protocol: parsedUrl.protocol,
2702
- protocolKnown: seed.schemeKnown ?? /^https?:\/\//i.test(url),
2703
- host: isSyntheticHost(parsedUrl.host) ? void 0 : parsedUrl.host || void 0,
2704
- headers: requestHeaders,
2705
- contentType: requestContentType,
2706
- bodyText: requestBodyText,
2707
- bodyJson: parseJsonBodyIfPresent(requestContentType, requestBodyText)
2708
- };
2709
- let response;
2710
- const responseStatus = seed.responseStatus;
2711
- if (responseStatus !== void 0) {
2712
- const responseHeaders = normalizeHeaders(seed.responseHeaders);
2713
- const responseContentType = seed.responseContentType ?? responseHeaders["content-type"];
2714
- const responseBodyText = decodeBody(seed.responseBody);
2715
- response = {
2716
- status: responseStatus,
2717
- statusText: seed.responseStatusText,
2718
- headers: responseHeaders,
2719
- contentType: responseContentType,
2720
- bodyText: responseBodyText,
2721
- bodyJson: parseJsonBodyIfPresent(responseContentType, responseBodyText)
2722
- };
2723
- }
2724
- return {
2725
- index,
2726
- source,
2727
- startedAt: seed.startedAt,
2728
- request,
2729
- response,
2730
- raw: seed.raw
2731
- };
2732
- }
2733
- async function* streamNdjsonObjects(filePath) {
2734
- const readStream = createReadStream(filePath, { encoding: "utf8" });
2735
- const reader = createInterface({ input: readStream, crlfDelay: Infinity });
2736
- for await (const line of reader) {
2737
- const trimmed = line.trim();
2738
- if (!trimmed) {
2739
- continue;
2740
- }
2741
- try {
2742
- const parsed = JSON.parse(trimmed);
2743
- if (isPlainObject(parsed)) {
2744
- yield parsed;
2745
- }
2746
- } catch {
2747
- }
2748
- }
2749
- }
2750
- async function* iterateJsonArray(filePath, arrayPath) {
2751
- const content = await readFile(filePath, "utf8");
2752
- let value = JSON.parse(content);
2753
- if (arrayPath) {
2754
- for (const key of arrayPath.split(".")) {
2755
- value = isPlainObject(value) ? value[key] : void 0;
2756
- }
2757
- }
2758
- if (Array.isArray(value)) {
2759
- for (const item of value) {
2760
- if (isPlainObject(item)) {
2761
- yield item;
2762
- }
2763
- }
2764
- }
2765
- }
2766
- function pickHeaderContentType(headers) {
2767
- const normalized = normalizeHeaders(headers);
2768
- return normalized["content-type"];
2769
- }
2770
-
2771
2399
  export {
2772
2400
  renderReport,
2773
- readProbe,
2774
- normalizeFsPath,
2775
- listFilesRecursively,
2776
- normalizeContentType,
2777
- isJsonMime,
2778
- coerceString,
2779
- coerceNumber,
2780
- decodeBody,
2781
- createNormalizedExchange,
2782
- streamNdjsonObjects,
2783
- iterateJsonArray,
2784
- pickHeaderContentType,
2785
2401
  ValidationSession,
2786
2402
  loadOpenApiIndex,
2787
2403
  parseCsv