@kubb/studio 5.12.0 → 5.12.2

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
@@ -23,22 +23,32 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  const require_protocol = require("./protocol.cjs");
25
25
  let node_crypto = require("node:crypto");
26
+ let node_fs = require("node:fs");
27
+ let node_module = require("node:module");
26
28
  let node_os = require("node:os");
27
29
  let node_path = require("node:path");
28
30
  node_path = __toESM(node_path, 1);
29
31
  let node_process = require("node:process");
30
32
  node_process = __toESM(node_process, 1);
31
33
  let node_timers_promises = require("node:timers/promises");
32
- let node_url = require("node:url");
33
- let node_fs = require("node:fs");
34
+ let tinyexec = require("tinyexec");
34
35
  let node_fs_promises = require("node:fs/promises");
35
36
  let _kubb_core = require("@kubb/core");
36
37
  let capnweb = require("capnweb");
37
38
  let magicast = require("magicast");
38
- let node_module = require("node:module");
39
+ let node_url = require("node:url");
39
40
  let diff = require("diff");
40
41
  let node_util = require("node:util");
41
42
  let _kubb_ast = require("@kubb/ast");
43
+ let node_dns = require("node:dns");
44
+ node_dns = __toESM(node_dns, 1);
45
+ let node_http = require("node:http");
46
+ node_http = __toESM(node_http, 1);
47
+ let node_https = require("node:https");
48
+ node_https = __toESM(node_https, 1);
49
+ let node_net = require("node:net");
50
+ node_net = __toESM(node_net, 1);
51
+ let node_stream = require("node:stream");
42
52
  let ofetch = require("ofetch");
43
53
  let node_zlib = require("node:zlib");
44
54
  //#region ../../internals/utils/src/casing.ts
@@ -220,7 +230,7 @@ function getElapsedMs(hrStart) {
220
230
  }
221
231
  //#endregion
222
232
  //#region package.json
223
- var version = "5.12.0";
233
+ var version = "5.12.2";
224
234
  //#endregion
225
235
  //#region src/resolveConfig.ts
226
236
  /**
@@ -1866,6 +1876,106 @@ function createGenerationStream(hooks, jobId) {
1866
1876
  };
1867
1877
  }
1868
1878
  //#endregion
1879
+ //#region src/publicFetch.ts
1880
+ const MAX_REDIRECTS = 5;
1881
+ const MAX_BYTES = 52428800;
1882
+ const REDIRECTS = /* @__PURE__ */ new Set([
1883
+ 301,
1884
+ 302,
1885
+ 303,
1886
+ 307,
1887
+ 308
1888
+ ]);
1889
+ const NO_BODY = /* @__PURE__ */ new Set([
1890
+ 204,
1891
+ 205,
1892
+ 304
1893
+ ]);
1894
+ function notPublic(host) {
1895
+ return /* @__PURE__ */ new Error(`${host} is not a public address, so a managed agent does not fetch from it`);
1896
+ }
1897
+ function createLookup(blocked) {
1898
+ return (hostname, options, callback) => {
1899
+ node_dns.default.lookup(hostname, {
1900
+ ...options,
1901
+ all: true
1902
+ }, (error, addresses) => {
1903
+ if (error) return callback(error, "", 0);
1904
+ const found = addresses;
1905
+ if (!found.length || found.some(({ address }) => !(0, _kubb_core.isPublicAddress)(address, blocked))) return callback(notPublic(hostname), "", 0);
1906
+ return options.all ? callback(null, found) : callback(null, found[0].address, found[0].family);
1907
+ });
1908
+ };
1909
+ }
1910
+ function request({ url, blocked, signal }) {
1911
+ if (url.protocol !== "http:" && url.protocol !== "https:") return Promise.reject(/* @__PURE__ */ new Error(`Only http and https URLs are fetched, not ${url.protocol}`));
1912
+ const host = url.hostname.replace(/^\[|\]$/g, "");
1913
+ if (node_net.default.isIP(host) && !(0, _kubb_core.isPublicAddress)(host, blocked)) return Promise.reject(notPublic(host));
1914
+ return new Promise((resolve, reject) => {
1915
+ const req = (url.protocol === "https:" ? node_https.default.request : node_http.default.request)(url, {
1916
+ method: "GET",
1917
+ lookup: createLookup(blocked),
1918
+ signal: signal ?? void 0,
1919
+ headers: { "accept-encoding": "identity" }
1920
+ }, (res) => {
1921
+ const headers = new Headers();
1922
+ for (const [key, value] of Object.entries(res.headers)) for (const item of [value].flat()) if (item !== void 0) headers.append(key, item);
1923
+ let total = 0;
1924
+ const limit = new node_stream.Transform({ transform(chunk, _encoding, done) {
1925
+ total += chunk.length;
1926
+ done(total > MAX_BYTES ? /* @__PURE__ */ new Error("The response is larger than 50 MB") : null, chunk);
1927
+ } });
1928
+ res.on("error", (error) => limit.destroy(error));
1929
+ res.pipe(limit);
1930
+ const body = NO_BODY.has(res.statusCode ?? 0) ? null : node_stream.Readable.toWeb(limit);
1931
+ resolve(new Response(body, {
1932
+ status: res.statusCode,
1933
+ statusText: res.statusMessage,
1934
+ headers
1935
+ }));
1936
+ });
1937
+ req.on("error", reject);
1938
+ req.end();
1939
+ });
1940
+ }
1941
+ /**
1942
+ * Builds a `fetch` that only reaches addresses `blocked` does not list. It follows redirects itself
1943
+ * and checks every hop, and takes GET requests only, since that is all a spec load needs.
1944
+ */
1945
+ function createPublicFetch(blocked) {
1946
+ const publicFetch = async (input, init) => {
1947
+ if (typeof input !== "string" && !(input instanceof URL)) throw new TypeError("A managed agent fetches by URL only");
1948
+ if (init?.method && init.method.toUpperCase() !== "GET") throw new TypeError("A managed agent only sends GET requests");
1949
+ let url = new URL(input);
1950
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
1951
+ const response = await request({
1952
+ url,
1953
+ blocked,
1954
+ signal: init?.signal
1955
+ });
1956
+ const location = response.headers.get("location");
1957
+ if (!REDIRECTS.has(response.status) || !location) return response;
1958
+ await response.body?.cancel();
1959
+ url = new URL(location, url);
1960
+ }
1961
+ throw new Error(`Stopped after ${MAX_REDIRECTS} redirects from ${String(input)}`);
1962
+ };
1963
+ return publicFetch;
1964
+ }
1965
+ /**
1966
+ * Runs `run` with the global `fetch` limited to public addresses, then puts the original back.
1967
+ * A managed agent runs one job at a time, so nothing else shares the window.
1968
+ */
1969
+ async function withPublicFetch(run) {
1970
+ const original = globalThis.fetch;
1971
+ globalThis.fetch = createPublicFetch();
1972
+ try {
1973
+ return await run();
1974
+ } finally {
1975
+ globalThis.fetch = original;
1976
+ }
1977
+ }
1978
+ //#endregion
1869
1979
  //#region src/url.ts
