@kubb/core 5.11.6 → 5.12.1

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/dist/index.cjs CHANGED
@@ -32,8 +32,12 @@ node_path = __toESM(node_path, 1);
32
32
  let node_fs = require("node:fs");
33
33
  let node_util = require("node:util");
34
34
  let _kubb_ast = require("@kubb/ast");
35
- let node_crypto = require("node:crypto");
35
+ let node_dns = require("node:dns");
36
+ node_dns = __toESM(node_dns, 1);
37
+ let node_net = require("node:net");
38
+ node_net = __toESM(node_net, 1);
36
39
  let node_os = require("node:os");
40
+ let node_crypto = require("node:crypto");
37
41
  let node_readline = require("node:readline");
38
42
  let unstorage = require("unstorage");
39
43
  let unstorage_drivers_fs = require("unstorage/drivers/fs");
@@ -41,7 +45,7 @@ unstorage_drivers_fs = __toESM(unstorage_drivers_fs, 1);
41
45
  let node_url = require("node:url");
42
46
  let node_module = require("node:module");
43
47
  //#region package.json
44
- var version = "5.11.6";
48
+ var version = "5.12.1";
45
49
  //#endregion
46
50
  //#region ../../internals/utils/src/casing.ts
47
51
  /**
@@ -529,6 +533,12 @@ const diagnosticCatalog = {
529
533
  cause: "A URL set as `input` (or reached through a `$ref`) never answered, so the request failed before a status came back. A refused connection, an unknown host, an expired certificate, and a timeout all land here.",
530
534
  fix: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`."
531
535
  },
536
+ gitInputFailed: {
537
+ code: "KUBB_GIT_INPUT_FAILED",
538
+ title: "Git input failed",
539
+ cause: "A git `input` (`git+<repo>#<ref>:<path>`) is malformed, or `git` could not fetch it: git is not installed, the repository or ref does not exist, or the credentials were refused.",
540
+ fix: "Write the input as `git+<repo>#<ref>:<path>`. Run `git fetch --depth 1 <repo> <ref>` yourself to see the git error, and set up a credential helper or SSH key for a private repository."
541
+ },
532
542
  inputRequired: {
533
543
  code: "KUBB_INPUT_REQUIRED",
534
544
  title: "Input required",
@@ -1417,18 +1427,209 @@ function definePlugin(definition) {
1417
1427
  });
1418
1428
  }
1419
1429
  //#endregion
1430
+ //#region src/publicAddress.ts
1431
+ function createBlockList() {
1432
+ const list = new node_net.default.BlockList();
1433
+ for (const [address, prefix] of [
1434
+ ["0.0.0.0", 8],
1435
+ ["10.0.0.0", 8],
1436
+ ["100.64.0.0", 10],
1437
+ ["127.0.0.0", 8],
1438
+ ["169.254.0.0", 16],
1439
+ ["172.16.0.0", 12],
1440
+ ["192.0.0.0", 24],
1441
+ ["192.168.0.0", 16],
1442
+ ["198.18.0.0", 15],
1443
+ ["224.0.0.0", 3]
1444
+ ]) list.addSubnet(address, prefix, "ipv4");
1445
+ for (const [address, prefix] of [
1446
+ ["::", 128],
1447
+ ["::1", 128],
1448
+ ["64:ff9b::", 96],
1449
+ ["fc00::", 7],
1450
+ ["fe80::", 10],
1451
+ ["ff00::", 8]
1452
+ ]) list.addSubnet(address, prefix, "ipv6");
1453
+ return list;
1454
+ }
1455
+ const privateAddresses = createBlockList();
1456
+ /**
1457
+ * Whether `address` is a routable public IP. Loopback, private, link-local, cloud metadata, and
1458
+ * multicast ranges are not, and an IPv4-mapped IPv6 address counts as the IPv4 address it wraps.
1459
+ * Pass `blocked` to replace the ranges.
1460
+ */
1461
+ function isPublicAddress(address, blocked = privateAddresses) {
1462
+ const family = node_net.default.isIP(address);
1463
+ return family !== 0 && !blocked.check(address, family === 4 ? "ipv4" : "ipv6");
1464
+ }
1465
+ //#endregion
1466
+ //#region src/gitInput.ts
1467
+ /** Matches a git `input`: the `git+` prefix npm uses for git dependencies. */
1468
+ const gitInputRegExp = /^git\+/i;
1469
+ function gitError(message, cause) {
1470
+ return new Diagnostics.Error({
1471
+ code: Diagnostics.code.gitInputFailed,
1472
+ severity: "error",
1473
+ message,
1474
+ help: "Write a git `input` as `git+<repo>#<ref>:<path>`, for example `git+https://github.com/org/api.git#main:specs/openapi.yaml`. Check that `git` is installed and that `git fetch` can reach the repository with your credentials.",
1475
+ location: { kind: "config" },
1476
+ cause: cause === void 0 ? void 0 : toError(cause)
1477
+ });
1478
+ }
1479
+ /** The repository URL without a user or password, safe to print. */
1480
+ function redact(repo) {
1481
+ if (!URL.canParse(repo)) return repo;
1482
+ const url = new URL(repo);
1483
+ url.username = "";
1484
+ url.password = "";
1485
+ return url.href;
1486
+ }
1487
+ /**
1488
+ * Splits `git+<repo>#<ref>:<path>` into its parts. Git refs cannot contain `:`, so the first one
1489
+ * in the fragment ends the ref. Throws `KUBB_GIT_INPUT_FAILED` when the fragment or path is missing.
1490
+ */
1491
+ function parseGitInput(input) {
1492
+ const hashIndex = input.indexOf("#");
1493
+ const repo = input.slice(4, hashIndex === -1 ? void 0 : hashIndex);
1494
+ const fragment = hashIndex === -1 ? "" : input.slice(hashIndex + 1);
1495
+ const colonIndex = fragment.indexOf(":");
1496
+ const ref = colonIndex === -1 ? void 0 : fragment.slice(0, colonIndex);
1497
+ const path = colonIndex === -1 ? fragment : fragment.slice(colonIndex + 1);
1498
+ if (!repo || !path || ref === "") throw gitError(`The git \`input\` for ${redact(repo)} has no repository, ref, or spec path after \`#\`.`);
1499
+ return {
1500
+ repo,
1501
+ ref,
1502
+ path
1503
+ };
1504
+ }
1505
+ /**
1506
+ * The `git -c` settings that confine a fetch to https and to `addresses`: the connection goes to
1507
+ * the addresses already checked, whatever the host answers to a second lookup, and a redirect to
1508
+ * another host is an error. Needs git 2.37 for the pinning.
1509
+ *
1510
+ * @internal
1511
+ */
1512
+ function publicFetchConfig({ host, port, addresses }) {
1513
+ const pinned = addresses.map((address) => node_net.default.isIPv6(address) ? `[${address}]` : address).join(",");
1514
+ const config = [
1515
+ "http.followRedirects=false",
1516
+ "protocol.allow=never",
1517
+ "protocol.https.allow=always"
1518
+ ];
1519
+ if (!node_net.default.isIP(host)) config.push(`http.curloptResolve=${host}:${port}:${pinned}`);
1520
+ return config.flatMap((setting) => ["-c", setting]);
1521
+ }
1522
+ async function resolvePublicFetchConfig(repo, signal) {
1523
+ if (!URL.canParse(repo) || new URL(repo).protocol !== "https:") throw gitError("A public git `input` must be an https repository URL, not an ssh, git, or file address.");
1524
+ const url = new URL(repo);
1525
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1526
+ const isLiteral = node_net.default.isIP(host) !== 0;
1527
+ const addresses = isLiteral ? [host] : (await node_dns.default.promises.lookup(host, { all: true })).map(({ address }) => address);
1528
+ if (!addresses.length || addresses.some((address) => !isPublicAddress(address))) throw gitError(`${host} is not a public address, so this git \`input\` is not fetched.`);
1529
+ if (!isLiteral) {
1530
+ const { stdout } = await (0, tinyexec.x)("git", ["--version"], {
1531
+ nodeOptions: { signal },
1532
+ throwOnError: true
1533
+ });
1534
+ const [, major = 0, minor = 0] = /(\d+)\.(\d+)/.exec(stdout)?.map(Number) ?? [];
1535
+ if (major < 2 || major === 2 && minor < 37) throw gitError(`Fetching a public git \`input\` needs git 2.37 or newer, found ${stdout.trim()}.`);
1536
+ }
1537
+ return publicFetchConfig({
1538
+ host,
1539
+ port: url.port || "443",
1540
+ addresses
1541
+ });
1542
+ }
1543
+ /**
1544
+ * Fetches one commit of a git repository into a temporary directory with a shallow `git fetch`,
1545
+ * so any host and the user's own git credentials work, and relative `$ref` files resolve.
1546
+ * Resolves to the spec path on disk and a `dispose` that deletes the checkout.
1547
+ *
1548
+ * With `publicOnly`, the repository must be an https URL on public addresses, the fetch connects to
1549
+ * the addresses checked, and it does not follow a redirect.
1550
+ */
1551
+ async function checkoutGitInput(input, signal, { publicOnly = false } = {}) {
1552
+ const { repo, ref, path } = parseGitInput(input);
1553
+ const fetchConfig = publicOnly ? await resolvePublicFetchConfig(repo, signal) : [];
1554
+ const dir = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "kubb-git-"));
1555
+ const dispose = () => (0, node_fs_promises.rm)(dir, {
1556
+ recursive: true,
1557
+ force: true
1558
+ });
1559
+ const specPath = (0, node_path.resolve)(dir, path);
1560
+ const relativePath = (0, node_path.relative)(dir, specPath);
1561
+ if (relativePath === ".." || relativePath.startsWith(`..${node_path.sep}`) || (0, node_path.isAbsolute)(relativePath)) {
1562
+ await dispose();
1563
+ throw gitError(`The spec path "${path}" in the git \`input\` points outside the repository.`);
1564
+ }
1565
+ const label = `${redact(repo)}${ref ? ` at ${ref}` : ""}`;
1566
+ const env = {
1567
+ ...node_process.default.env,
1568
+ GIT_TERMINAL_PROMPT: "0"
1569
+ };
1570
+ const steps = [
1571
+ ["init", "--quiet"],
1572
+ [
1573
+ "config",
1574
+ "core.symlinks",
1575
+ "false"
1576
+ ],
1577
+ [
1578
+ ...fetchConfig,
1579
+ "fetch",
1580
+ "--quiet",
1581
+ "--depth",
1582
+ "1",
1583
+ "--end-of-options",
1584
+ repo,
1585
+ ref ?? "HEAD"
1586
+ ],
1587
+ [
1588
+ "checkout",
1589
+ "--quiet",
1590
+ "FETCH_HEAD"
1591
+ ]
1592
+ ];
1593
+ try {
1594
+ for (const args of steps) {
1595
+ const result = await (0, tinyexec.x)("git", args, {
1596
+ nodeOptions: {
1597
+ cwd: dir,
1598
+ env,
1599
+ signal
1600
+ },
1601
+ throwOnError: false
1602
+ });
1603
+ if (result.exitCode !== 0) {
1604
+ const stderr = result.stderr.replaceAll(repo, redact(repo)).trim();
1605
+ throw gitError(`\`git ${args[0]}\` failed for ${label}: ${stderr || `exit code ${result.exitCode}`}`);
1606
+ }
1607
+ }
1608
+ } catch (error) {
1609
+ await dispose();
1610
+ if (Diagnostics.isError(error)) throw error;
1611
+ throw gitError(`Cannot run \`git\` for ${label}: ${getErrorMessage(error)}`, error);
1612
+ }
1613
+ return {
1614
+ path: specPath,
1615
+ dispose
1616
+ };
1617
+ }
1618
+ //#endregion
1420
1619
  //#region src/input.ts
