@exposify/express 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -8
- package/dist/cli.js +1 -1
- package/dist/index.cjs +102 -36
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +106 -37
- package/dist/scan-preload.cjs +169 -32
- package/dist/scan-preload.js +169 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Tools get Nest's full paths (global prefix, URI versioning, `RouterModule`
|
|
|
33
33
|
paths), and tool calls go through Nest itself: guards, pipes, interceptors
|
|
34
34
|
and exception filters apply exactly as for any HTTP client, so a guarded
|
|
35
35
|
route refuses an agent without credentials and a `ValidationPipe` error
|
|
36
|
-
reaches the agent as an actionable 400. `npx exposify
|
|
36
|
+
reaches the agent as an actionable 400. `npx @exposify/express setup` and
|
|
37
37
|
`inspect` find `src/main.ts` from `nest-cli.json` and load it with your
|
|
38
38
|
`ts-node`/`swc-node`. The Fastify platform isn't supported yet. See
|
|
39
39
|
`examples/nest-demo`.
|
|
@@ -54,12 +54,12 @@ takes a path to a JSON file, for any Express app with an OpenAPI document.
|
|
|
54
54
|
Description precedence: `describe()` in code, then
|
|
55
55
|
`exposify.descriptions.json`, then OpenAPI, then the derived text.
|
|
56
56
|
|
|
57
|
-
## Guided setup: `npx exposify
|
|
57
|
+
## Guided setup: `npx @exposify/express setup`
|
|
58
58
|
|
|
59
59
|
Instead of writing the config by hand, run this in your app's directory:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
npx exposify
|
|
62
|
+
npx @exposify/express setup
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
It loads your app without starting a server, lists every endpoint (reads
|
|
@@ -110,7 +110,7 @@ give your tools real descriptions (below).
|
|
|
110
110
|
|
|
111
111
|
**Tool descriptions.** Agents pick tools by their descriptions, and by default
|
|
112
112
|
they get one derived from the route ("Create an order (POST /orders)").
|
|
113
|
-
`setup`'s step 5, or `npx exposify
|
|
113
|
+
`setup`'s step 5, or `npx @exposify/express describe` any time, replaces those:
|
|
114
114
|
|
|
115
115
|
- `a` sends each route's handler source to Exposify's description service,
|
|
116
116
|
which writes a proposal with an LLM (`--no-source` sends only method, path
|
|
@@ -137,15 +137,15 @@ flagged for you to check. In CI, `describe --check` exits 1 if a tool has no
|
|
|
137
137
|
description or one is out of date, and `describe --ci` writes AI
|
|
138
138
|
descriptions for read routes only, listing the writes for a person.
|
|
139
139
|
|
|
140
|
-
`npx exposify
|
|
140
|
+
`npx @exposify/express inspect` prints every route with its tool name,
|
|
141
141
|
whether agents get it, and why (`--json` for scripts): the answer to "why
|
|
142
142
|
isn't my route showing up?".
|
|
143
143
|
|
|
144
|
-
**Catch tool changes in CI:** `npx exposify
|
|
144
|
+
**Catch tool changes in CI:** `npx @exposify/express inspect --snapshot` writes
|
|
145
145
|
`exposify.tools.json` (each route agents can call, and its tool name).
|
|
146
146
|
`setup` offers to save the same file as its last question (default no;
|
|
147
147
|
`--snapshot` to save without asking). Commit it, and run
|
|
148
|
-
`npx exposify
|
|
148
|
+
`npx @exposify/express inspect --check` in CI: it exits 1,
|
|
149
149
|
listing what changed, when a route becomes a tool (writes flagged), stops
|
|
150
150
|
being one, or gets a new tool name. Re-run `--snapshot` when the change is
|
|
151
151
|
intended, so it shows up in the PR diff for review.
|
|
@@ -168,11 +168,30 @@ signs in with your app's own login, with no code:
|
|
|
168
168
|
as that person; cookies you set later are kept; a 401 or a redirect to the
|
|
169
169
|
login ends the connection, so the assistant asks to connect again.
|
|
170
170
|
- `aiReady()` tells Exposify about the login and its MCP address before
|
|
171
|
-
answering `/mcp`, so there's nothing to add in the dashboard.
|
|
171
|
+
answering `/mcp`, so there's nothing to add in the dashboard. The address
|
|
172
|
+
comes from the request's `Host` (and `X-Forwarded-Host` only when you've
|
|
173
|
+
set Express's `trust proxy`, as you should behind a proxy). The first
|
|
174
|
+
domain reported is used as is; a new domain after that waits for you to
|
|
175
|
+
approve it on the dashboard, since any client can send a made-up `Host`.
|
|
172
176
|
|
|
173
177
|
`login: { path, fields, body? }` names another route; `login: false` turns
|
|
174
178
|
it off. Routers introspection can't see (sub-apps) need `mounts`.
|
|
175
179
|
|
|
180
|
+
Logins in two steps work too: when the login asks for a code next (a
|
|
181
|
+
redirect to a code page, or `{ mfaRequired: true }`), the form asks for it
|
|
182
|
+
and sends your second-step route (`POST /auth/2fa/verify`: named like
|
|
183
|
+
`verify`, `2fa`, `otp`, one code, sets the session) the code with the
|
|
184
|
+
cookies the first step set. Passwordless (an email, then a code) is found the
|
|
185
|
+
same way. `loginVerify: { path, fields, body? }` names the second step;
|
|
186
|
+
`loginVerify: false` turns it off.
|
|
187
|
+
|
|
188
|
+
**No login found, and no sign-in method in the dashboard?** Then agents use
|
|
189
|
+
your app signed out: `/mcp` doesn't ask them to sign in, and your own checks
|
|
190
|
+
decide what they can do. The startup log says which it is
|
|
191
|
+
(`[agentready] How agents sign in: ...`), and so does the dashboard's "How
|
|
192
|
+
agents sign in" page. Adding a sign-in method there takes effect within a
|
|
193
|
+
minute.
|
|
194
|
+
|
|
176
195
|
## "Click Connect and log in" (OAuth)
|
|
177
196
|
|
|
178
197
|
By default the MCP client has to be handed a token by hand. If your app's
|
package/dist/cli.js
CHANGED
|
@@ -227,7 +227,7 @@ ${last.output}`)) {
|
|
|
227
227
|
|
|
228
228
|
// bin/adapter.ts
|
|
229
229
|
var expressAdapter = {
|
|
230
|
-
bin: "exposify
|
|
230
|
+
bin: "npx @exposify/express",
|
|
231
231
|
entryExample: "src/server.ts",
|
|
232
232
|
entryHelp: "The file that creates your Express app",
|
|
233
233
|
locate(cwd, entryFlag) {
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
routesFromExpress: () => routesFromExpress
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
var import_node_fs = require("fs");
|
|
34
35
|
var import_node_path3 = require("path");
|
|
35
36
|
var import_core6 = require("@exposify/core");
|
|
36
37
|
var import_node2 = require("@exposify/core/node");
|
|
@@ -503,6 +504,26 @@ function parsesJson(app) {
|
|
|
503
504
|
if (names.includes("urlencodedParser")) return false;
|
|
504
505
|
return null;
|
|
505
506
|
}
|
|
507
|
+
var LOGIN_RULE = { not: import_core5.NOT_A_LOGIN, name: import_core5.LOGIN_NAME, mustBeNamed: false, fields: (n) => (0, import_core5.loginFields)(n), session: true };
|
|
508
|
+
var VERIFY_RULE = { not: import_core5.NOT_A_VERIFY, name: import_core5.VERIFY_NAME, mustBeNamed: true, fields: (n, named) => (0, import_core5.verifyFields)(n, named), session: true };
|
|
509
|
+
var START_RULE = { not: import_core5.NOT_A_LOGIN, name: new RegExp(`${import_core5.LOGIN_NAME.source}|${import_core5.START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => (0, import_core5.startFields)(n), session: false };
|
|
510
|
+
function candidates(app, routes, rule) {
|
|
511
|
+
const out = [];
|
|
512
|
+
for (const route of routes) {
|
|
513
|
+
if (route.method !== "POST" || route.params.length > 0 || rule.not.test(route.path)) continue;
|
|
514
|
+
const named = rule.name.test(route.path);
|
|
515
|
+
if (rule.mustBeNamed && !named) continue;
|
|
516
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
517
|
+
const own = sourceOf(handlers.at(-1));
|
|
518
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
519
|
+
const fields = rule.fields(bodyFieldsOf(own), named);
|
|
520
|
+
if (!fields) continue;
|
|
521
|
+
if (rule.session && !import_core5.LOGIN_NAME.test(route.path) && !SESSION_CODE.test(all)) continue;
|
|
522
|
+
out.push({ step: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
523
|
+
}
|
|
524
|
+
out.sort((a, b) => Number(b.named) - Number(a.named) || a.step.fields.length - b.step.fields.length);
|
|
525
|
+
return out.map((c) => c.step);
|
|
526
|
+
}
|
|
506
527
|
function findExpressLogin(app, mounts) {
|
|
507
528
|
let routes;
|
|
508
529
|
try {
|
|
@@ -510,21 +531,21 @@ function findExpressLogin(app, mounts) {
|
|
|
510
531
|
} catch {
|
|
511
532
|
return null;
|
|
512
533
|
}
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
534
|
+
const verifies = candidates(app, routes, VERIFY_RULE);
|
|
535
|
+
const logins = candidates(app, routes, LOGIN_RULE);
|
|
536
|
+
if (logins.length > 0) {
|
|
537
|
+
const [first2, ...others2] = logins;
|
|
538
|
+
return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2.map((c) => `POST ${c.path}`) };
|
|
539
|
+
}
|
|
540
|
+
const pairs = [];
|
|
541
|
+
for (const start of candidates(app, routes, START_RULE)) {
|
|
542
|
+
const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
|
|
543
|
+
const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
|
|
544
|
+
if (verify) pairs.push({ ...start, passwordless: true, verify });
|
|
524
545
|
}
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
return { login:
|
|
546
|
+
if (pairs.length === 0) return null;
|
|
547
|
+
const [first, ...others] = pairs;
|
|
548
|
+
return { login: first, others: others.map((c) => `POST ${c.path}`) };
|
|
528
549
|
}
|
|
529
550
|
|
|
530
551
|
// src/index.ts
|
|
@@ -557,7 +578,7 @@ function aiReady(options = {}) {
|
|
|
557
578
|
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
558
579
|
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
559
580
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
560
|
-
return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req)));
|
|
581
|
+
return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
561
582
|
};
|
|
562
583
|
const cwd = options.cwd ?? process.cwd();
|
|
563
584
|
const mounts = options.mounts;
|
|
@@ -567,29 +588,67 @@ function aiReady(options = {}) {
|
|
|
567
588
|
if (!brokerInfo || options.login === false) return null;
|
|
568
589
|
if (!loginFound) {
|
|
569
590
|
const given = options.login;
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (value) {
|
|
573
|
-
|
|
574
|
-
|
|
591
|
+
let value = given ? { path: given.path, fields: given.fields.map((name) => ({ name, kind: (0, import_core6.fieldKind)(name) })), body: given.body ?? "json" } : findExpressLogin(app, mounts)?.login ?? null;
|
|
592
|
+
const verify = options.loginVerify;
|
|
593
|
+
if (value && verify !== void 0) {
|
|
594
|
+
const { verify: _found, ...first } = value;
|
|
595
|
+
const kind = (name) => (0, import_core6.verifyFields)([name], true)?.[0]?.kind ?? (0, import_core6.fieldKind)(name);
|
|
596
|
+
value = verify === false ? first : { ...first, verify: { path: verify.path, fields: verify.fields.map((name) => ({ name, kind: kind(name) })), body: verify.body ?? "json" } };
|
|
575
597
|
}
|
|
598
|
+
loginFound = { value };
|
|
599
|
+
if (value) cfg.signedOutPaths = [value.path];
|
|
576
600
|
}
|
|
577
601
|
return loginFound.value;
|
|
578
602
|
};
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
603
|
+
let stacks;
|
|
604
|
+
const stacksOf = () => {
|
|
605
|
+
if (!stacks) {
|
|
606
|
+
try {
|
|
607
|
+
stacks = (0, import_core6.authStacksOf)((0, import_core6.dependencyNames)(JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path3.join)(cwd, "package.json"), "utf8")))).map((s) => s.pkg);
|
|
608
|
+
} catch {
|
|
609
|
+
stacks = [];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return stacks;
|
|
613
|
+
};
|
|
614
|
+
const report = (req, resource) => {
|
|
615
|
+
const login = loginOf(req.app);
|
|
616
|
+
const where = login ? `POST ${login.path}${login.verify ? `, then a code: POST ${login.verify.path}` : ""}` : void 0;
|
|
617
|
+
return (0, import_core6.reportAppLogin)(cfg.auth, resource, login, { ...where ? { where } : {}, stacks: stacksOf(), log: logReady });
|
|
618
|
+
};
|
|
619
|
+
const appLoginFor = (req, login) => {
|
|
620
|
+
const send = (step, values, reading, cookies = []) => {
|
|
621
|
+
const body = step.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
622
|
+
const request = new Request(`http://app.internal${step.path}`, {
|
|
586
623
|
method: "POST",
|
|
587
|
-
headers: {
|
|
624
|
+
headers: {
|
|
625
|
+
"content-type": step.body === "form" ? "application/x-www-form-urlencoded" : "application/json",
|
|
626
|
+
...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {},
|
|
627
|
+
...cookies.length ? { cookie: cookies.map((c) => `${c.name}=${c.value}`).join("; ") } : {}
|
|
628
|
+
},
|
|
588
629
|
body
|
|
589
630
|
});
|
|
590
|
-
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(import_core6.loginResultFromResponse);
|
|
591
|
-
}
|
|
592
|
-
|
|
631
|
+
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then((res) => (0, import_core6.loginResultFromResponse)(res, reading));
|
|
632
|
+
};
|
|
633
|
+
const verify = login.verify;
|
|
634
|
+
return {
|
|
635
|
+
label: `POST ${login.path}`,
|
|
636
|
+
fields: login.fields,
|
|
637
|
+
signedOutPaths: [login.path],
|
|
638
|
+
...login.passwordless ? { passwordless: true } : {},
|
|
639
|
+
attempt: (values) => send(login, values, {
|
|
640
|
+
...verify ? { verifyPaths: [verify.path], verifyFields: verify.fields.map((f) => f.name) } : {},
|
|
641
|
+
...login.passwordless ? { start: true } : {}
|
|
642
|
+
}),
|
|
643
|
+
...verify ? {
|
|
644
|
+
verify: {
|
|
645
|
+
label: `POST ${verify.path}`,
|
|
646
|
+
fields: verify.fields,
|
|
647
|
+
attempt: (values, ctx) => send(verify, values, { carried: ctx.carry.cookies, verifyPaths: [verify.path] }, ctx.carry.cookies)
|
|
648
|
+
}
|
|
649
|
+
} : {}
|
|
650
|
+
};
|
|
651
|
+
};
|
|
593
652
|
const descriptions = options.descriptions;
|
|
594
653
|
const openapi = options.openapi;
|
|
595
654
|
try {
|
|
@@ -633,7 +692,9 @@ function aiReady(options = {}) {
|
|
|
633
692
|
}
|
|
634
693
|
if (brokerGate && (path === PRM_ROOT || path === `${PRM_ROOT}${MCP_PATH}`)) {
|
|
635
694
|
const resource = resourceOf(req, path, MCP_PATH);
|
|
636
|
-
void (
|
|
695
|
+
void report(req, resource).then(
|
|
696
|
+
(verdict) => verdict === "open" ? void res.writeHead(404).end("not found") : sendWebResponse(res, brokerGate.metadataResponse(req.method, resource))
|
|
697
|
+
);
|
|
637
698
|
return;
|
|
638
699
|
}
|
|
639
700
|
if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
|
|
@@ -649,11 +710,13 @@ function aiReady(options = {}) {
|
|
|
649
710
|
console.warn("[agentready] oauth gate failed:", err);
|
|
650
711
|
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
651
712
|
return null;
|
|
652
|
-
}) : brokerGate ? (
|
|
713
|
+
}) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
|
|
714
|
+
if (verdict === "open") return {};
|
|
715
|
+
const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
|
|
653
716
|
if (result.ok) return {};
|
|
654
717
|
await sendWebResponse(res, result.response);
|
|
655
718
|
return null;
|
|
656
|
-
}
|
|
719
|
+
}).catch((err) => {
|
|
657
720
|
console.warn("[agentready] broker check failed:", err);
|
|
658
721
|
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
659
722
|
return null;
|
|
@@ -695,7 +758,7 @@ function aiReady(options = {}) {
|
|
|
695
758
|
async function serveLoginPage(req, res, login) {
|
|
696
759
|
const original = String(req.originalUrl ?? req.url);
|
|
697
760
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
698
|
-
const url = new URL((0, import_core6.publicRequestUrl)(local, headersOf(req)));
|
|
761
|
+
const url = new URL((0, import_core6.publicRequestUrl)(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
699
762
|
url.search = new URL(local).search;
|
|
700
763
|
let body;
|
|
701
764
|
if (req.method === "POST") {
|
|
@@ -706,7 +769,7 @@ function aiReady(options = {}) {
|
|
|
706
769
|
let size = 0;
|
|
707
770
|
for await (const chunk of req) {
|
|
708
771
|
size += chunk.length;
|
|
709
|
-
if (size >
|
|
772
|
+
if (size > 32 * 1024) break;
|
|
710
773
|
chunks.push(chunk);
|
|
711
774
|
}
|
|
712
775
|
body = Buffer.concat(chunks).toString("utf8");
|
|
@@ -793,6 +856,9 @@ async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
|
793
856
|
}
|
|
794
857
|
return (0, import_node2.createNodeMcpHandler)({ getSurface, getKb, cfg });
|
|
795
858
|
}
|
|
859
|
+
function trustsProxy(req) {
|
|
860
|
+
return Boolean(req.app?.get?.("trust proxy"));
|
|
861
|
+
}
|
|
796
862
|
// Annotate the CommonJS export names for ESM import in node:
|
|
797
863
|
0 && (module.exports = {
|
|
798
864
|
AIREADY_OPTIONS_TAG,
|
package/dist/index.d.cts
CHANGED
|
@@ -31,6 +31,18 @@ interface ExpressAiReadyOptions extends AgentReadyOptions {
|
|
|
31
31
|
fields: string[];
|
|
32
32
|
body?: "json" | "form";
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The login's second step, when it asks for a code after the first (a
|
|
36
|
+
* password, then a code; or an email, then a code). Found by itself (a
|
|
37
|
+
* POST route named like `verify`, `2fa`, `otp` that reads one code and
|
|
38
|
+
* sets the session); name another route (`{ path, fields }`), or `false`
|
|
39
|
+
* for none.
|
|
40
|
+
*/
|
|
41
|
+
loginVerify?: false | {
|
|
42
|
+
path: string;
|
|
43
|
+
fields: string[];
|
|
44
|
+
body?: "json" | "form";
|
|
45
|
+
};
|
|
34
46
|
/** Where docs globs and the .exposify cache resolve. Default: process.cwd() */
|
|
35
47
|
cwd?: string;
|
|
36
48
|
/**
|
|
@@ -45,7 +57,7 @@ interface ExpressAiReadyOptions extends AgentReadyOptions {
|
|
|
45
57
|
*/
|
|
46
58
|
mounts?: Record<string, unknown>;
|
|
47
59
|
/**
|
|
48
|
-
* Tool descriptions file written by
|
|
60
|
+
* Tool descriptions file written by `@exposify/express setup`/`describe`.
|
|
49
61
|
* Default: exposify.descriptions.json in `cwd`, if present. `false` ignores it.
|
|
50
62
|
* describe() text in code still wins over it.
|
|
51
63
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,18 @@ interface ExpressAiReadyOptions extends AgentReadyOptions {
|
|
|
31
31
|
fields: string[];
|
|
32
32
|
body?: "json" | "form";
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The login's second step, when it asks for a code after the first (a
|
|
36
|
+
* password, then a code; or an email, then a code). Found by itself (a
|
|
37
|
+
* POST route named like `verify`, `2fa`, `otp` that reads one code and
|
|
38
|
+
* sets the session); name another route (`{ path, fields }`), or `false`
|
|
39
|
+
* for none.
|
|
40
|
+
*/
|
|
41
|
+
loginVerify?: false | {
|
|
42
|
+
path: string;
|
|
43
|
+
fields: string[];
|
|
44
|
+
body?: "json" | "form";
|
|
45
|
+
};
|
|
34
46
|
/** Where docs globs and the .exposify cache resolve. Default: process.cwd() */
|
|
35
47
|
cwd?: string;
|
|
36
48
|
/**
|
|
@@ -45,7 +57,7 @@ interface ExpressAiReadyOptions extends AgentReadyOptions {
|
|
|
45
57
|
*/
|
|
46
58
|
mounts?: Record<string, unknown>;
|
|
47
59
|
/**
|
|
48
|
-
* Tool descriptions file written by
|
|
60
|
+
* Tool descriptions file written by `@exposify/express setup`/`describe`.
|
|
49
61
|
* Default: exposify.descriptions.json in `cwd`, if present. `false` ignores it.
|
|
50
62
|
* describe() text in code still wins over it.
|
|
51
63
|
*/
|
package/dist/index.js
CHANGED
|
@@ -6,11 +6,14 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
+
import { readFileSync } from "fs";
|
|
9
10
|
import { join as join3 } from "path";
|
|
10
11
|
import {
|
|
12
|
+
authStacksOf,
|
|
11
13
|
countTools,
|
|
12
14
|
createBrokerGate,
|
|
13
15
|
createOAuthGate,
|
|
16
|
+
dependencyNames,
|
|
14
17
|
describeAuth,
|
|
15
18
|
formatFailed,
|
|
16
19
|
formatReady,
|
|
@@ -21,6 +24,7 @@ import {
|
|
|
21
24
|
loginResultFromResponse,
|
|
22
25
|
publicRequestUrl,
|
|
23
26
|
reportAppLogin,
|
|
27
|
+
verifyFields as verifyFields2,
|
|
24
28
|
resolveOptions,
|
|
25
29
|
routeToolsPhrase,
|
|
26
30
|
startupLogEnabled
|
|
@@ -472,7 +476,7 @@ async function readPackageInfo(cwd) {
|
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
// src/login.ts
|
|
475
|
-
import { LOGIN_NAME, NOT_A_LOGIN, loginFields } from "@exposify/core";
|
|
479
|
+
import { LOGIN_NAME, NOT_A_LOGIN, NOT_A_VERIFY, START_NAME, VERIFY_NAME, loginFields, startFields, verifyFields } from "@exposify/core";
|
|
476
480
|
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
481
|
function bodyFieldsOf(source) {
|
|
478
482
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -501,6 +505,26 @@ function parsesJson(app) {
|
|
|
501
505
|
if (names.includes("urlencodedParser")) return false;
|
|
502
506
|
return null;
|
|
503
507
|
}
|
|
508
|
+
var LOGIN_RULE = { not: NOT_A_LOGIN, name: LOGIN_NAME, mustBeNamed: false, fields: (n) => loginFields(n), session: true };
|
|
509
|
+
var VERIFY_RULE = { not: NOT_A_VERIFY, name: VERIFY_NAME, mustBeNamed: true, fields: (n, named) => verifyFields(n, named), session: true };
|
|
510
|
+
var START_RULE = { not: NOT_A_LOGIN, name: new RegExp(`${LOGIN_NAME.source}|${START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => startFields(n), session: false };
|
|
511
|
+
function candidates(app, routes, rule) {
|
|
512
|
+
const out = [];
|
|
513
|
+
for (const route of routes) {
|
|
514
|
+
if (route.method !== "POST" || route.params.length > 0 || rule.not.test(route.path)) continue;
|
|
515
|
+
const named = rule.name.test(route.path);
|
|
516
|
+
if (rule.mustBeNamed && !named) continue;
|
|
517
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
518
|
+
const own = sourceOf(handlers.at(-1));
|
|
519
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
520
|
+
const fields = rule.fields(bodyFieldsOf(own), named);
|
|
521
|
+
if (!fields) continue;
|
|
522
|
+
if (rule.session && !LOGIN_NAME.test(route.path) && !SESSION_CODE.test(all)) continue;
|
|
523
|
+
out.push({ step: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
524
|
+
}
|
|
525
|
+
out.sort((a, b) => Number(b.named) - Number(a.named) || a.step.fields.length - b.step.fields.length);
|
|
526
|
+
return out.map((c) => c.step);
|
|
527
|
+
}
|
|
504
528
|
function findExpressLogin(app, mounts) {
|
|
505
529
|
let routes;
|
|
506
530
|
try {
|
|
@@ -508,21 +532,21 @@ function findExpressLogin(app, mounts) {
|
|
|
508
532
|
} catch {
|
|
509
533
|
return null;
|
|
510
534
|
}
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
535
|
+
const verifies = candidates(app, routes, VERIFY_RULE);
|
|
536
|
+
const logins = candidates(app, routes, LOGIN_RULE);
|
|
537
|
+
if (logins.length > 0) {
|
|
538
|
+
const [first2, ...others2] = logins;
|
|
539
|
+
return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2.map((c) => `POST ${c.path}`) };
|
|
540
|
+
}
|
|
541
|
+
const pairs = [];
|
|
542
|
+
for (const start of candidates(app, routes, START_RULE)) {
|
|
543
|
+
const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
|
|
544
|
+
const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
|
|
545
|
+
if (verify) pairs.push({ ...start, passwordless: true, verify });
|
|
522
546
|
}
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
return { login:
|
|
547
|
+
if (pairs.length === 0) return null;
|
|
548
|
+
const [first, ...others] = pairs;
|
|
549
|
+
return { login: first, others: others.map((c) => `POST ${c.path}`) };
|
|
526
550
|
}
|
|
527
551
|
|
|
528
552
|
// src/index.ts
|
|
@@ -562,7 +586,7 @@ function aiReady(options = {}) {
|
|
|
562
586
|
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
563
587
|
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
564
588
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
565
|
-
return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req)));
|
|
589
|
+
return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
566
590
|
};
|
|
567
591
|
const cwd = options.cwd ?? process.cwd();
|
|
568
592
|
const mounts = options.mounts;
|
|
@@ -572,29 +596,67 @@ function aiReady(options = {}) {
|
|
|
572
596
|
if (!brokerInfo || options.login === false) return null;
|
|
573
597
|
if (!loginFound) {
|
|
574
598
|
const given = options.login;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (value) {
|
|
578
|
-
|
|
579
|
-
|
|
599
|
+
let value = given ? { path: given.path, fields: given.fields.map((name) => ({ name, kind: fieldKind(name) })), body: given.body ?? "json" } : findExpressLogin(app, mounts)?.login ?? null;
|
|
600
|
+
const verify = options.loginVerify;
|
|
601
|
+
if (value && verify !== void 0) {
|
|
602
|
+
const { verify: _found, ...first } = value;
|
|
603
|
+
const kind = (name) => verifyFields2([name], true)?.[0]?.kind ?? fieldKind(name);
|
|
604
|
+
value = verify === false ? first : { ...first, verify: { path: verify.path, fields: verify.fields.map((name) => ({ name, kind: kind(name) })), body: verify.body ?? "json" } };
|
|
580
605
|
}
|
|
606
|
+
loginFound = { value };
|
|
607
|
+
if (value) cfg.signedOutPaths = [value.path];
|
|
581
608
|
}
|
|
582
609
|
return loginFound.value;
|
|
583
610
|
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
611
|
+
let stacks;
|
|
612
|
+
const stacksOf = () => {
|
|
613
|
+
if (!stacks) {
|
|
614
|
+
try {
|
|
615
|
+
stacks = authStacksOf(dependencyNames(JSON.parse(readFileSync(join3(cwd, "package.json"), "utf8")))).map((s) => s.pkg);
|
|
616
|
+
} catch {
|
|
617
|
+
stacks = [];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return stacks;
|
|
621
|
+
};
|
|
622
|
+
const report = (req, resource) => {
|
|
623
|
+
const login = loginOf(req.app);
|
|
624
|
+
const where = login ? `POST ${login.path}${login.verify ? `, then a code: POST ${login.verify.path}` : ""}` : void 0;
|
|
625
|
+
return reportAppLogin(cfg.auth, resource, login, { ...where ? { where } : {}, stacks: stacksOf(), log: logReady });
|
|
626
|
+
};
|
|
627
|
+
const appLoginFor = (req, login) => {
|
|
628
|
+
const send = (step, values, reading, cookies = []) => {
|
|
629
|
+
const body = step.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
630
|
+
const request = new Request(`http://app.internal${step.path}`, {
|
|
591
631
|
method: "POST",
|
|
592
|
-
headers: {
|
|
632
|
+
headers: {
|
|
633
|
+
"content-type": step.body === "form" ? "application/x-www-form-urlencoded" : "application/json",
|
|
634
|
+
...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {},
|
|
635
|
+
...cookies.length ? { cookie: cookies.map((c) => `${c.name}=${c.value}`).join("; ") } : {}
|
|
636
|
+
},
|
|
593
637
|
body
|
|
594
638
|
});
|
|
595
|
-
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(loginResultFromResponse);
|
|
596
|
-
}
|
|
597
|
-
|
|
639
|
+
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then((res) => loginResultFromResponse(res, reading));
|
|
640
|
+
};
|
|
641
|
+
const verify = login.verify;
|
|
642
|
+
return {
|
|
643
|
+
label: `POST ${login.path}`,
|
|
644
|
+
fields: login.fields,
|
|
645
|
+
signedOutPaths: [login.path],
|
|
646
|
+
...login.passwordless ? { passwordless: true } : {},
|
|
647
|
+
attempt: (values) => send(login, values, {
|
|
648
|
+
...verify ? { verifyPaths: [verify.path], verifyFields: verify.fields.map((f) => f.name) } : {},
|
|
649
|
+
...login.passwordless ? { start: true } : {}
|
|
650
|
+
}),
|
|
651
|
+
...verify ? {
|
|
652
|
+
verify: {
|
|
653
|
+
label: `POST ${verify.path}`,
|
|
654
|
+
fields: verify.fields,
|
|
655
|
+
attempt: (values, ctx) => send(verify, values, { carried: ctx.carry.cookies, verifyPaths: [verify.path] }, ctx.carry.cookies)
|
|
656
|
+
}
|
|
657
|
+
} : {}
|
|
658
|
+
};
|
|
659
|
+
};
|
|
598
660
|
const descriptions = options.descriptions;
|
|
599
661
|
const openapi = options.openapi;
|
|
600
662
|
try {
|
|
@@ -638,7 +700,9 @@ function aiReady(options = {}) {
|
|
|
638
700
|
}
|
|
639
701
|
if (brokerGate && (path === PRM_ROOT || path === `${PRM_ROOT}${MCP_PATH}`)) {
|
|
640
702
|
const resource = resourceOf(req, path, MCP_PATH);
|
|
641
|
-
void
|
|
703
|
+
void report(req, resource).then(
|
|
704
|
+
(verdict) => verdict === "open" ? void res.writeHead(404).end("not found") : sendWebResponse(res, brokerGate.metadataResponse(req.method, resource))
|
|
705
|
+
);
|
|
642
706
|
return;
|
|
643
707
|
}
|
|
644
708
|
if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
|
|
@@ -654,11 +718,13 @@ function aiReady(options = {}) {
|
|
|
654
718
|
console.warn("[agentready] oauth gate failed:", err);
|
|
655
719
|
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
656
720
|
return null;
|
|
657
|
-
}) : brokerGate ?
|
|
721
|
+
}) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
|
|
722
|
+
if (verdict === "open") return {};
|
|
723
|
+
const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
|
|
658
724
|
if (result.ok) return {};
|
|
659
725
|
await sendWebResponse(res, result.response);
|
|
660
726
|
return null;
|
|
661
|
-
}
|
|
727
|
+
}).catch((err) => {
|
|
662
728
|
console.warn("[agentready] broker check failed:", err);
|
|
663
729
|
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
664
730
|
return null;
|
|
@@ -700,7 +766,7 @@ function aiReady(options = {}) {
|
|
|
700
766
|
async function serveLoginPage(req, res, login) {
|
|
701
767
|
const original = String(req.originalUrl ?? req.url);
|
|
702
768
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
703
|
-
const url = new URL(publicRequestUrl(local, headersOf(req)));
|
|
769
|
+
const url = new URL(publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
704
770
|
url.search = new URL(local).search;
|
|
705
771
|
let body;
|
|
706
772
|
if (req.method === "POST") {
|
|
@@ -711,7 +777,7 @@ function aiReady(options = {}) {
|
|
|
711
777
|
let size = 0;
|
|
712
778
|
for await (const chunk of req) {
|
|
713
779
|
size += chunk.length;
|
|
714
|
-
if (size >
|
|
780
|
+
if (size > 32 * 1024) break;
|
|
715
781
|
chunks.push(chunk);
|
|
716
782
|
}
|
|
717
783
|
body = Buffer.concat(chunks).toString("utf8");
|
|
@@ -798,6 +864,9 @@ async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
|
798
864
|
}
|
|
799
865
|
return createNodeMcpHandler({ getSurface, getKb, cfg });
|
|
800
866
|
}
|
|
867
|
+
function trustsProxy(req) {
|
|
868
|
+
return Boolean(req.app?.get?.("trust proxy"));
|
|
869
|
+
}
|
|
801
870
|
export {
|
|
802
871
|
AIREADY_OPTIONS_TAG,
|
|
803
872
|
aiReady,
|
package/dist/scan-preload.cjs
CHANGED
|
@@ -2074,12 +2074,83 @@ function requireHttpsBaseUrl(optionName, value) {
|
|
|
2074
2074
|
return value.replace(/\/+$/, "");
|
|
2075
2075
|
}
|
|
2076
2076
|
|
|
2077
|
+
// ../core/src/seal.ts
|
|
2078
|
+
init_cjs_shims();
|
|
2079
|
+
function toBase64Url(bytes) {
|
|
2080
|
+
let s = "";
|
|
2081
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2082
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2083
|
+
}
|
|
2084
|
+
function fromBase64Url(text) {
|
|
2085
|
+
if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;
|
|
2086
|
+
try {
|
|
2087
|
+
const bin = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
2088
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
2089
|
+
} catch {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
function createSealer(secret, label) {
|
|
2094
|
+
let key;
|
|
2095
|
+
const keyOf2 = () => key ??= crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${label}
|
|
2096
|
+
${secret}`)).then((material) => crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]));
|
|
2097
|
+
return {
|
|
2098
|
+
async seal(text) {
|
|
2099
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
2100
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await keyOf2(), new TextEncoder().encode(text)));
|
|
2101
|
+
const out = new Uint8Array(iv.length + sealed.length);
|
|
2102
|
+
out.set(iv);
|
|
2103
|
+
out.set(sealed, iv.length);
|
|
2104
|
+
return toBase64Url(out);
|
|
2105
|
+
},
|
|
2106
|
+
async unseal(token) {
|
|
2107
|
+
const bytes = fromBase64Url(token);
|
|
2108
|
+
if (!bytes || bytes.length < 12 + 16) return null;
|
|
2109
|
+
try {
|
|
2110
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.slice(0, 12) }, await keyOf2(), bytes.slice(12));
|
|
2111
|
+
return new TextDecoder().decode(plain);
|
|
2112
|
+
} catch {
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// ../core/src/login-theme.ts
|
|
2120
|
+
init_cjs_shims();
|
|
2121
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
2122
|
+
var KEYS = ["accent", "background", "surface", "text"];
|
|
2123
|
+
function parseLoginTheme(raw) {
|
|
2124
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2125
|
+
const o = raw;
|
|
2126
|
+
if (Object.keys(o).length !== KEYS.length) return null;
|
|
2127
|
+
const out = {};
|
|
2128
|
+
for (const k of KEYS) {
|
|
2129
|
+
const v = o[k];
|
|
2130
|
+
if (typeof v !== "string" || !HEX.test(v)) return null;
|
|
2131
|
+
out[k] = v.toLowerCase();
|
|
2132
|
+
}
|
|
2133
|
+
return out;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2077
2136
|
// ../core/src/hosted-resolver.ts
|
|
2078
2137
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
2079
2138
|
var REPORT_TIMEOUT_MS = 3e3;
|
|
2139
|
+
var REPORT_REFRESH_MS = 6e4;
|
|
2080
2140
|
var DEFAULT_MAX_CACHE_MS = 3e4;
|
|
2081
2141
|
var EXPIRY_SKEW_MS = 6e4;
|
|
2082
2142
|
var MAX_CACHE_ENTRIES = 500;
|
|
2143
|
+
function signInMethodsOf(body) {
|
|
2144
|
+
const methods = body?.signIn?.methods;
|
|
2145
|
+
if (!Array.isArray(methods)) return null;
|
|
2146
|
+
return methods.map((m) => {
|
|
2147
|
+
const { kind, label } = m && typeof m === "object" ? m : {};
|
|
2148
|
+
return {
|
|
2149
|
+
kind: kind === "app-login" || kind === "oauth" || kind === "session" ? kind : "other",
|
|
2150
|
+
label: typeof label === "string" && label.trim() ? label.trim().slice(0, 100) : "a sign-in method"
|
|
2151
|
+
};
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2083
2154
|
function createHostedAuthResolver(config) {
|
|
2084
2155
|
const givenKey = config.key ?? config.licenseKey;
|
|
2085
2156
|
const projectId = config.projectId ?? (config.key ? parseExposifyKey(config.key)?.projectId : void 0);
|
|
@@ -2173,9 +2244,13 @@ function createHostedAuthResolver(config) {
|
|
|
2173
2244
|
}
|
|
2174
2245
|
const sessionWrites = /* @__PURE__ */ new Map();
|
|
2175
2246
|
const reported = /* @__PURE__ */ new Map();
|
|
2176
|
-
|
|
2247
|
+
let passwordDirect;
|
|
2248
|
+
const apiUrl = requireHttpsBaseUrl("apiUrl", config.apiUrl ?? readEnv(API_URL_ENV) ?? DEFAULT_API_URL);
|
|
2249
|
+
const stepSealer = createSealer(licenseKey, "exposify login-step v1");
|
|
2177
2250
|
const info = {
|
|
2178
2251
|
issuer,
|
|
2252
|
+
sealLoginStep: (text) => stepSealer.seal(text),
|
|
2253
|
+
unsealLoginStep: (token) => stepSealer.unseal(token),
|
|
2179
2254
|
resource: config.resource,
|
|
2180
2255
|
// Answers from the same cache, so the tool call right after a check doesn't ask the broker again.
|
|
2181
2256
|
async check(authorization) {
|
|
@@ -2192,7 +2267,8 @@ function createHostedAuthResolver(config) {
|
|
|
2192
2267
|
live: true,
|
|
2193
2268
|
clientName: typeof res.body.clientName === "string" ? res.body.clientName : "An AI assistant",
|
|
2194
2269
|
appName: typeof res.body.appName === "string" ? res.body.appName : null,
|
|
2195
|
-
resource: typeof res.body.resource === "string" ? res.body.resource : null
|
|
2270
|
+
resource: typeof res.body.resource === "string" ? res.body.resource : null,
|
|
2271
|
+
theme: parseLoginTheme(res.body.theme)
|
|
2196
2272
|
};
|
|
2197
2273
|
},
|
|
2198
2274
|
async completeLogin(handle, session) {
|
|
@@ -2220,23 +2296,39 @@ function createHostedAuthResolver(config) {
|
|
|
2220
2296
|
cache.delete(authorization);
|
|
2221
2297
|
await brokerPost("session-ended", {}, authorization);
|
|
2222
2298
|
},
|
|
2299
|
+
supabasePasswordDirect: () => passwordDirect,
|
|
2223
2300
|
reportLogin(report) {
|
|
2224
2301
|
const key = JSON.stringify(report);
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2302
|
+
const send2 = async () => {
|
|
2303
|
+
const res = await fetchImpl(`${apiUrl.replace(/\/+$/, "")}/api/v1/app/login`, {
|
|
2304
|
+
method: "PUT",
|
|
2305
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${licenseKey}` },
|
|
2306
|
+
body: JSON.stringify(report),
|
|
2307
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
2308
|
+
});
|
|
2309
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2310
|
+
const body = await res.json().catch(() => null);
|
|
2311
|
+
passwordDirect = typeof body?.supabasePasswordDirect === "boolean" ? body.supabasePasswordDirect : void 0;
|
|
2312
|
+
return signInMethodsOf(body);
|
|
2313
|
+
};
|
|
2314
|
+
const entry2 = reported.get(key);
|
|
2315
|
+
if (!entry2) {
|
|
2316
|
+
const pending = send2();
|
|
2317
|
+
reported.set(key, { answer: pending, at: Date.now() });
|
|
2237
2318
|
pending.catch(() => reported.delete(key));
|
|
2319
|
+
return pending;
|
|
2320
|
+
}
|
|
2321
|
+
if (!entry2.refreshing && Date.now() - entry2.at >= REPORT_REFRESH_MS) {
|
|
2322
|
+
entry2.refreshing = true;
|
|
2323
|
+
send2().then(
|
|
2324
|
+
(methods) => reported.set(key, { answer: Promise.resolve(methods), at: Date.now() }),
|
|
2325
|
+
() => {
|
|
2326
|
+
entry2.at = Date.now();
|
|
2327
|
+
entry2.refreshing = false;
|
|
2328
|
+
}
|
|
2329
|
+
);
|
|
2238
2330
|
}
|
|
2239
|
-
return
|
|
2331
|
+
return entry2.answer;
|
|
2240
2332
|
}
|
|
2241
2333
|
};
|
|
2242
2334
|
Object.defineProperty(resolver, HOSTED_RESOLVER, { value: info });
|
|
@@ -6483,15 +6575,26 @@ function applyOpenApi(routes, doc) {
|
|
|
6483
6575
|
|
|
6484
6576
|
// ../core/src/own-login.ts
|
|
6485
6577
|
init_cjs_shims();
|
|
6578
|
+
|
|
6579
|
+
// ../core/src/startup.ts
|
|
6580
|
+
init_cjs_shims();
|
|
6581
|
+
|
|
6582
|
+
// ../core/src/own-login.ts
|
|
6486
6583
|
var PASSWORD_NAME = /^(password|passwd|pass|pwd|pin|passcode|secret)$/i;
|
|
6487
6584
|
var PASSWORD_PART = /(password|passwd|passcode)/i;
|
|
6488
6585
|
var CONFIRM_NAME = /(confirm|repeat|again|verify|retype|new).*pass|pass.*(confirm|repeat|again|2)$/i;
|
|
6489
6586
|
var IDENTITY_NAME = /^(email|e-mail|mail|username|user|login|name|phone|phonenumber|phone_number|mobile|account|identifier|handle)$/i;
|
|
6587
|
+
var CODE_NAME = /^(code|otp|totp|pin[-_]?code|(mfa|2fa|two[-_]?factor|verification|verify|sms|one[-_]?time|otp|totp|backup|recovery|auth|security|login|access)[-_]?code)$/i;
|
|
6490
6588
|
var NOT_A_LOGIN = /(register|signup|sign[-_]?up|create[-_]?(account|user)|join|logout|log[-_]?out|signout|sign[-_]?out|reset|forgot|change[-_]?password|invite|verify)/i;
|
|
6491
6589
|
var LOGIN_NAME = /(^|[^a-z])(login|log[-_]?in|signin|sign[-_]?in|authenticate|session|sessions|auth)([^a-z]|$)/i;
|
|
6590
|
+
var VERIFY_NAME = /(verify|verification|2fa|two[-_]?factor|mfa|otp|totp|challenge|code)/i;
|
|
6591
|
+
var NOT_A_VERIFY = /(register|signup|sign[-_]?up|verify[-_]?email|email[-_]?verif|confirm[-_]?email|reset|forgot|invite|coupon|promo|discount|voucher|gift|referral)/i;
|
|
6592
|
+
var START_NAME = /((send|request|email|text|sms)[-_]?(code|otp|token|link))|passwordless|magic|otp/i;
|
|
6593
|
+
var bareName = (name) => name.replace(/^.*[.[]/, "").replace(/]$/, "");
|
|
6492
6594
|
function fieldKind(name) {
|
|
6493
|
-
const bare = name
|
|
6595
|
+
const bare = bareName(name);
|
|
6494
6596
|
if (PASSWORD_NAME.test(bare) || PASSWORD_PART.test(bare)) return "password";
|
|
6597
|
+
if (CODE_NAME.test(bare)) return "code";
|
|
6495
6598
|
if (IDENTITY_NAME.test(bare) || /email/i.test(bare)) return "identity";
|
|
6496
6599
|
return "other";
|
|
6497
6600
|
}
|
|
@@ -6502,7 +6605,21 @@ function loginFields(names) {
|
|
|
6502
6605
|
if (fields.length < 2) return null;
|
|
6503
6606
|
return fields;
|
|
6504
6607
|
}
|
|
6505
|
-
|
|
6608
|
+
function verifyFields(names, tokenIsCode = false) {
|
|
6609
|
+
const fields = names.map((name) => {
|
|
6610
|
+
const kind = fieldKind(name);
|
|
6611
|
+
return { name, kind: kind === "other" && tokenIsCode && /^token$/i.test(bareName(name)) ? "code" : kind };
|
|
6612
|
+
});
|
|
6613
|
+
if (fields.some((f) => f.kind === "password") || fields.filter((f) => f.kind === "code").length !== 1) return null;
|
|
6614
|
+
return fields;
|
|
6615
|
+
}
|
|
6616
|
+
function startFields(names) {
|
|
6617
|
+
const fields = names.map((name) => ({ name, kind: fieldKind(name) }));
|
|
6618
|
+
if (!fields.some((f) => f.kind === "identity") || fields.some((f) => f.kind === "password" || f.kind === "code")) return null;
|
|
6619
|
+
return fields;
|
|
6620
|
+
}
|
|
6621
|
+
var STEP_TTL_MS = 10 * 6e4;
|
|
6622
|
+
var MAX_FORM_BYTES = 32 * 1024;
|
|
6506
6623
|
|
|
6507
6624
|
// src/introspect.ts
|
|
6508
6625
|
init_cjs_shims();
|
|
@@ -6788,6 +6905,26 @@ function parsesJson(app) {
|
|
|
6788
6905
|
if (names.includes("urlencodedParser")) return false;
|
|
6789
6906
|
return null;
|
|
6790
6907
|
}
|
|
6908
|
+
var LOGIN_RULE = { not: NOT_A_LOGIN, name: LOGIN_NAME, mustBeNamed: false, fields: (n) => loginFields(n), session: true };
|
|
6909
|
+
var VERIFY_RULE = { not: NOT_A_VERIFY, name: VERIFY_NAME, mustBeNamed: true, fields: (n, named) => verifyFields(n, named), session: true };
|
|
6910
|
+
var START_RULE = { not: NOT_A_LOGIN, name: new RegExp(`${LOGIN_NAME.source}|${START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => startFields(n), session: false };
|
|
6911
|
+
function candidates(app, routes, rule) {
|
|
6912
|
+
const out = [];
|
|
6913
|
+
for (const route of routes) {
|
|
6914
|
+
if (route.method !== "POST" || route.params.length > 0 || rule.not.test(route.path)) continue;
|
|
6915
|
+
const named = rule.name.test(route.path);
|
|
6916
|
+
if (rule.mustBeNamed && !named) continue;
|
|
6917
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
6918
|
+
const own = sourceOf(handlers.at(-1));
|
|
6919
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
6920
|
+
const fields = rule.fields(bodyFieldsOf(own), named);
|
|
6921
|
+
if (!fields) continue;
|
|
6922
|
+
if (rule.session && !LOGIN_NAME.test(route.path) && !SESSION_CODE.test(all)) continue;
|
|
6923
|
+
out.push({ step: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
6924
|
+
}
|
|
6925
|
+
out.sort((a, b) => Number(b.named) - Number(a.named) || a.step.fields.length - b.step.fields.length);
|
|
6926
|
+
return out.map((c) => c.step);
|
|
6927
|
+
}
|
|
6791
6928
|
function findExpressLogin(app, mounts) {
|
|
6792
6929
|
let routes;
|
|
6793
6930
|
try {
|
|
@@ -6795,21 +6932,21 @@ function findExpressLogin(app, mounts) {
|
|
|
6795
6932
|
} catch {
|
|
6796
6933
|
return null;
|
|
6797
6934
|
}
|
|
6798
|
-
const
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
const
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
const
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
}
|
|
6810
|
-
if (
|
|
6811
|
-
|
|
6812
|
-
return { login:
|
|
6935
|
+
const verifies = candidates(app, routes, VERIFY_RULE);
|
|
6936
|
+
const logins = candidates(app, routes, LOGIN_RULE);
|
|
6937
|
+
if (logins.length > 0) {
|
|
6938
|
+
const [first2, ...others2] = logins;
|
|
6939
|
+
return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2.map((c) => `POST ${c.path}`) };
|
|
6940
|
+
}
|
|
6941
|
+
const pairs = [];
|
|
6942
|
+
for (const start of candidates(app, routes, START_RULE)) {
|
|
6943
|
+
const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
|
|
6944
|
+
const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
|
|
6945
|
+
if (verify) pairs.push({ ...start, passwordless: true, verify });
|
|
6946
|
+
}
|
|
6947
|
+
if (pairs.length === 0) return null;
|
|
6948
|
+
const [first, ...others] = pairs;
|
|
6949
|
+
return { login: first, others: others.map((c) => `POST ${c.path}`) };
|
|
6813
6950
|
}
|
|
6814
6951
|
|
|
6815
6952
|
// bin/scan/env.ts
|
package/dist/scan-preload.js
CHANGED
|
@@ -2078,12 +2078,83 @@ function requireHttpsBaseUrl(optionName, value) {
|
|
|
2078
2078
|
return value.replace(/\/+$/, "");
|
|
2079
2079
|
}
|
|
2080
2080
|
|
|
2081
|
+
// ../core/src/seal.ts
|
|
2082
|
+
init_esm_shims();
|
|
2083
|
+
function toBase64Url(bytes) {
|
|
2084
|
+
let s = "";
|
|
2085
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2086
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2087
|
+
}
|
|
2088
|
+
function fromBase64Url(text) {
|
|
2089
|
+
if (!/^[A-Za-z0-9_-]*$/.test(text)) return null;
|
|
2090
|
+
try {
|
|
2091
|
+
const bin = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
2092
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
2093
|
+
} catch {
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function createSealer(secret, label) {
|
|
2098
|
+
let key;
|
|
2099
|
+
const keyOf2 = () => key ??= crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${label}
|
|
2100
|
+
${secret}`)).then((material) => crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]));
|
|
2101
|
+
return {
|
|
2102
|
+
async seal(text) {
|
|
2103
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
2104
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await keyOf2(), new TextEncoder().encode(text)));
|
|
2105
|
+
const out = new Uint8Array(iv.length + sealed.length);
|
|
2106
|
+
out.set(iv);
|
|
2107
|
+
out.set(sealed, iv.length);
|
|
2108
|
+
return toBase64Url(out);
|
|
2109
|
+
},
|
|
2110
|
+
async unseal(token) {
|
|
2111
|
+
const bytes = fromBase64Url(token);
|
|
2112
|
+
if (!bytes || bytes.length < 12 + 16) return null;
|
|
2113
|
+
try {
|
|
2114
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: bytes.slice(0, 12) }, await keyOf2(), bytes.slice(12));
|
|
2115
|
+
return new TextDecoder().decode(plain);
|
|
2116
|
+
} catch {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// ../core/src/login-theme.ts
|
|
2124
|
+
init_esm_shims();
|
|
2125
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
2126
|
+
var KEYS = ["accent", "background", "surface", "text"];
|
|
2127
|
+
function parseLoginTheme(raw) {
|
|
2128
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
2129
|
+
const o = raw;
|
|
2130
|
+
if (Object.keys(o).length !== KEYS.length) return null;
|
|
2131
|
+
const out = {};
|
|
2132
|
+
for (const k of KEYS) {
|
|
2133
|
+
const v = o[k];
|
|
2134
|
+
if (typeof v !== "string" || !HEX.test(v)) return null;
|
|
2135
|
+
out[k] = v.toLowerCase();
|
|
2136
|
+
}
|
|
2137
|
+
return out;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2081
2140
|
// ../core/src/hosted-resolver.ts
|
|
2082
2141
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
2083
2142
|
var REPORT_TIMEOUT_MS = 3e3;
|
|
2143
|
+
var REPORT_REFRESH_MS = 6e4;
|
|
2084
2144
|
var DEFAULT_MAX_CACHE_MS = 3e4;
|
|
2085
2145
|
var EXPIRY_SKEW_MS = 6e4;
|
|
2086
2146
|
var MAX_CACHE_ENTRIES = 500;
|
|
2147
|
+
function signInMethodsOf(body) {
|
|
2148
|
+
const methods = body?.signIn?.methods;
|
|
2149
|
+
if (!Array.isArray(methods)) return null;
|
|
2150
|
+
return methods.map((m) => {
|
|
2151
|
+
const { kind, label } = m && typeof m === "object" ? m : {};
|
|
2152
|
+
return {
|
|
2153
|
+
kind: kind === "app-login" || kind === "oauth" || kind === "session" ? kind : "other",
|
|
2154
|
+
label: typeof label === "string" && label.trim() ? label.trim().slice(0, 100) : "a sign-in method"
|
|
2155
|
+
};
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2087
2158
|
function createHostedAuthResolver(config) {
|
|
2088
2159
|
const givenKey = config.key ?? config.licenseKey;
|
|
2089
2160
|
const projectId = config.projectId ?? (config.key ? parseExposifyKey(config.key)?.projectId : void 0);
|
|
@@ -2177,9 +2248,13 @@ function createHostedAuthResolver(config) {
|
|
|
2177
2248
|
}
|
|
2178
2249
|
const sessionWrites = /* @__PURE__ */ new Map();
|
|
2179
2250
|
const reported = /* @__PURE__ */ new Map();
|
|
2180
|
-
|
|
2251
|
+
let passwordDirect;
|
|
2252
|
+
const apiUrl = requireHttpsBaseUrl("apiUrl", config.apiUrl ?? readEnv(API_URL_ENV) ?? DEFAULT_API_URL);
|
|
2253
|
+
const stepSealer = createSealer(licenseKey, "exposify login-step v1");
|
|
2181
2254
|
const info = {
|
|
2182
2255
|
issuer,
|
|
2256
|
+
sealLoginStep: (text) => stepSealer.seal(text),
|
|
2257
|
+
unsealLoginStep: (token) => stepSealer.unseal(token),
|
|
2183
2258
|
resource: config.resource,
|
|
2184
2259
|
// Answers from the same cache, so the tool call right after a check doesn't ask the broker again.
|
|
2185
2260
|
async check(authorization) {
|
|
@@ -2196,7 +2271,8 @@ function createHostedAuthResolver(config) {
|
|
|
2196
2271
|
live: true,
|
|
2197
2272
|
clientName: typeof res.body.clientName === "string" ? res.body.clientName : "An AI assistant",
|
|
2198
2273
|
appName: typeof res.body.appName === "string" ? res.body.appName : null,
|
|
2199
|
-
resource: typeof res.body.resource === "string" ? res.body.resource : null
|
|
2274
|
+
resource: typeof res.body.resource === "string" ? res.body.resource : null,
|
|
2275
|
+
theme: parseLoginTheme(res.body.theme)
|
|
2200
2276
|
};
|
|
2201
2277
|
},
|
|
2202
2278
|
async completeLogin(handle, session) {
|
|
@@ -2224,23 +2300,39 @@ function createHostedAuthResolver(config) {
|
|
|
2224
2300
|
cache.delete(authorization);
|
|
2225
2301
|
await brokerPost("session-ended", {}, authorization);
|
|
2226
2302
|
},
|
|
2303
|
+
supabasePasswordDirect: () => passwordDirect,
|
|
2227
2304
|
reportLogin(report) {
|
|
2228
2305
|
const key = JSON.stringify(report);
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2306
|
+
const send2 = async () => {
|
|
2307
|
+
const res = await fetchImpl(`${apiUrl.replace(/\/+$/, "")}/api/v1/app/login`, {
|
|
2308
|
+
method: "PUT",
|
|
2309
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${licenseKey}` },
|
|
2310
|
+
body: JSON.stringify(report),
|
|
2311
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
2312
|
+
});
|
|
2313
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2314
|
+
const body = await res.json().catch(() => null);
|
|
2315
|
+
passwordDirect = typeof body?.supabasePasswordDirect === "boolean" ? body.supabasePasswordDirect : void 0;
|
|
2316
|
+
return signInMethodsOf(body);
|
|
2317
|
+
};
|
|
2318
|
+
const entry2 = reported.get(key);
|
|
2319
|
+
if (!entry2) {
|
|
2320
|
+
const pending = send2();
|
|
2321
|
+
reported.set(key, { answer: pending, at: Date.now() });
|
|
2241
2322
|
pending.catch(() => reported.delete(key));
|
|
2323
|
+
return pending;
|
|
2324
|
+
}
|
|
2325
|
+
if (!entry2.refreshing && Date.now() - entry2.at >= REPORT_REFRESH_MS) {
|
|
2326
|
+
entry2.refreshing = true;
|
|
2327
|
+
send2().then(
|
|
2328
|
+
(methods) => reported.set(key, { answer: Promise.resolve(methods), at: Date.now() }),
|
|
2329
|
+
() => {
|
|
2330
|
+
entry2.at = Date.now();
|
|
2331
|
+
entry2.refreshing = false;
|
|
2332
|
+
}
|
|
2333
|
+
);
|
|
2242
2334
|
}
|
|
2243
|
-
return
|
|
2335
|
+
return entry2.answer;
|
|
2244
2336
|
}
|
|
2245
2337
|
};
|
|
2246
2338
|
Object.defineProperty(resolver, HOSTED_RESOLVER, { value: info });
|
|
@@ -6487,15 +6579,26 @@ function applyOpenApi(routes, doc) {
|
|
|
6487
6579
|
|
|
6488
6580
|
// ../core/src/own-login.ts
|
|
6489
6581
|
init_esm_shims();
|
|
6582
|
+
|
|
6583
|
+
// ../core/src/startup.ts
|
|
6584
|
+
init_esm_shims();
|
|
6585
|
+
|
|
6586
|
+
// ../core/src/own-login.ts
|
|
6490
6587
|
var PASSWORD_NAME = /^(password|passwd|pass|pwd|pin|passcode|secret)$/i;
|
|
6491
6588
|
var PASSWORD_PART = /(password|passwd|passcode)/i;
|
|
6492
6589
|
var CONFIRM_NAME = /(confirm|repeat|again|verify|retype|new).*pass|pass.*(confirm|repeat|again|2)$/i;
|
|
6493
6590
|
var IDENTITY_NAME = /^(email|e-mail|mail|username|user|login|name|phone|phonenumber|phone_number|mobile|account|identifier|handle)$/i;
|
|
6591
|
+
var CODE_NAME = /^(code|otp|totp|pin[-_]?code|(mfa|2fa|two[-_]?factor|verification|verify|sms|one[-_]?time|otp|totp|backup|recovery|auth|security|login|access)[-_]?code)$/i;
|
|
6494
6592
|
var NOT_A_LOGIN = /(register|signup|sign[-_]?up|create[-_]?(account|user)|join|logout|log[-_]?out|signout|sign[-_]?out|reset|forgot|change[-_]?password|invite|verify)/i;
|
|
6495
6593
|
var LOGIN_NAME = /(^|[^a-z])(login|log[-_]?in|signin|sign[-_]?in|authenticate|session|sessions|auth)([^a-z]|$)/i;
|
|
6594
|
+
var VERIFY_NAME = /(verify|verification|2fa|two[-_]?factor|mfa|otp|totp|challenge|code)/i;
|
|
6595
|
+
var NOT_A_VERIFY = /(register|signup|sign[-_]?up|verify[-_]?email|email[-_]?verif|confirm[-_]?email|reset|forgot|invite|coupon|promo|discount|voucher|gift|referral)/i;
|
|
6596
|
+
var START_NAME = /((send|request|email|text|sms)[-_]?(code|otp|token|link))|passwordless|magic|otp/i;
|
|
6597
|
+
var bareName = (name) => name.replace(/^.*[.[]/, "").replace(/]$/, "");
|
|
6496
6598
|
function fieldKind(name) {
|
|
6497
|
-
const bare = name
|
|
6599
|
+
const bare = bareName(name);
|
|
6498
6600
|
if (PASSWORD_NAME.test(bare) || PASSWORD_PART.test(bare)) return "password";
|
|
6601
|
+
if (CODE_NAME.test(bare)) return "code";
|
|
6499
6602
|
if (IDENTITY_NAME.test(bare) || /email/i.test(bare)) return "identity";
|
|
6500
6603
|
return "other";
|
|
6501
6604
|
}
|
|
@@ -6506,7 +6609,21 @@ function loginFields(names) {
|
|
|
6506
6609
|
if (fields.length < 2) return null;
|
|
6507
6610
|
return fields;
|
|
6508
6611
|
}
|
|
6509
|
-
|
|
6612
|
+
function verifyFields(names, tokenIsCode = false) {
|
|
6613
|
+
const fields = names.map((name) => {
|
|
6614
|
+
const kind = fieldKind(name);
|
|
6615
|
+
return { name, kind: kind === "other" && tokenIsCode && /^token$/i.test(bareName(name)) ? "code" : kind };
|
|
6616
|
+
});
|
|
6617
|
+
if (fields.some((f) => f.kind === "password") || fields.filter((f) => f.kind === "code").length !== 1) return null;
|
|
6618
|
+
return fields;
|
|
6619
|
+
}
|
|
6620
|
+
function startFields(names) {
|
|
6621
|
+
const fields = names.map((name) => ({ name, kind: fieldKind(name) }));
|
|
6622
|
+
if (!fields.some((f) => f.kind === "identity") || fields.some((f) => f.kind === "password" || f.kind === "code")) return null;
|
|
6623
|
+
return fields;
|
|
6624
|
+
}
|
|
6625
|
+
var STEP_TTL_MS = 10 * 6e4;
|
|
6626
|
+
var MAX_FORM_BYTES = 32 * 1024;
|
|
6510
6627
|
|
|
6511
6628
|
// src/introspect.ts
|
|
6512
6629
|
init_esm_shims();
|
|
@@ -6792,6 +6909,26 @@ function parsesJson(app) {
|
|
|
6792
6909
|
if (names.includes("urlencodedParser")) return false;
|
|
6793
6910
|
return null;
|
|
6794
6911
|
}
|
|
6912
|
+
var LOGIN_RULE = { not: NOT_A_LOGIN, name: LOGIN_NAME, mustBeNamed: false, fields: (n) => loginFields(n), session: true };
|
|
6913
|
+
var VERIFY_RULE = { not: NOT_A_VERIFY, name: VERIFY_NAME, mustBeNamed: true, fields: (n, named) => verifyFields(n, named), session: true };
|
|
6914
|
+
var START_RULE = { not: NOT_A_LOGIN, name: new RegExp(`${LOGIN_NAME.source}|${START_NAME.source}`, "i"), mustBeNamed: true, fields: (n) => startFields(n), session: false };
|
|
6915
|
+
function candidates(app, routes, rule) {
|
|
6916
|
+
const out = [];
|
|
6917
|
+
for (const route of routes) {
|
|
6918
|
+
if (route.method !== "POST" || route.params.length > 0 || rule.not.test(route.path)) continue;
|
|
6919
|
+
const named = rule.name.test(route.path);
|
|
6920
|
+
if (rule.mustBeNamed && !named) continue;
|
|
6921
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
6922
|
+
const own = sourceOf(handlers.at(-1));
|
|
6923
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
6924
|
+
const fields = rule.fields(bodyFieldsOf(own), named);
|
|
6925
|
+
if (!fields) continue;
|
|
6926
|
+
if (rule.session && !LOGIN_NAME.test(route.path) && !SESSION_CODE.test(all)) continue;
|
|
6927
|
+
out.push({ step: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
6928
|
+
}
|
|
6929
|
+
out.sort((a, b) => Number(b.named) - Number(a.named) || a.step.fields.length - b.step.fields.length);
|
|
6930
|
+
return out.map((c) => c.step);
|
|
6931
|
+
}
|
|
6795
6932
|
function findExpressLogin(app, mounts) {
|
|
6796
6933
|
let routes;
|
|
6797
6934
|
try {
|
|
@@ -6799,21 +6936,21 @@ function findExpressLogin(app, mounts) {
|
|
|
6799
6936
|
} catch {
|
|
6800
6937
|
return null;
|
|
6801
6938
|
}
|
|
6802
|
-
const
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
const
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
const
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
}
|
|
6814
|
-
if (
|
|
6815
|
-
|
|
6816
|
-
return { login:
|
|
6939
|
+
const verifies = candidates(app, routes, VERIFY_RULE);
|
|
6940
|
+
const logins = candidates(app, routes, LOGIN_RULE);
|
|
6941
|
+
if (logins.length > 0) {
|
|
6942
|
+
const [first2, ...others2] = logins;
|
|
6943
|
+
return { login: { ...first2, ...verifies[0] ? { verify: verifies[0] } : {} }, others: others2.map((c) => `POST ${c.path}`) };
|
|
6944
|
+
}
|
|
6945
|
+
const pairs = [];
|
|
6946
|
+
for (const start of candidates(app, routes, START_RULE)) {
|
|
6947
|
+
const who = new Set(start.fields.filter((f) => f.kind === "identity").map((f) => f.name));
|
|
6948
|
+
const verify = verifies.find((v) => v.fields.some((f) => who.has(f.name))) ?? verifies.find((v) => !v.fields.some((f) => f.kind === "identity"));
|
|
6949
|
+
if (verify) pairs.push({ ...start, passwordless: true, verify });
|
|
6950
|
+
}
|
|
6951
|
+
if (pairs.length === 0) return null;
|
|
6952
|
+
const [first, ...others] = pairs;
|
|
6953
|
+
return { login: first, others: others.map((c) => `POST ${c.path}`) };
|
|
6817
6954
|
}
|
|
6818
6955
|
|
|
6819
6956
|
// bin/scan/env.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/express",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"prepack": "yarn build"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@exposify/cli": "0.0
|
|
30
|
-
"@exposify/core": "0.
|
|
29
|
+
"@exposify/cli": "0.1.0",
|
|
30
|
+
"@exposify/core": "0.4.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"express": ">=4 <6"
|