@exposify/next 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -184,7 +184,13 @@ route reads `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
184
184
  name them, or `supabase: false` to turn it off. The dashboard shows what was
185
185
  found and has the switch for passwords. `inspect` says which way it went and
186
186
  why; `npx @exposify/next test-sign-in` signs in on your running app and checks
187
- a protected page accepts the cookie. `@supabase/auth-helpers-nextjs` (an
187
+ a protected page accepts the cookie. If your `proxy.ts` sends signed-out
188
+ visitors to the login page (Supabase's starter does), it would send AI
189
+ assistants there too, before they can sign in: `inspect` warns, and
190
+ `setup --apply` leaves `/mcp` and `/.well-known/oauth-protected-resource` out
191
+ of its matcher (a matcher that already leaves out `/mcp` gets the second one
192
+ added). Your pages still go through
193
+ it on tool calls. `@supabase/auth-helpers-nextjs` (an
188
194
  older cookie format) isn't supported, and apps on `@supabase/supabase-js`
189
195
  alone keep their session in the browser, where the server never sees it.
190
196
 
@@ -2296,8 +2296,8 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
2296
2296
  import { createInterface } from "readline";
2297
2297
 
2298
2298
  // bin/adapter.ts
2299
- import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
2300
- import { dirname as dirname4, join as join9, relative as relative3, resolve as resolve2 } from "path";
2299
+ import { existsSync as existsSync5, readFileSync as readFileSync10 } from "fs";
2300
+ import { dirname as dirname4, join as join10, relative as relative3, resolve as resolve2 } from "path";
2301
2301
  import { decideExposure, deriveDescription, resolveOptions, toolNames } from "@exposify/core";
2302
2302
  import { renderConfig, replaceAiReadyOptions } from "@exposify/cli";
2303
2303
 
@@ -2646,13 +2646,142 @@ function scanActions(d, call, root, notes, suggestions = []) {
2646
2646
  return rows;
2647
2647
  }
2648
2648
 
2649
+ // bin/proxy.ts
2650
+ import { existsSync as existsSync4, readFileSync as readFileSync9 } from "fs";
2651
+ import { join as join9 } from "path";
2652
+ import { PRM_PATH } from "@exposify/core";
2653
+ var FILES = ["", "src/"].flatMap((dir) => ["proxy", "middleware"].flatMap((name) => [".ts", ".js", ".mjs"].map((ext) => `${dir}${name}${ext}`)));
2654
+ var REDIRECT = /\bredirect\s*\(/;
2655
+ function matcherRegExp(pattern) {
2656
+ let re = "";
2657
+ for (let i = 0; i < pattern.length; ) {
2658
+ const c = pattern[i];
2659
+ if (c === "(") {
2660
+ let depth = 0;
2661
+ let j = i;
2662
+ for (; j < pattern.length; j++) {
2663
+ if (pattern[j] === "\\") j++;
2664
+ else if (pattern[j] === "(") depth++;
2665
+ else if (pattern[j] === ")" && --depth === 0) break;
2666
+ }
2667
+ re += pattern.slice(i, j + 1);
2668
+ i = j + 1;
2669
+ } else if (c === "/" && pattern[i + 1] === ":") {
2670
+ const m = /^\/:\w+([*+?]?)/.exec(pattern.slice(i));
2671
+ re += m[1] === "*" ? "(?:/.*)?" : m[1] === "+" ? "/.+" : m[1] === "?" ? "(?:/[^/]+)?" : "/[^/]+";
2672
+ i += m[0].length;
2673
+ } else {
2674
+ re += c.replace(/[.*+?^${}|[\]\\]/g, "\\$&");
2675
+ i++;
2676
+ }
2677
+ }
2678
+ try {
2679
+ return new RegExp(`^${re}/?$`);
2680
+ } catch {
2681
+ return void 0;
2682
+ }
2683
+ }
2684
+ function matcherNode(program) {
2685
+ for (const n of program.body) {
2686
+ const decl = n.type === "ExportNamedDeclaration" ? n.declaration : void 0;
2687
+ if (decl?.type !== "VariableDeclaration") continue;
2688
+ for (const d of decl.declarations) {
2689
+ if (d.id?.name !== "config") continue;
2690
+ const obj = unwrapTs(d.init);
2691
+ if (obj?.type !== "ObjectExpression") return "opaque";
2692
+ const prop2 = obj.properties.find((p) => p.type === "ObjectProperty" && (p.key?.name ?? p.key?.value) === "matcher");
2693
+ return prop2 ? unwrapTs(prop2.value) ?? "opaque" : null;
2694
+ }
2695
+ }
2696
+ return null;
2697
+ }
2698
+ function matcherEntries(node) {
2699
+ const items = node.type === "ArrayExpression" ? node.elements : [node];
2700
+ const out = [];
2701
+ for (const item of items) {
2702
+ const literal = item?.type === "ObjectExpression" ? unwrapTs(item.properties.find((p) => (p.key?.name ?? p.key?.value) === "source")?.value) : item;
2703
+ const pattern = stringValue(literal);
2704
+ if (pattern === void 0 || !literal) return void 0;
2705
+ out.push({ pattern, node: literal });
2706
+ }
2707
+ return out;
2708
+ }
2709
+ function redirects(root, file, source, program) {
2710
+ if (REDIRECT.test(source)) return true;
2711
+ const load = moduleLoader(root);
2712
+ for (const n of program.body) {
2713
+ if (n.type !== "ImportDeclaration") continue;
2714
+ const found = load(n.source.value, file);
2715
+ if (found && REDIRECT.test(found.source)) return true;
2716
+ }
2717
+ return false;
2718
+ }
2719
+ function middlewareFile(root) {
2720
+ return FILES.find((f) => existsSync4(join9(root, f)));
2721
+ }
2722
+ function findProxyGate(root, mcpPath = "/mcp") {
2723
+ const file = middlewareFile(root);
2724
+ if (!file) return void 0;
2725
+ const source = readFileSync9(join9(root, file), "utf8");
2726
+ const program = parseModule(source, file);
2727
+ if (!program || !redirects(root, file, source, program)) return void 0;
2728
+ const matcher = matcherNode(program);
2729
+ if (matcher === "opaque") return void 0;
2730
+ if (matcher === null) return { file, mcpPath, matching: [] };
2731
+ const entries = matcherEntries(matcher);
2732
+ if (!entries) return void 0;
2733
+ const probes = exclusions(mcpPath).flatMap((x) => x.probes);
2734
+ const matching = entries.filter((e) => probes.some((p) => matcherRegExp(e.pattern)?.test(p))).map((e) => e.pattern);
2735
+ return matching.length > 0 ? { file, mcpPath, matching } : void 0;
2736
+ }
2737
+ function proxyGateHint(gate) {
2738
+ return `${gate.file} sends signed-out requests to another page and runs on ${gate.mcpPath}, so AI assistants get that page instead of a sign-in. Leave ${gate.mcpPath} and ${PRM_PATH} out of its matcher (the MCP route checks its own callers): ` + (gate.matching.some((p) => p.startsWith("/((?!")) ? `add ${inSource(gate.mcpPath)}| after "(?!" in config.matcher.` : `matcher: ["/((?!${inSource(gate.mcpPath)}).*)"].`);
2739
+ }
2740
+ var exclusions = (mcpPath) => [
2741
+ {
2742
+ alt: `${mcpPath.replace(/^\/+|\/+$/g, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|$)`,
2743
+ probes: [mcpPath, `${mcpPath.replace(/\/$/, "")}/login`]
2744
+ },
2745
+ { alt: String.raw`\.well-known/oauth-protected-resource(?:/|$)`, probes: [`${PRM_PATH}${mcpPath}`] }
2746
+ ];
2747
+ var exclusion = (mcpPath, pattern) => exclusions(mcpPath).filter((x) => pattern === void 0 || x.probes.some((p) => matcherRegExp(pattern)?.test(p))).map((x) => x.alt).join("|");
2748
+ var inSource = (mcpPath, pattern) => exclusion(mcpPath, pattern).replace(/\\/g, "\\\\");
2749
+ function excludeMcpFromProxy(root, gate) {
2750
+ const file = join9(root, gate.file);
2751
+ const before = readFileSync9(file, "utf8");
2752
+ const program = parseModule(before, gate.file);
2753
+ if (!program) return { blocked: `couldn't parse ${gate.file}` };
2754
+ const summary = `Leave ${gate.mcpPath} out of ${gate.file}'s matcher, so AI assistants reach the sign-in instead of your login page`;
2755
+ const matcher = matcherNode(program);
2756
+ if (matcher === null) {
2757
+ const after2 = `${before.replace(/\n*$/, "\n")}
2758
+ export const config = {
2759
+ matcher: ["/((?!${inSource(gate.mcpPath)}).*)"],
2760
+ };
2761
+ `;
2762
+ return { plan: { file, summary, before, after: after2 } };
2763
+ }
2764
+ const entries = matcher === "opaque" ? void 0 : matcherEntries(matcher);
2765
+ if (!entries) return { blocked: `${gate.file}'s matcher isn't a list of strings` };
2766
+ let after = before;
2767
+ for (const e of [...entries].reverse()) {
2768
+ if (!gate.matching.includes(e.pattern)) continue;
2769
+ const raw = before.slice(e.node.start, e.node.end);
2770
+ const at = raw.indexOf("/((?!");
2771
+ if (at < 0) return { blocked: `${gate.file}'s matcher "${e.pattern}" has no "(?!" to add ${gate.mcpPath} to` };
2772
+ const insert = e.node.start + at + "/((?!".length;
2773
+ after = `${after.slice(0, insert)}${inSource(gate.mcpPath, e.pattern)}|${after.slice(insert)}`;
2774
+ }
2775
+ return { plan: { file, summary, before, after } };
2776
+ }
2777
+
2649
2778
  // bin/adapter.ts
