@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/README.md +36 -0
- package/dist/config.cjs +361 -90
- package/dist/config.d.cts +49 -3
- package/dist/config.d.ts +49 -3
- package/dist/config.js +355 -84
- package/dist/exposify-next.js +545 -111
- package/dist/index.cjs +132 -35
- package/dist/index.d.cts +27 -123
- package/dist/index.d.ts +27 -123
- package/dist/index.js +140 -35
- package/dist/manifest-DZQCvzQy.d.cts +176 -0
- package/dist/manifest-DZQCvzQy.d.ts +176 -0
- package/package.json +3 -3
- package/dist/fields-olvyFnxj.d.cts +0 -23
- package/dist/fields-olvyFnxj.d.ts +0 -23
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,
|
|
890
|
+
function loginSpec(manifest, options, warnings) {
|
|
891
|
+
const config = options.login;
|
|
883
892
|
if (config === false) return null;
|
|
884
|
-
|
|
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:
|
|
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({
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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 ===
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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
|
+
"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.
|
|
36
|
-
"@exposify/core": "0.3.
|
|
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 };
|