@spring-systems/server 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +8 -0
  3. package/README.md +94 -0
  4. package/dist/api-route-handler.d.ts +49 -0
  5. package/dist/api-route-handler.js +19 -0
  6. package/dist/api-route-handler.js.map +1 -0
  7. package/dist/chunk-7IUSTA5W.js +113 -0
  8. package/dist/chunk-7IUSTA5W.js.map +1 -0
  9. package/dist/chunk-CLZU34DG.js +465 -0
  10. package/dist/chunk-CLZU34DG.js.map +1 -0
  11. package/dist/chunk-CP33WQ5Q.js +47 -0
  12. package/dist/chunk-CP33WQ5Q.js.map +1 -0
  13. package/dist/chunk-FEB3UZEG.js +407 -0
  14. package/dist/chunk-FEB3UZEG.js.map +1 -0
  15. package/dist/chunk-KA7RJCWA.js +24 -0
  16. package/dist/chunk-KA7RJCWA.js.map +1 -0
  17. package/dist/chunk-OYTV4D7E.js +159 -0
  18. package/dist/chunk-OYTV4D7E.js.map +1 -0
  19. package/dist/chunk-YV6DZVPI.js +43 -0
  20. package/dist/chunk-YV6DZVPI.js.map +1 -0
  21. package/dist/client.d.ts +6 -0
  22. package/dist/client.js +14 -0
  23. package/dist/client.js.map +1 -0
  24. package/dist/handlers/index.d.ts +81 -0
  25. package/dist/handlers/index.js +48 -0
  26. package/dist/handlers/index.js.map +1 -0
  27. package/dist/index.d.ts +10 -0
  28. package/dist/index.js +44 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/next-adapters.d.ts +25 -0
  31. package/dist/next-adapters.js +14 -0
  32. package/dist/next-adapters.js.map +1 -0
  33. package/dist/proxy-middleware.d.ts +8 -0
  34. package/dist/proxy-middleware.js +10 -0
  35. package/dist/proxy-middleware.js.map +1 -0
  36. package/dist/rate-limiter.d.ts +67 -0
  37. package/dist/rate-limiter.js +15 -0
  38. package/dist/rate-limiter.js.map +1 -0
  39. package/dist/runtime-env.d.ts +15 -0
  40. package/dist/runtime-env.js +9 -0
  41. package/dist/runtime-env.js.map +1 -0
  42. package/dist/security-headers.d.ts +8 -0
  43. package/dist/security-headers.js +11 -0
  44. package/dist/security-headers.js.map +1 -0
  45. package/package.json +114 -0