2650
2779
  var MCP_ROUTE_DIR = "mcp/[[...transport]]";
2651
2780
  function isNextProject(dir) {
2652
2781
  try {
2653
- const pkg = JSON.parse(readFileSync9(join9(dir, "package.json"), "utf8"));
2782
+ const pkg = JSON.parse(readFileSync10(join10(dir, "package.json"), "utf8"));
2654
2783
  const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
2655
- return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join9(dir, d)));
2784
+ return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync5(join10(dir, d)));
2656
2785
  } catch {
2657
2786
  return false;
2658
2787
  }
@@ -2704,7 +2833,11 @@ ${block}
2704
2833
  const wrap = hasWithExposify(root) ? null : wrapNextConfig(root);
2705
2834
  const also = wrap && "plan" in wrap ? [{ plan: wrap.plan, manual: WRAP_HINT }] : [];
2706
2835
  if (wrap && !("plan" in wrap)) lines.push(WRAP_HINT);
2707
- const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join9(root, f)).find((f) => existsSync4(f));
2836
+ const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
2837
+ const unblock = gate ? excludeMcpFromProxy(root, gate) : null;
2838
+ if (gate && unblock && "plan" in unblock) also.push({ plan: unblock.plan, manual: proxyGateHint(gate) });
2839
+ else if (gate) lines.push(proxyGateHint(gate));
2840
+ const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join10(root, f)).find((f) => existsSync5(f));
2708
2841
  const manifestFile = existing ?? generate(root, {}, () => {
2709
2842
  }).file;
