@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/CHANGELOG.md +21 -0
- package/CLAUDE.md +87 -85
- package/LICENSE +21 -21
- package/README.md +198 -198
- package/dist/{chunk-X3OSC5W3.js → chunk-RIVFTOTI.js} +1 -1
- package/dist/chunk-RIVFTOTI.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +90 -21
- package/dist/index.js.map +1 -1
- package/dist/parseArgv.js +1 -1
- package/dist/parseArgv.js.map +1 -1
- package/docs/argv-parsing.md +256 -256
- package/docs/create-server.md +295 -295
- package/docs/http-routes.md +311 -309
- package/docs/request-pipeline.md +198 -198
- package/docs/runtime-maps.md +206 -206
- package/docs/security-defaults.md +236 -236
- package/package.json +13 -10
- package/dist/chunk-X3OSC5W3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
getParsedBundles,
|
|
4
4
|
getParsedPort,
|
|
5
5
|
parseServerArgv
|
|
6
|
-
} from "./chunk-
|
|
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
|
|
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((
|
|
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
|
|
313
|
-
const ready = Boolean(bootUuid) && redisOk &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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 = () =>
|
|
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
|
-
|
|
2067
|
-
|
|
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 ??
|
|
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
|
|
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
|