@exposify/next 0.0.1 → 0.0.3
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 +27 -4
- package/dist/config.cjs +8 -6
- package/dist/config.js +9 -7
- package/dist/exposify-next.js +12 -10
- package/dist/index.cjs +72 -18
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +70 -18
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ your own app.
|
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install @exposify/next
|
|
9
|
-
npx exposify
|
|
9
|
+
npx @exposify/next setup # pick the endpoints, get the route file to create
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Or by hand, in two files:
|
|
@@ -32,7 +32,7 @@ export { handler as GET, handler as POST, handler as DELETE };
|
|
|
32
32
|
build` starts, and keeps it current in dev as route files change. It holds
|
|
33
33
|
the route table, your Server Actions, your docs (`README.md`, `docs/**/*.md`) for `search_docs`,
|
|
34
34
|
and `exposify.descriptions.json`'s texts, so nothing is read from disk at
|
|
35
|
-
runtime. Commit it or ignore it; `npx exposify
|
|
35
|
+
runtime. Commit it or ignore it; `npx @exposify/next generate` writes it too.
|
|
36
36
|
|
|
37
37
|
## Which routes become tools
|
|
38
38
|
|
|
@@ -79,6 +79,22 @@ it. The MCP client's address and host are forwarded as
|
|
|
79
79
|
- **Vercel Deployment Protection** blocks those requests on protected
|
|
80
80
|
previews: set `VERCEL_AUTOMATION_BYPASS_SECRET` (Vercel's "Protection
|
|
81
81
|
Bypass for Automation") and it's sent as `x-vercel-protection-bypass`.
|
|
82
|
+
Without it, a tool call fails with an error that says so. An MCP client
|
|
83
|
+
outside Vercel reaches a protected preview with
|
|
84
|
+
`?x-vercel-protection-bypass=<secret>` on the MCP URL.
|
|
85
|
+
- **The agent's address:** some hosts (Vercel) replace `x-forwarded-for` on
|
|
86
|
+
these requests with their own. Where a route rate-limits or logs by IP,
|
|
87
|
+
use `agentIp()`: the agent's address on a tool call (signed with
|
|
88
|
+
`EXPOSIFY_KEY`, so it can't be forged), the usual `x-forwarded-for`
|
|
89
|
+
otherwise.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { agentIp } from "@exposify/next";
|
|
93
|
+
|
|
94
|
+
export async function POST(req: Request) {
|
|
95
|
+
const ip = await agentIp(req);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
82
98
|
- `basePath` and `trailingSlash` from `next.config` are applied.
|
|
83
99
|
- `rateLimit` / `rateWindowMs`: MCP requests per client address per window
|
|
84
100
|
(default 60 per 60 s), in memory and per instance.
|
|
@@ -136,6 +152,13 @@ was found. Name another one with `aiReady(exposify, { login: "signIn" })`
|
|
|
136
152
|
(an action's export name) or `{ login: { path: "/api/login", fields:
|
|
137
153
|
["email", "password"] } }`, or turn it off with `login: false`.
|
|
138
154
|
|
|
155
|
+
**No login found, and no sign-in method in the dashboard?** Then agents use
|
|
156
|
+
your app signed out: `/mcp` doesn't ask them to sign in, and your own checks
|
|
157
|
+
decide what they can do. The startup log says which it is
|
|
158
|
+
(`[agentready] How agents sign in: ...`), and so does the dashboard's "How
|
|
159
|
+
agents sign in" page. Adding a sign-in method there takes effect within a
|
|
160
|
+
minute.
|
|
161
|
+
|
|
139
162
|
Not supported yet: logins in more than one step (SMS codes, email links),
|
|
140
163
|
and logins that need a CSRF token from your own form page.
|
|
141
164
|
|
|
@@ -279,8 +302,8 @@ errors; the thrown message stays in your server log.
|
|
|
279
302
|
|
|
280
303
|
## CLI
|
|
281
304
|
|
|
282
|
-
`npx exposify
|
|
283
|
-
are the same as
|
|
305
|
+
`npx @exposify/next setup | inspect | describe | generate`. The first three
|
|
306
|
+
are the same as `@exposify/express`'s; the scan reads your route files
|
|
284
307
|
instead of booting the app.
|
|
285
308
|
|
|
286
309
|
Server Actions get their own rows:
|
package/dist/config.cjs
CHANGED
|
@@ -1894,7 +1894,7 @@ function specifier(outFile, file) {
|
|
|
1894
1894
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1895
1895
|
}
|
|
1896
1896
|
function manifestLogin(l) {
|
|
1897
|
-
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {} } : { kind: "route", path: l.path, fields: l.fields, body: l.body };
|
|
1897
|
+
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {}, where: `${l.export} in ${l.file}` } : { kind: "route", path: l.path, fields: l.fields, body: l.body, where: `POST ${l.path} in ${l.file}` };
|
|
1898
1898
|
}
|
|
1899
1899
|
function renderManifest(d, opts) {
|
|
1900
1900
|
const ts = opts.outFile.endsWith(".ts");
|
|
@@ -1930,9 +1930,9 @@ function renderManifest(d, opts) {
|
|
|
1930
1930
|
return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
|
|
1931
1931
|
});
|
|
1932
1932
|
const lines = [
|
|
1933
|
-
"// Generated by exposify
|
|
1933
|
+
"// Generated by @exposify/next from your route files. Don't edit: withExposify()",
|
|
1934
1934
|
"// in next.config regenerates it on `next dev` and `next build`, or run",
|
|
1935
|
-
"// `npx exposify
|
|
1935
|
+
"// `npx @exposify/next generate`.",
|
|
1936
1936
|
"/* eslint-disable */",
|
|
1937
1937
|
...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
|
|
1938
1938
|
`export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
|
|
@@ -1946,6 +1946,7 @@ function renderManifest(d, opts) {
|
|
|
1946
1946
|
...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
|
|
1947
1947
|
...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
|
|
1948
1948
|
...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
|
|
1949
|
+
...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
|
|
1949
1950
|
` docs: ${j(opts.docs)},`,
|
|
1950
1951
|
...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
|
|
1951
1952
|
"};",
|
|
@@ -1964,14 +1965,15 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
1964
1965
|
descriptions: readDescriptions(root, warn),
|
|
1965
1966
|
info,
|
|
1966
1967
|
...opts.basePath ? { basePath: opts.basePath } : {},
|
|
1967
|
-
...opts.trailingSlash ? { trailingSlash: true } : {}
|
|
1968
|
+
...opts.trailingSlash ? { trailingSlash: true } : {},
|
|
1969
|
+
authStacks: (0, import_core2.authStacksOf)((0, import_core2.dependencyNames)(pkg)).map((s) => s.pkg)
|
|
1968
1970
|
});
|
|
1969
1971
|
for (const a of discovery.actions.filter((x) => x.proposed?.status === "stale")) {
|
|
1970
|
-
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until
|
|
1972
|
+
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`@exposify/next describe\` reviews it again`);
|
|
1971
1973
|
}
|
|
1972
1974
|
if (discovery.actions.length > 0 && !discovery.mcpPath) {
|
|
1973
1975
|
warn(
|
|
1974
|
-
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx exposify
|
|
1976
|
+
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx @exposify/next setup)`
|
|
1975
1977
|
);
|
|
1976
1978
|
}
|
|
1977
1979
|
const file = (0, import_node_path6.join)(root, outFile);
|
package/dist/config.js
CHANGED
|
@@ -14,7 +14,7 @@ import { DESCRIPTIONS_FILE as DESCRIPTIONS_FILE2 } from "@exposify/core";
|
|
|
14
14
|
import { createHash } from "crypto";
|
|
15
15
|
import { existsSync as existsSync2, readFileSync as readFileSync6, readdirSync, writeFileSync } from "fs";
|
|
16
16
|
import { dirname as dirname2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
17
|
-
import { DESCRIPTIONS_FILE, parseDescriptions } from "@exposify/core";
|
|
17
|
+
import { DESCRIPTIONS_FILE, authStacksOf, dependencyNames, parseDescriptions } from "@exposify/core";
|
|
18
18
|
import { globSync } from "tinyglobby";
|
|
19
19
|
|
|
20
20
|
// src/ast.ts
|
|
@@ -1827,7 +1827,7 @@ function specifier(outFile, file) {
|
|
|
1827
1827
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1828
1828
|
}
|
|
1829
1829
|
function manifestLogin(l) {
|
|
1830
|
-
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {} } : { kind: "route", path: l.path, fields: l.fields, body: l.body };
|
|
1830
|
+
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {}, where: `${l.export} in ${l.file}` } : { kind: "route", path: l.path, fields: l.fields, body: l.body, where: `POST ${l.path} in ${l.file}` };
|
|
1831
1831
|
}
|
|
1832
1832
|
function renderManifest(d, opts) {
|
|
1833
1833
|
const ts = opts.outFile.endsWith(".ts");
|
|
@@ -1863,9 +1863,9 @@ function renderManifest(d, opts) {
|
|
|
1863
1863
|
return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
|
|
1864
1864
|
});
|
|
1865
1865
|
const lines = [
|
|
1866
|
-
"// Generated by exposify
|
|
1866
|
+
"// Generated by @exposify/next from your route files. Don't edit: withExposify()",
|
|
1867
1867
|
"// in next.config regenerates it on `next dev` and `next build`, or run",
|
|
1868
|
-
"// `npx exposify
|
|
1868
|
+
"// `npx @exposify/next generate`.",
|
|
1869
1869
|
"/* eslint-disable */",
|
|
1870
1870
|
...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
|
|
1871
1871
|
`export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
|
|
@@ -1879,6 +1879,7 @@ function renderManifest(d, opts) {
|
|
|
1879
1879
|
...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
|
|
1880
1880
|
...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
|
|
1881
1881
|
...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
|
|
1882
|
+
...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
|
|
1882
1883
|
` docs: ${j(opts.docs)},`,
|
|
1883
1884
|
...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
|
|
1884
1885
|
"};",
|
|
@@ -1897,14 +1898,15 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
1897
1898
|
descriptions: readDescriptions(root, warn),
|
|
1898
1899
|
info,
|
|
1899
1900
|
...opts.basePath ? { basePath: opts.basePath } : {},
|
|
1900
|
-
...opts.trailingSlash ? { trailingSlash: true } : {}
|
|
1901
|
+
...opts.trailingSlash ? { trailingSlash: true } : {},
|
|
1902
|
+
authStacks: authStacksOf(dependencyNames(pkg)).map((s) => s.pkg)
|
|
1901
1903
|
});
|
|
1902
1904
|
for (const a of discovery.actions.filter((x) => x.proposed?.status === "stale")) {
|
|
1903
|
-
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until
|
|
1905
|
+
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`@exposify/next describe\` reviews it again`);
|
|
1904
1906
|
}
|
|
1905
1907
|
if (discovery.actions.length > 0 && !discovery.mcpPath) {
|
|
1906
1908
|
warn(
|
|
1907
|
-
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx exposify
|
|
1909
|
+
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx @exposify/next setup)`
|
|
1908
1910
|
);
|
|
1909
1911
|
}
|
|
1910
1912
|
const file = join6(root, outFile);
|
package/dist/exposify-next.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runCli } from "@exposify/cli";
|
|
|
8
8
|
import { createHash } from "crypto";
|
|
9
9
|
import { existsSync as existsSync2, readFileSync as readFileSync6, readdirSync, writeFileSync } from "fs";
|
|
10
10
|
import { dirname as dirname2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
11
|
-
import { DESCRIPTIONS_FILE, parseDescriptions } from "@exposify/core";
|
|
11
|
+
import { DESCRIPTIONS_FILE, authStacksOf, dependencyNames, parseDescriptions } from "@exposify/core";
|
|
12
12
|
import { globSync } from "tinyglobby";
|
|
13
13
|
|
|
14
14
|
// src/ast.ts
|
|
@@ -1925,7 +1925,7 @@ function specifier(outFile, file) {
|
|
|
1925
1925
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1926
1926
|
}
|
|
1927
1927
|
function manifestLogin(l) {
|
|
1928
|
-
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {} } : { kind: "route", path: l.path, fields: l.fields, body: l.body };
|
|
1928
|
+
return l.kind === "action" ? { kind: "action", key: l.key, fields: l.fields, ...l.host ? { host: l.host } : {}, where: `${l.export} in ${l.file}` } : { kind: "route", path: l.path, fields: l.fields, body: l.body, where: `POST ${l.path} in ${l.file}` };
|
|
1929
1929
|
}
|
|
1930
1930
|
function renderManifest(d, opts) {
|
|
1931
1931
|
const ts = opts.outFile.endsWith(".ts");
|
|
@@ -1961,9 +1961,9 @@ function renderManifest(d, opts) {
|
|
|
1961
1961
|
return ` { name: ${j(p.name)}, path: ${j(p.path)}, params: ${j(p.params)}, file: ${j(p.file)}${extra} },`;
|
|
1962
1962
|
});
|
|
1963
1963
|
const lines = [
|
|
1964
|
-
"// Generated by exposify
|
|
1964
|
+
"// Generated by @exposify/next from your route files. Don't edit: withExposify()",
|
|
1965
1965
|
"// in next.config regenerates it on `next dev` and `next build`, or run",
|
|
1966
|
-
"// `npx exposify
|
|
1966
|
+
"// `npx @exposify/next generate`.",
|
|
1967
1967
|
"/* eslint-disable */",
|
|
1968
1968
|
...ts ? ['import type { ExposifyManifest } from "@exposify/next";', ""] : ["", '/** @type {import("@exposify/next").ExposifyManifest} */'],
|
|
1969
1969
|
`export const exposify${ts ? ": ExposifyManifest" : ""} = {`,
|
|
@@ -1977,6 +1977,7 @@ function renderManifest(d, opts) {
|
|
|
1977
1977
|
...actions.length > 0 ? [" actions: [", ...actions, " ],"] : [],
|
|
1978
1978
|
...pages.length > 0 ? [" pages: [", ...pages, " ],"] : [],
|
|
1979
1979
|
...d.login ? [` login: ${j(manifestLogin(d.login))},`] : [],
|
|
1980
|
+
...opts.authStacks?.length ? [` authStacks: ${j(opts.authStacks)},`] : [],
|
|
1980
1981
|
` docs: ${j(opts.docs)},`,
|
|
1981
1982
|
...Object.keys(opts.descriptions).length > 0 ? [` descriptions: ${JSON.stringify(opts.descriptions, null, 2).replace(/\n/g, "\n ")},`] : [],
|
|
1982
1983
|
"};",
|
|
@@ -1995,14 +1996,15 @@ function generate(root, opts = {}, warn = (m) => console.warn(`[exposify] ${m}`)
|
|
|
1995
1996
|
descriptions: readDescriptions(root, warn),
|
|
1996
1997
|
info,
|
|
1997
1998
|
...opts.basePath ? { basePath: opts.basePath } : {},
|
|
1998
|
-
...opts.trailingSlash ? { trailingSlash: true } : {}
|
|
1999
|
+
...opts.trailingSlash ? { trailingSlash: true } : {},
|
|
2000
|
+
authStacks: authStacksOf(dependencyNames(pkg)).map((s) => s.pkg)
|
|
1999
2001
|
});
|
|
2000
2002
|
for (const a of discovery.actions.filter((x) => x.proposed?.status === "stale")) {
|
|
2001
|
-
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until
|
|
2003
|
+
warn(`the input accepted for the ${a.action.name} server action (${a.file}) is out of date, since the action changed: it's not used until \`@exposify/next describe\` reviews it again`);
|
|
2002
2004
|
}
|
|
2003
2005
|
if (discovery.actions.length > 0 && !discovery.mcpPath) {
|
|
2004
2006
|
warn(
|
|
2005
|
-
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx exposify
|
|
2007
|
+
discovery.mcpRoute ? `found ${discovery.actions.length} Server Action(s), but the MCP route (${discovery.mcpRoute.file}) isn't a catch-all route, which they're served under: move it to app/mcp/[[...transport]]/route.ts` : `found ${discovery.actions.length} Server Action(s), but no MCP route to serve them under: create app/mcp/[[...transport]]/route.ts (npx @exposify/next setup)`
|
|
2006
2008
|
);
|
|
2007
2009
|
}
|
|
2008
2010
|
const file = join6(root, outFile);
|
|
@@ -2385,7 +2387,7 @@ function findNextRoot(start) {
|
|
|
2385
2387
|
}
|
|
2386
2388
|
}
|
|
2387
2389
|
var nextAdapter = {
|
|
2388
|
-
bin: "exposify
|
|
2390
|
+
bin: "npx @exposify/next",
|
|
2389
2391
|
entryExample: "apps/web",
|
|
2390
2392
|
entryHelp: "The Next.js project directory",
|
|
2391
2393
|
locate(cwd, entryFlag) {
|
|
@@ -2589,7 +2591,7 @@ function applyReplace(site, config, entries, usesZod, relFile) {
|
|
|
2589
2591
|
}
|
|
2590
2592
|
|
|
2591
2593
|
// bin/exposify-next.ts
|
|
2592
|
-
var GENERATE_USAGE = `Usage: exposify
|
|
2594
|
+
var GENERATE_USAGE = `Usage: @exposify/next generate [options]
|
|
2593
2595
|
|
|
2594
2596
|
Writes exposify.generated.ts (the route table and docs the MCP route uses).
|
|
2595
2597
|
withExposify() in next.config runs this on \`next dev\` and \`next build\`.
|
|
@@ -2636,7 +2638,7 @@ ${GENERATE_USAGE}`);
|
|
|
2636
2638
|
console.log(
|
|
2637
2639
|
`${result.changed ? "Wrote" : "Unchanged:"} ${relative4(cwd, result.file) || result.file}: ${app} App Router route file${app === 1 ? "" : "s"}, ${routes.length - app} Pages Router API file${routes.length - app === 1 ? "" : "s"}.`
|
|
2638
2640
|
);
|
|
2639
|
-
if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx exposify
|
|
2641
|
+
if (!result.discovery.mcpRoute) console.log("No MCP route yet: run `npx @exposify/next setup`.");
|
|
2640
2642
|
return 0;
|
|
2641
2643
|
}
|
|
2642
2644
|
var argv = process.argv.slice(2);
|
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()}.`;
|
|
@@ -833,7 +878,7 @@ function loginSpec(manifest, config, warnings) {
|
|
|
833
878
|
if (config === void 0) return manifest.login ?? null;
|
|
834
879
|
if (typeof config === "object") {
|
|
835
880
|
const fields2 = config.fields.map((name) => ({ name, kind: (0, import_core.fieldKind)(name) }));
|
|
836
|
-
return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json" };
|
|
881
|
+
return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json", where: `POST ${config.path}` };
|
|
837
882
|
}
|
|
838
883
|
const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
|
|
839
884
|
if (!action) {
|
|
@@ -841,7 +886,7 @@ function loginSpec(manifest, config, warnings) {
|
|
|
841
886
|
return null;
|
|
842
887
|
}
|
|
843
888
|
const fields = action.fields.map((f) => ({ name: f.name, kind: (0, import_core.fieldKind)(f.name) }));
|
|
844
|
-
return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {} };
|
|
889
|
+
return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {}, where: `${action.export} in ${action.file}` };
|
|
845
890
|
}
|
|
846
891
|
async function actionLoginResult(raw, exportName) {
|
|
847
892
|
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." };
|
|
@@ -959,7 +1004,7 @@ function aiReady(manifest, options = {}) {
|
|
|
959
1004
|
(0, import_core.formatWarning)(
|
|
960
1005
|
LOG_PREFIX,
|
|
961
1006
|
`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
|
|
1007
|
+
"check that exposify.generated.ts is current (withExposify() in next.config, or `npx @exposify/next generate`)"
|
|
963
1008
|
)
|
|
964
1009
|
);
|
|
965
1010
|
return;
|
|
@@ -973,7 +1018,7 @@ function aiReady(manifest, options = {}) {
|
|
|
973
1018
|
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
1019
|
if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
|
|
975
1020
|
if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
|
|
976
|
-
const e =
|
|
1021
|
+
const e = env3();
|
|
977
1022
|
if (e.VERCEL && e.VERCEL_ENV !== "production" && !e.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
978
1023
|
details.push("on a protected Vercel preview, tool calls need VERCEL_AUTOMATION_BYPASS_SECRET");
|
|
979
1024
|
}
|
|
@@ -983,7 +1028,7 @@ function aiReady(manifest, options = {}) {
|
|
|
983
1028
|
const selfFetch = async (request, mcpReq, route) => {
|
|
984
1029
|
const target = new URL(request.url);
|
|
985
1030
|
const incoming = new URL(mcpReq.url);
|
|
986
|
-
const origin = options.baseUrl ??
|
|
1031
|
+
const origin = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? incoming.origin;
|
|
987
1032
|
let path = `${manifest.basePath ?? ""}${target.pathname}`;
|
|
988
1033
|
if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
|
|
989
1034
|
const headers = new Headers(request.headers);
|
|
@@ -994,8 +1039,11 @@ function aiReady(manifest, options = {}) {
|
|
|
994
1039
|
}
|
|
995
1040
|
headers.set("x-forwarded-proto", mcpReq.headers.get("x-forwarded-proto") ?? incoming.protocol.slice(0, -1));
|
|
996
1041
|
headers.set("x-forwarded-host", mcpReq.headers.get("x-forwarded-host") ?? mcpReq.headers.get("host") ?? incoming.host);
|
|
997
|
-
const bypass =
|
|
1042
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
998
1043
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1044
|
+
const signedIp = await signAgentIp(clientIp(mcpReq.headers));
|
|
1045
|
+
if (signedIp) headers.set(AGENT_IP_HEADER, signedIp);
|
|
1046
|
+
else headers.delete(AGENT_IP_HEADER);
|
|
999
1047
|
const body = request.body ? await request.text() : void 0;
|
|
1000
1048
|
const actionKey = actionUrl?.exec(target.pathname)?.[1];
|
|
1001
1049
|
if (actionKey !== void 0) {
|
|
@@ -1043,13 +1091,13 @@ function aiReady(manifest, options = {}) {
|
|
|
1043
1091
|
fields: spec.fields,
|
|
1044
1092
|
signedOutPaths: cfg.signedOutPaths ?? [],
|
|
1045
1093
|
attempt: async (values, ctx) => {
|
|
1046
|
-
const target = options.baseUrl ??
|
|
1094
|
+
const target = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
|
|
1047
1095
|
const pub = new URL(ctx.origin);
|
|
1048
1096
|
const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
|
|
1049
1097
|
if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
|
|
1050
1098
|
const ua = req.headers.get("user-agent");
|
|
1051
1099
|
if (ua) headers.set("user-agent", ua);
|
|
1052
|
-
const bypass =
|
|
1100
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
1053
1101
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1054
1102
|
const pathOf = (p) => {
|
|
1055
1103
|
let path = `${manifest.basePath ?? ""}${p}`;
|
|
@@ -1149,14 +1197,18 @@ function aiReady(manifest, options = {}) {
|
|
|
1149
1197
|
const url = (0, import_core.publicRequestUrl)(req.url, req.headers);
|
|
1150
1198
|
const isPrm = url.endsWith(PRM_SUFFIX);
|
|
1151
1199
|
const resource = brokerGate.resourceFor(isPrm ? url.slice(0, -PRM_SUFFIX.length) : url);
|
|
1152
|
-
await (0, import_core.reportAppLogin)(cfg.auth, resource, login);
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1200
|
+
const verdict = await (0, import_core.reportAppLogin)(cfg.auth, resource, login, { where: login?.where, stacks: manifest.authStacks, log: logReady });
|
|
1201
|
+
if (verdict === "open") {
|
|
1202
|
+
if (isPrm) return new Response("not found", { status: 404 });
|
|
1203
|
+
} else {
|
|
1204
|
+
if (isPrm) return brokerGate.metadataResponse(req.method, resource);
|
|
1205
|
+
try {
|
|
1206
|
+
const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
|
|
1207
|
+
if (!result.ok) return result.response;
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
console.warn(`${LOG_PREFIX} broker check failed:`, err);
|
|
1210
|
+
return new Response("authorization check failed", { status: 503 });
|
|
1211
|
+
}
|
|
1160
1212
|
}
|
|
1161
1213
|
}
|
|
1162
1214
|
return core(req);
|
|
@@ -1164,7 +1216,9 @@ function aiReady(manifest, options = {}) {
|
|
|
1164
1216
|
}
|
|
1165
1217
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1166
1218
|
0 && (module.exports = {
|
|
1219
|
+
AGENT_IP_HEADER,
|
|
1167
1220
|
DISPATCH_HEADER,
|
|
1221
|
+
agentIp,
|
|
1168
1222
|
aiReady,
|
|
1169
1223
|
completeMcpLogin,
|
|
1170
1224
|
createHostedAuthResolver,
|
package/dist/index.d.cts
CHANGED
|
@@ -85,7 +85,7 @@ interface ManifestPage {
|
|
|
85
85
|
* this request, for Connect with no code. `host` is the page with the
|
|
86
86
|
* action's form, which tool calls redirected to mean "signed out".
|
|
87
87
|
*/
|
|
88
|
-
type ManifestLogin = {
|
|
88
|
+
type ManifestLogin = ({
|
|
89
89
|
kind: "action";
|
|
90
90
|
key: string;
|
|
91
91
|
fields: LoginField[];
|
|
@@ -95,6 +95,9 @@ type ManifestLogin = {
|
|
|
95
95
|
path: string;
|
|
96
96
|
fields: LoginField[];
|
|
97
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;
|
|
98
101
|
};
|
|
99
102
|
interface ExposifyManifest {
|
|
100
103
|
info: {
|
|
@@ -111,12 +114,30 @@ interface ExposifyManifest {
|
|
|
111
114
|
actions?: ManifestAction[];
|
|
112
115
|
pages?: ManifestPage[];
|
|
113
116
|
login?: ManifestLogin;
|
|
117
|
+
/** Auth libraries in package.json ("next-auth", "@supabase/ssr", ...), reported with the login. */
|
|
118
|
+
authStacks?: string[];
|
|
114
119
|
/** Docs for search_docs, embedded so they work without a file system. */
|
|
115
120
|
docs: DocSource[];
|
|
116
121
|
/** exposify.descriptions.json's texts, "METHOD /path" -> text. */
|
|
117
122
|
descriptions?: Record<string, string>;
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/** Header carrying `<ip>;<unix seconds>;<signature>` on a tool call. */
|
|
126
|
+
declare const AGENT_IP_HEADER = "x-exposify-agent-ip";
|
|
127
|
+
/**
|
|
128
|
+
* The address of whoever made this request: on a tool call, the AI agent's
|
|
129
|
+
* (verified with EXPOSIFY_KEY, so it can't be forged); otherwise the
|
|
130
|
+
* platform's `x-forwarded-for`, as you'd read it anyway. Use it where a
|
|
131
|
+
* route rate-limits or logs by IP: on Vercel, a tool call's own
|
|
132
|
+
* `x-forwarded-for` is the function's address, not the agent's.
|
|
133
|
+
*/
|
|
134
|
+
declare function agentIp(request: {
|
|
135
|
+
headers: Headers;
|
|
136
|
+
}, options?: {
|
|
137
|
+
key?: string;
|
|
138
|
+
now?: number;
|
|
139
|
+
}): Promise<string | undefined>;
|
|
140
|
+
|
|
120
141
|
/** Set on every tool call; the MCP route refuses requests carrying it, so a tool can never call the MCP endpoint. */
|
|
121
142
|
declare const DISPATCH_HEADER = "x-exposify-dispatch";
|
|
122
143
|
type InputShape = NonNullable<Extract<RouteMarker, {
|
|
@@ -193,4 +214,4 @@ interface NextPageOptions {
|
|
|
193
214
|
type NextMcpHandler = (req: Request) => Promise<Response>;
|
|
194
215
|
declare function aiReady(manifest: ExposifyManifest, options?: NextAiReadyOptions): NextMcpHandler;
|
|
195
216
|
|
|
196
|
-
export { type AppRouteEntry, DISPATCH_HEADER, type ExposifyManifest, type ManifestAction, type ManifestLogin, type ManifestPage, type ManifestRoute, type NextActionOptions, type NextAiReadyOptions, type NextMcpHandler, type NextPageOptions, type PagesRouteEntry, aiReady };
|
|
217
|
+
export { AGENT_IP_HEADER, type AppRouteEntry, DISPATCH_HEADER, type ExposifyManifest, type ManifestAction, type ManifestLogin, type ManifestPage, type ManifestRoute, type NextActionOptions, type NextAiReadyOptions, type NextMcpHandler, type NextPageOptions, type PagesRouteEntry, agentIp, aiReady };
|
package/dist/index.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ interface ManifestPage {
|
|
|
85
85
|
* this request, for Connect with no code. `host` is the page with the
|
|
86
86
|
* action's form, which tool calls redirected to mean "signed out".
|
|
87
87
|
*/
|
|
88
|
-
type ManifestLogin = {
|
|
88
|
+
type ManifestLogin = ({
|
|
89
89
|
kind: "action";
|
|
90
90
|
key: string;
|
|
91
91
|
fields: LoginField[];
|
|
@@ -95,6 +95,9 @@ type ManifestLogin = {
|
|
|
95
95
|
path: string;
|
|
96
96
|
fields: LoginField[];
|
|
97
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;
|
|
98
101
|
};
|
|
99
102
|
interface ExposifyManifest {
|
|
100
103
|
info: {
|
|
@@ -111,12 +114,30 @@ interface ExposifyManifest {
|
|
|
111
114
|
actions?: ManifestAction[];
|
|
112
115
|
pages?: ManifestPage[];
|
|
113
116
|
login?: ManifestLogin;
|
|
117
|
+
/** Auth libraries in package.json ("next-auth", "@supabase/ssr", ...), reported with the login. */
|
|
118
|
+
authStacks?: string[];
|
|
114
119
|
/** Docs for search_docs, embedded so they work without a file system. */
|
|
115
120
|
docs: DocSource[];
|
|
116
121
|
/** exposify.descriptions.json's texts, "METHOD /path" -> text. */
|
|
117
122
|
descriptions?: Record<string, string>;
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/** Header carrying `<ip>;<unix seconds>;<signature>` on a tool call. */
|
|
126
|
+
declare const AGENT_IP_HEADER = "x-exposify-agent-ip";
|
|
127
|
+
/**
|
|
128
|
+
* The address of whoever made this request: on a tool call, the AI agent's
|
|
129
|
+
* (verified with EXPOSIFY_KEY, so it can't be forged); otherwise the
|
|
130
|
+
* platform's `x-forwarded-for`, as you'd read it anyway. Use it where a
|
|
131
|
+
* route rate-limits or logs by IP: on Vercel, a tool call's own
|
|
132
|
+
* `x-forwarded-for` is the function's address, not the agent's.
|
|
133
|
+
*/
|
|
134
|
+
declare function agentIp(request: {
|
|
135
|
+
headers: Headers;
|
|
136
|
+
}, options?: {
|
|
137
|
+
key?: string;
|
|
138
|
+
now?: number;
|
|
139
|
+
}): Promise<string | undefined>;
|
|
140
|
+
|
|
120
141
|
/** Set on every tool call; the MCP route refuses requests carrying it, so a tool can never call the MCP endpoint. */
|
|
121
142
|
declare const DISPATCH_HEADER = "x-exposify-dispatch";
|
|
122
143
|
type InputShape = NonNullable<Extract<RouteMarker, {
|
|
@@ -193,4 +214,4 @@ interface NextPageOptions {
|
|
|
193
214
|
type NextMcpHandler = (req: Request) => Promise<Response>;
|
|
194
215
|
declare function aiReady(manifest: ExposifyManifest, options?: NextAiReadyOptions): NextMcpHandler;
|
|
195
216
|
|
|
196
|
-
export { type AppRouteEntry, DISPATCH_HEADER, type ExposifyManifest, type ManifestAction, type ManifestLogin, type ManifestPage, type ManifestRoute, type NextActionOptions, type NextAiReadyOptions, type NextMcpHandler, type NextPageOptions, type PagesRouteEntry, aiReady };
|
|
217
|
+
export { AGENT_IP_HEADER, type AppRouteEntry, DISPATCH_HEADER, type ExposifyManifest, type ManifestAction, type ManifestLogin, type ManifestPage, type ManifestRoute, type NextActionOptions, type NextAiReadyOptions, type NextMcpHandler, type NextPageOptions, type PagesRouteEntry, agentIp, aiReady };
|
package/dist/index.js
CHANGED
|
@@ -638,6 +638,49 @@ async function readPageResponse(res, url) {
|
|
|
638
638
|
return Response.json(body);
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
// src/agent-ip.ts
|
|
642
|
+
var AGENT_IP_HEADER = "x-exposify-agent-ip";
|
|
643
|
+
var MAX_AGE_S = 300;
|
|
644
|
+
function env2() {
|
|
645
|
+
return globalThis.process?.env ?? {};
|
|
646
|
+
}
|
|
647
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
648
|
+
function hmacKey(secret) {
|
|
649
|
+
let key = keyCache.get(secret);
|
|
650
|
+
if (!key) {
|
|
651
|
+
key = (async () => {
|
|
652
|
+
const material = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`exposify agent-ip v1
|
|
653
|
+
${secret}`));
|
|
654
|
+
return crypto.subtle.importKey("raw", material, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
655
|
+
})();
|
|
656
|
+
keyCache.set(secret, key);
|
|
657
|
+
}
|
|
658
|
+
return key;
|
|
659
|
+
}
|
|
660
|
+
var hex2 = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
661
|
+
async function signAgentIp(ip, secret = env2().EXPOSIFY_KEY, now = Date.now()) {
|
|
662
|
+
if (!ip || !secret || /[;\s]/.test(ip) || ip.length > 64) return null;
|
|
663
|
+
const t = Math.floor(now / 1e3);
|
|
664
|
+
const sig = await crypto.subtle.sign("HMAC", await hmacKey(secret), new TextEncoder().encode(`${ip};${t}`));
|
|
665
|
+
return `${ip};${t};${hex2(sig)}`;
|
|
666
|
+
}
|
|
667
|
+
function forwardedIp(headers) {
|
|
668
|
+
return headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip")?.trim() || void 0;
|
|
669
|
+
}
|
|
670
|
+
async function agentIp(request, options = {}) {
|
|
671
|
+
const signed = request.headers.get(AGENT_IP_HEADER);
|
|
672
|
+
const secret = options.key ?? env2().EXPOSIFY_KEY;
|
|
673
|
+
if (signed && secret) {
|
|
674
|
+
const [ip, t, sig] = signed.split(";");
|
|
675
|
+
const age = Math.floor((options.now ?? Date.now()) / 1e3) - Number(t);
|
|
676
|
+
if (ip && sig && /^[0-9a-f]{64}$/.test(sig) && Number.isInteger(Number(t)) && age >= -60 && age <= MAX_AGE_S) {
|
|
677
|
+
const bytes = new Uint8Array(sig.match(/../g).map((b) => parseInt(b, 16)));
|
|
678
|
+
if (await crypto.subtle.verify("HMAC", await hmacKey(secret), bytes, new TextEncoder().encode(`${ip};${t}`))) return ip;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return forwardedIp(request.headers);
|
|
682
|
+
}
|
|
683
|
+
|
|
641
684
|
// src/index.ts
|
|
642
685
|
import {
|
|
643
686
|
exposed,
|
|
@@ -653,7 +696,7 @@ var LOG_PREFIX = "[agentready]";
|
|
|
653
696
|
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.";
|
|
654
697
|
var LEFT_BLANK = "A field you leave out is as if left blank on the form.";
|
|
655
698
|
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.";
|
|
656
|
-
function
|
|
699
|
+
function env3() {
|
|
657
700
|
return globalThis.process?.env ?? {};
|
|
658
701
|
}
|
|
659
702
|
function errorMessage(err) {
|
|
@@ -716,7 +759,7 @@ function derivedInput(action) {
|
|
|
716
759
|
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 } : {} }));
|
|
717
760
|
return { input: Object.fromEntries(fields.map((f) => [f.name, fieldSchema(f)])) };
|
|
718
761
|
}
|
|
719
|
-
if (reason) return { reason: action.fields.some((f) => f.type === "file") ? reason : `${reason};
|
|
762
|
+
if (reason) return { reason: action.fields.some((f) => f.type === "file") ? reason : `${reason}; \`@exposify/next describe\` can propose it for you to review` };
|
|
720
763
|
return { input: Object.fromEntries(action.fields.map((f) => [f.name, fieldSchema(f)])) };
|
|
721
764
|
}
|
|
722
765
|
var sentence = (s) => /[.!?]$/.test(s.trim()) ? s.trim() : `${s.trim()}.`;
|
|
@@ -841,7 +884,7 @@ function loginSpec(manifest, config, warnings) {
|
|
|
841
884
|
if (config === void 0) return manifest.login ?? null;
|
|
842
885
|
if (typeof config === "object") {
|
|
843
886
|
const fields2 = config.fields.map((name) => ({ name, kind: fieldKind(name) }));
|
|
844
|
-
return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json" };
|
|
887
|
+
return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json", where: `POST ${config.path}` };
|
|
845
888
|
}
|
|
846
889
|
const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
|
|
847
890
|
if (!action) {
|
|
@@ -849,7 +892,7 @@ function loginSpec(manifest, config, warnings) {
|
|
|
849
892
|
return null;
|
|
850
893
|
}
|
|
851
894
|
const fields = action.fields.map((f) => ({ name: f.name, kind: fieldKind(f.name) }));
|
|
852
|
-
return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {} };
|
|
895
|
+
return { kind: "action", key: action.key, fields, ...action.host ? { host: action.host } : {}, where: `${action.export} in ${action.file}` };
|
|
853
896
|
}
|
|
854
897
|
async function actionLoginResult(raw, exportName) {
|
|
855
898
|
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." };
|
|
@@ -967,7 +1010,7 @@ function aiReady(manifest, options = {}) {
|
|
|
967
1010
|
formatWarning(
|
|
968
1011
|
LOG_PREFIX,
|
|
969
1012
|
`MCP is up at ${endpoint} but exposes no route tools`,
|
|
970
|
-
"check that exposify.generated.ts is current (withExposify() in next.config, or `npx exposify
|
|
1013
|
+
"check that exposify.generated.ts is current (withExposify() in next.config, or `npx @exposify/next generate`)"
|
|
971
1014
|
)
|
|
972
1015
|
);
|
|
973
1016
|
return;
|
|
@@ -981,7 +1024,7 @@ function aiReady(manifest, options = {}) {
|
|
|
981
1024
|
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)`);
|
|
982
1025
|
if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
|
|
983
1026
|
if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
|
|
984
|
-
const e =
|
|
1027
|
+
const e = env3();
|
|
985
1028
|
if (e.VERCEL && e.VERCEL_ENV !== "production" && !e.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
986
1029
|
details.push("on a protected Vercel preview, tool calls need VERCEL_AUTOMATION_BYPASS_SECRET");
|
|
987
1030
|
}
|
|
@@ -991,7 +1034,7 @@ function aiReady(manifest, options = {}) {
|
|
|
991
1034
|
const selfFetch = async (request, mcpReq, route) => {
|
|
992
1035
|
const target = new URL(request.url);
|
|
993
1036
|
const incoming = new URL(mcpReq.url);
|
|
994
|
-
const origin = options.baseUrl ??
|
|
1037
|
+
const origin = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? incoming.origin;
|
|
995
1038
|
let path = `${manifest.basePath ?? ""}${target.pathname}`;
|
|
996
1039
|
if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
|
|
997
1040
|
const headers = new Headers(request.headers);
|
|
@@ -1002,8 +1045,11 @@ function aiReady(manifest, options = {}) {
|
|
|
1002
1045
|
}
|
|
1003
1046
|
headers.set("x-forwarded-proto", mcpReq.headers.get("x-forwarded-proto") ?? incoming.protocol.slice(0, -1));
|
|
1004
1047
|
headers.set("x-forwarded-host", mcpReq.headers.get("x-forwarded-host") ?? mcpReq.headers.get("host") ?? incoming.host);
|
|
1005
|
-
const bypass =
|
|
1048
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
1006
1049
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1050
|
+
const signedIp = await signAgentIp(clientIp(mcpReq.headers));
|
|
1051
|
+
if (signedIp) headers.set(AGENT_IP_HEADER, signedIp);
|
|
1052
|
+
else headers.delete(AGENT_IP_HEADER);
|
|
1007
1053
|
const body = request.body ? await request.text() : void 0;
|
|
1008
1054
|
const actionKey = actionUrl?.exec(target.pathname)?.[1];
|
|
1009
1055
|
if (actionKey !== void 0) {
|
|
@@ -1051,13 +1097,13 @@ function aiReady(manifest, options = {}) {
|
|
|
1051
1097
|
fields: spec.fields,
|
|
1052
1098
|
signedOutPaths: cfg.signedOutPaths ?? [],
|
|
1053
1099
|
attempt: async (values, ctx) => {
|
|
1054
|
-
const target = options.baseUrl ??
|
|
1100
|
+
const target = options.baseUrl ?? env3().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
|
|
1055
1101
|
const pub = new URL(ctx.origin);
|
|
1056
1102
|
const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
|
|
1057
1103
|
if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
|
|
1058
1104
|
const ua = req.headers.get("user-agent");
|
|
1059
1105
|
if (ua) headers.set("user-agent", ua);
|
|
1060
|
-
const bypass =
|
|
1106
|
+
const bypass = env3().VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
1061
1107
|
if (bypass) headers.set("x-vercel-protection-bypass", bypass);
|
|
1062
1108
|
const pathOf = (p) => {
|
|
1063
1109
|
let path = `${manifest.basePath ?? ""}${p}`;
|
|
@@ -1157,21 +1203,27 @@ function aiReady(manifest, options = {}) {
|
|
|
1157
1203
|
const url = publicRequestUrl(req.url, req.headers);
|
|
1158
1204
|
const isPrm = url.endsWith(PRM_SUFFIX);
|
|
1159
1205
|
const resource = brokerGate.resourceFor(isPrm ? url.slice(0, -PRM_SUFFIX.length) : url);
|
|
1160
|
-
await reportAppLogin(cfg.auth, resource, login);
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1206
|
+
const verdict = await reportAppLogin(cfg.auth, resource, login, { where: login?.where, stacks: manifest.authStacks, log: logReady });
|
|
1207
|
+
if (verdict === "open") {
|
|
1208
|
+
if (isPrm) return new Response("not found", { status: 404 });
|
|
1209
|
+
} else {
|
|
1210
|
+
if (isPrm) return brokerGate.metadataResponse(req.method, resource);
|
|
1211
|
+
try {
|
|
1212
|
+
const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
|
|
1213
|
+
if (!result.ok) return result.response;
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
console.warn(`${LOG_PREFIX} broker check failed:`, err);
|
|
1216
|
+
return new Response("authorization check failed", { status: 503 });
|
|
1217
|
+
}
|
|
1168
1218
|
}
|
|
1169
1219
|
}
|
|
1170
1220
|
return core(req);
|
|
1171
1221
|
};
|
|
1172
1222
|
}
|
|
1173
1223
|
export {
|
|
1224
|
+
AGENT_IP_HEADER,
|
|
1174
1225
|
DISPATCH_HEADER,
|
|
1226
|
+
agentIp,
|
|
1175
1227
|
aiReady,
|
|
1176
1228
|
completeMcpLogin,
|
|
1177
1229
|
createHostedAuthResolver,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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.2",
|
|
36
|
+
"@exposify/core": "0.3.15",
|
|
37
37
|
"tinyglobby": "^0.2.10",
|
|
38
38
|
"zod": "^3.24.0"
|
|
39
39
|
},
|