2710
2843
  const generated = relative3(root, manifestFile);
@@ -2715,7 +2848,7 @@ ${block}
2715
2848
  return { code: options, hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] }, apply };
2716
2849
  }
2717
2850
  const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
2718
- const manifestImport = relative3(join9(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
2851
+ const manifestImport = relative3(join10(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
2719
2852
  const code = [
2720
2853
  'import { aiReady } from "@exposify/next";',
2721
2854
  ...usesZod ? ['import { z } from "zod";'] : [],
@@ -2727,7 +2860,7 @@ ${block}
2727
2860
  ].join("\n");
2728
2861
  return {
2729
2862
  code,
2730
- apply: { plan: { file: join9(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2863
+ apply: { plan: { file: join10(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2731
2864
  ` }, ...also.length ? { also } : {} },
2732
2865
  hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
2733
2866
  };
@@ -2741,10 +2874,10 @@ function withAlso(outcome, also, lines) {
2741
2874
  return outcome;
2742
2875
  }
2743
2876
  function wrapNextConfig(root) {
2744
- const name = NEXT_CONFIGS.find((n) => existsSync4(join9(root, n)));
2877
+ const name = NEXT_CONFIGS.find((n) => existsSync5(join10(root, n)));
2745
2878
  if (!name) return { blocked: "no next.config file" };
2746
- const file = join9(root, name);
2747
- const before = readFileSync9(file, "utf8");
2879
+ const file = join10(root, name);
2880
+ const before = readFileSync10(file, "utf8");
2748
2881
  const program = parseModule(before, name);
2749
2882
  if (!program) return { blocked: `couldn't parse ${name}` };
2750
2883
  const body = program.body;
@@ -2770,7 +2903,7 @@ ${wrapped}`;
2770
2903
  function hasWithExposify(root) {
2771
2904
  for (const name of NEXT_CONFIGS) {
2772
2905
  try {
2773
- if (readFileSync9(join9(root, name), "utf8").includes("withExposify")) return true;
2906
+ if (readFileSync10(join10(root, name), "utf8").includes("withExposify")) return true;
2774
2907
  } catch {
2775
2908
  }
2776
2909
  }
@@ -2817,7 +2950,7 @@ function scanProject(root) {
2817
2950
  for (const p of d.pages) {
2818
2951
  let source;
2819
2952
  try {
2820
- source = readFileSync9(join9(root, p.file), "utf8");
2953
+ source = readFileSync10(join10(root, p.file), "utf8");
2821
2954
  } catch {
2822
2955
  source = void 0;
2823
2956
  }
@@ -2844,6 +2977,8 @@ function scanProject(root) {
2844
2977
  for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
2845
2978
  const suggestions = [];
2846
2979
  const actions = scanActions(d, call, root, notes, suggestions);
2980
+ const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
2981
+ if (gate) notes.push(`${proxyGateHint(gate)} setup --apply can make this change.`);
2847
2982
  const routes = rows.map((row) => {
2848
2983
  const key = `${row.info.method} ${row.info.path}`;
2849
2984
  const decision = exposure.get(key);
@@ -2886,7 +3021,7 @@ function scanProject(root) {
2886
3021
  } : {},
2887
3022
  ...call && d.mcpRoute ? {
2888
3023
  aiReady: {
2889
- site: { file: join9(root, d.mcpRoute.file), line: call.line },
3024
+ site: { file: join10(root, d.mcpRoute.file), line: call.line },
2890
3025
  options: {
2891
3026
  ...call.expose ? { expose: call.expose } : {},
2892
3027
  ...call.exclude ? { exclude: call.exclude } : {},
@@ -2900,7 +3035,7 @@ function scanProject(root) {
2900
3035
  }
2901
3036
  function applyReplace(site, config, entries, usesZod, relFile) {
2902
3037
  try {
2903
- const before = readFileSync9(site.file, "utf8");
3038
+ const before = readFileSync10(site.file, "utf8");
2904
3039
  const edit = replaceAiReadyOptions({
2905
3040
  source: before,
2906
3041
  file: site.file,
@@ -2919,8 +3054,8 @@ function applyReplace(site, config, entries, usesZod, relFile) {
2919
3054
  }
2920
3055
 
2921
3056
  // bin/test-sign-in.ts
2922
- import { readFileSync as readFileSync10 } from "fs";
2923
- import { join as join10 } from "path";
3057
+ import { readFileSync as readFileSync11 } from "fs";
3058
+ import { join as join11 } from "path";
2924
3059
  import { parseEnv } from "util";
2925
3060
  import { supabaseCookieName, supabasePasswordLogin, supabaseSessionCookies } from "@exposify/core";
2926
3061
  var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
@@ -2942,7 +3077,7 @@ environment, .env.local or .env. Signs the test session out afterwards.`;
2942
3077
  function supabaseEnv(root, env) {
2943
3078
  const files = [".env.local", ".env"].map((f) => {
2944
3079
  try {
2945
- return parseEnv(readFileSync10(join10(root, f), "utf8"));
3080
+ return parseEnv(readFileSync11(join11(root, f), "utf8"));
2946
3081
  } catch {
2947
3082
  return {};
2948
3083
  }
package/dist/index.cjs CHANGED
@@ -444,9 +444,58 @@ function tokenize(html) {
444
444
  if (last < html.length) out.push(html.slice(last));
445
445
  return out;
446
446
  }
447
+ function divEnd(html, from) {
448
+ const re = /<div\b(?:[^>"']|"[^"]*"|'[^']*')*>|<\/div\s*>/gi;
449
+ re.lastIndex = from;
450
+ let depth = 1;
451
+ for (let m = re.exec(html); m; m = re.exec(html)) {
452
+ depth += m[0][1] === "/" ? -1 : 1;
453
+ if (depth === 0) return re.lastIndex;
454
+ }
455
+ return html.length;
456
+ }
457
+ function boundaryEnd(html, from) {
458
+ const re = /<!--\$[?!]?-->|<!--\/\$-->/g;
459
+ re.lastIndex = from;
460
+ let depth = 1;
461
+ for (let m = re.exec(html); m; m = re.exec(html)) {
462
+ depth += m[0][4] === "/" ? -1 : 1;
463
+ if (depth === 0) return re.lastIndex;
464
+ }
465
+ return html.length;
466
+ }
467
+ function resolveStreamed(html) {
468
+ const chunks = [];
469
+ const open = /<div\s+hidden(?:="")?\s+id="S:([0-9a-f]+)"\s*>/g;
470
+ let out = "";
471
+ let last = 0;
472
+ for (let m = open.exec(html); m; m = open.exec(html)) {
473
+ const end = divEnd(html, open.lastIndex);
474
+ chunks.push({ id: m[1], content: html.slice(open.lastIndex, end).replace(/<\/div\s*>$/i, "") });
475
+ out += html.slice(last, m.index);
476
+ last = end;
477
+ open.lastIndex = end;
478
+ }
479
+ if (chunks.length === 0) return html;
480
+ out += html.slice(last);
481
+ for (let moved = true; moved; ) {
482
+ moved = false;
483
+ for (const c of chunks) {
484
+ const marker = `<template id="B:${c.id}"></template>`;
485
+ const at = out.indexOf(marker);
486
+ if (at < 0) continue;
487
+ const inBoundary = out.slice(Math.max(0, at - 9), at) === "<!--$?-->";
488
+ const start = inBoundary ? at - 9 : at;
489
+ const end = inBoundary ? boundaryEnd(out, at) : at + marker.length;
490
+ out = out.slice(0, start) + c.content + out.slice(end);
491
+ moved = true;
492
+ }
493
+ }
494
+ return out;
495
+ }
447
496
  var hidden = (t) => "hidden" in t.attrs || t.attrs["aria-hidden"] === "true" || /display\s*:\s*none|visibility\s*:\s*hidden/i.test(t.attrs.style ?? "") || t.name === "input" && t.attrs.type === "hidden";
448
497
  function htmlToText(html) {
449
- const tokens = tokenize(html);
498
+ const tokens = tokenize(resolveStreamed(html));
450
499
  const hasMain = tokens.some((t) => typeof t !== "string" && t.name === "main" && !t.close);
451
500
  const lines = [];
452
501
  let line = "";
package/dist/index.js CHANGED
@@ -454,9 +454,58 @@ function tokenize(html) {
454
454
  if (last < html.length) out.push(html.slice(last));
455
455
  return out;
456
456
  }
457
+ function divEnd(html, from) {
458
+ const re = /<div\b(?:[^>"']|"[^"]*"|'[^']*')*>|<\/div\s*>/gi;
459
+ re.lastIndex = from;
460
+ let depth = 1;
461
+ for (let m = re.exec(html); m; m = re.exec(html)) {
462
+ depth += m[0][1] === "/" ? -1 : 1;
463
+ if (depth === 0) return re.lastIndex;
464
+ }
465
+ return html.length;
466
+ }
467
+ function boundaryEnd(html, from) {
468
+ const re = /<!--\$[?!]?-->|<!--\/\$-->/g;
469
+ re.lastIndex = from;
470
+ let depth = 1;
471
+ for (let m = re.exec(html); m; m = re.exec(html)) {
472
+ depth += m[0][4] === "/" ? -1 : 1;
473
+ if (depth === 0) return re.lastIndex;
474
+ }
475
+ return html.length;
476
+ }
477
+ function resolveStreamed(html) {
478
+ const chunks = [];
479
+ const open = /<div\s+hidden(?:="")?\s+id="S:([0-9a-f]+)"\s*>/g;
480
+ let out = "";
481
+ let last = 0;
482
+ for (let m = open.exec(html); m; m = open.exec(html)) {
483
+ const end = divEnd(html, open.lastIndex);
484
+ chunks.push({ id: m[1], content: html.slice(open.lastIndex, end).replace(/<\/div\s*>$/i, "") });
485
+ out += html.slice(last, m.index);
486
+ last = end;
487
+ open.lastIndex = end;
488
+ }
489
+ if (chunks.length === 0) return html;
490
+ out += html.slice(last);
491
+ for (let moved = true; moved; ) {
492
+ moved = false;
493
+ for (const c of chunks) {
494
+ const marker = `<template id="B:${c.id}"></template>`;
495
+ const at = out.indexOf(marker);
496
+ if (at < 0) continue;
497
+ const inBoundary = out.slice(Math.max(0, at - 9), at) === "<!--$?-->";
498
+ const start = inBoundary ? at - 9 : at;
499
+ const end = inBoundary ? boundaryEnd(out, at) : at + marker.length;
500
+ out = out.slice(0, start) + c.content + out.slice(end);
501
+ moved = true;
502
+ }
503
+ }
504
+ return out;
505
+ }
457
506
  var hidden = (t) => "hidden" in t.attrs || t.attrs["aria-hidden"] === "true" || /display\s*:\s*none|visibility\s*:\s*hidden/i.test(t.attrs.style ?? "") || t.name === "input" && t.attrs.type === "hidden";
458
507
  function htmlToText(html) {
459
- const tokens = tokenize(html);
508
+ const tokens = tokenize(resolveStreamed(html));
460
509
  const hasMain = tokens.some((t) => typeof t !== "string" && t.name === "main" && !t.close);
461
510
  const lines = [];
462
511
  let line = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/next",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@babel/parser": "^8.0.0",
35
- "@exposify/cli": "0.0.4",
35
+ "@exposify/cli": "0.0.6",
36
36
  "@exposify/core": "0.3.16",
37
37
  "tinyglobby": "^0.2.10",
38
38
  "zod": "^3.24.0"