@exposify/next 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/dist/config.cjs +365 -94
- package/dist/config.d.cts +49 -3
- package/dist/config.d.ts +49 -3
- package/dist/config.js +359 -88
- package/dist/exposify-next.js +552 -118
- package/dist/index.cjs +190 -43
- package/dist/index.d.cts +40 -120
- package/dist/index.d.ts +40 -120
- package/dist/index.js +196 -43
- 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.cjs
CHANGED
|
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AGENT_IP_HEADER: () => AGENT_IP_HEADER,
|
|
23
24
|
DISPATCH_HEADER: () => DISPATCH_HEADER,
|
|
25
|
+
agentIp: () => agentIp,
|
|
24
26
|
aiReady: () => aiReady,
|
|
25
27
|
completeMcpLogin: () => import_core2.completeMcpLogin,
|
|
26
28
|
createHostedAuthResolver: () => import_core2.createHostedAuthResolver,
|
|
@@ -637,6 +639,49 @@ async function readPageResponse(res, url) {
|
|
|
637
639
|
// src/routes.ts
|
|
638
640
|
var HTTP_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
639
641
|
|
|
642
|
+
// src/agent-ip.ts
|
|
643
|
+
var AGENT_IP_HEADER = "x-exposify-agent-ip";
|
|
644
|
+
var MAX_AGE_S = 300;
|
|
645
|
+
function env2() {
|
|
646
|
+
return globalThis.process?.env ?? {};
|
|
647
|
+
}
|
|
648
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
649
|
+
function hmacKey(secret) {
|
|
650
|
+
let key = keyCache.get(secret);
|
|
651
|
+
if (!key) {
|
|
652
|
+
key = (async () => {
|
|
653
|
+
const material = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`exposify agent-ip v1
|
|
654
|
+
${secret}`));
|
|
655
|
+
return crypto.subtle.importKey("raw", material, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
656
|
+
})();
|
|
657
|
+
keyCache.set(secret, key);
|
|
658
|
+
}
|
|
659
|
+
return key;
|
|
660
|
+
}
|
|
661
|
+
var hex2 = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
662
|
+
async function signAgentIp(ip, secret = env2().EXPOSIFY_KEY, now = Date.now()) {
|
|
663
|
+
if (!ip || !secret || /[;\s]/.test(ip) || ip.length > 64) return null;
|
|
664
|
+
const t = Math.floor(now / 1e3);
|
|
665
|
+
const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret), new TextEncoder().encode(`${ip};${t}`));
|
|
666
|
+
return `${ip};${t};${hex2(sig)}`;
|
|
667
|
+
}
|
|
668
|
+
function forwardedIp(headers) {
|
|
669
|
+
return headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip")?.trim() || void 0;
|
|
670
|
+
}
|
|
671
|
+
async function agentIp(request, options = {}) {
|
|
672
|
+
const signed = request.headers.get(AGENT_IP_HEADER);
|
|
673
|
+
const secret = options.key ?? env2().EXPOSIFY_KEY;
|
|
674
|
+
if (signed && secret) {
|
|
675
|
+
const [ip, t, sig] = signed.split(";");
|
|
676
|
+
const age = Math.floor((options.now ?? Date.now()) / 1e3) - Number(t);
|
|
677
|
+
if (ip && sig && /^[0-9a-f]{64}$/.test(sig) && Number.isInteger(Number(t)) && age >= -60 && age <= MAX_AGE_S) {
|
|
678
|
+
const bytes = new Uint8Array(sig.match(/../g).map((b) => parseInt(b, 16)));
|
|
679
|
+
if (await crypto.subtle.verify("HMAC", await hmacKey(secret), bytes, new TextEncoder().encode(`${ip};${t}`))) return ip;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return forwardedIp(request.headers);
|
|
683
|
+
}
|
|
684
|
+
|
|
640
685
|
// src/index.ts
|
|
641
686
|
var import_core2 = require("@exposify/core");
|
|
642
687
|
var DISPATCH_HEADER = "x-exposify-dispatch";
|
|
@@ -645,7 +690,7 @@ var LOG_PREFIX = "[agentready]";
|
|
|
645
690
|
var ACTION_NOTE = "It returns the action's result, or { ok: true, redirectedTo } when the action redirects; a redirect to a sign-in page means the call wasn't authorised.";
|
|
646
691
|
var LEFT_BLANK = "A field you leave out is as if left blank on the form.";
|
|
647
692
|
var PAGE_NOTE = "Returns the page's main content as text, with links, and the data it was rendered with when the page passes any to its components. An error naming a sign-in page means signing in first.";
|
|
648
|
-
function
|
|
693
|
+
function env3() {
|
|
649
694
|
return globalThis.process?.env ?? {};
|
|
650
695
|
}
|
|
651
696
|
function errorMessage(err) {
|
|
@@ -708,7 +753,7 @@ function derivedInput(action) {
|
|
|
708
753
|
const fields = action.proposed.map((p) => ({ name: p.name, type: p.type, ...p.required ? { required: true } : {}, ...p.enum ? { enum: p.enum } : {}, ...p.description ? { description: p.description } : {} }));
|
|
709
754
|
return { input: Object.fromEntries(fields.map((f) => [f.name, fieldSchema(f)])) };
|
|
710
755
|
}
|
|
711
|
-
if (reason) return { reason: action.fields.some((f) => f.type === "file") ? reason : `${reason};
|
|
756
|
+
if (reason) return { reason: action.fields.some((f) => f.type === "file") ? reason : `${reason}; \`@exposify/next describe\` can propose it for you to review` };
|
|
712
757
|
return { input: Object.fromEntries(action.fields.map((f) => [f.name, fieldSchema(f)])) };
|
|
713
758
|
}
|
|
714
759
|
var sentence = (s) => /[.!?]$/.test(s.trim()) ? s.trim() : `${s.trim()}.`;
|
|
@@ -828,29 +873,65 @@ function pagesFrom(manifest, cfg, config) {
|
|
|
828
873
|
}
|
|
829
874
|
return { routes, hidden: hidden3, forms };
|
|
830
875
|
}
|
|
831
|
-
function loginSpec(manifest,
|
|
876
|
+
function loginSpec(manifest, options, warnings) {
|
|
877
|
+
const config = options.login;
|
|
832
878
|
if (config === false) return null;
|
|
833
|
-
|
|
879
|
+
let login;
|
|
880
|
+
if (config === void 0) login = manifest.login ?? null;
|
|
881
|
+
else {
|
|
882
|
+
const step = stepSpec(manifest, config, (name) => (0, import_core.fieldKind)(name), warnings, "login", "AI assistants can't sign in with your app's login");
|
|
883
|
+
login = step ? { ...step.step, where: step.where } : null;
|
|
884
|
+
}
|
|
885
|
+
if (!login || options.loginVerify === void 0) return login;
|
|
886
|
+
const { verify: _found, ...first } = login;
|
|
887
|
+
if (options.loginVerify === false) return first;
|
|
888
|
+
const kind = (name) => (0, import_core.verifyFields)([name], true)?.[0]?.kind ?? (0, import_core.fieldKind)(name);
|
|
889
|
+
const verify = stepSpec(manifest, options.loginVerify, kind, warnings, "loginVerify", "AI assistants can't finish your app's two-step login");
|
|
890
|
+
return verify ? { ...first, verify: verify.step, where: `${first.where ?? "your login"}, then a code: ${verify.where}` } : first;
|
|
891
|
+
}
|
|
892
|
+
function supabaseSpec(manifest, options, warnings) {
|
|
893
|
+
if (!manifest.supabase || options.supabase === false) return null;
|
|
894
|
+
if (options.supabase) return options.supabase;
|
|
895
|
+
const e = env3();
|
|
896
|
+
const url = e.NEXT_PUBLIC_SUPABASE_URL ?? e.SUPABASE_URL;
|
|
897
|
+
const key = e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? e.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? e.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY;
|
|
898
|
+
if (!url || !key) {
|
|
899
|
+
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 } })");
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
new URL(url);
|
|
904
|
+
} catch {
|
|
905
|
+
warnings.push(`NEXT_PUBLIC_SUPABASE_URL isn't a URL (${url}), so AI assistants can't sign in to your Supabase Auth`);
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
return { url, key };
|
|
909
|
+
}
|
|
910
|
+
function stepSpec(manifest, config, kind, warnings, option, consequence) {
|
|
834
911
|
if (typeof config === "object") {
|
|
835
|
-
const fields2 = config.fields.map((name) => ({ name, kind: (
|
|
836
|
-
return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json", where: `POST ${config.path}` };
|
|
912
|
+
const fields2 = config.fields.map((name) => ({ name, kind: kind(name) }));
|
|
913
|
+
return { step: { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json" }, where: `POST ${config.path}` };
|
|
837
914
|
}
|
|
838
915
|
const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
|
|
839
916
|
if (!action) {
|
|
840
|
-
warnings.push(`aiReady({
|
|
917
|
+
warnings.push(`aiReady({ ${option}: "${config}" }): no server action by that name, so ${consequence}`);
|
|
841
918
|
return null;
|
|
842
919
|
}
|
|
843
|
-
const fields = action.fields.map((f) => ({ name: f.name, kind: (
|
|
844
|
-
return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {}, where: `${action.export} in ${action.file}` };
|
|
920
|
+
const fields = action.fields.map((f) => ({ name: f.name, kind: kind(f.name) }));
|
|
921
|
+
return { step: { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {} }, where: `${action.export} in ${action.file}` };
|
|
845
922
|
}
|
|
846
|
-
async function actionLoginResult(raw, exportName) {
|
|
923
|
+
async function actionLoginResult(raw, exportName, reading = {}) {
|
|
847
924
|
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." };
|
|
848
|
-
const
|
|
925
|
+
const changes = (0, import_core.cookieChanges)(raw);
|
|
849
926
|
const out = await readActionResponse(raw, exportName);
|
|
850
927
|
const body = await out.json().catch(() => void 0);
|
|
928
|
+
const location = raw.headers.get("x-action-redirect")?.split(";")[0] ?? (typeof body?.redirectedTo === "string" ? body.redirectedTo : null);
|
|
929
|
+
const step = (0, import_core.readStep)({ status: location ? 303 : out.status, location, body, cookies: changes }, reading);
|
|
930
|
+
if (step) return (0, import_core.keepFailedCookies)(step, changes, reading);
|
|
851
931
|
const message = (0, import_core.errorMessageOf)(body);
|
|
852
|
-
if (out.status >= 400 || message && !body?.redirectedTo) return { ok: false, message };
|
|
853
|
-
|
|
932
|
+
if (out.status >= 400 || message && !body?.redirectedTo || (0, import_core.rejectedBody)(body)) return (0, import_core.keepFailedCookies)({ ok: false, message }, changes, reading);
|
|
933
|
+
const session = (0, import_core.sessionFrom)(changes, reading, Boolean(location) || body !== void 0 && body !== null && typeof body === "object");
|
|
934
|
+
return session ? { ok: true, session } : (0, import_core.keepFailedCookies)({ ok: false, message }, changes, reading);
|
|
854
935
|
}
|
|
855
936
|
var clientIp = (headers) => headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || void 0;
|
|
856
937
|
function aiReady(manifest, options = {}) {
|
|
@@ -863,9 +944,39 @@ function aiReady(manifest, options = {}) {
|
|
|
863
944
|
const actionsBase = (manifest.mcpPath ?? "").replace(/\/$/, "");
|
|
864
945
|
const actionUrl = manifest.mcpPath === void 0 ? null : new RegExp(`${actionsBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/actions/([^/]+)/?$`);
|
|
865
946
|
const loginWarnings = [];
|
|
866
|
-
const login = (0, import_core.hostedResolverInfo)(cfg.auth) ? loginSpec(manifest, options
|
|
947
|
+
const login = (0, import_core.hostedResolverInfo)(cfg.auth) ? loginSpec(manifest, options, loginWarnings) : null;
|
|
867
948
|
const loginPagePath = `${(manifest.mcpPath ?? "/mcp").replace(/\/$/, "")}/login`;
|
|
868
949
|
if (login) cfg.signedOutPaths = [login.kind === "action" ? login.host : login.path].filter((p) => Boolean(p)).map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`);
|
|
950
|
+
const supabase = (0, import_core.hostedResolverInfo)(cfg.auth) ? supabaseSpec(manifest, options, loginWarnings) : null;
|
|
951
|
+
if (supabase && manifest.supabase?.loginPages?.length) {
|
|
952
|
+
cfg.signedOutPaths = [...cfg.signedOutPaths ?? [], ...manifest.supabase.loginPages.map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`)];
|
|
953
|
+
}
|
|
954
|
+
const supabasePassword = supabase ? (0, import_core.supabasePasswordLogin)({
|
|
955
|
+
...supabase,
|
|
956
|
+
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")
|
|
957
|
+
}) : null;
|
|
958
|
+
const supabaseLoginNow = () => {
|
|
959
|
+
if (!supabasePassword) return null;
|
|
960
|
+
const choice = (0, import_core.hostedResolverInfo)(cfg.auth)?.supabasePasswordDirect();
|
|
961
|
+
return choice ?? (!login && manifest.supabase?.direct.includes("password")) ? supabasePassword : null;
|
|
962
|
+
};
|
|
963
|
+
let supabaseAuth;
|
|
964
|
+
const supabaseReport = () => {
|
|
965
|
+
if (!supabase || !manifest.supabase) return Promise.resolve(void 0);
|
|
966
|
+
const found = manifest.supabase;
|
|
967
|
+
supabaseAuth ??= (0, import_core.readSupabaseAuth)(supabase.url, supabase.key).then((read) => {
|
|
968
|
+
if (!read) {
|
|
969
|
+
supabaseAuth = void 0;
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
...read,
|
|
974
|
+
cookie: found.cookieName ?? (0, import_core.supabaseCookieName)(supabase.url),
|
|
975
|
+
found: { direct: found.direct, where: found.found, ...found.unusable ? { unusable: found.unusable } : {}, ...found.callbacks ? { callbacks: found.callbacks } : {} }
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
return supabaseAuth;
|
|
979
|
+
};
|
|
869
980
|
let built = null;
|
|
870
981
|
const build = async (endpoint) => {
|
|
871
982
|
const warnings = [...loginWarnings];
|
|
@@ -959,7 +1070,7 @@ function aiReady(manifest, options = {}) {
|
|
|
959
1070
|
(0, import_core.formatWarning)(
|
|
960
1071
|
LOG_PREFIX,
|
|
961
1072
|
`MCP is up at ${endpoint} but exposes no route tools`,
|
|
962
|
-
"check that exposify.generated.ts is current (withExposify() in next.config, or `npx exposify
|
|
1073
|
+
"check that exposify.generated.ts is current (withExposify() in next.config, or `npx @exposify/next generate`)"
|
|
963
1074
|
)
|
|
964
1075
|
);
|
|
965
1076
|
return;
|
|
@@ -969,11 +1080,16 @@ function aiReady(manifest, options = {}) {
|
|
|
969
1080
|
if (cfg.expose === "*") details.push('all write routes exposed (expose: "*")');
|
|
970
1081
|
if (counts.hiddenWrites > 0) details.push(`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`);
|
|
971
1082
|
if (pages.routes.length > 0) details.push(`${pages.routes.length} page${pages.routes.length === 1 ? "" : "s"} readable`);
|
|
972
|
-
if (
|
|
1083
|
+
if (supabaseLoginNow()) {
|
|
1084
|
+
details.push(`AI assistants sign in to your Supabase Auth with a password at ${loginPagePath}`);
|
|
1085
|
+
} else if (login) {
|
|
1086
|
+
const step = (l) => l.kind === "action" ? `server action ${l.key}` : `POST ${l.path}`;
|
|
1087
|
+
details.push(`AI assistants sign in with your app's own login (${step(login)}${login.verify ? `, then a code: ${step(login.verify)}` : ""}) at ${loginPagePath}`);
|
|
1088
|
+
}
|
|
973
1089
|
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)`);
|
|
974
1090
|
if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
|
|
975
1091
|
if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
|
|
976
|
-
const e =
|
|
1092
|
+
const e = env3();
|
|
977
1093
|
if (e.VERCEL && e.VERCEL_ENV !== "production" && !e.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
978
1094
|
details.push("on a protected Vercel preview, tool calls need VERCEL_AUTOMATION_BYPASS_SECRET");
|
|
979
1095
|
}
|
|
@@ -983,7 +1099,7 @@ function aiReady(manifest, options = {}) {
|
|
|
983
1099
|
const selfFetch = async (request, mcpReq, route) => {
|
|
984
1100
|
const target = new URL(request.url);
|
|
985
1101
|
const incoming = new URL(mcpReq.url);
|
|
986
|
-
const origin = options.baseUrl ??
|
|
1102
|
+
const origin = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? incoming.origin;
|
|
987
1103
|
let path = `${manifest.basePath ?? ""}${target.pathname}`;
|
|
988
1104
|
if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
|
|
989
1105
|
const headers = new Headers(request.headers);
|
|
@@ -994,8 +1110,11 @@ function aiReady(manifest, options = {}) {
|
|
|
994
1110
|
}
|
|
995
1111
|
headers.set("x-forwarded-proto", mcpReq.headers.get("x-forwarded-proto") ?? incoming.protocol.slice(0, -1));
|
|
996
1112
|
headers.set("x-forwarded-host", mcpReq.headers.get("x-forwarded-host") ?? mcpReq.headers.get("host") ?? incoming.host);
|
|
997
|
-
const bypass =
|
|
1113
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
998
1114
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1115
|
+
const signedIp = await signAgentIp(clientIp(mcpReq.headers));
|
|
1116
|
+
if (signedIp) headers.set(AGENT_IP_HEADER, signedIp);
|
|
1117
|
+
else headers.delete(AGENT_IP_HEADER);
|
|
999
1118
|
const body = request.body ? await request.text() : void 0;
|
|
1000
1119
|
const actionKey = actionUrl?.exec(target.pathname)?.[1];
|
|
1001
1120
|
if (actionKey !== void 0) {
|
|
@@ -1038,34 +1157,34 @@ function aiReady(manifest, options = {}) {
|
|
|
1038
1157
|
const reply = encodeReply(actionArguments(live.action.params, fields, jsonFieldsOf(live.action)));
|
|
1039
1158
|
return fetch(new URL(path, origin), { method: "POST", headers: out, body: reply, redirect: "manual", cache: "no-store" });
|
|
1040
1159
|
};
|
|
1041
|
-
const appLoginFor = (req, spec) =>
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1160
|
+
const appLoginFor = (req, spec) => {
|
|
1161
|
+
const pathOf = (p) => {
|
|
1162
|
+
let path = `${manifest.basePath ?? ""}${p}`;
|
|
1163
|
+
if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
|
|
1164
|
+
return path;
|
|
1165
|
+
};
|
|
1166
|
+
const stepPath = (step) => step.kind === "action" ? step.host ? pathOf(hostPath(step.host)) : void 0 : pathOf(step.path);
|
|
1167
|
+
const verifyPaths = spec.verify ? [stepPath(spec.verify)].filter((p) => Boolean(p)) : void 0;
|
|
1168
|
+
const send = async (step, values, ctx, reading, cookies = []) => {
|
|
1169
|
+
const target = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
|
|
1047
1170
|
const pub = new URL(ctx.origin);
|
|
1048
1171
|
const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
|
|
1049
1172
|
if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
|
|
1050
1173
|
const ua = req.headers.get("user-agent");
|
|
1051
1174
|
if (ua) headers.set("user-agent", ua);
|
|
1052
|
-
|
|
1175
|
+
if (cookies.length) headers.set("cookie", cookies.map((c) => `${c.name}=${c.value}`).join("; "));
|
|
1176
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
1053
1177
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
return path;
|
|
1058
|
-
};
|
|
1059
|
-
if (spec.kind === "route") {
|
|
1060
|
-
headers.set("content-type", spec.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
|
|
1061
|
-
const body = spec.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
1062
|
-
return (0, import_core.loginResultFromResponse)(await fetch(new URL(pathOf(spec.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }));
|
|
1178
|
+
if (step.kind === "route") {
|
|
1179
|
+
headers.set("content-type", step.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
|
|
1180
|
+
const body = step.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
1181
|
+
return (0, import_core.loginResultFromResponse)(await fetch(new URL(pathOf(step.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }), reading);
|
|
1063
1182
|
}
|
|
1064
|
-
const action = (manifest.actions ?? []).find((a) => a.key ===
|
|
1183
|
+
const action = (manifest.actions ?? []).find((a) => a.key === step.key);
|
|
1065
1184
|
const fn = action ? (await action.load())[action.export] : void 0;
|
|
1066
1185
|
const id = fn?.$$id;
|
|
1067
1186
|
if (!action?.host || typeof id !== "string") {
|
|
1068
|
-
warnOnce(`server action ${
|
|
1187
|
+
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`);
|
|
1069
1188
|
return { ok: false, message: "This app's sign-in can't be used by AI assistants yet." };
|
|
1070
1189
|
}
|
|
1071
1190
|
headers.set("next-action", id);
|
|
@@ -1073,9 +1192,28 @@ function aiReady(manifest, options = {}) {
|
|
|
1073
1192
|
headers.set("accept", "text/x-component");
|
|
1074
1193
|
const reply = encodeReply(actionArguments(action.params, values, jsonFieldsOf(action)));
|
|
1075
1194
|
const raw = await fetch(new URL(pathOf(hostPath(action.host)), target), { method: "POST", headers, body: reply, redirect: "manual", cache: "no-store" });
|
|
1076
|
-
return actionLoginResult(raw, action.export);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1195
|
+
return actionLoginResult(raw, action.export, reading);
|
|
1196
|
+
};
|
|
1197
|
+
const verify = spec.verify;
|
|
1198
|
+
return {
|
|
1199
|
+
label: spec.kind === "action" ? `server action ${spec.key}` : `POST ${spec.path}`,
|
|
1200
|
+
fields: spec.fields,
|
|
1201
|
+
signedOutPaths: cfg.signedOutPaths ?? [],
|
|
1202
|
+
...spec.passwordless ? { passwordless: true } : {},
|
|
1203
|
+
attempt: (values, ctx) => send(spec, values, ctx, {
|
|
1204
|
+
...verifyPaths ? { verifyPaths } : {},
|
|
1205
|
+
...verify ? { verifyFields: verify.fields.map((f) => f.name) } : {},
|
|
1206
|
+
...spec.passwordless ? { start: true } : {}
|
|
1207
|
+
}),
|
|
1208
|
+
...verify ? {
|
|
1209
|
+
verify: {
|
|
1210
|
+
label: verify.kind === "action" ? `server action ${verify.key}` : `POST ${verify.path}`,
|
|
1211
|
+
fields: verify.fields,
|
|
1212
|
+
attempt: (values, ctx) => send(verify, values, ctx, { carried: ctx.carry.cookies, verifyPaths: verifyPaths ?? [] }, ctx.carry.cookies)
|
|
1213
|
+
}
|
|
1214
|
+
} : {}
|
|
1215
|
+
};
|
|
1216
|
+
};
|
|
1079
1217
|
const carrySession = (raw, out) => {
|
|
1080
1218
|
const lines = (0, import_core.setCookieLines)(raw);
|
|
1081
1219
|
const redirect = raw.headers.get("x-action-redirect")?.split(";")[0];
|
|
@@ -1120,9 +1258,10 @@ function aiReady(manifest, options = {}) {
|
|
|
1120
1258
|
};
|
|
1121
1259
|
return async (req) => {
|
|
1122
1260
|
const pathname = new URL(req.url).pathname;
|
|
1123
|
-
|
|
1261
|
+
const supabaseLogin = supabaseLoginNow();
|
|
1262
|
+
if ((login || supabaseLogin) && (0, import_core.samePath)(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
|
|
1124
1263
|
const pub = new URL((0, import_core.publicRequestUrl)(req.url, req.headers));
|
|
1125
|
-
return (0, import_core.handleLoginPage)(req, appLoginFor(req, login), (0, import_core.hostedResolverInfo)(cfg.auth), {
|
|
1264
|
+
return (0, import_core.handleLoginPage)(req, supabaseLogin ?? appLoginFor(req, login), (0, import_core.hostedResolverInfo)(cfg.auth), {
|
|
1126
1265
|
path: pub.pathname,
|
|
1127
1266
|
secure: pub.protocol === "https:",
|
|
1128
1267
|
origin: pub.origin,
|
|
@@ -1149,7 +1288,13 @@ function aiReady(manifest, options = {}) {
|
|
|
1149
1288
|
const url = (0, import_core.publicRequestUrl)(req.url, req.headers);
|
|
1150
1289
|
const isPrm = url.endsWith(PRM_SUFFIX);
|
|
1151
1290
|
const resource = brokerGate.resourceFor(isPrm ? url.slice(0, -PRM_SUFFIX.length) : url);
|
|
1152
|
-
const
|
|
1291
|
+
const supabaseAuth2 = await supabaseReport();
|
|
1292
|
+
const verdict = await (0, import_core.reportAppLogin)(cfg.auth, resource, supabaseLogin ?? login, {
|
|
1293
|
+
where: supabaseLogin ? "your Supabase Auth (password)" : login?.where,
|
|
1294
|
+
stacks: manifest.authStacks,
|
|
1295
|
+
log: logReady,
|
|
1296
|
+
...supabaseAuth2 ? { supabase: supabaseAuth2 } : {}
|
|
1297
|
+
});
|
|
1153
1298
|
if (verdict === "open") {
|
|
1154
1299
|
if (isPrm) return new Response("not found", { status: 404 });
|
|
1155
1300
|
} else {
|
|
@@ -1168,7 +1313,9 @@ function aiReady(manifest, options = {}) {
|
|
|
1168
1313
|
}
|
|
1169
1314
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1170
1315
|
0 && (module.exports = {
|
|
1316
|
+
AGENT_IP_HEADER,
|
|
1171
1317
|
DISPATCH_HEADER,
|
|
1318
|
+
agentIp,
|
|
1172
1319
|
aiReady,
|
|
1173
1320
|
completeMcpLogin,
|
|
1174
1321
|
createHostedAuthResolver,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,126 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RouteMarker, AgentReadyOptions } from '@exposify/core';
|
|
2
2
|
export { HostedAuthResolverConfig, completeMcpLogin, createHostedAuthResolver, describe, exposed, hidden, hostedBroker } from '@exposify/core';
|
|
3
|
-
import {
|
|
3
|
+
import { E as ExposifyManifest } from './manifest-DZQCvzQy.cjs';
|
|
4
|
+
export { a as AppRouteEntry, b as ManifestAction, c as ManifestLogin, d as ManifestLoginStep, e as ManifestPage, f as ManifestRoute, P as PagesRouteEntry } from './manifest-DZQCvzQy.cjs';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
/** `/api/orders/:id`; catch-alls as `:slug*`, optional ones as `:slug*?`. */
|
|
8
|
-
path: string;
|
|
9
|
-
params: string[];
|
|
10
|
-
/** Project-relative, for messages. */
|
|
11
|
-
file: string;
|
|
12
|
-
/** Exported methods when generated; the loaded module's exports win at runtime. */
|
|
13
|
-
methods: string[];
|
|
14
|
-
/** Loads the route module to read exposed()/hidden()/describe() off its exports. Absent for edge routes. */
|
|
15
|
-
load?: () => Promise<Record<string, unknown>>;
|
|
16
|
-
/** `export const runtime = "edge"`: not loaded into the Node MCP route, so markers there aren't read. */
|
|
17
|
-
edge?: boolean;
|
|
18
|
-
}
|
|
19
|
-
interface PagesRouteEntry {
|
|
20
|
-
router: "pages";
|
|
21
|
-
path: string;
|
|
22
|
-
params: string[];
|
|
23
|
-
file: string;
|
|
24
|
-
}
|
|
25
|
-
type ManifestRoute = AppRouteEntry | PagesRouteEntry;
|
|
26
|
-
/** An exported function of a `"use server"` module. */
|
|
27
|
-
interface ManifestAction {
|
|
28
|
-
/** Unique among actions: the default tool name, and the URL segment under `<mcpPath>/actions/`. */
|
|
29
|
-
key: string;
|
|
30
|
-
file: string;
|
|
31
|
-
export: string;
|
|
32
|
-
/** How each parameter is filled on a call, in order: see `ActionParamRole` in parse.ts. */
|
|
33
|
-
params: Array<{
|
|
34
|
-
name: string;
|
|
35
|
-
role: "formData" | "prev" | "input" | "object";
|
|
36
|
-
}>;
|
|
37
|
-
/** It sets or deletes a cookie, which would land on exposify's internal call rather than the agent's client. */
|
|
38
|
-
setsCookies?: boolean;
|
|
39
|
-
/** What it takes, worked out from its code (fields.ts): its tool schema when the app doesn't write one. */
|
|
40
|
-
fields: ActionField[];
|
|
41
|
-
/** It reads every form field, so its code doesn't say which exist. */
|
|
42
|
-
readsAll?: boolean;
|
|
43
|
-
/** What the analysis couldn't work out ("line N: …"): with any, the app writes the schema. */
|
|
44
|
-
unresolved?: string[];
|
|
45
|
-
/** The forms that submit to it (pass 3), where they are. */
|
|
46
|
-
forms?: Array<{
|
|
47
|
-
file: string;
|
|
48
|
-
line: number;
|
|
49
|
-
}>;
|
|
50
|
-
/** An input `describe` proposed and a person accepted, current with the code: used when `fields` can't be the schema. */
|
|
51
|
-
proposed?: ProposedField[];
|
|
52
|
-
/**
|
|
53
|
-
* A page that uses it (`/checkout`, `/admin/orders/:id`): tool calls are
|
|
54
|
-
* sent there as a real Server Action request. Absent when no page uses it;
|
|
55
|
-
* it's then called in-process by the MCP route.
|
|
56
|
-
*/
|
|
57
|
-
host?: string;
|
|
58
|
-
/**
|
|
59
|
-
* The app's own checks it runs first (`lib/auth.ts#requireAdmin`): a
|
|
60
|
-
* user who doesn't pass one doesn't see the tool (guards.ts).
|
|
61
|
-
*/
|
|
62
|
-
guards?: string[];
|
|
63
|
-
load: () => Promise<Record<string, unknown>>;
|
|
64
|
-
}
|
|
65
|
-
/** An App Router page: a read tool that fetches it like a browser (pages.ts). */
|
|
66
|
-
interface ManifestPage {
|
|
67
|
-
/** Tool name, e.g. `view_product_slug`. */
|
|
68
|
-
name: string;
|
|
69
|
-
/** `/product/:slug`, as routes are written. */
|
|
70
|
-
path: string;
|
|
71
|
-
params: string[];
|
|
72
|
-
file: string;
|
|
73
|
-
/** Search params its code reads: optional tool arguments, sent as the query. */
|
|
74
|
-
search?: string[];
|
|
75
|
-
/** From its static `metadata`. */
|
|
76
|
-
title?: string;
|
|
77
|
-
description?: string;
|
|
78
|
-
/** Just a form posting to a Server Action: not a tool unless `pages` lists it. */
|
|
79
|
-
formOnly?: boolean;
|
|
80
|
-
/** The app's own checks it runs (`lib/auth.ts#requireAdmin`), see ManifestAction.guards. */
|
|
81
|
-
guards?: string[];
|
|
82
|
-
}
|
|
6
|
+
/** Header carrying `<ip>;<unix seconds>;<signature>` on a tool call. */
|
|
7
|
+
declare const AGENT_IP_HEADER = "x-exposify-agent-ip";
|
|
83
8
|
/**
|
|
84
|
-
* The
|
|
85
|
-
*
|
|
86
|
-
*
|
|
9
|
+
* The address of whoever made this request: on a tool call, the AI agent's
|
|
10
|
+
* (verified with EXPOSIFY_KEY, so it can't be forged); otherwise the
|
|
11
|
+
* platform's `x-forwarded-for`, as you'd read it anyway. Use it where a
|
|
12
|
+
* route rate-limits or logs by IP: on Vercel, a tool call's own
|
|
13
|
+
* `x-forwarded-for` is the function's address, not the agent's.
|
|
87
14
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} |
|
|
94
|
-
kind: "route";
|
|
95
|
-
path: string;
|
|
96
|
-
fields: LoginField[];
|
|
97
|
-
body: "json" | "form";
|
|
98
|
-
}) & {
|
|
99
|
-
/** Where it is, for the startup log and the dashboard: "signIn in app/actions/auth.ts". */
|
|
100
|
-
where?: string;
|
|
101
|
-
};
|
|
102
|
-
interface ExposifyManifest {
|
|
103
|
-
info: {
|
|
104
|
-
name: string;
|
|
105
|
-
version: string;
|
|
106
|
-
};
|
|
107
|
-
/** next.config's basePath, prefixed to every tool call's path. */
|
|
108
|
-
basePath?: string;
|
|
109
|
-
/** next.config's trailingSlash: tool calls add one, so they aren't redirected. */
|
|
110
|
-
trailingSlash?: boolean;
|
|
111
|
-
routes: ManifestRoute[];
|
|
112
|
-
/** The MCP route's URL without its catch-all segment, e.g. `/mcp`: Server Actions are served under `<mcpPath>/actions/`. */
|
|
113
|
-
mcpPath?: string;
|
|
114
|
-
actions?: ManifestAction[];
|
|
115
|
-
pages?: ManifestPage[];
|
|
116
|
-
login?: ManifestLogin;
|
|
117
|
-
/** Auth libraries in package.json ("next-auth", "@supabase/ssr", ...), reported with the login. */
|
|
118
|
-
authStacks?: string[];
|
|
119
|
-
/** Docs for search_docs, embedded so they work without a file system. */
|
|
120
|
-
docs: DocSource[];
|
|
121
|
-
/** exposify.descriptions.json's texts, "METHOD /path" -> text. */
|
|
122
|
-
descriptions?: Record<string, string>;
|
|
123
|
-
}
|
|
15
|
+
declare function agentIp(request: {
|
|
16
|
+
headers: Headers;
|
|
17
|
+
}, options?: {
|
|
18
|
+
key?: string;
|
|
19
|
+
now?: number;
|
|
20
|
+
}): Promise<string | undefined>;
|
|
124
21
|
|
|
125
22
|
/** Set on every tool call; the MCP route refuses requests carrying it, so a tool can never call the MCP endpoint. */
|
|
126
23
|
declare const DISPATCH_HEADER = "x-exposify-dispatch";
|
|
@@ -189,6 +86,29 @@ interface NextAiReadyOptions extends Omit<AgentReadyOptions, "docs"> {
|
|
|
189
86
|
fields: string[];
|
|
190
87
|
body?: "json" | "form";
|
|
191
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* The login's second step, when it asks for a code after the first (a
|
|
91
|
+
* password, then a code; or an email, then a code). Found by
|
|
92
|
+
* `withExposify` (a Server Action or POST route named like `verify`,
|
|
93
|
+
* `2fa`, `otp` that takes one code and sets the session). Name another
|
|
94
|
+
* Server Action, a route (`{ path, fields }`), or `false` for none.
|
|
95
|
+
*/
|
|
96
|
+
loginVerify?: false | string | {
|
|
97
|
+
path: string;
|
|
98
|
+
fields: string[];
|
|
99
|
+
body?: "json" | "form";
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The app's Supabase project, for Connect when the app uses Supabase Auth
|
|
103
|
+
* with `@supabase/ssr`: AI assistants sign in to it directly, and tool
|
|
104
|
+
* calls carry the session cookie `@supabase/ssr` reads. Default: read from
|
|
105
|
+
* `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`
|
|
106
|
+
* (or `NEXT_PUBLIC_SUPABASE_ANON_KEY`). `false` to not offer it.
|
|
107
|
+
*/
|
|
108
|
+
supabase?: false | {
|
|
109
|
+
url: string;
|
|
110
|
+
key: string;
|
|
111
|
+
};
|
|
192
112
|
}
|
|
193
113
|
interface NextPageOptions {
|
|
194
114
|
/** Tool name. Default: `view_` and the path, snake_cased. */
|
|
@@ -198,4 +118,4 @@ interface NextPageOptions {
|
|
|
198
118
|
type NextMcpHandler = (req: Request) => Promise<Response>;
|
|
199
119
|
declare function aiReady(manifest: ExposifyManifest, options?: NextAiReadyOptions): NextMcpHandler;
|
|
200
120
|
|
|
201
|
-
export {
|
|
121
|
+
export { AGENT_IP_HEADER, DISPATCH_HEADER, ExposifyManifest, type NextActionOptions, type NextAiReadyOptions, type NextMcpHandler, type NextPageOptions, agentIp, aiReady };
|