@luckystack/server 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  getParsedBundles,
4
4
  getParsedPort,
5
5
  parseServerArgv
6
- } from "./chunk-X3OSC5W3.js";
6
+ } from "./chunk-RIVFTOTI.js";
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, dispatchHook as dispatchHook8 } from "@luckystack/core";
11
11
 
12
12
  // src/httpHandler.ts
13
13
  import { randomUUID } from "crypto";
@@ -107,6 +107,7 @@ var OPTIONAL_PACKAGES = [
107
107
  "email",
108
108
  "error-tracking",
109
109
  "presence",
110
+ "cron",
110
111
  "docs-ui"
111
112
  ];
112
113
  var canResolve = (specifier) => has(specifier);
@@ -138,7 +139,12 @@ var getOriginExemptPaths = () => exemptPaths;
138
139
  var clearOriginExemptPaths = () => {
139
140
  exemptPaths.length = 0;
140
141
  };
141
- var isOriginExemptPath = (routePath) => exemptPaths.some((matcher) => routePath.startsWith(matcher.pathPrefix));
142
+ var isOriginExemptPath = (routePath) => exemptPaths.some(({ pathPrefix }) => {
143
+ if (!pathPrefix) return false;
144
+ if (routePath === pathPrefix) return true;
145
+ const boundary = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
146
+ return routePath.startsWith(boundary);
147
+ });
142
148
 
143
149
  // src/httpRoutes/timingSafeEqual.ts
144
150
  import { timingSafeEqual as cryptoTimingSafeEqual } from "crypto";
