@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 CHANGED
@@ -6,7 +6,7 @@ your own app.
6
6
 
7
7
  ```sh
8
8
  npm install @exposify/next
9
- npx exposify-next setup # pick the endpoints, get the route file to create
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-next generate` writes it too.
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-next setup | inspect | describe | generate`. The first three
283
- are the same as `exposify-express`'s; the scan reads your route files
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-next from your route files. Don't edit: withExposify()",
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-next generate`.",
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 \`exposify-next describe\` reviews it again`);
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-next setup)`
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-next from your route files. Don't edit: withExposify()",
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-next generate`.",
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 \`exposify-next describe\` reviews it again`);
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-next setup)`
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);
@@ -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-next from your route files. Don't edit: withExposify()",
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-next generate`.",
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 \`exposify-next describe\` reviews it again`);
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-next setup)`
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-next",
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-next generate [options]
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-next setup`.");
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 env2() {
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}; \`exposify-next describe\` can propose it for you to review` };
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-next generate`)"
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 = env2();
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 ?? env2().EXPOSIFY_BASE_URL ?? incoming.origin;
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 = env2().VERCEL_AUTOMATION_BYPASS_SECRET;
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 ?? env2().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
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 = env2().VERCEL_AUTOMATION_BYPASS_SECRET;
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 (isPrm) return brokerGate.metadataResponse(req.method, resource);
1154
- try {
1155
- const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
1156
- if (!result.ok) return result.response;
1157
- } catch (err) {
1158
- console.warn(`${LOG_PREFIX} broker check failed:`, err);
1159
- return new Response("authorization check failed", { status: 503 });
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 env2() {
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}; \`exposify-next describe\` can propose it for you to review` };
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-next generate`)"
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 = env2();
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 ?? env2().EXPOSIFY_BASE_URL ?? incoming.origin;
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 = env2().VERCEL_AUTOMATION_BYPASS_SECRET;
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 ?? env2().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
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 = env2().VERCEL_AUTOMATION_BYPASS_SECRET;
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 (isPrm) return brokerGate.metadataResponse(req.method, resource);
1162
- try {
1163
- const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
1164
- if (!result.ok) return result.response;
1165
- } catch (err) {
1166
- console.warn(`${LOG_PREFIX} broker check failed:`, err);
1167
- return new Response("authorization check failed", { status: 503 });
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.1",
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.1",
36
- "@exposify/core": "0.3.13",
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
  },