@spring-systems/server 0.8.6 → 0.8.7

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 (40) hide show
  1. package/dist/api-route-handler.js +1 -19
  2. package/dist/chunk-E4AX7MID.js +1 -0
  3. package/dist/chunk-FXUI75TW.js +1 -0
  4. package/dist/chunk-KEEIJ5EV.js +1 -0
  5. package/dist/chunk-M23YQYLU.js +1 -0
  6. package/dist/chunk-TVAJDSYB.js +1 -0
  7. package/dist/chunk-UDRRRHFI.js +1 -0
  8. package/dist/chunk-VB7DEUGT.js +1 -0
  9. package/dist/client.js +1 -14
  10. package/dist/handlers/index.js +1 -48
  11. package/dist/index.js +1 -44
  12. package/dist/next-adapters.js +1 -14
  13. package/dist/proxy-middleware.js +1 -10
  14. package/dist/rate-limiter.js +1 -15
  15. package/dist/runtime-env.js +1 -9
  16. package/dist/security-headers.js +1 -11
  17. package/package.json +109 -108
  18. package/dist/api-route-handler.js.map +0 -1
  19. package/dist/chunk-4SUIIQDW.js +0 -158
  20. package/dist/chunk-4SUIIQDW.js.map +0 -1
  21. package/dist/chunk-7IUSTA5W.js +0 -113
  22. package/dist/chunk-7IUSTA5W.js.map +0 -1
  23. package/dist/chunk-CP33WQ5Q.js +0 -47
  24. package/dist/chunk-CP33WQ5Q.js.map +0 -1
  25. package/dist/chunk-KA7RJCWA.js +0 -24
  26. package/dist/chunk-KA7RJCWA.js.map +0 -1
  27. package/dist/chunk-NFJ25NQQ.js +0 -377
  28. package/dist/chunk-NFJ25NQQ.js.map +0 -1
  29. package/dist/chunk-PZWKMIA4.js +0 -513
  30. package/dist/chunk-PZWKMIA4.js.map +0 -1
  31. package/dist/chunk-YV6DZVPI.js +0 -43
  32. package/dist/chunk-YV6DZVPI.js.map +0 -1
  33. package/dist/client.js.map +0 -1
  34. package/dist/handlers/index.js.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/next-adapters.js.map +0 -1
  37. package/dist/proxy-middleware.js.map +0 -1
  38. package/dist/rate-limiter.js.map +0 -1
  39. package/dist/runtime-env.js.map +0 -1
  40. package/dist/security-headers.js.map +0 -1
