@exposify/next 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1186 @@
1
+ import {
2
+ HTTP_METHODS
3
+ } from "./chunk-D5TQF5DY.js";
4
+
5
+ // src/index.ts
6
+ import {
7
+ applyDescriptions,
8
+ applyOpenApi,
9
+ buildMcpServer,
10
+ buildRequest,
11
+ countTools,
12
+ createBrokerGate,
13
+ createOAuthGate,
14
+ publicRequestUrl,
15
+ createWebMcpHandler,
16
+ cookieChanges,
17
+ describeAuth,
18
+ errorMessageOf,
19
+ fieldKind,
20
+ handleLoginPage,
21
+ hostedResolverInfo,
22
+ loginResultFromResponse,
23
+ reportAppLogin,
24
+ samePath,
25
+ setCookieLines,
26
+ SIGNED_OUT_HEADER,
27
+ matchesPathGlob,
28
+ formatFailed,
29
+ formatReady,
30
+ formatWarning,
31
+ getMarker,
32
+ KnowledgeBase,
33
+ parseMethodEntry,
34
+ resolveOptions,
35
+ routeToolsPhrase,
36
+ startupLogEnabled
37
+ } from "@exposify/core";
38
+ import { z } from "zod";
39
+
40
+ // src/actions.ts
41
+ var AUTH_LIKE = /^(log_?in|log_?out|sign_?in|sign_?out|sign_?up|register|authenticate)(_|$)/;
42
+ var looksLikeAuth = (action) => Boolean(action.setsCookies) || AUTH_LIKE.test(action.key);
43
+ function inputProblem(action) {
44
+ if (action.unresolved && action.unresolved.length > 0) return `its input couldn't all be worked out from its code (${action.unresolved.join("; ")})`;
45
+ if (action.readsAll && !action.forms?.length) return "it reads the whole form (Object.fromEntries or a loop over it) and no form that submits to it was found, so nothing says which fields exist";
46
+ const files = action.fields.filter((f) => f.type === "file").map((f) => f.name);
47
+ if (files.length > 0) return `it takes a file (${files.join(", ")}), which agents can't send yet`;
48
+ return void 0;
49
+ }
50
+ var ACTION_SECRET_HEADER = "x-exposify-action";
51
+ var SECRET_KEY = /* @__PURE__ */ Symbol.for("exposify.next.actionSecret");
52
+ var RENDERED_UI = "[rendered UI omitted]";
53
+ function env() {
54
+ return globalThis.process?.env ?? {};
55
+ }
56
+ var hex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
57
+ async function actionSecret() {
58
+ const e = env();
59
+ if (e.EXPOSIFY_ACTION_SECRET) return e.EXPOSIFY_ACTION_SECRET;
60
+ if (e.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY) {
61
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(e.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
62
+ return hex(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("exposify-action")));
63
+ }
64
+ if (e.NODE_ENV === "production") return null;
65
+ const g = globalThis;
66
+ g[SECRET_KEY] ??= hex(crypto.getRandomValues(new Uint8Array(32)).buffer);
67
+ return g[SECRET_KEY];
68
+ }
69
+ async function secretMatches(given, secret) {
70
+ if (!given) return false;
71
+ const [a, b] = await Promise.all([given, secret].map((s) => crypto.subtle.digest("SHA-256", new TextEncoder().encode(s))));
72
+ const x = new Uint8Array(a);
73
+ const y = new Uint8Array(b);
74
+ let diff = 0;
75
+ for (let i = 0; i < x.length; i++) diff |= x[i] ^ y[i];
76
+ return diff === 0;
77
+ }
78
+ function toFormData(fields, json = /* @__PURE__ */ new Set()) {
79
+ const form = new FormData();
80
+ const add = (name, value) => {
81
+ if (value === void 0 || value === null || value === false) return;
82
+ if (value === true) form.append(name, "on");
83
+ else if (typeof value === "object") form.append(name, JSON.stringify(value));
84
+ else form.append(name, String(value));
85
+ };
86
+ for (const [name, value] of Object.entries(fields)) {
87
+ if (json.has(name) && value !== null && value !== void 0) form.append(name, typeof value === "string" ? value : JSON.stringify(value));
88
+ else if (Array.isArray(value)) for (const v of value) add(name, v);
89
+ else add(name, value);
90
+ }
91
+ return form;
92
+ }
93
+ function actionArguments(params, args, json) {
94
+ const fields = { ...args };
95
+ for (const p of params) if (p.role === "input") delete fields[p.name];
96
+ return params.map((p) => {
97
+ if (p.role === "prev") return void 0;
98
+ if (p.role === "input") return args[p.name];
99
+ if (p.role === "formData") return toFormData(fields, json);
100
+ return fields;
101
+ });
102
+ }
103
+ function serializeResult(value) {
104
+ if (value === void 0) return JSON.stringify({ ok: true });
105
+ return JSON.stringify(value, (_key, v) => {
106
+ if (typeof v === "bigint") return v.toString();
107
+ if (v instanceof Map) return Object.fromEntries(v);
108
+ if (v instanceof Set) return [...v];
109
+ if (v && typeof v === "object" && typeof v.$$typeof === "symbol") return RENDERED_UI;
110
+ return v;
111
+ });
112
+ }
113
+ function nextControlFlow(err) {
114
+ const digest = err?.digest;
115
+ if (typeof digest !== "string") return null;
116
+ const parts = digest.split(";");
117
+ if (parts[0] === "NEXT_REDIRECT" && parts.length >= 4) return { redirect: parts.slice(2, -2).join(";") };
118
+ if (parts[0] === "NEXT_HTTP_ERROR_FALLBACK") {
119
+ const status = Number(parts[1]);
120
+ if (Number.isInteger(status)) return { status };
121
+ }
122
+ return null;
123
+ }
124
+ var ERRORS = { 401: "unauthorized", 403: "forbidden", 404: "not found" };
125
+ var jsonFieldsOf = (action) => new Set([...action.fields ?? [], ...action.proposed ?? []].filter((f) => f.type === "json").map((f) => f.name));
126
+ async function runAction(fn, action, args) {
127
+ const json = (body, status = 200) => new Response(body, { status, headers: { "content-type": "application/json" } });
128
+ try {
129
+ return json(serializeResult(await fn(...actionArguments(action.params, args, jsonFieldsOf(action)))));
130
+ } catch (err) {
131
+ const flow = nextControlFlow(err);
132
+ if (flow && "redirect" in flow) return json(JSON.stringify({ ok: true, redirectedTo: flow.redirect }));
133
+ if (flow) return json(JSON.stringify({ error: ERRORS[flow.status] ?? `HTTP ${flow.status}` }), flow.status);
134
+ console.error(`[agentready] server action ${action.export} threw:`, err);
135
+ return json(JSON.stringify({ error: "the action threw an error" }), 500);
136
+ }
137
+ }
138
+ function hostPath(pattern) {
139
+ const path = pattern.split("/").filter((seg) => !/^:[A-Za-z0-9_]+\*\?$/.test(seg)).map((seg) => seg.startsWith(":") ? "_" : seg).join("/");
140
+ return path.startsWith("/") ? path : `/${path}`;
141
+ }
142
+ var replyString = (s) => s.startsWith("$") ? `$${s}` : s;
143
+ function encodeReply(args) {
144
+ const body = new FormData();
145
+ let nextPart = 1;
146
+ const model = (value, depth) => {
147
+ if (value === void 0) return "$undefined";
148
+ if (value === null || typeof value === "boolean") return value;
149
+ if (typeof value === "string") return replyString(value);
150
+ if (typeof value === "bigint") return `$n${value.toString()}`;
151
+ if (typeof value === "number") {
152
+ if (Number.isNaN(value)) return "$NaN";
153
+ if (value === Infinity) return "$Infinity";
154
+ if (value === -Infinity) return "$-Infinity";
155
+ return Object.is(value, -0) ? "$-0" : value;
156
+ }
157
+ if (value instanceof Date) return `$D${value.toISOString()}`;
158
+ if (value instanceof FormData) {
159
+ const id = nextPart++;
160
+ for (const [name, v] of value.entries()) {
161
+ body.append(`_${id}_${name}`, v);
162
+ body.append(`${id}_${name}`, v);
163
+ }
164
+ return `$K${id.toString(16)}`;
165
+ }
166
+ if (depth > 32) return null;
167
+ if (Array.isArray(value)) return value.map((v) => model(v, depth + 1));
168
+ if (typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([, v]) => typeof v !== "function").map(([k, v]) => [k, model(v, depth + 1)]));
169
+ return null;
170
+ };
171
+ body.set("0", JSON.stringify(model(args, 0)));
172
+ return body;
173
+ }
174
+ var LENGTH_PREFIXED = /* @__PURE__ */ new Set(["T", "A", "O", "o", "U", "S", "s", "L", "l", "G", "g", "M", "m", "V", "b"]);
175
+ function flightRows(bytes) {
176
+ const rows = /* @__PURE__ */ new Map();
177
+ const decoder = new TextDecoder();
178
+ let i = 0;
179
+ while (i < bytes.length) {
180
+ const colon = bytes.indexOf(58, i);
181
+ if (colon < 0) break;
182
+ const id = decoder.decode(bytes.subarray(i, colon));
183
+ let j = colon + 1;
184
+ const first = String.fromCharCode(bytes[j] ?? 0);
185
+ let tag = "";
186
+ let text;
187
+ const comma = bytes.indexOf(44, j + 1);
188
+ const lengthHex = comma > j + 1 ? decoder.decode(bytes.subarray(j + 1, comma)) : "";
189
+ if (LENGTH_PREFIXED.has(first) && /^[0-9a-f]+$/.test(lengthHex)) {
190
+ tag = first;
191
+ const start = comma + 1;
192
+ const end = start + parseInt(lengthHex, 16);
193
+ text = decoder.decode(bytes.subarray(start, end));
194
+ i = end;
195
+ } else {
196
+ const newline = bytes.indexOf(10, j);
197
+ const end = newline < 0 ? bytes.length : newline;
198
+ if (/[A-Z]/.test(first)) {
199
+ tag = first;
200
+ j++;
201
+ }
202
+ text = decoder.decode(bytes.subarray(j, end));
203
+ i = end + 1;
204
+ }
205
+ if (/^\s*$/.test(id)) continue;
206
+ if (["D", "W", "N", "H", "J", "P", "C", "R", "r", "X", "x"].includes(tag)) continue;
207
+ rows.set(id.trim(), { tag, text });
208
+ }
209
+ return rows;
210
+ }
211
+ var FlightError = class extends Error {
212
+ constructor(digest, message) {
213
+ super(message);
214
+ this.digest = digest;
215
+ }
216
+ digest;
217
+ };
218
+ function decodeFlight(bytes, root = "0") {
219
+ return flightReviver(flightRows(bytes)).row(root);
220
+ }
221
+ function flightReviver(rows) {
222
+ const resolving = /* @__PURE__ */ new Set();
223
+ const row = (id, depth) => {
224
+ const r = rows.get(id);
225
+ if (!r || resolving.has(id) || depth > 64) return null;
226
+ if (r.tag === "T") return r.text;
227
+ if (r.tag === "E") {
228
+ let info = {};
229
+ try {
230
+ info = JSON.parse(r.text);
231
+ } catch {
232
+ }
233
+ throw new FlightError(info.digest, info.message ?? "the action threw an error");
234
+ }
235
+ if (r.tag === "I") return "[module reference]";
236
+ if (r.tag !== "") return null;
237
+ resolving.add(id);
238
+ try {
239
+ return revive(JSON.parse(r.text), depth + 1);
240
+ } finally {
241
+ resolving.delete(id);
242
+ }
243
+ };
244
+ const revive = (v, depth) => {
245
+ if (typeof v === "string") {
246
+ if (!v.startsWith("$")) return v;
247
+ if (v === "$") return "$";
248
+ const rest = v.slice(1);
249
+ if (rest.startsWith("$")) return rest;
250
+ if (rest === "undefined") return void 0;
251
+ if (rest === "NaN" || rest === "Infinity" || rest === "-Infinity" || rest === "-0") return Number(rest);
252
+ const kind = rest[0];
253
+ const ref = rest.slice(1);
254
+ switch (kind) {
255
+ case "D":
256
+ return ref;
257
+ // a Date, already as its ISO string
258
+ case "n":
259
+ return ref;
260
+ // a BigInt, as its digits
261
+ case "@":
262
+ return row(ref, depth);
263
+ case "Q": {
264
+ const entries = row(ref, depth);
265
+ return Array.isArray(entries) ? Object.fromEntries(entries.map((e) => Array.isArray(e) ? [String(e[0]), e[1]] : [String(e), null])) : {};
266
+ }
267
+ case "W":
268
+ return row(ref, depth);
269
+ case "L":
270
+ return RENDERED_UI;
271
+ case "K":
272
+ return "[FormData]";
273
+ case "S":
274
+ return `Symbol(${ref})`;
275
+ case "F":
276
+ case "h":
277
+ return "[function]";
278
+ default:
279
+ return /^[0-9a-f]+$/.test(rest) ? row(rest, depth) : v;
280
+ }
281
+ }
282
+ if (Array.isArray(v)) {
283
+ if (v[0] === "$") return RENDERED_UI;
284
+ return v.map((x) => revive(x, depth + 1));
285
+ }
286
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, revive(x, depth + 1)]));
287
+ return v;
288
+ };
289
+ return { row: (id) => row(id, 0), revive: (v) => revive(v, 0) };
290
+ }
291
+ var actionNotFound = (res) => res.status === 409 && res.headers.has("x-nextjs-action-not-found");
292
+ async function readActionResponse(res, exportName) {
293
+ const json = (body, status = 200) => new Response(body, { status, headers: { "content-type": "application/json" } });
294
+ const redirect = res.headers.get("x-action-redirect");
295
+ if (redirect) {
296
+ void res.body?.cancel();
297
+ return json(JSON.stringify({ ok: true, redirectedTo: redirect.split(";")[0] }));
298
+ }
299
+ if (!(res.headers.get("content-type") ?? "").startsWith("text/x-component")) return res;
300
+ const bytes = new Uint8Array(await res.arrayBuffer());
301
+ try {
302
+ const rootValue = decodeFlight(bytes);
303
+ return json(serializeResult(rootValue && typeof rootValue === "object" ? rootValue.a : void 0));
304
+ } catch (err) {
305
+ if (!(err instanceof FlightError)) throw err;
306
+ const flow = nextControlFlow({ digest: err.digest });
307
+ if (flow && "redirect" in flow) return json(JSON.stringify({ ok: true, redirectedTo: flow.redirect }));
308
+ if (flow) return json(JSON.stringify({ error: ERRORS[flow.status] ?? `HTTP ${flow.status}` }), flow.status);
309
+ console.error(`[agentready] server action ${exportName} threw: ${err.message}`);
310
+ return json(JSON.stringify({ error: "the action threw an error" }), 500);
311
+ }
312
+ }
313
+
314
+ // src/page-read.ts
315
+ var TEXT_BUDGET = 4e4;
316
+ var DATA_BUDGET = 16e3;
317
+ function pageOutcome(html, payload) {
318
+ const redirect = /NEXT_REDIRECT;(?:push|replace);([^;"\\]+);30[1278];/.exec(payload);
319
+ if (redirect) return { redirect: redirect[1] };
320
+ const fallback = /NEXT_HTTP_ERROR_FALLBACK;(40[134])/.exec(payload);
321
+ if (fallback) return { status: Number(fallback[1]) };
322
+ const refresh = /<meta[^>]+id="__next-page-redirect"[^>]*content="[^"]*url=([^"]+)"/i.exec(html);
323
+ if (refresh) return { redirect: decodeEntities(refresh[1]) };
324
+ return null;
325
+ }
326
+ function embeddedPayload(html) {
327
+ const parts = [];
328
+ for (const m of html.matchAll(/self\.__next_f\.push\((\[[\s\S]*?\])\)<\/script>/g)) {
329
+ try {
330
+ const v = JSON.parse(m[1]);
331
+ if (v[0] === 1 && typeof v[1] === "string") parts.push(v[1]);
332
+ } catch {
333
+ }
334
+ }
335
+ return parts.join("");
336
+ }
337
+ var NOT_DATA = /* @__PURE__ */ new Set(["children", "className", "style", "key", "ref"]);
338
+ var isElement = (v) => Array.isArray(v) && v[0] === "$";
339
+ var isReference = (v) => typeof v === "string" && /^\$(undefined|L|F|h|S|K|E|Z|Y|T|P)/.test(v);
340
+ function meaningful(v, depth = 0) {
341
+ if (v === null || v === void 0 || v === RENDERED_UI || isElement(v) || isReference(v) || depth > 40) return false;
342
+ if (typeof v === "string") return v.length > 0;
343
+ if (typeof v === "number" || typeof v === "boolean") return true;
344
+ if (Array.isArray(v)) return v.some((x) => meaningful(x, depth + 1));
345
+ if (typeof v === "object") return Object.values(v).some((x) => meaningful(x, depth + 1));
346
+ return false;
347
+ }
348
+ var holdsData = (v) => Boolean(v) && typeof v === "object" && meaningful(v);
349
+ function pageData(payload) {
350
+ if (!payload) return [];
351
+ let rows;
352
+ try {
353
+ rows = flightRows(new TextEncoder().encode(payload));
354
+ } catch {
355
+ return [];
356
+ }
357
+ const names = /* @__PURE__ */ new Map();
358
+ for (const [id, r] of rows) {
359
+ if (r.tag !== "I") continue;
360
+ try {
361
+ const v = JSON.parse(r.text);
362
+ const name = typeof v[2] === "string" ? v[2] : "";
363
+ if (name && name !== "default" && name !== "*") names.set(id, name);
364
+ } catch {
365
+ }
366
+ }
367
+ const { revive } = flightReviver(rows);
368
+ const out = [];
369
+ const seen = /* @__PURE__ */ new Set();
370
+ const visit = (v, depth) => {
371
+ if (depth > 80) return;
372
+ if (Array.isArray(v)) {
373
+ if (v[0] === "$" && typeof v[1] === "string" && v[1].startsWith("$L") && v[3] && typeof v[3] === "object") {
374
+ const props = {};
375
+ for (const [k, raw] of Object.entries(v[3])) {
376
+ if (NOT_DATA.has(k) || isElement(raw) || isReference(raw)) continue;
377
+ let value;
378
+ try {
379
+ value = revive(raw);
380
+ } catch {
381
+ continue;
382
+ }
383
+ if (holdsData(value)) props[k] = value;
384
+ }
385
+ if (Object.keys(props).length > 0) {
386
+ const component = names.get(v[1].slice(2));
387
+ const key = JSON.stringify([component, props]);
388
+ if (!seen.has(key)) {
389
+ seen.add(key);
390
+ out.push({ ...component ? { component } : {}, props });
391
+ }
392
+ }
393
+ }
394
+ for (const x of v) visit(x, depth + 1);
395
+ } else if (v && typeof v === "object") {
396
+ for (const x of Object.values(v)) visit(x, depth + 1);
397
+ }
398
+ };
399
+ for (const r of rows.values()) {
400
+ if (r.tag !== "") continue;
401
+ try {
402
+ visit(JSON.parse(r.text), 0);
403
+ } catch {
404
+ }
405
+ }
406
+ return out;
407
+ }
408
+ var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", ndash: "\u2013", mdash: "\u2014", hellip: "\u2026", rsquo: "\u2019", lsquo: "\u2018", rdquo: "\u201D", ldquo: "\u201C", copy: "\xA9", reg: "\xAE", trade: "\u2122", times: "\xD7", euro: "\u20AC", pound: "\xA3" };
409
+ function decodeEntities(s) {
410
+ return s.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (m, e) => {
411
+ if (e[0] === "#") {
412
+ const code = e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
413
+ return Number.isFinite(code) && code > 0 && code < 1114112 ? String.fromCodePoint(code) : m;
414
+ }
415
+ return ENTITIES[e.toLowerCase()] ?? m;
416
+ });
417
+ }
418
+ var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
419
+ var SKIP = /* @__PURE__ */ new Set(["script", "style", "template", "noscript", "svg", "head", "iframe", "object", "canvas", "audio", "video"]);
420
+ var CHROME = /* @__PURE__ */ new Set(["header", "nav", "footer", "aside"]);
421
+ var BLOCK = /* @__PURE__ */ new Set(["p", "div", "section", "article", "main", "ul", "ol", "li", "table", "tr", "thead", "tbody", "tfoot", "form", "fieldset", "dl", "dt", "dd", "blockquote", "pre", "figure", "figcaption", "details", "summary", "h1", "h2", "h3", "h4", "h5", "h6", "header", "footer", "nav", "aside", "label", "address", "hr"]);
422
+ function parseAttrs(src) {
423
+ const attrs = {};
424
+ for (const m of src.matchAll(/([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) {
425
+ attrs[m[1].toLowerCase()] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? "");
426
+ }
427
+ return attrs;
428
+ }
429
+ function tokenize(html) {
430
+ const out = [];
431
+ const re = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<![^>]*>|<\/?([a-zA-Z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
432
+ let last = 0;
433
+ for (let m = re.exec(html); m; m = re.exec(html)) {
434
+ if (m.index > last) out.push(html.slice(last, m.index));
435
+ last = re.lastIndex;
436
+ if (!m[1]) continue;
437
+ const name = m[1].toLowerCase();
438
+ const raw = m[2] ?? "";
439
+ out.push({ name, attrs: parseAttrs(raw), close: m[0][1] === "/", selfClosing: /\/\s*$/.test(raw) || VOID.has(name) });
440
+ if (!m[0].startsWith("</") && (name === "script" || name === "style")) {
441
+ const end = html.toLowerCase().indexOf(`</${name}`, last);
442
+ last = end < 0 ? html.length : end;
443
+ re.lastIndex = last;
444
+ }
445
+ }
446
+ if (last < html.length) out.push(html.slice(last));
447
+ return out;
448
+ }
449
+ var hidden = (t) => "hidden" in t.attrs || t.attrs["aria-hidden"] === "true" || /display\s*:\s*none|visibility\s*:\s*hidden/i.test(t.attrs.style ?? "") || t.name === "input" && t.attrs.type === "hidden";
450
+ function htmlToText(html) {
451
+ const tokens = tokenize(html);
452
+ const hasMain = tokens.some((t) => typeof t !== "string" && t.name === "main" && !t.close);
453
+ const lines = [];
454
+ let line = "";
455
+ const flush = (blank = true) => {
456
+ const l = line.replace(/[ \t]+/g, " ").trim();
457
+ if (l) lines.push(l);
458
+ else if (blank && lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
459
+ line = "";
460
+ };
461
+ const emit = (s) => {
462
+ line += s;
463
+ };
464
+ const stack = [];
465
+ let skipDepth = -1;
466
+ let inMain = !hasMain;
467
+ let mainDepth = -1;
468
+ let inBody = false;
469
+ const listCounters = [];
470
+ let link = null;
471
+ let select = null;
472
+ for (const tok of tokens) {
473
+ if (typeof tok === "string") {
474
+ if (skipDepth >= 0 || !inMain || !inBody) continue;
475
+ const text = decodeEntities(tok).replace(/\s+/g, " ");
476
+ if (select?.current) select.current.text += text;
477
+ else if (!select) emit(text);
478
+ continue;
479
+ }
480
+ const t = tok;
481
+ if (t.name === "body") {
482
+ inBody = !t.close;
483
+ continue;
484
+ }
485
+ if (t.close) {
486
+ const at = stack.lastIndexOf(t.name);
487
+ if (at < 0) continue;
488
+ stack.length = at;
489
+ if (skipDepth >= 0 && stack.length <= skipDepth) {
490
+ skipDepth = -1;
491
+ continue;
492
+ }
493
+ if (skipDepth >= 0) continue;
494
+ if (hasMain && t.name === "main" && stack.length <= mainDepth) {
495
+ flush();
496
+ inMain = false;
497
+ mainDepth = -1;
498
+ continue;
499
+ }
500
+ if (!inMain) continue;
501
+ if (t.name === "a" && link) {
502
+ if (lines.length > link.lines) {
503
+ flush();
504
+ if (link.href) lines.push(`\u2192 ${link.href}`);
505
+ } else {
506
+ const text = line.slice(link.start).trim();
507
+ if (link.href && text && text !== link.href) line = `${line.slice(0, link.start)}[${text}](${link.href})`;
508
+ else if (link.href && !text) emit(` [link](${link.href})`);
509
+ }
510
+ link = null;
511
+ } else if (t.name === "option" && select?.current) {
512
+ const o = select.current;
513
+ const label = o.text.trim() || o.value;
514
+ select.options.push(o.value && o.value !== label ? `${label} (${o.value})` : label);
515
+ if (o.selected) select.selected.push(label);
516
+ select.current = void 0;
517
+ } else if (t.name === "select" && select) {
518
+ emit(` [select ${select.name}: ${select.options.join(" | ")}${select.selected.length ? `; selected: ${select.selected.join(", ")}` : ""}]`);
519
+ select = null;
520
+ } else if (t.name === "ul" || t.name === "ol") {
521
+ listCounters.pop();
522
+ flush();
523
+ } else if (t.name === "td" || t.name === "th") {
524
+ emit(" |");
525
+ } else if (t.name === "tr") {
526
+ line = `|${line}`;
527
+ flush(false);
528
+ } else if (t.name === "textarea" || t.name === "button") {
529
+ emit("] ");
530
+ } else if (BLOCK.has(t.name)) {
531
+ flush();
532
+ }
533
+ continue;
534
+ }
535
+ if (!t.selfClosing) stack.push(t.name);
536
+ if (skipDepth >= 0) continue;
537
+ if (SKIP.has(t.name) || hidden(t) || !hasMain && CHROME.has(t.name)) {
538
+ if (!t.selfClosing) skipDepth = stack.length - 1;
539
+ continue;
540
+ }
541
+ if (hasMain && t.name === "main" && !inMain) {
542
+ inMain = true;
543
+ mainDepth = stack.length - 1;
544
+ continue;
545
+ }
546
+ if (!inMain || !inBody) continue;
547
+ const heading = /^h([1-6])$/.exec(t.name);
548
+ if (heading) {
549
+ flush();
550
+ emit(`${"#".repeat(Number(heading[1]))} `);
551
+ } else if (t.name === "br") {
552
+ flush();
553
+ } else if (t.name === "hr") {
554
+ flush();
555
+ lines.push("---");
556
+ } else if (t.name === "ul" || t.name === "ol") {
557
+ flush();
558
+ listCounters.push(t.name === "ol" ? 0 : -1);
559
+ } else if (t.name === "li") {
560
+ flush();
561
+ const top = listCounters.length - 1;
562
+ if (top >= 0 && listCounters[top] >= 0) emit(`${++listCounters[top]}. `);
563
+ else emit("- ");
564
+ } else if (t.name === "tr") {
565
+ flush(false);
566
+ } else if (t.name === "td" || t.name === "th") {
567
+ emit(" ");
568
+ } else if (t.name === "a") {
569
+ const href = t.attrs.href ?? "";
570
+ link = { href: /^(#|javascript:)/i.test(href) ? "" : href, start: line.length, lines: lines.length };
571
+ } else if (t.name === "img") {
572
+ if (t.attrs.alt) emit(` [image: ${t.attrs.alt}] `);
573
+ } else if (t.name === "input") {
574
+ const type = t.attrs.type ?? "text";
575
+ if (type === "submit" || type === "button") emit(` [button: ${t.attrs.value ?? type}] `);
576
+ else {
577
+ const parts = [`input ${t.attrs.name ?? ""}`.trim(), type !== "text" ? `type=${type}` : "", t.attrs.value ? `value=${t.attrs.value}` : "", t.attrs.placeholder ? `placeholder=${t.attrs.placeholder}` : "", "checked" in t.attrs ? "checked" : "", "required" in t.attrs ? "required" : ""];
578
+ emit(` [${parts.filter(Boolean).join(" ")}] `);
579
+ }
580
+ } else if (t.name === "textarea") {
581
+ emit(` [textarea ${t.attrs.name ?? ""}: `);
582
+ } else if (t.name === "select") {
583
+ select = { name: t.attrs.name ?? "", options: [], selected: [] };
584
+ } else if (t.name === "option" && select) {
585
+ select.current = { value: t.attrs.value ?? "", text: "", selected: "selected" in t.attrs };
586
+ } else if (t.name === "button") {
587
+ emit(" [button: ");
588
+ } else if (BLOCK.has(t.name)) {
589
+ flush();
590
+ }
591
+ }
592
+ flush();
593
+ return lines.join("\n").replace(/\[(button|textarea [^:\]]*): +/g, "[$1: ").replace(/ +\]/g, "]").replace(/\n{3,}/g, "\n\n").trim();
594
+ }
595
+ var title = (html) => {
596
+ const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
597
+ return m ? decodeEntities(m[1]).trim() || void 0 : void 0;
598
+ };
599
+ function fitData(data) {
600
+ const kept = [];
601
+ let size = 2;
602
+ for (const d of data) {
603
+ const n = JSON.stringify(d).length + 1;
604
+ if (size + n > DATA_BUDGET) return { data: kept, truncated: true };
605
+ kept.push(d);
606
+ size += n;
607
+ }
608
+ return { data: kept, truncated: false };
609
+ }
610
+ async function readPageResponse(res, url) {
611
+ if (!res.ok || !(res.headers.get("content-type") ?? "").includes("text/html")) return res;
612
+ const html = await res.text();
613
+ const payload = embeddedPayload(html);
614
+ const outcome = pageOutcome(html, payload);
615
+ if (outcome && "redirect" in outcome) return new Response(null, { status: 307, headers: { location: outcome.redirect } });
616
+ if (outcome) return Response.json({ error: outcome.status === 404 ? "not found" : outcome.status === 403 ? "forbidden" : "unauthorized" }, { status: outcome.status });
617
+ let text = htmlToText(html);
618
+ let textTruncated = false;
619
+ if (text.length > TEXT_BUDGET) {
620
+ text = text.slice(0, TEXT_BUDGET);
621
+ textTruncated = true;
622
+ }
623
+ let data = [];
624
+ let dataTruncated = false;
625
+ try {
626
+ ({ data, truncated: dataTruncated } = fitData(pageData(payload)));
627
+ } catch {
628
+ data = [];
629
+ }
630
+ const body = {
631
+ url,
632
+ ...title(html) ? { title: title(html) } : {},
633
+ text,
634
+ ...textTruncated ? { textTruncated: true } : {},
635
+ ...data.length > 0 ? { data } : {},
636
+ ...dataTruncated ? { dataTruncated: true } : {}
637
+ };
638
+ return Response.json(body);
639
+ }
640
+
641
+ // src/index.ts
642
+ import {
643
+ exposed,
644
+ hidden as hidden2,
645
+ describe,
646
+ completeMcpLogin,
647
+ createHostedAuthResolver,
648
+ hostedBroker
649
+ } from "@exposify/core";
650
+ var DISPATCH_HEADER = "x-exposify-dispatch";
651
+ var PRM_SUFFIX = "/.well-known/oauth-protected-resource";
652
+ var LOG_PREFIX = "[agentready]";
653
+ 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
+ var LEFT_BLANK = "A field you leave out is as if left blank on the form.";
655
+ 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() {
657
+ return globalThis.process?.env ?? {};
658
+ }
659
+ function errorMessage(err) {
660
+ return err instanceof Error ? err.message : String(err);
661
+ }
662
+ async function routesFrom(manifest, cfg, warnings) {
663
+ const listed = Array.isArray(cfg.expose) ? cfg.expose.map(parseMethodEntry).filter((e) => e !== null) : [];
664
+ const perEntry = await Promise.all(
665
+ manifest.routes.map(async (entry) => {
666
+ const base = { path: entry.path, params: entry.params };
667
+ if (entry.router === "pages") {
668
+ return listed.filter((e) => e.path === entry.path).map((e) => ({ ...base, method: e.method, optIn: true }));
669
+ }
670
+ let mod;
671
+ if (entry.load) {
672
+ try {
673
+ mod = await entry.load();
674
+ } catch (err) {
675
+ warnings.push(`couldn't load ${entry.file} to read exposed()/describe() on it (${errorMessage(err)}); its methods follow your config only`);
676
+ }
677
+ }
678
+ const methods = mod ? HTTP_METHODS.filter((m) => typeof mod[m] === "function") : entry.methods;
679
+ return methods.filter((m) => m !== "OPTIONS").map((method) => {
680
+ const marker = mod ? getMarker(mod[method]) : void 0;
681
+ return { ...base, method, ...marker ? { marker } : {} };
682
+ });
683
+ })
684
+ );
685
+ return perEntry.flat();
686
+ }
687
+ function fieldSchema(f) {
688
+ let base;
689
+ if (f.enum && f.enum.length > 0) base = z.enum(f.enum);
690
+ else if (f.type === "number") {
691
+ let n = z.number();
692
+ if (f.min !== void 0) n = n.min(f.min);
693
+ if (f.max !== void 0) n = n.max(f.max);
694
+ base = n;
695
+ } else if (f.type === "boolean") base = z.boolean();
696
+ else if (f.type === "json") base = z.unknown().describe("JSON: an object or array (sent as JSON text)");
697
+ else {
698
+ let s = z.string();
699
+ if (f.minLength !== void 0) s = s.min(f.minLength);
700
+ if (f.maxLength !== void 0) s = s.max(f.maxLength);
701
+ if (f.pattern !== void 0) {
702
+ try {
703
+ s = s.regex(new RegExp(`^(?:${f.pattern})$`));
704
+ } catch {
705
+ }
706
+ }
707
+ base = s;
708
+ }
709
+ let schema = f.array ? z.array(base) : base;
710
+ if (f.description) schema = schema.describe(f.description);
711
+ return f.required ? schema : schema.optional();
712
+ }
713
+ function derivedInput(action) {
714
+ const reason = inputProblem(action);
715
+ if (reason && action.proposed && !action.fields.some((f) => f.type === "file")) {
716
+ 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
+ return { input: Object.fromEntries(fields.map((f) => [f.name, fieldSchema(f)])) };
718
+ }
719
+ if (reason) return { reason: action.fields.some((f) => f.type === "file") ? reason : `${reason}; \`exposify-next describe\` can propose it for you to review` };
720
+ return { input: Object.fromEntries(action.fields.map((f) => [f.name, fieldSchema(f)])) };
721
+ }
722
+ var sentence = (s) => /[.!?]$/.test(s.trim()) ? s.trim() : `${s.trim()}.`;
723
+ var GUARD_CACHE_MS = 6e4;
724
+ var GUARD_CACHE_MAX = 500;
725
+ var GUARD_PROBE_TIMEOUT_MS = 5e3;
726
+ var dispatched = (live) => Boolean(live.id && live.action.host);
727
+ async function actionsFrom(manifest, config, warnings) {
728
+ const routes = [];
729
+ const live = /* @__PURE__ */ new Map();
730
+ let hidden3 = 0;
731
+ const list = manifest.actions ?? [];
732
+ const exportCounts = /* @__PURE__ */ new Map();
733
+ for (const a of list) exportCounts.set(a.export, (exportCounts.get(a.export) ?? 0) + 1);
734
+ const base = (manifest.mcpPath ?? "").replace(/\/$/, "");
735
+ for (const action of list) {
736
+ const where = `${action.export} (${action.file})`;
737
+ const conf = config[action.key] ?? (exportCounts.get(action.export) === 1 ? config[action.export] : void 0);
738
+ let mod;
739
+ try {
740
+ mod = await action.load();
741
+ } catch (err) {
742
+ warnings.push(`couldn't load ${action.file} to read the ${action.export} server action (${errorMessage(err)}); it's hidden`);
743
+ hidden3++;
744
+ continue;
745
+ }
746
+ const fn = mod[action.export];
747
+ const marker = getMarker(fn);
748
+ if (typeof fn !== "function" || marker?.kind === "hidden" || !conf && !marker) {
749
+ hidden3++;
750
+ continue;
751
+ }
752
+ const opts = conf === true || !conf ? {} : conf;
753
+ const described = marker?.kind === "described" ? marker : void 0;
754
+ let input = opts.input ?? described?.input;
755
+ let derived = false;
756
+ if (!input && (action.params.some((p) => p.role !== "prev") || (action.unresolved?.length ?? 0) > 0)) {
757
+ const found = derivedInput(action);
758
+ if ("reason" in found) {
759
+ warnings.push(`server action ${where} is opted in but stays hidden: ${found.reason}. Write its input: aiReady(exposify, { actions: { ${action.export}: { input: { ... } } } })`);
760
+ hidden3++;
761
+ continue;
762
+ }
763
+ input = found.input;
764
+ derived = true;
765
+ }
766
+ if (manifest.mcpPath === void 0) {
767
+ warnings.push(`server action ${where} can't be served: the MCP route isn't a catch-all (app/mcp/[[...transport]]/route.ts)`);
768
+ hidden3++;
769
+ continue;
770
+ }
771
+ if (looksLikeAuth(action)) {
772
+ warnings.push(`server action ${where} looks like it signs people in or out: a cookie it sets lands on exposify's internal call, not on the agent's client`);
773
+ }
774
+ const id = fn.$$id;
775
+ live.set(action.key, { action, fn, ...typeof id === "string" && id ? { id } : {} });
776
+ routes.push({
777
+ method: "POST",
778
+ path: `${base}/actions/${action.key}`,
779
+ params: [],
780
+ bodyArgs: true,
781
+ marker: {
782
+ kind: "described",
783
+ name: opts.name ?? described?.name ?? action.key,
784
+ // config, then describe(), then exposify.descriptions.json (`describe` writes it, keyed by the tool's URL).
785
+ description: [sentence(opts.description ?? described?.description ?? manifest.descriptions?.[`POST ${base}/actions/${action.key}`] ?? `Runs the ${action.export} server action`), derived && action.fields.length > 0 ? LEFT_BLANK : "", ACTION_NOTE].filter(Boolean).join(" "),
786
+ input: input ?? {}
787
+ }
788
+ });
789
+ }
790
+ return { routes, live, hidden: hidden3 };
791
+ }
792
+ function excludedPage(page, exclude) {
793
+ const globs = [];
794
+ for (const entry of exclude) {
795
+ const qualified = parseMethodEntry(entry);
796
+ if (!qualified) globs.push(entry);
797
+ else if (qualified.method === "GET" && qualified.path === page.path) return true;
798
+ }
799
+ return globs.length > 0 && matchesPathGlob(page.path, globs);
800
+ }
801
+ function pagesFrom(manifest, cfg, config) {
802
+ if (config === false) return { routes: [], hidden: manifest.pages?.length ?? 0, forms: 0 };
803
+ const routes = [];
804
+ let hidden3 = 0;
805
+ let forms = 0;
806
+ for (const page of manifest.pages ?? []) {
807
+ const conf = config?.[page.path];
808
+ const listed = conf !== void 0 || Array.isArray(cfg.expose) && cfg.expose.some((e) => parseMethodEntry(e)?.method === "GET" && parseMethodEntry(e)?.path === page.path);
809
+ if (conf === false || excludedPage(page, cfg.exclude) || page.formOnly && !listed) {
810
+ hidden3++;
811
+ if (page.formOnly && !listed) forms++;
812
+ continue;
813
+ }
814
+ const opts = typeof conf === "object" ? conf : {};
815
+ const input = {};
816
+ for (const p of page.params) {
817
+ const optional = new RegExp(`:${p}\\*\\?(/|$)`).test(page.path);
818
+ const catchAll = new RegExp(`:${p}\\*`).test(page.path);
819
+ const schema = z.string().describe(catchAll ? `${p}: one or more path segments, as "a/b/c"` : p);
820
+ input[p] = optional ? schema.optional() : schema;
821
+ }
822
+ for (const q of page.search ?? []) input[q] = z.string().optional().describe(`?${q}= in the page's URL`);
823
+ const what = page.description ? sentence(page.description) : page.title ? `Reads the "${page.title}" page (${page.path}).` : `Reads the ${page.path} page.`;
824
+ routes.push({
825
+ method: "GET",
826
+ path: page.path,
827
+ params: page.params,
828
+ queryArgs: page.search ?? [],
829
+ marker: {
830
+ kind: "described",
831
+ name: opts.name ?? page.name,
832
+ description: [sentence(opts.description ?? manifest.descriptions?.[`GET ${page.path}`] ?? what), PAGE_NOTE].join(" "),
833
+ input
834
+ }
835
+ });
836
+ }
837
+ return { routes, hidden: hidden3, forms };
838
+ }
839
+ function loginSpec(manifest, config, warnings) {
840
+ if (config === false) return null;
841
+ if (config === void 0) return manifest.login ?? null;
842
+ if (typeof config === "object") {
843
+ const fields2 = config.fields.map((name) => ({ name, kind: fieldKind(name) }));
844
+ return { kind: "route", path: config.path, fields: fields2, body: config.body ?? "json", where: `POST ${config.path}` };
845
+ }
846
+ const action = (manifest.actions ?? []).find((a) => a.key === config || a.export === config);
847
+ if (!action) {
848
+ warnings.push(`aiReady({ login: "${config}" }): no server action by that name, so AI assistants can't sign in with your app's login`);
849
+ return null;
850
+ }
851
+ 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 } : {}, where: `${action.export} in ${action.file}` };
853
+ }
854
+ async function actionLoginResult(raw, exportName) {
855
+ 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." };
856
+ const cookies = cookieChanges(raw).filter((c) => !c.deleted && c.value !== "");
857
+ const out = await readActionResponse(raw, exportName);
858
+ const body = await out.json().catch(() => void 0);
859
+ const message = errorMessageOf(body);
860
+ if (out.status >= 400 || message && !body?.redirectedTo) return { ok: false, message };
861
+ return cookies.length > 0 ? { ok: true, session: { cookies } } : { ok: false, message };
862
+ }
863
+ var clientIp = (headers) => headers.get("x-forwarded-for")?.split(",")[0]?.trim() || headers.get("x-real-ip") || void 0;
864
+ function aiReady(manifest, options = {}) {
865
+ const cfg = resolveOptions({ ...options, docs: [] });
866
+ const gate = cfg.oauth ? createOAuthGate(cfg.oauth, { metadataUrl: `${cfg.oauth.resource}${PRM_SUFFIX}` }) : null;
867
+ const brokerGate = !gate && cfg.docsAuth === "required" ? createBrokerGate(cfg.auth) : null;
868
+ const logReady = startupLogEnabled(options.startupLog);
869
+ const authContexts = /* @__PURE__ */ new WeakMap();
870
+ const pageRoutes = /* @__PURE__ */ new WeakSet();
871
+ const actionsBase = (manifest.mcpPath ?? "").replace(/\/$/, "");
872
+ const actionUrl = manifest.mcpPath === void 0 ? null : new RegExp(`${actionsBase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/actions/([^/]+)/?$`);
873
+ const loginWarnings = [];
874
+ const login = hostedResolverInfo(cfg.auth) ? loginSpec(manifest, options.login, loginWarnings) : null;
875
+ const loginPagePath = `${(manifest.mcpPath ?? "/mcp").replace(/\/$/, "")}/login`;
876
+ if (login) cfg.signedOutPaths = [login.kind === "action" ? login.host : login.path].filter((p) => Boolean(p)).map((p) => `${manifest.basePath ?? ""}${hostPath(p)}`);
877
+ let built = null;
878
+ const build = async (endpoint) => {
879
+ const warnings = [...loginWarnings];
880
+ let routes = await routesFrom(manifest, cfg, warnings);
881
+ if (options.openapi) routes = applyOpenApi(routes, options.openapi);
882
+ routes = applyDescriptions(routes, new Map(Object.entries(manifest.descriptions ?? {})));
883
+ const pages = pagesFrom(manifest, cfg, options.pages);
884
+ let actions = await actionsFrom(manifest, options.actions ?? {}, warnings);
885
+ const inProcess = [...actions.live.values()].filter((l) => !dispatched(l));
886
+ for (const l of inProcess) {
887
+ warnings.push(
888
+ `server action ${l.action.export} (${l.action.file}) isn't used by any page, so it runs inside the MCP route, where Next refuses updateTag() and refresh(); use it from a page's form to have it run as a real Server Action`
889
+ );
890
+ }
891
+ if (inProcess.length > 0 && !await actionSecret()) {
892
+ warnings.push(
893
+ `${inProcess.length} server action${inProcess.length === 1 ? " is" : "s are"} off: ${inProcess.length === 1 ? "it isn't" : "they aren't"} used by any page, and in production calling ${inProcess.length === 1 ? "it" : "them"} in-process needs EXPOSIFY_ACTION_SECRET (or NEXT_SERVER_ACTIONS_ENCRYPTION_KEY) set to the same value on every instance`
894
+ );
895
+ const off = new Set(inProcess.map((l) => l.action.key));
896
+ const offPaths = new Set(inProcess.map((l) => `${actionsBase}/actions/${l.action.key}`));
897
+ actions = {
898
+ routes: actions.routes.filter((r) => !offPaths.has(r.path)),
899
+ live: new Map([...actions.live].filter(([k]) => !off.has(k))),
900
+ hidden: actions.hidden + inProcess.length
901
+ };
902
+ }
903
+ const kb = KnowledgeBase.build(manifest.docs);
904
+ try {
905
+ for (const w of warnings) console.warn(formatWarning(LOG_PREFIX, w));
906
+ logSurface(endpoint, [...routes, ...pages.routes, ...actions.routes], actions.routes.length, actions.hidden, pages);
907
+ } catch {
908
+ }
909
+ for (const r of pages.routes) pageRoutes.add(r);
910
+ return { routes: [...routes, ...pages.routes, ...actions.routes], kb, actions: actions.live, guards: guardsFor(pages.routes, actions.routes) };
911
+ };
912
+ const guardsFor = (pageTools, actionTools) => {
913
+ const of = /* @__PURE__ */ new Map();
914
+ for (const r of pageTools) {
915
+ const g = manifest.pages?.find((p) => p.path === r.path)?.guards;
916
+ if (g?.length) of.set(r, g);
917
+ }
918
+ for (const r of actionTools) {
919
+ const key = decodeURIComponent(r.path.slice(r.path.lastIndexOf("/") + 1));
920
+ const g = manifest.actions?.find((a) => a.key === key)?.guards;
921
+ if (g?.length) of.set(r, g);
922
+ }
923
+ const probes = /* @__PURE__ */ new Map();
924
+ for (const guard of new Set([...of.values()].flat())) {
925
+ const page = (manifest.pages ?? []).filter((p) => p.params.length === 0 && p.guards?.includes(guard)).sort((a, b) => a.guards.length - b.guards.length || a.path.length - b.path.length)[0];
926
+ if (!page) continue;
927
+ const route = { method: "GET", path: page.path, params: [] };
928
+ pageRoutes.add(route);
929
+ probes.set(guard, route);
930
+ }
931
+ return { of, probes, seen: /* @__PURE__ */ new Map() };
932
+ };
933
+ const visibleRoutes = async (routes, guards, req) => {
934
+ if (guards.probes.size === 0) return routes;
935
+ const who = req.headers.get("authorization") ?? req.headers.get("cookie") ?? "";
936
+ let entry = guards.seen.get(who);
937
+ if (!entry || entry.at < Date.now() - GUARD_CACHE_MS) {
938
+ const failed2 = /* @__PURE__ */ new Set();
939
+ await Promise.all(
940
+ [...guards.probes].map(async ([guard, route]) => {
941
+ try {
942
+ const request = await buildRequest(route, {}, req.headers, cfg, authContexts.get(req) ?? {});
943
+ const res = await Promise.race([selfFetch(request, req, route), new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), GUARD_PROBE_TIMEOUT_MS))]);
944
+ void res.body?.cancel();
945
+ if (res.status < 200 || res.status >= 300) failed2.add(guard);
946
+ } catch {
947
+ }
948
+ })
949
+ );
950
+ entry = { at: Date.now(), failed: failed2 };
951
+ if (guards.seen.size >= GUARD_CACHE_MAX) guards.seen.delete(guards.seen.keys().next().value);
952
+ guards.seen.set(who, entry);
953
+ }
954
+ const failed = entry.failed;
955
+ if (failed.size === 0) return routes;
956
+ return routes.filter((r) => !(guards.of.get(r) ?? []).some((g) => failed.has(g)));
957
+ };
958
+ const ensureBuilt = (endpoint) => built ??= build(endpoint).catch((err) => {
959
+ built = null;
960
+ console.error(formatFailed(LOG_PREFIX, errorMessage(err), "the MCP route answers 503 and retries on the next request"), err);
961
+ throw err;
962
+ });
963
+ const logSurface = (endpoint, routes, exposedActions, hiddenActions, pages) => {
964
+ const counts = countTools(routes, cfg);
965
+ if (counts.reads + counts.writes === 0) {
966
+ console.warn(
967
+ formatWarning(
968
+ LOG_PREFIX,
969
+ `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`)"
971
+ )
972
+ );
973
+ return;
974
+ }
975
+ if (!logReady) return;
976
+ const details = [];
977
+ if (cfg.expose === "*") details.push('all write routes exposed (expose: "*")');
978
+ if (counts.hiddenWrites > 0) details.push(`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`);
979
+ if (pages.routes.length > 0) details.push(`${pages.routes.length} page${pages.routes.length === 1 ? "" : "s"} readable`);
980
+ if (login) details.push(`AI assistants sign in with your app's own login (${login.kind === "action" ? `server action ${login.key}` : `POST ${login.path}`}) at ${loginPagePath}`);
981
+ 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
+ if (exposedActions > 0) details.push(`${exposedActions} server action${exposedActions === 1 ? "" : "s"} exposed, each checking auth itself`);
983
+ if (hiddenActions > 0) details.push(`${hiddenActions} server action${hiddenActions === 1 ? "" : "s"} hidden (opt in with aiReady({ actions }))`);
984
+ const e = env2();
985
+ if (e.VERCEL && e.VERCEL_ENV !== "production" && !e.VERCEL_AUTOMATION_BYPASS_SECRET) {
986
+ details.push("on a protected Vercel preview, tool calls need VERCEL_AUTOMATION_BYPASS_SECRET");
987
+ }
988
+ const auth = describeAuth(cfg.auth, cfg.oauth);
989
+ console.log(formatReady(LOG_PREFIX, { endpoint, tools: routeToolsPhrase(counts, manifest.docs.length), details, docs: manifest.docs.length, auth }));
990
+ };
991
+ const selfFetch = async (request, mcpReq, route) => {
992
+ const target = new URL(request.url);
993
+ const incoming = new URL(mcpReq.url);
994
+ const origin = options.baseUrl ?? env2().EXPOSIFY_BASE_URL ?? incoming.origin;
995
+ let path = `${manifest.basePath ?? ""}${target.pathname}`;
996
+ if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
997
+ const headers = new Headers(request.headers);
998
+ headers.set(DISPATCH_HEADER, "1");
999
+ for (const name of ["x-forwarded-for", "x-real-ip", "user-agent"]) {
1000
+ const value = mcpReq.headers.get(name);
1001
+ if (value) headers.set(name, value);
1002
+ }
1003
+ headers.set("x-forwarded-proto", mcpReq.headers.get("x-forwarded-proto") ?? incoming.protocol.slice(0, -1));
1004
+ 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;
1006
+ if (bypass) headers.set("x-vercel-protection-bypass", bypass);
1007
+ const body = request.body ? await request.text() : void 0;
1008
+ const actionKey = actionUrl?.exec(target.pathname)?.[1];
1009
+ if (actionKey !== void 0) {
1010
+ const live = (await ensureBuilt(actionsBase || "/")).actions.get(decodeURIComponent(actionKey));
1011
+ if (live && dispatched(live)) {
1012
+ const res = await dispatchAction(live, body, headers, origin);
1013
+ if (!actionNotFound(res)) return carrySession(res, await readActionResponse(res, live.action.export));
1014
+ warnOnce(
1015
+ `server action ${live.action.export}: ${live.action.host} doesn't run it (Next answered "action not found"), so it's called in-process instead; regenerate exposify.generated.ts if the page changed`
1016
+ );
1017
+ }
1018
+ const secret = await actionSecret();
1019
+ if (secret) headers.set(ACTION_SECRET_HEADER, secret);
1020
+ }
1021
+ const url = new URL(`${path}${target.search}`, origin);
1022
+ if (route && pageRoutes.has(route)) {
1023
+ headers.set("accept", "text/html,application/xhtml+xml");
1024
+ const res = await fetch(url, { method: "GET", headers, redirect: "manual", cache: "no-store" });
1025
+ return carrySession(res, await readPageResponse(res, `${path}${target.search}`));
1026
+ }
1027
+ return fetch(url, { method: request.method, headers, body, redirect: "manual", cache: "no-store" });
1028
+ };
1029
+ const dispatchAction = async (live, body, headers, origin) => {
1030
+ let args;
1031
+ try {
1032
+ args = body ? JSON.parse(body) : {};
1033
+ } catch {
1034
+ args = {};
1035
+ }
1036
+ const fields = args && typeof args === "object" && !Array.isArray(args) ? args : {};
1037
+ const out = new Headers(headers);
1038
+ out.delete("content-type");
1039
+ out.delete("content-length");
1040
+ out.set("next-action", live.id);
1041
+ out.set("x-action-forwarded", "1");
1042
+ out.set("accept", "text/x-component");
1043
+ out.set("origin", `${out.get("x-forwarded-proto") ?? "http"}://${out.get("x-forwarded-host")}`);
1044
+ let path = `${manifest.basePath ?? ""}${hostPath(live.action.host)}`;
1045
+ if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
1046
+ const reply = encodeReply(actionArguments(live.action.params, fields, jsonFieldsOf(live.action)));
1047
+ return fetch(new URL(path, origin), { method: "POST", headers: out, body: reply, redirect: "manual", cache: "no-store" });
1048
+ };
1049
+ const appLoginFor = (req, spec) => ({
1050
+ label: spec.kind === "action" ? `server action ${spec.key}` : `POST ${spec.path}`,
1051
+ fields: spec.fields,
1052
+ signedOutPaths: cfg.signedOutPaths ?? [],
1053
+ attempt: async (values, ctx) => {
1054
+ const target = options.baseUrl ?? env2().EXPOSIFY_BASE_URL ?? new URL(req.url).origin;
1055
+ const pub = new URL(ctx.origin);
1056
+ const headers = new Headers({ origin: pub.origin, "x-forwarded-proto": pub.protocol.slice(0, -1), "x-forwarded-host": pub.host });
1057
+ if (ctx.ip) headers.set("x-forwarded-for", ctx.ip);
1058
+ const ua = req.headers.get("user-agent");
1059
+ if (ua) headers.set("user-agent", ua);
1060
+ const bypass = env2().VERCEL_AUTOMATION_BYPASS_SECRET;
1061
+ if (bypass) headers.set("x-vercel-protection-bypass", bypass);
1062
+ const pathOf = (p) => {
1063
+ let path = `${manifest.basePath ?? ""}${p}`;
1064
+ if (manifest.trailingSlash && !path.endsWith("/")) path += "/";
1065
+ return path;
1066
+ };
1067
+ if (spec.kind === "route") {
1068
+ headers.set("content-type", spec.body === "form" ? "application/x-www-form-urlencoded" : "application/json");
1069
+ const body = spec.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
1070
+ return loginResultFromResponse(await fetch(new URL(pathOf(spec.path), target), { method: "POST", headers, body, redirect: "manual", cache: "no-store" }));
1071
+ }
1072
+ const action = (manifest.actions ?? []).find((a) => a.key === spec.key);
1073
+ const fn = action ? (await action.load())[action.export] : void 0;
1074
+ const id = fn?.$$id;
1075
+ if (!action?.host || typeof id !== "string") {
1076
+ warnOnce(`server action ${spec.key} is your app's login, but no page uses it, so the sign-in form can't run it; use it from a page's form`);
1077
+ return { ok: false, message: "This app's sign-in can't be used by AI assistants yet." };
1078
+ }
1079
+ headers.set("next-action", id);
1080
+ headers.set("x-action-forwarded", "1");
1081
+ headers.set("accept", "text/x-component");
1082
+ const reply = encodeReply(actionArguments(action.params, values, jsonFieldsOf(action)));
1083
+ const raw = await fetch(new URL(pathOf(hostPath(action.host)), target), { method: "POST", headers, body: reply, redirect: "manual", cache: "no-store" });
1084
+ return actionLoginResult(raw, action.export);
1085
+ }
1086
+ });
1087
+ const carrySession = (raw, out) => {
1088
+ const lines = setCookieLines(raw);
1089
+ const redirect = raw.headers.get("x-action-redirect")?.split(";")[0];
1090
+ const signedOut = redirect !== void 0 && samePath(redirect, cfg.signedOutPaths ?? []);
1091
+ if (lines.length === 0 && !signedOut) return out;
1092
+ const headers = new Headers(out.headers);
1093
+ for (const line of lines) headers.append("set-cookie", line);
1094
+ if (signedOut) headers.set(SIGNED_OUT_HEADER, "1");
1095
+ return new Response(out.body, { status: out.status, headers });
1096
+ };
1097
+ const warned = /* @__PURE__ */ new Set();
1098
+ const warnOnce = (message) => {
1099
+ if (warned.has(message)) return;
1100
+ warned.add(message);
1101
+ console.warn(formatWarning(LOG_PREFIX, message));
1102
+ };
1103
+ const core = createWebMcpHandler({
1104
+ getServer: async (req) => {
1105
+ const { routes: all, kb, guards } = await ensureBuilt(new URL(req.url).pathname);
1106
+ const routes = await visibleRoutes(all, guards, req);
1107
+ const surface = { info: manifest.info, routes, docs: manifest.docs, dispatch: (request, route) => selfFetch(request, req, route) };
1108
+ return buildMcpServer({ surface, kb, cfg, incoming: req.headers, authContext: authContexts.get(req) ?? {} });
1109
+ }
1110
+ }, { ...options.rateLimit !== void 0 ? { rateLimit: options.rateLimit } : {}, ...options.rateWindowMs !== void 0 ? { rateWindowMs: options.rateWindowMs } : {} });
1111
+ const serveAction = async (req, key) => {
1112
+ const secret = await actionSecret();
1113
+ if (!secret || !await secretMatches(req.headers.get(ACTION_SECRET_HEADER), secret)) {
1114
+ return Response.json({ error: "server actions are only callable through the MCP endpoint" }, { status: 403 });
1115
+ }
1116
+ if (req.method !== "POST") return Response.json({ error: "method not allowed" }, { status: 405, headers: { allow: "POST" } });
1117
+ const { actions } = await ensureBuilt(actionsBase || "/");
1118
+ const live = actions.get(key);
1119
+ if (!live) return Response.json({ error: `no exposed server action ${key}` }, { status: 404 });
1120
+ let args;
1121
+ try {
1122
+ args = await req.json();
1123
+ } catch {
1124
+ args = void 0;
1125
+ }
1126
+ if (!args || typeof args !== "object" || Array.isArray(args)) return Response.json({ error: "expected a JSON object of arguments" }, { status: 400 });
1127
+ return runAction(live.fn, live.action, args);
1128
+ };
1129
+ return async (req) => {
1130
+ const pathname = new URL(req.url).pathname;
1131
+ if (login && samePath(pathname, [loginPagePath, `${manifest.basePath ?? ""}${loginPagePath}`])) {
1132
+ const pub = new URL(publicRequestUrl(req.url, req.headers));
1133
+ return handleLoginPage(req, appLoginFor(req, login), hostedResolverInfo(cfg.auth), {
1134
+ path: pub.pathname,
1135
+ secure: pub.protocol === "https:",
1136
+ origin: pub.origin,
1137
+ ...clientIp(req.headers) ? { ip: clientIp(req.headers) } : {}
1138
+ });
1139
+ }
1140
+ const actionKey = actionUrl?.exec(pathname)?.[1];
1141
+ if (actionKey !== void 0) return serveAction(req, decodeURIComponent(actionKey));
1142
+ if (req.headers.has(DISPATCH_HEADER)) {
1143
+ return Response.json({ error: "a tool call can't call the MCP endpoint itself" }, { status: 508 });
1144
+ }
1145
+ if (gate) {
1146
+ if (new URL(req.url).pathname.endsWith(PRM_SUFFIX)) return gate.metadataResponse(req.method);
1147
+ try {
1148
+ const result = await gate.check(req.headers);
1149
+ if (!result.ok) return result.response;
1150
+ authContexts.set(req, { claims: result.claims });
1151
+ } catch (err) {
1152
+ console.warn(`${LOG_PREFIX} oauth gate failed:`, err);
1153
+ return new Response("authorization check failed", { status: 503 });
1154
+ }
1155
+ }
1156
+ if (brokerGate) {
1157
+ const url = publicRequestUrl(req.url, req.headers);
1158
+ const isPrm = url.endsWith(PRM_SUFFIX);
1159
+ const resource = brokerGate.resourceFor(isPrm ? url.slice(0, -PRM_SUFFIX.length) : url);
1160
+ const verdict = await reportAppLogin(cfg.auth, resource, login, { where: login?.where, stacks: manifest.authStacks, log: logReady });
1161
+ if (verdict === "open") {
1162
+ if (isPrm) return new Response("not found", { status: 404 });
1163
+ } else {
1164
+ if (isPrm) return brokerGate.metadataResponse(req.method, resource);
1165
+ try {
1166
+ const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
1167
+ if (!result.ok) return result.response;
1168
+ } catch (err) {
1169
+ console.warn(`${LOG_PREFIX} broker check failed:`, err);
1170
+ return new Response("authorization check failed", { status: 503 });
1171
+ }
1172
+ }
1173
+ }
1174
+ return core(req);
1175
+ };
1176
+ }
1177
+ export {
1178
+ DISPATCH_HEADER,
1179
+ aiReady,
1180
+ completeMcpLogin,
1181
+ createHostedAuthResolver,
1182
+ describe,
1183
+ exposed,
1184
+ hidden2 as hidden,
1185
+ hostedBroker
1186
+ };