@exposify/express 0.3.10 → 0.3.12
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 +20 -8
- package/dist/cli.js +1 -1
- package/dist/index.cjs +79 -30
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +81 -31
- package/dist/scan-preload.cjs +126 -19
- package/dist/scan-preload.js +126 -19
- 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,23 @@ 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
|
+
|
|
176
188
|
**No login found, and no sign-in method in the dashboard?** Then agents use
|
|
177
189
|
your app signed out: `/mcp` doesn't ask them to sign in, and your own checks
|
|
178
190
|
decide what they can do. The startup log says which it is
|
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
|
@@ -504,6 +504,26 @@ function parsesJson(app) {
|
|
|
504
504
|
if (names.includes("urlencodedParser")) return false;
|
|
505
505
|
return null;
|
|
506
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
|
+
}
|
|
507
527
|
function findExpressLogin(app, mounts) {
|
|
508
528
|
let routes;
|
|
509
529
|
try {
|
|
@@ -511,21 +531,21 @@ function findExpressLogin(app, mounts) {
|
|
|
511
531
|
} catch {
|
|
512
532
|
return null;
|
|
513
533
|
}
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
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 });
|
|
525
545
|
}
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
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}`) };
|
|
529
549
|
}
|
|
530
550
|
|
|
531
551
|
// src/index.ts
|
|
@@ -558,7 +578,7 @@ function aiReady(options = {}) {
|
|
|
558
578
|
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
559
579
|
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
560
580
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
561
|
-
return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req)));
|
|
581
|
+
return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
562
582
|
};
|
|
563
583
|
const cwd = options.cwd ?? process.cwd();
|
|
564
584
|
const mounts = options.mounts;
|
|
@@ -568,7 +588,13 @@ function aiReady(options = {}) {
|
|
|
568
588
|
if (!brokerInfo || options.login === false) return null;
|
|
569
589
|
if (!loginFound) {
|
|
570
590
|
const given = options.login;
|
|
571
|
-
|
|
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" } };
|
|
597
|
+
}
|
|
572
598
|
loginFound = { value };
|
|
573
599
|
if (value) cfg.signedOutPaths = [value.path];
|
|
574
600
|
}
|
|
@@ -587,22 +613,42 @@ function aiReady(options = {}) {
|
|
|
587
613
|
};
|
|
588
614
|
const report = (req, resource) => {
|
|
589
615
|
const login = loginOf(req.app);
|
|
590
|
-
|
|
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 });
|
|
591
618
|
};
|
|
592
|
-
const appLoginFor = (req, login) =>
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
attempt: (values) => {
|
|
597
|
-
const body = login.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
598
|
-
const request = new Request(`http://app.internal${login.path}`, {
|
|
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}`, {
|
|
599
623
|
method: "POST",
|
|
600
|
-
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
|
+
},
|
|
601
629
|
body
|
|
602
630
|
});
|
|
603
|
-
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(import_core6.loginResultFromResponse);
|
|
604
|
-
}
|
|
605
|
-
|
|
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
|
+
};
|
|
606
652
|
const descriptions = options.descriptions;
|
|
607
653
|
const openapi = options.openapi;
|
|
608
654
|
try {
|
|
@@ -712,7 +758,7 @@ function aiReady(options = {}) {
|
|
|
712
758
|
async function serveLoginPage(req, res, login) {
|
|
713
759
|
const original = String(req.originalUrl ?? req.url);
|
|
714
760
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
715
|
-
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) }));
|
|
716
762
|
url.search = new URL(local).search;
|
|
717
763
|
let body;
|
|
718
764
|
if (req.method === "POST") {
|
|
@@ -723,7 +769,7 @@ function aiReady(options = {}) {
|
|
|
723
769
|
let size = 0;
|
|
724
770
|
for await (const chunk of req) {
|
|
725
771
|
size += chunk.length;
|
|
726
|
-
if (size >
|
|
772
|
+
if (size > 32 * 1024) break;
|
|
727
773
|
chunks.push(chunk);
|
|
728
774
|
}
|
|
729
775
|
body = Buffer.concat(chunks).toString("utf8");
|
|
@@ -810,6 +856,9 @@ async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
|
810
856
|
}
|
|
811
857
|
return (0, import_node2.createNodeMcpHandler)({ getSurface, getKb, cfg });
|
|
812
858
|
}
|
|
859
|
+
function trustsProxy(req) {
|
|
860
|
+
return Boolean(req.app?.get?.("trust proxy"));
|
|
861
|
+
}
|
|
813
862
|
// Annotate the CommonJS export names for ESM import in node:
|
|
814
863
|
0 && (module.exports = {
|
|
815
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
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
loginResultFromResponse,
|
|
25
25
|
publicRequestUrl,
|
|
26
26
|
reportAppLogin,
|
|
27
|
+
verifyFields as verifyFields2,
|
|
27
28
|
resolveOptions,
|
|
28
29
|
routeToolsPhrase,
|
|
29
30
|
startupLogEnabled
|
|
@@ -475,7 +476,7 @@ async function readPackageInfo(cwd) {
|
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
// src/login.ts
|
|
478
|
-
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";
|
|
479
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;
|
|
480
481
|
function bodyFieldsOf(source) {
|
|
481
482
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -504,6 +505,26 @@ function parsesJson(app) {
|
|
|
504
505
|
if (names.includes("urlencodedParser")) return false;
|
|
505
506
|
return null;
|
|
506
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
|
+
}
|
|
507
528
|
function findExpressLogin(app, mounts) {
|
|
508
529
|
let routes;
|
|
509
530
|
try {
|
|
@@ -511,21 +532,21 @@ function findExpressLogin(app, mounts) {
|
|
|
511
532
|
} catch {
|
|
512
533
|
return null;
|
|
513
534
|
}
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
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 });
|
|
525
546
|
}
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
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}`) };
|
|
529
550
|
}
|
|
530
551
|
|
|
531
552
|
// src/index.ts
|
|
@@ -565,7 +586,7 @@ function aiReady(options = {}) {
|
|
|
565
586
|
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
566
587
|
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
567
588
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
568
|
-
return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req)));
|
|
589
|
+
return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
569
590
|
};
|
|
570
591
|
const cwd = options.cwd ?? process.cwd();
|
|
571
592
|
const mounts = options.mounts;
|
|
@@ -575,7 +596,13 @@ function aiReady(options = {}) {
|
|
|
575
596
|
if (!brokerInfo || options.login === false) return null;
|
|
576
597
|
if (!loginFound) {
|
|
577
598
|
const given = options.login;
|
|
578
|
-
|
|
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" } };
|
|
605
|
+
}
|
|
579
606
|
loginFound = { value };
|
|
580
607
|
if (value) cfg.signedOutPaths = [value.path];
|
|
581
608
|
}
|
|
@@ -594,22 +621,42 @@ function aiReady(options = {}) {
|
|
|
594
621
|
};
|
|
595
622
|
const report = (req, resource) => {
|
|
596
623
|
const login = loginOf(req.app);
|
|
597
|
-
|
|
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 });
|
|
598
626
|
};
|
|
599
|
-
const appLoginFor = (req, login) =>
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
attempt: (values) => {
|
|
604
|
-
const body = login.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
605
|
-
const request = new Request(`http://app.internal${login.path}`, {
|
|
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}`, {
|
|
606
631
|
method: "POST",
|
|
607
|
-
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
|
+
},
|
|
608
637
|
body
|
|
609
638
|
});
|
|
610
|
-
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(loginResultFromResponse);
|
|
611
|
-
}
|
|
612
|
-
|
|
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
|
+
};
|
|
613
660
|
const descriptions = options.descriptions;
|
|
614
661
|
const openapi = options.openapi;
|
|
615
662
|
try {
|
|
@@ -719,7 +766,7 @@ function aiReady(options = {}) {
|
|
|
719
766
|
async function serveLoginPage(req, res, login) {
|
|
720
767
|
const original = String(req.originalUrl ?? req.url);
|
|
721
768
|
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
722
|
-
const url = new URL(publicRequestUrl(local, headersOf(req)));
|
|
769
|
+
const url = new URL(publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
|
|
723
770
|
url.search = new URL(local).search;
|
|
724
771
|
let body;
|
|
725
772
|
if (req.method === "POST") {
|
|
@@ -730,7 +777,7 @@ function aiReady(options = {}) {
|
|
|
730
777
|
let size = 0;
|
|
731
778
|
for await (const chunk of req) {
|
|
732
779
|
size += chunk.length;
|
|
733
|
-
if (size >
|
|
780
|
+
if (size > 32 * 1024) break;
|
|
734
781
|
chunks.push(chunk);
|
|
735
782
|
}
|
|
736
783
|
body = Buffer.concat(chunks).toString("utf8");
|
|
@@ -817,6 +864,9 @@ async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
|
817
864
|
}
|
|
818
865
|
return createNodeMcpHandler({ getSurface, getKb, cfg });
|
|
819
866
|
}
|
|
867
|
+
function trustsProxy(req) {
|
|
868
|
+
return Boolean(req.app?.get?.("trust proxy"));
|
|
869
|
+
}
|
|
820
870
|
export {
|
|
821
871
|
AIREADY_OPTIONS_TAG,
|
|
822
872
|
aiReady,
|
package/dist/scan-preload.cjs
CHANGED
|
@@ -2074,6 +2074,65 @@ 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;
|
|
@@ -2185,9 +2244,13 @@ function createHostedAuthResolver(config) {
|
|
|
2185
2244
|
}
|
|
2186
2245
|
const sessionWrites = /* @__PURE__ */ new Map();
|
|
2187
2246
|
const reported = /* @__PURE__ */ new Map();
|
|
2247
|
+
let passwordDirect;
|
|
2188
2248
|
const apiUrl = requireHttpsBaseUrl("apiUrl", config.apiUrl ?? readEnv(API_URL_ENV) ?? DEFAULT_API_URL);
|
|
2249
|
+
const stepSealer = createSealer(licenseKey, "exposify login-step v1");
|
|
2189
2250
|
const info = {
|
|
2190
2251
|
issuer,
|
|
2252
|
+
sealLoginStep: (text) => stepSealer.seal(text),
|
|
2253
|
+
unsealLoginStep: (token) => stepSealer.unseal(token),
|
|
2191
2254
|
resource: config.resource,
|
|
2192
2255
|
// Answers from the same cache, so the tool call right after a check doesn't ask the broker again.
|
|
2193
2256
|
async check(authorization) {
|
|
@@ -2204,7 +2267,8 @@ function createHostedAuthResolver(config) {
|
|
|
2204
2267
|
live: true,
|
|
2205
2268
|
clientName: typeof res.body.clientName === "string" ? res.body.clientName : "An AI assistant",
|
|
2206
2269
|
appName: typeof res.body.appName === "string" ? res.body.appName : null,
|
|
2207
|
-
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)
|
|
2208
2272
|
};
|
|
2209
2273
|
},
|
|
2210
2274
|
async completeLogin(handle, session) {
|
|
@@ -2232,6 +2296,7 @@ function createHostedAuthResolver(config) {
|
|
|
2232
2296
|
cache.delete(authorization);
|
|
2233
2297
|
await brokerPost("session-ended", {}, authorization);
|
|
2234
2298
|
},
|
|
2299
|
+
supabasePasswordDirect: () => passwordDirect,
|
|
2235
2300
|
reportLogin(report) {
|
|
2236
2301
|
const key = JSON.stringify(report);
|
|
2237
2302
|
const send2 = async () => {
|
|
@@ -2242,7 +2307,9 @@ function createHostedAuthResolver(config) {
|
|
|
2242
2307
|
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
2243
2308
|
});
|
|
2244
2309
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2245
|
-
|
|
2310
|
+
const body = await res.json().catch(() => null);
|
|
2311
|
+
passwordDirect = typeof body?.supabasePasswordDirect === "boolean" ? body.supabasePasswordDirect : void 0;
|
|
2312
|
+
return signInMethodsOf(body);
|
|
2246
2313
|
};
|
|
2247
2314
|
const entry2 = reported.get(key);
|
|
2248
2315
|
if (!entry2) {
|
|
@@ -6517,11 +6584,17 @@ var PASSWORD_NAME = /^(password|passwd|pass|pwd|pin|passcode|secret)$/i;
|
|
|
6517
6584
|
var PASSWORD_PART = /(password|passwd|passcode)/i;
|
|
6518
6585
|
var CONFIRM_NAME = /(confirm|repeat|again|verify|retype|new).*pass|pass.*(confirm|repeat|again|2)$/i;
|
|
6519
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;
|
|
6520
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;
|
|
6521
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(/]$/, "");
|
|
6522
6594
|
function fieldKind(name) {
|
|
6523
|
-
const bare = name
|
|
6595
|
+
const bare = bareName(name);
|
|
6524
6596
|
if (PASSWORD_NAME.test(bare) || PASSWORD_PART.test(bare)) return "password";
|
|
6597
|
+
if (CODE_NAME.test(bare)) return "code";
|
|
6525
6598
|
if (IDENTITY_NAME.test(bare) || /email/i.test(bare)) return "identity";
|
|
6526
6599
|
return "other";
|
|
6527
6600
|
}
|
|
@@ -6532,7 +6605,21 @@ function loginFields(names) {
|
|
|
6532
6605
|
if (fields.length < 2) return null;
|
|
6533
6606
|
return fields;
|
|
6534
6607
|
}
|
|
6535
|
-
|
|
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;
|
|
6536
6623
|
|
|
6537
6624
|
// src/introspect.ts
|
|
6538
6625
|
init_cjs_shims();
|
|
@@ -6818,6 +6905,26 @@ function parsesJson(app) {
|
|
|
6818
6905
|
if (names.includes("urlencodedParser")) return false;
|
|
6819
6906
|
return null;
|
|
6820
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
|
+
}
|
|
6821
6928
|
function findExpressLogin(app, mounts) {
|
|
6822
6929
|
let routes;
|
|
6823
6930
|
try {
|
|
@@ -6825,21 +6932,21 @@ function findExpressLogin(app, mounts) {
|
|
|
6825
6932
|
} catch {
|
|
6826
6933
|
return null;
|
|
6827
6934
|
}
|
|
6828
|
-
const
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
const
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
}
|
|
6840
|
-
if (
|
|
6841
|
-
|
|
6842
|
-
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}`) };
|
|
6843
6950
|
}
|
|
6844
6951
|
|
|
6845
6952
|
// bin/scan/env.ts
|
package/dist/scan-preload.js
CHANGED
|
@@ -2078,6 +2078,65 @@ 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;
|
|
@@ -2189,9 +2248,13 @@ function createHostedAuthResolver(config) {
|
|
|
2189
2248
|
}
|
|
2190
2249
|
const sessionWrites = /* @__PURE__ */ new Map();
|
|
2191
2250
|
const reported = /* @__PURE__ */ new Map();
|
|
2251
|
+
let passwordDirect;
|
|
2192
2252
|
const apiUrl = requireHttpsBaseUrl("apiUrl", config.apiUrl ?? readEnv(API_URL_ENV) ?? DEFAULT_API_URL);
|
|
2253
|
+
const stepSealer = createSealer(licenseKey, "exposify login-step v1");
|
|
2193
2254
|
const info = {
|
|
2194
2255
|
issuer,
|
|
2256
|
+
sealLoginStep: (text) => stepSealer.seal(text),
|
|
2257
|
+
unsealLoginStep: (token) => stepSealer.unseal(token),
|
|
2195
2258
|
resource: config.resource,
|
|
2196
2259
|
// Answers from the same cache, so the tool call right after a check doesn't ask the broker again.
|
|
2197
2260
|
async check(authorization) {
|
|
@@ -2208,7 +2271,8 @@ function createHostedAuthResolver(config) {
|
|
|
2208
2271
|
live: true,
|
|
2209
2272
|
clientName: typeof res.body.clientName === "string" ? res.body.clientName : "An AI assistant",
|
|
2210
2273
|
appName: typeof res.body.appName === "string" ? res.body.appName : null,
|
|
2211
|
-
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)
|
|
2212
2276
|
};
|
|
2213
2277
|
},
|
|
2214
2278
|
async completeLogin(handle, session) {
|
|
@@ -2236,6 +2300,7 @@ function createHostedAuthResolver(config) {
|
|
|
2236
2300
|
cache.delete(authorization);
|
|
2237
2301
|
await brokerPost("session-ended", {}, authorization);
|
|
2238
2302
|
},
|
|
2303
|
+
supabasePasswordDirect: () => passwordDirect,
|
|
2239
2304
|
reportLogin(report) {
|
|
2240
2305
|
const key = JSON.stringify(report);
|
|
2241
2306
|
const send2 = async () => {
|
|
@@ -2246,7 +2311,9 @@ function createHostedAuthResolver(config) {
|
|
|
2246
2311
|
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
2247
2312
|
});
|
|
2248
2313
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2249
|
-
|
|
2314
|
+
const body = await res.json().catch(() => null);
|
|
2315
|
+
passwordDirect = typeof body?.supabasePasswordDirect === "boolean" ? body.supabasePasswordDirect : void 0;
|
|
2316
|
+
return signInMethodsOf(body);
|
|
2250
2317
|
};
|
|
2251
2318
|
const entry2 = reported.get(key);
|
|
2252
2319
|
if (!entry2) {
|
|
@@ -6521,11 +6588,17 @@ var PASSWORD_NAME = /^(password|passwd|pass|pwd|pin|passcode|secret)$/i;
|
|
|
6521
6588
|
var PASSWORD_PART = /(password|passwd|passcode)/i;
|
|
6522
6589
|
var CONFIRM_NAME = /(confirm|repeat|again|verify|retype|new).*pass|pass.*(confirm|repeat|again|2)$/i;
|
|
6523
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;
|
|
6524
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;
|
|
6525
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(/]$/, "");
|
|
6526
6598
|
function fieldKind(name) {
|
|
6527
|
-
const bare = name
|
|
6599
|
+
const bare = bareName(name);
|
|
6528
6600
|
if (PASSWORD_NAME.test(bare) || PASSWORD_PART.test(bare)) return "password";
|
|
6601
|
+
if (CODE_NAME.test(bare)) return "code";
|
|
6529
6602
|
if (IDENTITY_NAME.test(bare) || /email/i.test(bare)) return "identity";
|
|
6530
6603
|
return "other";
|
|
6531
6604
|
}
|
|
@@ -6536,7 +6609,21 @@ function loginFields(names) {
|
|
|
6536
6609
|
if (fields.length < 2) return null;
|
|
6537
6610
|
return fields;
|
|
6538
6611
|
}
|
|
6539
|
-
|
|
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;
|
|
6540
6627
|
|
|
6541
6628
|
// src/introspect.ts
|
|
6542
6629
|
init_esm_shims();
|
|
@@ -6822,6 +6909,26 @@ function parsesJson(app) {
|
|
|
6822
6909
|
if (names.includes("urlencodedParser")) return false;
|
|
6823
6910
|
return null;
|
|
6824
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
|
+
}
|
|
6825
6932
|
function findExpressLogin(app, mounts) {
|
|
6826
6933
|
let routes;
|
|
6827
6934
|
try {
|
|
@@ -6829,21 +6936,21 @@ function findExpressLogin(app, mounts) {
|
|
|
6829
6936
|
} catch {
|
|
6830
6937
|
return null;
|
|
6831
6938
|
}
|
|
6832
|
-
const
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
const
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
const
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
}
|
|
6844
|
-
if (
|
|
6845
|
-
|
|
6846
|
-
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}`) };
|
|
6847
6954
|
}
|
|
6848
6955
|
|
|
6849
6956
|
// bin/scan/env.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/express",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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.3.
|
|
29
|
+
"@exposify/cli": "0.0.7",
|
|
30
|
+
"@exposify/core": "0.3.17"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"express": ">=4 <6"
|