@exposify/express 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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-express setup` and
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-express setup`
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-express setup
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-express describe` any time, replaces those:
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-express inspect` prints every route with its tool name,
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-express inspect --snapshot` writes
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-express inspect --check` in CI: it exits 1,
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-express",
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 candidates = [];
515
- for (const route of routes) {
516
- if (route.method !== "POST" || route.params.length > 0 || import_core5.NOT_A_LOGIN.test(route.path)) 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 = (0, import_core5.loginFields)(bodyFieldsOf(own));
521
- if (!fields) continue;
522
- const named = import_core5.LOGIN_NAME.test(route.path);
523
- if (!named && !SESSION_CODE.test(all)) continue;
524
- candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
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 (candidates.length === 0) return null;
527
- candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
528
- return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
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
- const 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;
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
- return (0, import_core6.reportAppLogin)(cfg.auth, resource, login, { ...login ? { where: `POST ${login.path}` } : {}, stacks: stacksOf(), log: logReady });
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
- label: `POST ${login.path}`,
594
- fields: login.fields,
595
- signedOutPaths: [login.path],
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: { "content-type": login.body === "form" ? "application/x-www-form-urlencoded" : "application/json", ...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {} },
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 > 16 * 1024) break;
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 `exposify-express setup`/`describe`.
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 `exposify-express setup`/`describe`.
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 candidates = [];
515
- for (const route of routes) {
516
- if (route.method !== "POST" || route.params.length > 0 || NOT_A_LOGIN.test(route.path)) 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 = loginFields(bodyFieldsOf(own));
521
- if (!fields) continue;
522
- const named = LOGIN_NAME.test(route.path);
523
- if (!named && !SESSION_CODE.test(all)) continue;
524
- candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
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 (candidates.length === 0) return null;
527
- candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
528
- return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
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
- const value = given ? { path: given.path, fields: given.fields.map((name) => ({ name, kind: fieldKind(name) })), body: given.body ?? "json" } : findExpressLogin(app, mounts)?.login ?? null;
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
- return reportAppLogin(cfg.auth, resource, login, { ...login ? { where: `POST ${login.path}` } : {}, stacks: stacksOf(), log: logReady });
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
- label: `POST ${login.path}`,
601
- fields: login.fields,
602
- signedOutPaths: [login.path],
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: { "content-type": login.body === "form" ? "application/x-www-form-urlencoded" : "application/json", ...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {} },
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 > 16 * 1024) break;
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,
@@ -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
- return signInMethodsOf(await res.json().catch(() => null));
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.replace(/^.*[.[]/, "").replace(/]$/, "");
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
- var MAX_FORM_BYTES = 16 * 1024;
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 candidates = [];
6829
- for (const route of routes) {
6830
- if (route.method !== "POST" || route.params.length > 0 || NOT_A_LOGIN.test(route.path)) continue;
6831
- const handlers = routeHandlers.get(route) ?? [];
6832
- const own = sourceOf(handlers.at(-1));
6833
- const all = handlers.map(sourceOf).join("\n");
6834
- const fields = loginFields(bodyFieldsOf(own));
6835
- if (!fields) continue;
6836
- const named = LOGIN_NAME.test(route.path);
6837
- if (!named && !SESSION_CODE.test(all)) continue;
6838
- candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
6839
- }
6840
- if (candidates.length === 0) return null;
6841
- candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
6842
- return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
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
@@ -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
- return signInMethodsOf(await res.json().catch(() => null));
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.replace(/^.*[.[]/, "").replace(/]$/, "");
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
- var MAX_FORM_BYTES = 16 * 1024;
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 candidates = [];
6833
- for (const route of routes) {
6834
- if (route.method !== "POST" || route.params.length > 0 || NOT_A_LOGIN.test(route.path)) continue;
6835
- const handlers = routeHandlers.get(route) ?? [];
6836
- const own = sourceOf(handlers.at(-1));
6837
- const all = handlers.map(sourceOf).join("\n");
6838
- const fields = loginFields(bodyFieldsOf(own));
6839
- if (!fields) continue;
6840
- const named = LOGIN_NAME.test(route.path);
6841
- if (!named && !SESSION_CODE.test(all)) continue;
6842
- candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
6843
- }
6844
- if (candidates.length === 0) return null;
6845
- candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
6846
- return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
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.10",
3
+ "version": "0.3.11",
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.2",
30
- "@exposify/core": "0.3.14"
29
+ "@exposify/cli": "0.0.3",
30
+ "@exposify/core": "0.3.16"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "express": ">=4 <6"