1870
1980
  /**
1871
1981
  * Hosted Kubb Studio URL, used when a host passes none.
@@ -1902,6 +2012,11 @@ const OPEN_TIMEOUT_MS = 5e3;
1902
2012
  const MAX_RETRY_DELAY_MS = 3e4;
1903
2013
  const DISK_SNAPSHOT_MAX_FILES = 1e4;
1904
2014
  const SANDBOX_GENERATION_TTL_MS = 9e5;
2015
+ const MANAGED_INPUT_KINDS = /* @__PURE__ */ new Set([
2016
+ "inline",
2017
+ "url",
2018
+ "git"
2019
+ ]);
1905
2020
  /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1906
2021
  const BUILT_IN_PACKAGES = [
1907
2022
  "@kubb/adapter-oas",
@@ -1927,6 +2042,43 @@ var IncompatibleAgentError = class extends Error {
1927
2042
  this.name = "IncompatibleAgentError";
1928
2043
  }
1929
2044
  };
2045
+ const INSTALL_TIMEOUT_MS = 3e5;
2046
+ /** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
2047
+ const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
2048
+ /** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
2049
+ function findKubbBin(root) {
2050
+ const require$1 = (0, node_module.createRequire)(node_path.default.resolve(root, "package.json"));
2051
+ const names = ["kubb", "@kubb/cli"];
2052
+ return (require$1.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => node_path.default.join(dir, name, "bin", "kubb.js"))).find((bin) => (0, node_fs.existsSync)(bin));
2053
+ }
2054
+ /** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
2055
+ async function runKubb({ bin, args, cwd, signal }) {
2056
+ try {
2057
+ const { stdout, stderr, exitCode } = await (0, tinyexec.x)(node_process.default.execPath, [bin, ...args], {
2058
+ signal,
2059
+ timeout: INSTALL_TIMEOUT_MS,
2060
+ throwOnError: false,
2061
+ nodeOptions: {
2062
+ cwd,
2063
+ env: {
2064
+ ...node_process.default.env,
2065
+ NO_COLOR: "1",
2066
+ FORCE_COLOR: "0"
2067
+ }
2068
+ }
2069
+ });
2070
+ const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
2071
+ return {
2072
+ failed: exitCode !== 0,
2073
+ output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
2074
+ };
2075
+ } catch (error) {
2076
+ return {
2077
+ failed: true,
2078
+ output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
2079
+ };
2080
+ }
2081
+ }
1930
2082
  function emit(hooks, name, ...args) {
1931
2083
  return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
1932
2084
  }
@@ -1988,12 +2140,17 @@ var Agent = class extends capnweb.RpcTarget {
1988
2140
  get #sandbox() {
1989
2141
  return this.#session.info?.sandbox === true;
1990
2142
  }
2143
+ get #managed() {
2144
+ return this.#session.info?.managed === true;
2145
+ }
1991
2146
  get #permissions() {
1992
2147
  const { permissions } = this.#options;
1993
2148
  return this.#sandbox ? {
1994
2149
  ...permissions,
1995
2150
  allowWrite: false,
1996
2151
  allowConfigEdit: false,
2152
+ allowInstall: false,
2153
+ allowUpdate: false,
1997
2154
  allowRead: true,
1998
2155
  allowReadAst: true
1999
2156
  } : permissions;
@@ -2145,6 +2302,8 @@ var Agent = class extends capnweb.RpcTarget {
2145
2302
  message: "Connection is closed"
2146
2303
  });
2147
2304
  if (this.#session.active) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_IN_PROGRESS });
2305
+ if (this.#session.install) return this.#refuse({ message: require_protocol.AgentMessage.INSTALL_IN_PROGRESS });
2306
+ if (this.#session.saving) return this.#refuse({ message: require_protocol.AgentMessage.SAVE_IN_PROGRESS });
2148
2307
  const settled = Promise.withResolvers();
2149
2308
  this.#session.active = {
2150
2309
  jobId: input.jobId,
@@ -2183,8 +2342,9 @@ var Agent = class extends capnweb.RpcTarget {
2183
2342
  */
