@exposify/next 0.0.3 → 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.
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,
@@ -879,29 +887,65 @@ function pagesFrom(manifest, cfg, config) {
879
887
  }
880
888
  return { routes, hidden: hidden3, forms };
881
889
  }
882
- function loginSpec(manifest, config, warnings) {
890
+ function loginSpec(manifest, options, warnings) {
891
+ const config = options.login;
883
892
  if (config === false) return null;
884
- if (config === void 0) return manifest.login ?? null;
893
+ let login;
894
+ if (config === void 0) login = manifest.login ?? null;
895
+ else {
896
+ const step = stepSpec(manifest, config, (name) => fieldKind(name), warnings, "login", "AI assistants can't sign in with your app's login");
897
+ login = step ? { ...step.step, where: step.where } : null;
898
+ }
899
+ if (!login || options.loginVerify === void 0) return login;
900
+ const { verify: _found, ...first } = login;
901
+ if (options.loginVerify === false) return first;
902
+ const kind = (name) => verifyFields([name], true)?.[0]?.kind ?? fieldKind(name);
903
+ const verify = stepSpec(manifest, options.loginVerify, kind, warnings, "loginVerify", "AI assistants can't finish your app's two-step login");
904
+ return verify ? { ...first, verify: verify.step, where: `${first.where ?? "your login"}, then a code: ${verify.where}` } : first;
905
+ }
906
+ function supabaseSpec(manifest, options, warnings) {
907
+ if (!manifest.supabase || options.supabase === false) return null;
908
+ if (options.supabase) return options.supabase;
909
+ const e = env3();
910
+ const url = e.NEXT_PUBLIC_SUPABASE_URL ?? e.SUPABASE_URL;
911
+ const key = e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? e.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY;
912
+ if (!url || !key) {
913
+ 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 } })");
914
+ return null;
915
+ }
916
+ try {
917
+ new URL(url);
918
+ } catch {
919
+ warnings.push(`NEXT_PUBLIC_SUPABASE_URL isn't a URL (${url}), so AI assistants can't sign in to your Supabase Auth`);
920
+ return null;
921
+ }
922
+ return { url, key };
923
+ }
924
+ function stepSpec(manifest, config, kind, warnings, option, consequence) {
885
925
  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}` };
926
+ const fields2 = config.fields.map((name) => ({ name, kind: kind(name) }));
927
+ return { step: { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json" }, where: `POST ${config.path}` };
888
928
  }
889
929
  const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
890
930
  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`);
931
+ warnings.push(`aiReady({ ${option}: "${config}" }): no server action by that name, so ${consequence}`);
892
932
  return null;
893
933
  }
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}` };
934
+ const fields = action.fields.map((f) => ({ name: f.name, kind: kind(f.name) }));
935
+ return { step: { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {} }, where: `${action.export} in ${action.file}` };
896
936
  }
897
- async function actionLoginResult(raw, exportName) {
937
+ async function actionLoginResult(raw, exportName, reading = {}) {
898
938
  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 !== "");
939
+ const changes = cookieChanges(raw);
900
940
  const out = await readActionResponse(raw, exportName);
901
941
  const body = await out.json().catch(() => void 0);
942
+ const location = raw.headers.get("x-action-redirect")?.split(";")[0] ?? (typeof body?.redirectedTo === "string" ? body.redirectedTo : null);
943
+ const step = readStep({ status: location ? 303 : out.status, location, body, cookies: changes }, reading);
944
+ if (step) return keepFailedCookies(step, changes, reading);
902
945
  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 };
946
+ if (out.status >= 400 || message && !body?.redirectedTo || rejectedBody(body)) return keepFailedCookies({ ok: false, message }, changes, reading);
947
+ const session = sessionFrom(changes, reading, Boolean(location) || body !== void 0 && body !== null && typeof body === "object");
948
+ return session ? { ok: true, session } : keepFailedCookies({ ok: false, message }, changes, reading);
905
949
  }
906
950
  var clientIp = (headers) => headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || void 0;
907
951
  function aiReady(manifest, options = {}) {
@@ -914,9 +958,39 @@ function aiReady(manifest, options = {}) {
914
958
  const actionsBase = (manifest.mcpPath ?? "").replace(/\/$/, "");
915
959
  const actionUrl = manifest.mcpPath === void 0 ? null : new RegExp(`${actionsBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/actions/([^/]+)/?$`);
916
960
  const loginWarnings = [];
917
- const login = hostedResolverInfo(cfg.auth) ? loginSpec(manifest, options.login, loginWarnings) : null;
961
+ const login = hostedResolverInfo(cfg.auth) ? loginSpec(manifest, options, loginWarnings) : null;
918
962
  const loginPagePath = `${(manifest.mcpPath ?? "/mcp").replace(/\/$/, "")}/login`;
919
963
  if (login) cfg.signedOutPaths = [login.kind === "action" ? login.host : login.path].filter((p) => Boolean(p)).map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`);