@@ -0,0 +1,407 @@
1
+ import {
2
+ checkRateLimit,
3
+ clearRateLimitEntries,
4
+ getRateLimiterAdapter,
5
+ recordFailedAttempt
6
+ } from "./chunk-7IUSTA5W.js";
7
+
8
+ // src/handlers/auth-session.ts
9
+ import { getFrameworkConfig, getSecureSessionCookieName, getSessionCookieName } from "@spring-systems/core/config";
10
+ var MAX_SESSION_AGE_SECONDS = 604800;
11
+ function getSessionCookieMaxAgeSeconds() {
12
+ return Number(process.env.AUTH_COOKIE_MAX_AGE_SECONDS || "28800");
13
+ }
14
+ function isTargetProdRuntime() {
15
+ return (process.env.TARGET_ENV || "") === "prod";
16
+ }
17
+ function getSessionCookieNameLazy() {
18
+ return getSessionCookieName(getFrameworkConfig().auth.sessionCookiePrefix);
19
+ }
20
+ function getSecureSessionCookieNameLazy() {
21
+ return getSecureSessionCookieName(getFrameworkConfig().auth.sessionCookiePrefix);
22
+ }
23
+ function getSessionExpiredCodeMarkers() {
24
+ return new Set(getFrameworkConfig().auth.sessionExpiredCodes);
25
+ }
26
+ function isLocalHostRequest(req) {
27
+ return req.nextUrl.hostname === "localhost" || req.nextUrl.hostname === "127.0.0.1" || req.nextUrl.hostname === "::1" || req.nextUrl.hostname === "[::1]";
28
+ }
29
+ function useSecureCookies(req) {
30
+ if (isLocalHostRequest(req)) return false;
31
+ if (isTargetProdRuntime()) return true;
32
+ return req.nextUrl.protocol === "https:";
33
+ }
34
+ function resolveSessionCookieName(req) {
35
+ return useSecureCookies(req) ? getSecureSessionCookieNameLazy() : getSessionCookieNameLazy();
36
+ }
37
+ function setCookieWithName(res, req, cookieName, value, maxAge) {
38
+ const secure = useSecureCookies(req);
39
+ res.cookies.set({
40
+ name: cookieName,
41
+ value,
42
+ httpOnly: true,
43
+ secure,
44
+ sameSite: "lax",
45
+ path: "/",
46
+ maxAge,
47
+ priority: "high"
48
+ });
49
+ }
50
+ function setSessionCookie(res, req, token) {
51
+ const configuredMaxAge = getSessionCookieMaxAgeSeconds();
52
+ const rawAge = Number.isFinite(configuredMaxAge) && configuredMaxAge > 0 ? Math.floor(configuredMaxAge) : 28800;
53
+ const maxAge = Math.min(rawAge, MAX_SESSION_AGE_SECONDS);
54
+ const cookieName = resolveSessionCookieName(req);
55
+ const staleCookieName = cookieName === getSessionCookieNameLazy() ? getSecureSessionCookieNameLazy() : getSessionCookieNameLazy();
56
+ setCookieWithName(res, req, cookieName, token, maxAge);
57
+ setCookieWithName(res, req, staleCookieName, "", 0);
58
+ }
59
+ function clearSessionCookieWithName(res, req, cookieName) {
60
+ const secure = useSecureCookies(req);
61
+ res.cookies.set({
62
+ name: cookieName,
63
+ value: "",
64
+ httpOnly: true,
65
+ secure,
66
+ sameSite: "lax",
67
+ path: "/",
68
+ maxAge: 0,
69
+ priority: "high"
70
+ });
71
+ const oppositeSecure = !secure;
72
+ const cookieParts = [`${cookieName}=`, "Path=/", "Max-Age=0", "HttpOnly", "SameSite=Lax", "Priority=High"];
73
+ if (oppositeSecure) {
74
+ cookieParts.push("Secure");
75
+ }
76
+ res.headers.append("Set-Cookie", cookieParts.join("; "));
77
+ }
78
+ function clearSessionCookie(res, req) {
79
+ clearSessionCookieWithName(res, req, getSessionCookieNameLazy());
80
+ clearSessionCookieWithName(res, req, getSecureSessionCookieNameLazy());
81
+ }
82
+ function getSessionToken(req) {
83
+ const cookies = [getSessionCookieNameLazy(), getSecureSessionCookieNameLazy()];
84
+ for (const cookieName of cookies) {
85
+ const fromCookiesApi = (req.cookies.get(cookieName)?.value || "").trim();
86
+ if (fromCookiesApi) return fromCookiesApi;
87
+ }
88
+ const cookieHeader = req.headers.get("cookie") || "";
89
+ if (!cookieHeader) return "";
90
+ const parts = cookieHeader.split(";").map((part) => part.trim());
91
+ for (const part of parts) {
92
+ if (!part) continue;
93
+ const eqIndex = part.indexOf("=");
94
+ if (eqIndex <= 0) continue;
95
+ const key = part.slice(0, eqIndex).trim();
96
+ if (key !== getSessionCookieNameLazy() && key !== getSecureSessionCookieNameLazy()) continue;
97
+ const rawValue = part.slice(eqIndex + 1).trim();
98
+ if (!rawValue) continue;
99
+ try {
100
+ return decodeURIComponent(rawValue);
101
+ } catch {
102
+ return rawValue;
103
+ }
104
+ }
105
+ return "";
106
+ }
107
+ function normalizeErrorText(value) {
108
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
109
+ }
110
+ function isSessionExpiredCode(value) {
111
+ const normalized = normalizeErrorText(value);
112
+ return normalized !== "" && getSessionExpiredCodeMarkers().has(normalized);
113
+ }
114
+ async function shouldClearSessionFromForbidden(response) {
115
+ if (response.status !== 403) return false;
116
+ try {
117
+ const cloned = response.clone();
118
+ try {
119
+ const payload = await cloned.json();
120
+ return isSessionExpiredCode(payload.code) || isSessionExpiredCode(payload.error_code);
121
+ } catch {
122
+ return false;
123
+ }
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ // src/handlers/csrf-cors.ts
130
+ import { getFrameworkConfig as getFrameworkConfig2 } from "@spring-systems/core/config";
131
+ var STATE_CHANGING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
132
+ function getFrontendUrl() {
133
+ return (process.env.FRONTEND_URL || "").trim();
134
+ }
135
+ function getFrontendIp() {
136
+ return (process.env.FRONTEND_IP || "").trim();
137
+ }
138
+ function trustProxyHeaders() {
139
+ return process.env.AUTH_TRUST_PROXY_HEADERS === "true";
140
+ }
141
+ function getCsrfExemptPaths() {
142
+ return new Set(
143
+ (process.env.CSRF_EXEMPT_PATHS || "").split(",").map(
144
+ (v) => v.trim().replace(/^\/+|\/+$/g, "").toLowerCase()
145
+ ).filter(Boolean)
146
+ );
147
+ }
148
+ function isProdRuntime() {
149
+ return (process.env.NODE_ENV || "") === "production" || (process.env.TARGET_ENV || "") === "prod";
150
+ }
151
+ function normalizeOrigin(value) {
152
+ try {
153
+ return new URL(value).origin;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+ function isInternalIpAccess(req) {
159
+ const frontendIp = getFrontendIp();
160
+ if (!frontendIp) return false;
161
+ const directIp = req.ip?.trim();
162
+ if (directIp) return directIp === frontendIp;
163
+ if (!trustProxyHeaders()) {
164
+ return false;
165
+ }
166
+ const xForwardedFor = (req.headers.get("x-forwarded-for") || "").trim();
167
+ const forwardedFirstIp = xForwardedFor.split(",")[0]?.trim();
168
+ if (forwardedFirstIp) return forwardedFirstIp === frontendIp;
169
+ const xRealIp = (req.headers.get("x-real-ip") || "").trim();
170
+ if (xRealIp) return xRealIp === frontendIp;
171
+ return false;
172
+ }
173
+ function resolveAllowedOrigin(req) {
174
+ const origin = (req.headers.get("origin") || "").trim();
175
+ if (!origin) return null;
176
+ const normalizedOrigin = normalizeOrigin(origin);
177
+ if (!normalizedOrigin) return null;
178
+ const frontendUrl = getFrontendUrl();
179
+ const normalizedFrontendOrigin = frontendUrl ? normalizeOrigin(frontendUrl) : null;
180
+ if (normalizedFrontendOrigin && normalizedOrigin === normalizedFrontendOrigin) {
181
+ return normalizedOrigin;
182
+ }
183
+ const frontendIp = getFrontendIp();
184
+ if (!frontendIp) return null;
185
+ try {
186
+ const parsedOrigin = new URL(normalizedOrigin);
187
+ if (parsedOrigin.hostname !== frontendIp) return null;
188
+ if (!["http:", "https:"].includes(parsedOrigin.protocol)) return null;
189
+ if (isProdRuntime() && parsedOrigin.protocol !== "https:") return null;
190
+ const hasCustomPort = parsedOrigin.protocol === "http:" && parsedOrigin.port !== "" && parsedOrigin.port !== "80" || parsedOrigin.protocol === "https:" && parsedOrigin.port !== "" && parsedOrigin.port !== "443";
191
+ if (hasCustomPort) return null;
192
+ return normalizedOrigin;
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+ function isStateChangingMethod(method) {
198
+ return STATE_CHANGING_METHODS.has(method.toUpperCase());
199
+ }
200
+ function mergeVaryHeader(headers, value) {
201
+ const existing = (headers.get("Vary") || "").trim();
202
+ if (!existing) {
203
+ headers.set("Vary", value);
204
+ return;
205
+ }
206
+ const tokens = existing.split(",").map((token) => token.trim()).filter(Boolean);
207
+ if (tokens.some((token) => token === "*")) {
208
+ headers.set("Vary", "*");
209
+ return;
210
+ }
211
+ const hasValue = tokens.some((token) => token.toLowerCase() === value.toLowerCase());
212
+ if (hasValue) {
213
+ headers.set("Vary", tokens.join(", "));
214
+ return;
215
+ }
216
+ headers.set("Vary", [...tokens, value].join(", "));
217
+ }
218
+ function hasUntrustedFetchSite(req) {
219
+ const fetchSite = (req.headers.get("sec-fetch-site") || "").trim().toLowerCase();
220
+ if (!fetchSite) return false;
221
+ return !(fetchSite === "same-origin" || fetchSite === "same-site" || fetchSite === "none");
222
+ }
223
+ function isTrustedOrigin(req) {
224
+ const origin = (req.headers.get("origin") || "").trim();
225
+ if (!origin) return false;
226
+ const normalizedOrigin = normalizeOrigin(origin);
227
+ if (!normalizedOrigin) return false;
228
+ const requestOrigin = req.nextUrl.origin;
229
+ if (normalizedOrigin === requestOrigin) return true;
230
+ const allowedOrigin = resolveAllowedOrigin(req);
231
+ return !!allowedOrigin && allowedOrigin === normalizedOrigin;
232
+ }
233
+ function extractOriginFromReferer(req) {
234
+ const referer = (req.headers.get("referer") || "").trim();
235
+ if (!referer) return null;
236
+ return normalizeOrigin(referer);
237
+ }
238
+ function isTrustedReferer(req) {
239
+ const refererOrigin = extractOriginFromReferer(req);
240
+ if (!refererOrigin) return false;
241
+ if (refererOrigin === req.nextUrl.origin) return true;
242
+ const allowedOrigin = resolveAllowedOrigin(req);
243
+ return !!allowedOrigin && allowedOrigin === refererOrigin;
244
+ }
245
+ function shouldRejectByCsrfProtection(req, method, pathKey) {
246
+ const csrfExemptPaths = getCsrfExemptPaths();
247
+ if (!isStateChangingMethod(method)) return false;
248
+ if (csrfExemptPaths.has(pathKey.toLowerCase())) return false;
249
+ if (hasUntrustedFetchSite(req)) return true;
250
+ if (isTrustedOrigin(req)) return false;
251
+ if (isTrustedReferer(req)) return false;
252
+ return true;
253
+ }
254
+ function applyCorsHeaders(headers, req, isIpAccess) {
255
+ const allowedOrigin = resolveAllowedOrigin(req);
256
+ const corsHeaders = getFrameworkConfig2().proxy.corsAllowedHeaders;
257
+ if (isIpAccess) {
258
+ if (allowedOrigin) {
259
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
260
+ }
261
+ headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
262
+ headers.set("Access-Control-Allow-Headers", corsHeaders);
263
+ headers.set("Access-Control-Max-Age", "86400");
264
+ if (allowedOrigin) {
265
+ headers.set("Access-Control-Allow-Credentials", "true");
266
+ } else {
267
+ headers.delete("Access-Control-Allow-Credentials");
268
+ }
269
+ mergeVaryHeader(headers, "Origin");
270
+ return;
271
+ }
272
+ if (allowedOrigin) {
273
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
274
+ headers.set("Access-Control-Allow-Credentials", "true");
275
+ } else {
276
+ headers.delete("Access-Control-Allow-Credentials");
277
+ }
278
+ mergeVaryHeader(headers, "Origin");
279
+ headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
280
+ headers.set("Access-Control-Allow-Headers", corsHeaders);
281
+ headers.set("Access-Control-Max-Age", "86400");
282
+ }
283
+ function hasCsrfExemptPaths() {
284
+ return getCsrfExemptPaths().size > 0;
285
+ }
286
+ function resolveProdSecurityConfigError() {
287
+ if (!isProdRuntime()) return null;
288
+ if (hasCsrfExemptPaths()) return "CSRF_EXEMPT_PATHS is not allowed in production";
289
+ return null;
290
+ }
291
+
292
+ // src/handlers/rate-limit-handler.ts
293
+ function toValidPositiveInteger(value, fallback) {
294
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
295
+ }
296
+ function isRateLimitEnabled() {
297
+ return process.env.AUTH_LOGIN_RATE_LIMIT_ENABLED !== "false";
298
+ }
299
+ function trustProxyHeaders2() {
300
+ return process.env.AUTH_TRUST_PROXY_HEADERS === "true";
301
+ }
302
+ function getHeader(request, name) {
303
+ return (request.headers.get(name) || "").trim();
304
+ }
305
+ function buildClientFingerprint(request) {
306
+ const userAgent = getHeader(request, "user-agent").toLowerCase().slice(0, 64);
307
+ const acceptLanguage = getHeader(request, "accept-language").toLowerCase().slice(0, 32);
308
+ const secChUa = getHeader(request, "sec-ch-ua").toLowerCase().slice(0, 64);
309
+ const signal = [userAgent, acceptLanguage, secChUa].filter(Boolean).join("|");
310
+ if (!signal) return "unknown";
311
+ return `fp:${signal}`.slice(0, 128);
312
+ }
313
+ function getPolicy() {
314
+ return {
315
+ windowMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_WINDOW_MS || "900000"), 9e5),
316
+ blockMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_BLOCK_MS || "900000"), 9e5),
317
+ maxAttemptsByIpAndAccount: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS || "8"), 8),
318
+ maxAttemptsByAccount: toValidPositiveInteger(
319
+ Number(process.env.AUTH_LOGIN_RATE_LIMIT_ACCOUNT_MAX_ATTEMPTS || "20"),
320
+ 20
321
+ ),
322
+ maxKeys: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_KEYS || "10000"), 1e4)
323
+ };
324
+ }
325
+ function getClientAddress(request) {
326
+ if (trustProxyHeaders2()) {
327
+ const xForwardedFor = getHeader(request, "x-forwarded-for");
328
+ const first = xForwardedFor.split(",")[0]?.trim();
329
+ if (first) return first;
330
+ const xRealIp = getHeader(request, "x-real-ip");
331
+ if (xRealIp) return xRealIp;
332
+ }
333
+ const directIp = request.ip?.trim();
334
+ if (directIp) return directIp;
335
+ return buildClientFingerprint(request);
336
+ }
337
+ function normalizeRateLimitKeyPart(value) {
338
+ const trimmed = value.trim().toLowerCase();
339
+ if (!trimmed) return "unknown";
340
+ return trimmed.slice(0, 128);
341
+ }
342
+ function getLoginRateLimitKeys(request, username) {
343
+ const normalizedUsername = normalizeRateLimitKeyPart(username);
344
+ const normalizedIp = normalizeRateLimitKeyPart(getClientAddress(request));
345
+ const pairKey = `${normalizedIp}:${normalizedUsername}`;
346
+ const accountKey = normalizedUsername !== "unknown" ? normalizedUsername : null;
347
+ return { pairKey, accountKey };
348
+ }
349
+ function cleanupExpiredLoginLimits(now) {
350
+ const policy = getPolicy();
351
+ const adapter = getRateLimiterAdapter();
352
+ for (const store of ["ip", "account"]) {
353
+ if (typeof adapter.sweepExpired === "function") {
354
+ adapter.sweepExpired(store, now, policy.windowMs);
355
+ }
356
+ const size = adapter.size(store);
357
+ if (size > policy.maxKeys) {
358
+ adapter.evictOldest(store, size - policy.maxKeys);
359
+ }
360
+ }
361
+ }
362
+ function getRateLimitRetryAfterMs(keys, now) {
363
+ if (!isRateLimitEnabled()) return 0;
364
+ const policy = getPolicy();
365
+ const result = checkRateLimit(keys.pairKey, keys.accountKey, policy);
366
+ if (!result) return 0;
367
+ const adapter = getRateLimiterAdapter();
368
+ const ipEntry = adapter.get("ip", keys.pairKey);
369
+ const accountEntry = keys.accountKey ? adapter.get("account", keys.accountKey) : null;
370
+ const ipRetry = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil - now : 0;
371
+ const accountRetry = accountEntry && accountEntry.blockedUntil > now ? accountEntry.blockedUntil - now : 0;
372
+ return Math.max(ipRetry, accountRetry);
373
+ }
374
+ function registerFailedLoginAttempt(keys, _now) {
375
+ if (!isRateLimitEnabled()) return;
376
+ const policy = getPolicy();
377
+ recordFailedAttempt(keys.pairKey, keys.accountKey, policy);
378
+ }
379
+ function clearLoginAttemptState(keys) {
380
+ if (!isRateLimitEnabled()) return;
381
+ clearRateLimitEntries(keys.pairKey, keys.accountKey);
382
+ }
383
+
384
+ export {
385
+ isLocalHostRequest,
386
+ useSecureCookies,
387
+ resolveSessionCookieName,
388
+ setSessionCookie,
389
+ clearSessionCookie,
390
+ getSessionToken,
391
+ isSessionExpiredCode,
392
+ shouldClearSessionFromForbidden,
393
+ normalizeOrigin,
394
+ isInternalIpAccess,
395
+ resolveAllowedOrigin,
396
+ shouldRejectByCsrfProtection,
397
+ applyCorsHeaders,
398
+ hasCsrfExemptPaths,
399
+ resolveProdSecurityConfigError,
400
+ getClientAddress,
401
+ getLoginRateLimitKeys,
402
+ cleanupExpiredLoginLimits,
403
+ getRateLimitRetryAfterMs,
404
+ registerFailedLoginAttempt,
405
+ clearLoginAttemptState
406
+ };
407
+ //# sourceMappingURL=chunk-FEB3UZEG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/handlers/auth-session.ts","../src/handlers/csrf-cors.ts","../src/handlers/rate-limit-handler.ts"],"sourcesContent":["/**\n * Session/cookie management for the API proxy.\n *\n * Handles session token extraction from cookies, secure cookie setting/clearing,\n * and session expiry detection from API responses.\n *\n * @module handlers/auth-session\n */\n\nimport { getFrameworkConfig, getSecureSessionCookieName, getSessionCookieName } from \"@spring-systems/core/config\";\nimport type { NextRequest, NextResponse } from \"next/server\";\n\n/** Hard upper bound for session cookie lifetime (7 days). */\nconst MAX_SESSION_AGE_SECONDS = 604800;\n\nfunction getSessionCookieMaxAgeSeconds(): number {\n return Number(process.env.AUTH_COOKIE_MAX_AGE_SECONDS || \"28800\");\n}\n\nfunction isTargetProdRuntime(): boolean {\n return (process.env.TARGET_ENV || \"\") === \"prod\";\n}\n\n// Config accessors — read fresh from getFrameworkConfig() on each call so\n// configureFramework() can be called after module import.\nfunction getSessionCookieNameLazy(): string {\n return getSessionCookieName(getFrameworkConfig().auth.sessionCookiePrefix);\n}\n\nfunction getSecureSessionCookieNameLazy(): string {\n return getSecureSessionCookieName(getFrameworkConfig().auth.sessionCookiePrefix);\n}\n\nfunction getSessionExpiredCodeMarkers(): Set<string> {\n return new Set(getFrameworkConfig().auth.sessionExpiredCodes);\n}\n\ninterface AuthFailurePayload {\n message?: string;\n error?: string;\n detail?: string;\n title?: string;\n code?: string;\n error_code?: string;\n}\n\n/** Check if a request targets localhost. */\nexport function isLocalHostRequest(req: NextRequest): boolean {\n return (\n req.nextUrl.hostname === \"localhost\" ||\n req.nextUrl.hostname === \"127.0.0.1\" ||\n req.nextUrl.hostname === \"::1\" ||\n req.nextUrl.hostname === \"[::1]\"\n );\n}\n\n/** Whether to use Secure flag on cookies (HTTPS or TARGET_ENV=prod, excluding localhost). */\nexport function useSecureCookies(req: NextRequest): boolean {\n if (isLocalHostRequest(req)) return false;\n if (isTargetProdRuntime()) return true;\n // Non-prod: secure cookies when the request arrived over HTTPS\n return req.nextUrl.protocol === \"https:\";\n}\n\n/** Resolve the correct session cookie name based on runtime context. */\nexport function resolveSessionCookieName(req: NextRequest): string {\n return useSecureCookies(req) ? getSecureSessionCookieNameLazy() : getSessionCookieNameLazy();\n}\n\nfunction setCookieWithName(res: NextResponse, req: NextRequest, cookieName: string, value: string, maxAge: number) {\n const secure = useSecureCookies(req);\n res.cookies.set({\n name: cookieName,\n value,\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path: \"/\",\n maxAge,\n priority: \"high\",\n });\n}\n\n/** Set the session cookie with the given token. Clears the stale variant. */\nexport function setSessionCookie(res: NextResponse, req: NextRequest, token: string): void {\n const configuredMaxAge = getSessionCookieMaxAgeSeconds();\n const rawAge =\n Number.isFinite(configuredMaxAge) && configuredMaxAge > 0\n ? Math.floor(configuredMaxAge)\n : 28800;\n const maxAge = Math.min(rawAge, MAX_SESSION_AGE_SECONDS);\n const cookieName = resolveSessionCookieName(req);\n const staleCookieName = cookieName === getSessionCookieNameLazy() ? getSecureSessionCookieNameLazy() : getSessionCookieNameLazy();\n\n setCookieWithName(res, req, cookieName, token, maxAge);\n setCookieWithName(res, req, staleCookieName, \"\", 0);\n}\n\nfunction clearSessionCookieWithName(res: NextResponse, req: NextRequest, cookieName: string) {\n const secure = useSecureCookies(req);\n res.cookies.set({\n name: cookieName,\n value: \"\",\n httpOnly: true,\n secure,\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 0,\n priority: \"high\",\n });\n\n // Also append the opposite Secure variant to clear cookies created under a different\n // runtime/proxy setup (e.g. switching between http/https during development).\n const oppositeSecure = !secure;\n const cookieParts = [`${cookieName}=`, \"Path=/\", \"Max-Age=0\", \"HttpOnly\", \"SameSite=Lax\", \"Priority=High\"];\n if (oppositeSecure) {\n cookieParts.push(\"Secure\");\n }\n res.headers.append(\"Set-Cookie\", cookieParts.join(\"; \"));\n}\n\n/** Clear all session cookies (both regular and secure variants). */\nexport function clearSessionCookie(res: NextResponse, req: NextRequest): void {\n clearSessionCookieWithName(res, req, getSessionCookieNameLazy());\n clearSessionCookieWithName(res, req, getSecureSessionCookieNameLazy());\n}\n\n/** Extract session token from request cookies (tries both cookie names). */\nexport function getSessionToken(req: NextRequest): string {\n const cookies = [getSessionCookieNameLazy(), getSecureSessionCookieNameLazy()];\n for (const cookieName of cookies) {\n const fromCookiesApi = (req.cookies.get(cookieName)?.value || \"\").trim();\n if (fromCookiesApi) return fromCookiesApi;\n }\n\n const cookieHeader = req.headers.get(\"cookie\") || \"\";\n if (!cookieHeader) return \"\";\n\n const parts = cookieHeader.split(\";\").map((part) => part.trim());\n for (const part of parts) {\n if (!part) continue;\n const eqIndex = part.indexOf(\"=\");\n if (eqIndex <= 0) continue;\n const key = part.slice(0, eqIndex).trim();\n if (key !== getSessionCookieNameLazy() && key !== getSecureSessionCookieNameLazy()) continue;\n const rawValue = part.slice(eqIndex + 1).trim();\n if (!rawValue) continue;\n try {\n return decodeURIComponent(rawValue);\n } catch {\n return rawValue;\n }\n }\n\n return \"\";\n}\n\nfunction normalizeErrorText(value: unknown): string {\n return typeof value === \"string\" ? value.trim().toLowerCase() : \"\";\n}\n\n/** Check if a value is a session-expired error code. */\nexport function isSessionExpiredCode(value: unknown): boolean {\n const normalized = normalizeErrorText(value);\n return normalized !== \"\" && getSessionExpiredCodeMarkers().has(normalized);\n}\n\n/** Detect if a 403 response indicates session expiry (reads response body). */\nexport async function shouldClearSessionFromForbidden(response: Response): Promise<boolean> {\n if (response.status !== 403) return false;\n\n try {\n const cloned = response.clone();\n try {\n const payload = (await cloned.json()) as AuthFailurePayload;\n return isSessionExpiredCode(payload.code) || isSessionExpiredCode(payload.error_code);\n } catch {\n return false;\n }\n } catch {\n return false;\n }\n}\n","/**\n * CSRF protection and CORS header management for the API proxy.\n *\n * Validates request origins against configured allowed origins,\n * applies CORS headers to responses, and enforces CSRF protection\n * for state-changing HTTP methods.\n *\n * @module handlers/csrf-cors\n */\n\nimport { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport type { NextRequest } from \"next/server\";\n\nconst STATE_CHANGING_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\nfunction getFrontendUrl(): string {\n return (process.env.FRONTEND_URL || \"\").trim();\n}\n\nfunction getFrontendIp(): string {\n return (process.env.FRONTEND_IP || \"\").trim();\n}\n\nfunction trustProxyHeaders(): boolean {\n return process.env.AUTH_TRUST_PROXY_HEADERS === \"true\";\n}\n\nfunction getCsrfExemptPaths(): Set<string> {\n return new Set(\n (process.env.CSRF_EXEMPT_PATHS || \"\")\n .split(\",\")\n .map((v) =>\n v\n .trim()\n .replace(/^\\/+|\\/+$/g, \"\")\n .toLowerCase()\n )\n .filter(Boolean)\n );\n}\n\nfunction isProdRuntime(): boolean {\n return (process.env.NODE_ENV || \"\") === \"production\" || (process.env.TARGET_ENV || \"\") === \"prod\";\n}\n\n/** Normalize a URL string to its origin (scheme + host + port). */\nexport function normalizeOrigin(value: string): string | null {\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n}\n\n/** Check if the request comes from the configured FRONTEND_IP. */\nexport function isInternalIpAccess(req: NextRequest): boolean {\n const frontendIp = getFrontendIp();\n if (!frontendIp) return false;\n\n const directIp = (req as unknown as { ip?: string }).ip?.trim();\n if (directIp) return directIp === frontendIp;\n\n if (!trustProxyHeaders()) {\n return false;\n }\n\n const xForwardedFor = (req.headers.get(\"x-forwarded-for\") || \"\").trim();\n const forwardedFirstIp = xForwardedFor.split(\",\")[0]?.trim();\n if (forwardedFirstIp) return forwardedFirstIp === frontendIp;\n\n const xRealIp = (req.headers.get(\"x-real-ip\") || \"\").trim();\n if (xRealIp) return xRealIp === frontendIp;\n\n return false;\n}\n\n/** Resolve the allowed origin for a request (checks FRONTEND_URL and FRONTEND_IP). */\nexport function resolveAllowedOrigin(req: NextRequest): string | null {\n const origin = (req.headers.get(\"origin\") || \"\").trim();\n if (!origin) return null;\n\n const normalizedOrigin = normalizeOrigin(origin);\n if (!normalizedOrigin) return null;\n\n const frontendUrl = getFrontendUrl();\n const normalizedFrontendOrigin = frontendUrl ? normalizeOrigin(frontendUrl) : null;\n if (normalizedFrontendOrigin && normalizedOrigin === normalizedFrontendOrigin) {\n return normalizedOrigin;\n }\n\n const frontendIp = getFrontendIp();\n if (!frontendIp) return null;\n\n try {\n const parsedOrigin = new URL(normalizedOrigin);\n if (parsedOrigin.hostname !== frontendIp) return null;\n if (![\"http:\", \"https:\"].includes(parsedOrigin.protocol)) return null;\n if (isProdRuntime() && parsedOrigin.protocol !== \"https:\") return null;\n // FRONTEND_IP is treated as host-only allowlist. If a custom port is needed,\n // use FRONTEND_URL to pin an exact origin.\n const hasCustomPort =\n (parsedOrigin.protocol === \"http:\" && parsedOrigin.port !== \"\" && parsedOrigin.port !== \"80\") ||\n (parsedOrigin.protocol === \"https:\" && parsedOrigin.port !== \"\" && parsedOrigin.port !== \"443\");\n if (hasCustomPort) return null;\n return normalizedOrigin;\n } catch {\n return null;\n }\n}\n\nfunction isStateChangingMethod(method: string): boolean {\n return STATE_CHANGING_METHODS.has(method.toUpperCase());\n}\n\nfunction mergeVaryHeader(headers: Headers, value: string): void {\n const existing = (headers.get(\"Vary\") || \"\").trim();\n if (!existing) {\n headers.set(\"Vary\", value);\n return;\n }\n\n const tokens = existing\n .split(\",\")\n .map((token) => token.trim())\n .filter(Boolean);\n\n // RFC: wildcard Vary must stand alone.\n if (tokens.some((token) => token === \"*\")) {\n headers.set(\"Vary\", \"*\");\n return;\n }\n\n const hasValue = tokens.some((token) => token.toLowerCase() === value.toLowerCase());\n if (hasValue) {\n headers.set(\"Vary\", tokens.join(\", \"));\n return;\n }\n\n headers.set(\"Vary\", [...tokens, value].join(\", \"));\n}\n\n/**\n * Check whether the Sec-Fetch-Site header indicates a cross-site request.\n *\n * When the header is **missing** (older browsers, non-browser clients, or proxies\n * that strip it), we return `false` (not untrusted) intentionally.\n * This is safe because `shouldRejectByCsrfProtection` always falls through to\n * origin and referer validation when this function returns false, so requests\n * without the header are still verified against the trusted origin/referer list\n * before being allowed through.\n */\nfunction hasUntrustedFetchSite(req: NextRequest): boolean {\n const fetchSite = (req.headers.get(\"sec-fetch-site\") || \"\").trim().toLowerCase();\n // Missing header: allow — origin/referer validation in shouldRejectByCsrfProtection\n // provides the safety net for state-changing methods (POST/PUT/DELETE/PATCH).\n if (!fetchSite) return false;\n return !(fetchSite === \"same-origin\" || fetchSite === \"same-site\" || fetchSite === \"none\");\n}\n\nfunction isTrustedOrigin(req: NextRequest): boolean {\n const origin = (req.headers.get(\"origin\") || \"\").trim();\n if (!origin) return false;\n\n const normalizedOrigin = normalizeOrigin(origin);\n if (!normalizedOrigin) return false;\n\n const requestOrigin = req.nextUrl.origin;\n if (normalizedOrigin === requestOrigin) return true;\n\n const allowedOrigin = resolveAllowedOrigin(req);\n return !!allowedOrigin && allowedOrigin === normalizedOrigin;\n}\n\nfunction extractOriginFromReferer(req: NextRequest): string | null {\n const referer = (req.headers.get(\"referer\") || \"\").trim();\n if (!referer) return null;\n return normalizeOrigin(referer);\n}\n\nfunction isTrustedReferer(req: NextRequest): boolean {\n const refererOrigin = extractOriginFromReferer(req);\n if (!refererOrigin) return false;\n\n if (refererOrigin === req.nextUrl.origin) return true;\n\n const allowedOrigin = resolveAllowedOrigin(req);\n return !!allowedOrigin && allowedOrigin === refererOrigin;\n}\n\n/** Check if a request should be rejected by CSRF protection. */\nexport function shouldRejectByCsrfProtection(req: NextRequest, method: string, pathKey: string): boolean {\n const csrfExemptPaths = getCsrfExemptPaths();\n if (!isStateChangingMethod(method)) return false;\n if (csrfExemptPaths.has(pathKey.toLowerCase())) return false;\n if (hasUntrustedFetchSite(req)) return true;\n if (isTrustedOrigin(req)) return false;\n if (isTrustedReferer(req)) return false;\n return true;\n}\n\n/** Apply CORS headers to a response. */\nexport function applyCorsHeaders(headers: Headers, req: NextRequest, isIpAccess: boolean): void {\n const allowedOrigin = resolveAllowedOrigin(req);\n const corsHeaders = getFrameworkConfig().proxy.corsAllowedHeaders;\n if (isIpAccess) {\n if (allowedOrigin) {\n headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n }\n headers.set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, PATCH, OPTIONS\");\n headers.set(\"Access-Control-Allow-Headers\", corsHeaders);\n headers.set(\"Access-Control-Max-Age\", \"86400\");\n if (allowedOrigin) {\n headers.set(\"Access-Control-Allow-Credentials\", \"true\");\n } else {\n headers.delete(\"Access-Control-Allow-Credentials\");\n }\n mergeVaryHeader(headers, \"Origin\");\n return;\n }\n if (allowedOrigin) {\n headers.set(\"Access-Control-Allow-Origin\", allowedOrigin);\n headers.set(\"Access-Control-Allow-Credentials\", \"true\");\n } else {\n headers.delete(\"Access-Control-Allow-Credentials\");\n }\n mergeVaryHeader(headers, \"Origin\");\n headers.set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, PATCH, OPTIONS\");\n headers.set(\"Access-Control-Allow-Headers\", corsHeaders);\n headers.set(\"Access-Control-Max-Age\", \"86400\");\n}\n\n/** Check if CSRF exempt paths are configured (disallowed in production). */\nexport function hasCsrfExemptPaths(): boolean {\n return getCsrfExemptPaths().size > 0;\n}\n\n/** Validate production security config. Returns error string or null. */\nexport function resolveProdSecurityConfigError(): string | null {\n if (!isProdRuntime()) return null;\n if (hasCsrfExemptPaths()) return \"CSRF_EXEMPT_PATHS is not allowed in production\";\n return null;\n}\n","/**\n * Login rate limiting logic for the API proxy.\n *\n * Uses the pluggable RateLimiterAdapter from rate-limiter.ts so multi-instance\n * deployments can swap in a shared-storage adapter.\n *\n * @module handlers/rate-limit-handler\n */\n\nimport type { NextRequest } from \"next/server\";\n\nimport {\n checkRateLimit,\n clearRateLimitEntries,\n getRateLimiterAdapter,\n type RateLimitEntry,\n type RateLimitPolicy,\n recordFailedAttempt,\n} from \"../rate-limiter\";\n\nexport { type RateLimitEntry };\n\nexport interface RateLimitKeys {\n pairKey: string;\n accountKey: string | null;\n}\n\nfunction toValidPositiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n}\n\nfunction isRateLimitEnabled(): boolean {\n return process.env.AUTH_LOGIN_RATE_LIMIT_ENABLED !== \"false\";\n}\n\nfunction trustProxyHeaders(): boolean {\n return process.env.AUTH_TRUST_PROXY_HEADERS === \"true\";\n}\n\nfunction getHeader(request: NextRequest, name: string): string {\n return (request.headers.get(name) || \"\").trim();\n}\n\nfunction buildClientFingerprint(request: NextRequest): string {\n const userAgent = getHeader(request, \"user-agent\").toLowerCase().slice(0, 64);\n const acceptLanguage = getHeader(request, \"accept-language\").toLowerCase().slice(0, 32);\n const secChUa = getHeader(request, \"sec-ch-ua\").toLowerCase().slice(0, 64);\n const signal = [userAgent, acceptLanguage, secChUa].filter(Boolean).join(\"|\");\n if (!signal) return \"unknown\";\n return `fp:${signal}`.slice(0, 128);\n}\n\nfunction getPolicy(): RateLimitPolicy {\n return {\n windowMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_WINDOW_MS || \"900000\"), 900_000),\n blockMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_BLOCK_MS || \"900000\"), 900_000),\n maxAttemptsByIpAndAccount: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS || \"8\"), 8),\n maxAttemptsByAccount: toValidPositiveInteger(\n Number(process.env.AUTH_LOGIN_RATE_LIMIT_ACCOUNT_MAX_ATTEMPTS || \"20\"),\n 20\n ),\n maxKeys: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_KEYS || \"10000\"), 10_000),\n };\n}\n\n/** Get client IP address from request headers or connection. */\nexport function getClientAddress(request: NextRequest): string {\n if (trustProxyHeaders()) {\n const xForwardedFor = getHeader(request, \"x-forwarded-for\");\n const first = xForwardedFor.split(\",\")[0]?.trim();\n if (first) return first;\n\n const xRealIp = getHeader(request, \"x-real-ip\");\n if (xRealIp) return xRealIp;\n }\n\n // Next.js does not expose the raw socket IP in NextRequest.\n // request.ip is available in some runtimes; otherwise use a soft fingerprint\n // to avoid collapsing all clients into one \"unknown\" key.\n const directIp = (request as unknown as { ip?: string }).ip?.trim();\n if (directIp) return directIp;\n return buildClientFingerprint(request);\n}\n\nfunction normalizeRateLimitKeyPart(value: string): string {\n const trimmed = value.trim().toLowerCase();\n if (!trimmed) return \"unknown\";\n return trimmed.slice(0, 128);\n}\n\n/** Build rate limit keys from request and username. */\nexport function getLoginRateLimitKeys(request: NextRequest, username: string): RateLimitKeys {\n const normalizedUsername = normalizeRateLimitKeyPart(username);\n const normalizedIp = normalizeRateLimitKeyPart(getClientAddress(request));\n const pairKey = `${normalizedIp}:${normalizedUsername}`;\n const accountKey = normalizedUsername !== \"unknown\" ? normalizedUsername : null;\n return { pairKey, accountKey };\n}\n\n/** Remove expired rate limit entries from adapter stores. */\nexport function cleanupExpiredLoginLimits(now: number): void {\n const policy = getPolicy();\n const adapter = getRateLimiterAdapter();\n\n for (const store of [\"ip\", \"account\"] as const) {\n if (typeof adapter.sweepExpired === \"function\") {\n adapter.sweepExpired(store, now, policy.windowMs);\n }\n\n const size = adapter.size(store);\n if (size > policy.maxKeys) {\n adapter.evictOldest(store, size - policy.maxKeys);\n }\n }\n}\n\n/** Get remaining block time in ms (0 = not blocked). */\nexport function getRateLimitRetryAfterMs(keys: RateLimitKeys, now: number): number {\n if (!isRateLimitEnabled()) return 0;\n const policy = getPolicy();\n const result = checkRateLimit(keys.pairKey, keys.accountKey, policy);\n if (!result) return 0;\n\n // Extract retry-after from adapter entries\n const adapter = getRateLimiterAdapter();\n const ipEntry = adapter.get(\"ip\", keys.pairKey);\n const accountEntry = keys.accountKey ? adapter.get(\"account\", keys.accountKey) : null;\n const ipRetry = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil - now : 0;\n const accountRetry = accountEntry && accountEntry.blockedUntil > now ? accountEntry.blockedUntil - now : 0;\n return Math.max(ipRetry, accountRetry);\n}\n\n/** Record a failed login attempt for rate limiting. */\nexport function registerFailedLoginAttempt(keys: RateLimitKeys, _now: number): void {\n if (!isRateLimitEnabled()) return;\n const policy = getPolicy();\n recordFailedAttempt(keys.pairKey, keys.accountKey, policy);\n}\n\n/** Clear rate limit state for given keys (after successful login). */\nexport function clearLoginAttemptState(keys: RateLimitKeys): void {\n if (!isRateLimitEnabled()) return;\n clearRateLimitEntries(keys.pairKey, keys.accountKey);\n}\n"],"mappings":";;;;;;;;AASA,SAAS,oBAAoB,4BAA4B,4BAA4B;AAIrF,IAAM,0BAA0B;AAEhC,SAAS,gCAAwC;AAC7C,SAAO,OAAO,QAAQ,IAAI,+BAA+B,OAAO;AACpE;AAEA,SAAS,sBAA+B;AACpC,UAAQ,QAAQ,IAAI,cAAc,QAAQ;AAC9C;AAIA,SAAS,2BAAmC;AACxC,SAAO,qBAAqB,mBAAmB,EAAE,KAAK,mBAAmB;AAC7E;AAEA,SAAS,iCAAyC;AAC9C,SAAO,2BAA2B,mBAAmB,EAAE,KAAK,mBAAmB;AACnF;AAEA,SAAS,+BAA4C;AACjD,SAAO,IAAI,IAAI,mBAAmB,EAAE,KAAK,mBAAmB;AAChE;AAYO,SAAS,mBAAmB,KAA2B;AAC1D,SACI,IAAI,QAAQ,aAAa,eACzB,IAAI,QAAQ,aAAa,eACzB,IAAI,QAAQ,aAAa,SACzB,IAAI,QAAQ,aAAa;AAEjC;AAGO,SAAS,iBAAiB,KAA2B;AACxD,MAAI,mBAAmB,GAAG,EAAG,QAAO;AACpC,MAAI,oBAAoB,EAAG,QAAO;AAElC,SAAO,IAAI,QAAQ,aAAa;AACpC;AAGO,SAAS,yBAAyB,KAA0B;AAC/D,SAAO,iBAAiB,GAAG,IAAI,+BAA+B,IAAI,yBAAyB;AAC/F;AAEA,SAAS,kBAAkB,KAAmB,KAAkB,YAAoB,OAAe,QAAgB;AAC/G,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,QAAQ,IAAI;AAAA,IACZ,MAAM;AAAA,IACN;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA,UAAU;AAAA,IACV,MAAM;AAAA,IACN;AAAA,IACA,UAAU;AAAA,EACd,CAAC;AACL;AAGO,SAAS,iBAAiB,KAAmB,KAAkB,OAAqB;AACvF,QAAM,mBAAmB,8BAA8B;AACvD,QAAM,SACF,OAAO,SAAS,gBAAgB,KAAK,mBAAmB,IAClD,KAAK,MAAM,gBAAgB,IAC3B;AACV,QAAM,SAAS,KAAK,IAAI,QAAQ,uBAAuB;AACvD,QAAM,aAAa,yBAAyB,GAAG;AAC/C,QAAM,kBAAkB,eAAe,yBAAyB,IAAI,+BAA+B,IAAI,yBAAyB;AAEhI,oBAAkB,KAAK,KAAK,YAAY,OAAO,MAAM;AACrD,oBAAkB,KAAK,KAAK,iBAAiB,IAAI,CAAC;AACtD;AAEA,SAAS,2BAA2B,KAAmB,KAAkB,YAAoB;AACzF,QAAM,SAAS,iBAAiB,GAAG;AACnC,MAAI,QAAQ,IAAI;AAAA,IACZ,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA,UAAU;AAAA,IACV,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,EACd,CAAC;AAID,QAAM,iBAAiB,CAAC;AACxB,QAAM,cAAc,CAAC,GAAG,UAAU,KAAK,UAAU,aAAa,YAAY,gBAAgB,eAAe;AACzG,MAAI,gBAAgB;AAChB,gBAAY,KAAK,QAAQ;AAAA,EAC7B;AACA,MAAI,QAAQ,OAAO,cAAc,YAAY,KAAK,IAAI,CAAC;AAC3D;AAGO,SAAS,mBAAmB,KAAmB,KAAwB;AAC1E,6BAA2B,KAAK,KAAK,yBAAyB,CAAC;AAC/D,6BAA2B,KAAK,KAAK,+BAA+B,CAAC;AACzE;AAGO,SAAS,gBAAgB,KAA0B;AACtD,QAAM,UAAU,CAAC,yBAAyB,GAAG,+BAA+B,CAAC;AAC7E,aAAW,cAAc,SAAS;AAC9B,UAAM,kBAAkB,IAAI,QAAQ,IAAI,UAAU,GAAG,SAAS,IAAI,KAAK;AACvE,QAAI,eAAgB,QAAO;AAAA,EAC/B;AAEA,QAAM,eAAe,IAAI,QAAQ,IAAI,QAAQ,KAAK;AAClD,MAAI,CAAC,aAAc,QAAO;AAE1B,QAAM,QAAQ,aAAa,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC;AAC/D,aAAW,QAAQ,OAAO;AACtB,QAAI,CAAC,KAAM;AACX,UAAM,UAAU,KAAK,QAAQ,GAAG;AAChC,QAAI,WAAW,EAAG;AAClB,UAAM,MAAM,KAAK,MAAM,GAAG,OAAO,EAAE,KAAK;AACxC,QAAI,QAAQ,yBAAyB,KAAK,QAAQ,+BAA+B,EAAG;AACpF,UAAM,WAAW,KAAK,MAAM,UAAU,CAAC,EAAE,KAAK;AAC9C,QAAI,CAAC,SAAU;AACf,QAAI;AACA,aAAO,mBAAmB,QAAQ;AAAA,IACtC,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ;AAEA,SAAO;AACX;AAEA,SAAS,mBAAmB,OAAwB;AAChD,SAAO,OAAO,UAAU,WAAW,MAAM,KAAK,EAAE,YAAY,IAAI;AACpE;AAGO,SAAS,qBAAqB,OAAyB;AAC1D,QAAM,aAAa,mBAAmB,KAAK;AAC3C,SAAO,eAAe,MAAM,6BAA6B,EAAE,IAAI,UAAU;AAC7E;AAGA,eAAsB,gCAAgC,UAAsC;AACxF,MAAI,SAAS,WAAW,IAAK,QAAO;AAEpC,MAAI;AACA,UAAM,SAAS,SAAS,MAAM;AAC9B,QAAI;AACA,YAAM,UAAW,MAAM,OAAO,KAAK;AACnC,aAAO,qBAAqB,QAAQ,IAAI,KAAK,qBAAqB,QAAQ,UAAU;AAAA,IACxF,QAAQ;AACJ,aAAO;AAAA,IACX;AAAA,EACJ,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;;;AC5KA,SAAS,sBAAAA,2BAA0B;AAGnC,IAAM,yBAAyB,oBAAI,IAAI,CAAC,QAAQ,OAAO,SAAS,QAAQ,CAAC;AAEzE,SAAS,iBAAyB;AAC9B,UAAQ,QAAQ,IAAI,gBAAgB,IAAI,KAAK;AACjD;AAEA,SAAS,gBAAwB;AAC7B,UAAQ,QAAQ,IAAI,eAAe,IAAI,KAAK;AAChD;AAEA,SAAS,oBAA6B;AAClC,SAAO,QAAQ,IAAI,6BAA6B;AACpD;AAEA,SAAS,qBAAkC;AACvC,SAAO,IAAI;AAAA,KACN,QAAQ,IAAI,qBAAqB,IAC7B,MAAM,GAAG,EACT;AAAA,MAAI,CAAC,MACF,EACK,KAAK,EACL,QAAQ,cAAc,EAAE,EACxB,YAAY;AAAA,IACrB,EACC,OAAO,OAAO;AAAA,EACvB;AACJ;AAEA,SAAS,gBAAyB;AAC9B,UAAQ,QAAQ,IAAI,YAAY,QAAQ,iBAAiB,QAAQ,IAAI,cAAc,QAAQ;AAC/F;AAGO,SAAS,gBAAgB,OAA8B;AAC1D,MAAI;AACA,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EAC1B,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAGO,SAAS,mBAAmB,KAA2B;AAC1D,QAAM,aAAa,cAAc;AACjC,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,WAAY,IAAmC,IAAI,KAAK;AAC9D,MAAI,SAAU,QAAO,aAAa;AAElC,MAAI,CAAC,kBAAkB,GAAG;AACtB,WAAO;AAAA,EACX;AAEA,QAAM,iBAAiB,IAAI,QAAQ,IAAI,iBAAiB,KAAK,IAAI,KAAK;AACtE,QAAM,mBAAmB,cAAc,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC3D,MAAI,iBAAkB,QAAO,qBAAqB;AAElD,QAAM,WAAW,IAAI,QAAQ,IAAI,WAAW,KAAK,IAAI,KAAK;AAC1D,MAAI,QAAS,QAAO,YAAY;AAEhC,SAAO;AACX;AAGO,SAAS,qBAAqB,KAAiC;AAClE,QAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,KAAK;AACtD,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,mBAAmB,gBAAgB,MAAM;AAC/C,MAAI,CAAC,iBAAkB,QAAO;AAE9B,QAAM,cAAc,eAAe;AACnC,QAAM,2BAA2B,cAAc,gBAAgB,WAAW,IAAI;AAC9E,MAAI,4BAA4B,qBAAqB,0BAA0B;AAC3E,WAAO;AAAA,EACX;AAEA,QAAM,aAAa,cAAc;AACjC,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI;AACA,UAAM,eAAe,IAAI,IAAI,gBAAgB;AAC7C,QAAI,aAAa,aAAa,WAAY,QAAO;AACjD,QAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,aAAa,QAAQ,EAAG,QAAO;AACjE,QAAI,cAAc,KAAK,aAAa,aAAa,SAAU,QAAO;AAGlE,UAAM,gBACD,aAAa,aAAa,WAAW,aAAa,SAAS,MAAM,aAAa,SAAS,QACvF,aAAa,aAAa,YAAY,aAAa,SAAS,MAAM,aAAa,SAAS;AAC7F,QAAI,cAAe,QAAO;AAC1B,WAAO;AAAA,EACX,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAEA,SAAS,sBAAsB,QAAyB;AACpD,SAAO,uBAAuB,IAAI,OAAO,YAAY,CAAC;AAC1D;AAEA,SAAS,gBAAgB,SAAkB,OAAqB;AAC5D,QAAM,YAAY,QAAQ,IAAI,MAAM,KAAK,IAAI,KAAK;AAClD,MAAI,CAAC,UAAU;AACX,YAAQ,IAAI,QAAQ,KAAK;AACzB;AAAA,EACJ;AAEA,QAAM,SAAS,SACV,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAC3B,OAAO,OAAO;AAGnB,MAAI,OAAO,KAAK,CAAC,UAAU,UAAU,GAAG,GAAG;AACvC,YAAQ,IAAI,QAAQ,GAAG;AACvB;AAAA,EACJ;AAEA,QAAM,WAAW,OAAO,KAAK,CAAC,UAAU,MAAM,YAAY,MAAM,MAAM,YAAY,CAAC;AACnF,MAAI,UAAU;AACV,YAAQ,IAAI,QAAQ,OAAO,KAAK,IAAI,CAAC;AACrC;AAAA,EACJ;AAEA,UAAQ,IAAI,QAAQ,CAAC,GAAG,QAAQ,KAAK,EAAE,KAAK,IAAI,CAAC;AACrD;AAYA,SAAS,sBAAsB,KAA2B;AACtD,QAAM,aAAa,IAAI,QAAQ,IAAI,gBAAgB,KAAK,IAAI,KAAK,EAAE,YAAY;AAG/E,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,EAAE,cAAc,iBAAiB,cAAc,eAAe,cAAc;AACvF;AAEA,SAAS,gBAAgB,KAA2B;AAChD,QAAM,UAAU,IAAI,QAAQ,IAAI,QAAQ,KAAK,IAAI,KAAK;AACtD,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,mBAAmB,gBAAgB,MAAM;AAC/C,MAAI,CAAC,iBAAkB,QAAO;AAE9B,QAAM,gBAAgB,IAAI,QAAQ;AAClC,MAAI,qBAAqB,cAAe,QAAO;AAE/C,QAAM,gBAAgB,qBAAqB,GAAG;AAC9C,SAAO,CAAC,CAAC,iBAAiB,kBAAkB;AAChD;AAEA,SAAS,yBAAyB,KAAiC;AAC/D,QAAM,WAAW,IAAI,QAAQ,IAAI,SAAS,KAAK,IAAI,KAAK;AACxD,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,gBAAgB,OAAO;AAClC;AAEA,SAAS,iBAAiB,KAA2B;AACjD,QAAM,gBAAgB,yBAAyB,GAAG;AAClD,MAAI,CAAC,cAAe,QAAO;AAE3B,MAAI,kBAAkB,IAAI,QAAQ,OAAQ,QAAO;AAEjD,QAAM,gBAAgB,qBAAqB,GAAG;AAC9C,SAAO,CAAC,CAAC,iBAAiB,kBAAkB;AAChD;AAGO,SAAS,6BAA6B,KAAkB,QAAgB,SAA0B;AACrG,QAAM,kBAAkB,mBAAmB;AAC3C,MAAI,CAAC,sBAAsB,MAAM,EAAG,QAAO;AAC3C,MAAI,gBAAgB,IAAI,QAAQ,YAAY,CAAC,EAAG,QAAO;AACvD,MAAI,sBAAsB,GAAG,EAAG,QAAO;AACvC,MAAI,gBAAgB,GAAG,EAAG,QAAO;AACjC,MAAI,iBAAiB,GAAG,EAAG,QAAO;AAClC,SAAO;AACX;AAGO,SAAS,iBAAiB,SAAkB,KAAkB,YAA2B;AAC5F,QAAM,gBAAgB,qBAAqB,GAAG;AAC9C,QAAM,cAAcA,oBAAmB,EAAE,MAAM;AAC/C,MAAI,YAAY;AACZ,QAAI,eAAe;AACf,cAAQ,IAAI,+BAA+B,aAAa;AAAA,IAC5D;AACA,YAAQ,IAAI,gCAAgC,wCAAwC;AACpF,YAAQ,IAAI,gCAAgC,WAAW;AACvD,YAAQ,IAAI,0BAA0B,OAAO;AAC7C,QAAI,eAAe;AACf,cAAQ,IAAI,oCAAoC,MAAM;AAAA,IAC1D,OAAO;AACH,cAAQ,OAAO,kCAAkC;AAAA,IACrD;AACA,oBAAgB,SAAS,QAAQ;AACjC;AAAA,EACJ;AACA,MAAI,eAAe;AACf,YAAQ,IAAI,+BAA+B,aAAa;AACxD,YAAQ,IAAI,oCAAoC,MAAM;AAAA,EAC1D,OAAO;AACH,YAAQ,OAAO,kCAAkC;AAAA,EACrD;AACA,kBAAgB,SAAS,QAAQ;AACjC,UAAQ,IAAI,gCAAgC,wCAAwC;AACpF,UAAQ,IAAI,gCAAgC,WAAW;AACvD,UAAQ,IAAI,0BAA0B,OAAO;AACjD;AAGO,SAAS,qBAA8B;AAC1C,SAAO,mBAAmB,EAAE,OAAO;AACvC;AAGO,SAAS,iCAAgD;AAC5D,MAAI,CAAC,cAAc,EAAG,QAAO;AAC7B,MAAI,mBAAmB,EAAG,QAAO;AACjC,SAAO;AACX;;;ACtNA,SAAS,uBAAuB,OAAe,UAA0B;AACrE,SAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;AACrE;AAEA,SAAS,qBAA8B;AACnC,SAAO,QAAQ,IAAI,kCAAkC;AACzD;AAEA,SAASC,qBAA6B;AAClC,SAAO,QAAQ,IAAI,6BAA6B;AACpD;AAEA,SAAS,UAAU,SAAsB,MAAsB;AAC3D,UAAQ,QAAQ,QAAQ,IAAI,IAAI,KAAK,IAAI,KAAK;AAClD;AAEA,SAAS,uBAAuB,SAA8B;AAC1D,QAAM,YAAY,UAAU,SAAS,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC5E,QAAM,iBAAiB,UAAU,SAAS,iBAAiB,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACtF,QAAM,UAAU,UAAU,SAAS,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACzE,QAAM,SAAS,CAAC,WAAW,gBAAgB,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAC5E,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG;AACtC;AAEA,SAAS,YAA6B;AAClC,SAAO;AAAA,IACH,UAAU,uBAAuB,OAAO,QAAQ,IAAI,mCAAmC,QAAQ,GAAG,GAAO;AAAA,IACzG,SAAS,uBAAuB,OAAO,QAAQ,IAAI,kCAAkC,QAAQ,GAAG,GAAO;AAAA,IACvG,2BAA2B,uBAAuB,OAAO,QAAQ,IAAI,sCAAsC,GAAG,GAAG,CAAC;AAAA,IAClH,sBAAsB;AAAA,MAClB,OAAO,QAAQ,IAAI,8CAA8C,IAAI;AAAA,MACrE;AAAA,IACJ;AAAA,IACA,SAAS,uBAAuB,OAAO,QAAQ,IAAI,kCAAkC,OAAO,GAAG,GAAM;AAAA,EACzG;AACJ;AAGO,SAAS,iBAAiB,SAA8B;AAC3D,MAAIA,mBAAkB,GAAG;AACrB,UAAM,gBAAgB,UAAU,SAAS,iBAAiB;AAC1D,UAAM,QAAQ,cAAc,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAChD,QAAI,MAAO,QAAO;AAElB,UAAM,UAAU,UAAU,SAAS,WAAW;AAC9C,QAAI,QAAS,QAAO;AAAA,EACxB;AAKA,QAAM,WAAY,QAAuC,IAAI,KAAK;AAClE,MAAI,SAAU,QAAO;AACrB,SAAO,uBAAuB,OAAO;AACzC;AAEA,SAAS,0BAA0B,OAAuB;AACtD,QAAM,UAAU,MAAM,KAAK,EAAE,YAAY;AACzC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,MAAM,GAAG,GAAG;AAC/B;AAGO,SAAS,sBAAsB,SAAsB,UAAiC;AACzF,QAAM,qBAAqB,0BAA0B,QAAQ;AAC7D,QAAM,eAAe,0BAA0B,iBAAiB,OAAO,CAAC;AACxE,QAAM,UAAU,GAAG,YAAY,IAAI,kBAAkB;AACrD,QAAM,aAAa,uBAAuB,YAAY,qBAAqB;AAC3E,SAAO,EAAE,SAAS,WAAW;AACjC;AAGO,SAAS,0BAA0B,KAAmB;AACzD,QAAM,SAAS,UAAU;AACzB,QAAM,UAAU,sBAAsB;AAEtC,aAAW,SAAS,CAAC,MAAM,SAAS,GAAY;AAC5C,QAAI,OAAO,QAAQ,iBAAiB,YAAY;AAC5C,cAAQ,aAAa,OAAO,KAAK,OAAO,QAAQ;AAAA,IACpD;AAEA,UAAM,OAAO,QAAQ,KAAK,KAAK;AAC/B,QAAI,OAAO,OAAO,SAAS;AACvB,cAAQ,YAAY,OAAO,OAAO,OAAO,OAAO;AAAA,IACpD;AAAA,EACJ;AACJ;AAGO,SAAS,yBAAyB,MAAqB,KAAqB;AAC/E,MAAI,CAAC,mBAAmB,EAAG,QAAO;AAClC,QAAM,SAAS,UAAU;AACzB,QAAM,SAAS,eAAe,KAAK,SAAS,KAAK,YAAY,MAAM;AACnE,MAAI,CAAC,OAAQ,QAAO;AAGpB,QAAM,UAAU,sBAAsB;AACtC,QAAM,UAAU,QAAQ,IAAI,MAAM,KAAK,OAAO;AAC9C,QAAM,eAAe,KAAK,aAAa,QAAQ,IAAI,WAAW,KAAK,UAAU,IAAI;AACjF,QAAM,UAAU,WAAW,QAAQ,eAAe,MAAM,QAAQ,eAAe,MAAM;AACrF,QAAM,eAAe,gBAAgB,aAAa,eAAe,MAAM,aAAa,eAAe,MAAM;AACzG,SAAO,KAAK,IAAI,SAAS,YAAY;AACzC;AAGO,SAAS,2BAA2B,MAAqB,MAAoB;AAChF,MAAI,CAAC,mBAAmB,EAAG;AAC3B,QAAM,SAAS,UAAU;AACzB,sBAAoB,KAAK,SAAS,KAAK,YAAY,MAAM;AAC7D;AAGO,SAAS,uBAAuB,MAA2B;AAC9D,MAAI,CAAC,mBAAmB,EAAG;AAC3B,wBAAsB,KAAK,SAAS,KAAK,UAAU;AACvD;","names":["getFrameworkConfig","trustProxyHeaders"]}
@@ -0,0 +1,24 @@
1
+ // src/security-headers.ts
2
+ var PERMISSIONS_POLICY_VALUE = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()";
3
+ var BASE_SECURITY_HEADER_VALUES = Object.freeze({
4
+ "Referrer-Policy": "strict-origin-when-cross-origin",
5
+ "X-Content-Type-Options": "nosniff",
6
+ "X-Frame-Options": "DENY",
7
+ "Cross-Origin-Opener-Policy": "same-origin",
8
+ "Cross-Origin-Resource-Policy": "same-origin",
9
+ "X-DNS-Prefetch-Control": "off",
10
+ "X-Permitted-Cross-Domain-Policies": "none",
11
+ "Origin-Agent-Cluster": "?1",
12
+ "Permissions-Policy": PERMISSIONS_POLICY_VALUE
13
+ });
14
+ var BASE_SECURITY_HEADERS = Object.entries(BASE_SECURITY_HEADER_VALUES).map(([key, value]) => ({
15
+ key,
16
+ value
17
+ }));
18
+
19
+ export {
20
+ PERMISSIONS_POLICY_VALUE,
21
+ BASE_SECURITY_HEADER_VALUES,
22
+ BASE_SECURITY_HEADERS
23
+ };
24
+ //# sourceMappingURL=chunk-KA7RJCWA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/security-headers.ts"],"sourcesContent":["export const PERMISSIONS_POLICY_VALUE =\n \"accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()\";\n\nexport const BASE_SECURITY_HEADER_VALUES: Readonly<Record<string, string>> = Object.freeze({\n \"Referrer-Policy\": \"strict-origin-when-cross-origin\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Cross-Origin-Opener-Policy\": \"same-origin\",\n \"Cross-Origin-Resource-Policy\": \"same-origin\",\n \"X-DNS-Prefetch-Control\": \"off\",\n \"X-Permitted-Cross-Domain-Policies\": \"none\",\n \"Origin-Agent-Cluster\": \"?1\",\n \"Permissions-Policy\": PERMISSIONS_POLICY_VALUE,\n});\n\nexport const BASE_SECURITY_HEADERS = Object.entries(BASE_SECURITY_HEADER_VALUES).map(([key, value]) => ({\n key,\n value,\n}));\n"],"mappings":";AAAO,IAAM,2BACT;AAEG,IAAM,8BAAgE,OAAO,OAAO;AAAA,EACvF,mBAAmB;AAAA,EACnB,0BAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA,EAChC,0BAA0B;AAAA,EAC1B,qCAAqC;AAAA,EACrC,wBAAwB;AAAA,EACxB,sBAAsB;AAC1B,CAAC;AAEM,IAAM,wBAAwB,OAAO,QAAQ,2BAA2B,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;AAAA,EACpG;AAAA,EACA;AACJ,EAAE;","names":[]}
@@ -0,0 +1,159 @@
1
+ import {
2
+ BASE_SECURITY_HEADER_VALUES
3
+ } from "./chunk-KA7RJCWA.js";
4
+
5
+ // src/proxy-middleware.ts
6
+ import { getFrameworkConfig } from "@spring-systems/core/config";
7
+ import { NextResponse } from "next/server.js";
8
+ function toOriginSource(value) {
9
+ const raw = value.trim();
10
+ if (!raw) return [];
11
+ try {
12
+ const parsed = new URL(raw);
13
+ const out = [parsed.origin];
14
+ if (parsed.protocol === "https:") {
15
+ out.push(`wss://${parsed.host}`);
16
+ } else if (parsed.protocol === "http:") {
17
+ out.push(`ws://${parsed.host}`);
18
+ }
19
+ return out;
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+ var BLOCKED_CSP_SCHEMES = /* @__PURE__ */ new Set(["javascript:", "data:", "blob:", "vbscript:", "filesystem:"]);
25
+ function parseCspSource(token) {
26
+ const value = token.trim();
27
+ if (!value) return null;
28
+ if (/[\s;,\r\n]/.test(value)) return null;
29
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {
30
+ const lower = value.toLowerCase();
31
+ if (BLOCKED_CSP_SCHEMES.has(lower)) return null;
32
+ return lower;
33
+ }
34
+ try {
35
+ const parsed = new URL(value);
36
+ if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) return null;
37
+ if (parsed.username || parsed.password) return null;
38
+ if (parsed.pathname !== "/" || parsed.search || parsed.hash) return null;
39
+ return `${parsed.protocol}//${parsed.host}`;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function parseReportUri(value) {
45
+ const trimmed = value.trim();
46
+ if (!trimmed) return null;
47
+ if (/[\s;,\r\n"]/.test(trimmed)) return null;
48
+ try {
49
+ const parsed = new URL(trimmed);
50
+ if (!["http:", "https:"].includes(parsed.protocol)) return null;
51
+ if (parsed.username || parsed.password) return null;
52
+ return parsed.toString();
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function parseCspList(value) {
58
+ const parsed = value.split(",").map((s) => parseCspSource(s)).filter((s) => !!s);
59
+ return [...new Set(parsed)];
60
+ }
61
+ function proxy(req) {
62
+ const url = req.nextUrl.clone();
63
+ const TARGET_ENV = process.env.TARGET_ENV || "test";
64
+ const isProdRuntime = TARGET_ENV === "prod" || process.env.NODE_ENV === "production";
65
+ const isTargetProd = TARGET_ENV === "prod";
66
+ const isLocalHost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
67
+ const nonce = crypto.randomUUID().replace(/-/g, "");
68
+ const allowUnsafeStyleAttr = process.env.CSP_ALLOW_UNSAFE_STYLE_ATTR === "true";
69
+ const denyStyleAttr = isTargetProd && !allowUnsafeStyleAttr;
70
+ const allowUnsafeInlineStyle = !isTargetProd;
71
+ const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || "");
72
+ const apiConnectSources = toOriginSource(process.env.API_URL || "");
73
+ const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || "");
74
+ const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || "");
75
+ const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || "");
76
+ const cspConfig = getFrameworkConfig().csp;
77
+ const validatedConnectSources = cspConfig.thirdPartyConnectSources.map((s) => parseCspSource(s)).filter((s) => !!s);
78
+ const validatedImgSources = cspConfig.thirdPartyImageSources.map((s) => parseCspSource(s)).filter((s) => !!s);
79
+ const validatedScriptSources = cspConfig.thirdPartyScriptSources.map((s) => parseCspSource(s)).filter((s) => !!s);
80
+ const validatedFrameSources = cspConfig.thirdPartyFrameSources.map((s) => parseCspSource(s)).filter((s) => !!s);
81
+ const connectSrc = [
82
+ "'self'",
83
+ ...validatedConnectSources,
84
+ ...apiConnectSources,
85
+ ...connectHostsEnv,
86
+ ...!isProdRuntime ? ["http://localhost:*", "http://127.0.0.1:*", "ws://localhost:*", "ws://127.0.0.1:*"] : []
87
+ ].join(" ");
88
+ const imgSrc = ["'self'", "data:", "blob:", ...validatedImgSources, ...imgHostsEnv].join(" ");
89
+ const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(" ");
90
+ const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(" ");
91
+ const reportUri = parseReportUri(process.env.CSP_REPORT_URI || "");
92
+ const cspDirectives = [
93
+ "default-src 'self'",
94
+ "base-uri 'self'",
95
+ "object-src 'none'",
96
+ `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,
97
+ `script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,
98
+ "script-src-attr 'none'",
99
+ allowUnsafeInlineStyle ? "style-src 'self' 'unsafe-inline'" : `style-src 'self' 'nonce-${nonce}'`,
100
+ denyStyleAttr ? "style-src-attr 'none'" : "style-src-attr 'unsafe-inline'",
101
+ `img-src ${imgSrc}`,
102
+ "media-src 'self' blob: data:",
103
+ "manifest-src 'self'",
104
+ `connect-src ${connectSrc}`,
105
+ `frame-src ${frameSrcHosts || "'none'"}`,
106
+ "frame-ancestors 'none'",
107
+ "form-action 'self'",
108
+ ...isTargetProd ? ["upgrade-insecure-requests"] : [],
109
+ ...reportUri ? [`report-uri ${reportUri}`, "report-to csp-violations"] : []
110
+ ];
111
+ const csp = cspDirectives.join("; ");
112
+ const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];
113
+ if (isProdRuntime) {
114
+ for (const prefix of blockedPrefixes) {
115
+ if (url.pathname.startsWith(prefix)) {
116
+ const res2 = new NextResponse("Not Found", { status: 404 });
117
+ return setSecurity(res2, { isLocalHost, isTargetProd, csp, nonce, reportUri });
118
+ }
119
+ }
120
+ }
121
+ if (url.pathname === "/") {
122
+ const dest = new URL(url.toString());
123
+ if (!isLocalHost) dest.protocol = "https:";
124
+ dest.pathname = getFrameworkConfig().app.defaultRoute;
125
+ dest.search = "";
126
+ const res2 = makeRedirect(dest, 308);
127
+ return setSecurity(res2, { isLocalHost, isTargetProd, csp, nonce, reportUri });
128
+ }
129
+ const requestHeaders = new Headers(req.headers);
130
+ requestHeaders.set("x-nonce", nonce);
131
+ const res = NextResponse.next({ request: { headers: requestHeaders } });
132
+ return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });
133
+ }
134
+ function setSecurity(res, opts) {
135
+ const { isLocalHost, csp, nonce, reportUri } = opts;
136
+ if (csp) res.headers.set("Content-Security-Policy", csp);
137
+ for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {
138
+ res.headers.set(key, value);
139
+ }
140
+ if (opts.isTargetProd && !isLocalHost)
141
+ res.headers.set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload");
142
+ if (reportUri) res.headers.set("Reporting-Endpoints", `csp-violations="${reportUri}"`);
143
+ if (nonce) res.headers.set("x-nonce", nonce);
144
+ return res;
145
+ }
146
+ function makeRedirect(to, status = 308) {
147
+ const res = new NextResponse(null, { status });
148
+ res.headers.set("Location", typeof to === "string" ? to : to.toString());
149
+ return res;
150
+ }
151
+ var proxyConfig = {
152
+ matcher: ["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)"]
153
+ };
154
+
155
+ export {
156
+ proxy,
157
+ proxyConfig
158
+ };
159
+ //# sourceMappingURL=chunk-OYTV4D7E.js.map