@luckystack/server 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,56 +1,75 @@
1
- # Changelog
2
-
3
- All notable changes to `@luckystack/server` are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.5.0] - 2026-07-11
11
-
12
- ### Added
13
-
14
- - `OVERLAY_ORDER` exported (consumed by the consumer's `bundleServer.mjs` at
15
- build time — kills the hardcoded-copy drift that silently dropped overlay
16
- slots from prod bundles). New `cron` overlay slot + `@luckystack/cron` in
17
- `OPTIONAL_PACKAGES` (boot auto-wiring).
18
- - `/readyz` database check is pluggable (core `registerDbHealthCheck`):
19
- registered probe built-in Prisma ping (when Prisma is present) →
20
- `'skipped'` for deliberately DB-less projects. Response gains the
21
- tri-state `checks.database`; `checks.prisma` kept for compatibility.
22
-
23
- ### Changed
24
-
25
- - Overlay files that fail to import abort boot with an actionable error
26
- naming the file (was: raw ERR_MODULE_NOT_FOUND).
27
- - Dev SIGINT/SIGTERM dispatches `preServerStop` (2s cap) before exiting so
28
- subscribers (e.g. the cron leader lease) release cleanly.
29
- - `@prisma/client` peer dependency is now optional (ADR 0020).
30
-
31
- ## [0.1.5]
32
-
33
- ### Changed
34
-
35
- - **OAuth post-login redirect no longer reads the `DNS` env var.** `authCallbackRoute`
36
- now redirects to `projectConfig.app.publicUrl` (the public origin where users
37
- browse) after a callback, instead of `process.env.DNS || app.publicUrl`. The
38
- callback is handled on the backend origin but must send the browser back to the
39
- public origin. Set `app.publicUrl` in your `config.ts` (the scaffold does this).
40
-
41
- ### Fixed
42
-
43
- - **CSRF no longer blocks credentials login/register when a session cookie already
44
- exists.** `POST /auth/api/credentials` is the session bootstrap, so requiring a
45
- pre-existing session's CSRF token to authenticate is circular and broke
46
- legitimate same-site re-login/register (403 `auth.csrfMismatch`). That endpoint
47
- is now exempt from CSRF enforcement. This removes no real protection: the session
48
- cookie is `SameSite=Strict`, so a cross-site POST never carries it and the guard
49
- wouldn't have fired anyway. All other `/auth/api/*`, `/api/*`, and `/sync/*`
50
- state-changing routes remain protected.
51
-
52
- ## [0.1.0]
53
-
54
- ### Added
55
-
56
- - Initial public release as part of the LuckyStack package split.
1
+ # Changelog
2
+
3
+ All notable changes to `@luckystack/server` are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.6.0] - 2026-07-12
9
+
10
+ ### Added
11
+
12
+ - **Email-code login + 2FA routes** (ADR 0024, `authSecondFactorRoutes.ts`):
13
+ POST `/auth/api/email-code/request|verify`, `/auth/api/2fa` (completes a
14
+ pending challenge through the session-cookie seam), `/auth/api/2fa/email-code`,
15
+ and the authenticated enrollment routes `/auth/api/2fa/setup|enable|disable|
16
+ recovery-codes` (fresh user re-read via the UserAdapter the session copy is
17
+ sanitized). Registered before the `/auth/api/*` catch-all; per-IP shields.
18
+ - `/auth/providers` now advertises `emailCodeLogin` so the login form can show
19
+ the passwordless entry point.
20
+
21
+ ### Changed
22
+
23
+ - `/auth/api/credentials` relays the 2FA challenge envelope
24
+ (`requiresTwoFactor`, `challengeToken`, `twoFactorMethods` — no session
25
+ transport) when the account has a second factor enrolled.
26
+ - CSRF middleware: the login-completing email-code/2FA routes joined the
27
+ auth-bootstrap exemption set; the authed enrollment routes stay enforced.
28
+
29
+ ## [0.5.0] - 2026-07-11
30
+
31
+ ### Added
32
+
33
+ - `OVERLAY_ORDER` exported (consumed by the consumer's `bundleServer.mjs` at
34
+ build time — kills the hardcoded-copy drift that silently dropped overlay
35
+ slots from prod bundles). New `cron` overlay slot + `@luckystack/cron` in
36
+ `OPTIONAL_PACKAGES` (boot auto-wiring).
37
+ - `/readyz` database check is pluggable (core `registerDbHealthCheck`):
38
+ registered probe built-in Prisma ping (when Prisma is present)
39
+ `'skipped'` for deliberately DB-less projects. Response gains the
40
+ tri-state `checks.database`; `checks.prisma` kept for compatibility.
41
+
42
+ ### Changed
43
+
44
+ - Overlay files that fail to import abort boot with an actionable error
45
+ naming the file (was: raw ERR_MODULE_NOT_FOUND).
46
+ - Dev SIGINT/SIGTERM dispatches `preServerStop` (2s cap) before exiting so
47
+ subscribers (e.g. the cron leader lease) release cleanly.
48
+ - `@prisma/client` peer dependency is now optional (ADR 0020).
49
+
50
+ ## [0.1.5]
51
+
52
+ ### Changed
53
+
54
+ - **OAuth post-login redirect no longer reads the `DNS` env var.** `authCallbackRoute`
55
+ now redirects to `projectConfig.app.publicUrl` (the public origin where users
56
+ browse) after a callback, instead of `process.env.DNS || app.publicUrl`. The
57
+ callback is handled on the backend origin but must send the browser back to the
58
+ public origin. Set `app.publicUrl` in your `config.ts` (the scaffold does this).
59
+
60
+ ### Fixed
61
+
62
+ - **CSRF no longer blocks credentials login/register when a session cookie already
63
+ exists.** `POST /auth/api/credentials` is the session bootstrap, so requiring a
64
+ pre-existing session's CSRF token to authenticate is circular and broke
65
+ legitimate same-site re-login/register (403 `auth.csrfMismatch`). That endpoint
66
+ is now exempt from CSRF enforcement. This removes no real protection: the session
67
+ cookie is `SameSite=Strict`, so a cross-site POST never carries it and the guard
68
+ wouldn't have fired anyway. All other `/auth/api/*`, `/api/*`, and `/sync/*`
69
+ state-changing routes remain protected.
70
+
71
+ ## [0.1.0]
72
+
73
+ ### Added
74
+
75
+ - Initial public release as part of the LuckyStack package split.
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  // src/createServer.ts
9
9
  import http from "http";
