@spring-systems/server 0.8.6 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-route-handler.js +1 -19
- package/dist/chunk-E4AX7MID.js +1 -0
- package/dist/chunk-FXUI75TW.js +1 -0
- package/dist/chunk-KEEIJ5EV.js +1 -0
- package/dist/chunk-M23YQYLU.js +1 -0
- package/dist/chunk-TVAJDSYB.js +1 -0
- package/dist/chunk-UDRRRHFI.js +1 -0
- package/dist/chunk-VB7DEUGT.js +1 -0
- package/dist/client.js +1 -14
- package/dist/handlers/index.js +1 -48
- package/dist/index.js +1 -44
- package/dist/next-adapters.js +1 -14
- package/dist/proxy-middleware.js +1 -10
- package/dist/rate-limiter.js +1 -15
- package/dist/runtime-env.js +1 -9
- package/dist/security-headers.js +1 -11
- package/package.json +109 -108
- package/dist/api-route-handler.js.map +0 -1
- package/dist/chunk-4SUIIQDW.js +0 -158
- package/dist/chunk-4SUIIQDW.js.map +0 -1
- package/dist/chunk-7IUSTA5W.js +0 -113
- package/dist/chunk-7IUSTA5W.js.map +0 -1
- package/dist/chunk-CP33WQ5Q.js +0 -47
- package/dist/chunk-CP33WQ5Q.js.map +0 -1
- package/dist/chunk-KA7RJCWA.js +0 -24
- package/dist/chunk-KA7RJCWA.js.map +0 -1
- package/dist/chunk-NFJ25NQQ.js +0 -377
- package/dist/chunk-NFJ25NQQ.js.map +0 -1
- package/dist/chunk-PZWKMIA4.js +0 -513
- package/dist/chunk-PZWKMIA4.js.map +0 -1
- package/dist/chunk-YV6DZVPI.js +0 -43
- package/dist/chunk-YV6DZVPI.js.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/handlers/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/next-adapters.js.map +0 -1
- package/dist/proxy-middleware.js.map +0 -1
- package/dist/rate-limiter.js.map +0 -1
- package/dist/runtime-env.js.map +0 -1
- package/dist/security-headers.js.map +0 -1
package/dist/chunk-NFJ25NQQ.js
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
applyCorsHeaders,
|
|
3
|
-
cleanupExpiredLoginLimits,
|
|
4
|
-
clearLoginAttemptState,
|
|
5
|
-
clearSessionCookie,
|
|
6
|
-
createSizeLimitedBodyStream,
|
|
7
|
-
extractLeadingVersion,
|
|
8
|
-
extractTrailingVersion,
|
|
9
|
-
getLoginRateLimitKeys,
|
|
10
|
-
getRateLimitRetryAfterMs,
|
|
11
|
-
getSessionToken,
|
|
12
|
-
isInternalIpAccess,
|
|
13
|
-
isLocalHostRequest,
|
|
14
|
-
isPayloadTooLargeError,
|
|
15
|
-
isSafeProxyPathSegment,
|
|
16
|
-
normalizePath,
|
|
17
|
-
parseConnectionHeaderTokens,
|
|
18
|
-
parseContentLength,
|
|
19
|
-
registerFailedLoginAttempt,
|
|
20
|
-
resolveProdSecurityConfigError,
|
|
21
|
-
setSessionCookie,
|
|
22
|
-
shouldClearSessionFromForbidden,
|
|
23
|
-
shouldDropHopByHopHeader,
|
|
24
|
-
shouldRejectByCsrfProtection,
|
|
25
|
-
toValidPositiveInteger
|
|
26
|
-
} from "./chunk-PZWKMIA4.js";
|
|
27
|
-
|
|
28
|
-
// src/api-route-handler.ts
|
|
29
|
-
import { getFrameworkConfig } from "@spring-systems/core/config";
|
|
30
|
-
import { logInfo, logWarn } from "@spring-systems/core/logger";
|
|
31
|
-
import { NextResponse } from "next/server.js";
|
|
32
|
-
var _validatedApiUrl;
|
|
33
|
-
function getApiBaseUrl() {
|
|
34
|
-
if (_validatedApiUrl !== void 0) return _validatedApiUrl;
|
|
35
|
-
const url = (process.env.API_URL || "").trim();
|
|
36
|
-
if (!url) throw new Error("API_URL environment variable is not set");
|
|
37
|
-
try {
|
|
38
|
-
new URL(url);
|
|
39
|
-
} catch {
|
|
40
|
-
throw new Error(`API_URL is not a valid URL: "${url}"`);
|
|
41
|
-
}
|
|
42
|
-
_validatedApiUrl = url;
|
|
43
|
-
return url;
|
|
44
|
-
}
|
|
45
|
-
function parseByteLimit(envName, fallback) {
|
|
46
|
-
const raw = process.env[envName];
|
|
47
|
-
if (!raw) return fallback;
|
|
48
|
-
const parsed = Number(raw);
|
|
49
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
50
|
-
throw new Error(`${envName} must be a positive number, got: "${raw}"`);
|
|
51
|
-
}
|
|
52
|
-
return parsed;
|
|
53
|
-
}
|
|
54
|
-
var NON_MULTIPART_BODY_MAX_BYTES = parseByteLimit("API_PROXY_NON_MULTIPART_MAX_BYTES", 10485760);
|
|
55
|
-
var MULTIPART_BODY_MAX_BYTES = parseByteLimit("API_PROXY_MULTIPART_MAX_BYTES", 52428800);
|
|
56
|
-
var LOGIN_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 429, 502]);
|
|
57
|
-
var AUTH_DEBUG = process.env.AUTH_DEBUG === "true";
|
|
58
|
-
function getPublicAuthPaths() {
|
|
59
|
-
return new Set(getFrameworkConfig().auth.publicAuthPaths);
|
|
60
|
-
}
|
|
61
|
-
function getSafeHashPattern() {
|
|
62
|
-
return getFrameworkConfig().proxy.safeHashPattern;
|
|
63
|
-
}
|
|
64
|
-
function getMaxProxyPathLength() {
|
|
65
|
-
return getFrameworkConfig().proxy.maxProxyPathLength;
|
|
66
|
-
}
|
|
67
|
-
function getProdSecurityConfigError() {
|
|
68
|
-
return resolveProdSecurityConfigError();
|
|
69
|
-
}
|
|
70
|
-
function getBodyLimitBytes(isMultipart) {
|
|
71
|
-
return isMultipart ? toValidPositiveInteger(MULTIPART_BODY_MAX_BYTES, 52428800) : toValidPositiveInteger(NON_MULTIPART_BODY_MAX_BYTES, 10485760);
|
|
72
|
-
}
|
|
73
|
-
function authDebug(event, data) {
|
|
74
|
-
if (!AUTH_DEBUG) return;
|
|
75
|
-
logInfo(`AuthProxy.${event}`, data);
|
|
76
|
-
}
|
|
77
|
-
function sanitizeIncomingHash(value) {
|
|
78
|
-
const trimmed = value.trim();
|
|
79
|
-
if (!trimmed) return "";
|
|
80
|
-
const pattern = getSafeHashPattern();
|
|
81
|
-
if (!pattern || !pattern.test(trimmed)) return "";
|
|
82
|
-
return trimmed;
|
|
83
|
-
}
|
|
84
|
-
function createJsonErrorResponse(request, error, status, headersInit) {
|
|
85
|
-
const headers = new Headers(headersInit);
|
|
86
|
-
applyCorsHeaders(headers, request, isInternalIpAccess(request));
|
|
87
|
-
return NextResponse.json({ error }, { status, headers });
|
|
88
|
-
}
|
|
89
|
-
async function GET(request, context) {
|
|
90
|
-
const { path } = await context.params;
|
|
91
|
-
return handleRequest(request, path, "GET");
|
|
92
|
-
}
|
|
93
|
-
async function POST(request, context) {
|
|
94
|
-
const { path } = await context.params;
|
|
95
|
-
return handleRequest(request, path, "POST");
|
|
96
|
-
}
|
|
97
|
-
async function PUT(request, context) {
|
|
98
|
-
const { path } = await context.params;
|
|
99
|
-
return handleRequest(request, path, "PUT");
|
|
100
|
-
}
|
|
101
|
-
async function DELETE(request, context) {
|
|
102
|
-
const { path } = await context.params;
|
|
103
|
-
return handleRequest(request, path, "DELETE");
|
|
104
|
-
}
|
|
105
|
-
async function PATCH(request, context) {
|
|
106
|
-
const { path } = await context.params;
|
|
107
|
-
return handleRequest(request, path, "PATCH");
|
|
108
|
-
}
|
|
109
|
-
async function handleRequest(request, pathSegments, method) {
|
|
110
|
-
const incomingPath = pathSegments.join("/");
|
|
111
|
-
try {
|
|
112
|
-
cleanupExpiredLoginLimits(Date.now());
|
|
113
|
-
const prodSecurityError = getProdSecurityConfigError();
|
|
114
|
-
if (prodSecurityError) {
|
|
115
|
-
return createJsonErrorResponse(request, prodSecurityError, 500);
|
|
116
|
-
}
|
|
117
|
-
if (!pathSegments.every((segment) => isSafeProxyPathSegment(segment))) {
|
|
118
|
-
return createJsonErrorResponse(request, "Invalid path", 400);
|
|
119
|
-
}
|
|
120
|
-
if (incomingPath.length > getMaxProxyPathLength()) {
|
|
121
|
-
return createJsonErrorResponse(request, "Path too long", 414);
|
|
122
|
-
}
|
|
123
|
-
const rawApiBase = getApiBaseUrl().replace(/\/+$/, "");
|
|
124
|
-
if (!rawApiBase) {
|
|
125
|
-
return createJsonErrorResponse(request, "API_URL is not configured", 500);
|
|
126
|
-
}
|
|
127
|
-
const apiBaseUrl = new URL(rawApiBase);
|
|
128
|
-
const apiBasePath = apiBaseUrl.pathname;
|
|
129
|
-
const baseVersion = extractTrailingVersion(apiBasePath);
|
|
130
|
-
const incomingVersion = extractLeadingVersion(incomingPath);
|
|
131
|
-
let normalizedPath = incomingPath;
|
|
132
|
-
if (baseVersion && incomingVersion && baseVersion.toLowerCase() === incomingVersion.toLowerCase()) {
|
|
133
|
-
normalizedPath = incomingPath.replace(new RegExp(`^${incomingVersion}/?`, "i"), "");
|
|
134
|
-
}
|
|
135
|
-
const targetBase = `${rawApiBase}/${normalizedPath.replace(/^\/+/, "")}`;
|
|
136
|
-
const normalizedPathKey = normalizePath(normalizedPath);
|
|
137
|
-
const authConfig = getFrameworkConfig().auth;
|
|
138
|
-
const loginPath = (authConfig.loginPath ?? "auth/login").toLowerCase();
|
|
139
|
-
const logoutPath = (authConfig.logoutPath ?? "auth/logout").toLowerCase();
|
|
140
|
-
const infoPath = (authConfig.infoPath ?? "auth/info").toLowerCase();
|
|
141
|
-
const isLoginRoute = normalizedPathKey === loginPath;
|
|
142
|
-
const isLogoutRoute = normalizedPathKey === logoutPath;
|
|
143
|
-
const isAuthRoute = normalizedPathKey === loginPath || normalizedPathKey === logoutPath || normalizedPathKey === infoPath;
|
|
144
|
-
const inUrl = new URL(request.url);
|
|
145
|
-
const targetUrl = new URL(targetBase);
|
|
146
|
-
inUrl.searchParams.forEach((v, k) => {
|
|
147
|
-
if (!targetUrl.searchParams.has(k)) targetUrl.searchParams.set(k, v);
|
|
148
|
-
});
|
|
149
|
-
const headers = new Headers();
|
|
150
|
-
const requestConnectionTokens = parseConnectionHeaderTokens(request.headers);
|
|
151
|
-
request.headers.forEach((value, key) => {
|
|
152
|
-
const lower = key.toLowerCase();
|
|
153
|
-
if (lower === "host" || lower === "content-length" || lower === "cookie" || lower === "authorization" || shouldDropHopByHopHeader(lower, requestConnectionTokens)) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
headers.set(key, value);
|
|
157
|
-
});
|
|
158
|
-
if (!headers.has("X-Requested-With")) {
|
|
159
|
-
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
160
|
-
}
|
|
161
|
-
const sessionToken = getSessionToken(request);
|
|
162
|
-
const incomingAuthorization = (request.headers.get("authorization") || "").trim();
|
|
163
|
-
const hasBearerAuthorization = /^Bearer\s+\S+$/i.test(incomingAuthorization);
|
|
164
|
-
authDebug("incoming-auth", {
|
|
165
|
-
path: normalizedPathKey,
|
|
166
|
-
method,
|
|
167
|
-
hasSessionToken: !!sessionToken,
|
|
168
|
-
hasBearerAuthorization,
|
|
169
|
-
host: request.nextUrl.host,
|
|
170
|
-
protocol: request.nextUrl.protocol
|
|
171
|
-
});
|
|
172
|
-
if (normalizedPathKey === infoPath && !sessionToken && !hasBearerAuthorization) {
|
|
173
|
-
const res = new NextResponse(null, { status: 204 });
|
|
174
|
-
applyCorsHeaders(res.headers, request, isInternalIpAccess(request));
|
|
175
|
-
res.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
176
|
-
res.headers.set("Pragma", "no-cache");
|
|
177
|
-
res.headers.set("Expires", "0");
|
|
178
|
-
clearSessionCookie(res, request);
|
|
179
|
-
authDebug("short-circuit-auth-info-no-token", {
|
|
180
|
-
host: request.nextUrl.host,
|
|
181
|
-
protocol: request.nextUrl.protocol
|
|
182
|
-
});
|
|
183
|
-
return res;
|
|
184
|
-
}
|
|
185
|
-
if (shouldRejectByCsrfProtection(request, method, normalizedPathKey)) {
|
|
186
|
-
logWarn(
|
|
187
|
-
"ApiProxy.CSRF",
|
|
188
|
-
`Blocked ${method} ${normalizedPathKey} \u2014 origin: ${request.headers.get("origin") ?? "none"}, referer: ${request.headers.get("referer") ?? "none"}, sec-fetch-site: ${request.headers.get("sec-fetch-site") ?? "none"}`
|
|
189
|
-
);
|
|
190
|
-
return createJsonErrorResponse(request, "Forbidden", 403);
|
|
191
|
-
}
|
|
192
|
-
if (sessionToken && !getPublicAuthPaths().has(normalizedPathKey)) {
|
|
193
|
-
headers.set("Authorization", `Bearer ${sessionToken}`);
|
|
194
|
-
} else if (hasBearerAuthorization) {
|
|
195
|
-
headers.set("Authorization", incomingAuthorization);
|
|
196
|
-
}
|
|
197
|
-
const incomingHash = request.headers.get("x-hash") || "";
|
|
198
|
-
const sanitizedHash = sanitizeIncomingHash(incomingHash);
|
|
199
|
-
if (sanitizedHash) {
|
|
200
|
-
headers.set("X-hash", sanitizedHash);
|
|
201
|
-
headers.set("Hash", sanitizedHash);
|
|
202
|
-
if (!targetUrl.searchParams.has("hash")) targetUrl.searchParams.set("hash", sanitizedHash);
|
|
203
|
-
if (!targetUrl.searchParams.has("Hash")) targetUrl.searchParams.set("Hash", sanitizedHash);
|
|
204
|
-
}
|
|
205
|
-
let body = void 0;
|
|
206
|
-
let loginRateLimitKeys = null;
|
|
207
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
208
|
-
const contentType = request.headers.get("content-type") || "";
|
|
209
|
-
const isMultipart = contentType.includes("multipart/form-data");
|
|
210
|
-
const bodyLimit = getBodyLimitBytes(isMultipart);
|
|
211
|
-
const contentLength = parseContentLength(request.headers.get("content-length"));
|
|
212
|
-
if (contentLength !== null && contentLength > bodyLimit) {
|
|
213
|
-
return createJsonErrorResponse(request, "Payload too large", 413);
|
|
214
|
-
}
|
|
215
|
-
if (isMultipart) {
|
|
216
|
-
if (contentLength === null) {
|
|
217
|
-
return createJsonErrorResponse(request, "Content-Length required for multipart payload", 411);
|
|
218
|
-
}
|
|
219
|
-
body = request.body ? createSizeLimitedBodyStream(request.body, bodyLimit) : request.body;
|
|
220
|
-
} else {
|
|
221
|
-
body = await request.arrayBuffer();
|
|
222
|
-
if (body.byteLength > bodyLimit) {
|
|
223
|
-
return createJsonErrorResponse(request, "Payload too large", 413);
|
|
224
|
-
}
|
|
225
|
-
if (isLoginRoute && contentType.includes("application/json")) {
|
|
226
|
-
try {
|
|
227
|
-
const rawText = new TextDecoder().decode(body);
|
|
228
|
-
const parsed = JSON.parse(rawText);
|
|
229
|
-
const usernameField = authConfig.loginUsernameField ?? "username";
|
|
230
|
-
loginRateLimitKeys = getLoginRateLimitKeys(request, String(parsed[usernameField] || ""));
|
|
231
|
-
const retryAfterMs = getRateLimitRetryAfterMs(loginRateLimitKeys, Date.now());
|
|
232
|
-
if (retryAfterMs > 0) {
|
|
233
|
-
logWarn(
|
|
234
|
-
"ApiProxy.RateLimit",
|
|
235
|
-
`Login rate-limited for key ${loginRateLimitKeys.pairKey} \u2014 retry after ${Math.ceil(retryAfterMs / 1e3)}s`
|
|
236
|
-
);
|
|
237
|
-
return createJsonErrorResponse(request, "Too many login attempts. Try again later.", 429, {
|
|
238
|
-
"Retry-After": String(Math.ceil(retryAfterMs / 1e3))
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
} catch {
|
|
242
|
-
loginRateLimitKeys = getLoginRateLimitKeys(request, "");
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const fetchOptions = {
|
|
248
|
-
method,
|
|
249
|
-
headers,
|
|
250
|
-
body,
|
|
251
|
-
redirect: "manual",
|
|
252
|
-
signal: AbortSignal.timeout(getFrameworkConfig().api?.timeoutMs ?? 3e4)
|
|
253
|
-
};
|
|
254
|
-
if (body instanceof ReadableStream) fetchOptions.duplex = "half";
|
|
255
|
-
const response = await fetch(targetUrl.toString(), fetchOptions);
|
|
256
|
-
const responseHeaders = new Headers();
|
|
257
|
-
const responseConnectionTokens = parseConnectionHeaderTokens(response.headers);
|
|
258
|
-
response.headers.forEach((value, key) => {
|
|
259
|
-
const lower = key.toLowerCase();
|
|
260
|
-
if (lower === "content-encoding" || lower === "content-length") return;
|
|
261
|
-
if (shouldDropHopByHopHeader(lower, responseConnectionTokens)) return;
|
|
262
|
-
responseHeaders.set(key, value);
|
|
263
|
-
});
|
|
264
|
-
let bodyToReturn = null;
|
|
265
|
-
let loginToken = "";
|
|
266
|
-
let finalStatus = response.status;
|
|
267
|
-
if (isLoginRoute) {
|
|
268
|
-
const contentType = (response.headers.get("content-type") || "").toLowerCase();
|
|
269
|
-
if (contentType.includes("application/json")) {
|
|
270
|
-
try {
|
|
271
|
-
const rawBody = await response.text();
|
|
272
|
-
const payload = JSON.parse(rawBody);
|
|
273
|
-
const tokenFields = authConfig.tokenResponseFields ?? {
|
|
274
|
-
accessToken: "access_token",
|
|
275
|
-
refreshToken: "refresh_token"
|
|
276
|
-
};
|
|
277
|
-
const accessTokenValue = payload[tokenFields.accessToken];
|
|
278
|
-
const accessToken = typeof accessTokenValue === "string" ? accessTokenValue.trim() : "";
|
|
279
|
-
if (accessToken) {
|
|
280
|
-
loginToken = accessToken;
|
|
281
|
-
}
|
|
282
|
-
const sanitizedPayload = { ...payload };
|
|
283
|
-
delete sanitizedPayload[tokenFields.accessToken];
|
|
284
|
-
delete sanitizedPayload[tokenFields.refreshToken];
|
|
285
|
-
bodyToReturn = JSON.stringify(sanitizedPayload);
|
|
286
|
-
responseHeaders.set("content-type", "application/json; charset=utf-8");
|
|
287
|
-
responseHeaders.delete("content-length");
|
|
288
|
-
} catch {
|
|
289
|
-
bodyToReturn = JSON.stringify({ error: "Invalid response from authentication service" });
|
|
290
|
-
finalStatus = 502;
|
|
291
|
-
responseHeaders.set("content-type", "application/json; charset=utf-8");
|
|
292
|
-
responseHeaders.delete("content-length");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
const ipAccess = isInternalIpAccess(request);
|
|
297
|
-
applyCorsHeaders(responseHeaders, request, ipAccess);
|
|
298
|
-
if (isAuthRoute) {
|
|
299
|
-
responseHeaders.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
300
|
-
responseHeaders.set("Pragma", "no-cache");
|
|
301
|
-
responseHeaders.set("Expires", "0");
|
|
302
|
-
} else if (!responseHeaders.has("Cache-Control")) {
|
|
303
|
-
responseHeaders.set("Cache-Control", "private, no-store");
|
|
304
|
-
}
|
|
305
|
-
if (isLogoutRoute && response.status >= 200 && response.status < 300 && !isLocalHostRequest(request)) {
|
|
306
|
-
responseHeaders.set("Clear-Site-Data", '"cache", "storage"');
|
|
307
|
-
}
|
|
308
|
-
const shouldClear403Session = await shouldClearSessionFromForbidden(response);
|
|
309
|
-
if (!bodyToReturn) bodyToReturn = response.body;
|
|
310
|
-
const nextResponse = new NextResponse(bodyToReturn, {
|
|
311
|
-
status: finalStatus,
|
|
312
|
-
headers: responseHeaders
|
|
313
|
-
});
|
|
314
|
-
if (loginToken && response.status >= 200 && response.status < 300) {
|
|
315
|
-
setSessionCookie(nextResponse, request, loginToken);
|
|
316
|
-
if (loginRateLimitKeys) {
|
|
317
|
-
clearLoginAttemptState(loginRateLimitKeys);
|
|
318
|
-
}
|
|
319
|
-
} else if (isLoginRoute && loginRateLimitKeys && LOGIN_FAILURE_STATUSES.has(finalStatus)) {
|
|
320
|
-
registerFailedLoginAttempt(loginRateLimitKeys, Date.now());
|
|
321
|
-
} else if (isLogoutRoute || response.status === 401 || normalizedPathKey === infoPath && response.status === 403 || shouldClear403Session) {
|
|
322
|
-
authDebug("clear-session-cookie", {
|
|
323
|
-
path: normalizedPathKey,
|
|
324
|
-
method,
|
|
325
|
-
status: response.status,
|
|
326
|
-
reason: isLogoutRoute ? "logout" : response.status === 401 ? "status_401" : normalizedPathKey === infoPath && response.status === 403 ? "auth_info_403" : shouldClear403Session ? "forbidden_session_text" : "unknown",
|
|
327
|
-
hadSessionToken: !!sessionToken
|
|
328
|
-
});
|
|
329
|
-
clearSessionCookie(nextResponse, request);
|
|
330
|
-
}
|
|
331
|
-
return nextResponse;
|
|
332
|
-
} catch (error) {
|
|
333
|
-
if (isPayloadTooLargeError(error)) {
|
|
334
|
-
return createJsonErrorResponse(request, "Payload too large", 413);
|
|
335
|
-
}
|
|
336
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
337
|
-
const response = NextResponse.json(
|
|
338
|
-
{
|
|
339
|
-
error: "Proxy request failed",
|
|
340
|
-
...isDev ? { details: error instanceof Error ? error.message : String(error) } : {}
|
|
341
|
-
},
|
|
342
|
-
{ status: 500 }
|
|
343
|
-
);
|
|
344
|
-
const normalizedIncomingPath = normalizePath(incomingPath).replace(/^v\d+(?:\.\d+)?\/?/i, "");
|
|
345
|
-
const catchLogoutPath = (getFrameworkConfig().auth.logoutPath ?? "auth/logout").toLowerCase();
|
|
346
|
-
applyCorsHeaders(response.headers, request, isInternalIpAccess(request));
|
|
347
|
-
if (normalizedIncomingPath === catchLogoutPath) {
|
|
348
|
-
clearSessionCookie(response, request);
|
|
349
|
-
response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
350
|
-
response.headers.set("Pragma", "no-cache");
|
|
351
|
-
response.headers.set("Expires", "0");
|
|
352
|
-
authDebug("clear-session-cookie", {
|
|
353
|
-
path: normalizedIncomingPath,
|
|
354
|
-
method,
|
|
355
|
-
status: 500,
|
|
356
|
-
reason: "logout_proxy_failure"
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
return response;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
async function OPTIONS(request) {
|
|
363
|
-
const headers = new Headers();
|
|
364
|
-
const ipAccess = isInternalIpAccess(request);
|
|
365
|
-
applyCorsHeaders(headers, request, ipAccess);
|
|
366
|
-
return new NextResponse(null, { status: 204, headers });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export {
|
|
370
|
-
GET,
|
|
371
|
-
POST,
|
|
372
|
-
PUT,
|
|
373
|
-
DELETE,
|
|
374
|
-
PATCH,
|
|
375
|
-
OPTIONS
|
|
376
|
-
};
|
|
377
|
-
//# sourceMappingURL=chunk-NFJ25NQQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|