@luckystack/server 0.4.0 → 0.4.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/dist/index.d.ts CHANGED
@@ -281,7 +281,12 @@ declare const getPreParamsCustomRoutes: () => readonly CustomRouteHandler[];
281
281
  declare const clearCustomRoutes: () => void;
282
282
 
283
283
  interface OriginExemptMatcher {
284
- /** A route is exempt when its path starts with this prefix. */
284
+ /**
285
+ * A route is exempt when its path equals this prefix OR continues past it on a
286
+ * path-SEGMENT boundary (i.e. `<prefix>/...`). Matching is boundary-aware so a
287
+ * prefix can't bleed into a sibling route — `/webhooks` exempts `/webhooks` and
288
+ * `/webhooks/stripe` but NOT `/webhooksadmin`.
289
+ */
285
290
  pathPrefix: string;
286
291
  }
287
292
  declare const registerOriginExemptPath: (matcher: OriginExemptMatcher) => void;
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 resolveEnvKey4 } from "@luckystack/core";
10
+ import { registerBindAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig13, tryCatch as tryCatch10, isProduction as isProduction2, resolveEnvKey as resolveEnvKey5 } from "@luckystack/core";
11
11
 
12
12
  // src/httpHandler.ts
13
13
  import { randomUUID } from "crypto";
@@ -138,7 +138,12 @@ var getOriginExemptPaths = () => exemptPaths;
138
138
  var clearOriginExemptPaths = () => {
139
139
  exemptPaths.length = 0;
140
140
  };
141
- var isOriginExemptPath = (routePath) => exemptPaths.some((matcher) => routePath.startsWith(matcher.pathPrefix));
141
+ var isOriginExemptPath = (routePath) => exemptPaths.some(({ pathPrefix }) => {
142
+ if (!pathPrefix) return false;
143
+ if (routePath === pathPrefix) return true;
144
+ const boundary = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
145
+ return routePath.startsWith(boundary);
146
+ });
142
147
 
143
148
  // src/httpRoutes/timingSafeEqual.ts
