@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 +75 -56
- package/dist/index.js +189 -24
- package/dist/index.js.map +1 -1
- package/docs/http-routes.md +14 -7
- package/package.json +10 -10
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
|
-
## [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- `/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 } =
|
|
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 =
|
|
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
|
|
958
|
+
import { getProjectConfig as getProjectConfig11, resolveClientIp as resolveClientIp3 } from "@luckystack/core";
|
|
796
959
|
var resolveRequesterIp = (req) => {
|
|
797
|
-
const trustProxy =
|
|
798
|
-
const 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"]) ?
|
|
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 } =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|