@exposify/next 0.0.5 → 0.0.6

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,11 @@ 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` out of its matcher. Your pages still go through
191
+ it on tool calls. `@supabase/auth-helpers-nextjs` (an
188
192
  older cookie format) isn't supported, and apps on `@supabase/supabase-js`
189
193
  alone keep their session in the browser, where the server never sees it.
190
194
 
@@ -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,134 @@ 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
+ var FILES = ["", "src/"].flatMap((dir) => ["proxy", "middleware"].flatMap((name) => [".ts", ".js", ".mjs"].map((ext) => `${dir}${name}${ext}`)));
2653
+ var REDIRECT = /\bredirect\s*\(/;
2654
+ function matcherRegExp(pattern) {
2655
+ let re = "";
2656
+ for (let i = 0; i < pattern.length; ) {
2657
+ const c = pattern[i];
2658
+ if (c === "(") {
2659
+ let depth = 0;
2660
+ let j = i;
2661
+ for (; j < pattern.length; j++) {
2662
+ if (pattern[j] === "\\") j++;
2663
+ else if (pattern[j] === "(") depth++;
2664
+ else if (pattern[j] === ")" && --depth === 0) break;
2665
+ }
2666
+ re += pattern.slice(i, j + 1);
2667
+ i = j + 1;
2668
+ } else if (c === "/" && pattern[i + 1] === ":") {
2669
+ const m = /^\/:\w+([*+?]?)/.exec(pattern.slice(i));
2670
+ re += m[1] === "*" ? "(?:/.*)?" : m[1] === "+" ? "/.+" : m[1] === "?" ? "(?:/[^/]+)?" : "/[^/]+";
2671
+ i += m[0].length;
2672
+ } else {
2673
+ re += c.replace(/[.*+?^${}|[\]\\]/g, "\\$&");
2674
+ i++;
2675
+ }
2676
+ }
2677
+ try {
2678
+ return new RegExp(`^${re}/?$`);
2679
+ } catch {
2680
+ return void 0;
2681
+ }
2682
+ }
2683
+ function matcherNode(program) {
2684
+ for (const n of program.body) {
2685
+ const decl = n.type === "ExportNamedDeclaration" ? n.declaration : void 0;
2686
+ if (decl?.type !== "VariableDeclaration") continue;
2687
+ for (const d of decl.declarations) {
2688
+ if (d.id?.name !== "config") continue;
2689
+ const obj = unwrapTs(d.init);
2690
+ if (obj?.type !== "ObjectExpression") return "opaque";
2691
+ const prop2 = obj.properties.find((p) => p.type === "ObjectProperty" && (p.key?.name ?? p.key?.value) === "matcher");
2692
+ return prop2 ? unwrapTs(prop2.value) ?? "opaque" : null;
2693
+ }
2694
+ }
2695
+ return null;
2696
+ }
2697
+ function matcherEntries(node) {
2698
+ const items = node.type === "ArrayExpression" ? node.elements : [node];
2699
+ const out = [];
2700
+ for (const item of items) {
2701
+ const literal = item?.type === "ObjectExpression" ? unwrapTs(item.properties.find((p) => (p.key?.name ?? p.key?.value) === "source")?.value) : item;
2702
+ const pattern = stringValue(literal);
2703
+ if (pattern === void 0 || !literal) return void 0;
2704
+ out.push({ pattern, node: literal });
2705
+ }
2706
+ return out;
2707
+ }
2708
+ function redirects(root, file, source, program) {
2709
+ if (REDIRECT.test(source)) return true;
2710
+ const load = moduleLoader(root);
2711
+ for (const n of program.body) {
2712
+ if (n.type !== "ImportDeclaration") continue;
2713
+ const found = load(n.source.value, file);
2714
+ if (found && REDIRECT.test(found.source)) return true;
2715
+ }
2716
+ return false;
2717
+ }
2718
+ function middlewareFile(root) {
2719
+ return FILES.find((f) => existsSync4(join9(root, f)));
2720
+ }
2721
+ function findProxyGate(root, mcpPath = "/mcp") {
2722
+ const file = middlewareFile(root);
2723
+ if (!file) return void 0;
2724
+ const source = readFileSync9(join9(root, file), "utf8");
2725
+ const program = parseModule(source, file);
2726
+ if (!program || !redirects(root, file, source, program)) return void 0;
2727
+ const matcher = matcherNode(program);
2728
+ if (matcher === "opaque") return void 0;
2729
+ if (matcher === null) return { file, mcpPath, matching: [] };
2730
+ const entries = matcherEntries(matcher);
2731
+ if (!entries) return void 0;
2732
+ const probes = [mcpPath, `${mcpPath.replace(/\/$/, "")}/login`];
2733
+ const matching = entries.filter((e) => probes.some((p) => matcherRegExp(e.pattern)?.test(p))).map((e) => e.pattern);
2734
+ return matching.length > 0 ? { file, mcpPath, matching } : void 0;
2735
+ }
2736
+ function proxyGateHint(gate) {
2737
+ 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} out of its matcher (the MCP route checks its own callers): ` + (gate.matching.some((p) => p.startsWith("/((?!")) ? `add ${exclusion(gate.mcpPath)}| after "(?!" in config.matcher.` : `matcher: ["/((?!${exclusion(gate.mcpPath)}).*)"].`);
2738
+ }
2739
+ var exclusion = (mcpPath) => `${mcpPath.replace(/^\/+|\/+$/g, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/|$)`;
2740
+ var inSource = (mcpPath) => exclusion(mcpPath).replace(/\\/g, "\\\\");
2741
+ function excludeMcpFromProxy(root, gate) {
2742
+ const file = join9(root, gate.file);
2743
+ const before = readFileSync9(file, "utf8");
2744
+ const program = parseModule(before, gate.file);
2745
+ if (!program) return { blocked: `couldn't parse ${gate.file}` };
2746
+ const summary = `Leave ${gate.mcpPath} out of ${gate.file}'s matcher, so AI assistants reach the sign-in instead of your login page`;
2747
+ const matcher = matcherNode(program);
2748
+ if (matcher === null) {
2749
+ const after2 = `${before.replace(/\n*$/, "\n")}
2750
+ export const config = {
2751
+ matcher: ["/((?!${inSource(gate.mcpPath)}).*)"],
2752
+ };
2753
+ `;
2754
+ return { plan: { file, summary, before, after: after2 } };
2755
+ }
2756
+ const entries = matcher === "opaque" ? void 0 : matcherEntries(matcher);
2757
+ if (!entries) return { blocked: `${gate.file}'s matcher isn't a list of strings` };
2758
+ let after = before;
2759
+ for (const e of [...entries].reverse()) {
2760
+ if (!gate.matching.includes(e.pattern)) continue;
2761
+ const raw = before.slice(e.node.start, e.node.end);
2762
+ const at = raw.indexOf("/((?!");
2763
+ if (at < 0) return { blocked: `${gate.file}'s matcher "${e.pattern}" has no "(?!" to add ${gate.mcpPath} to` };
2764
+ const insert = e.node.start + at + "/((?!".length;
2765
+ after = `${after.slice(0, insert)}${inSource(gate.mcpPath)}|${after.slice(insert)}`;
2766
+ }
2767
+ return { plan: { file, summary, before, after } };
2768
+ }
2769
+
2649
2770
  // bin/adapter.ts
