@exposify/express 0.3.8 → 0.3.9
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 +173 -7
- package/dist/cli.js +295 -0
- package/dist/index.cjs +350 -55
- package/dist/index.d.cts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +348 -40
- package/dist/scan-preload.cjs +7120 -0
- package/dist/scan-preload.js +7124 -0
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -6,14 +6,21 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { join as
|
|
9
|
+
import { join as join3 } from "path";
|
|
10
10
|
import {
|
|
11
11
|
countTools,
|
|
12
|
+
createBrokerGate,
|
|
12
13
|
createOAuthGate,
|
|
13
14
|
describeAuth,
|
|
14
15
|
formatFailed,
|
|
15
16
|
formatReady,
|
|
16
17
|
formatWarning,
|
|
18
|
+
fieldKind,
|
|
19
|
+
handleLoginPage,
|
|
20
|
+
hostedResolverInfo,
|
|
21
|
+
loginResultFromResponse,
|
|
22
|
+
publicRequestUrl,
|
|
23
|
+
reportAppLogin,
|
|
17
24
|
resolveOptions,
|
|
18
25
|
routeToolsPhrase,
|
|
19
26
|
startupLogEnabled
|
|
@@ -25,9 +32,14 @@ import {
|
|
|
25
32
|
} from "@exposify/core/node";
|
|
26
33
|
|
|
27
34
|
// src/surface.ts
|
|
28
|
-
import { readFile } from "fs/promises";
|
|
29
|
-
import { join } from "path";
|
|
30
|
-
import {
|
|
35
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
36
|
+
import { join as join2 } from "path";
|
|
37
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
38
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
39
|
+
import {
|
|
40
|
+
createServer,
|
|
41
|
+
request as httpRequest
|
|
42
|
+
} from "http";
|
|
31
43
|
import { loadDocsFromFs } from "@exposify/core/node";
|
|
32
44
|
|
|
33
45
|
// src/introspect.ts
|
|
@@ -140,6 +152,7 @@ function flattenHandlers(items) {
|
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
// src/introspect.ts
|
|
155
|
+
var routeHandlers = /* @__PURE__ */ new WeakMap();
|
|
143
156
|
function routesFromExpress(app, opts) {
|
|
144
157
|
const router = routerOf(app);
|
|
145
158
|
const out = [];
|
|
@@ -192,13 +205,15 @@ function walk(stack, prefix, owner, out, walked, unresolved) {
|
|
|
192
205
|
const marker = handlers.map(getMarker).find(Boolean);
|
|
193
206
|
for (const [method, enabled] of Object.entries(layer.route.methods)) {
|
|
194
207
|
if (!enabled || method === "_all") continue;
|
|
195
|
-
|
|
208
|
+
const route = {
|
|
196
209
|
method: method.toUpperCase(),
|
|
197
210
|
path,
|
|
198
211
|
params: extractParams(path),
|
|
199
212
|
marker,
|
|
200
213
|
description: marker?.kind === "described" ? marker.description : void 0
|
|
201
|
-
}
|
|
214
|
+
};
|
|
215
|
+
routeHandlers.set(route, handlers);
|
|
216
|
+
out.push(route);
|
|
202
217
|
}
|
|
203
218
|
continue;
|
|
204
219
|
}
|
|
@@ -283,60 +298,250 @@ function ownerAndStackOf(handle) {
|
|
|
283
298
|
return void 0;
|
|
284
299
|
}
|
|
285
300
|
|
|
301
|
+
// src/descriptions.ts
|
|
302
|
+
import { readFile } from "fs/promises";
|
|
303
|
+
import { join } from "path";
|
|
304
|
+
import { DESCRIPTIONS_FILE, parseDescriptions } from "@exposify/core";
|
|
305
|
+
import { applyDescriptions } from "@exposify/core";
|
|
306
|
+
async function loadDescriptions(cwd, option, warn) {
|
|
307
|
+
if (option === false) return /* @__PURE__ */ new Map();
|
|
308
|
+
const path = join(cwd, option ?? DESCRIPTIONS_FILE);
|
|
309
|
+
let text;
|
|
310
|
+
try {
|
|
311
|
+
text = await readFile(path, "utf8");
|
|
312
|
+
} catch {
|
|
313
|
+
if (option !== void 0) warn(`descriptions file not found: ${path}`);
|
|
314
|
+
return /* @__PURE__ */ new Map();
|
|
315
|
+
}
|
|
316
|
+
const parsed = parseDescriptions(text);
|
|
317
|
+
if (typeof parsed === "string") {
|
|
318
|
+
warn(`ignoring ${path}: ${parsed}`);
|
|
319
|
+
return /* @__PURE__ */ new Map();
|
|
320
|
+
}
|
|
321
|
+
return new Map(Object.entries(parsed.descriptions).map(([key, entry]) => [key, entry.text.trim()]));
|
|
322
|
+
}
|
|
323
|
+
async function loadOpenApi(cwd, option, warn) {
|
|
324
|
+
if (option === void 0) return void 0;
|
|
325
|
+
if (typeof option === "object") return option;
|
|
326
|
+
const path = join(cwd, option);
|
|
327
|
+
if (/\.ya?ml$/i.test(path)) {
|
|
328
|
+
warn(`openapi: YAML isn't supported (${path}); pass the parsed document object or a .json file`);
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
warn(`openapi: couldn't read ${path}: ${err.message}`);
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
286
339
|
// src/surface.ts
|
|
340
|
+
import { applyOpenApi } from "@exposify/core";
|
|
341
|
+
var warnOnce = (m) => console.warn(`[agentready] warning: ${m}`);
|
|
342
|
+
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
343
|
+
var IDENTITY_HEADERS = ["host", "forwarded", "x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "x-forwarded-port"];
|
|
344
|
+
var DISPATCH_HEADER = "x-exposify-dispatch";
|
|
345
|
+
var callerStore = new AsyncLocalStorage();
|
|
346
|
+
function callerOf(req) {
|
|
347
|
+
const headers = {};
|
|
348
|
+
for (const name of IDENTITY_HEADERS) {
|
|
349
|
+
const value = req.headers[name];
|
|
350
|
+
if (typeof value === "string") headers[name] = value;
|
|
351
|
+
else if (Array.isArray(value)) headers[name] = value.join(", ");
|
|
352
|
+
}
|
|
353
|
+
const socket = req.socket;
|
|
354
|
+
return {
|
|
355
|
+
remoteAddress: socket.remoteAddress,
|
|
356
|
+
remoteFamily: socket.remoteFamily,
|
|
357
|
+
encrypted: socket.encrypted === true,
|
|
358
|
+
headers
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function runAsCaller(caller, fn) {
|
|
362
|
+
return callerStore.run(caller, fn);
|
|
363
|
+
}
|
|
287
364
|
async function buildExpressSurface(app, cfg, cwd, opts) {
|
|
288
365
|
const info = await readPackageInfo(cwd);
|
|
289
|
-
const appish = app;
|
|
290
|
-
if (!appish.request || !appish.response) {
|
|
291
|
-
console.warn(
|
|
292
|
-
"[agentready] express request/response prototypes not found; light-my-request corruption guard inactive"
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
const reqProto = appish.request ? Object.getPrototypeOf(appish.request) : void 0;
|
|
296
|
-
const resProto = appish.response ? Object.getPrototypeOf(appish.response) : void 0;
|
|
297
|
-
const pristineReqProto = reqProto ? Object.getPrototypeOf(reqProto) : void 0;
|
|
298
|
-
const pristineResProto = resProto ? Object.getPrototypeOf(resProto) : void 0;
|
|
299
366
|
return {
|
|
300
367
|
info,
|
|
301
|
-
|
|
368
|
+
// Text precedence: describe() in code > exposify.descriptions.json (reviewed) > OpenAPI > derived.
|
|
369
|
+
routes: applyDescriptions(
|
|
370
|
+
applyOpenApi(routesFromExpress(app, opts), await loadOpenApi(cwd, opts?.openapi, warnOnce)),
|
|
371
|
+
await loadDescriptions(cwd, opts?.descriptions, warnOnce)
|
|
372
|
+
),
|
|
302
373
|
docs: await loadDocsFromFs(cfg.docs, cwd),
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
374
|
+
dispatch: appDispatcher(app)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
var dispatchers = /* @__PURE__ */ new WeakMap();
|
|
378
|
+
function appDispatcher(app) {
|
|
379
|
+
const known = dispatchers.get(app);
|
|
380
|
+
if (known) return known;
|
|
381
|
+
const loopback = lazyLoopback(app);
|
|
382
|
+
const dispatch = async (req) => {
|
|
383
|
+
const url = new URL(req.url);
|
|
384
|
+
const payload = req.body ? Buffer.from(await req.arrayBuffer()) : void 0;
|
|
385
|
+
const caller = callerStore.getStore();
|
|
386
|
+
const headers = {
|
|
387
|
+
...caller?.headers ?? {},
|
|
388
|
+
...Object.fromEntries(req.headers.entries())
|
|
389
|
+
};
|
|
390
|
+
if (payload) headers["content-length"] = String(payload.length);
|
|
391
|
+
const { port, tag, release } = await loopback(caller);
|
|
392
|
+
headers[DISPATCH_HEADER] = tag;
|
|
393
|
+
const res = await new Promise((resolve, reject) => {
|
|
394
|
+
const outgoing = httpRequest(
|
|
395
|
+
{ host: "127.0.0.1", port, method: req.method, path: url.pathname + url.search, headers, agent: false },
|
|
396
|
+
resolve
|
|
397
|
+
);
|
|
398
|
+
outgoing.on("error", reject);
|
|
399
|
+
outgoing.end(payload);
|
|
400
|
+
}).catch((err) => {
|
|
401
|
+
release();
|
|
402
|
+
throw err;
|
|
403
|
+
});
|
|
404
|
+
const chunks = [];
|
|
405
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
406
|
+
const responseHeaders = new Headers();
|
|
407
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
408
|
+
if (Array.isArray(value)) for (const v of value) responseHeaders.append(name, v);
|
|
409
|
+
else if (value !== void 0) responseHeaders.set(name, value);
|
|
410
|
+
}
|
|
411
|
+
const status = res.statusCode ?? 502;
|
|
412
|
+
return new Response(NULL_BODY_STATUSES.has(status) ? null : Buffer.concat(chunks), {
|
|
413
|
+
status,
|
|
414
|
+
headers: responseHeaders
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
dispatchers.set(app, dispatch);
|
|
418
|
+
return dispatch;
|
|
419
|
+
}
|
|
420
|
+
function lazyLoopback(app) {
|
|
421
|
+
const secret = randomBytes(32);
|
|
422
|
+
const pending = /* @__PURE__ */ new Map();
|
|
423
|
+
let nextId = 0;
|
|
424
|
+
let port = null;
|
|
425
|
+
const listener = (req, res) => {
|
|
426
|
+
const raw = req.headers[DISPATCH_HEADER];
|
|
427
|
+
delete req.headers[DISPATCH_HEADER];
|
|
428
|
+
for (let i = req.rawHeaders.length - 2; i >= 0; i -= 2) {
|
|
429
|
+
if (req.rawHeaders[i].toLowerCase() === DISPATCH_HEADER) req.rawHeaders.splice(i, 2);
|
|
320
430
|
}
|
|
431
|
+
const [key, id] = typeof raw === "string" ? raw.split(".") : [];
|
|
432
|
+
const presented = Buffer.from(key ?? "", "hex");
|
|
433
|
+
if (!id || presented.length !== secret.length || !timingSafeEqual(presented, secret) || !pending.has(id)) {
|
|
434
|
+
res.writeHead(403).end();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const caller = pending.get(id);
|
|
438
|
+
pending.delete(id);
|
|
439
|
+
if (caller) {
|
|
440
|
+
const socket = req.socket;
|
|
441
|
+
Object.defineProperty(socket, "remoteAddress", { value: caller.remoteAddress, configurable: true });
|
|
442
|
+
Object.defineProperty(socket, "remoteFamily", { value: caller.remoteFamily, configurable: true });
|
|
443
|
+
if (caller.encrypted) Object.defineProperty(socket, "encrypted", { value: true, configurable: true });
|
|
444
|
+
}
|
|
445
|
+
app(req, res);
|
|
446
|
+
};
|
|
447
|
+
return async (caller) => {
|
|
448
|
+
port ??= new Promise((resolve, reject) => {
|
|
449
|
+
const server = createServer(listener);
|
|
450
|
+
server.once("error", reject);
|
|
451
|
+
server.listen(0, "127.0.0.1", () => {
|
|
452
|
+
server.unref();
|
|
453
|
+
resolve(server.address().port);
|
|
454
|
+
});
|
|
455
|
+
}).catch((err) => {
|
|
456
|
+
port = null;
|
|
457
|
+
throw err;
|
|
458
|
+
});
|
|
459
|
+
const resolved = await port;
|
|
460
|
+
const id = String(nextId++);
|
|
461
|
+
pending.set(id, caller);
|
|
462
|
+
return { port: resolved, tag: `${secret.toString("hex")}.${id}`, release: () => pending.delete(id) };
|
|
321
463
|
};
|
|
322
464
|
}
|
|
323
465
|
async function readPackageInfo(cwd) {
|
|
324
466
|
try {
|
|
325
|
-
const pkg = JSON.parse(await
|
|
467
|
+
const pkg = JSON.parse(await readFile2(join2(cwd, "package.json"), "utf8"));
|
|
326
468
|
return { name: pkg.name ?? "app", version: pkg.version ?? "0.0.0" };
|
|
327
469
|
} catch {
|
|
328
470
|
return { name: "app", version: "0.0.0" };
|
|
329
471
|
}
|
|
330
472
|
}
|
|
331
473
|
|
|
474
|
+
// src/login.ts
|
|
475
|
+
import { LOGIN_NAME, NOT_A_LOGIN, loginFields } from "@exposify/core";
|
|
476
|
+
var SESSION_CODE = /\bres\.cookie\(|\.session\b|\.login\(|\blogIn\(|set-cookie|setHeader\(\s*["']set-cookie|\bjwt\.sign\(|new\s+SignJWT|\bsign\(|createSession|issueToken|generateToken/i;
|
|
477
|
+
function bodyFieldsOf(source) {
|
|
478
|
+
const names = /* @__PURE__ */ new Set();
|
|
479
|
+
for (const m of source.matchAll(/\b\w+\.body\.([A-Za-z_$][\w$]*)/g)) names.add(m[1]);
|
|
480
|
+
for (const m of source.matchAll(/\b\w+\.body\[\s*["'`]([^"'`]+)["'`]\s*\]/g)) names.add(m[1]);
|
|
481
|
+
for (const m of source.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=\s*\w+\.body\b/g)) {
|
|
482
|
+
for (const part of m[1].split(",")) {
|
|
483
|
+
const name = part.split(/[:=]/)[0].trim().replace(/^\.\.\./, "");
|
|
484
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return [...names];
|
|
488
|
+
}
|
|
489
|
+
function sourceOf(fn) {
|
|
490
|
+
try {
|
|
491
|
+
return typeof fn === "function" ? Function.prototype.toString.call(fn) : "";
|
|
492
|
+
} catch {
|
|
493
|
+
return "";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function parsesJson(app) {
|
|
497
|
+
const stack = (app?._router ?? app?.router)?.stack;
|
|
498
|
+
if (!Array.isArray(stack)) return null;
|
|
499
|
+
const names = stack.map((l) => l.name ?? "");
|
|
500
|
+
if (names.includes("jsonParser")) return true;
|
|
501
|
+
if (names.includes("urlencodedParser")) return false;
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
function findExpressLogin(app, mounts) {
|
|
505
|
+
let routes;
|
|
506
|
+
try {
|
|
507
|
+
routes = routesFromExpress(app, { mounts });
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
const candidates = [];
|
|
512
|
+
for (const route of routes) {
|
|
513
|
+
if (route.method !== "POST" || route.params.length > 0 || NOT_A_LOGIN.test(route.path)) continue;
|
|
514
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
515
|
+
const own = sourceOf(handlers.at(-1));
|
|
516
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
517
|
+
const fields = loginFields(bodyFieldsOf(own));
|
|
518
|
+
if (!fields) continue;
|
|
519
|
+
const named = LOGIN_NAME.test(route.path);
|
|
520
|
+
if (!named && !SESSION_CODE.test(all)) continue;
|
|
521
|
+
candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
522
|
+
}
|
|
523
|
+
if (candidates.length === 0) return null;
|
|
524
|
+
candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
|
|
525
|
+
return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
|
|
526
|
+
}
|
|
527
|
+
|
|
332
528
|
// src/index.ts
|
|
333
|
-
import {
|
|
529
|
+
import {
|
|
530
|
+
exposed,
|
|
531
|
+
hidden,
|
|
532
|
+
describe,
|
|
533
|
+
completeMcpLogin,
|
|
534
|
+
createHostedAuthResolver,
|
|
535
|
+
hostedBroker
|
|
536
|
+
} from "@exposify/core";
|
|
537
|
+
var AIREADY_OPTIONS_TAG = /* @__PURE__ */ Symbol.for("exposify.express.aiReadyOptions");
|
|
334
538
|
var MCP_PATH = "/mcp";
|
|
335
539
|
var PRM_ROOT = "/.well-known/oauth-protected-resource";
|
|
336
540
|
var LOG_PREFIX = "[agentready]";
|
|
337
541
|
function toolsSummary(routes, docs, cfg) {
|
|
338
542
|
const counts = countTools(routes, cfg);
|
|
339
543
|
const details = counts.hiddenWrites > 0 ? [`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`] : [];
|
|
544
|
+
if (cfg.expose === "*") details.unshift('all write routes exposed (expose: "*")');
|
|
340
545
|
return { tools: routeToolsPhrase(counts, docs), details, empty: counts.reads + counts.writes === 0 };
|
|
341
546
|
}
|
|
342
547
|
var NO_TOOLS_FIX = "register app.use(aiReady()) before your routers, list routers mounted earlier in the `mounts` option, or check whether routes are added later (e.g. after an await) -- those appear on the first /mcp request";
|
|
@@ -352,8 +557,46 @@ function aiReady(options = {}) {
|
|
|
352
557
|
const cfg = resolveOptions(options);
|
|
353
558
|
const gate = cfg.oauth ? createOAuthGate(cfg.oauth) : null;
|
|
354
559
|
const metadataPaths = cfg.oauth ? prmPaths(cfg.oauth.resource) : /* @__PURE__ */ new Set();
|
|
560
|
+
const brokerGate = !gate && cfg.docsAuth === "required" ? createBrokerGate(cfg.auth) : null;
|
|
561
|
+
const resourceOf = (req, path, mcpPath) => {
|
|
562
|
+
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
563
|
+
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
564
|
+
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
565
|
+
return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req)));
|
|
566
|
+
};
|
|
355
567
|
const cwd = options.cwd ?? process.cwd();
|
|
356
568
|
const mounts = options.mounts;
|
|
569
|
+
const brokerInfo = hostedResolverInfo(cfg.auth);
|
|
570
|
+
let loginFound = null;
|
|
571
|
+
const loginOf = (app) => {
|
|
572
|
+
if (!brokerInfo || options.login === false) return null;
|
|
573
|
+
if (!loginFound) {
|
|
574
|
+
const given = options.login;
|
|
575
|
+
const value = given ? { path: given.path, fields: given.fields.map((name) => ({ name, kind: fieldKind(name) })), body: given.body ?? "json" } : findExpressLogin(app, mounts)?.login ?? null;
|
|
576
|
+
loginFound = { value };
|
|
577
|
+
if (value) {
|
|
578
|
+
cfg.signedOutPaths = [value.path];
|
|
579
|
+
if (logReady) console.log(`${LOG_PREFIX} AI assistants sign in with your app's own login (POST ${value.path}) at ${MCP_PATH}/login`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return loginFound.value;
|
|
583
|
+
};
|
|
584
|
+
const appLoginFor = (req, login) => ({
|
|
585
|
+
label: `POST ${login.path}`,
|
|
586
|
+
fields: login.fields,
|
|
587
|
+
signedOutPaths: [login.path],
|
|
588
|
+
attempt: (values) => {
|
|
589
|
+
const body = login.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
590
|
+
const request = new Request(`http://app.internal${login.path}`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "content-type": login.body === "form" ? "application/x-www-form-urlencoded" : "application/json", ...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {} },
|
|
593
|
+
body
|
|
594
|
+
});
|
|
595
|
+
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(loginResultFromResponse);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
const descriptions = options.descriptions;
|
|
599
|
+
const openapi = options.openapi;
|
|
357
600
|
try {
|
|
358
601
|
installMountRecorder();
|
|
359
602
|
} catch {
|
|
@@ -380,10 +623,24 @@ function aiReady(options = {}) {
|
|
|
380
623
|
};
|
|
381
624
|
const middleware = (req, res, next) => {
|
|
382
625
|
const path = req.path ?? req.url.split("?")[0];
|
|
626
|
+
if (brokerInfo && (path === `${MCP_PATH}/login` || path === `${MCP_PATH}/login/`)) {
|
|
627
|
+
const login = loginOf(req.app);
|
|
628
|
+
if (!login) return next();
|
|
629
|
+
void serveLoginPage(req, res, login).catch((err) => {
|
|
630
|
+
console.warn("[agentready] sign-in form failed:", err);
|
|
631
|
+
if (!res.headersSent) res.writeHead(503).end("sign-in is unavailable");
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
383
635
|
if (gate && metadataPaths.has(path)) {
|
|
384
636
|
void sendWebResponse(res, gate.metadataResponse(req.method));
|
|
385
637
|
return;
|
|
386
638
|
}
|
|
639
|
+
if (brokerGate && (path === PRM_ROOT || path === `${PRM_ROOT}${MCP_PATH}`)) {
|
|
640
|
+
const resource = resourceOf(req, path, MCP_PATH);
|
|
641
|
+
void reportAppLogin(cfg.auth, resource, loginOf(req.app)).then(() => sendWebResponse(res, brokerGate.metadataResponse(req.method, resource)));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
387
644
|
if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
|
|
388
645
|
if (disabled) {
|
|
389
646
|
res.writeHead(503).end("agentready is disabled due to an internal error");
|
|
@@ -397,6 +654,14 @@ function aiReady(options = {}) {
|
|
|
397
654
|
console.warn("[agentready] oauth gate failed:", err);
|
|
398
655
|
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
399
656
|
return null;
|
|
657
|
+
}) : brokerGate ? reportAppLogin(cfg.auth, resourceOf(req, path, MCP_PATH), loginOf(req.app)).then(() => brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH))).then(async (result) => {
|
|
658
|
+
if (result.ok) return {};
|
|
659
|
+
await sendWebResponse(res, result.response);
|
|
660
|
+
return null;
|
|
661
|
+
}, (err) => {
|
|
662
|
+
console.warn("[agentready] broker check failed:", err);
|
|
663
|
+
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
664
|
+
return null;
|
|
400
665
|
}) : Promise.resolve({});
|
|
401
666
|
authorized.then((authContext) => {
|
|
402
667
|
if (!authContext) return;
|
|
@@ -405,9 +670,16 @@ function aiReady(options = {}) {
|
|
|
405
670
|
cfg,
|
|
406
671
|
cwd,
|
|
407
672
|
mounts,
|
|
673
|
+
{ descriptions, openapi },
|
|
408
674
|
(surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
|
|
409
675
|
);
|
|
410
|
-
return handlerPromise.then(
|
|
676
|
+
return handlerPromise.then(
|
|
677
|
+
(handler) => (
|
|
678
|
+
// Tool calls made while handling this request reach the app with
|
|
679
|
+
// this caller's address and forwarding headers (see surface.ts).
|
|
680
|
+
runAsCaller(callerOf(req), () => handler(req, res, authContext))
|
|
681
|
+
)
|
|
682
|
+
);
|
|
411
683
|
}).catch((err) => {
|
|
412
684
|
disabled = true;
|
|
413
685
|
handlerPromise = null;
|
|
@@ -425,7 +697,39 @@ function aiReady(options = {}) {
|
|
|
425
697
|
if (!res.headersSent) res.writeHead(503).end("agentready failed to initialize");
|
|
426
698
|
});
|
|
427
699
|
};
|
|
700
|
+
async function serveLoginPage(req, res, login) {
|
|
701
|
+
const original = String(req.originalUrl ?? req.url);
|
|
702
|
+
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
703
|
+
const url = new URL(publicRequestUrl(local, headersOf(req)));
|
|
704
|
+
url.search = new URL(local).search;
|
|
705
|
+
let body;
|
|
706
|
+
if (req.method === "POST") {
|
|
707
|
+
if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) body = new URLSearchParams(req.body).toString();
|
|
708
|
+
else if (typeof req.body === "string") body = req.body;
|
|
709
|
+
else {
|
|
710
|
+
const chunks = [];
|
|
711
|
+
let size = 0;
|
|
712
|
+
for await (const chunk of req) {
|
|
713
|
+
size += chunk.length;
|
|
714
|
+
if (size > 16 * 1024) break;
|
|
715
|
+
chunks.push(chunk);
|
|
716
|
+
}
|
|
717
|
+
body = Buffer.concat(chunks).toString("utf8");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const request = new Request(url, { method: req.method, headers: headersOf(req), ...body !== void 0 ? { body } : {} });
|
|
721
|
+
const forwarded = String(req.headers["x-forwarded-for"] ?? "").split(",")[0]?.trim();
|
|
722
|
+
const ip = forwarded || req.socket?.remoteAddress;
|
|
723
|
+
const response = await handleLoginPage(request, appLoginFor(req, login), brokerInfo, {
|
|
724
|
+
path: url.pathname,
|
|
725
|
+
secure: url.protocol === "https:",
|
|
726
|
+
origin: url.origin,
|
|
727
|
+
...ip ? { ip } : {}
|
|
728
|
+
});
|
|
729
|
+
await sendWebResponse(res, response);
|
|
730
|
+
}
|
|
428
731
|
middleware[MOUNT_OWNER_TAG] = true;
|
|
732
|
+
middleware[AIREADY_OPTIONS_TAG] = options;
|
|
429
733
|
setImmediate(() => {
|
|
430
734
|
void startupCheck().catch(() => {
|
|
431
735
|
});
|
|
@@ -474,16 +778,16 @@ function headersOf(req) {
|
|
|
474
778
|
}
|
|
475
779
|
return headers;
|
|
476
780
|
}
|
|
477
|
-
async function initialize(app, cfg, cwd, mounts, onReady) {
|
|
781
|
+
async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
478
782
|
let surface = null;
|
|
479
783
|
let kbPromise = null;
|
|
480
784
|
const getSurface = async () => {
|
|
481
|
-
surface ??= await buildExpressSurface(app, cfg, cwd, { mounts });
|
|
785
|
+
surface ??= await buildExpressSurface(app, cfg, cwd, { mounts, ...extras });
|
|
482
786
|
return surface;
|
|
483
787
|
};
|
|
484
788
|
const getKb = async () => {
|
|
485
789
|
const s = await getSurface();
|
|
486
|
-
kbPromise ??= loadOrBuildKb(s.docs,
|
|
790
|
+
kbPromise ??= loadOrBuildKb(s.docs, join3(cwd, ".exposify"));
|
|
487
791
|
return kbPromise;
|
|
488
792
|
};
|
|
489
793
|
const built = await getSurface();
|
|
@@ -495,9 +799,13 @@ async function initialize(app, cfg, cwd, mounts, onReady) {
|
|
|
495
799
|
return createNodeMcpHandler({ getSurface, getKb, cfg });
|
|
496
800
|
}
|
|
497
801
|
export {
|
|
802
|
+
AIREADY_OPTIONS_TAG,
|
|
498
803
|
aiReady,
|
|
804
|
+
completeMcpLogin,
|
|
805
|
+
createHostedAuthResolver,
|
|
499
806
|
describe,
|
|
500
807
|
exposed,
|
|
501
808
|
hidden,
|
|
809
|
+
hostedBroker,
|
|
502
810
|
routesFromExpress
|
|
503
811
|
};
|