@luckystack/server 0.2.7 → 0.3.1
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 +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +141 -70
- package/dist/index.js.map +1 -1
- package/docs/http-routes.md +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ Top-level `handleHttpRequest` + `dispatchRoutes(handlers, ctx)` are the only orc
|
|
|
153
153
|
| Export | Purpose |
|
|
154
154
|
| --- | --- |
|
|
155
155
|
| `bootstrapLuckyStack(options)` | High-level entry: verifies config, auto-imports overlay, then calls `createLuckyStackServer`. |
|
|
156
|
-
| `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen }
|
|
156
|
+
| `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }` (`stop`/`close` = graceful MIS-016 shutdown). |
|
|
157
157
|
| `verifyBootstrap(requirements?)` | Pre-flight check for project/deploy/services config and required env keys. |
|
|
158
158
|
| `registerCustomRoute(handler)` / `getCustomRoutes()` / `clearCustomRoutes()` | Global custom-route registry composed by `bootstrapLuckyStack`. |
|
|
159
159
|
| Hook payload types: `OnSocketConnectPayload`, `OnSocketDisconnectPayload`, `PreRoomJoinPayload`, `PostRoomJoinPayload`, `PreRoomLeavePayload`, `PostRoomLeavePayload`, `OnLocationUpdatePayload` | For socket-lifecycle hook handlers. |
|
package/dist/index.d.ts
CHANGED
|
@@ -82,10 +82,20 @@ interface CreateLuckyStackServerOptions {
|
|
|
82
82
|
* 1. This option (`options.port`)
|
|
83
83
|
* 2. Second positional argv (`npm run server -- <bundles> <port>`), parsed
|
|
84
84
|
* by `@luckystack/server/parseArgv`
|
|
85
|
-
* 3. `
|
|
86
|
-
*
|
|
85
|
+
* 3. `options.defaultPort` (the project's single-instance default, e.g. the
|
|
86
|
+
* `backend` port from `config.ports.ts`)
|
|
87
|
+
* 4. `process.env.SERVER_PORT` (back-compat for boots that skip `parseArgv`)
|
|
88
|
+
* 5. `80`
|
|
87
89
|
*/
|
|
88
90
|
port?: number | string;
|
|
91
|
+
/**
|
|
92
|
+
* Project's single-instance default backend port. Sits BELOW positional argv
|
|
93
|
+
* so a multi-instance boot (`npm run server -- <preset> <port>`) still wins,
|
|
94
|
+
* but ABOVE the legacy `SERVER_PORT` env. The scaffold passes the `backend`
|
|
95
|
+
* value from `config.ports.ts` here, so the listen port has ONE source of
|
|
96
|
+
* truth without re-introducing `SERVER_PORT` to `.env`.
|
|
97
|
+
*/
|
|
98
|
+
defaultPort?: number;
|
|
89
99
|
/** Bind address. Defaults to process.env.SERVER_IP or '127.0.0.1'. */
|
|
90
100
|
ip?: string;
|
|
91
101
|
/** Project-side static file handler (Vite output, etc.). Used as the catch-all. */
|
|
@@ -100,7 +110,7 @@ interface CreateLuckyStackServerOptions {
|
|
|
100
110
|
customRoutes?: CustomRouteHandler;
|
|
101
111
|
/**
|
|
102
112
|
* Enable dev-mode tooling (devkit hot reload, REPL, console init).
|
|
103
|
-
* Defaults to `
|
|
113
|
+
* Defaults to `resolveEnvKey() !== 'production'`.
|
|
104
114
|
*/
|
|
105
115
|
enableDevTools?: boolean;
|
|
106
116
|
/**
|
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 } from "@luckystack/core";
|
|
10
|
+
import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey4 } from "@luckystack/core";
|
|
11
11
|
|
|
12
12
|
// src/httpHandler.ts
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getParams,
|
|
20
20
|
getProjectConfig as getProjectConfig10,
|
|
21
21
|
hasCookie,
|
|
22
|
+
normalizeOrigin,
|
|
22
23
|
readSession as readSession3,
|
|
23
24
|
tryCatch as tryCatch7,
|
|
24
25
|
tryCatchSync
|
|
@@ -228,7 +229,11 @@ var handleCsrfRoute = async ({ res, routePath, token }) => {
|
|
|
228
229
|
if (!capabilities.login) {
|
|
229
230
|
const doubleSubmit = randomBytes(csrfConfig.tokenLength).toString("hex");
|
|
230
231
|
res.statusCode = 200;
|
|
231
|
-
|
|
232
|
+
const cookieOptions = {
|
|
233
|
+
...csrfConfig.cookieOptions,
|
|
234
|
+
secure: csrfConfig.cookieOptions.secure ?? process.env.SECURE === "true"
|
|
235
|
+
};
|
|
236
|
+
res.setHeader("Set-Cookie", serializeCsrfCookie(csrfConfig.cookieName, doubleSubmit, cookieOptions));
|
|
232
237
|
res.setHeader("Content-Type", "application/json");
|
|
233
238
|
res.end(JSON.stringify({ status: "success", csrfToken: doubleSubmit }));
|
|
234
239
|
return true;
|
|
@@ -351,6 +356,13 @@ var handleTestResetRoute = async ({ req, res, routePath }) => {
|
|
|
351
356
|
res.end(JSON.stringify({ status: "error", errorCode: "notFound" }));
|
|
352
357
|
return true;
|
|
353
358
|
}
|
|
359
|
+
if (req.method !== "POST") {
|
|
360
|
+
res.statusCode = 405;
|
|
361
|
+
res.setHeader("Allow", "POST");
|
|
362
|
+
res.setHeader("Content-Type", "application/json");
|
|
363
|
+
res.end(JSON.stringify({ status: "error", errorCode: "api.methodNotAllowed" }));
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
354
366
|
const requiredToken = process.env.TEST_RESET_TOKEN;
|
|
355
367
|
const providedToken = req.headers["x-test-reset-token"];
|
|
356
368
|
const tokenValue = Array.isArray(providedToken) ? providedToken[0] : providedToken;
|
|
@@ -425,6 +437,15 @@ var parseSessionBasedTokenHeader = (headerValue) => {
|
|
|
425
437
|
if (value === "0" || value === "false") return false;
|
|
426
438
|
return null;
|
|
427
439
|
};
|
|
440
|
+
var RESERVED_OAUTH_PARAMS = /* @__PURE__ */ new Set([
|
|
441
|
+
"client_id",
|
|
442
|
+
"redirect_uri",
|
|
443
|
+
"scope",
|
|
444
|
+
"response_type",
|
|
445
|
+
"state",
|
|
446
|
+
"code_challenge",
|
|
447
|
+
"code_challenge_method"
|
|
448
|
+
]);
|
|
428
449
|
var handleAuthApiRoute = async ({
|
|
429
450
|
req,
|
|
430
451
|
res,
|
|
@@ -459,19 +480,30 @@ var handleAuthApiRoute = async ({
|
|
|
459
480
|
res.end(JSON.stringify({ status: false, reason: "login.oauthStateInitFailed" }));
|
|
460
481
|
return true;
|
|
461
482
|
}
|
|
462
|
-
const clientId = encodeURIComponent(provider.clientID);
|
|
463
|
-
const callbackUrl = encodeURIComponent(provider.callbackURL);
|
|
464
|
-
const scope = encodeURIComponent(provider.scope.join(" "));
|
|
465
|
-
const state = encodeURIComponent(oauthState.state);
|
|
466
483
|
const stateTtl = config.auth.oauthStateTtlSeconds;
|
|
467
484
|
const secureFlag = resolveCookieSecure(config.http.sessionCookieSecure, process.env.SECURE) ? " Secure;" : "";
|
|
468
485
|
res.setHeader(
|
|
469
486
|
"Set-Cookie",
|
|
470
487
|
`${login.OAUTH_STATE_COOKIE_NAME}=${oauthState.stateCookie}; Path=/; HttpOnly;${secureFlag} SameSite=Lax; Max-Age=${stateTtl}`
|
|
471
488
|
);
|
|
472
|
-
const
|
|
489
|
+
const authParams = new URLSearchParams({
|
|
490
|
+
client_id: provider.clientID,
|
|
491
|
+
redirect_uri: provider.callbackURL,
|
|
492
|
+
scope: provider.scope.join(" "),
|
|
493
|
+
response_type: "code",
|
|
494
|
+
prompt: "select_account"
|
|
495
|
+
});
|
|
496
|
+
for (const [key, value] of Object.entries(provider.extraAuthorizationParams ?? {})) {
|
|
497
|
+
if (RESERVED_OAUTH_PARAMS.has(key)) continue;
|
|
498
|
+
authParams.set(key, value);
|
|
499
|
+
}
|
|
500
|
+
authParams.set("state", oauthState.state);
|
|
501
|
+
if (oauthState.codeChallenge) {
|
|
502
|
+
authParams.set("code_challenge", oauthState.codeChallenge);
|
|
503
|
+
authParams.set("code_challenge_method", "S256");
|
|
504
|
+
}
|
|
473
505
|
res.writeHead(302, {
|
|
474
|
-
Location: `${provider.authorizationURL}
|
|
506
|
+
Location: `${provider.authorizationURL}?${authParams.toString()}`
|
|
475
507
|
});
|
|
476
508
|
res.end();
|
|
477
509
|
return true;
|
|
@@ -528,6 +560,7 @@ var handleAuthApiRoute = async ({
|
|
|
528
560
|
res.setHeader("Set-Cookie", `${sessionCookieName}=${result.newToken}; ${sessionCookieOptions}`);
|
|
529
561
|
}
|
|
530
562
|
}
|
|
563
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
531
564
|
res.end(JSON.stringify({
|
|
532
565
|
status: result.status,
|
|
533
566
|
reason: result.reason,
|
|
@@ -613,6 +646,7 @@ var handleAuthCallbackRoute = async ({
|
|
|
613
646
|
supersedeToken: token ?? void 0
|
|
614
647
|
});
|
|
615
648
|
if (!callbackResult) {
|
|
649
|
+
res.setHeader("Set-Cookie", `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`);
|
|
616
650
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
617
651
|
res.end("Login failed");
|
|
618
652
|
return true;
|
|
@@ -620,12 +654,14 @@ var handleAuthCallbackRoute = async ({
|
|
|
620
654
|
if (token) await login.deleteSession(token, { skipSocketLogout: true });
|
|
621
655
|
if (shouldLogDev) getLogger3().debug("http: setting cookie or redirect with new token");
|
|
622
656
|
const { token: newToken, redirectUrl } = callbackResult;
|
|
657
|
+
const clearStateCookie = `${login.OAUTH_STATE_COOKIE_NAME}=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax`;
|
|
623
658
|
if (config.session.basedToken) {
|
|
624
659
|
const hashIndex = redirectUrl.indexOf("#");
|
|
625
660
|
const baseUrl = hashIndex === -1 ? redirectUrl : redirectUrl.slice(0, hashIndex);
|
|
661
|
+
res.setHeader("Set-Cookie", clearStateCookie);
|
|
626
662
|
res.writeHead(302, { Location: `${baseUrl}#token=${newToken}` });
|
|
627
663
|
} else {
|
|
628
|
-
res.setHeader("Set-Cookie", `${sessionCookieName}=${newToken}; ${sessionCookieOptions}`);
|
|
664
|
+
res.setHeader("Set-Cookie", [clearStateCookie, `${sessionCookieName}=${newToken}; ${sessionCookieOptions}`]);
|
|
629
665
|
res.writeHead(302, { Location: redirectUrl });
|
|
630
666
|
}
|
|
631
667
|
res.end();
|
|
@@ -711,16 +747,21 @@ var handleApiRoute = async ({
|
|
|
711
747
|
if (!routePath.startsWith("/api/")) return false;
|
|
712
748
|
const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });
|
|
713
749
|
let streamClosed = false;
|
|
714
|
-
|
|
750
|
+
let sseInitialized = false;
|
|
751
|
+
const ensureSseOpen = () => {
|
|
752
|
+
if (sseInitialized || res.writableEnded) return;
|
|
715
753
|
initSseResponse(res);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
754
|
+
sseInitialized = true;
|
|
755
|
+
};
|
|
756
|
+
const abortController = new AbortController();
|
|
757
|
+
const markClosed = () => {
|
|
758
|
+
streamClosed = true;
|
|
759
|
+
if (!abortController.signal.aborted) abortController.abort();
|
|
760
|
+
};
|
|
761
|
+
req.on("close", markClosed);
|
|
762
|
+
req.on("error", markClosed);
|
|
763
|
+
req.on("aborted", markClosed);
|
|
764
|
+
res.on("error", markClosed);
|
|
724
765
|
const [error, handled] = await tryCatch4(async () => {
|
|
725
766
|
const httpToken = extractTokenFromRequest(req);
|
|
726
767
|
const apiName = routePath.slice(5);
|
|
@@ -731,11 +772,6 @@ var handleApiRoute = async ({
|
|
|
731
772
|
message: "api.invalidName",
|
|
732
773
|
errorCode: "api.invalidName"
|
|
733
774
|
};
|
|
734
|
-
if (useHttpStream) {
|
|
735
|
-
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
736
|
-
res.end();
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
775
|
res.setHeader("Content-Type", "application/json");
|
|
740
776
|
res.writeHead(400);
|
|
741
777
|
res.end(JSON.stringify(response));
|
|
@@ -752,12 +788,15 @@ var handleApiRoute = async ({
|
|
|
752
788
|
xLanguageHeader: req.headers["x-language"],
|
|
753
789
|
acceptLanguageHeader: req.headers["accept-language"],
|
|
754
790
|
method,
|
|
791
|
+
abortSignal: abortController.signal,
|
|
755
792
|
stream: useHttpStream ? (payload) => {
|
|
756
793
|
if (streamClosed || res.writableEnded) return;
|
|
794
|
+
ensureSseOpen();
|
|
757
795
|
sendSseEvent({ res, event: "stream", data: payload });
|
|
758
796
|
} : void 0
|
|
759
797
|
});
|
|
760
|
-
if (useHttpStream) {
|
|
798
|
+
if (useHttpStream && (sseInitialized || result.status === "success")) {
|
|
799
|
+
ensureSseOpen();
|
|
761
800
|
if (!streamClosed) sendSseEvent({ res, event: "final", data: result });
|
|
762
801
|
res.end();
|
|
763
802
|
return true;
|
|
@@ -784,7 +823,7 @@ var handleApiRoute = async ({
|
|
|
784
823
|
message: "api.invalidRequestFormat",
|
|
785
824
|
errorCode: "api.invalidRequestFormat"
|
|
786
825
|
};
|
|
787
|
-
if (useHttpStream) {
|
|
826
|
+
if (useHttpStream && res.headersSent) {
|
|
788
827
|
if (!res.writableEnded) sendSseEvent({ res, event: "error", data: errResponse });
|
|
789
828
|
res.end();
|
|
790
829
|
return true;
|
|
@@ -832,16 +871,21 @@ var handleSyncRoute = async ({
|
|
|
832
871
|
}
|
|
833
872
|
const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });
|
|
834
873
|
let streamClosed = false;
|
|
835
|
-
|
|
874
|
+
let sseInitialized = false;
|
|
875
|
+
const ensureSseOpen = () => {
|
|
876
|
+
if (sseInitialized || res.writableEnded) return;
|
|
836
877
|
initSseResponse(res);
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
878
|
+
sseInitialized = true;
|
|
879
|
+
};
|
|
880
|
+
const abortController = new AbortController();
|
|
881
|
+
const markClosed = () => {
|
|
882
|
+
streamClosed = true;
|
|
883
|
+
if (!abortController.signal.aborted) abortController.abort();
|
|
884
|
+
};
|
|
885
|
+
req.on("close", markClosed);
|
|
886
|
+
req.on("error", markClosed);
|
|
887
|
+
req.on("aborted", markClosed);
|
|
888
|
+
res.on("error", markClosed);
|
|
845
889
|
const [error, handled] = await tryCatch5(async () => {
|
|
846
890
|
if (method !== "POST") {
|
|
847
891
|
const response = {
|
|
@@ -849,11 +893,6 @@ var handleSyncRoute = async ({
|
|
|
849
893
|
message: "sync.methodNotAllowed",
|
|
850
894
|
errorCode: "sync.methodNotAllowed"
|
|
851
895
|
};
|
|
852
|
-
if (useHttpStream) {
|
|
853
|
-
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
854
|
-
res.end();
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
896
|
res.setHeader("Content-Type", "application/json");
|
|
858
897
|
res.writeHead(405);
|
|
859
898
|
res.end(JSON.stringify(response));
|
|
@@ -867,11 +906,6 @@ var handleSyncRoute = async ({
|
|
|
867
906
|
message: "sync.invalidName",
|
|
868
907
|
errorCode: "sync.invalidName"
|
|
869
908
|
};
|
|
870
|
-
if (useHttpStream) {
|
|
871
|
-
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
872
|
-
res.end();
|
|
873
|
-
return true;
|
|
874
|
-
}
|
|
875
909
|
res.setHeader("Content-Type", "application/json");
|
|
876
910
|
res.writeHead(400);
|
|
877
911
|
res.end(JSON.stringify(response));
|
|
@@ -889,12 +923,15 @@ var handleSyncRoute = async ({
|
|
|
889
923
|
requesterIp,
|
|
890
924
|
xLanguageHeader: req.headers["x-language"],
|
|
891
925
|
acceptLanguageHeader: req.headers["accept-language"],
|
|
926
|
+
abortSignal: abortController.signal,
|
|
892
927
|
stream: useHttpStream ? (payload) => {
|
|
893
928
|
if (streamClosed || res.writableEnded) return;
|
|
929
|
+
ensureSseOpen();
|
|
894
930
|
sendSseEvent({ res, event: "stream", data: payload });
|
|
895
931
|
} : void 0
|
|
896
932
|
});
|
|
897
|
-
if (useHttpStream) {
|
|
933
|
+
if (useHttpStream && (sseInitialized || result.status === "success")) {
|
|
934
|
+
ensureSseOpen();
|
|
898
935
|
if (!streamClosed) sendSseEvent({ res, event: "final", data: result });
|
|
899
936
|
res.end();
|
|
900
937
|
return true;
|
|
@@ -920,7 +957,7 @@ var handleSyncRoute = async ({
|
|
|
920
957
|
message: "sync.invalidRequestFormat",
|
|
921
958
|
errorCode: "sync.invalidRequestFormat"
|
|
922
959
|
};
|
|
923
|
-
if (useHttpStream) {
|
|
960
|
+
if (useHttpStream && res.headersSent) {
|
|
924
961
|
if (!res.writableEnded) sendSseEvent({ res, event: "error", data: errResponse });
|
|
925
962
|
res.end();
|
|
926
963
|
return true;
|
|
@@ -1026,6 +1063,11 @@ var handleStaticAndSpaFallback = async ({
|
|
|
1026
1063
|
return true;
|
|
1027
1064
|
}
|
|
1028
1065
|
if (routePath.startsWith("/assets/")) {
|
|
1066
|
+
if (routePath.includes("..")) {
|
|
1067
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1068
|
+
res.end("Not Found");
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1029
1071
|
if (!options.serveFile) {
|
|
1030
1072
|
res.writeHead(404);
|
|
1031
1073
|
res.end("Not Found");
|
|
@@ -1115,7 +1157,10 @@ var dispatchRoutes = async (handlers2, ctx) => {
|
|
|
1115
1157
|
return false;
|
|
1116
1158
|
};
|
|
1117
1159
|
var enforceOriginPolicy = (req, res, routePath) => {
|
|
1118
|
-
const origin =
|
|
1160
|
+
const origin = normalizeOrigin({
|
|
1161
|
+
value: req.headers.origin ?? req.headers.referer ?? "",
|
|
1162
|
+
secure: process.env.SECURE === "true"
|
|
1163
|
+
});
|
|
1119
1164
|
const isStateChangingMethod = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
|
|
1120
1165
|
if (isOriginExemptPath(routePath)) {
|
|
1121
1166
|
return { origin, rejected: false };
|
|
@@ -1206,7 +1251,7 @@ var handleHttpRequestInner = async (req, res, options) => {
|
|
|
1206
1251
|
res.setHeader("X-Request-Id", requestId);
|
|
1207
1252
|
const safeHeaders = {};
|
|
1208
1253
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
1209
|
-
if (k === "authorization" || k === "cookie" || k === "set-cookie" || k === "x-csrf-token") continue;
|
|
1254
|
+
if (k === "authorization" || k === "cookie" || k === "set-cookie" || k === "x-csrf-token" || k === "x-test-reset-token" || k === "x-session-based-token") continue;
|
|
1210
1255
|
safeHeaders[k] = Array.isArray(v) ? v.join(", ") : v ?? "";
|
|
1211
1256
|
}
|
|
1212
1257
|
const preHttpResult = await dispatchHook5("preHttpRequest", {
|
|
@@ -1376,7 +1421,7 @@ var executeRoomMutation = async (opts) => {
|
|
|
1376
1421
|
const existingRoomCodes = getSessionRoomCodes(session);
|
|
1377
1422
|
const roomPurpose = preHook === "preRoomJoin" ? "join" : "leave";
|
|
1378
1423
|
const physicalRoom = formatRoomName(group, { purpose: roomPurpose, userId: session.id });
|
|
1379
|
-
const nextRoomCodes = await mutate(socket, physicalRoom, group, existingRoomCodes);
|
|
1424
|
+
const nextRoomCodes = await mutate(socket, physicalRoom, group, existingRoomCodes, session.id);
|
|
1380
1425
|
const sanitizedSession = sanitizeSessionRoomKeys(session);
|
|
1381
1426
|
await writeSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });
|
|
1382
1427
|
const visibleRooms = getVisibleSocketRooms(socket, token);
|
|
@@ -1433,10 +1478,19 @@ var registerRoomEvents = (ctx) => {
|
|
|
1433
1478
|
postHook: "postRoomJoin",
|
|
1434
1479
|
blockedErrorCode: "room.joinBlocked",
|
|
1435
1480
|
logVerb: "joined",
|
|
1436
|
-
mutate: async (sock, physicalRoom, rawGroup, existingCodes) => {
|
|
1437
|
-
const
|
|
1481
|
+
mutate: async (sock, physicalRoom, rawGroup, existingCodes, userId) => {
|
|
1482
|
+
const maxRooms = getProjectConfig11().socket.maxRoomsPerSession;
|
|
1483
|
+
let kept = existingCodes;
|
|
1484
|
+
if (maxRooms !== false && maxRooms > 0 && !existingCodes.includes(rawGroup)) {
|
|
1485
|
+
while (kept.length >= maxRooms) {
|
|
1486
|
+
const oldest = kept[0];
|
|
1487
|
+
if (oldest === void 0) break;
|
|
1488
|
+
kept = kept.slice(1);
|
|
1489
|
+
await sock.leave(formatRoomName(oldest, { purpose: "join", userId }));
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1438
1492
|
await sock.join(physicalRoom);
|
|
1439
|
-
return
|
|
1493
|
+
return [.../* @__PURE__ */ new Set([...kept, rawGroup])];
|
|
1440
1494
|
}
|
|
1441
1495
|
});
|
|
1442
1496
|
});
|
|
@@ -1459,7 +1513,7 @@ var registerRoomEvents = (ctx) => {
|
|
|
1459
1513
|
postHook: "postRoomLeave",
|
|
1460
1514
|
blockedErrorCode: "room.leaveBlocked",
|
|
1461
1515
|
logVerb: "left",
|
|
1462
|
-
mutate: async (sock, physicalRoom, rawGroup, existingCodes) => {
|
|
1516
|
+
mutate: async (sock, physicalRoom, rawGroup, existingCodes, _userId) => {
|
|
1463
1517
|
const nextCodes = existingCodes.filter((c) => c !== rawGroup);
|
|
1464
1518
|
await sock.leave(physicalRoom);
|
|
1465
1519
|
return nextCodes;
|
|
@@ -1492,7 +1546,7 @@ var registerDisconnectEvent = (ctx) => {
|
|
|
1492
1546
|
void dispatchHook6("onSocketDisconnect", { socketId: socket.id, token, reason });
|
|
1493
1547
|
if (activityBroadcasterEnabled) {
|
|
1494
1548
|
void getPresence().then((presence) => {
|
|
1495
|
-
presence?.clearActivity(socket.id);
|
|
1549
|
+
presence?.clearActivity(socket.id, token ?? void 0);
|
|
1496
1550
|
});
|
|
1497
1551
|
}
|
|
1498
1552
|
if (activityBroadcasterEnabled && token) {
|
|
@@ -1672,7 +1726,8 @@ import {
|
|
|
1672
1726
|
isDeployConfigRegistered,
|
|
1673
1727
|
isLocalizedNormalizerRegistered,
|
|
1674
1728
|
isProjectConfigRegistered,
|
|
1675
|
-
isRuntimeMapsProviderRegistered
|
|
1729
|
+
isRuntimeMapsProviderRegistered,
|
|
1730
|
+
resolveEnvKey as resolveEnvKey2
|
|
1676
1731
|
} from "@luckystack/core";
|
|
1677
1732
|
var verifyBootstrap = async (requirements = {}) => {
|
|
1678
1733
|
const missing = [];
|
|
@@ -1696,18 +1751,22 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1696
1751
|
}
|
|
1697
1752
|
if (requirements.requireOAuthProviders) {
|
|
1698
1753
|
const login = await getLogin();
|
|
1699
|
-
if (
|
|
1754
|
+
if (login) {
|
|
1755
|
+
const providers = login.getOAuthProviders();
|
|
1756
|
+
const onlyDefaultCredentials = providers.length === 0 || providers.length === 1 && providers[0]?.name === "credentials";
|
|
1757
|
+
if (onlyDefaultCredentials) {
|
|
1758
|
+
missing.push(
|
|
1759
|
+
"OAuth providers \u2014 call `registerOAuthProviders([...])` from `luckystack/login/oauthProviders.ts` (or skip this check if your app uses credentials only)."
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
} else {
|
|
1700
1763
|
missing.push(
|
|
1701
1764
|
"OAuth providers required (`requireOAuthProviders`) but `@luckystack/login` is not installed. Install it, or drop the requirement if this app has no auth."
|
|
1702
1765
|
);
|
|
1703
|
-
} else if (login.getOAuthProviders().length <= 1) {
|
|
1704
|
-
missing.push(
|
|
1705
|
-
"OAuth providers \u2014 call `registerOAuthProviders([...])` from `luckystack/login/oauthProviders.ts` (or skip this check if your app uses credentials only)."
|
|
1706
|
-
);
|
|
1707
1766
|
}
|
|
1708
1767
|
}
|
|
1709
1768
|
if (!isRuntimeMapsProviderRegistered()) {
|
|
1710
|
-
if (
|
|
1769
|
+
if (resolveEnvKey2() === "production") {
|
|
1711
1770
|
missing.push(
|
|
1712
1771
|
"RuntimeMapsProvider \u2014 call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound."
|
|
1713
1772
|
);
|
|
@@ -1718,7 +1777,7 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1718
1777
|
}
|
|
1719
1778
|
}
|
|
1720
1779
|
if (!isLocalizedNormalizerRegistered()) {
|
|
1721
|
-
if (
|
|
1780
|
+
if (resolveEnvKey2() === "production") {
|
|
1722
1781
|
missing.push(
|
|
1723
1782
|
"LocalizedNormalizer \u2014 call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n)."
|
|
1724
1783
|
);
|
|
@@ -1752,7 +1811,8 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1752
1811
|
// src/runtimeMapsLoader.ts
|
|
1753
1812
|
import {
|
|
1754
1813
|
getLogger as getLogger10,
|
|
1755
|
-
registerRuntimeMapsProvider
|
|
1814
|
+
registerRuntimeMapsProvider,
|
|
1815
|
+
resolveEnvKey as resolveEnvKey3
|
|
1756
1816
|
} from "@luckystack/core";
|
|
1757
1817
|
var emptyRuntimeMaps = {
|
|
1758
1818
|
apisObject: {},
|
|
@@ -1802,7 +1862,7 @@ var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
|
|
|
1802
1862
|
target[key] = source[key];
|
|
1803
1863
|
}
|
|
1804
1864
|
};
|
|
1805
|
-
var isProduction = () =>
|
|
1865
|
+
var isProduction = () => resolveEnvKey3() === "production";
|
|
1806
1866
|
var createProdRuntimeMapsProvider = (options) => {
|
|
1807
1867
|
let prodMapsPromise = null;
|
|
1808
1868
|
let devkitModulePromise = null;
|
|
@@ -1970,12 +2030,23 @@ var clearDevServerInfo = () => {
|
|
|
1970
2030
|
var initDevTools = async () => {
|
|
1971
2031
|
const { initConsolelog } = await import("@luckystack/core");
|
|
1972
2032
|
initConsolelog();
|
|
1973
|
-
const devkitModuleId = "@luckystack/devkit";
|
|
1974
|
-
const devkit = await import(devkitModuleId);
|
|
1975
|
-
await devkit.initializeAll();
|
|
1976
|
-
devkit.setupWatchers();
|
|
1977
2033
|
process.once("SIGINT", () => process.exit(0));
|
|
1978
2034
|
process.once("SIGTERM", () => process.exit(0));
|
|
2035
|
+
const devkitModuleId = "@luckystack/devkit";
|
|
2036
|
+
if (!canResolve(devkitModuleId)) {
|
|
2037
|
+
getLogger13().warn(
|
|
2038
|
+
"dev tooling unavailable \u2014 @luckystack/devkit is not installed, so hot reload + type-map generation are off. Install it as a devDependency for dev, or set NODE_ENV=production to run in production mode."
|
|
2039
|
+
);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const [devkitError] = await tryCatch10(async () => {
|
|
2043
|
+
const devkit = await import(devkitModuleId);
|
|
2044
|
+
await devkit.initializeAll();
|
|
2045
|
+
devkit.setupWatchers();
|
|
2046
|
+
});
|
|
2047
|
+
if (devkitError) {
|
|
2048
|
+
getLogger13().warn("dev tooling failed to initialize \u2014 continuing without hot reload.", { error: devkitError.message });
|
|
2049
|
+
}
|
|
1979
2050
|
};
|
|
1980
2051
|
var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((resolve, reject) => {
|
|
1981
2052
|
const startPort = typeof port === "string" ? Number.parseInt(port, 10) : port;
|
|
@@ -2031,9 +2102,9 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2031
2102
|
requireServicesConfig: options.requireServicesConfig,
|
|
2032
2103
|
requireOAuthProviders: options.requireOAuthProviders
|
|
2033
2104
|
});
|
|
2034
|
-
const port = options.port ?? getParsedPort() ?? process.env.SERVER_PORT ?? 80;
|
|
2105
|
+
const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;
|
|
2035
2106
|
const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
|
|
2036
|
-
const enableDevTools = options.enableDevTools ??
|
|
2107
|
+
const enableDevTools = options.enableDevTools ?? resolveEnvKey4() !== "production";
|
|
2037
2108
|
registerBindAddress({
|
|
2038
2109
|
ip,
|
|
2039
2110
|
port: typeof port === "string" ? Number.parseInt(port, 10) : port
|