2184
2343
  async #generate({ input, root, outputRoot, signal }) {
2185
2344
  const { jobId, config: patch } = input;
2186
- const studioInput = this.#sandbox ? patch?.input : void 0;
2345
+ const studioInput = this.#sandbox || this.#managed ? patch?.input : void 0;
2187
2346
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_INPUT_ONLY });
2347
+ if (this.#managed && studioInput !== void 0 && !MANAGED_INPUT_KINDS.has((0, _kubb_core.getInputKind)(studioInput))) return this.#refuse({ message: require_protocol.AgentMessage.MANAGED_INPUT_KIND });
2188
2348
  const [configs, extensions] = await Promise.all([this.#options.loadConfigs(), this.#installedExtensions()]);
2189
2349
  const config = pickConfig(configs, patch?.config);
2190
2350
  if (!config) return this.#refuse({ message: require_protocol.AgentMessage.CONFIG_ENTRY_NOT_FOUND });
@@ -2213,12 +2373,13 @@ var Agent = class extends capnweb.RpcTarget {
2213
2373
  throw error;
2214
2374
  });
2215
2375
  const activePlugins = plugins ?? config.plugins;
2216
- if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
2217
- const files = await generate({
2376
+ if (patch?.input && !this.#sandbox && !this.#managed) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox or managed agent" });
2377
+ const checkout = this.#managed && studioInput !== void 0 && (0, _kubb_core.getInputKind)(studioInput) === "git" ? await (0, _kubb_core.checkoutGitInput)(studioInput, signal, { publicOnly: true }).catch((error) => this.#refuse({ message: getErrorMessage(error) })) : void 0;
2378
+ const run = () => generate({
2218
2379
  config: {
2219
2380
  ...config,
2220
2381
  root,
2221
- input: studioInput ?? config.input,
2382
+ input: checkout?.path ?? studioInput ?? config.input,
2222
2383
  storage: outputRoot ? stagingStorage({
2223
2384
  root,
2224
2385
  directory: outputRoot
@@ -2230,11 +2391,12 @@ var Agent = class extends capnweb.RpcTarget {
2230
2391
  hooks: this.#options.hooks,
2231
2392
  signal,
2232
2393
  allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2233
- }).catch(async (error) => {
2394
+ });
2395
+ const files = await (this.#managed ? withPublicFetch(run) : run()).catch(async (error) => {
2234
2396
  await this.#generations.drop(jobId);
2235
2397
  await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
2236
2398
  throw error;
2237
- });
2399
+ }).finally(() => checkout?.dispose());
2238
2400
  const output = await this.#generations.keep({
2239
2401
  jobId,
2240
2402
  files,
@@ -2282,7 +2444,9 @@ var Agent = class extends capnweb.RpcTarget {
2282
2444
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
2283
2445
  return refuse("the agent was not granted permission to edit kubb.config.ts");
2284
2446
  }
2285
- if (this.#session.active) return refuse("a generation is in progress");
2447
+ const busy = this.#busy();
2448
+ if (busy) return refuse(busy);
2449
+ this.#session.saving = true;
2286
2450
  try {
2287
2451
  const { outcomes, changed } = await writeConfigEdits({
2288
2452
  filePath: configFile,
@@ -2298,6 +2462,110 @@ var Agent = class extends capnweb.RpcTarget {
2298
2462
  } catch (error) {
2299
2463
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
2300
2464
  return refuse(getErrorMessage(error));
2465
+ } finally {
2466
+ this.#session.saving = false;
2467
+ }
2468
+ }
2469
+ /**
2470
+ * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2471
+ * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2472
+ */
2473
+ async installExtension(input) {
2474
+ this.#assertConnected();
2475
+ const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
2476
+ const refuse = async (reason, permission) => {
2477
+ await this.#warn({
2478
+ message: `Ignored install: ${reason}`,
2479
+ permission
2480
+ });
2481
+ return {
2482
+ installed: false,
2483
+ reason
2484
+ };
2485
+ };
2486
+ if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
2487
+ if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", require_protocol.AgentPermission.INSTALL);
2488
+ if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", require_protocol.AgentPermission.CONFIG_EDIT);
2489
+ const busy = this.#busy();
2490
+ if (busy) return refuse(busy);
2491
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2492
+ const { failed, output } = await this.#runKubb({
2493
+ bin: kubbBin,
2494
+ args: [
2495
+ "add",
2496
+ input.id,
2497
+ "--yes",
2498
+ "--config",
2499
+ configFile
2500
+ ]
2501
+ });
2502
+ await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
2503
+ return failed ? {
2504
+ installed: false,
2505
+ reason: output
2506
+ } : { installed: true };
2507
+ }
2508
+ /**
2509
+ * Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
2510
+ * installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
2511
+ */
2512
+ async updateExtensions(input) {
2513
+ this.#assertConnected();
2514
+ const { root, kubbBin = findKubbBin(root) } = this.#options;
2515
+ const refuse = async (reason, permission) => {
2516
+ await this.#warn({
2517
+ message: `Ignored update: ${reason}`,
2518
+ permission
2519
+ });
2520
+ return {
2521
+ updated: false,
2522
+ reason
2523
+ };
2524
+ };
2525
+ const ids = input?.ids ?? [];
2526
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
2527
+ if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", require_protocol.AgentPermission.UPDATE);
2528
+ const busy = this.#busy();
2529
+ if (busy) return refuse(busy);
2530
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2531
+ const { failed, output } = await this.#runKubb({
2532
+ bin: kubbBin,
2533
+ args: [
2534
+ "update",
2535
+ ...ids,
2536
+ "--yes"
2537
+ ]
2538
+ });
2539
+ await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
2540
+ return failed ? {
2541
+ updated: false,
2542
+ reason: output
2543
+ } : { updated: true };
2544
+ }
2545
+ /** Why a command that touches the project cannot start right now. */
2546
+ #busy() {
2547
+ if (this.#session.active) return "a generation is in progress";
2548
+ if (this.#session.install) return "an install or update is in progress";
2549
+ if (this.#session.saving) return "a save is in progress";
2550
+ }
2551
+ /** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
2552
+ async #runKubb({ bin, args }) {
2553
+ const controller = new AbortController();
2554
+ const settled = Promise.withResolvers();
2555
+ this.#session.install = {
2556
+ abort: () => controller.abort(),
2557
+ settled: settled.promise
2558
+ };
2559
+ try {
2560
+ return await runKubb({
2561
+ bin,
2562
+ args,
2563
+ cwd: this.#options.root,
2564
+ signal: controller.signal
2565
+ });
2566
+ } finally {
2567
+ this.#session.install = void 0;
2568
+ settled.resolve();
2301
2569
  }
2302
2570
  }
2303
2571
  /**
@@ -2482,6 +2750,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
2482
2750
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2483
2751
  }
2484
2752
  await active?.settled;
2753
+ session.install?.abort();
2754
+ await session.install?.settled;
2485
2755
  await agent.clear();
2486
2756
  return {
2487
2757
  ...close,
@@ -2550,6 +2820,8 @@ async function runAgent({ options, openRpc }) {
2550
2820
  permissions: {
2551
2821
  allowWrite: false,
2552
2822
  allowConfigEdit: false,
2823
+ allowInstall: false,
2824
+ allowUpdate: false,
2553
2825
  allowExec: false,
2554
2826
  allowRead: false,
2555
2827
  allowReadAst: false,