144
149
  import { timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
@@ -427,7 +432,12 @@ import {
427
432
  } from "@luckystack/core";
428
433
 
429
434
  // src/httpRoutes/sessionCookie.ts
430
- var resolveCookieSecure = (sessionCookieSecure, secureEnv) => sessionCookieSecure ?? secureEnv === "true";
435
+ import { resolveEnvKey as resolveEnvKey2 } from "@luckystack/core";
436
+ var resolveCookieSecure = (sessionCookieSecure, secureEnv) => {
437
+ if (sessionCookieSecure !== void 0) return sessionCookieSecure;
438
+ if (secureEnv === "true") return true;
439
+ return resolveEnvKey2() === "production";
440
+ };
431
441
 
432
442
  // src/httpRoutes/authApiRoute.ts
433
443
  var parseSessionBasedTokenHeader = (headerValue) => {
@@ -481,7 +491,7 @@ var handleAuthApiRoute = async ({
481
491
  });
482
492
  const oauthInitLimit = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.defaultApiLimit : oauthRateLimiting.auth.enabled && oauthRateLimiting.auth.maxAttempts > 0 ? oauthRateLimiting.auth.maxAttempts : null;
483
493
  if (oauthInitLimit !== null) {
484
- const oauthInitWindowMs = oauthRateLimiting.defaultApiLimit === false ? oauthRateLimiting.auth.windowMs : oauthRateLimiting.windowMs;
494
+ const oauthInitWindowMs = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.windowMs : oauthRateLimiting.auth.windowMs;
485
495
  const { allowed, resetIn } = await checkRateLimit({
486
496
  key: `ip:${oauthRequesterIp}:auth:oauth-init`,
487
497
  limit: oauthInitLimit,
@@ -551,7 +561,7 @@ var handleAuthApiRoute = async ({
551
561
  });
552
562
  const ipLimitCount = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.defaultApiLimit : rateLimiting.auth.enabled && rateLimiting.auth.maxAttempts > 0 ? rateLimiting.auth.maxAttempts : null;
553
563
  if (ipLimitCount !== null) {
554
- const ipWindowMs = rateLimiting.defaultApiLimit === false ? rateLimiting.auth.windowMs : rateLimiting.windowMs;
564
+ const ipWindowMs = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.windowMs : rateLimiting.auth.windowMs;
555
565
  const { allowed, resetIn } = await checkRateLimit({
556
566
  key: `ip:${requesterIp}:auth:credentials`,
557
567
  limit: ipLimitCount,
@@ -1197,7 +1207,7 @@ var enforceOriginPolicy = (req, res, routePath) => {
1197
1207
  });
1198
1208
  const isStateChangingMethod = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
1199
1209
  if (isOriginExemptPath(routePath)) {
1200
- return { origin, rejected: false };
1210
+ return { origin: "", rejected: false };
1201
1211
  }
1202
1212
  if (!origin) {
1203
1213
  if (isStateChangingMethod) {
@@ -1760,7 +1770,7 @@ import {
1760
1770
  isLocalizedNormalizerRegistered,
1761
1771
  isProjectConfigRegistered,
1762
1772
  isRuntimeMapsProviderRegistered,
1763
- resolveEnvKey as resolveEnvKey2
1773
+ resolveEnvKey as resolveEnvKey3
1764
1774
  } from "@luckystack/core";
1765
1775
  var verifyBootstrap = async (requirements = {}) => {
1766
1776
  const missing = [];
@@ -1799,7 +1809,7 @@ var verifyBootstrap = async (requirements = {}) => {
1799
1809
  }
1800
1810
  }
1801
1811
  if (!isRuntimeMapsProviderRegistered()) {
1802
- if (resolveEnvKey2() === "production") {
1812
+ if (resolveEnvKey3() === "production") {
1803
1813
  missing.push(
1804
1814
  "RuntimeMapsProvider \u2014 call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound."
1805
1815
  );
@@ -1810,7 +1820,7 @@ var verifyBootstrap = async (requirements = {}) => {
1810
1820
  }
1811
1821
  }
1812
1822
  if (!isLocalizedNormalizerRegistered()) {
1813
- if (resolveEnvKey2() === "production") {
1823
+ if (resolveEnvKey3() === "production") {
1814
1824
  missing.push(
1815
1825
  "LocalizedNormalizer \u2014 call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n)."
1816
1826
  );
@@ -1829,6 +1839,14 @@ var verifyBootstrap = async (requirements = {}) => {
1829
1839
  );
1830
1840
  }
1831
1841
  }
1842
+ if (isProjectConfigRegistered()) {
1843
+ const cfg = getProjectConfig12();
1844
+ if (!cfg.session.basedToken && cfg.http.sessionCookieSameSite !== "Strict") {
1845
+ getLogger9().warn(
1846
+ `[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.`
1847
+ );
1848
+ }
1849
+ }
1832
1850
  if (missing.length === 0) return;
1833
1851
  const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join("\n");
1834
1852
  throw new Error(
@@ -1845,7 +1863,7 @@ var verifyBootstrap = async (requirements = {}) => {
1845
1863
  import {
1846
1864
  getLogger as getLogger10,
1847
1865
  registerRuntimeMapsProvider,
1848
- resolveEnvKey as resolveEnvKey3
1866
+ resolveEnvKey as resolveEnvKey4
1849
1867
  } from "@luckystack/core";
1850
1868
  var emptyRuntimeMaps = {
1851
1869
  apisObject: {},
@@ -1895,7 +1913,7 @@ var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
1895
1913
  target[key] = source[key];
1896
1914
  }
1897
1915
  };
1898
- var isProduction = () => resolveEnvKey3() === "production";
1916
+ var isProduction = () => resolveEnvKey4() === "production";
1899
1917
  var createProdRuntimeMapsProvider = (options) => {
1900
1918
  let prodMapsPromise = null;
1901
1919
  let devkitModulePromise = null;
@@ -2137,7 +2155,7 @@ var createLuckyStackServer = async (options = {}) => {
2137
2155
  });
2138
2156
  const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;
2139
2157
  const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
2140
- const enableDevTools = options.enableDevTools ?? resolveEnvKey4() !== "production";
2158
+ const enableDevTools = options.enableDevTools ?? resolveEnvKey5() !== "production";
2141
2159
  registerBindAddress({
2142
2160
  ip,
2143
2161
  port: typeof port === "string" ? Number.parseInt(port, 10) : port