@spring-systems/server 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-route-handler.js +2 -2
- package/dist/{chunk-CLZU34DG.js → chunk-NFJ25NQQ.js} +13 -101
- package/dist/chunk-NFJ25NQQ.js.map +1 -0
- package/dist/{chunk-FEB3UZEG.js → chunk-PZWKMIA4.js} +109 -3
- package/dist/chunk-PZWKMIA4.js.map +1 -0
- package/dist/{chunk-GSACJGAM.js → chunk-YB7NX4IX.js} +1 -1
- package/dist/chunk-YB7NX4IX.js.map +1 -0
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/handlers/index.js +1 -1
- package/dist/index.js +4 -4
- package/dist/proxy-middleware.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-CLZU34DG.js.map +0 -1
- package/dist/chunk-FEB3UZEG.js.map +0 -1
- package/dist/chunk-GSACJGAM.js.map +0 -1
|
@@ -3,120 +3,32 @@ import {
|
|
|
3
3
|
cleanupExpiredLoginLimits,
|
|
4
4
|
clearLoginAttemptState,
|
|
5
5
|
clearSessionCookie,
|
|
6
|
+
createSizeLimitedBodyStream,
|
|
7
|
+
extractLeadingVersion,
|
|
8
|
+
extractTrailingVersion,
|
|
6
9
|
getLoginRateLimitKeys,
|
|
7
10
|
getRateLimitRetryAfterMs,
|
|
8
11
|
getSessionToken,
|
|
9
12
|
isInternalIpAccess,
|
|
10
13
|
isLocalHostRequest,
|
|
14
|
+
isPayloadTooLargeError,
|
|
15
|
+
isSafeProxyPathSegment,
|
|
16
|
+
normalizePath,
|
|
17
|
+
parseConnectionHeaderTokens,
|
|
18
|
+
parseContentLength,
|
|
11
19
|
registerFailedLoginAttempt,
|
|
12
20
|
resolveProdSecurityConfigError,
|
|
13
21
|
setSessionCookie,
|
|
14
22
|
shouldClearSessionFromForbidden,
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
shouldDropHopByHopHeader,
|
|
24
|
+
shouldRejectByCsrfProtection,
|
|
25
|
+
toValidPositiveInteger
|
|
26
|
+
} from "./chunk-PZWKMIA4.js";
|
|
17
27
|
|
|
18
28
|
// src/api-route-handler.ts
|
|
19
29
|
import { getFrameworkConfig } from "@spring-systems/core/config";
|
|
20
30
|
import { logInfo, logWarn } from "@spring-systems/core/logger";
|
|
21
31
|
import { NextResponse } from "next/server.js";
|
|
22
|
-
|
|
23
|
-
// src/api-route-utils.ts
|
|
24
|
-
var RFC_HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
25
|
-
"connection",
|
|
26
|
-
"keep-alive",
|
|
27
|
-
"proxy-authenticate",
|
|
28
|
-
"proxy-authorization",
|
|
29
|
-
"te",
|
|
30
|
-
"trailer",
|
|
31
|
-
"transfer-encoding",
|
|
32
|
-
"upgrade",
|
|
33
|
-
"proxy-connection"
|
|
34
|
-
]);
|
|
35
|
-
function toValidPositiveInteger(value, fallback) {
|
|
36
|
-
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
37
|
-
}
|
|
38
|
-
function parseContentLength(rawValue) {
|
|
39
|
-
if (!rawValue) return null;
|
|
40
|
-
const parsed = Number(rawValue);
|
|
41
|
-
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
42
|
-
return Math.floor(parsed);
|
|
43
|
-
}
|
|
44
|
-
function normalizePath(pathname) {
|
|
45
|
-
return pathname.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
46
|
-
}
|
|
47
|
-
function isSafeProxyPathSegment(segment) {
|
|
48
|
-
if (!segment) return false;
|
|
49
|
-
if (segment === "." || segment === "..") return false;
|
|
50
|
-
if (segment.includes("\\") || segment.includes("/")) return false;
|
|
51
|
-
if (/[\u0000-\u001F\u007F]/.test(segment)) return false;
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
function extractTrailingVersion(pathname) {
|
|
55
|
-
const m = pathname.match(/\/(v\d+(?:\.\d+)?)\/?$/i);
|
|
56
|
-
return m?.[1] ?? null;
|
|
57
|
-
}
|
|
58
|
-
function extractLeadingVersion(path) {
|
|
59
|
-
const m = path.match(/^(v\d+(?:\.\d+)?)(?:\/|$)/i);
|
|
60
|
-
return m?.[1] ?? null;
|
|
61
|
-
}
|
|
62
|
-
function parseConnectionHeaderTokens(headers) {
|
|
63
|
-
const connectionValue = (headers.get("connection") || "").trim();
|
|
64
|
-
if (!connectionValue) return /* @__PURE__ */ new Set();
|
|
65
|
-
return new Set(
|
|
66
|
-
connectionValue.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
function shouldDropHopByHopHeader(name, connectionTokens) {
|
|
70
|
-
const lower = name.toLowerCase();
|
|
71
|
-
return RFC_HOP_BY_HOP_HEADERS.has(lower) || connectionTokens.has(lower);
|
|
72
|
-
}
|
|
73
|
-
var PayloadTooLargeError = class extends Error {
|
|
74
|
-
code = "PAYLOAD_TOO_LARGE";
|
|
75
|
-
constructor(message) {
|
|
76
|
-
super(message);
|
|
77
|
-
this.name = "PayloadTooLargeError";
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
function createSizeLimitedBodyStream(stream, maxBytes) {
|
|
81
|
-
let totalBytes = 0;
|
|
82
|
-
return stream.pipeThrough(
|
|
83
|
-
new TransformStream({
|
|
84
|
-
transform(chunk, controller) {
|
|
85
|
-
totalBytes += chunk.byteLength;
|
|
86
|
-
if (totalBytes > maxBytes) {
|
|
87
|
-
controller.error(new PayloadTooLargeError("Payload too large"));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
controller.enqueue(chunk);
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
function isPayloadTooLargeError(error) {
|
|
96
|
-
const queue = [error];
|
|
97
|
-
const visited = /* @__PURE__ */ new Set();
|
|
98
|
-
while (queue.length > 0) {
|
|
99
|
-
const current = queue.shift();
|
|
100
|
-
if (!current || visited.has(current)) continue;
|
|
101
|
-
visited.add(current);
|
|
102
|
-
if (current instanceof PayloadTooLargeError) return true;
|
|
103
|
-
if (current instanceof Error) {
|
|
104
|
-
if (current.name === "PayloadTooLargeError") return true;
|
|
105
|
-
const code = current.code;
|
|
106
|
-
if (code === "PAYLOAD_TOO_LARGE") return true;
|
|
107
|
-
const cause = current.cause;
|
|
108
|
-
if (cause) queue.push(cause);
|
|
109
|
-
} else if (typeof current === "object") {
|
|
110
|
-
const record = current;
|
|
111
|
-
if (record.code === "PAYLOAD_TOO_LARGE") return true;
|
|
112
|
-
if (typeof record.name === "string" && record.name === "PayloadTooLargeError") return true;
|
|
113
|
-
if (record.cause) queue.push(record.cause);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/api-route-handler.ts
|
|
120
32
|
var _validatedApiUrl;
|
|
121
33
|
function getApiBaseUrl() {
|
|
122
34
|
if (_validatedApiUrl !== void 0) return _validatedApiUrl;
|
|
@@ -462,4 +374,4 @@ export {
|
|
|
462
374
|
PATCH,
|
|
463
375
|
OPTIONS
|
|
464
376
|
};
|
|
465
|
-
//# sourceMappingURL=chunk-
|
|
377
|
+
//# sourceMappingURL=chunk-NFJ25NQQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api-route-handler.ts"],"sourcesContent":["/**\n * API proxy route handler for Next.js App Router.\n *\n * Proxies requests from the frontend to the backend API, handling:\n * - Session token management (cookie-based auth)\n * - CSRF protection and CORS headers\n * - Login rate limiting\n * - Request body size limits\n * - Path normalization and version deduplication\n *\n * @module api-route-handler\n */\n\nimport { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { logInfo, logWarn } from \"@spring-systems/core/logger\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport {\n createSizeLimitedBodyStream,\n extractLeadingVersion,\n extractTrailingVersion,\n isPayloadTooLargeError,\n isSafeProxyPathSegment,\n normalizePath,\n parseConnectionHeaderTokens,\n parseContentLength,\n shouldDropHopByHopHeader,\n toValidPositiveInteger,\n} from \"./api-route-utils\";\nimport {\n applyCorsHeaders,\n cleanupExpiredLoginLimits,\n clearLoginAttemptState,\n clearSessionCookie,\n getLoginRateLimitKeys,\n getRateLimitRetryAfterMs,\n getSessionToken,\n isInternalIpAccess,\n isLocalHostRequest,\n registerFailedLoginAttempt,\n resolveProdSecurityConfigError,\n setSessionCookie,\n shouldClearSessionFromForbidden,\n shouldRejectByCsrfProtection,\n} from \"./handlers\";\nimport type { RateLimitKeys } from \"./handlers/rate-limit-handler\";\n\n// ---------------------------------------------------------------------------\n// Config constants\n// ---------------------------------------------------------------------------\n\ntype LoginResponsePayload = Record<string, unknown>;\n\nlet _validatedApiUrl: string | undefined;\nfunction getApiBaseUrl(): string {\n if (_validatedApiUrl !== undefined) return _validatedApiUrl;\n const url = (process.env.API_URL || \"\").trim();\n if (!url) throw new Error(\"API_URL environment variable is not set\");\n try {\n new URL(url);\n } catch {\n throw new Error(`API_URL is not a valid URL: \"${url}\"`);\n }\n _validatedApiUrl = url;\n return url;\n}\n\n/** Parse a byte-limit env variable. Throws at startup on invalid values (fail-fast). */\nfunction parseByteLimit(envName: string, fallback: number): number {\n const raw = process.env[envName];\n if (!raw) return fallback;\n const parsed = Number(raw);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new Error(`${envName} must be a positive number, got: \"${raw}\"`);\n }\n return parsed;\n}\n\nconst NON_MULTIPART_BODY_MAX_BYTES = parseByteLimit(\"API_PROXY_NON_MULTIPART_MAX_BYTES\", 10_485_760); // 10 MB\nconst MULTIPART_BODY_MAX_BYTES = parseByteLimit(\"API_PROXY_MULTIPART_MAX_BYTES\", 52_428_800); // 50 MB\nconst LOGIN_FAILURE_STATUSES = new Set([400, 401, 403, 404, 429, 502]);\nconst AUTH_DEBUG = process.env.AUTH_DEBUG === \"true\";\n\n// Config accessors — read fresh from getFrameworkConfig() on each call so\n// configureFramework() can be called after module import.\nfunction getPublicAuthPaths(): Set<string> {\n return new Set(getFrameworkConfig().auth.publicAuthPaths);\n}\n\nfunction getSafeHashPattern(): RegExp | undefined {\n return getFrameworkConfig().proxy.safeHashPattern;\n}\n\nfunction getMaxProxyPathLength(): number {\n return getFrameworkConfig().proxy.maxProxyPathLength;\n}\n\nfunction getProdSecurityConfigError(): string | null {\n return resolveProdSecurityConfigError();\n}\n\n// ---------------------------------------------------------------------------\n// Utility helpers\n// ---------------------------------------------------------------------------\n\nfunction getBodyLimitBytes(isMultipart: boolean): number {\n return isMultipart\n ? toValidPositiveInteger(MULTIPART_BODY_MAX_BYTES, 52_428_800)\n : toValidPositiveInteger(NON_MULTIPART_BODY_MAX_BYTES, 10_485_760);\n}\n\nfunction authDebug(event: string, data?: Record<string, unknown>) {\n if (!AUTH_DEBUG) return;\n logInfo(`AuthProxy.${event}`, data);\n}\n\nfunction sanitizeIncomingHash(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n const pattern = getSafeHashPattern();\n if (!pattern || !pattern.test(trimmed)) return \"\";\n return trimmed;\n}\n\nfunction createJsonErrorResponse(\n request: NextRequest,\n error: string,\n status: number,\n headersInit?: HeadersInit,\n): NextResponse {\n const headers = new Headers(headersInit);\n applyCorsHeaders(headers, request, isInternalIpAccess(request));\n return NextResponse.json({ error }, { status, headers });\n}\n\n// ---------------------------------------------------------------------------\n// HTTP method exports\n// ---------------------------------------------------------------------------\n\n/** Handle GET requests through the API proxy. */\nexport async function GET(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"GET\");\n}\n\n/** Handle POST requests through the API proxy. */\nexport async function POST(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"POST\");\n}\n\n/** Handle PUT requests through the API proxy. */\nexport async function PUT(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"PUT\");\n}\n\n/** Handle DELETE requests through the API proxy. */\nexport async function DELETE(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"DELETE\");\n}\n\n/** Handle PATCH requests through the API proxy. */\nexport async function PATCH(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"PATCH\");\n}\n\n// ---------------------------------------------------------------------------\n// Main request handler\n// ---------------------------------------------------------------------------\n\nasync function handleRequest(request: NextRequest, pathSegments: string[], method: string) {\n const incomingPath = pathSegments.join(\"/\");\n try {\n cleanupExpiredLoginLimits(Date.now());\n\n const prodSecurityError = getProdSecurityConfigError();\n if (prodSecurityError) {\n return createJsonErrorResponse(request, prodSecurityError, 500);\n }\n\n // --- Path validation ---\n if (!pathSegments.every((segment) => isSafeProxyPathSegment(segment))) {\n return createJsonErrorResponse(request, \"Invalid path\", 400);\n }\n if (incomingPath.length > getMaxProxyPathLength()) {\n return createJsonErrorResponse(request, \"Path too long\", 414);\n }\n\n // --- URL building ---\n const rawApiBase = getApiBaseUrl().replace(/\\/+$/, \"\");\n if (!rawApiBase) {\n return createJsonErrorResponse(request, \"API_URL is not configured\", 500);\n }\n\n const apiBaseUrl = new URL(rawApiBase);\n const apiBasePath = apiBaseUrl.pathname;\n const baseVersion = extractTrailingVersion(apiBasePath);\n const incomingVersion = extractLeadingVersion(incomingPath);\n\n let normalizedPath = incomingPath;\n if (baseVersion && incomingVersion && baseVersion.toLowerCase() === incomingVersion.toLowerCase()) {\n normalizedPath = incomingPath.replace(new RegExp(`^${incomingVersion}/?`, \"i\"), \"\");\n }\n\n const targetBase = `${rawApiBase}/${normalizedPath.replace(/^\\/+/, \"\")}`;\n const normalizedPathKey = normalizePath(normalizedPath);\n const authConfig = getFrameworkConfig().auth;\n const loginPath = (authConfig.loginPath ?? \"auth/login\").toLowerCase();\n const logoutPath = (authConfig.logoutPath ?? \"auth/logout\").toLowerCase();\n const infoPath = (authConfig.infoPath ?? \"auth/info\").toLowerCase();\n const isLoginRoute = normalizedPathKey === loginPath;\n const isLogoutRoute = normalizedPathKey === logoutPath;\n const isAuthRoute =\n normalizedPathKey === loginPath || normalizedPathKey === logoutPath || normalizedPathKey === infoPath;\n const inUrl = new URL(request.url);\n const targetUrl = new URL(targetBase);\n inUrl.searchParams.forEach((v, k) => {\n if (!targetUrl.searchParams.has(k)) targetUrl.searchParams.set(k, v);\n });\n\n // --- Headers ---\n const headers = new Headers();\n const requestConnectionTokens = parseConnectionHeaderTokens(request.headers);\n request.headers.forEach((value, key) => {\n const lower = key.toLowerCase();\n if (\n lower === \"host\" ||\n lower === \"content-length\" ||\n lower === \"cookie\" ||\n lower === \"authorization\" ||\n shouldDropHopByHopHeader(lower, requestConnectionTokens)\n ) {\n return;\n }\n headers.set(key, value);\n });\n\n if (!headers.has(\"X-Requested-With\")) {\n headers.set(\"X-Requested-With\", \"XMLHttpRequest\");\n }\n\n // --- Auth ---\n const sessionToken = getSessionToken(request);\n const incomingAuthorization = (request.headers.get(\"authorization\") || \"\").trim();\n const hasBearerAuthorization = /^Bearer\\s+\\S+$/i.test(incomingAuthorization);\n authDebug(\"incoming-auth\", {\n path: normalizedPathKey,\n method,\n hasSessionToken: !!sessionToken,\n hasBearerAuthorization,\n host: request.nextUrl.host,\n protocol: request.nextUrl.protocol,\n });\n\n // Short-circuit auth/info when unauthenticated\n if (normalizedPathKey === infoPath && !sessionToken && !hasBearerAuthorization) {\n const res = new NextResponse(null, { status: 204 });\n applyCorsHeaders(res.headers, request, isInternalIpAccess(request));\n res.headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n res.headers.set(\"Pragma\", \"no-cache\");\n res.headers.set(\"Expires\", \"0\");\n clearSessionCookie(res, request);\n authDebug(\"short-circuit-auth-info-no-token\", {\n host: request.nextUrl.host,\n protocol: request.nextUrl.protocol,\n });\n return res;\n }\n\n // CSRF check\n if (shouldRejectByCsrfProtection(request, method, normalizedPathKey)) {\n logWarn(\n \"ApiProxy.CSRF\",\n `Blocked ${method} ${normalizedPathKey} — origin: ${request.headers.get(\"origin\") ?? \"none\"}, referer: ${request.headers.get(\"referer\") ?? \"none\"}, sec-fetch-site: ${request.headers.get(\"sec-fetch-site\") ?? \"none\"}`,\n );\n return createJsonErrorResponse(request, \"Forbidden\", 403);\n }\n\n // Set Authorization header\n if (sessionToken && !getPublicAuthPaths().has(normalizedPathKey)) {\n headers.set(\"Authorization\", `Bearer ${sessionToken}`);\n } else if (hasBearerAuthorization) {\n headers.set(\"Authorization\", incomingAuthorization);\n }\n\n // --- Hash handling ---\n // Only trust X-Hash header (sent via fetch with proper CORS).\n // URL params and cookies are untrusted sources and could be spoofed.\n const incomingHash = request.headers.get(\"x-hash\") || \"\";\n\n const sanitizedHash = sanitizeIncomingHash(incomingHash);\n if (sanitizedHash) {\n headers.set(\"X-hash\", sanitizedHash);\n headers.set(\"Hash\", sanitizedHash);\n if (!targetUrl.searchParams.has(\"hash\")) targetUrl.searchParams.set(\"hash\", sanitizedHash);\n if (!targetUrl.searchParams.has(\"Hash\")) targetUrl.searchParams.set(\"Hash\", sanitizedHash);\n }\n\n // --- Body + rate limiting ---\n let body = undefined;\n let loginRateLimitKeys: RateLimitKeys | null = null;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const contentType = request.headers.get(\"content-type\") || \"\";\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const bodyLimit = getBodyLimitBytes(isMultipart);\n const contentLength = parseContentLength(request.headers.get(\"content-length\"));\n if (contentLength !== null && contentLength > bodyLimit) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n\n if (isMultipart) {\n if (contentLength === null) {\n return createJsonErrorResponse(request, \"Content-Length required for multipart payload\", 411);\n }\n body = request.body ? createSizeLimitedBodyStream(request.body, bodyLimit) : request.body;\n } else {\n body = await request.arrayBuffer();\n if (body.byteLength > bodyLimit) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n if (isLoginRoute && contentType.includes(\"application/json\")) {\n try {\n const rawText = new TextDecoder().decode(body);\n const parsed = JSON.parse(rawText) as Record<string, unknown>;\n const usernameField = authConfig.loginUsernameField ?? \"username\";\n loginRateLimitKeys = getLoginRateLimitKeys(request, String(parsed[usernameField] || \"\"));\n const retryAfterMs = getRateLimitRetryAfterMs(loginRateLimitKeys, Date.now());\n if (retryAfterMs > 0) {\n logWarn(\n \"ApiProxy.RateLimit\",\n `Login rate-limited for key ${loginRateLimitKeys.pairKey} — retry after ${Math.ceil(retryAfterMs / 1000)}s`,\n );\n return createJsonErrorResponse(request, \"Too many login attempts. Try again later.\", 429, {\n \"Retry-After\": String(Math.ceil(retryAfterMs / 1000)),\n });\n }\n } catch {\n loginRateLimitKeys = getLoginRateLimitKeys(request, \"\");\n }\n }\n }\n }\n\n // --- Proxy fetch ---\n const fetchOptions: RequestInit & { duplex?: string } = {\n method,\n headers,\n body,\n redirect: \"manual\",\n signal: AbortSignal.timeout(getFrameworkConfig().api?.timeoutMs ?? 30_000),\n };\n if (body instanceof ReadableStream) fetchOptions.duplex = \"half\";\n\n const response = await fetch(targetUrl.toString(), fetchOptions);\n\n // --- Response headers ---\n const responseHeaders = new Headers();\n const responseConnectionTokens = parseConnectionHeaderTokens(response.headers);\n response.headers.forEach((value, key) => {\n const lower = key.toLowerCase();\n if (lower === \"content-encoding\" || lower === \"content-length\") return;\n if (shouldDropHopByHopHeader(lower, responseConnectionTokens)) return;\n responseHeaders.set(key, value);\n });\n\n // --- Login token extraction ---\n // Body is assigned after shouldClearSessionFromForbidden() to avoid\n // stream locking — clone() inside that function tees the original stream.\n let bodyToReturn: BodyInit | ReadableStream<Uint8Array> | null = null;\n let loginToken = \"\";\n let finalStatus = response.status;\n\n if (isLoginRoute) {\n const contentType = (response.headers.get(\"content-type\") || \"\").toLowerCase();\n if (contentType.includes(\"application/json\")) {\n try {\n const rawBody = await response.text();\n const payload = JSON.parse(rawBody) as LoginResponsePayload;\n const tokenFields = authConfig.tokenResponseFields ?? {\n accessToken: \"access_token\",\n refreshToken: \"refresh_token\",\n };\n const accessTokenValue = payload[tokenFields.accessToken];\n const accessToken = typeof accessTokenValue === \"string\" ? accessTokenValue.trim() : \"\";\n if (accessToken) {\n loginToken = accessToken;\n }\n // Always strip tokens from login responses (success and error)\n // to prevent token leakage to the client\n const sanitizedPayload = { ...payload };\n delete sanitizedPayload[tokenFields.accessToken];\n delete sanitizedPayload[tokenFields.refreshToken];\n bodyToReturn = JSON.stringify(sanitizedPayload);\n responseHeaders.set(\"content-type\", \"application/json; charset=utf-8\");\n responseHeaders.delete(\"content-length\");\n } catch {\n // If JSON parsing fails, return minimal error to avoid leaking raw body\n bodyToReturn = JSON.stringify({ error: \"Invalid response from authentication service\" });\n // Upstream returned malformed JSON on login endpoint.\n // Surface this as a gateway failure instead of forwarding a misleading success status.\n finalStatus = 502;\n responseHeaders.set(\"content-type\", \"application/json; charset=utf-8\");\n responseHeaders.delete(\"content-length\");\n }\n }\n }\n\n // --- CORS + cache headers ---\n const ipAccess = isInternalIpAccess(request);\n applyCorsHeaders(responseHeaders, request, ipAccess);\n if (isAuthRoute) {\n responseHeaders.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n responseHeaders.set(\"Pragma\", \"no-cache\");\n responseHeaders.set(\"Expires\", \"0\");\n } else if (!responseHeaders.has(\"Cache-Control\")) {\n // Prevent intermediate proxies/CDNs from caching API responses\n // containing potentially sensitive data. The upstream API can\n // override this by setting its own Cache-Control header.\n responseHeaders.set(\"Cache-Control\", \"private, no-store\");\n }\n if (isLogoutRoute && response.status >= 200 && response.status < 300 && !isLocalHostRequest(request)) {\n responseHeaders.set(\"Clear-Site-Data\", '\"cache\", \"storage\"');\n }\n\n const shouldClear403Session = await shouldClearSessionFromForbidden(response);\n\n // Assign body AFTER potential clone() in shouldClearSessionFromForbidden\n // to get a readable tee branch instead of the locked original stream.\n if (!bodyToReturn) bodyToReturn = response.body;\n\n const nextResponse = new NextResponse(bodyToReturn, {\n status: finalStatus,\n headers: responseHeaders,\n });\n\n // --- Session cookie management ---\n if (loginToken && response.status >= 200 && response.status < 300) {\n setSessionCookie(nextResponse, request, loginToken);\n if (loginRateLimitKeys) {\n clearLoginAttemptState(loginRateLimitKeys);\n }\n } else if (isLoginRoute && loginRateLimitKeys && LOGIN_FAILURE_STATUSES.has(finalStatus)) {\n registerFailedLoginAttempt(loginRateLimitKeys, Date.now());\n } else if (\n isLogoutRoute ||\n response.status === 401 ||\n (normalizedPathKey === infoPath && response.status === 403) ||\n shouldClear403Session\n ) {\n authDebug(\"clear-session-cookie\", {\n path: normalizedPathKey,\n method,\n status: response.status,\n reason: isLogoutRoute\n ? \"logout\"\n : response.status === 401\n ? \"status_401\"\n : normalizedPathKey === infoPath && response.status === 403\n ? \"auth_info_403\"\n : shouldClear403Session\n ? \"forbidden_session_text\"\n : \"unknown\",\n hadSessionToken: !!sessionToken,\n });\n clearSessionCookie(nextResponse, request);\n }\n\n return nextResponse;\n } catch (error: unknown) {\n if (isPayloadTooLargeError(error)) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n const isDev = process.env.NODE_ENV === \"development\";\n const response = NextResponse.json(\n {\n error: \"Proxy request failed\",\n ...(isDev ? { details: error instanceof Error ? error.message : String(error) } : {}),\n },\n { status: 500 },\n );\n\n const normalizedIncomingPath = normalizePath(incomingPath).replace(/^v\\d+(?:\\.\\d+)?\\/?/i, \"\");\n const catchLogoutPath = (getFrameworkConfig().auth.logoutPath ?? \"auth/logout\").toLowerCase();\n applyCorsHeaders(response.headers, request, isInternalIpAccess(request));\n if (normalizedIncomingPath === catchLogoutPath) {\n clearSessionCookie(response, request);\n response.headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n response.headers.set(\"Pragma\", \"no-cache\");\n response.headers.set(\"Expires\", \"0\");\n authDebug(\"clear-session-cookie\", {\n path: normalizedIncomingPath,\n method,\n status: 500,\n reason: \"logout_proxy_failure\",\n });\n }\n\n return response;\n }\n}\n\n/** Handle CORS preflight requests. */\nexport async function OPTIONS(request: NextRequest) {\n const headers = new Headers();\n const ipAccess = isInternalIpAccess(request);\n applyCorsHeaders(headers, request, ipAccess);\n return new NextResponse(null, { status: 204, headers });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -289,10 +289,103 @@ function resolveProdSecurityConfigError() {
|
|
|
289
289
|
return null;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
// src/
|
|
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
|
+
]);
|
|
293
304
|
function toValidPositiveInteger(value, fallback) {
|
|
294
305
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
295
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
|
|
296
389
|
function isRateLimitEnabled() {
|
|
297
390
|
return process.env.AUTH_LOGIN_RATE_LIMIT_ENABLED !== "false";
|
|
298
391
|
}
|
|
@@ -314,7 +407,10 @@ function getPolicy() {
|
|
|
314
407
|
return {
|
|
315
408
|
windowMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_WINDOW_MS || "900000"), 9e5),
|
|
316
409
|
blockMs: toValidPositiveInteger(Number(process.env.AUTH_LOGIN_RATE_LIMIT_BLOCK_MS || "900000"), 9e5),
|
|
317
|
-
maxAttemptsByIpAndAccount: toValidPositiveInteger(
|
|
410
|
+
maxAttemptsByIpAndAccount: toValidPositiveInteger(
|
|
411
|
+
Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS || "8"),
|
|
412
|
+
8
|
|
413
|
+
),
|
|
318
414
|
maxAttemptsByAccount: toValidPositiveInteger(
|
|
319
415
|
Number(process.env.AUTH_LOGIN_RATE_LIMIT_ACCOUNT_MAX_ATTEMPTS || "20"),
|
|
320
416
|
20
|
|
@@ -382,6 +478,16 @@ function clearLoginAttemptState(keys) {
|
|
|
382
478
|
}
|
|
383
479
|
|
|
384
480
|
export {
|
|
481
|
+
toValidPositiveInteger,
|
|
482
|
+
parseContentLength,
|
|
483
|
+
normalizePath,
|
|
484
|
+
isSafeProxyPathSegment,
|
|
485
|
+
extractTrailingVersion,
|
|
486
|
+
extractLeadingVersion,
|
|
487
|
+
parseConnectionHeaderTokens,
|
|
488
|
+
shouldDropHopByHopHeader,
|
|
489
|
+
createSizeLimitedBodyStream,
|
|
490
|
+
isPayloadTooLargeError,
|
|
385
491
|
isLocalHostRequest,
|
|
386
492
|
useSecureCookies,
|
|
387
493
|
resolveSessionCookieName,
|
|
@@ -404,4 +510,4 @@ export {
|
|
|
404
510
|
registerFailedLoginAttempt,
|
|
405
511
|
clearLoginAttemptState
|
|
406
512
|
};
|
|
407
|
-
//# sourceMappingURL=chunk-
|
|
513
|
+
//# sourceMappingURL=chunk-PZWKMIA4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/proxy-middleware.ts"],"sourcesContent":["import { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport { BASE_SECURITY_HEADER_VALUES } from \"./security-headers\";\n\nfunction toOriginSource(value: string): string[] {\n const raw = value.trim();\n if (!raw) return [];\n\n try {\n const parsed = new URL(raw);\n const out = [parsed.origin];\n\n if (parsed.protocol === \"https:\") {\n out.push(`wss://${parsed.host}`);\n } else if (parsed.protocol === \"http:\") {\n out.push(`ws://${parsed.host}`);\n }\n\n return out;\n } catch {\n return [];\n }\n}\n\nconst BLOCKED_CSP_SCHEMES = new Set([\"javascript:\", \"data:\", \"blob:\", \"vbscript:\", \"filesystem:\"]);\n\nfunction parseCspSource(token: string): string | null {\n const value = token.trim();\n if (!value) return null;\n\n // Prevent header/CSP injection via env values.\n if (/[\\s;,\\r\\n]/.test(value)) return null;\n\n // Allow safe scheme sources (e.g. https:, wss:). Block dangerous schemes\n // that enable XSS or data exfiltration when used in CSP directives.\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {\n const lower = value.toLowerCase();\n if (BLOCKED_CSP_SCHEMES.has(lower)) return null;\n return lower;\n }\n\n try {\n const parsed = new URL(value);\n if (![\"http:\", \"https:\", \"ws:\", \"wss:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n if (parsed.pathname !== \"/\" || parsed.search || parsed.hash) return null;\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n return null;\n }\n}\n\nfunction parseReportUri(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n // Block header injection characters and double-quotes (used in Reporting-Endpoints structured header).\n if (/[\\s;,\\r\\n\"]/.test(trimmed)) return null;\n try {\n const parsed = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n return parsed.toString();\n } catch {\n return null;\n }\n}\n\nfunction parseCspList(value: string): string[] {\n const parsed = value\n .split(\",\")\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n return [...new Set(parsed)];\n}\n\nexport function proxy(req: NextRequest) {\n const url = req.nextUrl.clone();\n\n const TARGET_ENV = process.env.TARGET_ENV || \"test\";\n const isProdRuntime = TARGET_ENV === \"prod\" || process.env.NODE_ENV === \"production\";\n const isTargetProd = TARGET_ENV === \"prod\";\n\n const isLocalHost = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\";\n\n const nonce = crypto.randomUUID().replace(/-/g, \"\");\n const allowUnsafeStyleAttr = process.env.CSP_ALLOW_UNSAFE_STYLE_ATTR === \"true\";\n const denyStyleAttr = isTargetProd && !allowUnsafeStyleAttr;\n const allowUnsafeInlineStyle = !isTargetProd;\n\n const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || \"\");\n const apiConnectSources = toOriginSource(process.env.API_URL || \"\");\n const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || \"\");\n const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || \"\");\n const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || \"\");\n\n const cspConfig = getFrameworkConfig().csp;\n\n // Validate framework config CSP sources through parseCspSource to prevent injection\n const validatedConnectSources = cspConfig.thirdPartyConnectSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedImgSources = cspConfig.thirdPartyImageSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedScriptSources = cspConfig.thirdPartyScriptSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedFrameSources = cspConfig.thirdPartyFrameSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n\n const connectSrc = [\n \"'self'\",\n ...validatedConnectSources,\n ...apiConnectSources,\n ...connectHostsEnv,\n ...(!isProdRuntime ? [\"http://localhost:*\", \"http://127.0.0.1:*\", \"ws://localhost:*\", \"ws://127.0.0.1:*\"] : []),\n ].join(\" \");\n\n const imgSrc = [\"'self'\", \"data:\", \"blob:\", ...validatedImgSources, ...imgHostsEnv].join(\" \");\n\n const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(\" \");\n const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(\" \");\n\n const reportUri = parseReportUri(process.env.CSP_REPORT_URI || \"\");\n\n const cspDirectives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n `script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n \"script-src-attr 'none'\",\n allowUnsafeInlineStyle ? \"style-src 'self' 'unsafe-inline'\" : `style-src 'self' 'nonce-${nonce}'`,\n denyStyleAttr ? \"style-src-attr 'none'\" : \"style-src-attr 'unsafe-inline'\",\n `img-src ${imgSrc}`,\n \"media-src 'self' blob: data:\",\n \"manifest-src 'self'\",\n `connect-src ${connectSrc}`,\n `frame-src ${frameSrcHosts || \"'none'\"}`,\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n ...(isTargetProd ? [\"upgrade-insecure-requests\"] : []),\n ...(reportUri ? [`report-uri ${reportUri}`, \"report-to csp-violations\"] : []),\n ];\n const csp = cspDirectives.join(\"; \");\n\n const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];\n if (isProdRuntime) {\n for (const prefix of blockedPrefixes) {\n if (url.pathname.startsWith(prefix)) {\n const res = new NextResponse(\"Not Found\", { status: 404 });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n }\n }\n\n if (url.pathname === \"/\") {\n const dest = new URL(url.toString());\n if (!isLocalHost && isTargetProd) dest.protocol = \"https:\";\n dest.pathname = getFrameworkConfig().app.defaultRoute;\n dest.search = \"\";\n\n const res = makeRedirect(dest, 308);\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set(\"x-nonce\", nonce);\n\n const res = NextResponse.next({ request: { headers: requestHeaders } });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n}\n\nfunction setSecurity(\n res: NextResponse,\n opts: { isLocalHost: boolean; isTargetProd: boolean; csp?: string; nonce?: string; reportUri?: string | null },\n) {\n const { isLocalHost, csp, nonce, reportUri } = opts;\n if (csp) res.headers.set(\"Content-Security-Policy\", csp);\n for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {\n res.headers.set(key, value);\n }\n if (opts.isTargetProd && !isLocalHost)\n res.headers.set(\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\");\n if (reportUri) res.headers.set(\"Reporting-Endpoints\", `csp-violations=\"${reportUri}\"`);\n if (nonce) res.headers.set(\"x-nonce\", nonce);\n return res;\n}\n\nfunction makeRedirect(to: URL | string, status = 308) {\n const res = new NextResponse(null, { status });\n res.headers.set(\"Location\", typeof to === \"string\" ? to : to.toString());\n return res;\n}\n\nexport const proxyConfig = {\n matcher: [\"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)\"],\n};\n"],"mappings":"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","names":["res"]}
|
package/dist/client.js
CHANGED
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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.
|
|
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":[]}
|
package/dist/handlers/index.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
proxy,
|
|
3
3
|
proxyConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-YB7NX4IX.js";
|
|
5
5
|
import {
|
|
6
6
|
DELETE,
|
|
7
7
|
GET,
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
PATCH,
|
|
10
10
|
POST,
|
|
11
11
|
PUT
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-NFJ25NQQ.js";
|
|
13
|
+
import "./chunk-PZWKMIA4.js";
|
|
14
14
|
import {
|
|
15
15
|
GET as GET2,
|
|
16
16
|
RuntimeEnvScript
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// src/index.ts
|
|
26
26
|
import "server-only";
|
|
27
|
-
var SPRING_SERVER_VERSION = true ? "0.8.
|
|
27
|
+
var SPRING_SERVER_VERSION = true ? "0.8.3" : "0.0.0-dev";
|
|
28
28
|
export {
|
|
29
29
|
BASE_SECURITY_HEADERS,
|
|
30
30
|
BASE_SECURITY_HEADER_VALUES,
|
package/dist/proxy-middleware.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spring-systems/server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Next.js server-only code for the Spring Systems framework (proxy, API routes, runtime env)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -90,8 +90,9 @@
|
|
|
90
90
|
"dev": "tsup --watch",
|
|
91
91
|
"typecheck": "tsc --noEmit",
|
|
92
92
|
"test": "vitest run",
|
|
93
|
+
"test:ci": "vitest run --coverage",
|
|
93
94
|
"check:exports": "node scripts/check-exports.mjs",
|
|
94
|
-
"prepack": "pnpm run build && pnpm check:exports",
|
|
95
|
+
"prepack": "pnpm typecheck && pnpm lint && pnpm run build && pnpm check:exports",
|
|
95
96
|
"check:circular": "madge --circular --extensions ts,tsx --ts-config ../../tsconfig.base.json ./src",
|
|
96
97
|
"lint": "eslint src/"
|
|
97
98
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-route-handler.ts","../src/api-route-utils.ts"],"sourcesContent":["/**\n * API proxy route handler for Next.js App Router.\n *\n * Proxies requests from the frontend to the backend API, handling:\n * - Session token management (cookie-based auth)\n * - CSRF protection and CORS headers\n * - Login rate limiting\n * - Request body size limits\n * - Path normalization and version deduplication\n *\n * @module api-route-handler\n */\n\nimport { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { logInfo, logWarn } from \"@spring-systems/core/logger\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport {\n createSizeLimitedBodyStream,\n extractLeadingVersion,\n extractTrailingVersion,\n isPayloadTooLargeError,\n isSafeProxyPathSegment,\n normalizePath,\n parseConnectionHeaderTokens,\n parseContentLength,\n shouldDropHopByHopHeader,\n toValidPositiveInteger,\n} from \"./api-route-utils\";\nimport {\n applyCorsHeaders,\n cleanupExpiredLoginLimits,\n clearLoginAttemptState,\n clearSessionCookie,\n getLoginRateLimitKeys,\n getRateLimitRetryAfterMs,\n getSessionToken,\n isInternalIpAccess,\n isLocalHostRequest,\n registerFailedLoginAttempt,\n resolveProdSecurityConfigError,\n setSessionCookie,\n shouldClearSessionFromForbidden,\n shouldRejectByCsrfProtection,\n} from \"./handlers\";\nimport type { RateLimitKeys } from \"./handlers/rate-limit-handler\";\n\n// ---------------------------------------------------------------------------\n// Config constants\n// ---------------------------------------------------------------------------\n\ntype LoginResponsePayload = Record<string, unknown>;\n\nlet _validatedApiUrl: string | undefined;\nfunction getApiBaseUrl(): string {\n if (_validatedApiUrl !== undefined) return _validatedApiUrl;\n const url = (process.env.API_URL || \"\").trim();\n if (!url) throw new Error(\"API_URL environment variable is not set\");\n try {\n new URL(url);\n } catch {\n throw new Error(`API_URL is not a valid URL: \"${url}\"`);\n }\n _validatedApiUrl = url;\n return url;\n}\n\n/** Parse a byte-limit env variable. Throws at startup on invalid values (fail-fast). */\nfunction parseByteLimit(envName: string, fallback: number): number {\n const raw = process.env[envName];\n if (!raw) return fallback;\n const parsed = Number(raw);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n throw new Error(`${envName} must be a positive number, got: \"${raw}\"`);\n }\n return parsed;\n}\n\nconst NON_MULTIPART_BODY_MAX_BYTES = parseByteLimit(\"API_PROXY_NON_MULTIPART_MAX_BYTES\", 10_485_760); // 10 MB\nconst MULTIPART_BODY_MAX_BYTES = parseByteLimit(\"API_PROXY_MULTIPART_MAX_BYTES\", 52_428_800); // 50 MB\nconst LOGIN_FAILURE_STATUSES = new Set([400, 401, 403, 404, 429, 502]);\nconst AUTH_DEBUG = process.env.AUTH_DEBUG === \"true\";\n\n// Config accessors — read fresh from getFrameworkConfig() on each call so\n// configureFramework() can be called after module import.\nfunction getPublicAuthPaths(): Set<string> {\n return new Set(getFrameworkConfig().auth.publicAuthPaths);\n}\n\nfunction getSafeHashPattern(): RegExp | undefined {\n return getFrameworkConfig().proxy.safeHashPattern;\n}\n\nfunction getMaxProxyPathLength(): number {\n return getFrameworkConfig().proxy.maxProxyPathLength;\n}\n\nfunction getProdSecurityConfigError(): string | null {\n return resolveProdSecurityConfigError();\n}\n\n// ---------------------------------------------------------------------------\n// Utility helpers\n// ---------------------------------------------------------------------------\n\nfunction getBodyLimitBytes(isMultipart: boolean): number {\n return isMultipart\n ? toValidPositiveInteger(MULTIPART_BODY_MAX_BYTES, 52_428_800)\n : toValidPositiveInteger(NON_MULTIPART_BODY_MAX_BYTES, 10_485_760);\n}\n\nfunction authDebug(event: string, data?: Record<string, unknown>) {\n if (!AUTH_DEBUG) return;\n logInfo(`AuthProxy.${event}`, data);\n}\n\nfunction sanitizeIncomingHash(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return \"\";\n const pattern = getSafeHashPattern();\n if (!pattern || !pattern.test(trimmed)) return \"\";\n return trimmed;\n}\n\nfunction createJsonErrorResponse(\n request: NextRequest,\n error: string,\n status: number,\n headersInit?: HeadersInit,\n): NextResponse {\n const headers = new Headers(headersInit);\n applyCorsHeaders(headers, request, isInternalIpAccess(request));\n return NextResponse.json({ error }, { status, headers });\n}\n\n// ---------------------------------------------------------------------------\n// HTTP method exports\n// ---------------------------------------------------------------------------\n\n/** Handle GET requests through the API proxy. */\nexport async function GET(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"GET\");\n}\n\n/** Handle POST requests through the API proxy. */\nexport async function POST(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"POST\");\n}\n\n/** Handle PUT requests through the API proxy. */\nexport async function PUT(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"PUT\");\n}\n\n/** Handle DELETE requests through the API proxy. */\nexport async function DELETE(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"DELETE\");\n}\n\n/** Handle PATCH requests through the API proxy. */\nexport async function PATCH(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {\n const { path } = await context.params;\n return handleRequest(request, path, \"PATCH\");\n}\n\n// ---------------------------------------------------------------------------\n// Main request handler\n// ---------------------------------------------------------------------------\n\nasync function handleRequest(request: NextRequest, pathSegments: string[], method: string) {\n const incomingPath = pathSegments.join(\"/\");\n try {\n cleanupExpiredLoginLimits(Date.now());\n\n const prodSecurityError = getProdSecurityConfigError();\n if (prodSecurityError) {\n return createJsonErrorResponse(request, prodSecurityError, 500);\n }\n\n // --- Path validation ---\n if (!pathSegments.every((segment) => isSafeProxyPathSegment(segment))) {\n return createJsonErrorResponse(request, \"Invalid path\", 400);\n }\n if (incomingPath.length > getMaxProxyPathLength()) {\n return createJsonErrorResponse(request, \"Path too long\", 414);\n }\n\n // --- URL building ---\n const rawApiBase = getApiBaseUrl().replace(/\\/+$/, \"\");\n if (!rawApiBase) {\n return createJsonErrorResponse(request, \"API_URL is not configured\", 500);\n }\n\n const apiBaseUrl = new URL(rawApiBase);\n const apiBasePath = apiBaseUrl.pathname;\n const baseVersion = extractTrailingVersion(apiBasePath);\n const incomingVersion = extractLeadingVersion(incomingPath);\n\n let normalizedPath = incomingPath;\n if (baseVersion && incomingVersion && baseVersion.toLowerCase() === incomingVersion.toLowerCase()) {\n normalizedPath = incomingPath.replace(new RegExp(`^${incomingVersion}/?`, \"i\"), \"\");\n }\n\n const targetBase = `${rawApiBase}/${normalizedPath.replace(/^\\/+/, \"\")}`;\n const normalizedPathKey = normalizePath(normalizedPath);\n const authConfig = getFrameworkConfig().auth;\n const loginPath = (authConfig.loginPath ?? \"auth/login\").toLowerCase();\n const logoutPath = (authConfig.logoutPath ?? \"auth/logout\").toLowerCase();\n const infoPath = (authConfig.infoPath ?? \"auth/info\").toLowerCase();\n const isLoginRoute = normalizedPathKey === loginPath;\n const isLogoutRoute = normalizedPathKey === logoutPath;\n const isAuthRoute =\n normalizedPathKey === loginPath || normalizedPathKey === logoutPath || normalizedPathKey === infoPath;\n const inUrl = new URL(request.url);\n const targetUrl = new URL(targetBase);\n inUrl.searchParams.forEach((v, k) => {\n if (!targetUrl.searchParams.has(k)) targetUrl.searchParams.set(k, v);\n });\n\n // --- Headers ---\n const headers = new Headers();\n const requestConnectionTokens = parseConnectionHeaderTokens(request.headers);\n request.headers.forEach((value, key) => {\n const lower = key.toLowerCase();\n if (\n lower === \"host\" ||\n lower === \"content-length\" ||\n lower === \"cookie\" ||\n lower === \"authorization\" ||\n shouldDropHopByHopHeader(lower, requestConnectionTokens)\n ) {\n return;\n }\n headers.set(key, value);\n });\n\n if (!headers.has(\"X-Requested-With\")) {\n headers.set(\"X-Requested-With\", \"XMLHttpRequest\");\n }\n\n // --- Auth ---\n const sessionToken = getSessionToken(request);\n const incomingAuthorization = (request.headers.get(\"authorization\") || \"\").trim();\n const hasBearerAuthorization = /^Bearer\\s+\\S+$/i.test(incomingAuthorization);\n authDebug(\"incoming-auth\", {\n path: normalizedPathKey,\n method,\n hasSessionToken: !!sessionToken,\n hasBearerAuthorization,\n host: request.nextUrl.host,\n protocol: request.nextUrl.protocol,\n });\n\n // Short-circuit auth/info when unauthenticated\n if (normalizedPathKey === infoPath && !sessionToken && !hasBearerAuthorization) {\n const res = new NextResponse(null, { status: 204 });\n applyCorsHeaders(res.headers, request, isInternalIpAccess(request));\n res.headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n res.headers.set(\"Pragma\", \"no-cache\");\n res.headers.set(\"Expires\", \"0\");\n clearSessionCookie(res, request);\n authDebug(\"short-circuit-auth-info-no-token\", {\n host: request.nextUrl.host,\n protocol: request.nextUrl.protocol,\n });\n return res;\n }\n\n // CSRF check\n if (shouldRejectByCsrfProtection(request, method, normalizedPathKey)) {\n logWarn(\n \"ApiProxy.CSRF\",\n `Blocked ${method} ${normalizedPathKey} — origin: ${request.headers.get(\"origin\") ?? \"none\"}, referer: ${request.headers.get(\"referer\") ?? \"none\"}, sec-fetch-site: ${request.headers.get(\"sec-fetch-site\") ?? \"none\"}`,\n );\n return createJsonErrorResponse(request, \"Forbidden\", 403);\n }\n\n // Set Authorization header\n if (sessionToken && !getPublicAuthPaths().has(normalizedPathKey)) {\n headers.set(\"Authorization\", `Bearer ${sessionToken}`);\n } else if (hasBearerAuthorization) {\n headers.set(\"Authorization\", incomingAuthorization);\n }\n\n // --- Hash handling ---\n // Only trust X-Hash header (sent via fetch with proper CORS).\n // URL params and cookies are untrusted sources and could be spoofed.\n const incomingHash = request.headers.get(\"x-hash\") || \"\";\n\n const sanitizedHash = sanitizeIncomingHash(incomingHash);\n if (sanitizedHash) {\n headers.set(\"X-hash\", sanitizedHash);\n headers.set(\"Hash\", sanitizedHash);\n if (!targetUrl.searchParams.has(\"hash\")) targetUrl.searchParams.set(\"hash\", sanitizedHash);\n if (!targetUrl.searchParams.has(\"Hash\")) targetUrl.searchParams.set(\"Hash\", sanitizedHash);\n }\n\n // --- Body + rate limiting ---\n let body = undefined;\n let loginRateLimitKeys: RateLimitKeys | null = null;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const contentType = request.headers.get(\"content-type\") || \"\";\n const isMultipart = contentType.includes(\"multipart/form-data\");\n const bodyLimit = getBodyLimitBytes(isMultipart);\n const contentLength = parseContentLength(request.headers.get(\"content-length\"));\n if (contentLength !== null && contentLength > bodyLimit) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n\n if (isMultipart) {\n if (contentLength === null) {\n return createJsonErrorResponse(request, \"Content-Length required for multipart payload\", 411);\n }\n body = request.body ? createSizeLimitedBodyStream(request.body, bodyLimit) : request.body;\n } else {\n body = await request.arrayBuffer();\n if (body.byteLength > bodyLimit) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n if (isLoginRoute && contentType.includes(\"application/json\")) {\n try {\n const rawText = new TextDecoder().decode(body);\n const parsed = JSON.parse(rawText) as Record<string, unknown>;\n const usernameField = authConfig.loginUsernameField ?? \"username\";\n loginRateLimitKeys = getLoginRateLimitKeys(request, String(parsed[usernameField] || \"\"));\n const retryAfterMs = getRateLimitRetryAfterMs(loginRateLimitKeys, Date.now());\n if (retryAfterMs > 0) {\n logWarn(\n \"ApiProxy.RateLimit\",\n `Login rate-limited for key ${loginRateLimitKeys.pairKey} — retry after ${Math.ceil(retryAfterMs / 1000)}s`,\n );\n return createJsonErrorResponse(request, \"Too many login attempts. Try again later.\", 429, {\n \"Retry-After\": String(Math.ceil(retryAfterMs / 1000)),\n });\n }\n } catch {\n loginRateLimitKeys = getLoginRateLimitKeys(request, \"\");\n }\n }\n }\n }\n\n // --- Proxy fetch ---\n const fetchOptions: RequestInit & { duplex?: string } = {\n method,\n headers,\n body,\n redirect: \"manual\",\n signal: AbortSignal.timeout(getFrameworkConfig().api?.timeoutMs ?? 30_000),\n };\n if (body instanceof ReadableStream) fetchOptions.duplex = \"half\";\n\n const response = await fetch(targetUrl.toString(), fetchOptions);\n\n // --- Response headers ---\n const responseHeaders = new Headers();\n const responseConnectionTokens = parseConnectionHeaderTokens(response.headers);\n response.headers.forEach((value, key) => {\n const lower = key.toLowerCase();\n if (lower === \"content-encoding\" || lower === \"content-length\") return;\n if (shouldDropHopByHopHeader(lower, responseConnectionTokens)) return;\n responseHeaders.set(key, value);\n });\n\n // --- Login token extraction ---\n // Body is assigned after shouldClearSessionFromForbidden() to avoid\n // stream locking — clone() inside that function tees the original stream.\n let bodyToReturn: BodyInit | ReadableStream<Uint8Array> | null = null;\n let loginToken = \"\";\n let finalStatus = response.status;\n\n if (isLoginRoute) {\n const contentType = (response.headers.get(\"content-type\") || \"\").toLowerCase();\n if (contentType.includes(\"application/json\")) {\n try {\n const rawBody = await response.text();\n const payload = JSON.parse(rawBody) as LoginResponsePayload;\n const tokenFields = authConfig.tokenResponseFields ?? {\n accessToken: \"access_token\",\n refreshToken: \"refresh_token\",\n };\n const accessTokenValue = payload[tokenFields.accessToken];\n const accessToken = typeof accessTokenValue === \"string\" ? accessTokenValue.trim() : \"\";\n if (accessToken) {\n loginToken = accessToken;\n }\n // Always strip tokens from login responses (success and error)\n // to prevent token leakage to the client\n const sanitizedPayload = { ...payload };\n delete sanitizedPayload[tokenFields.accessToken];\n delete sanitizedPayload[tokenFields.refreshToken];\n bodyToReturn = JSON.stringify(sanitizedPayload);\n responseHeaders.set(\"content-type\", \"application/json; charset=utf-8\");\n responseHeaders.delete(\"content-length\");\n } catch {\n // If JSON parsing fails, return minimal error to avoid leaking raw body\n bodyToReturn = JSON.stringify({ error: \"Invalid response from authentication service\" });\n // Upstream returned malformed JSON on login endpoint.\n // Surface this as a gateway failure instead of forwarding a misleading success status.\n finalStatus = 502;\n responseHeaders.set(\"content-type\", \"application/json; charset=utf-8\");\n responseHeaders.delete(\"content-length\");\n }\n }\n }\n\n // --- CORS + cache headers ---\n const ipAccess = isInternalIpAccess(request);\n applyCorsHeaders(responseHeaders, request, ipAccess);\n if (isAuthRoute) {\n responseHeaders.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n responseHeaders.set(\"Pragma\", \"no-cache\");\n responseHeaders.set(\"Expires\", \"0\");\n } else if (!responseHeaders.has(\"Cache-Control\")) {\n // Prevent intermediate proxies/CDNs from caching API responses\n // containing potentially sensitive data. The upstream API can\n // override this by setting its own Cache-Control header.\n responseHeaders.set(\"Cache-Control\", \"private, no-store\");\n }\n if (isLogoutRoute && response.status >= 200 && response.status < 300 && !isLocalHostRequest(request)) {\n responseHeaders.set(\"Clear-Site-Data\", '\"cache\", \"storage\"');\n }\n\n const shouldClear403Session = await shouldClearSessionFromForbidden(response);\n\n // Assign body AFTER potential clone() in shouldClearSessionFromForbidden\n // to get a readable tee branch instead of the locked original stream.\n if (!bodyToReturn) bodyToReturn = response.body;\n\n const nextResponse = new NextResponse(bodyToReturn, {\n status: finalStatus,\n headers: responseHeaders,\n });\n\n // --- Session cookie management ---\n if (loginToken && response.status >= 200 && response.status < 300) {\n setSessionCookie(nextResponse, request, loginToken);\n if (loginRateLimitKeys) {\n clearLoginAttemptState(loginRateLimitKeys);\n }\n } else if (isLoginRoute && loginRateLimitKeys && LOGIN_FAILURE_STATUSES.has(finalStatus)) {\n registerFailedLoginAttempt(loginRateLimitKeys, Date.now());\n } else if (\n isLogoutRoute ||\n response.status === 401 ||\n (normalizedPathKey === infoPath && response.status === 403) ||\n shouldClear403Session\n ) {\n authDebug(\"clear-session-cookie\", {\n path: normalizedPathKey,\n method,\n status: response.status,\n reason: isLogoutRoute\n ? \"logout\"\n : response.status === 401\n ? \"status_401\"\n : normalizedPathKey === infoPath && response.status === 403\n ? \"auth_info_403\"\n : shouldClear403Session\n ? \"forbidden_session_text\"\n : \"unknown\",\n hadSessionToken: !!sessionToken,\n });\n clearSessionCookie(nextResponse, request);\n }\n\n return nextResponse;\n } catch (error: unknown) {\n if (isPayloadTooLargeError(error)) {\n return createJsonErrorResponse(request, \"Payload too large\", 413);\n }\n const isDev = process.env.NODE_ENV === \"development\";\n const response = NextResponse.json(\n {\n error: \"Proxy request failed\",\n ...(isDev ? { details: error instanceof Error ? error.message : String(error) } : {}),\n },\n { status: 500 },\n );\n\n const normalizedIncomingPath = normalizePath(incomingPath).replace(/^v\\d+(?:\\.\\d+)?\\/?/i, \"\");\n const catchLogoutPath = (getFrameworkConfig().auth.logoutPath ?? \"auth/logout\").toLowerCase();\n applyCorsHeaders(response.headers, request, isInternalIpAccess(request));\n if (normalizedIncomingPath === catchLogoutPath) {\n clearSessionCookie(response, request);\n response.headers.set(\"Cache-Control\", \"no-store, no-cache, must-revalidate, proxy-revalidate\");\n response.headers.set(\"Pragma\", \"no-cache\");\n response.headers.set(\"Expires\", \"0\");\n authDebug(\"clear-session-cookie\", {\n path: normalizedIncomingPath,\n method,\n status: 500,\n reason: \"logout_proxy_failure\",\n });\n }\n\n return response;\n }\n}\n\n/** Handle CORS preflight requests. */\nexport async function OPTIONS(request: NextRequest) {\n const headers = new Headers();\n const ipAccess = isInternalIpAccess(request);\n applyCorsHeaders(headers, request, ipAccess);\n return new NextResponse(null, { status: 204, headers });\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"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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":"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","names":["getFrameworkConfig","trustProxyHeaders"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/proxy-middleware.ts"],"sourcesContent":["import { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport { BASE_SECURITY_HEADER_VALUES } from \"./security-headers\";\n\nfunction toOriginSource(value: string): string[] {\n const raw = value.trim();\n if (!raw) return [];\n\n try {\n const parsed = new URL(raw);\n const out = [parsed.origin];\n\n if (parsed.protocol === \"https:\") {\n out.push(`wss://${parsed.host}`);\n } else if (parsed.protocol === \"http:\") {\n out.push(`ws://${parsed.host}`);\n }\n\n return out;\n } catch {\n return [];\n }\n}\n\nconst BLOCKED_CSP_SCHEMES = new Set([\"javascript:\", \"data:\", \"blob:\", \"vbscript:\", \"filesystem:\"]);\n\nfunction parseCspSource(token: string): string | null {\n const value = token.trim();\n if (!value) return null;\n\n // Prevent header/CSP injection via env values.\n if (/[\\s;,\\r\\n]/.test(value)) return null;\n\n // Allow safe scheme sources (e.g. https:, wss:). Block dangerous schemes\n // that enable XSS or data exfiltration when used in CSP directives.\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {\n const lower = value.toLowerCase();\n if (BLOCKED_CSP_SCHEMES.has(lower)) return null;\n return lower;\n }\n\n try {\n const parsed = new URL(value);\n if (![\"http:\", \"https:\", \"ws:\", \"wss:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n if (parsed.pathname !== \"/\" || parsed.search || parsed.hash) return null;\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n return null;\n }\n}\n\nfunction parseReportUri(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n // Block header injection characters and double-quotes (used in Reporting-Endpoints structured header).\n if (/[\\s;,\\r\\n\"]/.test(trimmed)) return null;\n try {\n const parsed = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n return parsed.toString();\n } catch {\n return null;\n }\n}\n\nfunction parseCspList(value: string): string[] {\n const parsed = value\n .split(\",\")\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n return [...new Set(parsed)];\n}\n\nexport function proxy(req: NextRequest) {\n const url = req.nextUrl.clone();\n\n const TARGET_ENV = process.env.TARGET_ENV || \"test\";\n const isProdRuntime = TARGET_ENV === \"prod\" || process.env.NODE_ENV === \"production\";\n const isTargetProd = TARGET_ENV === \"prod\";\n\n const isLocalHost = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\";\n\n const nonce = crypto.randomUUID().replace(/-/g, \"\");\n const allowUnsafeStyleAttr = process.env.CSP_ALLOW_UNSAFE_STYLE_ATTR === \"true\";\n const denyStyleAttr = isTargetProd && !allowUnsafeStyleAttr;\n const allowUnsafeInlineStyle = !isTargetProd;\n\n const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || \"\");\n const apiConnectSources = toOriginSource(process.env.API_URL || \"\");\n const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || \"\");\n const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || \"\");\n const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || \"\");\n\n const cspConfig = getFrameworkConfig().csp;\n\n // Validate framework config CSP sources through parseCspSource to prevent injection\n const validatedConnectSources = cspConfig.thirdPartyConnectSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedImgSources = cspConfig.thirdPartyImageSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedScriptSources = cspConfig.thirdPartyScriptSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n const validatedFrameSources = cspConfig.thirdPartyFrameSources.map((s) => parseCspSource(s)).filter((s): s is string => !!s);\n\n const connectSrc = [\n \"'self'\",\n ...validatedConnectSources,\n ...apiConnectSources,\n ...connectHostsEnv,\n ...(!isProdRuntime ? [\"http://localhost:*\", \"http://127.0.0.1:*\", \"ws://localhost:*\", \"ws://127.0.0.1:*\"] : []),\n ].join(\" \");\n\n const imgSrc = [\"'self'\", \"data:\", \"blob:\", ...validatedImgSources, ...imgHostsEnv].join(\" \");\n\n const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(\" \");\n const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(\" \");\n\n const reportUri = parseReportUri(process.env.CSP_REPORT_URI || \"\");\n\n const cspDirectives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n `script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n \"script-src-attr 'none'\",\n allowUnsafeInlineStyle ? \"style-src 'self' 'unsafe-inline'\" : `style-src 'self' 'nonce-${nonce}'`,\n denyStyleAttr ? \"style-src-attr 'none'\" : \"style-src-attr 'unsafe-inline'\",\n `img-src ${imgSrc}`,\n \"media-src 'self' blob: data:\",\n \"manifest-src 'self'\",\n `connect-src ${connectSrc}`,\n `frame-src ${frameSrcHosts || \"'none'\"}`,\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n ...(isTargetProd ? [\"upgrade-insecure-requests\"] : []),\n ...(reportUri ? [`report-uri ${reportUri}`, \"report-to csp-violations\"] : []),\n ];\n const csp = cspDirectives.join(\"; \");\n\n const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];\n if (isProdRuntime) {\n for (const prefix of blockedPrefixes) {\n if (url.pathname.startsWith(prefix)) {\n const res = new NextResponse(\"Not Found\", { status: 404 });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n }\n }\n\n if (url.pathname === \"/\") {\n const dest = new URL(url.toString());\n if (!isLocalHost && isTargetProd) dest.protocol = \"https:\";\n dest.pathname = getFrameworkConfig().app.defaultRoute;\n dest.search = \"\";\n\n const res = makeRedirect(dest, 308);\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set(\"x-nonce\", nonce);\n\n const res = NextResponse.next({ request: { headers: requestHeaders } });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n}\n\nfunction setSecurity(\n res: NextResponse,\n opts: { isLocalHost: boolean; isTargetProd: boolean; csp?: string; nonce?: string; reportUri?: string | null }\n) {\n const { isLocalHost, csp, nonce, reportUri } = opts;\n if (csp) res.headers.set(\"Content-Security-Policy\", csp);\n for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {\n res.headers.set(key, value);\n }\n if (opts.isTargetProd && !isLocalHost)\n res.headers.set(\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\");\n if (reportUri) res.headers.set(\"Reporting-Endpoints\", `csp-violations=\"${reportUri}\"`);\n if (nonce) res.headers.set(\"x-nonce\", nonce);\n return res;\n}\n\nfunction makeRedirect(to: URL | string, status = 308) {\n const res = new NextResponse(null, { status });\n res.headers.set(\"Location\", typeof to === \"string\" ? to : to.toString());\n return res;\n}\n\nexport const proxyConfig = {\n matcher: [\"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)\"],\n};\n"],"mappings":"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","names":["res"]}
|