@luckystack/server 0.7.5 → 0.8.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/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-07-27
11
+
12
+ ### Added
13
+
14
+ - Routed GET API calls can carry a lossless typed payload through the reserved `__luckystack_data` query value while public key-per-query callers remain compatible.
15
+
16
+ ## [0.7.6] - 2026-07-23
17
+
18
+ ### Fixed
19
+
20
+ - A successfully listening server now refreshes its boot-UUID TTL every third
21
+ of the configured lifetime and stops the heartbeat during shutdown. Healthy
22
+ long-running servers therefore no longer become not-ready after one hour.
23
+
10
24
  ## [0.7.4] - 2026-07-22
11
25
 
12
26
  ### Fixed
package/CLAUDE.md CHANGED
@@ -25,7 +25,7 @@ One-call server bootstrap for a LuckyStack project. Wires together a raw Node.js
25
25
 
26
26
  | Function / Export | One-liner | Deep doc |
27
27
  |---|---|---|
28
- | `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }`. Wires HTTP + Socket.io + framework routes; installs a persistent `httpServer.on('error')` listener and (in prod) SIGTERM/SIGINT → graceful `stop()`. | -> docs/create-server.md |
28
+ | `createLuckyStackServer(options)` | Lower-level factory that returns `{ httpServer, ioServer, listen, stop, close }`. Wires HTTP + Socket.io + framework routes; after a successful listen starts the boot-UUID TTL heartbeat, and stops it during shutdown. Installs a persistent `httpServer.on('error')` listener and (in prod) SIGTERM/SIGINT → graceful `stop()`. | -> docs/create-server.md |
29
29
  | `RunningLuckyStackServer.stop(options?)` / `.close(options?)` | Graceful shutdown (MIS-016). Stops accepting new connections, dispatches the core `preServerStop` hook, flushes error-trackers (bounded by a per-step timeout race), then closes io + http + the Redis-adapter pub/sub clients. Each step isolated + time-bounded so one hanging step can't stall shutdown. Idempotent. `options`: `{ reason?, timeoutMs? }` (default `timeoutMs` 10000). | -> docs/create-server.md |
30
30
  | `bootstrapLuckyStack(options)` | High-level entry: auto-imports `luckystack/<pkg>/*.ts` overlay in topological order, then delegates to `createLuckyStackServer`. An overlay file that fails to import (classic: it imports a package removed via `luckystack remove`) aborts boot with an ACTIONABLE error naming the file, instead of a raw ERR_MODULE_NOT_FOUND. | -> docs/create-server.md |
31
31
  | `OVERLAY_ORDER` (exported const) | The canonical overlay walk order (`core, deploy, login, email, sentry, presence, cron, docs-ui, server`). Exported so the consumer's `scripts/bundleServer.mjs` imports it at build time — a hardcoded bundler copy once silently dropped the cron slot from prod; `overlayOrderParity.test.ts` pins the bundlers' fallback lists to this. | -> docs/create-server.md |
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/createServer.ts
11
11
  import http from "http";