2650
2771
  var MCP_ROUTE_DIR = "mcp/[[...transport]]";
2651
2772
  function isNextProject(dir) {
2652
2773
  try {
2653
- const pkg = JSON.parse(readFileSync9(join9(dir, "package.json"), "utf8"));
2774
+ const pkg = JSON.parse(readFileSync10(join10(dir, "package.json"), "utf8"));
2654
2775
  const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
2655
- return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join9(dir, d)));
2776
+ return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync5(join10(dir, d)));
2656
2777
  } catch {
2657
2778
  return false;
2658
2779
  }
@@ -2704,7 +2825,11 @@ ${block}
2704
2825
  const wrap = hasWithExposify(root) ? null : wrapNextConfig(root);
2705
2826
  const also = wrap && "plan" in wrap ? [{ plan: wrap.plan, manual: WRAP_HINT }] : [];
2706
2827
  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));
2828
+ const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
2829
+ const unblock = gate ? excludeMcpFromProxy(root, gate) : null;
2830
+ if (gate && unblock && "plan" in unblock) also.push({ plan: unblock.plan, manual: proxyGateHint(gate) });
2831
+ else if (gate) lines.push(proxyGateHint(gate));
2832
+ const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join10(root, f)).find((f) => existsSync5(f));
2708
2833
  const manifestFile = existing ?? generate(root, {}, () => {
2709
2834
  }).file;
2710
2835
  const generated = relative3(root, manifestFile);
@@ -2715,7 +2840,7 @@ ${block}
2715
2840
  return { code: options, hint: { lines: [`Replace the aiReady(...) call in ${file}:${result.aiReady.site.line} with this.`, ...lines] }, apply };
2716
2841
  }
2717
2842
  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$/, "");