@@ -276,7 +282,10 @@ var handleFaviconRoute = async ({ res, routePath, options }) => {
276
282
  import {
277
283
  computeSynchronizedEnvHashes,
278
284
  describeHealthHashConfig,
285
+ getDbHealthCheck,
279
286
  getProjectConfig as getProjectConfig3,
287
+ isPrismaClientRegistered,
288
+ isPrismaClientResolvable,
280
289
  prisma,
281
290
  readBootUuid,
282
291
  redis,
@@ -297,6 +306,16 @@ var pingPrisma = async () => {
297
306
  }
298
307
  return false;
299
308
  };
309
+ var checkDatabaseReady = async () => {
310
+ const registered = getDbHealthCheck();
311
+ if (registered) {
312
+ const [error, result] = await tryCatch(async () => registered());
313
+ if (error || result === null) return false;
314
+ return result;
315
+ }
316
+ if (isPrismaClientRegistered() || isPrismaClientResolvable()) return pingPrisma();
317
+ return "skipped";
318
+ };
300
319
  var handleLivezRoute = ({ res, routePath }) => {
301
320
  if (routePath !== getProjectConfig3().http.liveEndpoint) return Promise.resolve(false);
302
321
  res.statusCode = 200;
@@ -309,13 +328,21 @@ var handleReadyzRoute = async ({ res, routePath }) => {
309
328
  const bootUuid = await readBootUuid();
310
329
  const [redisError, pong] = await tryCatch(() => redis.ping());
311
330
  const redisOk = !redisError && (pong === "PONG" || Boolean(pong));
312
- const prismaOk = await pingPrisma();
313
- const ready = Boolean(bootUuid) && redisOk && prismaOk;
331
+ const databaseResult = await checkDatabaseReady();
332
+ const ready = Boolean(bootUuid) && redisOk && databaseResult !== false;
314
333
  res.statusCode = ready ? 200 : 503;
315
334
  res.setHeader("Content-Type", "application/json");
316
335
  res.end(JSON.stringify({
317
336
  status: ready ? "ready" : "not-ready",
318
- checks: { bootUuid: Boolean(bootUuid), redis: redisOk, prisma: prismaOk }
337
+ //? `prisma` kept for backward compatibility with existing probes/dashboards
338
+ //? (true when the database check passed OR was deliberately skipped);
339
+ //? `database` carries the richer tri-state.
340
+ checks: {
341
+ bootUuid: Boolean(bootUuid),
342
+ redis: redisOk,
343
+ database: databaseResult,
344
+ prisma: databaseResult !== false
345
+ }
319
346
  }));
320
347
  return true;
321
348
  };
@@ -427,7 +454,12 @@ import {
427
454
  } from "@luckystack/core";
428
455
 
429
456
  // src/httpRoutes/sessionCookie.ts
430
- var resolveCookieSecure = (sessionCookieSecure, secureEnv) => sessionCookieSecure ?? secureEnv === "true";
457
+ import { resolveEnvKey as resolveEnvKey2 } from "@luckystack/core";
458
+ var resolveCookieSecure = (sessionCookieSecure, secureEnv) => {
459
+ if (sessionCookieSecure !== void 0) return sessionCookieSecure;
460
+ if (secureEnv === "true") return true;
461
+ return resolveEnvKey2() === "production";
462
+ };
431
463
 
432
464
  // src/httpRoutes/authApiRoute.ts
433
465
  var parseSessionBasedTokenHeader = (headerValue) => {
@@ -481,7 +513,7 @@ var handleAuthApiRoute = async ({
481
513
  });
482
514
  const oauthInitLimit = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.defaultApiLimit : oauthRateLimiting.auth.enabled && oauthRateLimiting.auth.maxAttempts > 0 ? oauthRateLimiting.auth.maxAttempts : null;
483
515
  if (oauthInitLimit !== null) {
484
- const oauthInitWindowMs = oauthRateLimiting.defaultApiLimit === false ? oauthRateLimiting.auth.windowMs : oauthRateLimiting.windowMs;
516
+ const oauthInitWindowMs = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.windowMs : oauthRateLimiting.auth.windowMs;
485
517
  const { allowed, resetIn } = await checkRateLimit({
486
518
  key: `ip:${oauthRequesterIp}:auth:oauth-init`,
487
519
  limit: oauthInitLimit,
@@ -551,7 +583,7 @@ var handleAuthApiRoute = async ({
551
583
  });
552
584
  const ipLimitCount = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.defaultApiLimit : rateLimiting.auth.enabled && rateLimiting.auth.maxAttempts > 0 ? rateLimiting.auth.maxAttempts : null;
553
585
  if (ipLimitCount !== null) {
554
- const ipWindowMs = rateLimiting.defaultApiLimit === false ? rateLimiting.auth.windowMs : rateLimiting.windowMs;
586
+ const ipWindowMs = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.windowMs : rateLimiting.auth.windowMs;
555
587
  const { allowed, resetIn } = await checkRateLimit({
556
588
  key: `ip:${requesterIp}:auth:credentials`,
557
589
  limit: ipLimitCount,
@@ -1197,7 +1229,7 @@ var enforceOriginPolicy = (req, res, routePath) => {
1197
1229
  });
1198
1230
  const isStateChangingMethod = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
1199
1231
  if (isOriginExemptPath(routePath)) {
1200
- return { origin, rejected: false };
1232
+ return { origin: "", rejected: false };
1201
1233
  }
1202
1234
  if (!origin) {
1203
1235
  if (isStateChangingMethod) {
@@ -1760,7 +1792,7 @@ import {
1760
1792
  isLocalizedNormalizerRegistered,
1761
1793
  isProjectConfigRegistered,
1762
1794
  isRuntimeMapsProviderRegistered,
1763
- resolveEnvKey as resolveEnvKey2
1795
+ resolveEnvKey as resolveEnvKey3
1764
1796
  } from "@luckystack/core";
1765
1797
  var verifyBootstrap = async (requirements = {}) => {
1766
1798
  const missing = [];
@@ -1799,7 +1831,7 @@ var verifyBootstrap = async (requirements = {}) => {
1799
1831
  }
1800
1832
  }
1801
1833
  if (!isRuntimeMapsProviderRegistered()) {
1802
- if (resolveEnvKey2() === "production") {
1834
+ if (resolveEnvKey3() === "production") {
1803
1835
  missing.push(
1804
1836
  "RuntimeMapsProvider \u2014 call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound."
1805
1837
  );
@@ -1810,7 +1842,7 @@ var verifyBootstrap = async (requirements = {}) => {
1810
1842
  }
1811
1843
  }
1812
1844
  if (!isLocalizedNormalizerRegistered()) {
1813
- if (resolveEnvKey2() === "production") {
1845
+ if (resolveEnvKey3() === "production") {
1814
1846
  missing.push(
1815
1847
  "LocalizedNormalizer \u2014 call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n)."
1816
1848
  );
@@ -1829,6 +1861,14 @@ var verifyBootstrap = async (requirements = {}) => {
1829
1861
  );
1830
1862
  }
1831
1863
  }
1864
+ if (isProjectConfigRegistered()) {
1865
+ const cfg = getProjectConfig12();
1866
+ if (!cfg.session.basedToken && cfg.http.sessionCookieSameSite !== "Strict") {
1867
+ getLogger9().warn(
1868
+ `[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.`
1869
+ );
1870
+ }
1871
+ }
1832
1872
  if (missing.length === 0) return;
1833
1873
  const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join("\n");
1834
1874
  throw new Error(
@@ -1845,7 +1885,7 @@ var verifyBootstrap = async (requirements = {}) => {
1845
1885
  import {
1846
1886
  getLogger as getLogger10,
1847
1887
  registerRuntimeMapsProvider,
1848
- resolveEnvKey as resolveEnvKey3
1888
+ resolveEnvKey as resolveEnvKey4
1849
1889
  } from "@luckystack/core";
1850
1890
  var emptyRuntimeMaps = {
1851
1891
  apisObject: {},
@@ -1895,7 +1935,7 @@ var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
1895
1935
  target[key] = source[key];
1896
1936
  }
1897
1937
  };
1898
- var isProduction = () => resolveEnvKey3() === "production";
1938
+ var isProduction = () => resolveEnvKey4() === "production";
1899
1939
  var createProdRuntimeMapsProvider = (options) => {
1900
1940
  let prodMapsPromise = null;
1901
1941
  let devkitModulePromise = null;
@@ -2063,8 +2103,21 @@ var clearDevServerInfo = () => {
2063
2103
  var initDevTools = async () => {
2064
2104
  const { initConsolelog } = await import("@luckystack/core");
2065
2105
  initConsolelog();
2066
- process.once("SIGINT", () => process.exit(0));
2067
- process.once("SIGTERM", () => process.exit(0));
2106
+ const devSignalExit = (reason) => {
2107
+ void Promise.race([
2108
+ dispatchHook8("preServerStop", { reason, timeoutMs: 2e3 }),
2109
+ new Promise((resolve) => {
2110
+ setTimeout(resolve, 2e3);
2111
+ })
2112
+ // eslint-disable-next-line unicorn/no-process-exit -- deliberate dev hard-exit once the bounded hook window closes (mirrors the original inline handler)
2113
+ ]).finally(() => process.exit(0));
2114
+ };
2115
+ process.once("SIGINT", () => {
2116
+ devSignalExit("SIGINT");
2117
+ });
2118
+ process.once("SIGTERM", () => {
2119
+ devSignalExit("SIGTERM");
2120
+ });
2068
2121
  const devkitModuleId = "@luckystack/devkit";
2069
2122
  if (!canResolve(devkitModuleId)) {
2070
2123
  getLogger13().warn(
@@ -2137,7 +2190,7 @@ var createLuckyStackServer = async (options = {}) => {
2137
2190
  });
2138
2191
  const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;
2139
2192
  const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
2140
- const enableDevTools = options.enableDevTools ?? resolveEnvKey4() !== "production";
2193
+ const enableDevTools = options.enableDevTools ?? resolveEnvKey5() !== "production";
2141
2194
  registerBindAddress({
2142
2195
  ip,
2143
2196
  port: typeof port === "string" ? Number.parseInt(port, 10) : port
@@ -2189,7 +2242,7 @@ var createLuckyStackServer = async (options = {}) => {
2189
2242
  import path3 from "path";
2190
2243
  import fs2 from "fs";
2191
2244
  import { pathToFileURL } from "url";
2192
- import { ROOT_DIR } from "@luckystack/core";
2245
+ import { ROOT_DIR, tryCatch as tryCatch11 } from "@luckystack/core";
2193
2246
  var OVERLAY_ORDER = [
2194
2247
  // Core registries first — clients, paths, routing rules. Anything below
2195
2248
  // depends on these being in place.
@@ -2204,6 +2257,9 @@ var OVERLAY_ORDER = [
2204
2257
  // Sentry / docs-ui / presence sit on top of core but don't block boot.
2205
2258
  "sentry",
2206
2259
  "presence",
2260
+ // Cron jobs — `registerCronJob(...)` calls in `luckystack/cron/*.ts` lazily
2261
+ // start the leader-elected scheduler (optional @luckystack/cron).
2262
+ "cron",
2207
2263
  "docs-ui",
2208
2264
  // Server overlay last — typically empty, but a place to wire framework
2209
2265
  // hooks (`registerHook('onSocketConnect', ...)` etc.) before listen().
@@ -2211,7 +2267,18 @@ var OVERLAY_ORDER = [
2211
2267
  ];
2212
2268
  var importIfExists = async (filePath) => {
2213
2269
  if (!fs2.existsSync(filePath)) return;
2214
- await import(pathToFileURL(filePath).href);
2270
+ const [error] = await tryCatch11(async () => {
2271
+ await import(pathToFileURL(filePath).href);
2272
+ });
2273
+ if (error) {
2274
+ throw new Error(
2275
+ `[luckystack] failed to load overlay file ${filePath}: ${error.message}. If you removed an optional package (e.g. via \`luckystack remove <feature>\` or \`luckystack manage\`), delete or update the overlay files that still import it (the \`luckystack/<feature>/\` folder).`
2276
+ );
2277
+ }
2278
+ };
2279
+ var registeredOverlayLoader = null;
2280
+ var registerOverlayLoader = (loader) => {
2281
+ registeredOverlayLoader = loader;
2215
2282
  };
2216
2283
  var loadOverlayFolder = async (overlayRoot) => {
2217
2284
  const overlayAbs = path3.isAbsolute(overlayRoot) ? overlayRoot : path3.join(ROOT_DIR, overlayRoot);
@@ -2250,7 +2317,7 @@ var bootstrapLuckyStack = async (options = {}) => {
2250
2317
  const overlayRoot = options.overlayRoot ?? "luckystack";
2251
2318
  if (!options.skipOverlayLoad) {
2252
2319
  await importOptionalPackageRegisters();
2253
- await loadOverlayFolder(overlayRoot);
2320
+ await (registeredOverlayLoader ? registeredOverlayLoader() : loadOverlayFolder(overlayRoot));
2254
2321
  }
2255
2322
  await getLogin();
2256
2323
  const server = await createLuckyStackServer(options);
@@ -2264,6 +2331,7 @@ import {
2264
2331
  applyErrorFormatter
2265
2332
  } from "@luckystack/core";
2266
2333
  export {
2334
+ OVERLAY_ORDER,
2267
2335
  applyServerArgv,
2268
2336
  bootstrapLuckyStack,
2269
2337
  clearCustomRoutes,
@@ -2282,6 +2350,7 @@ export {
2282
2350
  registerCustomRoute,
2283
2351
  registerErrorFormatter,
2284
2352
  registerOriginExemptPath,
2353
+ registerOverlayLoader,
2285
2354
  registerProdRuntimeMapsProvider,
2286
2355
  registerSecurityHeaders,
2287
2356
  verifyBootstrap