@exposify/next 0.0.2 → 0.0.4

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.
@@ -6,8 +6,8 @@ import { runCli } from "@exposify/cli";
6
6
 
7
7
  // src/codegen.ts
8
8
  import { createHash } from "crypto";
9
- import { existsSync as existsSync2, readFileSync as readFileSync6, readdirSync, writeFileSync } from "fs";
10
- import { dirname as dirname2, join as join6, relative as relative2, sep as sep2 } from "path";
9
+ import { existsSync as existsSync2, readFileSync as readFileSync7, readdirSync, writeFileSync } from "fs";
10
+ import { dirname as dirname2, join as join7, relative as relative2, sep as sep2 } from "path";
11
11
  import { DESCRIPTIONS_FILE, authStacksOf, dependencyNames, parseDescriptions } from "@exposify/core";
12
12
  import { globSync } from "tinyglobby";
13
13
 
@@ -336,8 +336,8 @@ function readsOf(fn, formNames, st, depth) {
336
336
  st.readsAll = true;
337
337
  const decl = [...parents].reverse().find((p) => !["TSAsExpression", "TSSatisfiesExpression", "ParenthesizedExpression"].includes(p.type));
338
338
  if (decl?.type === "VariableDeclarator" && decl.id.type === "ObjectPattern") {
339
- for (const prop of decl.id.properties) {
340
- const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
339
+ for (const prop2 of decl.id.properties) {
340
+ const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
341
341
  if (key) addField(st, key, {}, false);
342
342
  }
343
343
  }
@@ -379,19 +379,19 @@ function analyzeFields(fn, roles, ctx) {
379
379
  if (literal?.type === "TSTypeLiteral") {
380
380
  for (const m of literal.members) if (m.type === "TSPropertySignature" && keyName(m.key)) members.set(keyName(m.key), m);
381
381
  }
382
- for (const prop of id.properties) {
383
- const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
382
+ for (const prop2 of id.properties) {
383
+ const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
384
384
  if (!key) {
385
- st.unresolved.push(`line ${lineOf(prop)}: a rest element in parameter ${role.name}`);
385
+ st.unresolved.push(`line ${lineOf(prop2)}: a rest element in parameter ${role.name}`);
386
386
  continue;
387
387
  }
388
388
  const member = members.get(key);
389
389
  const hint = annotated(member?.typeAnnotation?.typeAnnotation);
390
390
  if (!hint) {
391
- st.unresolved.push(`line ${lineOf(prop)}: ${key} has no type it can read (string, number, boolean or a union of strings)`);
391
+ st.unresolved.push(`line ${lineOf(prop2)}: ${key} has no type it can read (string, number, boolean or a union of strings)`);
392
392
  continue;
393
393
  }
394
- st.fields.set(key, { name: key, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, ...member?.optional || prop.value.type === "AssignmentPattern" ? {} : { required: true } });
394
+ st.fields.set(key, { name: key, type: hint.type, ...hint.enum ? { enum: hint.enum } : {}, ...member?.optional || prop2.value.type === "AssignmentPattern" ? {} : { required: true } });
395
395
  }
396
396
  });
397
397
  return { fields: [...st.fields.values()], readsAll: st.readsAll, unresolved: st.unresolved };
@@ -469,20 +469,20 @@ function renderedAttributes(el, fn, use, mod) {
469
469
  for (const p of pattern.properties) {
470
470
  if (p.type === "RestElement" && p.argument.type === "Identifier") restName = p.argument.name;
471
471
  if (p.type !== "ObjectProperty") continue;
472
- const prop = keyName(p.key);
472
+ const prop2 = keyName(p.key);
473
473
  const value = p.value.type === "AssignmentPattern" ? p.value.left : p.value;
474
- if (!prop || value.type !== "Identifier") continue;
474
+ if (!prop2 || value.type !== "Identifier") continue;
475
475
  const fallback = p.value.type === "AssignmentPattern" ? attrValue({ type: "JSXAttribute", value: { type: "JSXExpressionContainer", expression: p.value.right } }, [], mod.scope) : void 0;
476
- locals.set(value.name, { prop, ...fallback ? { fallback } : {} });
476
+ locals.set(value.name, { prop: prop2, ...fallback ? { fallback } : {} });
477
477
  }
478
478
  }
479
- const named2 = new Set([...locals.values()].map((l) => l.prop));
479
+ const named = new Set([...locals.values()].map((l) => l.prop));
480
480
  const out = /* @__PURE__ */ new Map();
481
481
  for (const a of el.openingElement.attributes) {
482
482
  if (a.type === "JSXSpreadAttribute") {
483
483
  const arg = unwrapTs(a.argument);
484
484
  if (arg?.type === "Identifier" && (arg.name === restName || arg.name === propsName)) {
485
- for (const [k, v] of use) if (arg.name === propsName || !named2.has(k)) out.set(k, v);
485
+ for (const [k, v] of use) if (arg.name === propsName || !named.has(k)) out.set(k, v);
486
486
  }
487
487
  continue;
488
488
  }
@@ -490,14 +490,14 @@ function renderedAttributes(el, fn, use, mod) {
490
490
  if (!key) continue;
491
491
  const expr = a.value?.type === "JSXExpressionContainer" ? unwrapTs(a.value.expression) : void 0;
492
492
  if (expr?.type === "Identifier" && locals.has(expr.name)) {
493
- const { prop, fallback } = locals.get(expr.name);
494
- const v = use.has(prop) ? use.get(prop) : fallback;
493
+ const { prop: prop2, fallback } = locals.get(expr.name);
494
+ const v = use.has(prop2) ? use.get(prop2) : fallback;
495
495
  if (v !== void 0) out.set(key, v);
496
496
  continue;
497
497
  }
498
498
  if (expr?.type === "MemberExpression" && unwrapTs(expr.object)?.type === "Identifier" && unwrapTs(expr.object).name === propsName) {
499
- const prop = keyName(expr.property);
500
- if (prop && use.has(prop)) out.set(key, use.get(prop));
499
+ const prop2 = keyName(expr.property);
500
+ if (prop2 && use.has(prop2)) out.set(key, use.get(prop2));
501
501
  continue;
502
502
  }
503
503
  out.set(key, attrValue(a, [], mod.scope));
@@ -893,8 +893,8 @@ function actionHosts(pages, graph, actionExports) {
893
893
  // src/login.ts
894
894
  import { readFileSync as readFileSync2 } from "fs";
895
895
  import { join as join2 } from "path";
896
- import { LOGIN_NAME, NOT_A_LOGIN, loginFields } from "@exposify/core";
897
- var SESSION_CODE = /cookies\(\)\s*\.set|\.cookies\.set\(|cookieStore\.set|set-cookie|setCookie|\bjwt\.sign\(|new\s+SignJWT|\bsign\(|createSession|session\.(save|create)|\.login\(/i;
896
+ import { LOGIN_NAME, NOT_A_LOGIN, NOT_A_VERIFY, START_NAME, VERIFY_NAME, loginFields, startFields, verifyFields } from "@exposify/core";
897
+ var SESSION_CODE = /cookies\(\)\)?\s*\.set|\.cookies\.set\(|cookieStore\.set|set-cookie|setCookie|\bjwt\.sign\(|new\s+SignJWT|\bsign\(|createSession|session\.(save|create)|\.login\(/i;
898
898
  function routeBodyFields(source) {
899
899
  const names = /* @__PURE__ */ new Set();
900
900
  let body = null;
@@ -915,16 +915,27 @@ function routeBodyFields(source) {
915
915
  }
916
916
  return names.size && body ? { fields: [...names], body } : null;
917
917
  }
918
- function actionCandidate(a) {
919
- if (NOT_A_LOGIN.test(a.key) || NOT_A_LOGIN.test(a.action.name)) return null;
918
+ var LOGIN_RULE = { not: NOT_A_LOGIN, name: LOGIN_NAME, mustBeNamed: false, fields: (n) => loginFields(n), session: true };
919
+ var VERIFY_RULE = { not: NOT_A_VERIFY, name: VERIFY_NAME, mustBeNamed: true, fields: (n, named) => verifyFields(n, named), session: true };
920
+ var START_RULE = { not: NOT_A_LOGIN, name: new RegExp(`${LOGIN_NAME.source}|${START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => startFields(n), session: false };
921
+ var knownKey = (l) => l.kind === "action" ? `${l.file}#${l.export}` : l.file;
922
+ function actionCandidate(a, rule, known) {
923
+ if (rule.not.test(a.key) || rule.not.test(a.action.name)) return null;
920
924
  if (!a.action.params.some((p) => p.role === "formData" || p.role === "object")) return null;
921
- const fields = loginFields(a.action.fields.map((f) => f.name));
925
+ const isNamed = rule.name.test(a.action.name);
926
+ if (rule.mustBeNamed && !isNamed) return null;
927
+ const fields = rule.fields(
928
+ a.action.fields.map((f) => f.name),
929
+ isNamed
930
+ );
922
931
  if (!fields) return null;
923
- if (!a.action.setsCookies && !LOGIN_NAME.test(a.action.name)) return null;
932
+ if (rule.session && !a.action.setsCookies && !LOGIN_NAME.test(a.action.name) && !(rule === LOGIN_RULE && known.has(`${a.file}#${a.action.name}`))) return null;
924
933
  return { kind: "action", key: a.key, export: a.action.name, file: a.file, fields, ...a.host ? { host: a.host } : {} };
925
934
  }
926
- function routeCandidate(root, r) {
927
- if (r.router !== "app" || !r.module.methods.includes("POST") || r.params.length > 0 || NOT_A_LOGIN.test(r.path)) return null;
935
+ function routeCandidate(root, r, rule, known) {
936
+ if (r.router !== "app" || !r.module.methods.includes("POST") || r.params.length > 0 || rule.not.test(r.path)) return null;
937
+ const isNamed = rule.name.test(r.path);
938
+ if (rule.mustBeNamed && !isNamed) return null;
928
939
  let source;
929
940
  try {
930
941
  source = readFileSync2(join2(root, r.file), "utf8");
@@ -932,29 +943,47 @@ function routeCandidate(root, r) {
932
943
  return null;
933
944
  }
934
945
  const read2 = routeBodyFields(source);
935
- const fields = read2 ? loginFields(read2.fields) : null;
946
+ const fields = read2 ? rule.fields(read2.fields, isNamed) : null;
936
947
  if (!read2 || !fields) return null;
937
- if (!LOGIN_NAME.test(r.path) && !SESSION_CODE.test(source)) return null;
948
+ if (rule.session && !LOGIN_NAME.test(r.path) && !SESSION_CODE.test(source) && !(rule === LOGIN_RULE && known.has(r.file))) return null;
938
949
  return { kind: "route", path: r.path, file: r.file, fields, body: read2.body };
939
950
  }
940
- var named = (l) => LOGIN_NAME.test(l.kind === "action" ? l.export : l.path);
941
- function findLogin(d) {
942
- const candidates = [];
951
+ var nameOf = (l) => l.kind === "action" ? l.export : l.path;
952
+ function candidates(d, rule, known) {
953
+ const out = [];
943
954
  for (const a of d.actions) {
944
- const c = actionCandidate(a);
945
- if (c) candidates.push(c);
955
+ const c = actionCandidate(a, rule, known);
956
+ if (c) out.push(c);
946
957
  }
947
958
  for (const r of d.routes) {
948
- const c = routeCandidate(d.root, r);
949
- if (c) candidates.push(c);
950
- }
951
- if (candidates.length === 0) return null;
952
- const score = (l) => (named(l) ? 0 : 4) + (l.kind === "action" && l.host ? 0 : 2) + l.fields.length / 100;
953
- candidates.sort((a, b) => score(a) - score(b));
954
- return { login: candidates[0], others: candidates.slice(1) };
959
+ const c = routeCandidate(d.root, r, rule, known);
960
+ if (c) out.push(c);
961
+ }
962
+ const score = (l) => (rule === LOGIN_RULE && known.has(knownKey(l)) ? 0 : 8) + (rule.name.test(nameOf(l)) ? 0 : 4) + (l.kind === "action" && l.host ? 0 : 2) + l.fields.length / 100;
963
+ return out.sort((a, b) => score(a) - score(b));
964
+ }
965
+ function findLogin(d, known = /* @__PURE__ */ new Set()) {
966
+ const verifies = candidates(d, VERIFY_RULE, known);
967
+ const logins = candidates(d, LOGIN_RULE, known);
968
+ if (logins.length > 0) {
969
+ const [first2, ...others2] = logins;
970
+ return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2 };
971
+ }
972
+ const pairs = [];
973
+ for (const start of candidates(d, START_RULE, known)) {
974
+ const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
975
+ const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
976
+ if (verify) pairs.push({ ...start, passwordless: true, verify });
977
+ }
978
+ if (pairs.length === 0) return null;
979
+ const [first, ...others] = pairs;
980
+ return { login: first, others };
981
+ }
982
+ function stepLabel(l) {
983
+ return l.kind === "action" ? `server action ${l.export} (${l.file})` : `POST ${l.path} (${l.file})`;
955
984
  }
956
985
  function loginLabel(l) {
957
- return l.kind === "action" ? `server action ${l.export} (${l.file})` : `POST ${l.path} (${l.file})`;
986
+ return l.verify ? `${stepLabel(l)}, then a code: ${stepLabel(l.verify)}` : stepLabel(l);
958
987
  }
959
988
 
960
989
  // src/guards.ts
@@ -1136,9 +1165,9 @@ function pageSearchParams(program) {
1136
1165
  if (!FUNCTION_TYPES.has(fn.type)) return;
1137
1166
  const param = fn.params?.[0];
1138
1167
  if (param?.type === "ObjectPattern") {
1139
- for (const prop of param.properties) {
1140
- if (prop.type !== "ObjectProperty" || keyName(prop.key) !== "searchParams") continue;
1141
- const value = prop.value.type === "AssignmentPattern" ? prop.value.left : prop.value;
1168
+ for (const prop2 of param.properties) {
1169
+ if (prop2.type !== "ObjectProperty" || keyName(prop2.key) !== "searchParams") continue;
1170
+ const value = prop2.value.type === "AssignmentPattern" ? prop2.value.left : prop2.value;
1142
1171
  if (value.type === "Identifier") sources.add(value.name);
1143
1172
  for (const k of patternKeys(value)) names.add(k);
1144
1173
  }
@@ -1205,10 +1234,10 @@ function staticMetadata(program) {
1205
1234
  const init = unwrapTs(d.init);
1206
1235
  if (d.id?.type !== "Identifier" || d.id.name !== "metadata" || init?.type !== "ObjectExpression") continue;
1207
1236
  const out = {};
1208
- for (const prop of init.properties) {
1209
- if (prop.type !== "ObjectProperty") continue;
1210
- const key = keyName(prop.key);
1211
- const value = unwrapTs(prop.value);
1237
+ for (const prop2 of init.properties) {
1238
+ if (prop2.type !== "ObjectProperty") continue;
1239
+ const key = keyName(prop2.key);
1240
+ const value = unwrapTs(prop2.value);
1212
1241
  if (key === "description") {
1213
1242
  const s = stringValue(value);
1214
1243
  if (s) out.description = s;
@@ -1311,15 +1340,15 @@ function readTsconfig(root) {
1311
1340
  }
1312
1341
  }
1313
1342
  function asFile(path) {
1314
- const candidates = [path, ...EXTENSIONS.map((e) => path + e), ...EXTENSIONS.map((e) => join5(path, `index${e}`))];
1315
- if (/\.[mc]?jsx?$/.test(path)) candidates.push(...EXTENSIONS.map((e) => path.replace(/\.[mc]?jsx?$/, e)));
1316
- return candidates.find((c) => existsSync(c) && statSync(c).isFile());
1343
+ const candidates2 = [path, ...EXTENSIONS.map((e) => path + e), ...EXTENSIONS.map((e) => join5(path, `index${e}`))];
1344
+ if (/\.[mc]?jsx?$/.test(path)) candidates2.push(...EXTENSIONS.map((e) => path.replace(/\.[mc]?jsx?$/, e)));
1345
+ return candidates2.find((c) => existsSync(c) && statSync(c).isFile());
1317
1346
  }
1318
1347
  function moduleLoader(root) {
1319
1348
  const { baseUrl, paths } = readTsconfig(root);
1320
1349
  const base = join5(root, baseUrl ?? ".");
1321
1350
  const cache = /* @__PURE__ */ new Map();
1322
- const candidates = (specifier2, fromFile) => {
1351
+ const candidates2 = (specifier2, fromFile) => {
1323
1352
  if (specifier2.startsWith(".")) return [resolve(root, dirname(fromFile), specifier2)];
1324
1353
  for (const [pattern, targets] of Object.entries(paths ?? {})) {
1325
1354
  const star = pattern.indexOf("*");
@@ -1336,7 +1365,7 @@ function moduleLoader(root) {
1336
1365
  const key = `${fromFile}\0${specifier2}`;
1337
1366
  if (cache.has(key)) return cache.get(key);
1338
1367
  let found;
1339
- for (const candidate of candidates(specifier2, fromFile)) {
1368
+ for (const candidate of candidates2(specifier2, fromFile)) {
1340
1369
  const path = asFile(candidate);
1341
1370
  const rel = path && relative(root, path);
1342
1371
  if (!path || !rel || rel.startsWith("..") || rel.split(sep).includes("node_modules")) continue;
@@ -1368,11 +1397,11 @@ function writtenInput(n) {
1368
1397
  n = unwrapTs(n);
1369
1398
  if (n?.type !== "ObjectExpression") return "opaque";
1370
1399
  const out = {};
1371
- for (const prop of n.properties) {
1372
- const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
1400
+ for (const prop2 of n.properties) {
1401
+ const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
1373
1402
  if (!key) continue;
1374
1403
  let values;
1375
- walk(prop.value, (c) => {
1404
+ walk(prop2.value, (c) => {
1376
1405
  if (values || c.type !== "CallExpression" || keyName(c.callee?.property) !== "enum") return;
1377
1406
  const list = unwrapTs(c.arguments[0]);
1378
1407
  const items = list?.type === "ArrayExpression" ? list.elements.map((e) => stringValue(e)) : [];
@@ -1392,13 +1421,13 @@ function markerOf(init, imports) {
1392
1421
  const meta = call.arguments[1];
1393
1422
  const marker = { kind: "described" };
1394
1423
  if (meta?.type === "ObjectExpression") {
1395
- for (const prop of meta.properties) {
1396
- if (prop.type !== "ObjectProperty") continue;
1397
- const key = keyName(prop.key);
1398
- const value = stringValue(prop.value);
1424
+ for (const prop2 of meta.properties) {
1425
+ if (prop2.type !== "ObjectProperty") continue;
1426
+ const key = keyName(prop2.key);
1427
+ const value = stringValue(prop2.value);
1399
1428
  if (key === "name" && value) marker.name = value;
1400
1429
  if (key === "description" && value) marker.description = value;
1401
- if (key === "input") marker.input = writtenInput(prop.value);
1430
+ if (key === "input") marker.input = writtenInput(prop2.value);
1402
1431
  }
1403
1432
  }
1404
1433
  return { marker, handler };
@@ -1669,13 +1698,13 @@ function findInlineActions(source, file) {
1669
1698
  const program = parseModule(source, file);
1670
1699
  if (!program) return [];
1671
1700
  const out = [];
1672
- const named2 = /* @__PURE__ */ new Map();
1701
+ const named = /* @__PURE__ */ new Map();
1673
1702
  walk(program, (n) => {
1674
- if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && n.init) named2.set(unwrapTs(n.init), n.id.name);
1703
+ if (n.type === "VariableDeclarator" && n.id.type === "Identifier" && n.init) named.set(unwrapTs(n.init), n.id.name);
1675
1704
  if (!FUNCTION_TYPES.has(n.type) || n.body?.type !== "BlockStatement") return;
1676
1705
  if (!n.body.directives.some((d) => d.value?.value === "use server")) return;
1677
1706
  if (isActionModule(program) && program.body.some((s) => s === n || s.declaration === n || s.declaration?.declarations?.some((d) => unwrapTs(d.init) === n))) return;
1678
- const name = n.id?.name ?? named2.get(n);
1707
+ const name = n.id?.name ?? named.get(n);
1679
1708
  out.push({ ...name ? { name } : {}, line: n.loc?.start.line ?? 1 });
1680
1709
  });
1681
1710
  return out;
@@ -1696,13 +1725,13 @@ function findAiReadyCall(source, file) {
1696
1725
  const call = { line: n.loc?.start.line ?? 1, otherKeys: [] };
1697
1726
  const options = unwrapTs(n.arguments[1]);
1698
1727
  if (options && options.type !== "ObjectExpression") call.otherKeys.push("(options variable)");
1699
- for (const prop of options?.type === "ObjectExpression" ? options.properties : []) {
1700
- const key = prop.type === "ObjectProperty" ? keyName(prop.key) : void 0;
1728
+ for (const prop2 of options?.type === "ObjectExpression" ? options.properties : []) {
1729
+ const key = prop2.type === "ObjectProperty" ? keyName(prop2.key) : void 0;
1701
1730
  if (!key) {
1702
1731
  call.otherKeys.push("(spread)");
1703
1732
  continue;
1704
1733
  }
1705
- const value = unwrapTs(prop.value);
1734
+ const value = unwrapTs(prop2.value);
1706
1735
  if (key === "expose" && stringValue(value) === "*") call.expose = "*";
1707
1736
  else if ((key === "expose" || key === "exclude") && value?.type === "ArrayExpression") {
1708
1737
  call[key] = value.elements.map((e) => stringValue(e)).filter((s) => s !== void 0);
@@ -1733,6 +1762,229 @@ function findAiReadyCall(source, file) {
1733
1762
  return found;
1734
1763
  }
1735
1764
 
1765
+ // src/supabase.ts
1766
+ import { readFileSync as readFileSync6 } from "fs";
1767
+ import { join as join6 } from "path";
1768
+ var AUTH_CALLS = {
1769
+ signInWithPassword: "password",
1770
+ signInWithOtp: "email-code",
1771
+ verifyOtp: "email-code",
1772
+ signInWithOAuth: "social",
1773
+ signInWithIdToken: "other",
1774
+ signInWithSSO: "other",
1775
+ signInAnonymously: "other",
1776
+ signInWithWeb3: "other"
1777
+ };
1778
+ var MFA_CALLS = /* @__PURE__ */ new Set(["challenge", "verify", "challengeAndVerify"]);
1779
+ var APP_SERVER_FILE = /^(page|layout|template|route|default|loading|not-found|forbidden|unauthorized|error|global-error)\.[^.]+$/;
1780
+ var MIDDLEWARE_FILE = /^(src\/)?(middleware|proxy|instrumentation)\.[^./]+$/;
1781
+ var directive2 = (program) => program.directives?.[0]?.value?.value;
1782
+ var baseName = (f) => f.slice(f.lastIndexOf("/") + 1);
1783
+ function isAuthObject(n) {
1784
+ n = unwrapTs(n);
1785
+ if (n?.type === "Identifier") return n.name === "auth";
1786
+ return (n?.type === "MemberExpression" || n?.type === "OptionalMemberExpression") && keyName(n.property) === "auth" && !n.computed;
1787
+ }
1788
+ var propertyOf = (n) => n.computed ? stringValue(n.property) : keyName(n.property);
1789
+ function prop(obj, name) {
1790
+ obj = unwrapTs(obj);
1791
+ if (obj?.type !== "ObjectExpression") return void 0;
1792
+ return obj.properties.find((p) => p.type === "ObjectProperty" && keyName(p.key) === name)?.value;
1793
+ }
1794
+ function findSupabase(root, files, graph, dependencies, dirs) {
1795
+ const ssr = dependencies.includes("@supabase/ssr");
1796
+ if (!ssr) return dependencies.includes("@supabase/auth-helpers-nextjs") ? { authHelpers: true, signIns: [], callbacks: [] } : null;
1797
+ const facts = /* @__PURE__ */ new Map();
1798
+ const factsOf = (file) => {
1799
+ if (facts.has(file)) return facts.get(file);
1800
+ let found = null;
1801
+ try {
1802
+ const source = readFileSync6(join6(root, file), "utf8");
1803
+ const program = parseModule(source, file);
1804
+ if (program) {
1805
+ const d = directive2(program);
1806
+ found = {
1807
+ program,
1808
+ ...d ? { directive: d } : {},
1809
+ serverClient: /\bcreateServerClient\s*\(|from\s+["']next\/headers["']/.test(source),
1810
+ browserClient: /\bcreateBrowserClient\s*\(/.test(source)
1811
+ };
1812
+ }
1813
+ } catch {
1814
+ found = null;
1815
+ }
1816
+ facts.set(file, found);
1817
+ return found;
1818
+ };
1819
+ const isPagesPage = (f) => Boolean(dirs.pagesDir && f.startsWith(`${dirs.pagesDir}/`) && !f.startsWith(`${dirs.pagesDir}/api/`));
1820
+ const isPagesApi = (f) => Boolean(dirs.pagesDir && f.startsWith(`${dirs.pagesDir}/api/`));
1821
+ const isAppRoute = (f) => Boolean(dirs.appDir && f.startsWith(`${dirs.appDir}/`) && /^route\./.test(baseName(f)));
1822
+ const candidates2 = files.filter((f) => {
1823
+ try {
1824
+ return /\b(signIn\w*|verifyOtp|exchangeCodeForSession|mfa|auth-ui-react|storageKey|cookieOptions)\b/.test(readFileSync6(join6(root, f), "utf8"));
1825
+ } catch {
1826
+ return false;
1827
+ }
1828
+ });
1829
+ const clientRoots = [];
1830
+ const serverRoots = [];
1831
+ for (const f of files) {
1832
+ if (isPagesPage(f)) {
1833
+ clientRoots.push(f);
1834
+ continue;
1835
+ }
1836
+ const isEntry = isPagesApi(f) || MIDDLEWARE_FILE.test(f) || Boolean(dirs.appDir && f.startsWith(`${dirs.appDir}/`) && APP_SERVER_FILE.test(baseName(f)));
1837
+ let text2;
1838
+ try {
1839
+ text2 = readFileSync6(join6(root, f), "utf8");
1840
+ } catch {
1841
+ continue;
1842
+ }
1843
+ const head = /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*["']use (client|server)["']/.exec(text2)?.[1];
1844
+ if (head === "client") clientRoots.push(f);
1845
+ else if (head === "server" || isEntry) serverRoots.push(f);
1846
+ }
1847
+ const browserSet = graph.reachable(clientRoots);
1848
+ const serverSet = /* @__PURE__ */ new Set();
1849
+ const queue = [...serverRoots];
1850
+ while (queue.length > 0 && serverSet.size < 2e3) {
1851
+ const f = queue.shift();
1852
+ if (serverSet.has(f)) continue;
1853
+ if (!serverRoots.includes(f) && factsOf(f)?.directive === "use client") continue;
1854
+ serverSet.add(f);
1855
+ for (const e of graph.edges(f)) if (!serverSet.has(e.file)) queue.push(e.file);
1856
+ }
1857
+ const sideOf = (file, inServerFn) => {
1858
+ const f = factsOf(file);
1859
+ if (inServerFn || f?.directive === "use server") return { side: "server", why: "a Server Action" };
1860
+ if (isAppRoute(file) || isPagesApi(file)) return { side: "server", why: "a route handler" };
1861
+ if (f?.directive === "use client") return { side: "browser", why: '"use client"' };
1862
+ if (isPagesPage(file)) return { side: "browser", why: "a Pages Router page" };
1863
+ let server = f?.serverClient ?? false;
1864
+ let browser = f?.browserClient ?? false;
1865
+ let helper;
1866
+ if (!server && !browser) {
1867
+ for (const e of graph.edges(file)) {
1868
+ const g = factsOf(e.file);
1869
+ if (g?.serverClient && !g.browserClient) {
1870
+ server = true;
1871
+ helper = e.file;
1872
+ }
1873
+ if (g?.browserClient && !g.serverClient) {
1874
+ browser = true;
1875
+ helper = e.file;
1876
+ }
1877
+ }
1878
+ }
1879
+ if (server !== browser) return { side: server ? "server" : "browser", why: `uses the ${server ? "server" : "browser"} client${helper ? ` from ${helper}` : ""}` };
1880
+ const inBrowser = browserSet.has(file);
1881
+ const inServer = serverSet.has(file);
1882
+ if (inBrowser && !inServer) return { side: "browser", why: "imported by browser code" };
1883
+ if (inServer && !inBrowser) return { side: "server", why: "imported by server code" };
1884
+ return { side: "unknown", why: inBrowser ? "imported by both browser and server code" : "not imported by a page or route we could follow" };
1885
+ };
1886
+ const out = { signIns: [], callbacks: [] };
1887
+ for (const file of candidates2) {
1888
+ const f = factsOf(file);
1889
+ if (!f) continue;
1890
+ const top = f.program;
1891
+ const topName = (n) => {
1892
+ const d = n.type === "ExportNamedDeclaration" ? n.declaration : n.type === "ExportDefaultDeclaration" ? n.declaration : n;
1893
+ if (n.type === "ExportDefaultDeclaration") return "default";
1894
+ if (d?.type === "FunctionDeclaration") return d.id?.name;
1895
+ if (d?.type === "VariableDeclaration") return keyName(d.declarations?.[0]?.id);
1896
+ return void 0;
1897
+ };
1898
+ let authUi;
1899
+ walk(top, (n, parents) => {
1900
+ if (n.type === "ImportDeclaration" && n.source?.value === "@supabase/auth-ui-react") {
1901
+ authUi = n.specifiers.find((s) => s.type === "ImportSpecifier" && keyName(s.imported) === "Auth")?.local?.name;
1902
+ }
1903
+ if (n.type === "ObjectProperty" && !out.cookieName) {
1904
+ const key = keyName(n.key);
1905
+ const value = stringValue(unwrapTs(n.value));
1906
+ const parentProp = parents.at(-2);
1907
+ if (key === "storageKey" && value) out.cookieName = value;
1908
+ if (key === "name" && value && parentProp?.type === "ObjectProperty" && keyName(parentProp.key) === "cookieOptions") out.cookieName = value;
1909
+ }
1910
+ const push = (call, method2, extra = {}) => {
1911
+ const inServerFn = parents.some((p) => FUNCTION_TYPES.has(p.type) && p.body?.directives?.some((d) => d.value?.value === "use server"));
1912
+ const { side, why } = sideOf(file, inServerFn);
1913
+ const holder = parents.find((p) => top.body.includes(p));
1914
+ const name2 = holder ? topName(holder) : void 0;
1915
+ out.signIns.push({
1916
+ method: method2,
1917
+ call,
1918
+ ...extra,
1919
+ file,
1920
+ line: n.loc?.start.line ?? 1,
1921
+ side,
1922
+ why,
1923
+ ...side === "server" && name2 && (isActionModule(top) || inServerFn) ? { action: `${file}#${name2}` } : {},
1924
+ ...side === "server" && (isAppRoute(file) || isPagesApi(file)) ? { route: file } : {}
1925
+ });
1926
+ };
1927
+ if (n.type === "JSXOpeningElement" && authUi && n.name?.type === "JSXIdentifier" && n.name.name === authUi) {
1928
+ push("<Auth />", "password");
1929
+ return;
1930
+ }
1931
+ if (n.type !== "CallExpression" && n.type !== "OptionalCallExpression") return;
1932
+ const callee = unwrapTs(n.callee);
1933
+ if (callee?.type !== "MemberExpression" && callee?.type !== "OptionalMemberExpression") return;
1934
+ const name = propertyOf(callee);
1935
+ if (!name) return;
1936
+ const object = unwrapTs(callee.object);
1937
+ if (MFA_CALLS.has(name) && (object?.type === "MemberExpression" || object?.type === "OptionalMemberExpression") && propertyOf(object) === "mfa" && isAuthObject(object.object)) {
1938
+ push(`mfa.${name}`, "totp");
1939
+ return;
1940
+ }
1941
+ if (!isAuthObject(object)) return;
1942
+ const arg = n.arguments?.[0];
1943
+ if (name === "exchangeCodeForSession" || name === "verifyOtp" && prop(arg, "token_hash")) {
1944
+ if (!out.callbacks.includes(file)) out.callbacks.push(file);
1945
+ return;
1946
+ }
1947
+ const method = Object.hasOwn(AUTH_CALLS, name) ? AUTH_CALLS[name] : void 0;
1948
+ if (!method) return;
1949
+ const provider = name === "signInWithOAuth" ? stringValue(unwrapTs(prop(arg, "provider"))) : void 0;
1950
+ push(name, method, provider ? { provider } : {});
1951
+ });
1952
+ }
1953
+ return out;
1954
+ }
1955
+ function signInLabel(s) {
1956
+ const side = s.side === "unknown" ? "side unclear, counted as server" : s.side;
1957
+ return `${s.call}${s.provider ? ` (${s.provider})` : ""} in ${s.file}:${s.line} (${side}: ${s.why})`;
1958
+ }
1959
+ function supabaseChoice(found, login, loginPages2 = []) {
1960
+ const onServer = (method) => found.signIns.filter((s) => s.method === method && s.side !== "browser");
1961
+ const direct = [];
1962
+ const unusable = [];
1963
+ for (const method of ["password", "email-code"]) {
1964
+ const server = onServer(method);
1965
+ if (server.length === 0) direct.push(method);
1966
+ else if (!login) unusable.push(...server.map(signInLabel));
1967
+ }
1968
+ direct.push("social");
1969
+ return {
1970
+ ...found.cookieName ? { cookieName: found.cookieName } : {},
1971
+ direct,
1972
+ found: found.signIns.map(signInLabel),
1973
+ ...unusable.length ? { unusable } : {},
1974
+ ...found.callbacks.length ? { callbacks: found.callbacks } : {},
1975
+ ...loginPages2.length ? { loginPages: loginPages2 } : {}
1976
+ };
1977
+ }
1978
+ function knownLogins(found) {
1979
+ const out = /* @__PURE__ */ new Set();
1980
+ for (const s of found?.signIns ?? []) {
1981
+ if (s.method !== "password" || s.side === "browser") continue;
1982
+ if (s.action) out.add(s.action);
1983
+ if (s.route) out.add(s.route);
1984
+ }
1985
+ return out;
1986
+ }
1987
+
1736
1988
  // src/codegen.ts
1737
1989
  var DEFAULT_DOCS = ["README.md", "docs/**/*.md"];
1738
1990
  var actionRoute = (mcpPath, key) => `POST ${(mcpPath ?? "/mcp").replace(/\/$/, "")}/actions/${key}`;
@@ -1742,14 +1994,14 @@ function actionSignature(route, source) {
1742
1994
  }
1743
1995
  var posix = (p) => p.split(sep2).join("/");
1744
1996
  function firstDir(root, names) {
1745
- return names.find((n) => existsSync2(join6(root, n)));
1997
+ return names.find((n) => existsSync2(join7(root, n)));
1746
1998
  }
1747
1999
  function filesUnder(root, dir) {
1748
2000
  const out = [];
1749
2001
  const visit = (rel) => {
1750
2002
  let entries;
1751
2003
  try {
1752
- entries = readdirSync(join6(root, rel), { withFileTypes: true });
2004
+ entries = readdirSync(join7(root, rel), { withFileTypes: true });
1753
2005
  } catch {
1754
2006
  return;
1755
2007
  }
@@ -1776,7 +2028,7 @@ function discover(root, opts = {}) {
1776
2028
  const dirSegs = file.slice(appDir.length + 1).split("/").slice(0, -1);
1777
2029
  const route = appRoute(dirSegs);
1778
2030
  if (!route) continue;
1779
- const source = readFileSync6(join6(root, file), "utf8");
2031
+ const source = readFileSync7(join7(root, file), "utf8");
1780
2032
  const module = analyzeRouteModule(source, file);
1781
2033
  if (module.mcpRoute) {
1782
2034
  out.mcpRoute ??= { file, source };
@@ -1792,7 +2044,7 @@ function discover(root, opts = {}) {
1792
2044
  const segs = file.slice(pagesDir.length + 1).replace(extPattern, "").split("/");
1793
2045
  const route = pagesApiRoute(segs);
1794
2046
  if (!route) continue;
1795
- const source = readFileSync6(join6(root, file), "utf8");
2047
+ const source = readFileSync7(join7(root, file), "utf8");
1796
2048
  const src = defaultExportSource(source, file);
1797
2049
  out.routes.push({ router: "pages", file, ...route, detected: detectPagesMethods(source, file), ...src ? { source: src } : {} });
1798
2050
  }
@@ -1804,14 +2056,22 @@ function discover(root, opts = {}) {
1804
2056
  const catchAll = route ? /^(.*)\/:[A-Za-z0-9_]+\*\??$/.exec(route.path) : null;
1805
2057
  if (catchAll) out.mcpPath = catchAll[1] || "/";
1806
2058
  }
1807
- discoverActions(root, out, exts);
1808
- const found = findLogin(out);
2059
+ const { files, graph, pages } = discoverActions(root, out, exts);
2060
+ const supabase = findSupabase(root, files, graph, dependencyNames(readJson(join7(root, "package.json"))), out);
2061
+ const found = findLogin(out, knownLogins(supabase));
1809
2062
  if (found) {
1810
2063
  out.login = found.login;
1811
2064
  out.loginOthers = found.others;
1812
2065
  }
2066
+ if (supabase && !supabase.authHelpers) out.supabase = { found: supabase, choice: supabaseChoice(supabase, out.login, loginPages(supabase, pages, graph)) };
2067
+ else if (supabase) out.supabase = { found: supabase, choice: { direct: [], found: [] } };
1813
2068
  return out;
1814
2069
  }
2070
+ function loginPages(found, pages, graph) {
2071
+ const files = new Set(found.signIns.filter((s) => s.method === "password" || s.method === "email-code").map((s) => s.file));
2072
+ if (files.size === 0) return [];
2073
+ return pages.filter((p) => p.params.length === 0 && [...graph.reachable(p.roots)].some((f) => files.has(f))).map((p) => p.path);
2074
+ }
1815
2075
  var SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs}";
1816
2076
  var NOT_SOURCE = ["**/node_modules/**", ".next/**", "out/**", "build/**", "dist/**", "coverage/**", "**/*.d.ts"];
1817
2077
  function snakeCase(name) {
@@ -1825,7 +2085,7 @@ function discoverActions(root, out, exts) {
1825
2085
  for (const file of files) {
1826
2086
  let source;
1827
2087
  try {
1828
- source = readFileSync6(join6(root, file), "utf8");
2088
+ source = readFileSync7(join7(root, file), "utf8");
1829
2089
  } catch {
1830
2090
  continue;
1831
2091
  }
@@ -1882,24 +2142,25 @@ function discoverActions(root, out, exts) {
1882
2142
  const found2 = guards.action(a.file, a.action.name);
1883
2143
  if (found2.length > 0) a.guards = found2;
1884
2144
  }
2145
+ return { files, graph, pages };
1885
2146
  }
1886
2147
  function readJson(path) {
1887
2148
  try {
1888
- return JSON.parse(readFileSync6(path, "utf8"));
2149
+ return JSON.parse(readFileSync7(path, "utf8"));
1889
2150
  } catch {
1890
2151
  return void 0;
1891
2152
  }
1892
2153
  }
1893
2154
  function readProposedInputs(root) {
1894
- const path = join6(root, DESCRIPTIONS_FILE);
2155
+ const path = join7(root, DESCRIPTIONS_FILE);
1895
2156
  if (!existsSync2(path)) return {};
1896
- const parsed = parseDescriptions(readFileSync6(path, "utf8"));
2157
+ const parsed = parseDescriptions(readFileSync7(path, "utf8"));
1897
2158
  return typeof parsed === "string" ? {} : parsed.inputs ?? {};
1898
2159
  }
1899
2160
  function readDescriptions(root, warn) {
1900
- const path = join6(root, DESCRIPTIONS_FILE);
2161
+ const path = join7(root, DESCRIPTIONS_FILE);
1901
2162
  if (!existsSync2(path)) return {};
1902
- const parsed = parseDescriptions(readFileSync6(path, "utf8"));
2163
+ const parsed = parseDescriptions(readFileSync7(path, "utf8"));
1903
2164
  if (typeof parsed === "string") {
1904
2165
  warn(`ignoring ${DESCRIPTIONS_FILE}: ${parsed}`);
1905
2166
  return {};
@@ -1911,22 +2172,28 @@ function readDocs(root, globs) {
1911
2172
  const docs = [];
1912
2173
  for (const path of paths) {
1913
2174
  try {
1914
- docs.push({ path, content: readFileSync6(join6(root, path), "utf8") });
2175
+ docs.push({ path, content: readFileSync7(join7(root, path), "utf8") });
1915
2176
  } catch {
1916
2177
  }
1917
2178
  }
1918
2179
  return docs;
1919
2180
  }
1920
2181
  function defaultOutFile(root) {
1921
- return existsSync2(join6(root, "tsconfig.json")) ? "exposify.generated.ts" : "exposify.generated.js";
2182
+ return existsSync2(join7(root, "tsconfig.json")) ? "exposify.generated.ts" : "exposify.generated.js";
1922
2183
  }
1923
2184
  function specifier(outFile, file) {
1924
2185
  const rel = posix(relative2(dirname2(outFile), file)).replace(/\.[^./]+$/, "");
1925
2186
  return rel.startsWith(".") ? rel : `./${rel}`;
1926
2187
  }
1927
2188
  function manifestLogin(l) {
1928
- return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {}, where: `${l.export} in ${l.file}` } : { kind: "route", path: l.path, fields: l.fields, body: l.body, where: `POST ${l.path} in ${l.file}` };
2189
+ const verify = l.verify ? manifestStep(l.verify) : void 0;
2190
+ const where = l.verify ? `${whereOf(l)}, then a code: ${whereOf(l.verify)}` : whereOf(l);
2191
+ return { ...manifestStep(l), where, ...l.passwordless ? { passwordless: true } : {}, ...verify ? { verify } : {} };
2192
+ }
2193
+ function manifestStep(l) {
2194
+ return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {} } : { kind: "route", path: l.path, fields: l.fields, body: l.body };
1929
2195
  }
2196
+ var whereOf = (l) => l.kind === "action" ? `${l.export} in ${l.file}` : `POST ${l.path} in ${l.file}`;
1930
2197
  function renderManifest(d, opts) {
1931
2198
  const ts = opts.outFile.endsWith(".ts");
1932
2199
  const j = (v) => JSON.stringify(v);
@@ -1961,22 +2228,23 @@ function renderManifest(d, opts) {
1961
2228
  return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
1962
2229
  });
1963
2230
  const lines = [
1964
- "// Generated by exposify-next from your route files. Don't edit: withExposify()",
2231
+ "// Generated by @exposify/next from your route files. Don't edit: withExposify()",
1965
2232
  "// in next.config regenerates it on `next dev` and `next build`, or run",
1966
- "// `npx exposify-next generate`.",
2233
+ "// `npx @exposify/next generate`.",
1967
2234
  "/* eslint-disable */",
1968
2235
  ...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
1969
2236
  `export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
1970
2237
  ` info: ${j(opts.info)},`,
1971
2238
  ...opts.basePath ? [` basePath: ${j(opts.basePath)},`] : [],
1972
2239
  ...opts.trailingSlash ? [" trailingSlash: true,"] : [],
1973
- ...d.mcpPath && (actions.length > 0 || d.login) ? [` mcpPath: ${j(d.mcpPath)},`] : [],
2240
+ ...d.mcpPath && (actions.length > 0 || d.login || d.supabase) ? [` mcpPath: ${j(d.mcpPath)},`] : [],
1974
2241
  " routes: [",
1975
2242
  ...routes,
1976
2243
  " ],",
1977
2244
  ...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
1978
2245
  ...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
1979
2246
  ...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
2247
+ ...d.supabase && !d.supabase.found.authHelpers ? [` supabase: ${j(d.supabase.choice)},`] : [],
1980
2248
  ...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
1981
2249
  ` docs: ${j(opts.docs)},`,
1982
2250
  ...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
@@ -1988,7 +2256,7 @@ function renderManifest(d, opts) {
1988
2256
  function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)) {
1989
2257
  const outFile = opts.out ?? defaultOutFile(root);
1990
2258
  const discovery = discover(root, opts);
1991
- const pkg = readJson(join6(root, "package.json"));
2259
+ const pkg = readJson(join7(root, "package.json"));
1992
2260
  const info = { name: typeof pkg?.name === "string" ? pkg.name : "next-app", version: typeof pkg?.version === "string" ? pkg.version : "0.0.0" };
1993
2261
  const content = renderManifest(discovery, {
1994
2262
  outFile,
@@ -1999,18 +2267,24 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
1999
2267
  ...opts.trailingSlash ? { trailingSlash: true } : {},
2000
2268
  authStacks: authStacksOf(dependencyNames(pkg)).map((s) => s.pkg)
2001
2269
  });
2270
+ if (discovery.supabase?.found.authHelpers) {
2271
+ warn("@supabase/auth-helpers-nextjs keeps its session in an older cookie format AI assistants can't sign in with; move to @supabase/ssr (Supabase's replacement for it) to use Connect");
2272
+ }
2273
+ for (const s of discovery.supabase?.choice.unusable ?? []) {
2274
+ warn(`your app signs users in to Supabase on the server (${s}), but not in a Server Action or route the sign-in form can run, so AI assistants can't sign in with a password; switch on "Sign in with Supabase" on the dashboard to have them sign in to Supabase directly`);
2275
+ }
2002
2276
  for (const a of discovery.actions.filter((x) => x.proposed?.status === "stale")) {
2003
- warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`exposify-next describe\` reviews it again`);
2277
+ warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`@exposify/next describe\` reviews it again`);
2004
2278
  }
2005
2279
  if (discovery.actions.length > 0 && !discovery.mcpPath) {
2006
2280
  warn(
2007
- discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx exposify-next setup)`
2281
+ discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx @exposify/next setup)`
2008
2282
  );
2009
2283
  }
2010
- const file = join6(root, outFile);
2284
+ const file = join7(root, outFile);
2011
2285
  let current;
2012
2286
  try {
2013
- current = readFileSync6(file, "utf8");
2287
+ current = readFileSync7(file, "utf8");
2014
2288
  } catch {
2015
2289
  current = void 0;
2016
2290
  }
@@ -2018,14 +2292,17 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
2018
2292
  return { file, changed: current !== content, discovery };
2019
2293
  }
2020
2294
 
2295
+ // bin/exposify-next.ts
2296
+ import { createInterface } from "readline";
2297
+
2021
2298
  // bin/adapter.ts
2022
- import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
2023
- import { dirname as dirname4, join as join8, relative as relative3, resolve as resolve2 } from "path";
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";
2024
2301
  import { decideExposure, deriveDescription, resolveOptions, toolNames } from "@exposify/core";
2025
2302
  import { renderConfig, replaceAiReadyOptions } from "@exposify/cli";
2026
2303
 
2027
2304
  // bin/actions.ts
2028
- import { existsSync as existsSync3, readFileSync as readFileSync7 } from "fs";
2305
+ import { existsSync as existsSync3, readFileSync as readFileSync8 } from "fs";
2029
2306
 
2030
2307
  // src/inline.ts
2031
2308
  import { dirname as dirname3 } from "path";
@@ -2034,7 +2311,7 @@ function patternNames(p, out = []) {
2034
2311
  if (p.type === "Identifier") out.push(p.name);
2035
2312
  else if (p.type === "AssignmentPattern") patternNames(p.left, out);
2036
2313
  else if (p.type === "RestElement") patternNames(p.argument, out);
2037
- else if (p.type === "ObjectPattern") for (const prop of p.properties) patternNames(prop.type === "RestElement" ? prop : prop.value, out);
2314
+ else if (p.type === "ObjectPattern") for (const prop2 of p.properties) patternNames(prop2.type === "RestElement" ? prop2 : prop2.value, out);
2038
2315
  else if (p.type === "ArrayPattern") for (const el of p.elements) patternNames(el ?? void 0, out);
2039
2316
  else if (p.type === "TSParameterProperty") patternNames(p.parameter, out);
2040
2317
  return out;
@@ -2095,10 +2372,10 @@ function suggestInlineMove(source, file, line) {
2095
2372
  if (stmt.type === "ImportDeclaration") {
2096
2373
  const used = stmt.specifiers.filter((s) => reads.includes(s.local.name));
2097
2374
  if (used.length === 0) continue;
2098
- const named2 = used.filter((s) => s.type === "ImportSpecifier").map((s) => s.imported.name === s.local.name ? s.local.name : `${s.imported.name} as ${s.local.name}`);
2375
+ const named = used.filter((s) => s.type === "ImportSpecifier").map((s) => s.imported.name === s.local.name ? s.local.name : `${s.imported.name} as ${s.local.name}`);
2099
2376
  const def = used.find((s) => s.type === "ImportDefaultSpecifier")?.local.name;
2100
2377
  const ns = used.find((s) => s.type === "ImportNamespaceSpecifier")?.local.name;
2101
- const parts = [def, ns ? `* as ${ns}` : void 0, named2.length ? `{ ${named2.join(", ")} }` : void 0].filter(Boolean);
2378
+ const parts = [def, ns ? `* as ${ns}` : void 0, named.length ? `{ ${named.join(", ")} }` : void 0].filter(Boolean);
2102
2379
  imports.push(`import ${parts.join(", ")} from ${JSON.stringify(stmt.source.value)};`);
2103
2380
  } else {
2104
2381
  const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
@@ -2110,8 +2387,8 @@ function suggestInlineMove(source, file, line) {
2110
2387
  const dir = dirname3(file);
2111
2388
  const newFile = `${dir === "." ? "" : `${dir}/`}actions.${ext}`;
2112
2389
  const ownParams = target.params.map((p) => text(source, p) ?? paramName(p) ?? "arg");
2113
- const directive2 = target.body.directives.find((d) => d.value?.value === "use server");
2114
- const bodyText = source.slice(directive2.end, target.body.end - 1).replace(/^\s*\n/, "").replace(/\s+$/, "");
2390
+ const directive3 = target.body.directives.find((d) => d.value?.value === "use server");
2391
+ const bodyText = source.slice(directive3.end, target.body.end - 1).replace(/^\s*\n/, "").replace(/\s+$/, "");
2115
2392
  const bodyLines = bodyText.split("\n");
2116
2393
  const indent = Math.min(...bodyLines.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length), 99);
2117
2394
  const body = bodyLines.map((l) => ` ${l.slice(Math.min(indent, l.length - l.trimStart().length))}`.trimEnd()).join("\n");
@@ -2168,7 +2445,7 @@ function suggestInlineMove(source, file, line) {
2168
2445
  }
2169
2446
 
2170
2447
  // bin/actions.ts
2171
- import { join as join7 } from "path";
2448
+ import { join as join8 } from "path";
2172
2449
 
2173
2450
  // src/actions.ts
2174
2451
  var AUTH_LIKE = /^(log_?in|log_?out|sign_?in|sign_?out|sign_?up|register|authenticate)(_|$)/;
@@ -2243,7 +2520,7 @@ function formSources(root, forms) {
2243
2520
  const sources = [];
2244
2521
  for (const file of [...new Set(forms.map((f) => f.file))].slice(0, 2)) {
2245
2522
  try {
2246
- const text2 = readFileSync7(join7(root, file), "utf8");
2523
+ const text2 = readFileSync8(join8(root, file), "utf8");
2247
2524
  files.push(file);
2248
2525
  sources.push(text2.length > 6e3 ? `${text2.slice(0, 6e3)}
2249
2526
  /* \u2026 truncated */` : text2);
@@ -2253,11 +2530,11 @@ function formSources(root, forms) {
2253
2530
  return { files, sources };
2254
2531
  }
2255
2532
  function crossCheck(root, d) {
2256
- const path = join7(root, ".next", "server", "server-reference-manifest.json");
2533
+ const path = join8(root, ".next", "server", "server-reference-manifest.json");
2257
2534
  if (!existsSync3(path)) return [];
2258
2535
  let manifest;
2259
2536
  try {
2260
- manifest = JSON.parse(readFileSync7(path, "utf8"));
2537
+ manifest = JSON.parse(readFileSync8(path, "utf8"));
2261
2538
  } catch {
2262
2539
  return [];
2263
2540
  }
@@ -2351,7 +2628,7 @@ function scanActions(d, call, root, notes, suggestions = []) {
2351
2628
  for (const inline of d.inlineActions) {
2352
2629
  let move;
2353
2630
  try {
2354
- move = suggestInlineMove(readFileSync7(join7(root, inline.file), "utf8"), inline.file, inline.line);
2631
+ move = suggestInlineMove(readFileSync8(join8(root, inline.file), "utf8"), inline.file, inline.line);
2355
2632
  } catch {
2356
2633
  move = void 0;
2357
2634
  }
@@ -2373,9 +2650,9 @@ function scanActions(d, call, root, notes, suggestions = []) {
2373
2650
  var MCP_ROUTE_DIR = "mcp/[[...transport]]";
2374
2651
  function isNextProject(dir) {
2375
2652
  try {
2376
- const pkg = JSON.parse(readFileSync8(join8(dir, "package.json"), "utf8"));
2653
+ const pkg = JSON.parse(readFileSync9(join9(dir, "package.json"), "utf8"));
2377
2654
  const hasNext = Boolean(pkg.dependencies?.next ?? pkg.devDependencies?.next);
2378
- return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join8(dir, d)));
2655
+ return hasNext && ["app", "src/app", "pages", "src/pages"].some((d) => existsSync4(join9(dir, d)));
2379
2656
  } catch {
2380
2657
  return false;
2381
2658
  }
@@ -2387,7 +2664,7 @@ function findNextRoot(start) {
2387
2664
  }
2388
2665
  }
2389
2666
  var nextAdapter = {
2390
- bin: "exposify-next",
2667
+ bin: "npx @exposify/next",
2391
2668
  entryExample: "apps/web",
2392
2669
  entryHelp: "The Next.js project directory",
2393
2670
  locate(cwd, entryFlag) {
@@ -2425,7 +2702,7 @@ ${block}
2425
2702
  'Wrap your next.config export: `export default withExposify(nextConfig)`, imported from "@exposify/next/config". It keeps exposify.generated.ts current on `next dev` and `next build`.'
2426
2703
  );
2427
2704
  }
2428
- const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join8(root, f)).find((f) => existsSync4(f));
2705
+ const existing = ["exposify.generated.ts", "exposify.generated.js"].map((f) => join9(root, f)).find((f) => existsSync4(f));
2429
2706
  const manifestFile = existing ?? generate(root, {}, () => {
2430
2707
  }).file;
2431
2708
  const generated = relative3(root, manifestFile);
@@ -2439,7 +2716,7 @@ ${block}
2439
2716
  };
2440
2717
  }
2441
2718
  const routeFile = `${d.appDir ?? "app"}/${MCP_ROUTE_DIR}/route.${generated.endsWith(".ts") ? "ts" : "js"}`;
2442
- const manifestImport = relative3(join8(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
2719
+ const manifestImport = relative3(join9(root, dirname4(routeFile)), manifestFile).replace(/\.[jt]s$/, "");
2443
2720
  const code = [
2444
2721
  'import { aiReady } from "@exposify/next";',
2445
2722
  ...usesZod ? ['import { z } from "zod";'] : [],
@@ -2451,7 +2728,7 @@ ${block}
2451
2728
  ].join("\n");
2452
2729
  return {
2453
2730
  code,
2454
- apply: { plan: { file: join8(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2731
+ apply: { plan: { file: join9(root, routeFile), summary: `Create ${routeFile}`, before: null, after: `${code}
2455
2732
  ` } },
2456
2733
  hint: { lines: [`Create ${routeFile} with this (agents connect to /mcp). It imports ${generated}${existing ? "" : ", written just now"}.`, ...lines] }
2457
2734
  };
@@ -2460,7 +2737,7 @@ ${block}
2460
2737
  function hasWithExposify(root) {
2461
2738
  for (const name of ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.cjs", "next.config.mts"]) {
2462
2739
  try {
2463
- if (readFileSync8(join8(root, name), "utf8").includes("withExposify")) return true;
2740
+ if (readFileSync9(join9(root, name), "utf8").includes("withExposify")) return true;
2464
2741
  } catch {
2465
2742
  }
2466
2743
  }
@@ -2499,7 +2776,7 @@ function scanProject(root) {
2499
2776
  for (const p of d.pages) {
2500
2777
  let source;
2501
2778
  try {
2502
- source = readFileSync8(join8(root, p.file), "utf8");
2779
+ source = readFileSync9(join9(root, p.file), "utf8");
2503
2780
  } catch {
2504
2781
  source = void 0;
2505
2782
  }
@@ -2515,14 +2792,14 @@ function scanProject(root) {
2515
2792
  });
2516
2793
  }
2517
2794
  const exposure = new Map(rows.map((row) => [`${row.info.method} ${row.info.path}`, decideExposure(row.info, cfg)]));
2518
- const named2 = rows.map((row) => ({
2795
+ const named = rows.map((row) => ({
2519
2796
  method: row.info.method,
2520
2797
  path: row.info.path,
2521
2798
  params: row.info.params,
2522
2799
  ...row.info.marker ? { marker: row.info.marker.kind } : {},
2523
2800
  ...row.describedName ? { describedName: row.describedName } : {}
2524
2801
  }));
2525
- const names = toolNames(named2, (r) => exposure.get(`${r.method} ${r.path}`).exposed);
2802
+ const names = toolNames(named, (r) => exposure.get(`${r.method} ${r.path}`).exposed);
2526
2803
  for (const row of rows) if (row.page && row.describedName) names.set(`GET ${row.info.path}`, row.describedName);
2527
2804
  const suggestions = [];
2528
2805
  const actions = scanActions(d, call, root, notes, suggestions);
@@ -2556,9 +2833,18 @@ function scanProject(root) {
2556
2833
  ...notes.length > 0 ? { notes } : {},
2557
2834
  ...suggestions.length > 0 ? { suggestions } : {},
2558
2835
  ...d.login ? { login: { what: loginLabel(d.login), fields: d.login.fields.map((f) => f.name), ...d.loginOthers?.length ? { others: d.loginOthers.map(loginLabel) } : {} } } : {},
2836
+ ...d.supabase ? {
2837
+ supabase: {
2838
+ direct: d.supabase.choice.direct,
2839
+ found: d.supabase.choice.found,
2840
+ ...d.supabase.choice.unusable ? { unusable: d.supabase.choice.unusable } : {},
2841
+ ...d.supabase.choice.callbacks ? { callbacks: d.supabase.choice.callbacks } : {},
2842
+ ...d.supabase.found.authHelpers ? { authHelpers: true } : {}
2843
+ }
2844
+ } : {},
2559
2845
  ...call && d.mcpRoute ? {
2560
2846
  aiReady: {
2561
- site: { file: join8(root, d.mcpRoute.file), line: call.line },
2847
+ site: { file: join9(root, d.mcpRoute.file), line: call.line },
2562
2848
  options: {
2563
2849
  ...call.expose ? { expose: call.expose } : {},
2564
2850
  ...call.exclude ? { exclude: call.exclude } : {},
@@ -2572,7 +2858,7 @@ function scanProject(root) {
2572
2858
  }
2573
2859
  function applyReplace(site, config, entries, usesZod, relFile) {
2574
2860
  try {
2575
- const before = readFileSync8(site.file, "utf8");
2861
+ const before = readFileSync9(site.file, "utf8");
2576
2862
  const edit = replaceAiReadyOptions({
2577
2863
  source: before,
2578
2864
  file: site.file,
@@ -2590,8 +2876,110 @@ function applyReplace(site, config, entries, usesZod, relFile) {
2590
2876
  }
2591
2877
  }
2592
2878
 
2879
+ // bin/test-sign-in.ts
2880
+ import { readFileSync as readFileSync10 } from "fs";
2881
+ import { join as join10 } from "path";
2882
+ import { parseEnv } from "util";
2883
+ import { supabaseCookieName, supabasePasswordLogin, supabaseSessionCookies } from "@exposify/core";
2884
+ var TEST_SIGN_IN_USAGE = `Usage: @exposify/next test-sign-in [options]
2885
+
2886
+ Signs in to your app's Supabase Auth as AI assistants will (a password, then
2887
+ the authenticator code if you have one), and checks that a page your app
2888
+ protects accepts the session cookie Connect sends. Run it with your app up.
2889
+
2890
+ Options:
2891
+ --url <origin> Where the app runs (default: http://localhost:3000)
2892
+ --page <path> A page that redirects signed-out visitors (default: found)
2893
+ --entry <path> The Next.js project directory (default: detected)
2894
+
2895
+ Reads NEXT_PUBLIC_SUPABASE_URL and the publishable (or anon) key from the
2896
+ environment, .env.local or .env. Signs the test session out afterwards.`;
2897
+ function supabaseEnv(root, env) {
2898
+ const files = [".env.local", ".env"].map((f) => {
2899
+ try {
2900
+ return parseEnv(readFileSync10(join10(root, f), "utf8"));
2901
+ } catch {
2902
+ return {};
2903
+ }
2904
+ });
2905
+ const read2 = (name) => env[name] || files.find((f) => f[name])?.[name];
2906
+ const url = read2("NEXT_PUBLIC_SUPABASE_URL");
2907
+ const key = read2("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY") ?? read2("NEXT_PUBLIC_SUPABASE_ANON_KEY") ?? read2("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY");
2908
+ return url && key ? { url, key } : null;
2909
+ }
2910
+ var redirected = (res) => res.status >= 300 && res.status < 400 || res.status === 401 || res.status === 403;
2911
+ async function testSignIn(root, opts, deps) {
2912
+ const d = discover(root);
2913
+ if (!d.supabase || d.supabase.found.authHelpers) {
2914
+ deps.log(d.supabase ? "Your app uses @supabase/auth-helpers-nextjs, whose cookie format isn't supported: move to @supabase/ssr." : "This app doesn't use Supabase Auth with @supabase/ssr, so there's nothing to test here.");
2915
+ return 1;
2916
+ }
2917
+ const project = supabaseEnv(root, opts.env);
2918
+ if (!project) {
2919
+ deps.log("Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (or _ANON_KEY), in your shell or .env.local.");
2920
+ return 1;
2921
+ }
2922
+ const origin = opts.url.replace(/\/+$/, "");
2923
+ const get = (path, cookie) => deps.fetch(`${origin}${path}`, { headers: cookie ? { cookie } : {}, redirect: "manual" }).catch(() => null);
2924
+ let page = opts.page;
2925
+ if (!page) {
2926
+ const skip = new Set(d.supabase.choice.loginPages ?? []);
2927
+ const candidates2 = d.pages.filter((p) => p.params.length === 0 && !skip.has(p.path) && !p.path.startsWith(d.mcpPath ?? "/mcp")).slice(0, 15);
2928
+ for (const p of candidates2) {
2929
+ const res2 = await get(p.path);
2930
+ if (res2 === null) {
2931
+ deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
2932
+ return 1;
2933
+ }
2934
+ if (redirected(res2)) {
2935
+ page = p.path;
2936
+ break;
2937
+ }
2938
+ }
2939
+ if (!page) {
2940
+ deps.log("No page turned a signed-out request away, so there's nothing to check the session against. Pass --page with one that does.");
2941
+ return 1;
2942
+ }
2943
+ } else {
2944
+ const res2 = await get(page);
2945
+ if (res2 === null) {
2946
+ deps.log(`Couldn't reach ${origin}. Start your app (next dev), or pass --url.`);
2947
+ return 1;
2948
+ }
2949
+ if (!redirected(res2)) {
2950
+ deps.log(`${page} answered ${res2.status} without signing in, so it can't tell whether the session works. Pass --page with a page that redirects signed-out visitors.`);
2951
+ return 1;
2952
+ }
2953
+ }
2954
+ const login = supabasePasswordLogin({ ...project, fetch: deps.fetch });
2955
+ const ctx = { origin };
2956
+ const email = (await deps.ask("Email: ")).trim();
2957
+ const password = await deps.askSecret("Password: ");
2958
+ let result = await login.attempt({ email, password }, ctx);
2959
+ if (result.ok === "next") {
2960
+ const code = await deps.ask(`${result.message ?? "Code"} `);
2961
+ result = await login.verify.attempt({ code }, { ...ctx, carry: result.carry });
2962
+ }
2963
+ if (result.ok !== true || !("supabase" in result.session)) {
2964
+ deps.log(`Supabase didn't sign you in: ${result.ok === false ? result.message ?? "no reason given" : "no session came back"}`);
2965
+ return 1;
2966
+ }
2967
+ const session = result.session.supabase;
2968
+ const name = d.supabase.choice.cookieName ?? supabaseCookieName(project.url);
2969
+ const cookies = supabaseSessionCookies(name, session);
2970
+ const res = await get(page, cookies.map((c) => `${c.name}=${c.value}`).join("; "));
2971
+ await deps.fetch(`${project.url.replace(/\/+$/, "")}/auth/v1/logout?scope=local`, { method: "POST", headers: { apikey: project.key, authorization: `Bearer ${session.accessToken}` } }).catch(() => null);
2972
+ if (res && res.status >= 200 && res.status < 300) {
2973
+ deps.log(`\u2713 ${page} answered ${res.status} with the session cookie Connect sends (${cookies.length === 1 ? name : `${name}.0\u2013.${cookies.length - 1}`}), and turned you away without it.`);
2974
+ return 0;
2975
+ }
2976
+ deps.log(`\u2717 ${page} answered ${res?.status ?? "nothing"} with the session cookie${res?.headers.get("location") ? ` (to ${res.headers.get("location")})` : ""}: your app didn't accept it.`);
2977
+ if (d.supabase.choice.cookieName === void 0) deps.log(` It was named ${name}. If your app sets another cookie name, the generator didn't see it.`);
2978
+ return 1;
2979
+ }
2980
+
2593
2981
  // bin/exposify-next.ts
2594
- var GENERATE_USAGE = `Usage: exposify-next generate [options]
2982
+ var GENERATE_USAGE = `Usage: @exposify/next generate [options]
2595
2983
 
2596
2984
  Writes exposify.generated.ts (the route table and docs the MCP route uses).
2597
2985
  withExposify() in next.config runs this on \`next dev\` and \`next build\`.
@@ -2638,11 +3026,57 @@ ${GENERATE_USAGE}`);
2638
3026
  console.log(
2639
3027
  `${result.changed ? "Wrote" : "Unchanged:"} ${relative4(cwd, result.file) || result.file}: ${app} App Router route file${app === 1 ? "" : "s"}, ${routes.length - app} Pages Router API file${routes.length - app === 1 ? "" : "s"}.`
2640
3028
  );
2641
- if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx exposify-next setup`.");
3029
+ if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx @exposify/next setup`.");
2642
3030
  return 0;
2643
3031
  }
3032
+ function prompt(question, secret = false) {
3033
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
3034
+ if (secret) {
3035
+ const write = rl._writeToOutput.bind(rl);
3036
+ rl._writeToOutput = (s) => write(s.startsWith(question) ? s : s.replace(/[^\r\n]/g, ""));
3037
+ }
3038
+ return new Promise(
3039
+ (resolve3) => rl.question(question, (answer) => {
3040
+ rl.close();
3041
+ if (secret) process.stdout.write("\n");
3042
+ resolve3(answer);
3043
+ })
3044
+ );
3045
+ }
3046
+ async function runTestSignIn(argv2, cwd) {
3047
+ let entry;
3048
+ let url = "http://localhost:3000";
3049
+ let page;
3050
+ for (let i = 0; i < argv2.length; i++) {
3051
+ const flag = argv2[i];
3052
+ const value = () => {
3053
+ const v = argv2[++i];
3054
+ if (v === void 0) throw new Error(`${flag} needs a value`);
3055
+ return v;
3056
+ };
3057
+ if (flag === "--entry") entry = value();
3058
+ else if (flag === "--url") url = value();
3059
+ else if (flag === "--page") page = value();
3060
+ else if (flag === "--help" || flag === "-h") {
3061
+ console.log(TEST_SIGN_IN_USAGE);
3062
+ return 0;
3063
+ } else {
3064
+ console.error(`Unknown option: ${flag}
3065
+
3066
+ ${TEST_SIGN_IN_USAGE}`);
3067
+ return 1;
3068
+ }
3069
+ }
3070
+ const root = findNextRoot(entry ? `${cwd}/${entry}` : cwd);
3071
+ if (!root) {
3072
+ console.error("Couldn't find a Next.js project (a package.json with next, and an app/ or pages/ directory).");
3073
+ return 1;
3074
+ }
3075
+ return testSignIn(root, { url, ...page ? { page } : {}, env: process.env }, { ask: (q) => prompt(q), askSecret: (q) => prompt(q, true), log: (l) => console.log(l), fetch });
3076
+ }
2644
3077
  var argv = process.argv.slice(2);
2645
- (argv[0] === "generate" ? Promise.resolve().then(() => runGenerate(argv.slice(1), process.cwd())) : runCli(argv, nextAdapter)).then(
3078
+ var run = argv[0] === "generate" ? Promise.resolve().then(() => runGenerate(argv.slice(1), process.cwd())) : argv[0] === "test-sign-in" ? runTestSignIn(argv.slice(1), process.cwd()) : runCli(argv, nextAdapter);
3079
+ run.then(
2646
3080
  (code) => process.exit(code),
2647
3081
  (err) => {
2648
3082
  console.error(err instanceof Error ? err.message : err);