2843
+ const manifestImport = relative3(join10(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
2719
2844
  const code = [
2720
2845
  'import { aiReady } from "@exposify/next";',
2721
2846
  ...usesZod ? ['import { z } from "zod";'] : [],
@@ -2727,7 +2852,7 @@ ${block}
2727
2852
  ].join("\n");
2728
2853
  return {
2729
2854
  code,
2730
- apply: { plan: { file: join9(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2855
+ apply: { plan: { file: join10(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2731
2856
  ` }, ...also.length ? { also } : {} },
2732
2857
  hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
2733
2858
  };
@@ -2741,10 +2866,10 @@ function withAlso(outcome, also, lines) {
2741
2866
  return outcome;
2742
2867
  }
2743
2868
  function wrapNextConfig(root) {
2744
- const name = NEXT_CONFIGS.find((n) => existsSync4(join9(root, n)));
2869
+ const name = NEXT_CONFIGS.find((n) => existsSync5(join10(root, n)));
2745
2870
  if (!name) return { blocked: "no next.config file" };
2746
- const file = join9(root, name);
2747
- const before = readFileSync9(file, "utf8");
2871
+ const file = join10(root, name);
2872
+ const before = readFileSync10(file, "utf8");
2748
2873
  const program = parseModule(before, name);
2749
2874
  if (!program) return { blocked: `couldn't parse ${name}` };
2750
2875
  const body = program.body;
@@ -2770,7 +2895,7 @@ ${wrapped}`;
2770
2895
  function hasWithExposify(root) {
2771
2896
  for (const name of NEXT_CONFIGS) {
2772
2897
  try {
2773
- if (readFileSync9(join9(root, name), "utf8").includes("withExposify")) return true;
2898
+ if (readFileSync10(join10(root, name), "utf8").includes("withExposify")) return true;
2774
2899
  } catch {
2775
2900
  }
2776
2901
  }
@@ -2817,7 +2942,7 @@ function scanProject(root) {
2817
2942
  for (const p of d.pages) {
2818
2943
  let source;
2819
2944
  try {
2820
- source = readFileSync9(join9(root, p.file), "utf8");
2945
+ source = readFileSync10(join10(root, p.file), "utf8");
2821
2946
  } catch {
2822
2947
  source = void 0;
2823
2948
  }
@@ -2844,6 +2969,8 @@ function scanProject(root) {
2844
2969
  for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
2845
2970
  const suggestions = [];
2846
2971
  const actions = scanActions(d, call, root, notes, suggestions);
2972
+ const gate = findProxyGate(root, d.mcpPath ?? "/mcp");
2973
+ if (gate) notes.push(`${proxyGateHint(gate)} setup --apply can make this change.`);
2847
2974
  const routes = rows.map((row) => {
2848
2975
  const key = `${row.info.method} ${row.info.path}`;
2849
2976
  const decision = exposure.get(key);
@@ -2886,7 +3013,7 @@ function scanProject(root) {
2886
3013
  } : {},
2887
3014
  ...call && d.mcpRoute ? {
2888
3015
  aiReady: {
2889
- site: { file: join9(root, d.mcpRoute.file), line: call.line },
3016
+ site: { file: join10(root, d.mcpRoute.file), line: call.line },
2890
3017
  options: {
2891
3018
  ...call.expose ? { expose: call.expose } : {},
2892
3019
  ...call.exclude ? { exclude: call.exclude } : {},
@@ -2900,7 +3027,7 @@ function scanProject(root) {
2900
3027
  }
2901
3028
  function applyReplace(site, config, entries, usesZod, relFile) {
2902
3029
  try {
2903
- const before = readFileSync9(site.file, "utf8");
3030
+ const before = readFileSync10(site.file, "utf8");
2904
3031
  const edit = replaceAiReadyOptions({
2905
3032
  source: before,
2906
3033
  file: site.file,
@@ -2919,8 +3046,8 @@ function applyReplace(site, config, entries, usesZod, relFile) {
2919
3046
  }
2920
3047
 
2921
3048
  // bin/test-sign-in.ts
2922
- import { readFileSync as readFileSync10 } from "fs";
2923
- import { join as join10 } from "path";
3049
+ import { readFileSync as readFileSync11 } from "fs";
3050
+ import { join as join11 } from "path";
2924
3051
  import { parseEnv } from "util";
2925
3052
  import { supabaseCookieName, supabasePasswordLogin, supabaseSessionCookies } from "@exposify/core";
2926
3053
  var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
@@ -2942,7 +3069,7 @@ environment, .env.local or .env. Signs the test session out afterwards.`;
2942
3069
  function supabaseEnv(root, env) {
2943
3070
  const files = [".env.local", ".env"].map((f) => {
2944
3071
  try {
2945
- return parseEnv(readFileSync10(join10(root, f), "utf8"));
3072
+ return parseEnv(readFileSync11(join11(root, f), "utf8"));
2946
3073
  } catch {
2947
3074
  return {};
2948
3075
  }
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.6",
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.5",
36
36
  "@exposify/core": "0.3.16",
37
37
  "tinyglobby": "^0.2.10",
38
38
  "zod": "^3.24.0"