@luckystack/server 0.2.5 → 0.2.7

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/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 tryCatch9, isProduction as isProduction2 } from "@luckystack/core";
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
- const secure = process.env.SECURE === "true";
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 handleHttpRequest = async (req, res, options) => {
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 routePath = routePathRaw ?? "/";
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 requestId = (Array.isArray(incomingRequestId) ? incomingRequestId[0] : incomingRequestId) ?? randomUUID();
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 tryCatch7
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 tryCatch7(() => next);
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 tryCatch7(async () => {
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 tryCatch8
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 tryCatch8(fn);
1903
+ const [error] = await tryCatch9(fn);
1870
1904
  if (error) getLogger11().warn(`[shutdown] step "${label}" failed: ${error.message}`);
1871
1905
  return true;
1872
1906
  })();
@@ -2007,7 +2041,7 @@ var createLuckyStackServer = async (options = {}) => {
2007
2041
  if (enableDevTools) {
2008
2042
  await initDevTools();
2009
2043
  }
2010
- const [bootUuidError] = await tryCatch9(() => writeBootUuid());
2044
+ const [bootUuidError] = await tryCatch10(() => writeBootUuid());
2011
2045
  if (bootUuidError) {
2012
2046
  throw new Error(
2013
2047
  "Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USER / REDIS_PASSWORD and that Redis is reachable.",