12
- import { registerBindAddress, registerBoundAddress, writeBootUuid, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10, tryCatchSync as tryCatchSync2, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
12
+ import { registerBindAddress, registerBoundAddress, writeBootUuid, startBootUuidHeartbeat, getLogger as getLogger13, getProjectConfig as getProjectConfig15, tryCatch as tryCatch10, tryCatchSync as tryCatchSync3, resolveEnvKey as resolveEnvKey5, dispatchHook as dispatchHook8 } from "@luckystack/core";
13
13
 
14
14
  // src/httpHandler.ts
15
15
  import { randomUUID } from "crypto";
@@ -24,7 +24,7 @@ import {
24
24
  normalizeOrigin,
25
25
  readSession as readSession3,
26
26
  tryCatch as tryCatch7,
27
- tryCatchSync
27
+ tryCatchSync as tryCatchSync2
28
28
  } from "@luckystack/core";
29
29
 
30
30
  // src/logSanitize.ts
@@ -921,7 +921,8 @@ import {
921
921
  dispatchHook as dispatchHook3,
922
922
  extractTokenFromRequest,
923
923
  getLogger as getLogger4,
924
- tryCatch as tryCatch4
924
+ tryCatch as tryCatch4,
925
+ tryCatchSync
925
926
  } from "@luckystack/core";
926
927
  import { handleHttpApiRequest } from "@luckystack/api";
927
928
 
@@ -1038,8 +1039,25 @@ var handleApiRoute = async ({
1038
1039
  res.end(JSON.stringify(response));
1039
1040
  return true;
1040
1041
  }
1041
- const apiData = typeof params === "object" ? { ...params } : {};
1042
+ let apiData = typeof params === "object" ? { ...params } : {};
1042
1043
  delete apiData.stream;
1044
+ const routedData = apiData.__luckystack_data;
1045
+ if (typeof routedData === "string") {
1046
+ const [parseError, parsed] = tryCatchSync(() => JSON.parse(routedData));
1047
+ if (parseError || parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1048
+ const response = {
1049
+ status: "error",
1050
+ httpStatus: 400,
1051
+ message: "api.invalidRequestFormat",
1052
+ errorCode: "api.invalidRequestFormat"
1053
+ };
1054
+ res.setHeader("Content-Type", "application/json");
1055
+ res.writeHead(400);
1056
+ res.end(JSON.stringify(response));
1057
+ return true;
1058
+ }
1059
+ apiData = { ...parsed };
1060
+ }
1043
1061
  const requesterIp = resolveRequesterIp(req);
1044
1062
  const result = await handleHttpApiRequest({
1045
1063
  name: apiName,
@@ -1390,7 +1408,7 @@ var setSecurityHeaders = (req, res, origin) => {
1390
1408
  res.setHeader("X-Content-Type-Options", securityHeaders.contentTypeOptions);
1391
1409
  const builder = getSecurityHeadersBuilder();
1392
1410
  if (builder) {
1393
- const [error] = tryCatchSync(() => {
1411
+ const [error] = tryCatchSync2(() => {
1394
1412
  const custom = builder(req);
1395
1413
  if (custom) {
1396
1414
  for (const [name, value] of Object.entries(custom)) {
@@ -1509,7 +1527,7 @@ var handleHttpRequestInner = async (req, res, options) => {
1509
1527
  );
1510
1528
  const url = req.url ?? "/";
1511
1529
  const [routePathRaw, queryStringRaw] = url.split("?");
1512
- const [decodeError, decodedPath] = tryCatchSync(() => decodeURIComponent(routePathRaw ?? "/"));
1530
+ const [decodeError, decodedPath] = tryCatchSync2(() => decodeURIComponent(routePathRaw ?? "/"));
1513
1531
  if (decodeError || decodedPath === null) {
1514
1532
  res.statusCode = 400;
1515
1533
  res.setHeader("Content-Type", "text/plain");
@@ -2398,8 +2416,8 @@ var listenLuckyStackServer = (httpServer, ip, port, callback) => new Promise((re
2398
2416
  process.once("exit", clearDevServerInfo);
2399
2417
  }
2400
2418
  if (attemptPort !== startPort && !production) {
2401
- const configuredCallbackBase = tryCatchSync2(() => getProjectConfig15().oauthCallbackBase)[1] ?? "";
2402
- const callbackPort = tryCatchSync2(() => {
2419
+ const configuredCallbackBase = tryCatchSync3(() => getProjectConfig15().oauthCallbackBase)[1] ?? "";
2420
+ const callbackPort = tryCatchSync3(() => {
2403
2421
  const callbackUrl = new URL(configuredCallbackBase);
2404
2422
  return callbackUrl.port || (callbackUrl.protocol === "https:" ? "443" : "80");
2405
2423
  })[1] ?? "";
@@ -2460,9 +2478,15 @@ var createLuckyStackServer = async (options = {}) => {
2460
2478
  const { io: ioServer, adapterClients } = loadSocket(httpServer, {
2461
2479
  maxHttpBufferSize: options.maxHttpBufferSize
2462
2480
  });
2463
- const listen = (callback) => listenLuckyStackServer(httpServer, ip, port, callback);
2481
+ let bootUuidHeartbeat = null;
2482
+ const listen = async (callback) => {
2483
+ const listeningServer = await listenLuckyStackServer(httpServer, ip, port, callback);
2484
+ bootUuidHeartbeat ??= startBootUuidHeartbeat();
2485
+ return listeningServer;
2486
+ };
2464
2487
  let shutdownPromise = null;
2465
2488
  const stop = (stopOptions = {}) => {
2489
+ bootUuidHeartbeat?.stop();
2466
2490
  shutdownPromise ??= runGracefulShutdown({ httpServer, ioServer, adapterClients }, stopOptions);
2467
2491
  return shutdownPromise;
2468
2492
  };