964
+ const supabase = hostedResolverInfo(cfg.auth) ? supabaseSpec(manifest, options, loginWarnings) : null;
965
+ if (supabase && manifest.supabase?.loginPages?.length) {
966
+ cfg.signedOutPaths = [...cfg.signedOutPaths ?? [], ...manifest.supabase.loginPages.map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`)];
967
+ }
968
+ const supabasePassword = supabase ? supabasePasswordLogin({
969
+ ...supabase,
970
+ 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")
971
+ }) : null;
972
+ const supabaseLoginNow = () => {
973
+ if (!supabasePassword) return null;
974
+ const choice = hostedResolverInfo(cfg.auth)?.supabasePasswordDirect();
975
+ return choice ?? (!login && manifest.supabase?.direct.includes("password")) ? supabasePassword : null;
976
+ };
977
+ let supabaseAuth;
978
+ const supabaseReport = () => {
979
+ if (!supabase || !manifest.supabase) return Promise.resolve(void 0);
980
+ const found = manifest.supabase;
981
+ supabaseAuth ??= readSupabaseAuth(supabase.url, supabase.key).then((read) => {
982
+ if (!read) {
983
+ supabaseAuth = void 0;
984
+ return void 0;
985
+ }
986
+ return {
987
+ ...read,
988
+ cookie: found.cookieName ?? supabaseCookieName(supabase.url),
989
+ found: { direct: found.direct, where: found.found, ...found.unusable ? { unusable: found.unusable } : {}, ...found.callbacks ? { callbacks: found.callbacks } : {} }
990
+ };
991
+ });
992
+ return supabaseAuth;
993
+ };
920
994
  let built = null;
921
995
  const build = async (endpoint) => {
922
996
  const warnings = [...loginWarnings];
@@ -1020,7 +1094,12 @@ function aiReady(manifest, options = {}) {
1020
1094
  if (cfg.expose === "*") details.push('all write routes exposed (expose: "*")');
1021
1095
  if (counts.hiddenWrites > 0) details.push(`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`);
1022
1096
  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}`);
1097
+ if (supabaseLoginNow()) {
1098
+ details.push(`AI assistants sign in to your Supabase Auth with a password at ${loginPagePath}`);
1099
+ } else if (login) {
1100
+ const step = (l) => l.kind === "action" ? `server action ${l.key}` : `POST ${l.path}`;
1101
+ details.push(`AI assistants sign in with your app's own login (${step(login)}${login.verify ? `, then a code: ${step(login.verify)}` : ""}) at ${loginPagePath}`);
1102
+ }
1024
1103
  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
1104
  if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
1026
1105
  if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
@@ -1092,34 +1171,34 @@ function aiReady(manifest, options = {}) {
1092
1171
  const reply = encodeReply(actionArguments(live.action.params, fields, jsonFieldsOf(live.action)));
1093
1172
  return fetch(new URL(path, origin), { method: "POST", headers: out, body: reply, redirect: "manual", cache: "no-store" });
1094
1173
  };
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) => {
1174
+ const appLoginFor = (req, spec) => {
1175
+ const pathOf = (p) => {
1176
+ let path = `${manifest.basePath ?? ""}${p}`;
1177
+ if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
1178
+ return path;
1179
+ };
1180
+ const stepPath = (step) => step.kind === "action" ? step.host ? pathOf(hostPath(step.host)) : void 0 : pathOf(step.path);
1181
+ const verifyPaths = spec.verify ? [stepPath(spec.verify)].filter((p) => Boolean(p)) : void 0;
1182
+ const send = async (step, values, ctx, reading, cookies = []) => {
1100
1183
  const target = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
1101
1184
  const pub = new URL(ctx.origin);
1102
1185
  const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
1103
1186
  if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
1104
1187
  const ua = req.headers.get("user-agent");
1105
1188
  if (ua) headers.set("user-agent", ua);
1189
+ if (cookies.length) headers.set("cookie", cookies.map((c) => `${c.name}=${c.value}`).join("; "));
1106
1190
  const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
1107
1191
  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" }));
1192
+ if (step.kind === "route") {
1193
+ headers.set("content-type", step.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
1194
+ const body = step.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
1195
+ return loginResultFromResponse(await fetch(new URL(pathOf(step.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }), reading);
1117
1196
  }
1118
- const action = (manifest.actions ?? []).find((a) => a.key === spec.key);
1197
+ const action = (manifest.actions ?? []).find((a) => a.key === step.key);
1119
1198
  const fn = action ? (await action.load())[action.export] : void 0;
1120
1199
  const id = fn?.$$id;
1121
1200
  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`);
1201
+ 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
1202
  return { ok: false, message: "This app's sign-in can't be used by AI assistants yet." };
1124
1203
  }
1125
1204
  headers.set("next-action", id);
@@ -1127,9 +1206,28 @@ function aiReady(manifest, options = {}) {
1127
1206
  headers.set("accept", "text/x-component");
1128
1207
  const reply = encodeReply(actionArguments(action.params, values, jsonFieldsOf(action)));
1129
1208
  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
- });
1209
+ return actionLoginResult(raw, action.export, reading);
1210
+ };
1211
+ const verify = spec.verify;
1212
+ return {
1213
+ label: spec.kind === "action" ? `server action ${spec.key}` : `POST ${spec.path}`,
1214
+ fields: spec.fields,
1215
+ signedOutPaths: cfg.signedOutPaths ?? [],
1216
+ ...spec.passwordless ? { passwordless: true } : {},
1217
+ attempt: (values, ctx) => send(spec, values, ctx, {
1218
+ ...verifyPaths ? { verifyPaths } : {},
1219
+ ...verify ? { verifyFields: verify.fields.map((f) => f.name) } : {},
1220
+ ...spec.passwordless ? { start: true } : {}
1221
+ }),
1222
+ ...verify ? {
1223
+ verify: {
1224
+ label: verify.kind === "action" ? `server action ${verify.key}` : `POST ${verify.path}`,
1225
+ fields: verify.fields,
1226
+ attempt: (values, ctx) => send(verify, values, ctx, { carried: ctx.carry.cookies, verifyPaths: verifyPaths ?? [] }, ctx.carry.cookies)
1227
+ }
1228
+ } : {}
1229
+ };
1230
+ };
1133
1231
  const carrySession = (raw, out) => {
1134
1232
  const lines = setCookieLines(raw);
1135
1233
  const redirect = raw.headers.get("x-action-redirect")?.split(";")[0];
@@ -1174,9 +1272,10 @@ function aiReady(manifest, options = {}) {
1174
1272
  };
1175
1273
  return async (req) => {
1176
1274
  const pathname = new URL(req.url).pathname;
1177
- if (login && samePath(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
1275
+ const supabaseLogin = supabaseLoginNow();
1276
+ if ((login || supabaseLogin) && samePath(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
1178
1277
  const pub = new URL(publicRequestUrl(req.url, req.headers));
1179
- return handleLoginPage(req, appLoginFor(req, login), hostedResolverInfo(cfg.auth), {
1278
+ return handleLoginPage(req, supabaseLogin ?? appLoginFor(req, login), hostedResolverInfo(cfg.auth), {
1180
1279
  path: pub.pathname,
1181
1280
  secure: pub.protocol === "https:",
1182
1281
  origin: pub.origin,
@@ -1203,7 +1302,13 @@ function aiReady(manifest, options = {}) {
1203
1302
  const url = publicRequestUrl(req.url, req.headers);
1204
1303
  const isPrm = url.endsWith(PRM_SUFFIX);
1205
1304
  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 });
1305
+ const supabaseAuth2 = await supabaseReport();
1306
+ const verdict = await reportAppLogin(cfg.auth, resource, supabaseLogin ?? login, {
1307
+ where: supabaseLogin ? "your Supabase Auth (password)" : login?.where,
1308
+ stacks: manifest.authStacks,
1309
+ log: logReady,
1310
+ ...supabaseAuth2 ? { supabase: supabaseAuth2 } : {}
1311
+ });
1207
1312
  if (verdict === "open") {
1208
1313
  if (isPrm) return new Response("not found", { status: 404 });
1209
1314
  } 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.4",
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.3",
36
+ "@exposify/core": "0.3.16",
37
37
  "tinyglobby": "^0.2.10",
38
38
  "zod": "^3.24.0"
39
39
  },
@@ -1,23 +0,0 @@
1
- type FieldType = "string" | "number" | "boolean" | "json" | "file";
2
- interface ActionField {
3
- name: string;
4
- type: FieldType;
5
- /** Read with getAll(): a repeated field, sent as an array. */
6
- array?: true;
7
- /** Its allowed values, from `LIST.includes(x)` over a constant list. */
8
- enum?: string[];
9
- /** 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. */
10
- required?: true;
11
- /** From the form's input (pass 3, forms.ts). */
12
- min?: number;
13
- max?: number;
14
- minLength?: number;
15
- maxLength?: number;
16
- pattern?: string;
17
- /** Where it was found: the action's code, its form, or both. */
18
- origin?: "action" | "form" | "both";
19
- /** For the agent: format, units, what it refers to (from an accepted proposal). */
20
- description?: string;
21
- }
22
-
23
- export type { ActionField as A };