@luckystack/server 0.3.1 → 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 +6 -1
- package/dist/index.js +66 -15
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
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
|
-
/**
|
|
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
|
|
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((
|
|
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
|
-
|
|
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) => {
|
|
@@ -472,6 +482,40 @@ var handleAuthApiRoute = async ({
|
|
|
472
482
|
return true;
|
|
473
483
|
}
|
|
474
484
|
if (login.isFullOAuthProvider(provider)) {
|
|
485
|
+
const oauthRateLimiting = config.rateLimiting;
|
|
486
|
+
const oauthRequesterIp = resolveClientIp({
|
|
487
|
+
rawAddress: req.socket.remoteAddress,
|
|
488
|
+
headers: req.headers,
|
|
489
|
+
trustProxy: config.http.trustProxy,
|
|
490
|
+
trustedProxyHopCount: config.http.trustedProxyHopCount
|
|
491
|
+
});
|
|
492
|
+
const oauthInitLimit = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.defaultApiLimit : oauthRateLimiting.auth.enabled && oauthRateLimiting.auth.maxAttempts > 0 ? oauthRateLimiting.auth.maxAttempts : null;
|
|
493
|
+
if (oauthInitLimit !== null) {
|
|
494
|
+
const oauthInitWindowMs = oauthRateLimiting.defaultApiLimit !== false && oauthRateLimiting.defaultApiLimit > 0 ? oauthRateLimiting.windowMs : oauthRateLimiting.auth.windowMs;
|
|
495
|
+
const { allowed, resetIn } = await checkRateLimit({
|
|
496
|
+
key: `ip:${oauthRequesterIp}:auth:oauth-init`,
|
|
497
|
+
limit: oauthInitLimit,
|
|
498
|
+
windowMs: oauthInitWindowMs
|
|
499
|
+
});
|
|
500
|
+
if (!allowed) {
|
|
501
|
+
void dispatchHook2("rateLimitExceeded", {
|
|
502
|
+
scope: "auth",
|
|
503
|
+
key: `ip:${oauthRequesterIp}:auth:oauth-init`,
|
|
504
|
+
limit: oauthInitLimit,
|
|
505
|
+
windowMs: oauthInitWindowMs,
|
|
506
|
+
count: oauthInitLimit + 1,
|
|
507
|
+
route: routePath,
|
|
508
|
+
ip: oauthRequesterIp
|
|
509
|
+
});
|
|
510
|
+
res.writeHead(429, { "content-type": "application/json; charset=utf-8" });
|
|
511
|
+
res.end(JSON.stringify({
|
|
512
|
+
status: false,
|
|
513
|
+
reason: "api.rateLimitExceeded",
|
|
514
|
+
errorParams: [{ key: "seconds", value: resetIn }]
|
|
515
|
+
}));
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
475
519
|
const reqUrl = new URL(req.url ?? "/", "http://placeholder");
|
|
476
520
|
const returnUrl = reqUrl.searchParams.get("return_url") ?? void 0;
|
|
477
521
|
const oauthState = await login.createOAuthState(provider.name, { usePkce: provider.usePkce, returnUrl });
|
|
@@ -517,7 +561,7 @@ var handleAuthApiRoute = async ({
|
|
|
517
561
|
});
|
|
518
562
|
const ipLimitCount = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.defaultApiLimit : rateLimiting.auth.enabled && rateLimiting.auth.maxAttempts > 0 ? rateLimiting.auth.maxAttempts : null;
|
|
519
563
|
if (ipLimitCount !== null) {
|
|
520
|
-
const ipWindowMs = rateLimiting.defaultApiLimit
|
|
564
|
+
const ipWindowMs = rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0 ? rateLimiting.windowMs : rateLimiting.auth.windowMs;
|
|
521
565
|
const { allowed, resetIn } = await checkRateLimit({
|
|
522
566
|
key: `ip:${requesterIp}:auth:credentials`,
|
|
523
567
|
limit: ipLimitCount,
|
|
@@ -1163,7 +1207,7 @@ var enforceOriginPolicy = (req, res, routePath) => {
|
|
|
1163
1207
|
});
|
|
1164
1208
|
const isStateChangingMethod = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
|
|
1165
1209
|
if (isOriginExemptPath(routePath)) {
|
|
1166
|
-
return { origin, rejected: false };
|
|
1210
|
+
return { origin: "", rejected: false };
|
|
1167
1211
|
}
|
|
1168
1212
|
if (!origin) {
|
|
1169
1213
|
if (isStateChangingMethod) {
|
|
@@ -1419,8 +1463,7 @@ var executeRoomMutation = async (opts) => {
|
|
|
1419
1463
|
return;
|
|
1420
1464
|
}
|
|
1421
1465
|
const existingRoomCodes = getSessionRoomCodes(session);
|
|
1422
|
-
const
|
|
1423
|
-
const physicalRoom = formatRoomName(group, { purpose: roomPurpose, userId: session.id });
|
|
1466
|
+
const physicalRoom = formatRoomName(group, { purpose: "broadcast", userId: session.id });
|
|
1424
1467
|
const nextRoomCodes = await mutate(socket, physicalRoom, group, existingRoomCodes, session.id);
|
|
1425
1468
|
const sanitizedSession = sanitizeSessionRoomKeys(session);
|
|
1426
1469
|
await writeSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });
|
|
@@ -1486,7 +1529,7 @@ var registerRoomEvents = (ctx) => {
|
|
|
1486
1529
|
const oldest = kept[0];
|
|
1487
1530
|
if (oldest === void 0) break;
|
|
1488
1531
|
kept = kept.slice(1);
|
|
1489
|
-
await sock.leave(formatRoomName(oldest, { purpose: "
|
|
1532
|
+
await sock.leave(formatRoomName(oldest, { purpose: "broadcast", userId }));
|
|
1490
1533
|
}
|
|
1491
1534
|
}
|
|
1492
1535
|
await sock.join(physicalRoom);
|
|
@@ -1637,7 +1680,7 @@ var rejoinPersistedRooms = (ctx) => {
|
|
|
1637
1680
|
const roomCodes = session ? getSessionRoomCodes(session) : [];
|
|
1638
1681
|
const userId = session?.id ?? null;
|
|
1639
1682
|
for (const roomCode of roomCodes) {
|
|
1640
|
-
await socket.join(formatRoomName(roomCode, { purpose: "
|
|
1683
|
+
await socket.join(formatRoomName(roomCode, { purpose: "broadcast", userId }));
|
|
1641
1684
|
}
|
|
1642
1685
|
return roomCodes;
|
|
1643
1686
|
});
|
|
@@ -1727,7 +1770,7 @@ import {
|
|
|
1727
1770
|
isLocalizedNormalizerRegistered,
|
|
1728
1771
|
isProjectConfigRegistered,
|
|
1729
1772
|
isRuntimeMapsProviderRegistered,
|
|
1730
|
-
resolveEnvKey as
|
|
1773
|
+
resolveEnvKey as resolveEnvKey3
|
|
1731
1774
|
} from "@luckystack/core";
|
|
1732
1775
|
var verifyBootstrap = async (requirements = {}) => {
|
|
1733
1776
|
const missing = [];
|
|
@@ -1766,7 +1809,7 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1766
1809
|
}
|
|
1767
1810
|
}
|
|
1768
1811
|
if (!isRuntimeMapsProviderRegistered()) {
|
|
1769
|
-
if (
|
|
1812
|
+
if (resolveEnvKey3() === "production") {
|
|
1770
1813
|
missing.push(
|
|
1771
1814
|
"RuntimeMapsProvider \u2014 call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound."
|
|
1772
1815
|
);
|
|
@@ -1777,7 +1820,7 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1777
1820
|
}
|
|
1778
1821
|
}
|
|
1779
1822
|
if (!isLocalizedNormalizerRegistered()) {
|
|
1780
|
-
if (
|
|
1823
|
+
if (resolveEnvKey3() === "production") {
|
|
1781
1824
|
missing.push(
|
|
1782
1825
|
"LocalizedNormalizer \u2014 call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n)."
|
|
1783
1826
|
);
|
|
@@ -1796,6 +1839,14 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1796
1839
|
);
|
|
1797
1840
|
}
|
|
1798
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
|
+
}
|
|
1799
1850
|
if (missing.length === 0) return;
|
|
1800
1851
|
const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join("\n");
|
|
1801
1852
|
throw new Error(
|
|
@@ -1812,7 +1863,7 @@ var verifyBootstrap = async (requirements = {}) => {
|
|
|
1812
1863
|
import {
|
|
1813
1864
|
getLogger as getLogger10,
|
|
1814
1865
|
registerRuntimeMapsProvider,
|
|
1815
|
-
resolveEnvKey as
|
|
1866
|
+
resolveEnvKey as resolveEnvKey4
|
|
1816
1867
|
} from "@luckystack/core";
|
|
1817
1868
|
var emptyRuntimeMaps = {
|
|
1818
1869
|
apisObject: {},
|
|
@@ -1862,7 +1913,7 @@ var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
|
|
|
1862
1913
|
target[key] = source[key];
|
|
1863
1914
|
}
|
|
1864
1915
|
};
|
|
1865
|
-
var isProduction = () =>
|
|
1916
|
+
var isProduction = () => resolveEnvKey4() === "production";
|
|
1866
1917
|
var createProdRuntimeMapsProvider = (options) => {
|
|
1867
1918
|
let prodMapsPromise = null;
|
|
1868
1919
|
let devkitModulePromise = null;
|
|
@@ -2104,7 +2155,7 @@ var createLuckyStackServer = async (options = {}) => {
|
|
|
2104
2155
|
});
|
|
2105
2156
|
const port = options.port ?? getParsedPort() ?? options.defaultPort ?? process.env.SERVER_PORT ?? 80;
|
|
2106
2157
|
const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
|
|
2107
|
-
const enableDevTools = options.enableDevTools ??
|
|
2158
|
+
const enableDevTools = options.enableDevTools ?? resolveEnvKey5() !== "production";
|
|
2108
2159
|
registerBindAddress({
|
|
2109
2160
|
ip,
|
|
2110
2161
|
port: typeof port === "string" ? Number.parseInt(port, 10) : port
|