10
- import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
10
+ import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
11
11
 
12
12
  // src/httpHandler.ts
13
13
  import { randomUUID } from "crypto";
@@ -17,7 +17,7 @@ import {
17
17
  extractTokenFromRequest as extractTokenFromRequest3,
18
18
  getLogger as getLogger7,
19
19
  getParams,
20
- getProjectConfig as getProjectConfig10,
20
+ getProjectConfig as getProjectConfig12,
21
21
  hasCookie,
22
22
  normalizeOrigin,
23
23
  readSession as readSession3,
@@ -167,7 +167,14 @@ var enforceCsrfOnStateChangingRequest = async ({
167
167
  const isCookieMode = !config.session.basedToken;
168
168
  const isStateChanging = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
169
169
  const isCallbackPath = routePath.startsWith("/auth/callback");
170
- const isAuthBootstrap = routePath === "/auth/api/credentials";
170
+ const AUTH_BOOTSTRAP_PATHS = /* @__PURE__ */ new Set([
171
+ "/auth/api/credentials",
172
+ "/auth/api/email-code/request",
173
+ "/auth/api/email-code/verify",
174
+ "/auth/api/2fa",
175
+ "/auth/api/2fa/email-code"
176
+ ]);
177
+ const isAuthBootstrap = AUTH_BOOTSTRAP_PATHS.has(routePath);
171
178
  const isExemptFromCsrf = isAuthBootstrap || isCallbackPath || isOriginExemptPath(routePath);
172
179
  const isCsrfCandidate = routePath.startsWith("/api/") || routePath.startsWith("/sync/") || routePath.startsWith("/auth/api/") || !routePath.startsWith("/auth/") && !routePath.startsWith("/assets/");
173
180
  if (!(isCookieMode && isStateChanging && isCsrfCandidate && !isExemptFromCsrf)) {
@@ -615,6 +622,18 @@ var handleAuthApiRoute = async ({
615
622
  res.end(JSON.stringify({ status: false, reason: reasonKey }));
616
623
  return true;
617
624
  }
625
+ if (result.requiresTwoFactor) {
626
+ res.setHeader("content-type", "application/json; charset=utf-8");
627
+ res.end(JSON.stringify({
628
+ status: true,
629
+ reason: result.reason,
630
+ requiresTwoFactor: true,
631
+ challengeToken: result.challengeToken,
632
+ twoFactorMethods: result.twoFactorMethods,
633
+ authenticated: false
634
+ }));
635
+ return true;
636
+ }
618
637
  if (result.newToken) {
619
638
  if (token) await login.deleteSession(token, { skipSocketLogout: true });
620
639
  const requestedSessionMode = parseSessionBasedTokenHeader(req.headers["x-session-based-token"]);
@@ -636,8 +655,150 @@ var handleAuthApiRoute = async ({
636
655
  return true;
637
656
  };
638
657
 
658
+ // src/httpRoutes/authSecondFactorRoutes.ts
659
+ import { checkRateLimit as checkRateLimit2, getProjectConfig as getProjectConfig6, resolveClientIp as resolveClientIp2 } from "@luckystack/core";
660
+ var json = (ctx, statusCode, body) => {
661
+ ctx.res.statusCode = statusCode;
662
+ ctx.res.setHeader("content-type", "application/json; charset=utf-8");
663
+ ctx.res.end(JSON.stringify(body));
664
+ return true;
665
+ };
666
+ var methodNotAllowed = (ctx) => {
667
+ ctx.res.statusCode = 405;
668
+ ctx.res.setHeader("Allow", "POST");
669
+ ctx.res.end();
670
+ return true;
671
+ };
672
+ var requesterIpOf = (ctx) => {
673
+ const config = getProjectConfig6();
674
+ return resolveClientIp2({
675
+ rawAddress: ctx.req.socket.remoteAddress,
676
+ headers: ctx.req.headers,
677
+ trustProxy: config.http.trustProxy,
678
+ trustedProxyHopCount: config.http.trustedProxyHopCount
679
+ });
680
+ };
681
+ var ipThrottled = async (ctx, bucket, limit) => {
682
+ const { allowed } = await checkRateLimit2({
683
+ key: `ip:${requesterIpOf(ctx)}:auth:${bucket}`,
684
+ limit,
685
+ windowMs: 15 * 60 * 1e3
686
+ });
687
+ return !allowed;
688
+ };
689
+ var str = (value) => typeof value === "string" ? value : "";
690
+ var sendLoginResult = async (ctx, login, result) => {
691
+ if (!result.status) return json(ctx, 200, { status: false, reason: result.reason });
692
+ if (result.requiresTwoFactor) {
693
+ return json(ctx, 200, {
694
+ status: true,
695
+ reason: result.reason,
696
+ requiresTwoFactor: true,
697
+ challengeToken: result.challengeToken,
698
+ twoFactorMethods: result.twoFactorMethods,
699
+ authenticated: false
700
+ });
701
+ }
702
+ if (result.newToken) {
703
+ if (ctx.token) await login.deleteSession(ctx.token, { skipSocketLogout: true });
704
+ const config = getProjectConfig6();
705
+ const headerValue = ctx.req.headers["x-session-based-token"];
706
+ const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
707
+ const requestedSessionMode = raw === "true" ? true : raw === "false" ? false : null;
708
+ const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;
709
+ if (useSessionBasedToken) {
710
+ ctx.res.setHeader("X-Session-Token", result.newToken);
711
+ } else {
712
+ ctx.res.setHeader("Set-Cookie", `${config.http.sessionCookieName}=${result.newToken}; ${ctx.sessionCookieOptions}`);
713
+ }
714
+ }
715
+ return json(ctx, 200, {
716
+ status: true,
717
+ reason: result.reason,
718
+ session: result.session,
719
+ authenticated: Boolean(result.newToken)
720
+ });
721
+ };
722
+ var requireUser = async (ctx, login) => {
723
+ if (!ctx.token) return null;
724
+ const session = await login.getSession(ctx.token);
725
+ const userId = session?.id;
726
+ if (!userId) return null;
727
+ return login.getUserAdapter().findById(userId);
728
+ };
729
+ var handleAuthEmailCodeRoute = async (ctx) => {
730
+ if (ctx.routePath !== "/auth/api/email-code/request" && ctx.routePath !== "/auth/api/email-code/verify") return false;
731
+ if (ctx.method !== "POST") return methodNotAllowed(ctx);
732
+ const login = await getLogin();
733
+ if (!login) return json(ctx, 200, { status: false, reason: "auth.disabled" });
734
+ const params = ctx.params;
735
+ const requesterIp = requesterIpOf(ctx);
736
+ if (ctx.routePath === "/auth/api/email-code/request") {
737
+ if (await ipThrottled(ctx, "email-code-request", 10)) return json(ctx, 429, { status: false, reason: "api.rateLimitExceeded" });
738
+ const result2 = await login.requestEmailLoginCode({ email: str(params.email), requesterIp });
739
+ return json(ctx, 200, result2.ok ? { status: true } : { status: false, reason: result2.reason });
740
+ }
741
+ if (await ipThrottled(ctx, "email-code-verify", 20)) return json(ctx, 429, { status: false, reason: "api.rateLimitExceeded" });
742
+ const result = await login.verifyEmailLoginCode({
743
+ email: str(params.email),
744
+ code: str(params.code),
745
+ supersedeToken: ctx.token ?? void 0,
746
+ requesterIp
747
+ });
748
+ return sendLoginResult(ctx, login, result);
749
+ };
750
+ var handleAuthTwoFactorRoute = async (ctx) => {
751
+ if (ctx.routePath !== "/auth/api/2fa" && !ctx.routePath.startsWith("/auth/api/2fa/")) return false;
752
+ if (ctx.method !== "POST") return methodNotAllowed(ctx);
753
+ const login = await getLogin();
754
+ if (!login) return json(ctx, 200, { status: false, reason: "auth.disabled" });
755
+ const params = ctx.params;
756
+ const requesterIp = requesterIpOf(ctx);
757
+ if (ctx.routePath === "/auth/api/2fa") {
758
+ if (await ipThrottled(ctx, "2fa-verify", 20)) return json(ctx, 429, { status: false, reason: "api.rateLimitExceeded" });
759
+ const method = str(params.method);
760
+ const result = await login.verifyTwoFactorChallenge({
761
+ challengeToken: str(params.challengeToken),
762
+ code: str(params.code),
763
+ method: method === "email-code" || method === "recovery-code" ? method : "totp",
764
+ supersedeToken: ctx.token ?? void 0,
765
+ requesterIp
766
+ });
767
+ return sendLoginResult(ctx, login, result);
768
+ }
769
+ if (ctx.routePath === "/auth/api/2fa/email-code") {
770
+ if (await ipThrottled(ctx, "2fa-email-code", 5)) return json(ctx, 429, { status: false, reason: "api.rateLimitExceeded" });
771
+ const result = await login.requestTwoFactorEmailCode(str(params.challengeToken));
772
+ return json(ctx, 200, result.ok ? { status: true } : { status: false, reason: result.reason });
773
+ }
774
+ if (await ipThrottled(ctx, "2fa-manage", 15)) return json(ctx, 429, { status: false, reason: "api.rateLimitExceeded" });
775
+ const user = await requireUser(ctx, login);
776
+ if (!user) return json(ctx, 401, { status: false, reason: "api.unauthorized" });
777
+ switch (ctx.routePath) {
778
+ case "/auth/api/2fa/setup": {
779
+ const start = await login.beginTotpEnrollment(user);
780
+ return start.ok ? json(ctx, 200, { status: true, secret: start.secret, otpauthUri: start.otpauthUri }) : json(ctx, 200, { status: false, reason: start.reason });
781
+ }
782
+ case "/auth/api/2fa/enable": {
783
+ const confirmed = await login.confirmTotpEnrollment(user, str(params.code));
784
+ return confirmed.ok ? json(ctx, 200, { status: true, recoveryCodes: confirmed.recoveryCodes }) : json(ctx, 200, { status: false, reason: confirmed.reason });
785
+ }
786
+ case "/auth/api/2fa/disable": {
787
+ const disabled = await login.disableTwoFactor(user, str(params.code));
788
+ return json(ctx, 200, disabled.ok ? { status: true } : { status: false, reason: disabled.reason ?? "login.twoFactorInvalidCode" });
789
+ }
790
+ case "/auth/api/2fa/recovery-codes": {
791
+ const regenerated = await login.regenerateRecoveryCodes(user, str(params.code));
792
+ return regenerated.ok ? json(ctx, 200, { status: true, recoveryCodes: regenerated.recoveryCodes }) : json(ctx, 200, { status: false, reason: regenerated.reason });
793
+ }
794
+ default: {
795
+ return json(ctx, 404, { status: false, reason: "common.404" });
796
+ }
797
+ }
798
+ };
799
+
639
800
  // src/httpRoutes/authLogoutRoute.ts
640
- import { getLogger as getLogger2, getProjectConfig as getProjectConfig6, tryCatch as tryCatch3 } from "@luckystack/core";
801
+ import { getLogger as getLogger2, getProjectConfig as getProjectConfig7, tryCatch as tryCatch3 } from "@luckystack/core";
641
802
  var warnedNonStrictSameSite = false;
642
803
  var handleAuthLogoutRoute = async ({ res, routePath, method, token }) => {
643
804
  if (routePath !== "/auth/logout") return false;
@@ -657,7 +818,7 @@ var handleAuthLogoutRoute = async ({ res, routePath, method, token }) => {
657
818
  }
658
819
  }
659
820
  }
660
- const http2 = getProjectConfig6().http;
821
+ const http2 = getProjectConfig7().http;
661
822
  if (http2.sessionCookieSameSite !== "Strict" && !warnedNonStrictSameSite) {
662
823
  warnedNonStrictSameSite = true;
663
824
  getLogger2().warn(
@@ -676,17 +837,19 @@ var handleAuthLogoutRoute = async ({ res, routePath, method, token }) => {
676
837
  };
677
838
 
678
839
  // src/httpRoutes/authProvidersRoute.ts
840
+ import { getProjectConfig as getProjectConfig8 } from "@luckystack/core";
679
841
  var handleAuthProvidersRoute = async ({ req, res, routePath }) => {
680
842
  if (routePath !== "/auth/providers" || req.method !== "GET") return false;
681
843
  const login = await getLogin();
682
844
  const providers = login ? login.getOAuthProviders().map((provider) => provider.name) : [];
845
+ const emailCodeLogin = Boolean(login) && getProjectConfig8().auth.emailCodeLogin;
683
846
  res.setHeader("Content-Type", "application/json");
684
- res.end(JSON.stringify({ providers }));
847
+ res.end(JSON.stringify({ providers, emailCodeLogin }));
685
848
  return true;
686
849
  };
687
850
 
688
851
  // src/httpRoutes/authCallbackRoute.ts
689
- import { getLogger as getLogger3, getProjectConfig as getProjectConfig7 } from "@luckystack/core";
852
+ import { getLogger as getLogger3, getProjectConfig as getProjectConfig9 } from "@luckystack/core";
690
853
  var handleAuthCallbackRoute = async ({
691
854
  req,
692
855
  res,
@@ -701,7 +864,7 @@ var handleAuthCallbackRoute = async ({
701
864
  res.end("Auth is not enabled");
702
865
  return true;
703
866
  }
704
- const config = getProjectConfig7();
867
+ const config = getProjectConfig9();
705
868
  const sessionCookieName = config.http.sessionCookieName;
706
869
  const shouldLogDev = config.logging.devLogs;
707
870
  const publicOrigin = (config.app.publicUrl || "").replace(/\/+$/, "");
@@ -745,7 +908,7 @@ import {
745
908
  import { handleHttpApiRequest } from "@luckystack/api";
746
909
 
747
910
  // src/sse.ts
748
- import { getProjectConfig as getProjectConfig8 } from "@luckystack/core";
911
+ import { getProjectConfig as getProjectConfig10 } from "@luckystack/core";
749
912
  var isExpectingEventStream = (acceptHeader) => {
750
913
  if (!acceptHeader) return false;
751
914
  const value = Array.isArray(acceptHeader) ? acceptHeader.join(",") : acceptHeader;
@@ -753,7 +916,7 @@ var isExpectingEventStream = (acceptHeader) => {
753
916
  };
754
917
  var queryRequestsStream = (queryString) => {
755
918
  if (!queryString) return false;
756
- const { queryParam, enabledValue } = getProjectConfig8().http.stream;
919
+ const { queryParam, enabledValue } = getProjectConfig10().http.stream;
757
920
  const params = new URLSearchParams(queryString);
758
921
  const value = params.get(queryParam);
759
922
  return value === enabledValue || value === "1";
@@ -769,7 +932,7 @@ var initSseResponse = (res) => {
769
932
  Connection: "keep-alive",
770
933
  "X-Accel-Buffering": "no"
771
934
  });
772
- const connectedComment = getProjectConfig8().http.stream.connectedComment;
935
+ const connectedComment = getProjectConfig10().http.stream.connectedComment;
773
936
  if (connectedComment) {
774
937
  res.write(`${connectedComment}
775
938
 
@@ -792,12 +955,12 @@ var sendSseEvent = ({
792
955
  };
793
956
 
794
957
  // src/httpRoutes/resolveRequesterIp.ts
795
- import { getProjectConfig as getProjectConfig9, resolveClientIp as resolveClientIp2 } from "@luckystack/core";
958
+ import { getProjectConfig as getProjectConfig11, resolveClientIp as resolveClientIp3 } from "@luckystack/core";
796
959
  var resolveRequesterIp = (req) => {
797
- const trustProxy = getProjectConfig9().http.trustProxy;
798
- const trustedProxyHopCount = getProjectConfig9().http.trustedProxyHopCount;
960
+ const trustProxy = getProjectConfig11().http.trustProxy;
961
+ const trustedProxyHopCount = getProjectConfig11().http.trustedProxyHopCount;
799
962
  const rawRemoteAddress = req.socket.remoteAddress;
800
- return rawRemoteAddress || trustProxy && (req.headers["x-forwarded-for"] || req.headers["x-real-ip"]) ? resolveClientIp2({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount }) : void 0;
963
+ return rawRemoteAddress || trustProxy && (req.headers["x-forwarded-for"] || req.headers["x-real-ip"]) ? resolveClientIp3({ rawAddress: rawRemoteAddress, headers: req.headers, trustProxy, trustedProxyHopCount }) : void 0;
801
964
  };
802
965
 
803
966
  // src/httpRoutes/apiRoute.ts
@@ -1168,7 +1331,7 @@ var handleStaticAndSpaFallback = async ({
1168
1331
  // src/httpHandler.ts
1169
1332
  var buildSessionCookieOptions = (sessionExpiryDays, secure, http2) => `HttpOnly; SameSite=${http2.sessionCookieSameSite}; Path=${http2.sessionCookiePath}; Max-Age=${60 * 60 * 24 * sessionExpiryDays}; ${secure ? "Secure;" : ""}`;
1170
1333
  var setSecurityHeaders = (req, res, origin) => {
1171
- const { cors, securityHeaders } = getProjectConfig10().http;
1334
+ const { cors, securityHeaders } = getProjectConfig12().http;
1172
1335
  res.setHeader("Access-Control-Allow-Origin", origin);
1173
1336
  res.setHeader("Access-Control-Allow-Methods", cors.allowedMethods);
1174
1337
  res.setHeader("Access-Control-Allow-Headers", cors.allowedHeaders);
@@ -1208,6 +1371,8 @@ var PRE_PARAMS_ROUTES = [
1208
1371
  ];
1209
1372
  var POST_PARAMS_ROUTES = [
1210
1373
  handleUploadsRoute,
1374
+ handleAuthEmailCodeRoute,
1375
+ handleAuthTwoFactorRoute,
1211
1376
  handleAuthApiRoute,
1212
1377
  handleAuthCallbackRoute,
1213
1378
  handleApiRoute,
@@ -1286,7 +1451,7 @@ var parseRequestParams = async ({
1286
1451
  return params;
1287
1452
  };
1288
1453
  var handleHttpRequestInner = async (req, res, options) => {
1289
- const config = getProjectConfig10();
1454
+ const config = getProjectConfig12();
1290
1455
  const shouldLogDev = config.logging.devLogs;
1291
1456
  const sessionCookieName = config.http.sessionCookieName;
1292
1457
  const sessionCookieOptions = buildSessionCookieOptions(
@@ -1402,7 +1567,7 @@ import {
1402
1567
  extractLanguageFromHeader,
1403
1568
  extractTokenFromSocket,
1404
1569
  getLogger as getLogger8,
1405
- getProjectConfig as getProjectConfig11,
1570
+ getProjectConfig as getProjectConfig13,
1406
1571
  dispatchHook as dispatchHook6,
1407
1572
  normalizeErrorResponse,
1408
1573
  readSession as readSession4,
@@ -1544,7 +1709,7 @@ var registerRoomEvents = (ctx) => {
1544
1709
  blockedErrorCode: "room.joinBlocked",
1545
1710
  logVerb: "joined",
1546
1711
  mutate: async (sock, physicalRoom, rawGroup, existingCodes, userId) => {
1547
- const maxRooms = getProjectConfig11().socket.maxRoomsPerSession;
1712
+ const maxRooms = getProjectConfig13().socket.maxRoomsPerSession;
1548
1713
  let kept = existingCodes;
1549
1714
  if (maxRooms !== false && maxRooms > 0 && !existingCodes.includes(rawGroup)) {
1550
1715
  while (kept.length >= maxRooms) {
@@ -1716,7 +1881,7 @@ var rejoinPersistedRooms = (ctx) => {
1716
1881
  })();
1717
1882
  };
1718
1883
  var loadSocket = (httpServer, options = {}) => {
1719
- const config = getProjectConfig11();
1884
+ const config = getProjectConfig13();
1720
1885
  const shouldLogDev = config.logging.devLogs;
1721
1886
  const shouldLogSocketStartup = config.logging.socketStartup;
1722
1887
  const io = new SocketIOServer(httpServer, {
@@ -1787,7 +1952,7 @@ var loadSocket = (httpServer, options = {}) => {
1787
1952
  import {
1788
1953
  collectSynchronizedEnvKeys,
1789
1954
  getLogger as getLogger9,
1790
- getProjectConfig as getProjectConfig12,
1955
+ getProjectConfig as getProjectConfig14,
1791
1956
  isDeployConfigRegistered,
1792
1957
  isLocalizedNormalizerRegistered,
1793
1958
  isProjectConfigRegistered,
@@ -1854,7 +2019,7 @@ var verifyBootstrap = async (requirements = {}) => {
1854
2019
  }
1855
2020
  if (isProjectConfigRegistered()) {
1856
2021
  const synchronizedKeyCount = collectSynchronizedEnvKeys().length;
1857
- const healthHashMode = getProjectConfig12().http.healthHash.mode;
2022
+ const healthHashMode = getProjectConfig14().http.healthHash.mode;
1858
2023
  if (synchronizedKeyCount > 0 && healthHashMode === "plain") {
1859
2024
  getLogger9().warn(
1860
2025
  `[LuckyStack] SECURITY: /_health exposes UNSALTED sha256 fingerprints of ${String(synchronizedKeyCount)} synchronized env secret(s) by default. Set \`http.healthHash.mode\` to 'hmac' (or 'salted' with salt '@bootUuid') to stop publishing brute-forceable secret fingerprints to unauthenticated callers.`
@@ -1862,7 +2027,7 @@ var verifyBootstrap = async (requirements = {}) => {
1862
2027
  }
1863
2028
  }
1864
2029
  if (isProjectConfigRegistered()) {
1865
- const cfg = getProjectConfig12();
2030
+ const cfg = getProjectConfig14();
1866
2031
  if (!cfg.session.basedToken && cfg.http.sessionCookieSameSite !== "Strict") {
1867
2032
  getLogger9().warn(
1868
2033
  `[LuckyStack] SECURITY: http.sessionCookieSameSite is '${cfg.http.sessionCookieSameSite}', not 'Strict', but the CSRF-exempt auth-bootstrap endpoints (/auth/api/credentials, /auth/callback/*) rely on SameSite=Strict to block cross-site login-CSRF. Use 'Strict', or add your own CSRF/origin check on those routes.`
@@ -2166,7 +2331,7 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2166
2331
  writeDevServerInfo(ip, attemptPort);
2167
2332
  process.once("exit", clearDevServerInfo);
2168
2333
  }
2169
- const config = getProjectConfig13();
2334
+ const config = getProjectConfig15();
2170
2335
  if (config.logging.socketStartup || config.logging.devLogs) {
2171
2336
  getLogger13().info(`Server is running on http://${ip}:${String(attemptPort)}/`);
2172
2337
  }