@luckystack/server 0.1.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 +14 -0
- package/CLAUDE.md +83 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +1665 -0
- package/dist/index.js.map +1 -0
- package/dist/parseArgv.d.ts +2 -0
- package/dist/parseArgv.js +38 -0
- package/dist/parseArgv.js.map +1 -0
- package/docs/argv-parsing.md +256 -0
- package/docs/create-server.md +295 -0
- package/docs/http-routes.md +309 -0
- package/docs/request-pipeline.md +198 -0
- package/docs/runtime-maps.md +206 -0
- package/docs/security-defaults.md +236 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1665 @@
|
|
|
1
|
+
// src/createServer.ts
|
|
2
|
+
import http from "http";
|
|
3
|
+
import { registerBindAddress, writeBootUuid, getLogger as getLogger9, getProjectConfig as getProjectConfig10, tryCatch as tryCatch7 } from "@luckystack/core";
|
|
4
|
+
|
|
5
|
+
// src/httpHandler.ts
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
import {
|
|
8
|
+
allowedOrigin,
|
|
9
|
+
dispatchHook as dispatchHook5,
|
|
10
|
+
extractTokenFromRequest as extractTokenFromRequest3,
|
|
11
|
+
getLogger as getLogger6,
|
|
12
|
+
getParams,
|
|
13
|
+
getProjectConfig as getProjectConfig8,
|
|
14
|
+
hasCookie
|
|
15
|
+
} from "@luckystack/core";
|
|
16
|
+
import { getSession as getSession3 } from "@luckystack/login";
|
|
17
|
+
|
|
18
|
+
// src/logSanitize.ts
|
|
19
|
+
import { getProjectConfig, isRedactedLogKey } from "@luckystack/core";
|
|
20
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
21
|
+
var isRedactedKey = (key) => {
|
|
22
|
+
if (isRedactedLogKey(key)) return true;
|
|
23
|
+
return key.toLowerCase() === getProjectConfig().http.sessionCookieName.toLowerCase();
|
|
24
|
+
};
|
|
25
|
+
var sanitizeForLog = (value) => {
|
|
26
|
+
if (value === null || typeof value !== "object") return value;
|
|
27
|
+
if (Array.isArray(value)) return value.map((entry) => sanitizeForLog(entry));
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [key, val] of Object.entries(value)) {
|
|
30
|
+
out[key] = isRedactedKey(key) ? REDACTED_PLACEHOLDER : sanitizeForLog(val);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/securityHeadersRegistry.ts
|
|
36
|
+
var registeredBuilder = null;
|
|
37
|
+
var registerSecurityHeaders = (builder) => {
|
|
38
|
+
registeredBuilder = builder;
|
|
39
|
+
};
|
|
40
|
+
var getSecurityHeadersBuilder = () => registeredBuilder;
|
|
41
|
+
|
|
42
|
+
// src/httpRoutes/csrfMiddleware.ts
|
|
43
|
+
import { dispatchHook, getCsrfConfig, getProjectConfig as getProjectConfig2 } from "@luckystack/core";
|
|
44
|
+
import { getSession } from "@luckystack/login";
|
|
45
|
+
var enforceCsrfOnStateChangingRequest = async ({
|
|
46
|
+
req,
|
|
47
|
+
res,
|
|
48
|
+
routePath,
|
|
49
|
+
token,
|
|
50
|
+
requestId
|
|
51
|
+
}) => {
|
|
52
|
+
const config = getProjectConfig2();
|
|
53
|
+
const isCookieMode = !config.session.basedToken;
|
|
54
|
+
const isStateChanging = req.method !== "GET" && req.method !== "OPTIONS";
|
|
55
|
+
const isCallbackPath = routePath.startsWith("/auth/callback");
|
|
56
|
+
const looksLikeFrameworkRoute = routePath.startsWith("/api/") || routePath.startsWith("/sync/") || routePath.startsWith("/auth/api/");
|
|
57
|
+
if (!(isCookieMode && isStateChanging && looksLikeFrameworkRoute && !isCallbackPath && token)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const csrfSession = await getSession(token);
|
|
61
|
+
if (!csrfSession?.id) return false;
|
|
62
|
+
const csrfConfig = getCsrfConfig();
|
|
63
|
+
const headerKey = csrfConfig.headerName.toLowerCase();
|
|
64
|
+
const headerValue = req.headers[headerKey];
|
|
65
|
+
const provided = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
66
|
+
if (provided && provided === csrfSession.csrfToken) return false;
|
|
67
|
+
void dispatchHook("csrfMismatch", {
|
|
68
|
+
route: routePath,
|
|
69
|
+
method: req.method,
|
|
70
|
+
requestId,
|
|
71
|
+
userId: csrfSession.id,
|
|
72
|
+
providedToken: Boolean(provided)
|
|
73
|
+
});
|
|
74
|
+
res.statusCode = 403;
|
|
75
|
+
res.setHeader("Content-Type", "application/json");
|
|
76
|
+
res.end(JSON.stringify({
|
|
77
|
+
status: "error",
|
|
78
|
+
errorCode: "auth.csrfMismatch",
|
|
79
|
+
message: "CSRF token missing or invalid. Fetch /auth/csrf first."
|
|
80
|
+
}));
|
|
81
|
+
return true;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/httpRoutes/csrfRoute.ts
|
|
85
|
+
import { getSession as getSession2 } from "@luckystack/login";
|
|
86
|
+
var handleCsrfRoute = async ({ res, routePath, token }) => {
|
|
87
|
+
if (routePath !== "/auth/csrf") return false;
|
|
88
|
+
if (!token) {
|
|
89
|
+
res.statusCode = 401;
|
|
90
|
+
res.setHeader("Content-Type", "application/json");
|
|
91
|
+
res.end(JSON.stringify({ status: "error", errorCode: "auth.unauthenticated" }));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const csrfSession = await getSession2(token);
|
|
95
|
+
if (!csrfSession?.id) {
|
|
96
|
+
res.statusCode = 401;
|
|
97
|
+
res.setHeader("Content-Type", "application/json");
|
|
98
|
+
res.end(JSON.stringify({ status: "error", errorCode: "auth.unauthenticated" }));
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
res.statusCode = 200;
|
|
102
|
+
res.setHeader("Content-Type", "application/json");
|
|
103
|
+
res.end(JSON.stringify({
|
|
104
|
+
status: "success",
|
|
105
|
+
csrfToken: csrfSession.csrfToken ?? null
|
|
106
|
+
}));
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// src/httpRoutes/faviconRoute.ts
|
|
111
|
+
var handleFaviconRoute = async ({ res, routePath, options }) => {
|
|
112
|
+
if (routePath !== "/favicon.ico") return false;
|
|
113
|
+
if (options.serveFavicon) {
|
|
114
|
+
await options.serveFavicon(res);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
res.writeHead(404);
|
|
118
|
+
res.end();
|
|
119
|
+
return true;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/httpRoutes/healthRoutes.ts
|
|
123
|
+
import {
|
|
124
|
+
computeSynchronizedEnvHashes,
|
|
125
|
+
getProjectConfig as getProjectConfig3,
|
|
126
|
+
prisma,
|
|
127
|
+
readBootUuid,
|
|
128
|
+
redis,
|
|
129
|
+
resolveEnvKey,
|
|
130
|
+
tryCatch
|
|
131
|
+
} from "@luckystack/core";
|
|
132
|
+
var pingPrisma = async () => {
|
|
133
|
+
const client = prisma;
|
|
134
|
+
const queryRaw = client.$queryRaw;
|
|
135
|
+
if (typeof queryRaw === "function") {
|
|
136
|
+
const [sqlError] = await tryCatch(() => queryRaw`SELECT 1`);
|
|
137
|
+
if (!sqlError) return true;
|
|
138
|
+
}
|
|
139
|
+
const runCommandRaw = client.$runCommandRaw;
|
|
140
|
+
if (typeof runCommandRaw === "function") {
|
|
141
|
+
const [mongoError] = await tryCatch(() => runCommandRaw({ ping: 1 }));
|
|
142
|
+
return !mongoError;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
};
|
|
146
|
+
var handleLivezRoute = ({ res, routePath }) => {
|
|
147
|
+
if (routePath !== getProjectConfig3().http.liveEndpoint) return Promise.resolve(false);
|
|
148
|
+
res.statusCode = 200;
|
|
149
|
+
res.setHeader("Content-Type", "application/json");
|
|
150
|
+
res.end(JSON.stringify({ status: "live" }));
|
|
151
|
+
return Promise.resolve(true);
|
|
152
|
+
};
|
|
153
|
+
var handleReadyzRoute = async ({ res, routePath }) => {
|
|
154
|
+
if (routePath !== getProjectConfig3().http.readyEndpoint) return false;
|
|
155
|
+
const bootUuid = await readBootUuid();
|
|
156
|
+
const [redisError, pong] = await tryCatch(() => redis.ping());
|
|
157
|
+
const redisOk = !redisError && (pong === "PONG" || Boolean(pong));
|
|
158
|
+
const prismaOk = await pingPrisma();
|
|
159
|
+
const ready = Boolean(bootUuid) && redisOk && prismaOk;
|
|
160
|
+
res.statusCode = ready ? 200 : 503;
|
|
161
|
+
res.setHeader("Content-Type", "application/json");
|
|
162
|
+
res.end(JSON.stringify({
|
|
163
|
+
status: ready ? "ready" : "not-ready",
|
|
164
|
+
checks: { bootUuid: Boolean(bootUuid), redis: redisOk, prisma: prismaOk }
|
|
165
|
+
}));
|
|
166
|
+
return true;
|
|
167
|
+
};
|
|
168
|
+
var handleHealthRoute = async ({ res, routePath }) => {
|
|
169
|
+
if (routePath !== getProjectConfig3().http.healthEndpoint) return false;
|
|
170
|
+
const bootUuid = await readBootUuid();
|
|
171
|
+
const synchronizedHashes = computeSynchronizedEnvHashes();
|
|
172
|
+
res.statusCode = bootUuid ? 200 : 503;
|
|
173
|
+
res.setHeader("Content-Type", "application/json");
|
|
174
|
+
res.end(JSON.stringify({
|
|
175
|
+
status: bootUuid ? "ok" : "degraded",
|
|
176
|
+
bootUuid,
|
|
177
|
+
envKey: resolveEnvKey(),
|
|
178
|
+
synchronizedHashes
|
|
179
|
+
}));
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/httpRoutes/testResetRoute.ts
|
|
184
|
+
import {
|
|
185
|
+
clearAllHooks,
|
|
186
|
+
clearAllRateLimits,
|
|
187
|
+
getProjectConfig as getProjectConfig4,
|
|
188
|
+
formatKey,
|
|
189
|
+
redis as redis2,
|
|
190
|
+
tryCatch as tryCatch2
|
|
191
|
+
} from "@luckystack/core";
|
|
192
|
+
var handleTestResetRoute = async ({ req, res, routePath }) => {
|
|
193
|
+
if (routePath !== getProjectConfig4().http.testResetEndpoint) return false;
|
|
194
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
195
|
+
if (nodeEnv !== "development" && nodeEnv !== "test") {
|
|
196
|
+
res.statusCode = 404;
|
|
197
|
+
res.setHeader("Content-Type", "application/json");
|
|
198
|
+
res.end(JSON.stringify({ status: "error", errorCode: "notFound" }));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const requiredToken = process.env.TEST_RESET_TOKEN;
|
|
202
|
+
if (!requiredToken || req.headers["x-test-reset-token"] !== requiredToken) {
|
|
203
|
+
res.statusCode = 403;
|
|
204
|
+
res.setHeader("Content-Type", "application/json");
|
|
205
|
+
res.end(JSON.stringify({ status: "error", errorCode: "auth.forbidden" }));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const cleared = [];
|
|
209
|
+
await clearAllRateLimits();
|
|
210
|
+
cleared.push("rateLimits");
|
|
211
|
+
const sessionPattern = `${formatKey("-session", "")}:*`;
|
|
212
|
+
const activeUsersPattern = `${formatKey("-activeUsers", "")}:*`;
|
|
213
|
+
const scanAndDelete = async (pattern, label) => {
|
|
214
|
+
const [error, deleted] = await tryCatch2(async () => {
|
|
215
|
+
let cursor = "0";
|
|
216
|
+
let total = 0;
|
|
217
|
+
do {
|
|
218
|
+
const [next, keys] = await redis2.scan(cursor, "MATCH", pattern, "COUNT", 200);
|
|
219
|
+
cursor = next;
|
|
220
|
+
if (Array.isArray(keys) && keys.length > 0) {
|
|
221
|
+
await redis2.del(...keys);
|
|
222
|
+
total += keys.length;
|
|
223
|
+
}
|
|
224
|
+
} while (cursor !== "0");
|
|
225
|
+
return total;
|
|
226
|
+
});
|
|
227
|
+
if (error || !deleted) return 0;
|
|
228
|
+
if (deleted > 0) cleared.push(label);
|
|
229
|
+
return deleted;
|
|
230
|
+
};
|
|
231
|
+
await scanAndDelete(sessionPattern, "sessions");
|
|
232
|
+
await scanAndDelete(activeUsersPattern, "activeUsers");
|
|
233
|
+
const rawUrl = req.url ?? "/";
|
|
234
|
+
const base = `http://${req.headers.host ?? "localhost"}`;
|
|
235
|
+
const includeFlag = URL.canParse(rawUrl, base) ? new URL(rawUrl, base).searchParams.get("include") ?? "" : "";
|
|
236
|
+
if (includeFlag.split(",").map((s) => s.trim()).includes("hooks")) {
|
|
237
|
+
clearAllHooks();
|
|
238
|
+
cleared.push("hooks");
|
|
239
|
+
}
|
|
240
|
+
res.statusCode = 200;
|
|
241
|
+
res.setHeader("Content-Type", "application/json");
|
|
242
|
+
res.end(JSON.stringify({ status: "success", cleared }));
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/httpRoutes/uploadsRoute.ts
|
|
247
|
+
import { serveAvatar } from "@luckystack/core";
|
|
248
|
+
var handleUploadsRoute = async ({ res, routePath }) => {
|
|
249
|
+
if (!routePath.startsWith("/uploads/")) return false;
|
|
250
|
+
await serveAvatar({ routePath, res });
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/httpRoutes/authApiRoute.ts
|
|
255
|
+
import {
|
|
256
|
+
checkRateLimit,
|
|
257
|
+
dispatchHook as dispatchHook2,
|
|
258
|
+
getLogger,
|
|
259
|
+
getProjectConfig as getProjectConfig5
|
|
260
|
+
} from "@luckystack/core";
|
|
261
|
+
import {
|
|
262
|
+
createOAuthState,
|
|
263
|
+
deleteSession,
|
|
264
|
+
getOAuthProviders,
|
|
265
|
+
isFullOAuthProvider,
|
|
266
|
+
loginWithCredentials
|
|
267
|
+
} from "@luckystack/login";
|
|
268
|
+
var parseSessionBasedTokenHeader = (headerValue) => {
|
|
269
|
+
if (headerValue === void 0) return null;
|
|
270
|
+
const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
271
|
+
if (value === "1" || value === "true") return true;
|
|
272
|
+
if (value === "0" || value === "false") return false;
|
|
273
|
+
return null;
|
|
274
|
+
};
|
|
275
|
+
var handleAuthApiRoute = async ({
|
|
276
|
+
req,
|
|
277
|
+
res,
|
|
278
|
+
routePath,
|
|
279
|
+
token,
|
|
280
|
+
params,
|
|
281
|
+
sessionCookieOptions
|
|
282
|
+
}) => {
|
|
283
|
+
if (!routePath.startsWith("/auth/api")) return false;
|
|
284
|
+
const config = getProjectConfig5();
|
|
285
|
+
const sessionCookieName = config.http.sessionCookieName;
|
|
286
|
+
const shouldLogDev = config.logging.devLogs;
|
|
287
|
+
const providerName = routePath.split("/")[3];
|
|
288
|
+
const provider = getOAuthProviders().find((p) => p.name === providerName);
|
|
289
|
+
if (!provider?.name) {
|
|
290
|
+
res.setHeader("Content-Type", "application/json");
|
|
291
|
+
res.end(JSON.stringify({ status: false, reason: "login.providerNotFound" }));
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (isFullOAuthProvider(provider)) {
|
|
295
|
+
const oauthState = await createOAuthState(provider.name);
|
|
296
|
+
if (!oauthState) {
|
|
297
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
298
|
+
res.end(JSON.stringify({ status: false, reason: "login.oauthStateInitFailed" }));
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
const clientId = encodeURIComponent(provider.clientID);
|
|
302
|
+
const callbackUrl = encodeURIComponent(provider.callbackURL);
|
|
303
|
+
const scope = encodeURIComponent(provider.scope.join(" "));
|
|
304
|
+
const state = encodeURIComponent(oauthState);
|
|
305
|
+
res.writeHead(302, {
|
|
306
|
+
Location: `${provider.authorizationURL}?client_id=${clientId}&redirect_uri=${callbackUrl}&scope=${scope}&response_type=code&prompt=select_account&state=${state}`
|
|
307
|
+
});
|
|
308
|
+
res.end();
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
const rateLimiting = config.rateLimiting;
|
|
312
|
+
if (rateLimiting.defaultApiLimit !== false && rateLimiting.defaultApiLimit > 0) {
|
|
313
|
+
const requesterIp = req.socket.remoteAddress ?? "unknown";
|
|
314
|
+
const { allowed, resetIn } = await checkRateLimit({
|
|
315
|
+
key: `ip:${requesterIp}:auth:credentials`,
|
|
316
|
+
limit: rateLimiting.defaultApiLimit,
|
|
317
|
+
windowMs: rateLimiting.windowMs
|
|
318
|
+
});
|
|
319
|
+
if (!allowed) {
|
|
320
|
+
void dispatchHook2("rateLimitExceeded", {
|
|
321
|
+
scope: "auth",
|
|
322
|
+
key: `ip:${requesterIp}:auth:credentials`,
|
|
323
|
+
limit: rateLimiting.defaultApiLimit,
|
|
324
|
+
windowMs: rateLimiting.windowMs,
|
|
325
|
+
count: rateLimiting.defaultApiLimit + 1,
|
|
326
|
+
route: routePath,
|
|
327
|
+
ip: requesterIp
|
|
328
|
+
});
|
|
329
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
330
|
+
res.end(JSON.stringify({
|
|
331
|
+
status: false,
|
|
332
|
+
reason: "api.rateLimitExceeded",
|
|
333
|
+
errorParams: [{ key: "seconds", value: resetIn }]
|
|
334
|
+
}));
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const result = await loginWithCredentials(params);
|
|
339
|
+
if (!result?.status) {
|
|
340
|
+
const reasonKey = typeof result?.reason === "string" && result.reason.length > 0 ? result.reason : "api.internalServerError";
|
|
341
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
342
|
+
res.end(JSON.stringify({ status: false, reason: reasonKey }));
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (result.newToken) {
|
|
346
|
+
if (token) await deleteSession(token);
|
|
347
|
+
const requestedSessionMode = parseSessionBasedTokenHeader(req.headers["x-session-based-token"]);
|
|
348
|
+
const useSessionBasedToken = requestedSessionMode ?? config.session.basedToken;
|
|
349
|
+
if (shouldLogDev) getLogger().debug("http: setting cookie with new token");
|
|
350
|
+
if (useSessionBasedToken) {
|
|
351
|
+
res.setHeader("X-Session-Token", result.newToken);
|
|
352
|
+
} else {
|
|
353
|
+
res.setHeader("Set-Cookie", `${sessionCookieName}=${result.newToken}; ${sessionCookieOptions}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
res.end(JSON.stringify({
|
|
357
|
+
status: result.status,
|
|
358
|
+
reason: result.reason,
|
|
359
|
+
session: result.session,
|
|
360
|
+
authenticated: Boolean(result.newToken)
|
|
361
|
+
}));
|
|
362
|
+
return true;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// src/httpRoutes/authCallbackRoute.ts
|
|
366
|
+
import { getLogger as getLogger2, getProjectConfig as getProjectConfig6 } from "@luckystack/core";
|
|
367
|
+
import { deleteSession as deleteSession2, loginCallback } from "@luckystack/login";
|
|
368
|
+
var handleAuthCallbackRoute = async ({
|
|
369
|
+
req,
|
|
370
|
+
res,
|
|
371
|
+
routePath,
|
|
372
|
+
token,
|
|
373
|
+
sessionCookieOptions
|
|
374
|
+
}) => {
|
|
375
|
+
if (!routePath.startsWith("/auth/callback")) return false;
|
|
376
|
+
const config = getProjectConfig6();
|
|
377
|
+
const sessionCookieName = config.http.sessionCookieName;
|
|
378
|
+
const shouldLogDev = config.logging.devLogs;
|
|
379
|
+
const baseLocation = process.env.DNS || config.app.publicUrl || "/";
|
|
380
|
+
const callbackResult = await loginCallback(routePath, req, res, {
|
|
381
|
+
defaultRedirectUrl: baseLocation
|
|
382
|
+
});
|
|
383
|
+
if (!callbackResult) {
|
|
384
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
385
|
+
res.end("Login failed");
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
if (token) await deleteSession2(token);
|
|
389
|
+
if (shouldLogDev) getLogger2().debug("http: setting cookie or redirect with new token");
|
|
390
|
+
const { token: newToken, redirectUrl } = callbackResult;
|
|
391
|
+
if (config.session.basedToken) {
|
|
392
|
+
const separator = redirectUrl.includes("?") ? "&" : "?";
|
|
393
|
+
res.writeHead(302, { Location: `${redirectUrl}${separator}token=${newToken}` });
|
|
394
|
+
} else {
|
|
395
|
+
res.setHeader("Set-Cookie", `${sessionCookieName}=${newToken}; ${sessionCookieOptions}`);
|
|
396
|
+
res.writeHead(302, { Location: redirectUrl });
|
|
397
|
+
}
|
|
398
|
+
res.end();
|
|
399
|
+
return true;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/httpRoutes/apiRoute.ts
|
|
403
|
+
import {
|
|
404
|
+
captureException,
|
|
405
|
+
dispatchHook as dispatchHook3,
|
|
406
|
+
extractTokenFromRequest,
|
|
407
|
+
getLogger as getLogger3,
|
|
408
|
+
tryCatch as tryCatch3
|
|
409
|
+
} from "@luckystack/core";
|
|
410
|
+
import { handleHttpApiRequest } from "@luckystack/api";
|
|
411
|
+
|
|
412
|
+
// src/sse.ts
|
|
413
|
+
import { getProjectConfig as getProjectConfig7 } from "@luckystack/core";
|
|
414
|
+
var isExpectingEventStream = (acceptHeader) => {
|
|
415
|
+
if (!acceptHeader) return false;
|
|
416
|
+
const value = Array.isArray(acceptHeader) ? acceptHeader.join(",") : acceptHeader;
|
|
417
|
+
return value.toLowerCase().includes("text/event-stream");
|
|
418
|
+
};
|
|
419
|
+
var queryRequestsStream = (queryString) => {
|
|
420
|
+
if (!queryString) return false;
|
|
421
|
+
const { queryParam, enabledValue } = getProjectConfig7().http.stream;
|
|
422
|
+
const params = new URLSearchParams(queryString);
|
|
423
|
+
const value = params.get(queryParam);
|
|
424
|
+
return value === enabledValue || value === "1";
|
|
425
|
+
};
|
|
426
|
+
var shouldUseHttpStream = ({
|
|
427
|
+
acceptHeader,
|
|
428
|
+
queryString
|
|
429
|
+
}) => isExpectingEventStream(acceptHeader) || queryRequestsStream(queryString);
|
|
430
|
+
var initSseResponse = (res) => {
|
|
431
|
+
res.writeHead(200, {
|
|
432
|
+
"Content-Type": "text/event-stream",
|
|
433
|
+
"Cache-Control": "no-cache, no-transform",
|
|
434
|
+
Connection: "keep-alive",
|
|
435
|
+
"X-Accel-Buffering": "no"
|
|
436
|
+
});
|
|
437
|
+
const connectedComment = getProjectConfig7().http.stream.connectedComment;
|
|
438
|
+
if (connectedComment) {
|
|
439
|
+
res.write(`${connectedComment}
|
|
440
|
+
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var sendSseEvent = ({
|
|
445
|
+
res,
|
|
446
|
+
event,
|
|
447
|
+
data
|
|
448
|
+
}) => {
|
|
449
|
+
if (res.writableEnded) return;
|
|
450
|
+
res.write(`event: ${event}
|
|
451
|
+
`);
|
|
452
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
453
|
+
|
|
454
|
+
`);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/httpRoutes/apiRoute.ts
|
|
458
|
+
var handleApiRoute = async ({
|
|
459
|
+
req,
|
|
460
|
+
res,
|
|
461
|
+
routePath,
|
|
462
|
+
queryString,
|
|
463
|
+
method,
|
|
464
|
+
params,
|
|
465
|
+
requestId
|
|
466
|
+
}) => {
|
|
467
|
+
if (!routePath.startsWith("/api/")) return false;
|
|
468
|
+
const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });
|
|
469
|
+
let streamClosed = false;
|
|
470
|
+
if (useHttpStream) {
|
|
471
|
+
initSseResponse(res);
|
|
472
|
+
req.on("close", () => {
|
|
473
|
+
streamClosed = true;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const [error, handled] = await tryCatch3(async () => {
|
|
477
|
+
const httpToken = extractTokenFromRequest(req);
|
|
478
|
+
const apiName = routePath.slice(5);
|
|
479
|
+
if (!apiName) {
|
|
480
|
+
const response = {
|
|
481
|
+
status: "error",
|
|
482
|
+
httpStatus: 400,
|
|
483
|
+
message: "api.invalidName",
|
|
484
|
+
errorCode: "api.invalidName"
|
|
485
|
+
};
|
|
486
|
+
if (useHttpStream) {
|
|
487
|
+
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
488
|
+
res.end();
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
res.setHeader("Content-Type", "application/json");
|
|
492
|
+
res.writeHead(400);
|
|
493
|
+
res.end(JSON.stringify(response));
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
const apiData = typeof params === "object" ? { ...params } : {};
|
|
497
|
+
delete apiData.stream;
|
|
498
|
+
const result = await handleHttpApiRequest({
|
|
499
|
+
name: apiName,
|
|
500
|
+
data: apiData,
|
|
501
|
+
token: httpToken,
|
|
502
|
+
requesterIp: req.socket.remoteAddress ?? void 0,
|
|
503
|
+
xLanguageHeader: req.headers["x-language"],
|
|
504
|
+
acceptLanguageHeader: req.headers["accept-language"],
|
|
505
|
+
method,
|
|
506
|
+
stream: useHttpStream ? (payload) => {
|
|
507
|
+
if (streamClosed || res.writableEnded) return;
|
|
508
|
+
sendSseEvent({ res, event: "stream", data: payload });
|
|
509
|
+
} : void 0
|
|
510
|
+
});
|
|
511
|
+
if (useHttpStream) {
|
|
512
|
+
if (!streamClosed) sendSseEvent({ res, event: "final", data: result });
|
|
513
|
+
res.end();
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
res.setHeader("Content-Type", "application/json");
|
|
517
|
+
res.writeHead(result.httpStatus);
|
|
518
|
+
res.end(JSON.stringify(result));
|
|
519
|
+
return true;
|
|
520
|
+
}, void 0, { routePath, method, requestId, source: "httpHandler.api" });
|
|
521
|
+
if (!error) {
|
|
522
|
+
return handled ?? true;
|
|
523
|
+
}
|
|
524
|
+
getLogger3().error("http-api: top-level handler threw", error, { routePath, method, requestId });
|
|
525
|
+
captureException(error, { routePath, method, requestId, source: "httpHandler.api" });
|
|
526
|
+
void dispatchHook3("apiError", {
|
|
527
|
+
route: routePath,
|
|
528
|
+
method,
|
|
529
|
+
requestId,
|
|
530
|
+
error
|
|
531
|
+
});
|
|
532
|
+
const errResponse = {
|
|
533
|
+
status: "error",
|
|
534
|
+
httpStatus: 500,
|
|
535
|
+
message: "api.invalidRequestFormat",
|
|
536
|
+
errorCode: "api.invalidRequestFormat"
|
|
537
|
+
};
|
|
538
|
+
if (useHttpStream) {
|
|
539
|
+
if (!res.writableEnded) sendSseEvent({ res, event: "error", data: errResponse });
|
|
540
|
+
res.end();
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
res.setHeader("Content-Type", "application/json");
|
|
544
|
+
res.writeHead(500);
|
|
545
|
+
res.end(JSON.stringify(errResponse));
|
|
546
|
+
return true;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/httpRoutes/syncRoute.ts
|
|
550
|
+
import {
|
|
551
|
+
captureException as captureException2,
|
|
552
|
+
dispatchHook as dispatchHook4,
|
|
553
|
+
extractTokenFromRequest as extractTokenFromRequest2,
|
|
554
|
+
getLogger as getLogger4,
|
|
555
|
+
tryCatch as tryCatch4
|
|
556
|
+
} from "@luckystack/core";
|
|
557
|
+
import { handleHttpSyncRequest } from "@luckystack/sync";
|
|
558
|
+
var normalizeHttpSyncParams = (params) => {
|
|
559
|
+
const source = params ?? {};
|
|
560
|
+
const data = source.data && typeof source.data === "object" ? source.data : {};
|
|
561
|
+
return {
|
|
562
|
+
data,
|
|
563
|
+
receiver: typeof source.receiver === "string" ? source.receiver : "",
|
|
564
|
+
ignoreSelf: typeof source.ignoreSelf === "boolean" ? source.ignoreSelf : void 0,
|
|
565
|
+
cb: typeof source.cb === "string" ? source.cb : void 0
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
var handleSyncRoute = async ({
|
|
569
|
+
req,
|
|
570
|
+
res,
|
|
571
|
+
routePath,
|
|
572
|
+
queryString,
|
|
573
|
+
method,
|
|
574
|
+
params,
|
|
575
|
+
requestId
|
|
576
|
+
}) => {
|
|
577
|
+
if (!routePath.startsWith("/sync/")) return false;
|
|
578
|
+
const useHttpStream = shouldUseHttpStream({ acceptHeader: req.headers.accept, queryString });
|
|
579
|
+
let streamClosed = false;
|
|
580
|
+
if (useHttpStream) {
|
|
581
|
+
initSseResponse(res);
|
|
582
|
+
req.on("close", () => {
|
|
583
|
+
streamClosed = true;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const [error, handled] = await tryCatch4(async () => {
|
|
587
|
+
if (method !== "POST") {
|
|
588
|
+
const response = {
|
|
589
|
+
status: "error",
|
|
590
|
+
message: "sync.methodNotAllowed",
|
|
591
|
+
errorCode: "sync.methodNotAllowed"
|
|
592
|
+
};
|
|
593
|
+
if (useHttpStream) {
|
|
594
|
+
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
595
|
+
res.end();
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
res.setHeader("Content-Type", "application/json");
|
|
599
|
+
res.writeHead(405);
|
|
600
|
+
res.end(JSON.stringify(response));
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
const httpToken = extractTokenFromRequest2(req);
|
|
604
|
+
const syncName = routePath.slice(6);
|
|
605
|
+
if (!syncName) {
|
|
606
|
+
const response = {
|
|
607
|
+
status: "error",
|
|
608
|
+
message: "sync.invalidName",
|
|
609
|
+
errorCode: "sync.invalidName"
|
|
610
|
+
};
|
|
611
|
+
if (useHttpStream) {
|
|
612
|
+
if (!streamClosed) sendSseEvent({ res, event: "final", data: response });
|
|
613
|
+
res.end();
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
res.setHeader("Content-Type", "application/json");
|
|
617
|
+
res.writeHead(400);
|
|
618
|
+
res.end(JSON.stringify(response));
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
const syncParams = normalizeHttpSyncParams(params);
|
|
622
|
+
const result = await handleHttpSyncRequest({
|
|
623
|
+
name: `sync/${syncName}`,
|
|
624
|
+
cb: syncParams.cb,
|
|
625
|
+
data: syncParams.data,
|
|
626
|
+
receiver: syncParams.receiver,
|
|
627
|
+
ignoreSelf: syncParams.ignoreSelf,
|
|
628
|
+
token: httpToken,
|
|
629
|
+
requesterIp: req.socket.remoteAddress ?? void 0,
|
|
630
|
+
xLanguageHeader: req.headers["x-language"],
|
|
631
|
+
acceptLanguageHeader: req.headers["accept-language"],
|
|
632
|
+
stream: useHttpStream ? (payload) => {
|
|
633
|
+
if (streamClosed || res.writableEnded) return;
|
|
634
|
+
sendSseEvent({ res, event: "stream", data: payload });
|
|
635
|
+
} : void 0
|
|
636
|
+
});
|
|
637
|
+
if (useHttpStream) {
|
|
638
|
+
if (!streamClosed) sendSseEvent({ res, event: "final", data: result });
|
|
639
|
+
res.end();
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
res.setHeader("Content-Type", "application/json");
|
|
643
|
+
res.writeHead(result.httpStatus ?? (result.status === "success" ? 200 : 400));
|
|
644
|
+
res.end(JSON.stringify(result));
|
|
645
|
+
return true;
|
|
646
|
+
}, void 0, { routePath, method, requestId, source: "httpHandler.sync" });
|
|
647
|
+
if (!error) {
|
|
648
|
+
return handled ?? true;
|
|
649
|
+
}
|
|
650
|
+
getLogger4().error("http-sync: top-level handler threw", error, { routePath, method, requestId });
|
|
651
|
+
captureException2(error, { routePath, method, requestId, source: "httpHandler.sync" });
|
|
652
|
+
void dispatchHook4("syncError", {
|
|
653
|
+
route: routePath,
|
|
654
|
+
method,
|
|
655
|
+
requestId,
|
|
656
|
+
error
|
|
657
|
+
});
|
|
658
|
+
const errResponse = {
|
|
659
|
+
status: "error",
|
|
660
|
+
message: "sync.invalidRequestFormat",
|
|
661
|
+
errorCode: "sync.invalidRequestFormat"
|
|
662
|
+
};
|
|
663
|
+
if (useHttpStream) {
|
|
664
|
+
if (!res.writableEnded) sendSseEvent({ res, event: "error", data: errResponse });
|
|
665
|
+
res.end();
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
res.setHeader("Content-Type", "application/json");
|
|
669
|
+
res.writeHead(500);
|
|
670
|
+
res.end(JSON.stringify(errResponse));
|
|
671
|
+
return true;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// src/httpRoutes/customRoutes.ts
|
|
675
|
+
import { captureException as captureException3, getLogger as getLogger5, tryCatch as tryCatch5 } from "@luckystack/core";
|
|
676
|
+
|
|
677
|
+
// src/customRoutesRegistry.ts
|
|
678
|
+
var handlers = [];
|
|
679
|
+
var preParamsHandlers = [];
|
|
680
|
+
var registerCustomRoute = (handler, options = {}) => {
|
|
681
|
+
if (options.phase === "pre-params") {
|
|
682
|
+
preParamsHandlers.push(handler);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
handlers.push(handler);
|
|
686
|
+
};
|
|
687
|
+
var getCustomRoutes = () => handlers;
|
|
688
|
+
var getPreParamsCustomRoutes = () => preParamsHandlers;
|
|
689
|
+
var clearCustomRoutes = () => {
|
|
690
|
+
handlers.length = 0;
|
|
691
|
+
preParamsHandlers.length = 0;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/httpRoutes/customRoutes.ts
|
|
695
|
+
var runHandler = async (handler, req, res, ctx, source) => {
|
|
696
|
+
const [error, handled] = await tryCatch5(() => handler(req, res, ctx));
|
|
697
|
+
if (error) {
|
|
698
|
+
getLogger5().error(`${source} threw`, error, { routePath: ctx.routePath, method: ctx.method });
|
|
699
|
+
captureException3(error, { routePath: ctx.routePath, method: ctx.method, source });
|
|
700
|
+
if (!res.writableEnded) {
|
|
701
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
702
|
+
res.end(JSON.stringify({ status: "error", errorCode: "server.customRouteFailed" }));
|
|
703
|
+
}
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
return Boolean(handled) || res.writableEnded;
|
|
707
|
+
};
|
|
708
|
+
var handlePreParamsCustomRoutes = async ({
|
|
709
|
+
req,
|
|
710
|
+
res,
|
|
711
|
+
routePath,
|
|
712
|
+
queryString,
|
|
713
|
+
method,
|
|
714
|
+
token
|
|
715
|
+
}) => {
|
|
716
|
+
const ctx = { routePath, method, queryString, token };
|
|
717
|
+
for (const handler of getPreParamsCustomRoutes()) {
|
|
718
|
+
const handled = await runHandler(handler, req, res, ctx, "preParamsCustomRoute");
|
|
719
|
+
if (handled) return true;
|
|
720
|
+
}
|
|
721
|
+
return false;
|
|
722
|
+
};
|
|
723
|
+
var handleCustomRoutes = async ({
|
|
724
|
+
req,
|
|
725
|
+
res,
|
|
726
|
+
options,
|
|
727
|
+
routePath,
|
|
728
|
+
queryString,
|
|
729
|
+
method,
|
|
730
|
+
token
|
|
731
|
+
}) => {
|
|
732
|
+
const ctx = { routePath, method, queryString, token };
|
|
733
|
+
for (const handler of getCustomRoutes()) {
|
|
734
|
+
const handled = await runHandler(handler, req, res, ctx, "customRoutesRegistry");
|
|
735
|
+
if (handled) return true;
|
|
736
|
+
}
|
|
737
|
+
if (options.customRoutes) {
|
|
738
|
+
const handled = await runHandler(options.customRoutes, req, res, ctx, "createServer.customRoutes");
|
|
739
|
+
if (handled) return true;
|
|
740
|
+
}
|
|
741
|
+
return false;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/httpRoutes/staticRoutes.ts
|
|
745
|
+
import path from "path";
|
|
746
|
+
var KNOWN_STATIC_FILE_REGEX = /^\/(assets\/[a-zA-Z0-9_/-]+|[a-zA-Z0-9_-]+)\.(png|jpg|jpeg|gif|svg|html|css|js)$/;
|
|
747
|
+
var serveWithRewrittenUrl = async (serveFile, req, res, rewrittenUrl) => {
|
|
748
|
+
const originalUrl = req.url;
|
|
749
|
+
req.url = rewrittenUrl;
|
|
750
|
+
try {
|
|
751
|
+
await serveFile(req, res);
|
|
752
|
+
} finally {
|
|
753
|
+
req.url = originalUrl;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
var handleStaticAndSpaFallback = async ({
|
|
757
|
+
req,
|
|
758
|
+
res,
|
|
759
|
+
routePath,
|
|
760
|
+
options
|
|
761
|
+
}) => {
|
|
762
|
+
if (routePath.includes("/assets/")) {
|
|
763
|
+
if (!options.serveFile) {
|
|
764
|
+
res.writeHead(404);
|
|
765
|
+
res.end("Not Found");
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
const assetPath = routePath.slice(routePath.indexOf("/assets/"));
|
|
769
|
+
await serveWithRewrittenUrl(options.serveFile, req, res, assetPath);
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
if (KNOWN_STATIC_FILE_REGEX.test(routePath)) {
|
|
773
|
+
if (!options.serveFile) {
|
|
774
|
+
res.writeHead(404);
|
|
775
|
+
res.end("Not Found");
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
await options.serveFile(req, res);
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
if (path.extname(routePath)) {
|
|
782
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
783
|
+
res.end("Not Found");
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
if (!options.serveFile) {
|
|
787
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
788
|
+
res.end("Not Found");
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
await serveWithRewrittenUrl(options.serveFile, req, res, "/");
|
|
792
|
+
return true;
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// src/originExemptRegistry.ts
|
|
796
|
+
var exemptPaths = [];
|
|
797
|
+
var registerOriginExemptPath = (matcher) => {
|
|
798
|
+
exemptPaths.push(matcher);
|
|
799
|
+
};
|
|
800
|
+
var getOriginExemptPaths = () => exemptPaths;
|
|
801
|
+
var clearOriginExemptPaths = () => {
|
|
802
|
+
exemptPaths.length = 0;
|
|
803
|
+
};
|
|
804
|
+
var isOriginExemptPath = (routePath) => exemptPaths.some((matcher) => routePath.startsWith(matcher.pathPrefix));
|
|
805
|
+
|
|
806
|
+
// src/httpHandler.ts
|
|
807
|
+
var buildSessionCookieOptions = (sessionExpiryDays, secure, http2) => `HttpOnly; SameSite=${http2.sessionCookieSameSite}; Path=${http2.sessionCookiePath}; Max-Age=${60 * 60 * 24 * sessionExpiryDays}; ${secure ? "Secure;" : ""}`;
|
|
808
|
+
var setSecurityHeaders = (req, res, origin) => {
|
|
809
|
+
const { cors, securityHeaders } = getProjectConfig8().http;
|
|
810
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
811
|
+
res.setHeader("Access-Control-Allow-Methods", cors.allowedMethods);
|
|
812
|
+
res.setHeader("Access-Control-Allow-Headers", cors.allowedHeaders);
|
|
813
|
+
res.setHeader("Access-Control-Expose-Headers", cors.exposedHeaders);
|
|
814
|
+
if (cors.credentials) {
|
|
815
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
816
|
+
}
|
|
817
|
+
res.setHeader("Referrer-Policy", securityHeaders.referrerPolicy);
|
|
818
|
+
res.setHeader("X-Frame-Options", securityHeaders.frameOptions);
|
|
819
|
+
res.setHeader("X-XSS-Protection", securityHeaders.xssProtection);
|
|
820
|
+
res.setHeader("X-Content-Type-Options", securityHeaders.contentTypeOptions);
|
|
821
|
+
const builder = getSecurityHeadersBuilder();
|
|
822
|
+
if (builder) {
|
|
823
|
+
try {
|
|
824
|
+
const custom = builder(req);
|
|
825
|
+
if (custom) {
|
|
826
|
+
for (const [name, value] of Object.entries(custom)) {
|
|
827
|
+
res.setHeader(name, value);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
getLogger6().warn("securityHeadersBuilder threw \u2014 falling back to defaults", { err: error });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
var PRE_PARAMS_ROUTES = [
|
|
836
|
+
handleCsrfRoute,
|
|
837
|
+
handleFaviconRoute,
|
|
838
|
+
handleLivezRoute,
|
|
839
|
+
handleReadyzRoute,
|
|
840
|
+
handleHealthRoute,
|
|
841
|
+
handleTestResetRoute,
|
|
842
|
+
handlePreParamsCustomRoutes
|
|
843
|
+
];
|
|
844
|
+
var POST_PARAMS_ROUTES = [
|
|
845
|
+
handleUploadsRoute,
|
|
846
|
+
handleAuthApiRoute,
|
|
847
|
+
handleAuthCallbackRoute,
|
|
848
|
+
handleApiRoute,
|
|
849
|
+
handleSyncRoute,
|
|
850
|
+
handleCustomRoutes,
|
|
851
|
+
handleStaticAndSpaFallback
|
|
852
|
+
];
|
|
853
|
+
var dispatchRoutes = async (handlers2, ctx) => {
|
|
854
|
+
for (const handler of handlers2) {
|
|
855
|
+
const handled = await handler(ctx);
|
|
856
|
+
if (handled || ctx.res.writableEnded) return true;
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
};
|
|
860
|
+
var enforceOriginPolicy = (req, res, routePath) => {
|
|
861
|
+
const origin = req.headers.origin ?? req.headers.referer ?? "";
|
|
862
|
+
const isStateChangingMethod = req.method !== "GET" && req.method !== "HEAD" && req.method !== "OPTIONS";
|
|
863
|
+
if (isOriginExemptPath(routePath)) {
|
|
864
|
+
return { origin, rejected: false };
|
|
865
|
+
}
|
|
866
|
+
if (!origin) {
|
|
867
|
+
if (isStateChangingMethod) {
|
|
868
|
+
res.statusCode = 403;
|
|
869
|
+
res.setHeader("Content-Type", "text/plain");
|
|
870
|
+
res.end("Forbidden");
|
|
871
|
+
return { origin, rejected: true };
|
|
872
|
+
}
|
|
873
|
+
} else if (!allowedOrigin(origin)) {
|
|
874
|
+
res.statusCode = 403;
|
|
875
|
+
res.setHeader("Content-Type", "text/plain");
|
|
876
|
+
res.end("Forbidden");
|
|
877
|
+
return { origin, rejected: true };
|
|
878
|
+
}
|
|
879
|
+
return { origin, rejected: false };
|
|
880
|
+
};
|
|
881
|
+
var refreshSessionCookieIfPresent = async ({
|
|
882
|
+
req,
|
|
883
|
+
res,
|
|
884
|
+
token,
|
|
885
|
+
sessionCookieName,
|
|
886
|
+
sessionCookieOptions
|
|
887
|
+
}) => {
|
|
888
|
+
const hasTokenCookie = hasCookie(req.headers.cookie, sessionCookieName);
|
|
889
|
+
if (!hasTokenCookie || !token) return;
|
|
890
|
+
const currentSession = await getSession3(token);
|
|
891
|
+
if (currentSession?.id) {
|
|
892
|
+
res.setHeader("Set-Cookie", `${sessionCookieName}=${token}; ${sessionCookieOptions}`);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
var parseRequestParams = async ({
|
|
896
|
+
req,
|
|
897
|
+
res,
|
|
898
|
+
method,
|
|
899
|
+
routePath,
|
|
900
|
+
queryString,
|
|
901
|
+
requestId,
|
|
902
|
+
shouldLogDev
|
|
903
|
+
}) => {
|
|
904
|
+
let params = await getParams({ method, req, res, queryString });
|
|
905
|
+
if (res.writableEnded) return null;
|
|
906
|
+
if (params && typeof params === "object" && Object.keys(params).length > 0) {
|
|
907
|
+
if (shouldLogDev) {
|
|
908
|
+
getLogger6().debug(`[${requestId}] ${method} ${routePath}`, {
|
|
909
|
+
params: sanitizeForLog(params)
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
} else {
|
|
913
|
+
if (shouldLogDev) {
|
|
914
|
+
getLogger6().debug(`[${requestId}] ${method} ${routePath}`);
|
|
915
|
+
}
|
|
916
|
+
params = {};
|
|
917
|
+
}
|
|
918
|
+
return params;
|
|
919
|
+
};
|
|
920
|
+
var handleHttpRequest = async (req, res, options) => {
|
|
921
|
+
const config = getProjectConfig8();
|
|
922
|
+
const shouldLogDev = config.logging.devLogs;
|
|
923
|
+
const sessionCookieName = config.http.sessionCookieName;
|
|
924
|
+
const sessionCookieOptions = buildSessionCookieOptions(
|
|
925
|
+
config.session.expiryDays,
|
|
926
|
+
process.env.SECURE === "true",
|
|
927
|
+
config.http
|
|
928
|
+
);
|
|
929
|
+
const url = req.url ?? "/";
|
|
930
|
+
const [routePathRaw, queryStringRaw] = url.split("?");
|
|
931
|
+
const routePath = routePathRaw ?? "/";
|
|
932
|
+
const queryString = queryStringRaw ?? "";
|
|
933
|
+
const { origin, rejected } = enforceOriginPolicy(req, res, routePath);
|
|
934
|
+
if (rejected) return;
|
|
935
|
+
setSecurityHeaders(req, res, origin);
|
|
936
|
+
const incomingRequestId = req.headers["x-request-id"];
|
|
937
|
+
const requestId = (Array.isArray(incomingRequestId) ? incomingRequestId[0] : incomingRequestId) ?? randomUUID();
|
|
938
|
+
res.setHeader("X-Request-Id", requestId);
|
|
939
|
+
const safeHeaders = {};
|
|
940
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
941
|
+
if (k === "authorization" || k === "cookie" || k === "set-cookie" || k === "x-csrf-token") continue;
|
|
942
|
+
safeHeaders[k] = Array.isArray(v) ? v.join(", ") : v ?? "";
|
|
943
|
+
}
|
|
944
|
+
const preHttpResult = await dispatchHook5("preHttpRequest", {
|
|
945
|
+
method: req.method?.toUpperCase() ?? "GET",
|
|
946
|
+
url: req.url ?? "/",
|
|
947
|
+
requestId,
|
|
948
|
+
origin,
|
|
949
|
+
headers: safeHeaders
|
|
950
|
+
});
|
|
951
|
+
if (preHttpResult.stopped) {
|
|
952
|
+
res.statusCode = preHttpResult.signal.httpStatus ?? 403;
|
|
953
|
+
res.setHeader("Content-Type", "application/json");
|
|
954
|
+
res.end(JSON.stringify({ status: "error", errorCode: preHttpResult.signal.errorCode }));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (req.method === "OPTIONS") {
|
|
958
|
+
res.writeHead(204);
|
|
959
|
+
res.end();
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const method = req.method;
|
|
963
|
+
if (method !== "GET" && method !== "POST" && method !== "PUT" && method !== "DELETE") {
|
|
964
|
+
res.statusCode = 404;
|
|
965
|
+
res.setHeader("Content-Type", "text/plain");
|
|
966
|
+
res.end(`method: ${String(method)} not supported, use one of: GET, POST, PUT, DELETE`);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const token = extractTokenFromRequest3(req);
|
|
970
|
+
await refreshSessionCookieIfPresent({ req, res, token, sessionCookieName, sessionCookieOptions });
|
|
971
|
+
const csrfRejected = await enforceCsrfOnStateChangingRequest({ req, res, routePath, token, requestId });
|
|
972
|
+
if (csrfRejected) return;
|
|
973
|
+
const baseCtx = {
|
|
974
|
+
req,
|
|
975
|
+
res,
|
|
976
|
+
options,
|
|
977
|
+
routePath,
|
|
978
|
+
queryString,
|
|
979
|
+
method,
|
|
980
|
+
token,
|
|
981
|
+
requestId,
|
|
982
|
+
sessionCookieOptions
|
|
983
|
+
};
|
|
984
|
+
if (await dispatchRoutes(PRE_PARAMS_ROUTES, { ...baseCtx, params: {} })) return;
|
|
985
|
+
const params = await parseRequestParams({
|
|
986
|
+
req,
|
|
987
|
+
res,
|
|
988
|
+
method,
|
|
989
|
+
routePath,
|
|
990
|
+
queryString,
|
|
991
|
+
requestId,
|
|
992
|
+
shouldLogDev
|
|
993
|
+
});
|
|
994
|
+
if (params === null) return;
|
|
995
|
+
await dispatchRoutes(POST_PARAMS_ROUTES, { ...baseCtx, params });
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/loadSocket.ts
|
|
999
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
1000
|
+
import {
|
|
1001
|
+
abortAllForSocket,
|
|
1002
|
+
abortApiByResponseIndex,
|
|
1003
|
+
abortSyncByCb,
|
|
1004
|
+
allowedOrigin as allowedOrigin2,
|
|
1005
|
+
applySocketMiddlewares,
|
|
1006
|
+
attachSocketRedisAdapter,
|
|
1007
|
+
setIoInstance,
|
|
1008
|
+
socketEventNames,
|
|
1009
|
+
buildJoinRoomResponseEventName,
|
|
1010
|
+
buildLeaveRoomResponseEventName,
|
|
1011
|
+
buildGetJoinedRoomsResponseEventName,
|
|
1012
|
+
extractLanguageFromHeader,
|
|
1013
|
+
extractTokenFromSocket,
|
|
1014
|
+
getLogger as getLogger7,
|
|
1015
|
+
getProjectConfig as getProjectConfig9,
|
|
1016
|
+
dispatchHook as dispatchHook6,
|
|
1017
|
+
normalizeErrorResponse,
|
|
1018
|
+
tryCatch as tryCatch6
|
|
1019
|
+
} from "@luckystack/core";
|
|
1020
|
+
import { handleApiRequest } from "@luckystack/api";
|
|
1021
|
+
import { handleSyncRequest } from "@luckystack/sync";
|
|
1022
|
+
import { getSession as getSession4, saveSession } from "@luckystack/login";
|
|
1023
|
+
import {
|
|
1024
|
+
initActivityBroadcaster,
|
|
1025
|
+
socketConnected,
|
|
1026
|
+
socketDisconnecting,
|
|
1027
|
+
socketLeaveRoom
|
|
1028
|
+
} from "@luckystack/presence";
|
|
1029
|
+
var sessionLocks = /* @__PURE__ */ new Map();
|
|
1030
|
+
var withSessionLock = async (token, fn) => {
|
|
1031
|
+
const prev = sessionLocks.get(token) ?? Promise.resolve();
|
|
1032
|
+
const next = prev.then(fn, fn);
|
|
1033
|
+
sessionLocks.set(token, next);
|
|
1034
|
+
await tryCatch6(() => next);
|
|
1035
|
+
if (sessionLocks.get(token) === next) sessionLocks.delete(token);
|
|
1036
|
+
};
|
|
1037
|
+
var getVisibleSocketRooms = (socket, token) => {
|
|
1038
|
+
return [...socket.rooms].filter((room) => typeof room === "string").filter((room) => room !== socket.id).filter((room) => !token || room !== token);
|
|
1039
|
+
};
|
|
1040
|
+
var getSessionRoomCodes = (session) => {
|
|
1041
|
+
const roomCodes = Array.isArray(session.roomCodes) ? session.roomCodes.filter(
|
|
1042
|
+
(roomCode) => typeof roomCode === "string" && roomCode.length > 0
|
|
1043
|
+
) : [];
|
|
1044
|
+
return [...new Set(roomCodes)];
|
|
1045
|
+
};
|
|
1046
|
+
var sanitizeSessionRoomKeys = (session) => {
|
|
1047
|
+
const { code: _legacyCode, codes: _legacyCodes, ...sanitizedSession } = session;
|
|
1048
|
+
return sanitizedSession;
|
|
1049
|
+
};
|
|
1050
|
+
var loadSocket = (httpServer, options = {}) => {
|
|
1051
|
+
const config = getProjectConfig9();
|
|
1052
|
+
const shouldLogDev = config.logging.devLogs;
|
|
1053
|
+
const shouldLogSocketStartup = config.logging.socketStartup;
|
|
1054
|
+
const io = new SocketIOServer(httpServer, {
|
|
1055
|
+
cors: {
|
|
1056
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
1057
|
+
origin: (origin, callback) => {
|
|
1058
|
+
if (!origin || allowedOrigin2(origin)) {
|
|
1059
|
+
callback(null, true);
|
|
1060
|
+
} else {
|
|
1061
|
+
callback(new Error("Not allowed by CORS"));
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
credentials: true
|
|
1065
|
+
},
|
|
1066
|
+
maxHttpBufferSize: options.maxHttpBufferSize ?? config.socket.maxHttpBufferSize,
|
|
1067
|
+
pingTimeout: config.socket.pingTimeout,
|
|
1068
|
+
pingInterval: config.socket.pingInterval
|
|
1069
|
+
});
|
|
1070
|
+
setIoInstance(io);
|
|
1071
|
+
applySocketMiddlewares(io);
|
|
1072
|
+
attachSocketRedisAdapter(io);
|
|
1073
|
+
if (shouldLogSocketStartup) {
|
|
1074
|
+
getLogger7().info("SocketIO server initialized (redis adapter attached)");
|
|
1075
|
+
}
|
|
1076
|
+
io.on(socketEventNames.connect, (socket) => {
|
|
1077
|
+
const token = extractTokenFromSocket(socket);
|
|
1078
|
+
const activityBroadcasterEnabled = config.socketActivityBroadcaster ?? false;
|
|
1079
|
+
const locationProviderEnabled = config.locationProviderEnabled ?? false;
|
|
1080
|
+
const preferredLocale = extractLanguageFromHeader(socket.handshake.headers["x-language"]) || extractLanguageFromHeader(socket.handshake.headers["accept-language"]) || void 0;
|
|
1081
|
+
if (token) {
|
|
1082
|
+
void socketConnected({ token, io });
|
|
1083
|
+
}
|
|
1084
|
+
void dispatchHook6("onSocketConnect", {
|
|
1085
|
+
socketId: socket.id,
|
|
1086
|
+
token,
|
|
1087
|
+
ip: socket.handshake.address
|
|
1088
|
+
});
|
|
1089
|
+
socket.on(socketEventNames.apiRequest, (msg) => {
|
|
1090
|
+
void handleApiRequest({ msg, socket, token });
|
|
1091
|
+
});
|
|
1092
|
+
socket.on(socketEventNames.sync, (msg) => {
|
|
1093
|
+
void handleSyncRequest({ msg, socket, token });
|
|
1094
|
+
});
|
|
1095
|
+
socket.on(socketEventNames.syncCancel, (data) => {
|
|
1096
|
+
const cb = typeof data.cb === "string" ? data.cb : null;
|
|
1097
|
+
if (!cb) return;
|
|
1098
|
+
abortSyncByCb(socket.id, cb);
|
|
1099
|
+
});
|
|
1100
|
+
socket.on(socketEventNames.apiCancel, (data) => {
|
|
1101
|
+
const responseIndex = data.responseIndex;
|
|
1102
|
+
if (typeof responseIndex !== "number" && typeof responseIndex !== "string") return;
|
|
1103
|
+
abortApiByResponseIndex(socket.id, responseIndex);
|
|
1104
|
+
});
|
|
1105
|
+
socket.on(socketEventNames.joinRoom, (data) => {
|
|
1106
|
+
const group = typeof data.group === "string" ? data.group.trim() : "";
|
|
1107
|
+
const responseIndex = data.responseIndex;
|
|
1108
|
+
if (typeof responseIndex !== "number") return;
|
|
1109
|
+
if (!token) {
|
|
1110
|
+
socket.emit(buildJoinRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1111
|
+
response: { status: "error", errorCode: "auth.required" },
|
|
1112
|
+
preferredLocale
|
|
1113
|
+
}));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (!group) {
|
|
1117
|
+
socket.emit(buildJoinRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1118
|
+
response: { status: "error", errorCode: "room.invalid" },
|
|
1119
|
+
preferredLocale
|
|
1120
|
+
}));
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
void withSessionLock(token, async () => {
|
|
1124
|
+
const session = await getSession4(token);
|
|
1125
|
+
if (!session) {
|
|
1126
|
+
socket.emit(buildJoinRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1127
|
+
response: { status: "error", errorCode: "session.notFound" },
|
|
1128
|
+
preferredLocale
|
|
1129
|
+
}));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const preResult = await dispatchHook6("preRoomJoin", { token, room: group });
|
|
1133
|
+
if (preResult.stopped) {
|
|
1134
|
+
socket.emit(buildJoinRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1135
|
+
response: {
|
|
1136
|
+
status: "error",
|
|
1137
|
+
errorCode: preResult.signal.errorCode || "room.joinBlocked"
|
|
1138
|
+
},
|
|
1139
|
+
preferredLocale,
|
|
1140
|
+
userLanguage: session.language
|
|
1141
|
+
}));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const existingRoomCodes = getSessionRoomCodes(session);
|
|
1145
|
+
const nextRoomCodes = [.../* @__PURE__ */ new Set([...existingRoomCodes, group])];
|
|
1146
|
+
await socket.join(group);
|
|
1147
|
+
const sanitizedSession = sanitizeSessionRoomKeys(session);
|
|
1148
|
+
await saveSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });
|
|
1149
|
+
const visibleRooms = getVisibleSocketRooms(socket, token);
|
|
1150
|
+
socket.emit(buildJoinRoomResponseEventName(responseIndex), { rooms: visibleRooms });
|
|
1151
|
+
if (shouldLogDev) {
|
|
1152
|
+
getLogger7().debug(`Socket ${socket.id} joined group ${group}`);
|
|
1153
|
+
}
|
|
1154
|
+
void dispatchHook6("postRoomJoin", { token, room: group, allRooms: visibleRooms });
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
socket.on(socketEventNames.leaveRoom, (data) => {
|
|
1158
|
+
const group = typeof data.group === "string" ? data.group.trim() : "";
|
|
1159
|
+
const responseIndex = data.responseIndex;
|
|
1160
|
+
if (typeof responseIndex !== "number") return;
|
|
1161
|
+
if (!token) {
|
|
1162
|
+
socket.emit(buildLeaveRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1163
|
+
response: { status: "error", errorCode: "auth.required" },
|
|
1164
|
+
preferredLocale
|
|
1165
|
+
}));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (!group) {
|
|
1169
|
+
socket.emit(buildLeaveRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1170
|
+
response: { status: "error", errorCode: "room.invalid" },
|
|
1171
|
+
preferredLocale
|
|
1172
|
+
}));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
void withSessionLock(token, async () => {
|
|
1176
|
+
const session = await getSession4(token);
|
|
1177
|
+
if (!session) {
|
|
1178
|
+
socket.emit(buildLeaveRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1179
|
+
response: { status: "error", errorCode: "session.notFound" },
|
|
1180
|
+
preferredLocale
|
|
1181
|
+
}));
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const preResult = await dispatchHook6("preRoomLeave", { token, room: group });
|
|
1185
|
+
if (preResult.stopped) {
|
|
1186
|
+
socket.emit(buildLeaveRoomResponseEventName(responseIndex), normalizeErrorResponse({
|
|
1187
|
+
response: {
|
|
1188
|
+
status: "error",
|
|
1189
|
+
errorCode: preResult.signal.errorCode || "room.leaveBlocked"
|
|
1190
|
+
},
|
|
1191
|
+
preferredLocale,
|
|
1192
|
+
userLanguage: session.language
|
|
1193
|
+
}));
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const existingRoomCodes = getSessionRoomCodes(session);
|
|
1197
|
+
const nextRoomCodes = existingRoomCodes.filter((roomCode) => roomCode !== group);
|
|
1198
|
+
await socket.leave(group);
|
|
1199
|
+
const sanitizedSession = sanitizeSessionRoomKeys(session);
|
|
1200
|
+
await saveSession(token, { ...sanitizedSession, roomCodes: nextRoomCodes });
|
|
1201
|
+
const visibleRooms = getVisibleSocketRooms(socket, token);
|
|
1202
|
+
socket.emit(buildLeaveRoomResponseEventName(responseIndex), { rooms: visibleRooms });
|
|
1203
|
+
if (shouldLogDev) {
|
|
1204
|
+
getLogger7().debug(`Socket ${socket.id} left group ${group}`);
|
|
1205
|
+
}
|
|
1206
|
+
void dispatchHook6("postRoomLeave", { token, room: group, allRooms: visibleRooms });
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
socket.on(socketEventNames.getJoinedRooms, (data) => {
|
|
1210
|
+
const responseIndex = data.responseIndex;
|
|
1211
|
+
if (typeof responseIndex !== "number") return;
|
|
1212
|
+
if (!token) {
|
|
1213
|
+
socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {
|
|
1214
|
+
...normalizeErrorResponse({
|
|
1215
|
+
response: { status: "error", errorCode: "auth.required" },
|
|
1216
|
+
preferredLocale
|
|
1217
|
+
}),
|
|
1218
|
+
rooms: []
|
|
1219
|
+
});
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
socket.emit(buildGetJoinedRoomsResponseEventName(responseIndex), {
|
|
1223
|
+
rooms: getVisibleSocketRooms(socket, token)
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
socket.on(socketEventNames.disconnect, (reason) => {
|
|
1227
|
+
abortAllForSocket(socket.id);
|
|
1228
|
+
void dispatchHook6("onSocketDisconnect", { socketId: socket.id, token, reason });
|
|
1229
|
+
if (activityBroadcasterEnabled && token) {
|
|
1230
|
+
void socketDisconnecting({ token, socket, reason });
|
|
1231
|
+
} else {
|
|
1232
|
+
if (!token) return;
|
|
1233
|
+
if (shouldLogDev) {
|
|
1234
|
+
getLogger7().debug(`user disconnected`, { reason });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
socket.on(
|
|
1239
|
+
socketEventNames.updateLocation,
|
|
1240
|
+
(newLocation) => {
|
|
1241
|
+
if (!token) return;
|
|
1242
|
+
if (!locationProviderEnabled) return;
|
|
1243
|
+
if (shouldLogDev) {
|
|
1244
|
+
getLogger7().debug("updating location", { pathName: newLocation.pathName });
|
|
1245
|
+
}
|
|
1246
|
+
void withSessionLock(token, async () => {
|
|
1247
|
+
let returnedUser = null;
|
|
1248
|
+
if (activityBroadcasterEnabled) {
|
|
1249
|
+
returnedUser = await socketLeaveRoom({ token, socket, newPath: newLocation.pathName });
|
|
1250
|
+
}
|
|
1251
|
+
const user = returnedUser ?? await getSession4(token);
|
|
1252
|
+
if (!user) return;
|
|
1253
|
+
const extendedUser = user;
|
|
1254
|
+
const oldLocation = extendedUser.location;
|
|
1255
|
+
extendedUser.location = newLocation;
|
|
1256
|
+
await saveSession(token, user);
|
|
1257
|
+
void dispatchHook6("onLocationUpdate", { token, oldLocation, newLocation });
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
if (activityBroadcasterEnabled && token) {
|
|
1262
|
+
initActivityBroadcaster({ socket, token });
|
|
1263
|
+
}
|
|
1264
|
+
if (token) {
|
|
1265
|
+
void (async () => {
|
|
1266
|
+
const [rejoinError, codes] = await tryCatch6(async () => {
|
|
1267
|
+
await socket.join(token);
|
|
1268
|
+
const session = await getSession4(token);
|
|
1269
|
+
const roomCodes = session ? getSessionRoomCodes(session) : [];
|
|
1270
|
+
for (const roomCode of roomCodes) {
|
|
1271
|
+
await socket.join(roomCode);
|
|
1272
|
+
}
|
|
1273
|
+
return roomCodes;
|
|
1274
|
+
});
|
|
1275
|
+
if (rejoinError) {
|
|
1276
|
+
getLogger7().warn(`socket room rejoin failed for ${socket.id}`, { error: rejoinError.message });
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (shouldLogDev) {
|
|
1280
|
+
getLogger7().debug(`socket ${socket.id} (re)joined rooms: ${(codes ?? []).join(", ") || "(none)"}`);
|
|
1281
|
+
}
|
|
1282
|
+
})();
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
return io;
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/verifyBootstrap.ts
|
|
1289
|
+
import {
|
|
1290
|
+
getLogger as getLogger8,
|
|
1291
|
+
isDeployConfigRegistered,
|
|
1292
|
+
isLocalizedNormalizerRegistered,
|
|
1293
|
+
isProjectConfigRegistered,
|
|
1294
|
+
isRuntimeMapsProviderRegistered
|
|
1295
|
+
} from "@luckystack/core";
|
|
1296
|
+
var verifyBootstrap = async (requirements = {}) => {
|
|
1297
|
+
const missing = [];
|
|
1298
|
+
if (!isProjectConfigRegistered()) {
|
|
1299
|
+
missing.push(
|
|
1300
|
+
"ProjectConfig \u2014 call `registerProjectConfig({...})` from `luckystack/core/bootstrap.ts` (or your config.ts)."
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (requirements.requireDeployConfig && !isDeployConfigRegistered()) {
|
|
1304
|
+
missing.push(
|
|
1305
|
+
"DeployConfig \u2014 call `registerDeployConfig({...})` from `luckystack/deploy/deploy.config.ts`."
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
if (requirements.requireServicesConfig) {
|
|
1309
|
+
const { isServicesConfigRegistered } = await import("@luckystack/core");
|
|
1310
|
+
if (!isServicesConfigRegistered()) {
|
|
1311
|
+
missing.push(
|
|
1312
|
+
"ServicesConfig \u2014 call `registerServicesConfig({...})` from `services.config.ts`."
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (requirements.requireOAuthProviders) {
|
|
1317
|
+
const { getOAuthProviders: getOAuthProviders2 } = await import("@luckystack/login");
|
|
1318
|
+
if (getOAuthProviders2().length <= 1) {
|
|
1319
|
+
missing.push(
|
|
1320
|
+
"OAuth providers \u2014 call `registerOAuthProviders([...])` from `luckystack/login/oauthProviders.ts` (or skip this check if your app uses credentials only)."
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (!isRuntimeMapsProviderRegistered()) {
|
|
1325
|
+
if (process.env.NODE_ENV === "production") {
|
|
1326
|
+
missing.push(
|
|
1327
|
+
"RuntimeMapsProvider \u2014 call `registerRuntimeMapsProvider({...})` from `server/prod/runtimeMaps.ts`. Without it, every api/sync request returns notFound."
|
|
1328
|
+
);
|
|
1329
|
+
} else {
|
|
1330
|
+
getLogger8().warn(
|
|
1331
|
+
"[LuckyStack] No RuntimeMapsProvider registered \u2014 api/sync requests will resolve to empty maps. Devkit hot-reload usually registers one automatically."
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (!isLocalizedNormalizerRegistered()) {
|
|
1336
|
+
if (process.env.NODE_ENV === "production") {
|
|
1337
|
+
missing.push(
|
|
1338
|
+
"LocalizedNormalizer \u2014 call `registerLocalizedNormalizer({...})` from your bootstrap. Without it, error response messages will be the raw errorCode (no i18n)."
|
|
1339
|
+
);
|
|
1340
|
+
} else {
|
|
1341
|
+
getLogger8().warn(
|
|
1342
|
+
"[LuckyStack] No LocalizedNormalizer registered \u2014 error messages will pass through as the raw errorCode."
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (missing.length === 0) return;
|
|
1347
|
+
const detail = missing.map((line, idx) => ` ${idx + 1}. ${line}`).join("\n");
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
[
|
|
1350
|
+
"[LuckyStack] Bootstrap incomplete \u2014 the following registrations are missing:",
|
|
1351
|
+
detail,
|
|
1352
|
+
"",
|
|
1353
|
+
"See docs/ARCHITECTURE_PACKAGING.md (overlay layout) for the recommended bootstrap order."
|
|
1354
|
+
].join("\n")
|
|
1355
|
+
);
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
// src/runtimeMapsLoader.ts
|
|
1359
|
+
import {
|
|
1360
|
+
registerRuntimeMapsProvider
|
|
1361
|
+
} from "@luckystack/core";
|
|
1362
|
+
|
|
1363
|
+
// src/argv.ts
|
|
1364
|
+
var parsedBundles = [];
|
|
1365
|
+
var parsedPort = null;
|
|
1366
|
+
var hasRun = false;
|
|
1367
|
+
var PORT_PATTERN = /^\d+$/;
|
|
1368
|
+
var parseServerArgv = (argv) => {
|
|
1369
|
+
if (argv.length > 2) {
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
`[luckystack:argv] unexpected positional argument(s): "${argv.slice(2).join(" ")}". Usage: npm run server -- <bundle[,bundle...]> [port]`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
const bundles = argv[0] && argv[0].length > 0 ? [...new Set(argv[0].split(",").map((s) => s.trim()).filter(Boolean))] : [];
|
|
1375
|
+
let port = null;
|
|
1376
|
+
const portArg = argv[1];
|
|
1377
|
+
if (portArg !== void 0) {
|
|
1378
|
+
if (!PORT_PATTERN.test(portArg)) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`[luckystack:argv] port argument must be numeric, got: "${portArg}". Usage: npm run server -- <bundle[,bundle...]> [port]`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
port = Number.parseInt(portArg, 10);
|
|
1384
|
+
}
|
|
1385
|
+
return { bundles, port };
|
|
1386
|
+
};
|
|
1387
|
+
var applyServerArgv = () => {
|
|
1388
|
+
if (hasRun) return;
|
|
1389
|
+
hasRun = true;
|
|
1390
|
+
const result = parseServerArgv(process.argv.slice(2));
|
|
1391
|
+
parsedBundles = result.bundles;
|
|
1392
|
+
parsedPort = result.port;
|
|
1393
|
+
if (parsedPort !== null) {
|
|
1394
|
+
process.env.SERVER_PORT = String(parsedPort);
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
var getParsedBundles = () => parsedBundles;
|
|
1398
|
+
var getParsedPort = () => parsedPort;
|
|
1399
|
+
|
|
1400
|
+
// src/runtimeMapsLoader.ts
|
|
1401
|
+
var emptyRuntimeMaps = {
|
|
1402
|
+
apisObject: {},
|
|
1403
|
+
syncObject: {},
|
|
1404
|
+
functionsObject: {}
|
|
1405
|
+
};
|
|
1406
|
+
var isRuntimeMapRecord = (value) => Boolean(value) && typeof value === "object";
|
|
1407
|
+
var normalizeGeneratedModule = (moduleValue) => {
|
|
1408
|
+
const moduleRecord = moduleValue && typeof moduleValue === "object" ? moduleValue : {};
|
|
1409
|
+
const apiCandidate = moduleRecord.apis;
|
|
1410
|
+
const syncCandidate = moduleRecord.syncs;
|
|
1411
|
+
const functionCandidate = moduleRecord.functions;
|
|
1412
|
+
return {
|
|
1413
|
+
apisObject: isRuntimeMapRecord(apiCandidate) ? apiCandidate : {},
|
|
1414
|
+
syncObject: isRuntimeMapRecord(syncCandidate) ? syncCandidate : {},
|
|
1415
|
+
functionsObject: isRuntimeMapRecord(functionCandidate) ? functionCandidate : {}
|
|
1416
|
+
};
|
|
1417
|
+
};
|
|
1418
|
+
var resolvePresets = (options) => {
|
|
1419
|
+
const fromOptions = options.preset;
|
|
1420
|
+
if (typeof fromOptions === "string" && fromOptions.length > 0) {
|
|
1421
|
+
return [fromOptions];
|
|
1422
|
+
}
|
|
1423
|
+
if (Array.isArray(fromOptions) && fromOptions.length > 0) {
|
|
1424
|
+
return [...new Set(fromOptions)];
|
|
1425
|
+
}
|
|
1426
|
+
const fromArgv = getParsedBundles();
|
|
1427
|
+
if (fromArgv.length > 0) {
|
|
1428
|
+
return fromArgv;
|
|
1429
|
+
}
|
|
1430
|
+
return ["default"];
|
|
1431
|
+
};
|
|
1432
|
+
var mergeInto = (target, source, kind, fromPreset, keyOrigin) => {
|
|
1433
|
+
for (const key of Object.keys(source)) {
|
|
1434
|
+
const previousPreset = keyOrigin.get(key);
|
|
1435
|
+
if (previousPreset !== void 0 && previousPreset !== fromPreset) {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
`[luckystack:runtimeMaps] ${kind} key collision: "${key}" present in both preset "${previousPreset}" and preset "${fromPreset}". Services must belong to exactly one preset (see docs/ARCHITECTURE_PACKAGING.md \xA710).`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
keyOrigin.set(key, fromPreset);
|
|
1441
|
+
target[key] = source[key];
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
var isProduction = () => process.env.NODE_ENV === "production";
|
|
1445
|
+
var createProdRuntimeMapsProvider = (options) => {
|
|
1446
|
+
let prodMapsPromise = null;
|
|
1447
|
+
let devkitModulePromise = null;
|
|
1448
|
+
const getDevkit = async () => {
|
|
1449
|
+
devkitModulePromise ??= import("@luckystack/devkit");
|
|
1450
|
+
return await devkitModulePromise;
|
|
1451
|
+
};
|
|
1452
|
+
const loadProdRuntimeMaps = async () => {
|
|
1453
|
+
if (prodMapsPromise) return await prodMapsPromise;
|
|
1454
|
+
prodMapsPromise = (async () => {
|
|
1455
|
+
const presets = resolvePresets(options);
|
|
1456
|
+
const merged = {
|
|
1457
|
+
apisObject: {},
|
|
1458
|
+
syncObject: {},
|
|
1459
|
+
functionsObject: {}
|
|
1460
|
+
};
|
|
1461
|
+
const apiOrigin = /* @__PURE__ */ new Map();
|
|
1462
|
+
const syncOrigin = /* @__PURE__ */ new Map();
|
|
1463
|
+
const functionOrigin = /* @__PURE__ */ new Map();
|
|
1464
|
+
const loadedModules = await Promise.all(
|
|
1465
|
+
presets.map(async (preset) => ({
|
|
1466
|
+
preset,
|
|
1467
|
+
mod: await options.loadGenerated(preset).catch(() => null)
|
|
1468
|
+
}))
|
|
1469
|
+
);
|
|
1470
|
+
let loadedAny = false;
|
|
1471
|
+
for (const { preset, mod } of loadedModules) {
|
|
1472
|
+
if (!mod) {
|
|
1473
|
+
console.warn(
|
|
1474
|
+
`[luckystack:runtimeMaps] preset "${preset}" failed to load \u2014 skipping. Calls owned by that preset will return notFound until the generated module resolves.`
|
|
1475
|
+
);
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
loadedAny = true;
|
|
1479
|
+
const normalized = normalizeGeneratedModule(mod);
|
|
1480
|
+
mergeInto(merged.apisObject, normalized.apisObject, "api", preset, apiOrigin);
|
|
1481
|
+
mergeInto(merged.syncObject, normalized.syncObject, "sync", preset, syncOrigin);
|
|
1482
|
+
mergeInto(merged.functionsObject, normalized.functionsObject, "function", preset, functionOrigin);
|
|
1483
|
+
}
|
|
1484
|
+
if (!loadedAny) {
|
|
1485
|
+
console.warn(
|
|
1486
|
+
`[luckystack:runtimeMaps] no presets resolved (tried: ${presets.join(", ")}). Every api/sync request will return notFound until at least one generated module loads.`
|
|
1487
|
+
);
|
|
1488
|
+
return emptyRuntimeMaps;
|
|
1489
|
+
}
|
|
1490
|
+
return merged;
|
|
1491
|
+
})();
|
|
1492
|
+
return await prodMapsPromise;
|
|
1493
|
+
};
|
|
1494
|
+
const getRuntimeApiMaps = async () => {
|
|
1495
|
+
if (!isProduction()) {
|
|
1496
|
+
const { devApis, devFunctions } = await getDevkit();
|
|
1497
|
+
return {
|
|
1498
|
+
apisObject: isRuntimeMapRecord(devApis) ? devApis : {},
|
|
1499
|
+
functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {}
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const { apisObject, functionsObject } = await loadProdRuntimeMaps();
|
|
1503
|
+
return { apisObject, functionsObject };
|
|
1504
|
+
};
|
|
1505
|
+
const getRuntimeSyncMaps = async () => {
|
|
1506
|
+
if (!isProduction()) {
|
|
1507
|
+
const { devSyncs, devFunctions } = await getDevkit();
|
|
1508
|
+
return {
|
|
1509
|
+
syncObject: isRuntimeMapRecord(devSyncs) ? devSyncs : {},
|
|
1510
|
+
functionsObject: isRuntimeMapRecord(devFunctions) ? devFunctions : {}
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
const { syncObject, functionsObject } = await loadProdRuntimeMaps();
|
|
1514
|
+
return { syncObject, functionsObject };
|
|
1515
|
+
};
|
|
1516
|
+
return { getRuntimeApiMaps, getRuntimeSyncMaps };
|
|
1517
|
+
};
|
|
1518
|
+
var registerProdRuntimeMapsProvider = (options) => {
|
|
1519
|
+
const provider = createProdRuntimeMapsProvider(options);
|
|
1520
|
+
registerRuntimeMapsProvider(provider);
|
|
1521
|
+
return provider;
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
// src/createServer.ts
|
|
1525
|
+
var createLuckyStackServer = async (options = {}) => {
|
|
1526
|
+
if (options.loadGeneratedMaps) {
|
|
1527
|
+
registerProdRuntimeMapsProvider({
|
|
1528
|
+
loadGenerated: options.loadGeneratedMaps,
|
|
1529
|
+
preset: options.runtimeMapsPreset
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
await verifyBootstrap({
|
|
1533
|
+
requireDeployConfig: options.requireDeployConfig,
|
|
1534
|
+
requireServicesConfig: options.requireServicesConfig,
|
|
1535
|
+
requireOAuthProviders: options.requireOAuthProviders
|
|
1536
|
+
});
|
|
1537
|
+
const port = options.port ?? getParsedPort() ?? process.env.SERVER_PORT ?? 80;
|
|
1538
|
+
const ip = options.ip ?? process.env.SERVER_IP ?? "127.0.0.1";
|
|
1539
|
+
const enableDevTools = options.enableDevTools ?? process.env.NODE_ENV !== "production";
|
|
1540
|
+
registerBindAddress({
|
|
1541
|
+
ip,
|
|
1542
|
+
port: typeof port === "string" ? Number.parseInt(port, 10) : port
|
|
1543
|
+
});
|
|
1544
|
+
if (enableDevTools) {
|
|
1545
|
+
const { initConsolelog } = await import("@luckystack/core");
|
|
1546
|
+
initConsolelog();
|
|
1547
|
+
const devkitModuleId = "@luckystack/devkit";
|
|
1548
|
+
const devkit = await import(devkitModuleId);
|
|
1549
|
+
await devkit.initializeAll();
|
|
1550
|
+
devkit.setupWatchers();
|
|
1551
|
+
process.once("SIGINT", () => process.exit(0));
|
|
1552
|
+
process.once("SIGTERM", () => process.exit(0));
|
|
1553
|
+
}
|
|
1554
|
+
const [bootUuidError] = await tryCatch7(() => writeBootUuid());
|
|
1555
|
+
if (bootUuidError) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
"Failed to write the boot UUID to Redis. Check REDIS_HOST / REDIS_PORT / REDIS_USERNAME / REDIS_PASSWORD and that Redis is reachable.",
|
|
1558
|
+
{ cause: bootUuidError }
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
const httpServer = http.createServer((req, res) => {
|
|
1562
|
+
void handleHttpRequest(req, res, options);
|
|
1563
|
+
});
|
|
1564
|
+
const ioServer = loadSocket(httpServer, {
|
|
1565
|
+
maxHttpBufferSize: options.maxHttpBufferSize
|
|
1566
|
+
});
|
|
1567
|
+
const listen = (callback) => new Promise((resolve) => {
|
|
1568
|
+
const portValue = typeof port === "string" ? Number.parseInt(port, 10) : port;
|
|
1569
|
+
httpServer.listen(portValue, ip, () => {
|
|
1570
|
+
const config = getProjectConfig10();
|
|
1571
|
+
if (config.logging.socketStartup || config.logging.devLogs) {
|
|
1572
|
+
getLogger9().info(`Server is running on http://${ip}:${String(port)}/`);
|
|
1573
|
+
}
|
|
1574
|
+
callback?.();
|
|
1575
|
+
resolve(httpServer);
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
return { httpServer, ioServer, listen };
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
// src/bootstrap.ts
|
|
1582
|
+
import path2 from "path";
|
|
1583
|
+
import fs from "fs";
|
|
1584
|
+
import { pathToFileURL } from "url";
|
|
1585
|
+
import { ROOT_DIR } from "@luckystack/core";
|
|
1586
|
+
var OVERLAY_ORDER = [
|
|
1587
|
+
// Core registries first — clients, paths, routing rules. Anything below
|
|
1588
|
+
// depends on these being in place.
|
|
1589
|
+
"core",
|
|
1590
|
+
// Deploy + services topology — needed by the router but harmless for
|
|
1591
|
+
// single-instance deploys.
|
|
1592
|
+
"deploy",
|
|
1593
|
+
// Auth — OAuth providers + user adapter sit on top of core.
|
|
1594
|
+
"login",
|
|
1595
|
+
// Sentry / docs-ui / presence sit on top of core but don't block boot.
|
|
1596
|
+
"sentry",
|
|
1597
|
+
"presence",
|
|
1598
|
+
"docs-ui",
|
|
1599
|
+
// Server overlay last — typically empty, but a place to wire framework
|
|
1600
|
+
// hooks (`registerHook('onSocketConnect', ...)` etc.) before listen().
|
|
1601
|
+
"server"
|
|
1602
|
+
];
|
|
1603
|
+
var importIfExists = async (filePath) => {
|
|
1604
|
+
if (!fs.existsSync(filePath)) return;
|
|
1605
|
+
await import(pathToFileURL(filePath).href);
|
|
1606
|
+
};
|
|
1607
|
+
var loadOverlayFolder = async (overlayRoot) => {
|
|
1608
|
+
const overlayAbs = path2.isAbsolute(overlayRoot) ? overlayRoot : path2.join(ROOT_DIR, overlayRoot);
|
|
1609
|
+
if (!fs.existsSync(overlayAbs)) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
for (const packageName of OVERLAY_ORDER) {
|
|
1613
|
+
const packageDir = path2.join(overlayAbs, packageName);
|
|
1614
|
+
if (!fs.existsSync(packageDir)) continue;
|
|
1615
|
+
const indexCandidates = ["index.ts", "index.js"];
|
|
1616
|
+
for (const candidate of indexCandidates) {
|
|
1617
|
+
await importIfExists(path2.join(packageDir, candidate));
|
|
1618
|
+
}
|
|
1619
|
+
const entries = fs.readdirSync(packageDir).toSorted();
|
|
1620
|
+
for (const entry of entries) {
|
|
1621
|
+
if (indexCandidates.includes(entry)) continue;
|
|
1622
|
+
if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
|
|
1623
|
+
await importIfExists(path2.join(packageDir, entry));
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
var bootstrapLuckyStack = async (options = {}) => {
|
|
1628
|
+
const overlayRoot = options.overlayRoot ?? "luckystack";
|
|
1629
|
+
if (!options.skipOverlayLoad) {
|
|
1630
|
+
await loadOverlayFolder(overlayRoot);
|
|
1631
|
+
}
|
|
1632
|
+
const server = await createLuckyStackServer(options);
|
|
1633
|
+
return server;
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
// src/errorFormatterRegistry.ts
|
|
1637
|
+
import {
|
|
1638
|
+
registerErrorFormatter,
|
|
1639
|
+
getErrorFormatter,
|
|
1640
|
+
applyErrorFormatter
|
|
1641
|
+
} from "@luckystack/core";
|
|
1642
|
+
export {
|
|
1643
|
+
applyServerArgv,
|
|
1644
|
+
bootstrapLuckyStack,
|
|
1645
|
+
clearCustomRoutes,
|
|
1646
|
+
clearOriginExemptPaths,
|
|
1647
|
+
createLuckyStackServer,
|
|
1648
|
+
createProdRuntimeMapsProvider,
|
|
1649
|
+
getCustomRoutes,
|
|
1650
|
+
getErrorFormatter,
|
|
1651
|
+
getOriginExemptPaths,
|
|
1652
|
+
getParsedBundles,
|
|
1653
|
+
getParsedPort,
|
|
1654
|
+
getPreParamsCustomRoutes,
|
|
1655
|
+
getSecurityHeadersBuilder,
|
|
1656
|
+
isOriginExemptPath,
|
|
1657
|
+
parseServerArgv,
|
|
1658
|
+
registerCustomRoute,
|
|
1659
|
+
registerErrorFormatter,
|
|
1660
|
+
registerOriginExemptPath,
|
|
1661
|
+
registerProdRuntimeMapsProvider,
|
|
1662
|
+
registerSecurityHeaders,
|
|
1663
|
+
verifyBootstrap
|
|
1664
|
+
};
|
|
1665
|
+
//# sourceMappingURL=index.js.map
|