@exposify/next 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,10 +20,18 @@ import {
20
20
  handleLoginPage,
21
21
  hostedResolverInfo,
22
22
  loginResultFromResponse,
23
+ keepFailedCookies,
24
+ readStep,
25
+ rejectedBody,
26
+ readSupabaseAuth,
23
27
  reportAppLogin,
24
28
  samePath,
29
+ sessionFrom,
25
30
  setCookieLines,
26
31
  SIGNED_OUT_HEADER,
32
+ supabaseCookieName,
33
+ supabasePasswordLogin,
34
+ verifyFields,
27
35
  matchesPathGlob,
28
36
  formatFailed,
29
37
  formatReady,
@@ -704,6 +712,7 @@ function errorMessage(err) {
704
712
  }
705
713
  async function routesFrom(manifest, cfg, warnings) {
706
714
  const listed = Array.isArray(cfg.expose) ? cfg.expose.map(parseMethodEntry).filter((e) => e !== null) : [];
715
+ const callbacks = new Set(manifest.supabase?.callbacks ?? []);
707
716
  const perEntry = await Promise.all(
708
717
  manifest.routes.map(async (entry) => {
709
718
  const base = { path: entry.path, params: entry.params };
@@ -721,7 +730,7 @@ async function routesFrom(manifest, cfg, warnings) {
721
730
  const methods = mod ? HTTP_METHODS.filter((m) => typeof mod[m] === "function") : entry.methods;
722
731
  return methods.filter((m) => m !== "OPTIONS").map((method) => {
723
732
  const marker = mod ? getMarker(mod[method]) : void 0;
724
- return { ...base, method, ...marker ? { marker } : {} };
733
+ return { ...base, method, ...marker ? { marker } : {}, ...callbacks.has(entry.file) ? { optIn: true } : {} };
725
734
  });
726
735
  })
727
736
  );
@@ -879,29 +888,65 @@ function pagesFrom(manifest, cfg, config) {
879
888
  }
880
889
  return { routes, hidden: hidden3, forms };
881
890
  }
882
- function loginSpec(manifest, config, warnings) {
891
+ function loginSpec(manifest, options, warnings) {
892
+ const config = options.login;
883
893
  if (config === false) return null;
884
- if (config === void 0) return manifest.login ?? null;
894
+ let login;
895
+ if (config === void 0) login = manifest.login ?? null;
896
+ else {
897
+ const step = stepSpec(manifest, config, (name) => fieldKind(name), warnings, "login", "AI assistants can't sign in with your app's login");
898
+ login = step ? { ...step.step, where: step.where } : null;
899
+ }
900
+ if (!login || options.loginVerify === void 0) return login;
901
+ const { verify: _found, ...first } = login;
902
+ if (options.loginVerify === false) return first;
903
+ const kind = (name) => verifyFields([name], true)?.[0]?.kind ?? fieldKind(name);
904
+ const verify = stepSpec(manifest, options.loginVerify, kind, warnings, "loginVerify", "AI assistants can't finish your app's two-step login");
905
+ return verify ? { ...first, verify: verify.step, where: `${first.where ?? "your login"}, then a code: ${verify.where}` } : first;
906
+ }
907
+ function supabaseSpec(manifest, options, warnings) {
908
+ if (!manifest.supabase || options.supabase === false) return null;
909
+ if (options.supabase) return options.supabase;
910
+ const e = env3();
911
+ const url = e.NEXT_PUBLIC_SUPABASE_URL ?? e.SUPABASE_URL;
912
+ const key = e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? e.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY;
913
+ if (!url || !key) {
914
+ warnings.push("your app uses Supabase Auth, but NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY (or _ANON_KEY) aren't both set where the MCP route runs, so AI assistants can't sign in to it; set them, or pass aiReady({ supabase: { url, key } })");
915
+ return null;
916
+ }
917
+ try {
918
+ new URL(url);
919
+ } catch {
920
+ warnings.push(`NEXT_PUBLIC_SUPABASE_URL isn't a URL (${url}), so AI assistants can't sign in to your Supabase Auth`);
921
+ return null;
922
+ }
923
+ return { url, key };
924
+ }
925
+ function stepSpec(manifest, config, kind, warnings, option, consequence) {
885
926
  if (typeof config === "object") {
886
- const fields2 = config.fields.map((name) => ({ name, kind: fieldKind(name) }));
887
- return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json", where: `POST ${config.path}` };
927
+ const fields2 = config.fields.map((name) => ({ name, kind: kind(name) }));
928
+ return { step: { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json" }, where: `POST ${config.path}` };
888
929
  }
889
930
  const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
890
931
  if (!action) {
891
- warnings.push(`aiReady({ login: "${config}" }): no server action by that name, so AI assistants can't sign in with your app's login`);
932
+ warnings.push(`aiReady({ ${option}: "${config}" }): no server action by that name, so ${consequence}`);
892
933
  return null;
893
934
  }
894
- const fields = action.fields.map((f) => ({ name: f.name, kind: fieldKind(f.name) }));
895
- return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {}, where: `${action.export} in ${action.file}` };
935
+ const fields = action.fields.map((f) => ({ name: f.name, kind: kind(f.name) }));
936
+ return { step: { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {} }, where: `${action.export} in ${action.file}` };
896
937
  }
897
- async function actionLoginResult(raw, exportName) {
938
+ async function actionLoginResult(raw, exportName, reading = {}) {
898
939
  if (actionNotFound(raw)) return { ok: false, message: "The app's sign-in page didn't run its login. Its developer needs to rebuild the app." };
899
- const cookies = cookieChanges(raw).filter((c) => !c.deleted && c.value !== "");
940
+ const changes = cookieChanges(raw);
900
941
  const out = await readActionResponse(raw, exportName);
901
942
  const body = await out.json().catch(() => void 0);
943
+ const location = raw.headers.get("x-action-redirect")?.split(";")[0] ?? (typeof body?.redirectedTo === "string" ? body.redirectedTo : null);
944
+ const step = readStep({ status: location ? 303 : out.status, location, body, cookies: changes }, reading);
945
+ if (step) return keepFailedCookies(step, changes, reading);
902
946
  const message = errorMessageOf(body);
903
- if (out.status >= 400 || message && !body?.redirectedTo) return { ok: false, message };
904
- return cookies.length > 0 ? { ok: true, session: { cookies } } : { ok: false, message };
947
+ if (out.status >= 400 || message && !body?.redirectedTo || rejectedBody(body)) return keepFailedCookies({ ok: false, message }, changes, reading);
948
+ const session = sessionFrom(changes, reading, Boolean(location) || body !== void 0 && body !== null && typeof body === "object");
949
+ return session ? { ok: true, session } : keepFailedCookies({ ok: false, message }, changes, reading);
905
950
  }
906
951
  var clientIp = (headers) => headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || void 0;
907
952
  function aiReady(manifest, options = {}) {
@@ -914,9 +959,39 @@ function aiReady(manifest, options = {}) {
914
959
  const actionsBase = (manifest.mcpPath ?? "").replace(/\/$/, "");
915
960
  const actionUrl = manifest.mcpPath === void 0 ? null : new RegExp(`${actionsBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/actions/([^/]+)/?$`);
916
961
  const loginWarnings = [];
917
- const login = hostedResolverInfo(cfg.auth) ? loginSpec(manifest, options.login, loginWarnings) : null;
962
+ const login = hostedResolverInfo(cfg.auth) ? loginSpec(manifest, options, loginWarnings) : null;
918
963
  const loginPagePath = `${(manifest.mcpPath ?? "/mcp").replace(/\/$/, "")}/login`;
919
964
  if (login) cfg.signedOutPaths = [login.kind === "action" ? login.host : login.path].filter((p) => Boolean(p)).map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`);
965
+ const supabase = hostedResolverInfo(cfg.auth) ? supabaseSpec(manifest, options, loginWarnings) : null;
966
+ if (supabase && manifest.supabase?.loginPages?.length) {
967
+ cfg.signedOutPaths = [...cfg.signedOutPaths ?? [], ...manifest.supabase.loginPages.map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`)];
968
+ }
969
+ const supabasePassword = supabase ? supabasePasswordLogin({
970
+ ...supabase,
971
+ onCaptcha: () => warnOnce("your Supabase project asks for a CAPTCHA to sign in, so AI assistants can't sign in with a password; social sign-ins still work")
972
+ }) : null;
973
+ const supabaseLoginNow = () => {
974
+ if (!supabasePassword) return null;
975
+ const choice = hostedResolverInfo(cfg.auth)?.supabasePasswordDirect();
976
+ return choice ?? (!login && manifest.supabase?.direct.includes("password")) ? supabasePassword : null;
977
+ };
978
+ let supabaseAuth;
979
+ const supabaseReport = () => {
980
+ if (!supabase || !manifest.supabase) return Promise.resolve(void 0);
981
+ const found = manifest.supabase;
982
+ supabaseAuth ??= readSupabaseAuth(supabase.url, supabase.key).then((read) => {
983
+ if (!read) {
984
+ supabaseAuth = void 0;
985
+ return void 0;
986
+ }
987
+ return {
988
+ ...read,
989
+ cookie: found.cookieName ?? supabaseCookieName(supabase.url),
990
+ found: { direct: found.direct, where: found.found, ...found.unusable ? { unusable: found.unusable } : {}, ...found.callbacks ? { callbacks: found.callbacks } : {} }
991
+ };
992
+ });
993
+ return supabaseAuth;
994
+ };
920
995
  let built = null;
921
996
  const build = async (endpoint) => {
922
997
  const warnings = [...loginWarnings];
@@ -1020,7 +1095,12 @@ function aiReady(manifest, options = {}) {
1020
1095
  if (cfg.expose === "*") details.push('all write routes exposed (expose: "*")');
1021
1096
  if (counts.hiddenWrites > 0) details.push(`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`);
1022
1097
  if (pages.routes.length > 0) details.push(`${pages.routes.length} page${pages.routes.length === 1 ? "" : "s"} readable`);
1023
- if (login) details.push(`AI assistants sign in with your app's own login (${login.kind === "action" ? `server action ${login.key}` : `POST ${login.path}`}) at ${loginPagePath}`);
1098
+ if (supabaseLoginNow()) {
1099
+ details.push(`AI assistants sign in to your Supabase Auth with a password at ${loginPagePath}`);
1100
+ } else if (login) {
1101
+ const step = (l) => l.kind === "action" ? `server action ${l.key}` : `POST ${l.path}`;
1102
+ details.push(`AI assistants sign in with your app's own login (${step(login)}${login.verify ? `, then a code: ${step(login.verify)}` : ""}) at ${loginPagePath}`);
1103
+ }
1024
1104
  if (pages.forms > 0) details.push(`${pages.forms} page${pages.forms === 1 ? "" : "s"} left out as just a form (list in aiReady({ pages }) to include)`);
1025
1105
  if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
1026
1106
  if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
@@ -1092,34 +1172,34 @@ function aiReady(manifest, options = {}) {
1092
1172
  const reply = encodeReply(actionArguments(live.action.params, fields, jsonFieldsOf(live.action)));
1093
1173
  return fetch(new URL(path, origin), { method: "POST", headers: out, body: reply, redirect: "manual", cache: "no-store" });
1094
1174
  };
1095
- const appLoginFor = (req, spec) => ({
1096
- label: spec.kind === "action" ? `server action ${spec.key}` : `POST ${spec.path}`,
1097
- fields: spec.fields,
1098
- signedOutPaths: cfg.signedOutPaths ?? [],
1099
- attempt: async (values, ctx) => {
1175
+ const appLoginFor = (req, spec) => {
1176
+ const pathOf = (p) => {
1177
+ let path = `${manifest.basePath ?? ""}${p}`;
1178
+ if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
1179
+ return path;
1180
+ };
1181
+ const stepPath = (step) => step.kind === "action" ? step.host ? pathOf(hostPath(step.host)) : void 0 : pathOf(step.path);
1182
+ const verifyPaths = spec.verify ? [stepPath(spec.verify)].filter((p) => Boolean(p)) : void 0;
1183
+ const send = async (step, values, ctx, reading, cookies = []) => {
1100
1184
  const target = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
1101
1185
  const pub = new URL(ctx.origin);
1102
1186
  const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
1103
1187
  if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
1104
1188
  const ua = req.headers.get("user-agent");
1105
1189
  if (ua) headers.set("user-agent", ua);
1190
+ if (cookies.length) headers.set("cookie", cookies.map((c) => `${c.name}=${c.value}`).join("; "));
1106
1191
  const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
1107
1192
  if (bypass) headers.set("x-vercel-protection-bypass", bypass);
1108
- const pathOf = (p) => {
1109
- let path = `${manifest.basePath ?? ""}${p}`;
1110
- if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
1111
- return path;
1112
- };
1113
- if (spec.kind === "route") {
1114
- headers.set("content-type", spec.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
1115
- const body = spec.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
1116
- return loginResultFromResponse(await fetch(new URL(pathOf(spec.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }));
1193
+ if (step.kind === "route") {
1194
+ headers.set("content-type", step.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
1195
+ const body = step.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
1196
+ return loginResultFromResponse(await fetch(new URL(pathOf(step.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }), reading);
1117
1197
  }
1118
- const action = (manifest.actions ?? []).find((a) => a.key === spec.key);
1198
+ const action = (manifest.actions ?? []).find((a) => a.key === step.key);
1119
1199
  const fn = action ? (await action.load())[action.export] : void 0;
1120
1200
  const id = fn?.$$id;
1121
1201
  if (!action?.host || typeof id !== "string") {
1122
- warnOnce(`server action ${spec.key} is your app's login, but no page uses it, so the sign-in form can't run it; use it from a page's form`);
1202
+ warnOnce(`server action ${step.key} is part of your app's login, but no page uses it, so the sign-in form can't run it; use it from a page's form`);
1123
1203
  return { ok: false, message: "This app's sign-in can't be used by AI assistants yet." };
1124
1204
  }
1125
1205
  headers.set("next-action", id);
@@ -1127,9 +1207,28 @@ function aiReady(manifest, options = {}) {
1127
1207
  headers.set("accept", "text/x-component");
1128
1208
  const reply = encodeReply(actionArguments(action.params, values, jsonFieldsOf(action)));
1129
1209
  const raw = await fetch(new URL(pathOf(hostPath(action.host)), target), { method: "POST", headers, body: reply, redirect: "manual", cache: "no-store" });
1130
- return actionLoginResult(raw, action.export);
1131
- }
1132
- });
1210
+ return actionLoginResult(raw, action.export, reading);
1211
+ };
1212
+ const verify = spec.verify;
1213
+ return {
1214
+ label: spec.kind === "action" ? `server action ${spec.key}` : `POST ${spec.path}`,
1215
+ fields: spec.fields,
1216
+ signedOutPaths: cfg.signedOutPaths ?? [],
1217
+ ...spec.passwordless ? { passwordless: true } : {},
1218
+ attempt: (values, ctx) => send(spec, values, ctx, {
1219
+ ...verifyPaths ? { verifyPaths } : {},
1220
+ ...verify ? { verifyFields: verify.fields.map((f) => f.name) } : {},
1221
+ ...spec.passwordless ? { start: true } : {}
1222
+ }),
1223
+ ...verify ? {
1224
+ verify: {
1225
+ label: verify.kind === "action" ? `server action ${verify.key}` : `POST ${verify.path}`,
1226
+ fields: verify.fields,
1227
+ attempt: (values, ctx) => send(verify, values, ctx, { carried: ctx.carry.cookies, verifyPaths: verifyPaths ?? [] }, ctx.carry.cookies)
1228
+ }
1229
+ } : {}
1230
+ };
1231
+ };
1133
1232
  const carrySession = (raw, out) => {
1134
1233
  const lines = setCookieLines(raw);
1135
1234
  const redirect = raw.headers.get("x-action-redirect")?.split(";")[0];
@@ -1174,9 +1273,10 @@ function aiReady(manifest, options = {}) {
1174
1273
  };
1175
1274
  return async (req) => {
1176
1275
  const pathname = new URL(req.url).pathname;
1177
- if (login && samePath(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
1276
+ const supabaseLogin = supabaseLoginNow();
1277
+ if ((login || supabaseLogin) && samePath(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
1178
1278
  const pub = new URL(publicRequestUrl(req.url, req.headers));
1179
- return handleLoginPage(req, appLoginFor(req, login), hostedResolverInfo(cfg.auth), {
1279
+ return handleLoginPage(req, supabaseLogin ?? appLoginFor(req, login), hostedResolverInfo(cfg.auth), {
1180
1280
  path: pub.pathname,
1181
1281
  secure: pub.protocol === "https:",
1182
1282
  origin: pub.origin,
@@ -1203,7 +1303,13 @@ function aiReady(manifest, options = {}) {
1203
1303
  const url = publicRequestUrl(req.url, req.headers);
1204
1304
  const isPrm = url.endsWith(PRM_SUFFIX);
1205
1305
  const resource = brokerGate.resourceFor(isPrm ? url.slice(0, -PRM_SUFFIX.length) : url);
1206
- const verdict = await reportAppLogin(cfg.auth, resource, login, { where: login?.where, stacks: manifest.authStacks, log: logReady });
1306
+ const supabaseAuth2 = await supabaseReport();
1307
+ const verdict = await reportAppLogin(cfg.auth, resource, supabaseLogin ?? login, {
1308
+ where: supabaseLogin ? "your Supabase Auth (password)" : login?.where,
1309
+ stacks: manifest.authStacks,
1310
+ log: logReady,
1311
+ ...supabaseAuth2 ? { supabase: supabaseAuth2 } : {}
1312
+ });
1207
1313
  if (verdict === "open") {
1208
1314
  if (isPrm) return new Response("not found", { status: 404 });
1209
1315
  } else {
@@ -0,0 +1,176 @@
1
+ import { ProposedField, LoginField, DocSource } from '@exposify/core';
2
+
3
+ type FieldType = "string" | "number" | "boolean" | "json" | "file";
4
+ interface ActionField {
5
+ name: string;
6
+ type: FieldType;
7
+ /** Read with getAll(): a repeated field, sent as an array. */
8
+ array?: true;
9
+ /** Its allowed values, from `LIST.includes(x)` over a constant list. */
10
+ enum?: string[];
11
+ /** Always needed: a parameter of its own (a `.bind` argument), a non-optional key of an object parameter, or required by every form that submits to it. */
12
+ required?: true;
13
+ /** From the form's input (pass 3, forms.ts). */
14
+ min?: number;
15
+ max?: number;
16
+ minLength?: number;
17
+ maxLength?: number;
18
+ pattern?: string;
19
+ /** Where it was found: the action's code, its form, or both. */
20
+ origin?: "action" | "form" | "both";
21
+ /** For the agent: format, units, what it refers to (from an accepted proposal). */
22
+ description?: string;
23
+ }
24
+
25
+ interface AppRouteEntry {
26
+ router: "app";
27
+ /** `/api/orders/:id`; catch-alls as `:slug*`, optional ones as `:slug*?`. */
28
+ path: string;
29
+ params: string[];
30
+ /** Project-relative, for messages. */
31
+ file: string;
32
+ /** Exported methods when generated; the loaded module's exports win at runtime. */
33
+ methods: string[];
34
+ /** Loads the route module to read exposed()/hidden()/describe() off its exports. Absent for edge routes. */
35
+ load?: () => Promise<Record<string, unknown>>;
36
+ /** `export const runtime = "edge"`: not loaded into the Node MCP route, so markers there aren't read. */
37
+ edge?: boolean;
38
+ }
39
+ interface PagesRouteEntry {
40
+ router: "pages";
41
+ path: string;
42
+ params: string[];
43
+ file: string;
44
+ }
45
+ type ManifestRoute = AppRouteEntry | PagesRouteEntry;
46
+ /** An exported function of a `"use server"` module. */
47
+ interface ManifestAction {
48
+ /** Unique among actions: the default tool name, and the URL segment under `<mcpPath>/actions/`. */
49
+ key: string;
50
+ file: string;
51
+ export: string;
52
+ /** How each parameter is filled on a call, in order: see `ActionParamRole` in parse.ts. */
53
+ params: Array<{
54
+ name: string;
55
+ role: "formData" | "prev" | "input" | "object";
56
+ }>;
57
+ /** It sets or deletes a cookie, which would land on exposify's internal call rather than the agent's client. */
58
+ setsCookies?: boolean;
59
+ /** What it takes, worked out from its code (fields.ts): its tool schema when the app doesn't write one. */
60
+ fields: ActionField[];
61
+ /** It reads every form field, so its code doesn't say which exist. */
62
+ readsAll?: boolean;
63
+ /** What the analysis couldn't work out ("line N: …"): with any, the app writes the schema. */
64
+ unresolved?: string[];
65
+ /** The forms that submit to it (pass 3), where they are. */
66
+ forms?: Array<{
67
+ file: string;
68
+ line: number;
69
+ }>;
70
+ /** An input `describe` proposed and a person accepted, current with the code: used when `fields` can't be the schema. */
71
+ proposed?: ProposedField[];
72
+ /**
73
+ * A page that uses it (`/checkout`, `/admin/orders/:id`): tool calls are
74
+ * sent there as a real Server Action request. Absent when no page uses it;
75
+ * it's then called in-process by the MCP route.
76
+ */
77
+ host?: string;
78
+ /**
79
+ * The app's own checks it runs first (`lib/auth.ts#requireAdmin`): a
80
+ * user who doesn't pass one doesn't see the tool (guards.ts).
81
+ */
82
+ guards?: string[];
83
+ load: () => Promise<Record<string, unknown>>;
84
+ }
85
+ /** An App Router page: a read tool that fetches it like a browser (pages.ts). */
86
+ interface ManifestPage {
87
+ /** Tool name, e.g. `view_product_slug`. */
88
+ name: string;
89
+ /** `/product/:slug`, as routes are written. */
90
+ path: string;
91
+ params: string[];
92
+ file: string;
93
+ /** Search params its code reads: optional tool arguments, sent as the query. */
94
+ search?: string[];
95
+ /** From its static `metadata`. */
96
+ title?: string;
97
+ description?: string;
98
+ /** Just a form posting to a Server Action: not a tool unless `pages` lists it. */
99
+ formOnly?: boolean;
100
+ /** The app's own checks it runs (`lib/auth.ts#requireAdmin`), see ManifestAction.guards. */
101
+ guards?: string[];
102
+ }
103
+ /**
104
+ * The app's own login (login.ts): a sign-in form under the MCP route makes
105
+ * this request, for Connect with no code. `host` is the page with the
106
+ * action's form, which tool calls redirected to mean "signed out".
107
+ */
108
+ type ManifestLogin = ManifestLoginStep & {
109
+ /** Where it is, for the startup log and the dashboard: "signIn in app/actions/auth.ts". */
110
+ where?: string;
111
+ /** The first step takes no password: an email or phone number, then the code. */
112
+ passwordless?: boolean;
113
+ /** The login's second step, a code; `host` (or the route's path) is where the first step redirects to ask for it. */
114
+ verify?: ManifestLoginStep;
115
+ };
116
+ /** One request of the login. */
117
+ type ManifestLoginStep = {
118
+ kind: "action";
119
+ key: string;
120
+ fields: LoginField[];
121
+ host?: string;
122
+ } | {
123
+ kind: "route";
124
+ path: string;
125
+ fields: LoginField[];
126
+ body: "json" | "form";
127
+ };
128
+ /**
129
+ * A Next app on Supabase Auth with `@supabase/ssr` (supabase.ts): which
130
+ * sign-ins Connect makes on Supabase directly, decided at build time from
131
+ * where the app signs its users in (plan Part 2, decisions A and B).
132
+ */
133
+ interface ManifestSupabase {
134
+ /** The session cookie's name when the app sets one; else `sb-<ref>-auth-token`, from the project's URL. */
135
+ cookieName?: string;
136
+ /**
137
+ * Signed in to on Supabase directly: by password on the form at
138
+ * `<MCP path>/login` (the password goes from the app's server to Supabase),
139
+ * the rest on the broker's pages. A method the app signs in with on its
140
+ * server isn't here: the app's own login does it, or nothing does.
141
+ */
142
+ direct: Array<"password" | "email-code" | "social">;
143
+ /** Where the app signs in, as found: "signInWithPassword in components/login-form.tsx:42 (browser: "use client")". */
144
+ found: string[];
145
+ /** Server sign-ins the app's own login couldn't be made from, so that method has no sign-in until the owner switches to Supabase directly. */
146
+ unusable?: string[];
147
+ /** Routes that finish a sign-in (`exchangeCodeForSession`): their checks don't run for Connect's sign-ins. */
148
+ callbacks?: string[];
149
+ /** The app's sign-in pages (`/auth/login`): a tool call redirected there was signed out. */
150
+ loginPages?: string[];
151
+ }
152
+ interface ExposifyManifest {
153
+ info: {
154
+ name: string;
155
+ version: string;
156
+ };
157
+ /** next.config's basePath, prefixed to every tool call's path. */
158
+ basePath?: string;
159
+ /** next.config's trailingSlash: tool calls add one, so they aren't redirected. */
160
+ trailingSlash?: boolean;
161
+ routes: ManifestRoute[];
162
+ /** The MCP route's URL without its catch-all segment, e.g. `/mcp`: Server Actions are served under `<mcpPath>/actions/`. */
163
+ mcpPath?: string;
164
+ actions?: ManifestAction[];
165
+ pages?: ManifestPage[];
166
+ login?: ManifestLogin;
167
+ supabase?: ManifestSupabase;
168
+ /** Auth libraries in package.json ("next-auth", "@supabase/ssr", ...), reported with the login. */
169
+ authStacks?: string[];
170
+ /** Docs for search_docs, embedded so they work without a file system. */
171
+ docs: DocSource[];
172
+ /** exposify.descriptions.json's texts, "METHOD /path" -> text. */
173
+ descriptions?: Record<string, string>;
174
+ }
175
+
176
+ export type { ActionField as A, ExposifyManifest as E, ManifestSupabase as M, PagesRouteEntry as P, AppRouteEntry as a, ManifestAction as b, ManifestLogin as c, ManifestLoginStep as d, ManifestPage as e, ManifestRoute as f };
@@ -0,0 +1,176 @@
1
+ import { ProposedField, LoginField, DocSource } from '@exposify/core';
2
+
3
+ type FieldType = "string" | "number" | "boolean" | "json" | "file";
4
+ interface ActionField {
5
+ name: string;
6
+ type: FieldType;
7
+ /** Read with getAll(): a repeated field, sent as an array. */
8
+ array?: true;
9
+ /** Its allowed values, from `LIST.includes(x)` over a constant list. */
10
+ enum?: string[];
11
+ /** Always needed: a parameter of its own (a `.bind` argument), a non-optional key of an object parameter, or required by every form that submits to it. */
12
+ required?: true;
13
+ /** From the form's input (pass 3, forms.ts). */
14
+ min?: number;
15
+ max?: number;
16
+ minLength?: number;
17
+ maxLength?: number;
18
+ pattern?: string;
19
+ /** Where it was found: the action's code, its form, or both. */
20
+ origin?: "action" | "form" | "both";
21
+ /** For the agent: format, units, what it refers to (from an accepted proposal). */
22
+ description?: string;
23
+ }
24
+
25
+ interface AppRouteEntry {
26
+ router: "app";
27
+ /** `/api/orders/:id`; catch-alls as `:slug*`, optional ones as `:slug*?`. */
28
+ path: string;
29
+ params: string[];
30
+ /** Project-relative, for messages. */
31
+ file: string;
32
+ /** Exported methods when generated; the loaded module's exports win at runtime. */
33
+ methods: string[];
34
+ /** Loads the route module to read exposed()/hidden()/describe() off its exports. Absent for edge routes. */
35
+ load?: () => Promise<Record<string, unknown>>;
36
+ /** `export const runtime = "edge"`: not loaded into the Node MCP route, so markers there aren't read. */
37
+ edge?: boolean;
38
+ }
39
+ interface PagesRouteEntry {
40
+ router: "pages";
41
+ path: string;
42
+ params: string[];
43
+ file: string;
44
+ }
45
+ type ManifestRoute = AppRouteEntry | PagesRouteEntry;
46
+ /** An exported function of a `"use server"` module. */
47
+ interface ManifestAction {
48
+ /** Unique among actions: the default tool name, and the URL segment under `<mcpPath>/actions/`. */
49
+ key: string;
50
+ file: string;
51
+ export: string;
52
+ /** How each parameter is filled on a call, in order: see `ActionParamRole` in parse.ts. */
53
+ params: Array<{
54
+ name: string;
55
+ role: "formData" | "prev" | "input" | "object";
56
+ }>;
57
+ /** It sets or deletes a cookie, which would land on exposify's internal call rather than the agent's client. */
58
+ setsCookies?: boolean;
59
+ /** What it takes, worked out from its code (fields.ts): its tool schema when the app doesn't write one. */
60
+ fields: ActionField[];
61
+ /** It reads every form field, so its code doesn't say which exist. */
62
+ readsAll?: boolean;
63
+ /** What the analysis couldn't work out ("line N: …"): with any, the app writes the schema. */
64
+ unresolved?: string[];
65
+ /** The forms that submit to it (pass 3), where they are. */
66
+ forms?: Array<{
67
+ file: string;
68
+ line: number;
69
+ }>;
70
+ /** An input `describe` proposed and a person accepted, current with the code: used when `fields` can't be the schema. */
71
+ proposed?: ProposedField[];
72
+ /**
73
+ * A page that uses it (`/checkout`, `/admin/orders/:id`): tool calls are
74
+ * sent there as a real Server Action request. Absent when no page uses it;
75
+ * it's then called in-process by the MCP route.
76
+ */
77
+ host?: string;
78
+ /**
79
+ * The app's own checks it runs first (`lib/auth.ts#requireAdmin`): a
80
+ * user who doesn't pass one doesn't see the tool (guards.ts).
81
+ */
82
+ guards?: string[];
83
+ load: () => Promise<Record<string, unknown>>;
84
+ }
85
+ /** An App Router page: a read tool that fetches it like a browser (pages.ts). */
86
+ interface ManifestPage {
87
+ /** Tool name, e.g. `view_product_slug`. */
88
+ name: string;
89
+ /** `/product/:slug`, as routes are written. */
90
+ path: string;
91
+ params: string[];
92
+ file: string;
93
+ /** Search params its code reads: optional tool arguments, sent as the query. */
94
+ search?: string[];
95
+ /** From its static `metadata`. */
96
+ title?: string;
97
+ description?: string;
98
+ /** Just a form posting to a Server Action: not a tool unless `pages` lists it. */
99
+ formOnly?: boolean;
100
+ /** The app's own checks it runs (`lib/auth.ts#requireAdmin`), see ManifestAction.guards. */
101
+ guards?: string[];
102
+ }
103
+ /**
104
+ * The app's own login (login.ts): a sign-in form under the MCP route makes
105
+ * this request, for Connect with no code. `host` is the page with the
106
+ * action's form, which tool calls redirected to mean "signed out".
107
+ */
108
+ type ManifestLogin = ManifestLoginStep & {
109
+ /** Where it is, for the startup log and the dashboard: "signIn in app/actions/auth.ts". */
110
+ where?: string;
111
+ /** The first step takes no password: an email or phone number, then the code. */
112
+ passwordless?: boolean;
113
+ /** The login's second step, a code; `host` (or the route's path) is where the first step redirects to ask for it. */
114
+ verify?: ManifestLoginStep;
115
+ };
116
+ /** One request of the login. */
117
+ type ManifestLoginStep = {
118
+ kind: "action";
119
+ key: string;
120
+ fields: LoginField[];
121
+ host?: string;
122
+ } | {
123
+ kind: "route";
124
+ path: string;
125
+ fields: LoginField[];
126
+ body: "json" | "form";
127
+ };
128
+ /**
129
+ * A Next app on Supabase Auth with `@supabase/ssr` (supabase.ts): which
130
+ * sign-ins Connect makes on Supabase directly, decided at build time from
131
+ * where the app signs its users in (plan Part 2, decisions A and B).
132
+ */
133
+ interface ManifestSupabase {
134
+ /** The session cookie's name when the app sets one; else `sb-<ref>-auth-token`, from the project's URL. */
135
+ cookieName?: string;
136
+ /**
137
+ * Signed in to on Supabase directly: by password on the form at
138
+ * `<MCP path>/login` (the password goes from the app's server to Supabase),
139
+ * the rest on the broker's pages. A method the app signs in with on its
140
+ * server isn't here: the app's own login does it, or nothing does.
141
+ */
142
+ direct: Array<"password" | "email-code" | "social">;
143
+ /** Where the app signs in, as found: "signInWithPassword in components/login-form.tsx:42 (browser: "use client")". */
144
+ found: string[];
145
+ /** Server sign-ins the app's own login couldn't be made from, so that method has no sign-in until the owner switches to Supabase directly. */
146
+ unusable?: string[];
147
+ /** Routes that finish a sign-in (`exchangeCodeForSession`): their checks don't run for Connect's sign-ins. */
148
+ callbacks?: string[];
149
+ /** The app's sign-in pages (`/auth/login`): a tool call redirected there was signed out. */
150
+ loginPages?: string[];
151
+ }
152
+ interface ExposifyManifest {
153
+ info: {
154
+ name: string;
155
+ version: string;
156
+ };
157
+ /** next.config's basePath, prefixed to every tool call's path. */
158
+ basePath?: string;
159
+ /** next.config's trailingSlash: tool calls add one, so they aren't redirected. */
160
+ trailingSlash?: boolean;
161
+ routes: ManifestRoute[];
162
+ /** The MCP route's URL without its catch-all segment, e.g. `/mcp`: Server Actions are served under `<mcpPath>/actions/`. */
163
+ mcpPath?: string;
164
+ actions?: ManifestAction[];
165
+ pages?: ManifestPage[];
166
+ login?: ManifestLogin;
167
+ supabase?: ManifestSupabase;
168
+ /** Auth libraries in package.json ("next-auth", "@supabase/ssr", ...), reported with the login. */
169
+ authStacks?: string[];
170
+ /** Docs for search_docs, embedded so they work without a file system. */
171
+ docs: DocSource[];
172
+ /** exposify.descriptions.json's texts, "METHOD /path" -> text. */
173
+ descriptions?: Record<string, string>;
174
+ }
175
+
176
+ export type { ActionField as A, ExposifyManifest as E, ManifestSupabase as M, PagesRouteEntry as P, AppRouteEntry as a, ManifestAction as b, ManifestLogin as c, ManifestLoginStep as d, ManifestPage as e, ManifestRoute as f };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/next",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@babel/parser": "^8.0.0",
35
- "@exposify/cli": "0.0.2",
36
- "@exposify/core": "0.3.15",
35
+ "@exposify/cli": "0.0.4",
36
+ "@exposify/core": "0.3.16",
37
37
  "tinyglobby": "^0.2.10",
38
38
  "zod": "^3.24.0"
39
39
  },