1421
1620
  /**
1422
1621
  * Classifies an `input` value so callers branch on it once instead of repeating the checks.
1423
1622
  *
1424
1623
  * A non-string is a parsed spec (`object`). A string is `inline` when it holds OpenAPI content,
1425
1624
  * meaning it starts with `{` or `[`, spans multiple lines, or opens with a YAML `openapi:` or
1426
- * `swagger:` key. Otherwise a string is a `url` when it parses as one, or a `file` path.
1625
+ * `swagger:` key. Otherwise a string is `git` when it starts with `git+`, a `url` when it parses as
1626
+ * one, or a `file` path.
1427
1627
  */
1428
1628
  function getInputKind(input) {
1429
1629
  if (typeof input !== "string") return "object";
1430
1630
  const trimmed = input.trimStart();
1431
1631
  if (trimmed.startsWith("{") || trimmed.startsWith("[") || input.includes("\n") || /^(openapi|swagger)\s*:/i.test(trimmed)) return "inline";
1632
+ if (gitInputRegExp.test(input)) return "git";
1432
1633
  if (URL.canParse(input)) return "url";
1433
1634
  return "file";
1434
1635
  }
@@ -1452,8 +1653,9 @@ function isLegacyInput(input) {
1452
1653
  /**
1453
1654
  * Normalizes `config.input` into an `AdapterSource` the adapter can parse.
1454
1655
  *
1455
- * A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
1456
- * local path is resolved against `config.root`, both as `{ type: 'path' }`.
1656
+ * A parsed object and inline content become `{ type: 'data' }`; a URL or git input is kept
1657
+ * verbatim and a local path is resolved against `config.root`, all as `{ type: 'path' }`. The
1658
+ * driver checks out a git input before the adapter parses it.
1457
1659
  */
1458
1660
  function inputToAdapterSource(config) {
1459
1661
  const input = config.input;
@@ -1480,7 +1682,7 @@ function inputToAdapterSource(config) {
1480
1682
  type: "data",
1481
1683
  data: input
1482
1684
  };
1483
- if (kind === "url") return {
1685
+ if (kind === "url" || kind === "git") return {
1484
1686
  type: "path",
1485
1687
  path: input
1486
1688
  };
@@ -2414,13 +2616,20 @@ var KubbDriver = class {
2414
2616
  async #parseInput() {
2415
2617
  if (this.inputNode || !this.adapter || !this.#adapterSource) return;
2416
2618
  this.options.signal?.throwIfAborted();
2619
+ const source = this.#adapterSource;
2620
+ const checkout = source.type === "path" && gitInputRegExp.test(source.path) ? await checkoutGitInput(source.path, this.options.signal) : void 0;
2417
2621
  try {
2418
- this.inputNode = await this.adapter.parse(this.#adapterSource, { signal: this.options.signal });
2622
+ this.inputNode = await this.adapter.parse(checkout ? {
2623
+ type: "path",
2624
+ path: checkout.path
2625
+ } : source, { signal: this.options.signal });
2419
2626
  } catch (caughtError) {
2420
2627
  throw new Diagnostics.Error(Diagnostics.fromExtension(caughtError, {
2421
2628
  kind: "adapter",
2422
2629
  extension: this.adapter
2423
2630
  }));
2631
+ } finally {
2632
+ await checkout?.dispose();
2424
2633
  }
2425
2634
  }
2426
2635
  /**
@@ -4262,6 +4471,7 @@ exports.addMacro = addMacro;
4262
4471
  exports.assertPluginOption = assertPluginOption;
4263
4472
  exports.cacheStorage = cacheStorage;
4264
4473
  exports.checkCompatibility = checkCompatibility;
4474
+ exports.checkoutGitInput = checkoutGitInput;
4265
4475
  exports.cliReporter = cliReporter;
4266
4476
  exports.createAdapter = createAdapter;
4267
4477
  exports.createCliReporter = createCliReporter;
@@ -4282,6 +4492,7 @@ exports.htmlReporter = htmlReporter;
4282
4492
  exports.injectFile = injectFile;
4283
4493
  exports.isExtensionManifest = isExtensionManifest;
4284
4494
  exports.isPackageName = isPackageName;
4495
+ exports.isPublicAddress = isPublicAddress;
4285
4496
  exports.isRegistryEntry = isRegistryEntry;
4286
4497
  exports.jsonReporter = jsonReporter;
4287
4498
  exports.kubbContext = kubbContext;