@luckystack/server 0.2.4 → 0.2.6
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/CLAUDE.md +1 -1
- package/dist/index.js +90 -26
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/CLAUDE.md
CHANGED
|
@@ -61,7 +61,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
|
|
|
61
61
|
|
|
62
62
|
## Config keys (env vars + registerProjectConfig slots)
|
|
63
63
|
|
|
64
|
-
- `SERVER_PORT` (env, optional) — fallback when neither `options.port` nor positional argv supplies one. Written back by `applyServerArgv()` when argv carries a port.
|
|
64
|
+
- `SERVER_PORT` (env, optional) — fallback when neither `options.port` nor positional argv supplies one. Written back by `applyServerArgv()` when argv carries a port. In dev, the ACTUALLY-bound port (after any `SERVER_PORT_AUTO_INCREMENT` hop off a busy port) is advertised to `node_modules/.luckystack/dev-server.json` so the template Vite proxy follows the real port instead of the stale `.env` one (written by `devServerInfo.ts`; skipped in prod + tests, removed on exit).
|
|
65
65
|
- `SERVER_IP` (env, optional, default `127.0.0.1`) — bind address fallback when `options.ip` is omitted.
|
|
66
66
|
- `NODE_ENV` (env, required for security-sensitive branches) — `development` / `test` toggle devkit hot reload + REPL and gate `/_test/reset`.
|
|
67
67
|
- `TEST_RESET_TOKEN` (env, required for `/_test/reset` to be reachable at all) — must match the `x-test-reset-token` request header. No fallback "no auth" mode.
|
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
|
|
10
|
+
import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2 } from "@luckystack/core";
|
|
11
11
|
|
|
12
12
|
// src/httpHandler.ts
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getProjectConfig as getProjectConfig10,
|
|
21
21
|
hasCookie,
|
|
22
22
|
readSession as readSession3,
|
|
23
|
+
tryCatch as tryCatch7,
|
|
23
24
|
tryCatchSync
|
|
24
25
|
} from "@luckystack/core";
|
|
25
26
|
|
|
@@ -538,6 +539,7 @@ var handleAuthApiRoute = async ({
|
|
|
538
539
|
|
|
539
540
|
// src/httpRoutes/authLogoutRoute.ts
|
|
540
541
|
import { getLogger as getLogger2, getProjectConfig as getProjectConfig6, tryCatch as tryCatch3 } from "@luckystack/core";
|
|
542
|
+
var warnedNonStrictSameSite = false;
|
|
541
543
|
var handleAuthLogoutRoute = async ({ res, routePath, method, token }) => {
|
|
542
544
|
if (routePath !== "/auth/logout") return false;
|
|
543
545
|
if (method !== "POST") {
|
|
@@ -557,7 +559,13 @@ var handleAuthLogoutRoute = async ({ res, routePath, method, token }) => {
|
|
|
557
559
|
}
|
|
558
560
|
}
|
|
559
561
|
const http2 = getProjectConfig6().http;
|
|
560
|
-
|
|
562
|
+
if (http2.sessionCookieSameSite !== "Strict" && !warnedNonStrictSameSite) {
|
|
563
|
+
warnedNonStrictSameSite = true;
|
|
564
|
+
getLogger2().warn(
|
|
565
|
+
`/auth/logout is exempt from CSRF middleware and relies on SameSite=Strict. Current sessionCookieSameSite="${http2.sessionCookieSameSite}" weakens this protection.`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const secure = resolveCookieSecure(http2.sessionCookieSecure, process.env.SECURE);
|
|
561
569
|
res.setHeader(
|
|
562
570
|
"Set-Cookie",
|
|
563
571
|
`${http2.sessionCookieName}=; HttpOnly; SameSite=${http2.sessionCookieSameSite}; Path=${http2.sessionCookiePath}; Max-Age=0; ${secure ? "Secure;" : ""}`
|
|
@@ -1166,7 +1174,7 @@ var parseRequestParams = async ({
|
|
|
1166
1174
|
}
|
|
1167
1175
|
return params;
|
|
1168
1176
|
};
|
|
1169
|
-
var
|
|
1177
|
+
var handleHttpRequestInner = async (req, res, options) => {
|
|
1170
1178
|
const config = getProjectConfig10();
|
|
1171
1179
|
const shouldLogDev = config.logging.devLogs;
|
|
1172
1180
|
const sessionCookieName = config.http.sessionCookieName;
|
|
@@ -1180,13 +1188,21 @@ var handleHttpRequest = async (req, res, options) => {
|
|
|
1180
1188
|
);
|
|
1181
1189
|
const url = req.url ?? "/";
|
|
1182
1190
|
const [routePathRaw, queryStringRaw] = url.split("?");
|
|
1183
|
-
const
|
|
1191
|
+
const [decodeError, decodedPath] = tryCatchSync(() => decodeURIComponent(routePathRaw ?? "/"));
|
|
1192
|
+
if (decodeError || decodedPath === null) {
|
|
1193
|
+
res.statusCode = 400;
|
|
1194
|
+
res.setHeader("Content-Type", "text/plain");
|
|
1195
|
+
res.end("Bad Request");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const routePath = decodedPath;
|
|
1184
1199
|
const queryString = queryStringRaw ?? "";
|
|
1185
1200
|
const { origin, rejected } = enforceOriginPolicy(req, res, routePath);
|
|
1186
1201
|
if (rejected) return;
|
|
1187
1202
|
setSecurityHeaders(req, res, origin);
|
|
1188
1203
|
const incomingRequestId = req.headers["x-request-id"];
|
|
1189
|
-
const
|
|
1204
|
+
const rawRequestId = Array.isArray(incomingRequestId) ? incomingRequestId[0] : incomingRequestId;
|
|
1205
|
+
const requestId = rawRequestId && /^[a-zA-Z0-9-]{1,128}$/.test(rawRequestId) ? rawRequestId : randomUUID();
|
|
1190
1206
|
res.setHeader("X-Request-Id", requestId);
|
|
1191
1207
|
const safeHeaders = {};
|
|
1192
1208
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
@@ -1246,6 +1262,15 @@ var handleHttpRequest = async (req, res, options) => {
|
|
|
1246
1262
|
if (params === null) return;
|
|
1247
1263
|
await dispatchRoutes(POST_PARAMS_ROUTES, { ...baseCtx, params });
|
|
1248
1264
|
};
|
|
1265
|
+
var handleHttpRequest = async (req, res, options) => {
|
|
1266
|
+
const [error] = await tryCatch7(() => handleHttpRequestInner(req, res, options));
|
|
1267
|
+
if (error && !res.writableEnded) {
|
|
1268
|
+
getLogger7().error("handleHttpRequest: unhandled error", { err: error });
|
|
1269
|
+
res.statusCode = 500;
|
|
1270
|
+
res.setHeader("Content-Type", "application/json");
|
|
1271
|
+
res.end(JSON.stringify({ status: "error", errorCode: "server.internalError" }));
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1249
1274
|
|
|
1250
1275
|
// src/loadSocket.ts
|
|
1251
1276
|
import { Server as SocketIOServer } from "socket.io";
|
|
@@ -1271,7 +1296,7 @@ import {
|
|
|
1271
1296
|
normalizeErrorResponse,
|
|
1272
1297
|
readSession as readSession4,
|
|
1273
1298
|
writeSession,
|
|
1274
|
-
tryCatch as
|
|
1299
|
+
tryCatch as tryCatch8
|
|
1275
1300
|
} from "@luckystack/core";
|
|
1276
1301
|
import { handleApiRequest } from "@luckystack/api";
|
|
1277
1302
|
var sessionLocks = /* @__PURE__ */ new Map();
|
|
@@ -1279,7 +1304,7 @@ var withSessionLock = async (token, fn) => {
|
|
|
1279
1304
|
const prev = sessionLocks.get(token) ?? Promise.resolve();
|
|
1280
1305
|
const next = prev.then(fn, fn);
|
|
1281
1306
|
sessionLocks.set(token, next);
|
|
1282
|
-
await
|
|
1307
|
+
await tryCatch8(() => next);
|
|
1283
1308
|
if (sessionLocks.get(token) === next) sessionLocks.delete(token);
|
|
1284
1309
|
};
|
|
1285
1310
|
var getVisibleSocketRooms = (socket, token) => {
|
|
@@ -1304,7 +1329,7 @@ var validateRoomRequest = (socket, responseIndex, token, group, preferredLocale,
|
|
|
1304
1329
|
}));
|
|
1305
1330
|
return false;
|
|
1306
1331
|
}
|
|
1307
|
-
if (!group) {
|
|
1332
|
+
if (!group || group.length > 256) {
|
|
1308
1333
|
socket.emit(buildResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1309
1334
|
response: { status: "error", errorCode: "room.invalid" },
|
|
1310
1335
|
preferredLocale
|
|
@@ -1490,6 +1515,15 @@ var registerUpdateLocationEvent = (ctx) => {
|
|
|
1490
1515
|
(newLocation) => {
|
|
1491
1516
|
if (!token) return;
|
|
1492
1517
|
if (!locationProviderEnabled) return;
|
|
1518
|
+
if (typeof newLocation.pathName !== "string" || !newLocation.pathName.startsWith("/") || newLocation.pathName.length > 2048 || newLocation.pathName.includes("\0")) return;
|
|
1519
|
+
if (newLocation.searchParams !== void 0) {
|
|
1520
|
+
if (typeof newLocation.searchParams !== "object" || Array.isArray(newLocation.searchParams)) return;
|
|
1521
|
+
const entries = Object.entries(newLocation.searchParams);
|
|
1522
|
+
if (entries.length > 50) return;
|
|
1523
|
+
for (const [k, v] of entries) {
|
|
1524
|
+
if (typeof k !== "string" || k.length > 256 || typeof v !== "string" || v.length > 1024) return;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1493
1527
|
if (shouldLogDev) {
|
|
1494
1528
|
getLogger8().debug("updating location", { pathName: newLocation.pathName });
|
|
1495
1529
|
}
|
|
@@ -1543,7 +1577,7 @@ var rejoinPersistedRooms = (ctx) => {
|
|
|
1543
1577
|
const { socket, token, shouldLogDev } = ctx;
|
|
1544
1578
|
if (!token) return;
|
|
1545
1579
|
void (async () => {
|
|
1546
|
-
const [rejoinError, codes] = await
|
|
1580
|
+
const [rejoinError, codes] = await tryCatch8(async () => {
|
|
1547
1581
|
await socket.join(token);
|
|
1548
1582
|
const session = await readSession4(token);
|
|
1549
1583
|
const roomCodes = session ? getSessionRoomCodes(session) : [];
|
|
@@ -1853,7 +1887,7 @@ import {
|
|
|
1853
1887
|
dispatchHook as dispatchHook7,
|
|
1854
1888
|
flushErrorTrackers,
|
|
1855
1889
|
getLogger as getLogger11,
|
|
1856
|
-
tryCatch as
|
|
1890
|
+
tryCatch as tryCatch9
|
|
1857
1891
|
} from "@luckystack/core";
|
|
1858
1892
|
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
1859
1893
|
var withTimeout = async (label, timeoutMs, fn) => {
|
|
@@ -1866,7 +1900,7 @@ var withTimeout = async (label, timeoutMs, fn) => {
|
|
|
1866
1900
|
timer.unref();
|
|
1867
1901
|
});
|
|
1868
1902
|
const run = (async () => {
|
|
1869
|
-
const [error] = await
|
|
1903
|
+
const [error] = await tryCatch9(fn);
|
|
1870
1904
|
if (error) getLogger11().warn(`[shutdown] step "${label}" failed: ${error.message}`);
|
|
1871
1905
|
return true;
|
|
1872
1906
|
})();
|
|
@@ -1906,6 +1940,32 @@ var runGracefulShutdown = async (deps, options = {}) => {
|
|
|
1906
1940
|
getLogger11().info("[shutdown] graceful shutdown complete");
|
|
1907
1941
|
};
|
|
1908
1942
|
|
|
1943
|
+
// src/devServerInfo.ts
|
|
1944
|
+
import fs from "fs";
|
|
1945
|
+
import path2 from "path";
|
|
1946
|
+
import { getLogger as getLogger12 } from "@luckystack/core";
|
|
1947
|
+
var DEV_SERVER_FILE = ["node_modules", ".luckystack", "dev-server.json"];
|
|
1948
|
+
var devServerInfoPath = () => path2.join(process.cwd(), ...DEV_SERVER_FILE);
|
|
1949
|
+
var writeDevServerInfo = (ip, port) => {
|
|
1950
|
+
try {
|
|
1951
|
+
const file = devServerInfoPath();
|
|
1952
|
+
fs.mkdirSync(path2.dirname(file), { recursive: true });
|
|
1953
|
+
const info = { ip, port, pid: process.pid };
|
|
1954
|
+
fs.writeFileSync(file, `${JSON.stringify(info, null, 2)}
|
|
1955
|
+
`);
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
getLogger12().debug(
|
|
1958
|
+
`[dev-server-info] could not write port file (proxy will fall back to SERVER_PORT): ${error instanceof Error ? error.message : String(error)}`
|
|
1959
|
+
);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
var clearDevServerInfo = () => {
|
|
1963
|
+
try {
|
|
1964
|
+
fs.rmSync(devServerInfoPath(), { force: true });
|
|
1965
|
+
} catch {
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
|
|
1909
1969
|
// src/createServer.ts
|
|
1910
1970
|
var initDevTools = async () => {
|
|
1911
1971
|
const { initConsolelog } = await import("@luckystack/core");
|
|
@@ -1931,13 +1991,13 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
|
|
|
1931
1991
|
return;
|
|
1932
1992
|
}
|
|
1933
1993
|
if (autoIncrement) {
|
|
1934
|
-
|
|
1994
|
+
getLogger13().warn(
|
|
1935
1995
|
`Port ${String(attemptPort)} is in use \u2014 trying ${String(attemptPort + 1)} (auto-increment; set SERVER_PORT_AUTO_INCREMENT=0 to disable)`
|
|
1936
1996
|
);
|
|
1937
1997
|
tryListen(attemptPort + 1);
|
|
1938
1998
|
return;
|
|
1939
1999
|
}
|
|
1940
|
-
|
|
2000
|
+
getLogger13().error(
|
|
1941
2001
|
`Port ${String(attemptPort)} is already in use \u2014 the server did NOT start. Another \`npm run server\` is probably still running (stop it), or set SERVER_PORT to a free port, or set SERVER_PORT_AUTO_INCREMENT=1 to auto-pick the next free port.`
|
|
1942
2002
|
);
|
|
1943
2003
|
reject(err);
|
|
@@ -1945,9 +2005,13 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
|
|
|
1945
2005
|
httpServer.once("error", onError);
|
|
1946
2006
|
httpServer.listen(attemptPort, ip, () => {
|
|
1947
2007
|
httpServer.off("error", onError);
|
|
2008
|
+
if (!isProduction2 && process.env.NODE_ENV !== "test") {
|
|
2009
|
+
writeDevServerInfo(ip, attemptPort);
|
|
2010
|
+
process.once("exit", clearDevServerInfo);
|
|
2011
|
+
}
|
|
1948
2012
|
const config = getProjectConfig13();
|
|
1949
2013
|
if (config.logging.socketStartup || config.logging.devLogs) {
|
|
1950
|
-
|
|
2014
|
+
getLogger13().info(`Server is running on http://${ip}:${String(attemptPort)}/`);
|
|
1951
2015
|
}
|
|
1952
2016
|
callback?.();
|
|
1953
2017
|
resolve(httpServer);
|
|
@@ -1977,7 +2041,7 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
1977
2041
|
if (enableDevTools) {
|
|
1978
2042
|
await initDevTools();
|
|
1979
2043
|
}
|
|
1980
|
-
const [bootUuidError] = await
|
|
2044
|
+
const [bootUuidError] = await tryCatch10(() => writeBootUuid());
|
|
1981
2045
|
if (bootUuidError) {
|
|
1982
2046
|
throw new Error(
|
|
1983
2047
|
"Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USER / REDIS_PASSWORD and that Redis is reachable.",
|
|
@@ -1989,7 +2053,7 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
1989
2053
|
});
|
|
1990
2054
|
httpServer.on("error", (err) => {
|
|
1991
2055
|
if (err.code === "EADDRINUSE") return;
|
|
1992
|
-
|
|
2056
|
+
getLogger13().error("[http-server] runtime error", err);
|
|
1993
2057
|
});
|
|
1994
2058
|
const { io: ioServer, adapterClients } = loadSocket(httpServer, {
|
|
1995
2059
|
maxHttpBufferSize: options.maxHttpBufferSize
|
|
@@ -2018,8 +2082,8 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2018
2082
|
};
|
|
2019
2083
|
|
|
2020
2084
|
// src/bootstrap.ts
|
|
2021
|
-
import
|
|
2022
|
-
import
|
|
2085
|
+
import path3 from "path";
|
|
2086
|
+
import fs2 from "fs";
|
|
2023
2087
|
import { pathToFileURL } from "url";
|
|
2024
2088
|
import { ROOT_DIR } from "@luckystack/core";
|
|
2025
2089
|
var OVERLAY_ORDER = [
|
|
@@ -2042,26 +2106,26 @@ var OVERLAY_ORDER = [
|
|
|
2042
2106
|
"server"
|
|
2043
2107
|
];
|
|
2044
2108
|
var importIfExists = async (filePath) => {
|
|
2045
|
-
if (!
|
|
2109
|
+
if (!fs2.existsSync(filePath)) return;
|
|
2046
2110
|
await import(pathToFileURL(filePath).href);
|
|
2047
2111
|
};
|
|
2048
2112
|
var loadOverlayFolder = async (overlayRoot) => {
|
|
2049
|
-
const overlayAbs =
|
|
2050
|
-
if (!
|
|
2113
|
+
const overlayAbs = path3.isAbsolute(overlayRoot) ? overlayRoot : path3.join(ROOT_DIR, overlayRoot);
|
|
2114
|
+
if (!fs2.existsSync(overlayAbs)) {
|
|
2051
2115
|
return;
|
|
2052
2116
|
}
|
|
2053
2117
|
for (const packageName of OVERLAY_ORDER) {
|
|
2054
|
-
const packageDir =
|
|
2055
|
-
if (!
|
|
2118
|
+
const packageDir = path3.join(overlayAbs, packageName);
|
|
2119
|
+
if (!fs2.existsSync(packageDir)) continue;
|
|
2056
2120
|
const indexCandidates = ["index.ts", "index.js"];
|
|
2057
2121
|
for (const candidate of indexCandidates) {
|
|
2058
|
-
await importIfExists(
|
|
2122
|
+
await importIfExists(path3.join(packageDir, candidate));
|
|
2059
2123
|
}
|
|
2060
|
-
const entries =
|
|
2124
|
+
const entries = fs2.readdirSync(packageDir).toSorted();
|
|
2061
2125
|
for (const entry of entries) {
|
|
2062
2126
|
if (indexCandidates.includes(entry)) continue;
|
|
2063
2127
|
if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
|
|
2064
|
-
await importIfExists(
|
|
2128
|
+
await importIfExists(path3.join(packageDir, entry));
|
|
2065
2129
|
}
|
|
2066
2130
|
}
|
|
2067
2131
|
};
|