@@ -1,513 +0,0 @@
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/api-route-utils.ts
293
- var RFC_HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
294
- "connection",
295
- "keep-alive",
296
- "proxy-authenticate",
297
- "proxy-authorization",
298
- "te",
299
- "trailer",
300
- "transfer-encoding",
301
- "upgrade",
302
- "proxy-connection"
303
- ]);
304
- function toValidPositiveInteger(value, fallback) {
305
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
306
- }
307
- function parseContentLength(rawValue) {
308
- if (!rawValue) return null;
309
- const parsed = Number(rawValue);
310
- if (!Number.isFinite(parsed) || parsed < 0) return null;
311
- return Math.floor(parsed);
312
- }
313
- function normalizePath(pathname) {
314
- return pathname.replace(/^\/+|\/+$/g, "").toLowerCase();
315
- }
316
- function isSafeProxyPathSegment(segment) {
317
- if (!segment) return false;
318
- if (segment === "." || segment === "..") return false;
319
- if (segment.includes("\\") || segment.includes("/")) return false;
320
- if (/[\u0000-\u001F\u007F]/.test(segment)) return false;
321
- return true;
322
- }
323
- function extractTrailingVersion(pathname) {
324
- const m = pathname.match(/\/(v\d+(?:\.\d+)?)\/?$/i);
325
- return m?.[1] ?? null;
326
- }
327
- function extractLeadingVersion(path) {
328
- const m = path.match(/^(v\d+(?:\.\d+)?)(?:\/|$)/i);
329
- return m?.[1] ?? null;
330
- }
331
- function parseConnectionHeaderTokens(headers) {
332
- const connectionValue = (headers.get("connection") || "").trim();
333
- if (!connectionValue) return /* @__PURE__ */ new Set();
334
- return new Set(
335
- connectionValue.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean)
336
- );
337
- }
338
- function shouldDropHopByHopHeader(name, connectionTokens) {
339
- const lower = name.toLowerCase();
340
- return RFC_HOP_BY_HOP_HEADERS.has(lower) || connectionTokens.has(lower);
341
- }
342
- var PayloadTooLargeError = class extends Error {
343
- code = "PAYLOAD_TOO_LARGE";
344
- constructor(message) {
345
- super(message);
346
- this.name = "PayloadTooLargeError";
347
- }
348
- };
349
- function createSizeLimitedBodyStream(stream, maxBytes) {
350
- let totalBytes = 0;
351
- return stream.pipeThrough(
352
- new TransformStream({
353
- transform(chunk, controller) {
354
- totalBytes += chunk.byteLength;
355
- if (totalBytes > maxBytes) {
356
- controller.error(new PayloadTooLargeError("Payload too large"));
357
- return;
358
- }
359
- controller.enqueue(chunk);
360
- }
361
- })
362
- );
363
- }
364
- function isPayloadTooLargeError(error) {
365
- const queue = [error];
366
- const visited = /* @__PURE__ */ new Set();
367
- while (queue.length > 0) {
368
- const current = queue.shift();
369
- if (!current || visited.has(current)) continue;
370
- visited.add(current);
371
- if (current instanceof PayloadTooLargeError) return true;
372
- if (current instanceof Error) {
373
- if (current.name === "PayloadTooLargeError") return true;
374
- const code = current.code;
375
- if (code === "PAYLOAD_TOO_LARGE") return true;
376
- const cause = current.cause;
377
- if (cause) queue.push(cause);
378
- } else if (typeof current === "object") {
379
- const record = current;
380
- if (record.code === "PAYLOAD_TOO_LARGE") return true;
381
- if (typeof record.name === "string" && record.name === "PayloadTooLargeError") return true;
382
- if (record.cause) queue.push(record.cause);
383
- }
384
- }
385
- return false;
386
- }
387
-
388
- // src/handlers/rate-limit-handler.ts
389
- function isRateLimitEnabled() {
390
- return process.env.AUTH_LOGIN_RATE_LIMIT_ENABLED !== "false";
391
- }
392
- function trustProxyHeaders2() {
393
- return process.env.AUTH_TRUST_PROXY_HEADERS === "true";
394
- }
395
- function getHeader(request, name) {
396
- return (request.headers.get(name) || "").trim();
397
- }
398
- function buildClientFingerprint(request) {
399
- const userAgent = getHeader(request, "user-agent").toLowerCase().slice(0, 64);
400
- const acceptLanguage = getHeader(request, "accept-language").toLowerCase().slice(0, 32);
401
- const secChUa = getHeader(request, "sec-ch-ua").toLowerCase().slice(0, 64);
402
- const signal = [userAgent, acceptLanguage, secChUa].filter(Boolean).join("|");
403
- if (!signal) return "unknown";
404
- return `fp:${signal}`.slice(0, 128);
405
- }
406
- function getPolicy() {
407
- return {
408
- windowMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_WINDOW_MS || "900000"), 9e5),
409
- blockMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_BLOCK_MS || "900000"), 9e5),
410
- maxAttemptsByIpAndAccount: toValidPositiveInteger(
411
- Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS || "8"),
412
- 8
413
- ),
414
- maxAttemptsByAccount: toValidPositiveInteger(
415
- Number(process.env.AUTH_LOGIN_RATE_LIMIT_ACCOUNT_MAX_ATTEMPTS || "20"),
416
- 20
417
- ),
418
- maxKeys: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_KEYS || "10000"), 1e4)
419
- };
420
- }
421
- function getClientAddress(request) {
422
- if (trustProxyHeaders2()) {
423
- const xForwardedFor = getHeader(request, "x-forwarded-for");
424
- const first = xForwardedFor.split(",")[0]?.trim();
425
- if (first) return first;
426
- const xRealIp = getHeader(request, "x-real-ip");
427
- if (xRealIp) return xRealIp;
428
- }
429
- const directIp = request.ip?.trim();
430
- if (directIp) return directIp;
431
- return buildClientFingerprint(request);
432
- }
433
- function normalizeRateLimitKeyPart(value) {
434
- const trimmed = value.trim().toLowerCase();
435
- if (!trimmed) return "unknown";
436
- return trimmed.slice(0, 128);
437
- }
438
- function getLoginRateLimitKeys(request, username) {
439
- const normalizedUsername = normalizeRateLimitKeyPart(username);
440
- const normalizedIp = normalizeRateLimitKeyPart(getClientAddress(request));
441
- const pairKey = `${normalizedIp}:${normalizedUsername}`;
442
- const accountKey = normalizedUsername !== "unknown" ? normalizedUsername : null;
443
- return { pairKey, accountKey };
444
- }
445
- function cleanupExpiredLoginLimits(now) {
446
- const policy = getPolicy();
447
- const adapter = getRateLimiterAdapter();
448
- for (const store of ["ip", "account"]) {
449
- if (typeof adapter.sweepExpired === "function") {
450
- adapter.sweepExpired(store, now, policy.windowMs);
451
- }
452
- const size = adapter.size(store);
453
- if (size > policy.maxKeys) {
454
- adapter.evictOldest(store, size - policy.maxKeys);
455
- }
456
- }
457
- }
458
- function getRateLimitRetryAfterMs(keys, now) {
459
- if (!isRateLimitEnabled()) return 0;
460
- const policy = getPolicy();
461
- const result = checkRateLimit(keys.pairKey, keys.accountKey, policy);
462
- if (!result) return 0;
463
- const adapter = getRateLimiterAdapter();
464
- const ipEntry = adapter.get("ip", keys.pairKey);
465
- const accountEntry = keys.accountKey ? adapter.get("account", keys.accountKey) : null;
466
- const ipRetry = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil - now : 0;
467
- const accountRetry = accountEntry && accountEntry.blockedUntil > now ? accountEntry.blockedUntil - now : 0;
468
- return Math.max(ipRetry, accountRetry);
469
- }
470
- function registerFailedLoginAttempt(keys, _now) {
471
- if (!isRateLimitEnabled()) return;
472
- const policy = getPolicy();
473
- recordFailedAttempt(keys.pairKey, keys.accountKey, policy);
474
- }
475
- function clearLoginAttemptState(keys) {
476
- if (!isRateLimitEnabled()) return;
477
- clearRateLimitEntries(keys.pairKey, keys.accountKey);
478
- }
479
-
480
- export {
481
- toValidPositiveInteger,
482
- parseContentLength,
483
- normalizePath,
484
- isSafeProxyPathSegment,
485
- extractTrailingVersion,
486
- extractLeadingVersion,
487
- parseConnectionHeaderTokens,
488
- shouldDropHopByHopHeader,
489
- createSizeLimitedBodyStream,
490
- isPayloadTooLargeError,
491
- isLocalHostRequest,
492
- useSecureCookies,
493
- resolveSessionCookieName,
494
- setSessionCookie,
495
- clearSessionCookie,
496
- getSessionToken,
497
- isSessionExpiredCode,
498
- shouldClearSessionFromForbidden,
499
- normalizeOrigin,
500
- isInternalIpAccess,
501
- resolveAllowedOrigin,
502
- shouldRejectByCsrfProtection,
503
- applyCorsHeaders,
504
- hasCsrfExemptPaths,
505
- resolveProdSecurityConfigError,
506
- getClientAddress,
507
- getLoginRateLimitKeys,
508
- cleanupExpiredLoginLimits,
509
- getRateLimitRetryAfterMs,
510
- registerFailedLoginAttempt,
511
- clearLoginAttemptState
512
- };
513
- //# sourceMappingURL=chunk-PZWKMIA4.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/handlers/auth-session.ts","../src/handlers/csrf-cors.ts","../src/api-route-utils.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","export const RFC_HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"proxy-connection\",\n]);\n\nexport function toValidPositiveInteger(value: number, fallback: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n}\n\nexport function parseContentLength(rawValue: string | null): number | null {\n if (!rawValue) return null;\n const parsed = Number(rawValue);\n if (!Number.isFinite(parsed) || parsed < 0) return null;\n return Math.floor(parsed);\n}\n\nexport function normalizePath(pathname: string): string {\n return pathname.replace(/^\\/+|\\/+$/g, \"\").toLowerCase();\n}\n\nexport function isSafeProxyPathSegment(segment: string): boolean {\n if (!segment) return false;\n if (segment === \".\" || segment === \"..\") return false;\n if (segment.includes(\"\\\\\") || segment.includes(\"/\")) return false;\n if (/[\\u0000-\\u001F\\u007F]/.test(segment)) return false;\n return true;\n}\n\nexport function extractTrailingVersion(pathname: string): string | null {\n const m = pathname.match(/\\/(v\\d+(?:\\.\\d+)?)\\/?$/i);\n return m?.[1] ?? null;\n}\n\nexport function extractLeadingVersion(path: string): string | null {\n const m = path.match(/^(v\\d+(?:\\.\\d+)?)(?:\\/|$)/i);\n return m?.[1] ?? null;\n}\n\nexport function parseConnectionHeaderTokens(headers: Headers): Set<string> {\n const connectionValue = (headers.get(\"connection\") || \"\").trim();\n if (!connectionValue) return new Set();\n return new Set(\n connectionValue\n .split(\",\")\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean),\n );\n}\n\nexport function shouldDropHopByHopHeader(name: string, connectionTokens: Set<string>): boolean {\n const lower = name.toLowerCase();\n return RFC_HOP_BY_HOP_HEADERS.has(lower) || connectionTokens.has(lower);\n}\n\nclass PayloadTooLargeError extends Error {\n code = \"PAYLOAD_TOO_LARGE\" as const;\n\n constructor(message: string) {\n super(message);\n this.name = \"PayloadTooLargeError\";\n }\n}\n\nexport function createSizeLimitedBodyStream(\n stream: ReadableStream<Uint8Array>,\n maxBytes: number,\n): ReadableStream<Uint8Array> {\n let totalBytes = 0;\n return stream.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n totalBytes += chunk.byteLength;\n if (totalBytes > maxBytes) {\n controller.error(new PayloadTooLargeError(\"Payload too large\"));\n return;\n }\n controller.enqueue(chunk);\n },\n }),\n );\n}\n\nexport function isPayloadTooLargeError(error: unknown): boolean {\n const queue: unknown[] = [error];\n const visited = new Set<unknown>();\n\n while (queue.length > 0) {\n const current = queue.shift();\n if (!current || visited.has(current)) continue;\n visited.add(current);\n\n if (current instanceof PayloadTooLargeError) return true;\n\n if (current instanceof Error) {\n if (current.name === \"PayloadTooLargeError\") return true;\n\n const code = (current as Error & { code?: unknown }).code;\n if (code === \"PAYLOAD_TOO_LARGE\") return true;\n\n const cause = (current as Error & { cause?: unknown }).cause;\n if (cause) queue.push(cause);\n } else if (typeof current === \"object\") {\n const record = current as { cause?: unknown; code?: unknown; name?: unknown };\n if (record.code === \"PAYLOAD_TOO_LARGE\") return true;\n if (typeof record.name === \"string\" && record.name === \"PayloadTooLargeError\") return true;\n if (record.cause) queue.push(record.cause);\n }\n }\n\n return false;\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 { toValidPositiveInteger } from \"../api-route-utils\";\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 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(\n Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS || \"8\"),\n 8,\n ),\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":"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","names":["getFrameworkConfig","trustProxyHeaders"]}
@@ -1,43 +0,0 @@
1
- // src/runtime-env-route.ts
2
- import { getRuntimeEnvKeys } from "@spring-systems/core/config";
3
- import { NextResponse } from "next/server.js";
4
- async function GET() {
5
- const isProd = (process.env.NODE_ENV || "") === "production" || (process.env.TARGET_ENV || "") === "prod";
6
- const allowInProd = (process.env.RUNTIME_ENV_PUBLIC_IN_PROD || "").toLowerCase() === "true";
7
- if (isProd && !allowInProd) {
8
- return NextResponse.json({ error: "Not found" }, { status: 404 });
9
- }
10
- const filtered = Object.fromEntries(
11
- getRuntimeEnvKeys().map((key) => [key, process.env[key]]).filter(([, value]) => typeof value === "string")
12
- );
13
- return NextResponse.json(filtered, {
14
- status: 200,
15
- headers: {
16
- "Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
17
- Pragma: "no-cache",
18
- Expires: "0"
19
- }
20
- });
21
- }
22
-
23
- // src/RuntimeEnvScript.tsx
24
- import { getRuntimeEnvKeys as getRuntimeEnvKeys2 } from "@spring-systems/core/config";
25
- import { jsx } from "react/jsx-runtime";
26
- function serializeForInlineScript(value) {
27
- return JSON.stringify(value).replace(/</g, "\\u003C").replace(/>/g, "\\u003E").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
28
- }
29
- function RuntimeEnvScript({ nonce }) {
30
- const runtimeEnv = {};
31
- for (const k of getRuntimeEnvKeys2()) {
32
- const v = process.env[k];
33
- if (typeof v === "string") runtimeEnv[k] = v;
34
- }
35
- const inline = `window.__RUNTIME_ENV__ = ${serializeForInlineScript(runtimeEnv)};`;
36
- return /* @__PURE__ */ jsx("script", { id: "runtime-env", nonce, suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: inline } });
37
- }
38
-
39
- export {
40
- GET,
41
- RuntimeEnvScript
42
- };
43
- //# sourceMappingURL=chunk-YV6DZVPI.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime-env-route.ts","../src/RuntimeEnvScript.tsx"],"sourcesContent":["import { getRuntimeEnvKeys } from \"@spring-systems/core/config\";\nimport { NextResponse } from \"next/server.js\";\n\nexport async function GET() {\n const isProd = (process.env.NODE_ENV || \"\") === \"production\" || (process.env.TARGET_ENV || \"\") === \"prod\";\n const allowInProd = (process.env.RUNTIME_ENV_PUBLIC_IN_PROD || \"\").toLowerCase() === \"true\";\n if (isProd && !allowInProd) {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n\n const filtered = Object.fromEntries(\n getRuntimeEnvKeys()\n .map((key) => [key, process.env[key]] as const)\n .filter(([, value]) => typeof value === \"string\")\n );\n\n return NextResponse.json(filtered, {\n status: 200,\n headers: {\n \"Cache-Control\": \"no-store, no-cache, must-revalidate, proxy-revalidate\",\n Pragma: \"no-cache\",\n Expires: \"0\",\n },\n });\n}\n","import { getRuntimeEnvKeys } from \"@spring-systems/core/config\";\n\nfunction serializeForInlineScript(value: unknown): string {\n return JSON.stringify(value)\n .replace(/</g, \"\\\\u003C\")\n .replace(/>/g, \"\\\\u003E\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\nexport interface RuntimeEnvScriptProps {\n nonce?: string;\n}\n\nexport function RuntimeEnvScript({ nonce }: RuntimeEnvScriptProps) {\n const runtimeEnv: Record<string, string> = {};\n for (const k of getRuntimeEnvKeys()) {\n const v = process.env[k];\n if (typeof v === \"string\") runtimeEnv[k] = v;\n }\n\n const inline = `window.__RUNTIME_ENV__ = ${serializeForInlineScript(runtimeEnv)};`;\n\n return (\n <script id=\"runtime-env\" nonce={nonce} suppressHydrationWarning dangerouslySetInnerHTML={{ __html: inline }} />\n );\n}\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAE7B,eAAsB,MAAM;AACxB,QAAM,UAAU,QAAQ,IAAI,YAAY,QAAQ,iBAAiB,QAAQ,IAAI,cAAc,QAAQ;AACnG,QAAM,eAAe,QAAQ,IAAI,8BAA8B,IAAI,YAAY,MAAM;AACrF,MAAI,UAAU,CAAC,aAAa;AACxB,WAAO,aAAa,KAAK,EAAE,OAAO,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACpE;AAEA,QAAM,WAAW,OAAO;AAAA,IACpB,kBAAkB,EACb,IAAI,CAAC,QAAQ,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,CAAU,EAC7C,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,OAAO,UAAU,QAAQ;AAAA,EACxD;AAEA,SAAO,aAAa,KAAK,UAAU;AAAA,IAC/B,QAAQ;AAAA,IACR,SAAS;AAAA,MACL,iBAAiB;AAAA,MACjB,QAAQ;AAAA,MACR,SAAS;AAAA,IACb;AAAA,EACJ,CAAC;AACL;;;ACxBA,SAAS,qBAAAA,0BAAyB;AAyB1B;AAvBR,SAAS,yBAAyB,OAAwB;AACtD,SAAO,KAAK,UAAU,KAAK,EACtB,QAAQ,MAAM,SAAS,EACvB,QAAQ,MAAM,SAAS,EACvB,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AACrC;AAMO,SAAS,iBAAiB,EAAE,MAAM,GAA0B;AAC/D,QAAM,aAAqC,CAAC;AAC5C,aAAW,KAAKA,mBAAkB,GAAG;AACjC,UAAM,IAAI,QAAQ,IAAI,CAAC;AACvB,QAAI,OAAO,MAAM,SAAU,YAAW,CAAC,IAAI;AAAA,EAC/C;AAEA,QAAM,SAAS,4BAA4B,yBAAyB,UAAU,CAAC;AAE/E,SACI,oBAAC,YAAO,IAAG,eAAc,OAAc,0BAAwB,MAAC,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AAErH;","names":["getRuntimeEnvKeys"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client.ts"],"sourcesContent":["\"use client\";\n\ndeclare const __VERSION__: string;\nexport const SPRING_SERVER_VERSION: string = typeof __VERSION__ !== \"undefined\" ? __VERSION__ : \"0.0.0-dev\";\n\nexport { createClientOnlyNextUIAdapter, createNextRouteAdapter, type NextUIAdapterOptions } from \"./next-adapters\";\n"],"mappings":";;;;;;;AAGO,IAAM,wBAAgC,OAAqC,UAAc;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @spring-systems/server\n// Next.js server-only code: proxy middleware, API route handler, runtime env injection.\n// Use sub-path imports: @spring-systems/server/proxy, /api-handler, /runtime-env\nimport \"server-only\";\n\ndeclare const __VERSION__: string;\nexport const SPRING_SERVER_VERSION: string = typeof __VERSION__ !== \"undefined\" ? __VERSION__ : \"0.0.0-dev\";\n\nexport { DELETE, GET, OPTIONS, PATCH, POST, PUT } from \"./api-route-handler\";\nexport { proxy, proxyConfig } from \"./proxy-middleware\";\nexport { runtimeEnvGET, RuntimeEnvScript } from \"./runtime-env\";\nexport { BASE_SECURITY_HEADER_VALUES, BASE_SECURITY_HEADERS, PERMISSIONS_POLICY_VALUE } from \"./security-headers\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAGA,OAAO;AAGA,IAAM,wBAAgC,OAAqC,UAAc;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}