@spring-systems/server 0.8.4 → 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/CHANGELOG.md +8 -0
- 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 +3 -3
- 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
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rate-limiter.ts"],"sourcesContent":["/**\n * Pluggable rate limiter for API proxy authentication.\n *\n * The default implementation uses in-memory Maps (suitable for single-instance deployments).\n * Multi-instance deployments can replace this with a custom adapter via `setRateLimiterAdapter()`.\n *\n * @example\n * ```ts\n * import { setRateLimiterAdapter } from \"@spring-systems/server/rate-limiter\";\n * import { createCustomRateLimiter } from \"./my-rate-limiter\";\n *\n * setRateLimiterAdapter(createCustomRateLimiter());\n * ```\n *\n * @module rate-limiter\n */\n\nexport interface RateLimitEntry {\n count: number;\n windowStartedAt: number;\n blockedUntil: number;\n}\n\nexport interface RateLimitPolicy {\n windowMs: number;\n blockMs: number;\n maxAttemptsByIpAndAccount: number;\n maxAttemptsByAccount: number;\n maxKeys: number;\n}\n\n/**\n * Rate limiter adapter interface. Implementations must be async-safe.\n */\nexport interface RateLimiterAdapter {\n /** Get the current rate limit entry for a key, or null if no entry exists. */\n get(store: \"ip\" | \"account\", key: string): RateLimitEntry | null;\n\n /** Set/update the rate limit entry for a key. */\n set(store: \"ip\" | \"account\", key: string, entry: RateLimitEntry): void;\n\n /** Delete an entry (e.g. on successful login). */\n delete(store: \"ip\" | \"account\", key: string): void;\n\n /** Get the total number of tracked keys (for eviction logic). */\n size(store: \"ip\" | \"account\"): number;\n\n /** Clear the oldest entries when maxKeys is exceeded. */\n evictOldest(store: \"ip\" | \"account\", count: number): void;\n\n /**\n * Remove expired entries for a store.\n * Optional to preserve compatibility with existing custom adapters.\n */\n sweepExpired?(store: \"ip\" | \"account\", now: number, windowMs: number): void;\n}\n\n// ---------------------------------------------------------------------------\n// Default in-memory implementation\n// ---------------------------------------------------------------------------\n\nclass InMemoryRateLimiter implements RateLimiterAdapter {\n private ipStore = new Map<string, RateLimitEntry>();\n private accountStore = new Map<string, RateLimitEntry>();\n\n private getStore(store: \"ip\" | \"account\"): Map<string, RateLimitEntry> {\n return store === \"ip\" ? this.ipStore : this.accountStore;\n }\n\n get(store: \"ip\" | \"account\", key: string): RateLimitEntry | null {\n return this.getStore(store).get(key) ?? null;\n }\n\n set(store: \"ip\" | \"account\", key: string, entry: RateLimitEntry): void {\n this.getStore(store).set(key, entry);\n }\n\n delete(store: \"ip\" | \"account\", key: string): void {\n this.getStore(store).delete(key);\n }\n\n size(store: \"ip\" | \"account\"): number {\n return this.getStore(store).size;\n }\n\n evictOldest(store: \"ip\" | \"account\", count: number): void {\n const map = this.getStore(store);\n const entries = [...map.entries()].sort((a, b) => a[1].windowStartedAt - b[1].windowStartedAt);\n for (let i = 0; i < count && i < entries.length; i++) {\n const candidate = entries[i];\n if (!candidate) break;\n map.delete(candidate[0]);\n }\n }\n\n sweepExpired(store: \"ip\" | \"account\", now: number, windowMs: number): void {\n const map = this.getStore(store);\n for (const [key, entry] of map.entries()) {\n const windowExpired = now - entry.windowStartedAt > windowMs;\n const noActiveBlock = entry.blockedUntil <= now;\n if (windowExpired && noActiveBlock) {\n map.delete(key);\n }\n }\n }\n}\n\n// Singleton adapter\nlet adapter: RateLimiterAdapter = new InMemoryRateLimiter();\nlet hasWarnedInMemory = false;\n\n/** Replace the default in-memory rate limiter with a custom adapter. */\nexport function setRateLimiterAdapter(custom: RateLimiterAdapter): void {\n adapter = custom;\n}\n\n/** Get the current rate limiter adapter. */\nexport function getRateLimiterAdapter(): RateLimiterAdapter {\n return adapter;\n}\n\n/**\n * Check if a login attempt should be rate-limited.\n * @returns A reason string if blocked, or null if allowed.\n */\nexport function checkRateLimit(\n ipKey: string,\n accountKey: string | null,\n policy: RateLimitPolicy,\n): string | null {\n if (!hasWarnedInMemory && adapter instanceof InMemoryRateLimiter && process.env.NODE_ENV === \"production\") {\n hasWarnedInMemory = true;\n console.warn(\"[server] Rate limiter is using in-memory storage in production. For multi-instance deployments, call setRateLimiterAdapter() with a shared-storage adapter.\");\n }\n const now = Date.now();\n\n // Check IP-based limit\n const ipResult = checkSingleLimit(adapter, \"ip\", ipKey, policy.maxAttemptsByIpAndAccount, policy, now);\n if (ipResult) return ipResult;\n\n // Check account-based limit\n if (accountKey) {\n const accountResult = checkSingleLimit(adapter, \"account\", accountKey, policy.maxAttemptsByAccount, policy, now);\n if (accountResult) return accountResult;\n }\n\n return null;\n}\n\n/**\n * Record a failed login attempt.\n */\nexport function recordFailedAttempt(\n ipKey: string,\n accountKey: string | null,\n policy: RateLimitPolicy,\n): void {\n const now = Date.now();\n recordAttempt(adapter, \"ip\", ipKey, policy, now);\n if (accountKey) {\n recordAttempt(adapter, \"account\", accountKey, policy, now);\n }\n}\n\n/**\n * Clear rate limit entries for a key (e.g. on successful login).\n */\nexport function clearRateLimitEntries(ipKey: string, accountKey: string | null): void {\n adapter.delete(\"ip\", ipKey);\n if (accountKey) {\n adapter.delete(\"account\", accountKey);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction checkSingleLimit(\n adap: RateLimiterAdapter,\n store: \"ip\" | \"account\",\n key: string,\n maxAttempts: number,\n policy: RateLimitPolicy,\n now: number,\n): string | null {\n const entry = adap.get(store, key);\n if (!entry) return null;\n\n if (entry.blockedUntil > now) {\n const remainSec = Math.ceil((entry.blockedUntil - now) / 1000);\n return `Rate limited (${store}). Try again in ${remainSec}s.`;\n }\n\n if (now - entry.windowStartedAt > policy.windowMs) {\n adap.delete(store, key);\n return null;\n }\n\n if (entry.count >= maxAttempts) {\n entry.blockedUntil = now + policy.blockMs;\n adap.set(store, key, entry);\n return `Too many attempts (${store}). Blocked for ${Math.ceil(policy.blockMs / 1000)}s.`;\n }\n\n return null;\n}\n\nfunction recordAttempt(\n adap: RateLimiterAdapter,\n store: \"ip\" | \"account\",\n key: string,\n policy: RateLimitPolicy,\n now: number,\n): void {\n // Evict old entries if needed\n if (adap.size(store) >= policy.maxKeys) {\n adap.evictOldest(store, Math.floor(policy.maxKeys * 0.1));\n }\n\n const entry = adap.get(store, key);\n if (!entry || now - entry.windowStartedAt > policy.windowMs) {\n adap.set(store, key, { count: 1, windowStartedAt: now, blockedUntil: 0 });\n } else {\n entry.count++;\n adap.set(store, key, entry);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-CP33WQ5Q.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// src/next-adapters.ts
|
|
2
|
-
import nextDynamic from "next/dynamic";
|
|
3
|
-
import NextImage from "next/image";
|
|
4
|
-
import NextLink from "next/link";
|
|
5
|
-
import {
|
|
6
|
-
useParams as useNextParams,
|
|
7
|
-
usePathname as useNextPathname,
|
|
8
|
-
useRouter as useNextRouter,
|
|
9
|
-
useSearchParams as useNextSearchParams
|
|
10
|
-
} from "next/navigation";
|
|
11
|
-
var hasLoggedDefaultSsrWarning = false;
|
|
12
|
-
function createNextRouteAdapter() {
|
|
13
|
-
return {
|
|
14
|
-
usePathname: useNextPathname,
|
|
15
|
-
useSearchParams: () => useNextSearchParams() ?? new URLSearchParams(),
|
|
16
|
-
useParams: () => useNextParams() ?? {},
|
|
17
|
-
useRouter: useNextRouter
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function createNextUIAdapter(options) {
|
|
21
|
-
const defaultDynamicSsr = options?.defaultDynamicSsr ?? true;
|
|
22
|
-
if (options?.defaultDynamicSsr === void 0 && !hasLoggedDefaultSsrWarning && process.env.NODE_ENV !== "production") {
|
|
23
|
-
hasLoggedDefaultSsrWarning = true;
|
|
24
|
-
console.warn(
|
|
25
|
-
"[server] createNextUIAdapter() defaults dynamic imports to ssr:true. For client-only apps use createClientOnlyNextUIAdapter() or pass { defaultDynamicSsr: false }."
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
Image: NextImage,
|
|
30
|
-
Link: NextLink,
|
|
31
|
-
dynamic: (loader, opts) => nextDynamic(loader, { ssr: opts?.ssr ?? defaultDynamicSsr })
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
function createClientOnlyNextUIAdapter() {
|
|
35
|
-
return createNextUIAdapter({ defaultDynamicSsr: false });
|
|
36
|
-
}
|
|
37
|
-
function createSsrNextUIAdapter() {
|
|
38
|
-
return createNextUIAdapter({ defaultDynamicSsr: true });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export {
|
|
42
|
-
createNextRouteAdapter,
|
|
43
|
-
createNextUIAdapter,
|
|
44
|
-
createClientOnlyNextUIAdapter,
|
|
45
|
-
createSsrNextUIAdapter
|
|
46
|
-
};
|
|
47
|
-
//# sourceMappingURL=chunk-CP33WQ5Q.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/next-adapters.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Next.js adapter presets for RouteAdapter and UIAdapter.\n * Consumer app calls these in Root.tsx / layout.tsx to register the Next.js implementations.\n * @module next-adapters\n */\n\nimport type { ImageProps, LinkProps, RouteAdapter, UIAdapter } from \"@spring-systems/ui/adapters\";\nimport nextDynamic from \"next/dynamic\";\nimport NextImage from \"next/image\";\nimport NextLink from \"next/link\";\nimport {\n useParams as useNextParams,\n usePathname as useNextPathname,\n useRouter as useNextRouter,\n useSearchParams as useNextSearchParams,\n} from \"next/navigation\";\nimport type React from \"react\";\n\nlet hasLoggedDefaultSsrWarning = false;\n\nexport interface NextUIAdapterOptions {\n /**\n * Default SSR behavior for `dynamic` when call-site does not provide `{ ssr }`.\n * Defaults to `true` for backward compatibility.\n */\n defaultDynamicSsr?: boolean;\n}\n\n/** Creates a RouteAdapter backed by Next.js App Router hooks. */\nexport function createNextRouteAdapter(): RouteAdapter {\n return {\n usePathname: useNextPathname,\n useSearchParams: () => useNextSearchParams() ?? new URLSearchParams(),\n useParams: () => (useNextParams() ?? {}) as Record<string, string | string[]>,\n useRouter: useNextRouter,\n };\n}\n\n/** Creates a UIAdapter backed by Next.js Image, Link, and dynamic imports. */\nexport function createNextUIAdapter(options?: NextUIAdapterOptions): UIAdapter {\n const defaultDynamicSsr = options?.defaultDynamicSsr ?? true;\n if (options?.defaultDynamicSsr === undefined && !hasLoggedDefaultSsrWarning && process.env.NODE_ENV !== \"production\") {\n hasLoggedDefaultSsrWarning = true;\n // Keeping backward-compatible default SSR behavior, but warn in dev to avoid accidental use in client-only apps.\n console.warn(\n \"[server] createNextUIAdapter() defaults dynamic imports to ssr:true. For client-only apps use createClientOnlyNextUIAdapter() or pass { defaultDynamicSsr: false }.\"\n );\n }\n return {\n Image: NextImage as React.ComponentType<ImageProps>,\n Link: NextLink as React.ComponentType<LinkProps>,\n dynamic: <T extends React.ComponentType<React.ComponentProps<T>>>(\n loader: () => Promise<{ default: T }>,\n opts?: { ssr?: boolean },\n ) => nextDynamic(loader, { ssr: opts?.ssr ?? defaultDynamicSsr }) as unknown as T,\n };\n}\n\n/** Creates a UIAdapter preset optimized for client-only apps (dynamic imports default to `ssr: false`). */\nexport function createClientOnlyNextUIAdapter(): UIAdapter {\n return createNextUIAdapter({ defaultDynamicSsr: false });\n}\n\n/** Creates a UIAdapter preset with SSR-enabled dynamic imports by default. */\nexport function createSsrNextUIAdapter(): UIAdapter {\n return createNextUIAdapter({ defaultDynamicSsr: true });\n}\n"],"mappings":";AASA,OAAO,iBAAiB;AACxB,OAAO,eAAe;AACtB,OAAO,cAAc;AACrB;AAAA,EACI,aAAa;AAAA,EACb,eAAe;AAAA,EACf,aAAa;AAAA,EACb,mBAAmB;AAAA,OAChB;AAGP,IAAI,6BAA6B;AAW1B,SAAS,yBAAuC;AACnD,SAAO;AAAA,IACH,aAAa;AAAA,IACb,iBAAiB,MAAM,oBAAoB,KAAK,IAAI,gBAAgB;AAAA,IACpE,WAAW,MAAO,cAAc,KAAK,CAAC;AAAA,IACtC,WAAW;AAAA,EACf;AACJ;AAGO,SAAS,oBAAoB,SAA2C;AAC3E,QAAM,oBAAoB,SAAS,qBAAqB;AACxD,MAAI,SAAS,sBAAsB,UAAa,CAAC,8BAA8B,QAAQ,IAAI,aAAa,cAAc;AAClH,iCAA6B;AAE7B,YAAQ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACA,SAAO;AAAA,IACH,OAAO;AAAA,IACP,MAAM;AAAA,IACN,SAAS,CACL,QACA,SACC,YAAY,QAAQ,EAAE,KAAK,MAAM,OAAO,kBAAkB,CAAC;AAAA,EACpE;AACJ;AAGO,SAAS,gCAA2C;AACvD,SAAO,oBAAoB,EAAE,mBAAmB,MAAM,CAAC;AAC3D;AAGO,SAAS,yBAAoC;AAChD,SAAO,oBAAoB,EAAE,mBAAmB,KAAK,CAAC;AAC1D;","names":[]}
|
package/dist/chunk-KA7RJCWA.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// src/security-headers.ts
|
|
2
|
-
var PERMISSIONS_POLICY_VALUE = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()";
|
|
3
|
-
var BASE_SECURITY_HEADER_VALUES = Object.freeze({
|
|
4
|
-
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
5
|
-
"X-Content-Type-Options": "nosniff",
|
|
6
|
-
"X-Frame-Options": "DENY",
|
|
7
|
-
"Cross-Origin-Opener-Policy": "same-origin",
|
|
8
|
-
"Cross-Origin-Resource-Policy": "same-origin",
|
|
9
|
-
"X-DNS-Prefetch-Control": "off",
|
|
10
|
-
"X-Permitted-Cross-Domain-Policies": "none",
|
|
11
|
-
"Origin-Agent-Cluster": "?1",
|
|
12
|
-
"Permissions-Policy": PERMISSIONS_POLICY_VALUE
|
|
13
|
-
});
|
|
14
|
-
var BASE_SECURITY_HEADERS = Object.entries(BASE_SECURITY_HEADER_VALUES).map(([key, value]) => ({
|
|
15
|
-
key,
|
|
16
|
-
value
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
PERMISSIONS_POLICY_VALUE,
|
|
21
|
-
BASE_SECURITY_HEADER_VALUES,
|
|
22
|
-
BASE_SECURITY_HEADERS
|
|
23
|
-
};
|
|
24
|
-
//# sourceMappingURL=chunk-KA7RJCWA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/security-headers.ts"],"sourcesContent":["export const PERMISSIONS_POLICY_VALUE =\n \"accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()\";\n\nexport const BASE_SECURITY_HEADER_VALUES: Readonly<Record<string, string>> = Object.freeze({\n \"Referrer-Policy\": \"strict-origin-when-cross-origin\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Cross-Origin-Opener-Policy\": \"same-origin\",\n \"Cross-Origin-Resource-Policy\": \"same-origin\",\n \"X-DNS-Prefetch-Control\": \"off\",\n \"X-Permitted-Cross-Domain-Policies\": \"none\",\n \"Origin-Agent-Cluster\": \"?1\",\n \"Permissions-Policy\": PERMISSIONS_POLICY_VALUE,\n});\n\nexport const BASE_SECURITY_HEADERS = Object.entries(BASE_SECURITY_HEADER_VALUES).map(([key, value]) => ({\n key,\n value,\n}));\n"],"mappings":";AAAO,IAAM,2BACT;AAEG,IAAM,8BAAgE,OAAO,OAAO;AAAA,EACvF,mBAAmB;AAAA,EACnB,0BAA0B;AAAA,EAC1B,mBAAmB;AAAA,EACnB,8BAA8B;AAAA,EAC9B,gCAAgC;AAAA,EAChC,0BAA0B;AAAA,EAC1B,qCAAqC;AAAA,EACrC,wBAAwB;AAAA,EACxB,sBAAsB;AAC1B,CAAC;AAEM,IAAM,wBAAwB,OAAO,QAAQ,2BAA2B,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;AAAA,EACpG;AAAA,EACA;AACJ,EAAE;","names":[]}
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,SAAS,0BAA0B;AACnC,SAAS,SAAS,eAAe;AACjC,SAA2B,oBAAoB;AAsC/C,IAAI;AACJ,SAAS,gBAAwB;AAC7B,MAAI,qBAAqB,OAAW,QAAO;AAC3C,QAAM,OAAO,QAAQ,IAAI,WAAW,IAAI,KAAK;AAC7C,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,yCAAyC;AACnE,MAAI;AACA,QAAI,IAAI,GAAG;AAAA,EACf,QAAQ;AACJ,UAAM,IAAI,MAAM,gCAAgC,GAAG,GAAG;AAAA,EAC1D;AACA,qBAAmB;AACnB,SAAO;AACX;AAGA,SAAS,eAAe,SAAiB,UAA0B;AAC/D,QAAM,MAAM,QAAQ,IAAI,OAAO;AAC/B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,OAAO,GAAG;AACzB,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GAAG;AACzC,UAAM,IAAI,MAAM,GAAG,OAAO,qCAAqC,GAAG,GAAG;AAAA,EACzE;AACA,SAAO;AACX;AAEA,IAAM,+BAA+B,eAAe,qCAAqC,QAAU;AACnG,IAAM,2BAA2B,eAAe,iCAAiC,QAAU;AAC3F,IAAM,yBAAyB,oBAAI,IAAI,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AACrE,IAAM,aAAa,QAAQ,IAAI,eAAe;AAI9C,SAAS,qBAAkC;AACvC,SAAO,IAAI,IAAI,mBAAmB,EAAE,KAAK,eAAe;AAC5D;AAEA,SAAS,qBAAyC;AAC9C,SAAO,mBAAmB,EAAE,MAAM;AACtC;AAEA,SAAS,wBAAgC;AACrC,SAAO,mBAAmB,EAAE,MAAM;AACtC;AAEA,SAAS,6BAA4C;AACjD,SAAO,+BAA+B;AAC1C;AAMA,SAAS,kBAAkB,aAA8B;AACrD,SAAO,cACD,uBAAuB,0BAA0B,QAAU,IAC3D,uBAAuB,8BAA8B,QAAU;AACzE;AAEA,SAAS,UAAU,OAAe,MAAgC;AAC9D,MAAI,CAAC,WAAY;AACjB,UAAQ,aAAa,KAAK,IAAI,IAAI;AACtC;AAEA,SAAS,qBAAqB,OAAuB;AACjD,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,UAAU,mBAAmB;AACnC,MAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,OAAO,EAAG,QAAO;AAC/C,SAAO;AACX;AAEA,SAAS,wBACL,SACA,OACA,QACA,aACY;AACZ,QAAM,UAAU,IAAI,QAAQ,WAAW;AACvC,mBAAiB,SAAS,SAAS,mBAAmB,OAAO,CAAC;AAC9D,SAAO,aAAa,KAAK,EAAE,MAAM,GAAG,EAAE,QAAQ,QAAQ,CAAC;AAC3D;AAOA,eAAsB,IAAI,SAAsB,SAAkD;AAC9F,QAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,SAAO,cAAc,SAAS,MAAM,KAAK;AAC7C;AAGA,eAAsB,KAAK,SAAsB,SAAkD;AAC/F,QAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,SAAO,cAAc,SAAS,MAAM,MAAM;AAC9C;AAGA,eAAsB,IAAI,SAAsB,SAAkD;AAC9F,QAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,SAAO,cAAc,SAAS,MAAM,KAAK;AAC7C;AAGA,eAAsB,OAAO,SAAsB,SAAkD;AACjG,QAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,SAAO,cAAc,SAAS,MAAM,QAAQ;AAChD;AAGA,eAAsB,MAAM,SAAsB,SAAkD;AAChG,QAAM,EAAE,KAAK,IAAI,MAAM,QAAQ;AAC/B,SAAO,cAAc,SAAS,MAAM,OAAO;AAC/C;AAMA,eAAe,cAAc,SAAsB,cAAwB,QAAgB;AACvF,QAAM,eAAe,aAAa,KAAK,GAAG;AAC1C,MAAI;AACA,8BAA0B,KAAK,IAAI,CAAC;AAEpC,UAAM,oBAAoB,2BAA2B;AACrD,QAAI,mBAAmB;AACnB,aAAO,wBAAwB,SAAS,mBAAmB,GAAG;AAAA,IAClE;AAGA,QAAI,CAAC,aAAa,MAAM,CAAC,YAAY,uBAAuB,OAAO,CAAC,GAAG;AACnE,aAAO,wBAAwB,SAAS,gBAAgB,GAAG;AAAA,IAC/D;AACA,QAAI,aAAa,SAAS,sBAAsB,GAAG;AAC/C,aAAO,wBAAwB,SAAS,iBAAiB,GAAG;AAAA,IAChE;AAGA,UAAM,aAAa,cAAc,EAAE,QAAQ,QAAQ,EAAE;AACrD,QAAI,CAAC,YAAY;AACb,aAAO,wBAAwB,SAAS,6BAA6B,GAAG;AAAA,IAC5E;AAEA,UAAM,aAAa,IAAI,IAAI,UAAU;AACrC,UAAM,cAAc,WAAW;AAC/B,UAAM,cAAc,uBAAuB,WAAW;AACtD,UAAM,kBAAkB,sBAAsB,YAAY;AAE1D,QAAI,iBAAiB;AACrB,QAAI,eAAe,mBAAmB,YAAY,YAAY,MAAM,gBAAgB,YAAY,GAAG;AAC/F,uBAAiB,aAAa,QAAQ,IAAI,OAAO,IAAI,eAAe,MAAM,GAAG,GAAG,EAAE;AAAA,IACtF;AAEA,UAAM,aAAa,GAAG,UAAU,IAAI,eAAe,QAAQ,QAAQ,EAAE,CAAC;AACtE,UAAM,oBAAoB,cAAc,cAAc;AACtD,UAAM,aAAa,mBAAmB,EAAE;AACxC,UAAM,aAAa,WAAW,aAAa,cAAc,YAAY;AACrE,UAAM,cAAc,WAAW,cAAc,eAAe,YAAY;AACxE,UAAM,YAAY,WAAW,YAAY,aAAa,YAAY;AAClE,UAAM,eAAe,sBAAsB;AAC3C,UAAM,gBAAgB,sBAAsB;AAC5C,UAAM,cACF,sBAAsB,aAAa,sBAAsB,cAAc,sBAAsB;AACjG,UAAM,QAAQ,IAAI,IAAI,QAAQ,GAAG;AACjC,UAAM,YAAY,IAAI,IAAI,UAAU;AACpC,UAAM,aAAa,QAAQ,CAAC,GAAG,MAAM;AACjC,UAAI,CAAC,UAAU,aAAa,IAAI,CAAC,EAAG,WAAU,aAAa,IAAI,GAAG,CAAC;AAAA,IACvE,CAAC;AAGD,UAAM,UAAU,IAAI,QAAQ;AAC5B,UAAM,0BAA0B,4BAA4B,QAAQ,OAAO;AAC3E,YAAQ,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACpC,YAAM,QAAQ,IAAI,YAAY;AAC9B,UACI,UAAU,UACV,UAAU,oBACV,UAAU,YACV,UAAU,mBACV,yBAAyB,OAAO,uBAAuB,GACzD;AACE;AAAA,MACJ;AACA,cAAQ,IAAI,KAAK,KAAK;AAAA,IAC1B,CAAC;AAED,QAAI,CAAC,QAAQ,IAAI,kBAAkB,GAAG;AAClC,cAAQ,IAAI,oBAAoB,gBAAgB;AAAA,IACpD;AAGA,UAAM,eAAe,gBAAgB,OAAO;AAC5C,UAAM,yBAAyB,QAAQ,QAAQ,IAAI,eAAe,KAAK,IAAI,KAAK;AAChF,UAAM,yBAAyB,kBAAkB,KAAK,qBAAqB;AAC3E,cAAU,iBAAiB;AAAA,MACvB,MAAM;AAAA,MACN;AAAA,MACA,iBAAiB,CAAC,CAAC;AAAA,MACnB;AAAA,MACA,MAAM,QAAQ,QAAQ;AAAA,MACtB,UAAU,QAAQ,QAAQ;AAAA,IAC9B,CAAC;AAGD,QAAI,sBAAsB,YAAY,CAAC,gBAAgB,CAAC,wBAAwB;AAC5E,YAAM,MAAM,IAAI,aAAa,MAAM,EAAE,QAAQ,IAAI,CAAC;AAClD,uBAAiB,IAAI,SAAS,SAAS,mBAAmB,OAAO,CAAC;AAClE,UAAI,QAAQ,IAAI,iBAAiB,uDAAuD;AACxF,UAAI,QAAQ,IAAI,UAAU,UAAU;AACpC,UAAI,QAAQ,IAAI,WAAW,GAAG;AAC9B,yBAAmB,KAAK,OAAO;AAC/B,gBAAU,oCAAoC;AAAA,QAC1C,MAAM,QAAQ,QAAQ;AAAA,QACtB,UAAU,QAAQ,QAAQ;AAAA,MAC9B,CAAC;AACD,aAAO;AAAA,IACX;AAGA,QAAI,6BAA6B,SAAS,QAAQ,iBAAiB,GAAG;AAClE;AAAA,QACI;AAAA,QACA,WAAW,MAAM,IAAI,iBAAiB,mBAAc,QAAQ,QAAQ,IAAI,QAAQ,KAAK,MAAM,cAAc,QAAQ,QAAQ,IAAI,SAAS,KAAK,MAAM,qBAAqB,QAAQ,QAAQ,IAAI,gBAAgB,KAAK,MAAM;AAAA,MACzN;AACA,aAAO,wBAAwB,SAAS,aAAa,GAAG;AAAA,IAC5D;AAGA,QAAI,gBAAgB,CAAC,mBAAmB,EAAE,IAAI,iBAAiB,GAAG;AAC9D,cAAQ,IAAI,iBAAiB,UAAU,YAAY,EAAE;AAAA,IACzD,WAAW,wBAAwB;AAC/B,cAAQ,IAAI,iBAAiB,qBAAqB;AAAA,IACtD;AAKA,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AAEtD,UAAM,gBAAgB,qBAAqB,YAAY;AACvD,QAAI,eAAe;AACf,cAAQ,IAAI,UAAU,aAAa;AACnC,cAAQ,IAAI,QAAQ,aAAa;AACjC,UAAI,CAAC,UAAU,aAAa,IAAI,MAAM,EAAG,WAAU,aAAa,IAAI,QAAQ,aAAa;AACzF,UAAI,CAAC,UAAU,aAAa,IAAI,MAAM,EAAG,WAAU,aAAa,IAAI,QAAQ,aAAa;AAAA,IAC7F;AAGA,QAAI,OAAO;AACX,QAAI,qBAA2C;AAC/C,QAAI,WAAW,SAAS,WAAW,QAAQ;AACvC,YAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,KAAK;AAC3D,YAAM,cAAc,YAAY,SAAS,qBAAqB;AAC9D,YAAM,YAAY,kBAAkB,WAAW;AAC/C,YAAM,gBAAgB,mBAAmB,QAAQ,QAAQ,IAAI,gBAAgB,CAAC;AAC9E,UAAI,kBAAkB,QAAQ,gBAAgB,WAAW;AACrD,eAAO,wBAAwB,SAAS,qBAAqB,GAAG;AAAA,MACpE;AAEA,UAAI,aAAa;AACb,YAAI,kBAAkB,MAAM;AACxB,iBAAO,wBAAwB,SAAS,iDAAiD,GAAG;AAAA,QAChG;AACA,eAAO,QAAQ,OAAO,4BAA4B,QAAQ,MAAM,SAAS,IAAI,QAAQ;AAAA,MACzF,OAAO;AACH,eAAO,MAAM,QAAQ,YAAY;AACjC,YAAI,KAAK,aAAa,WAAW;AAC7B,iBAAO,wBAAwB,SAAS,qBAAqB,GAAG;AAAA,QACpE;AACA,YAAI,gBAAgB,YAAY,SAAS,kBAAkB,GAAG;AAC1D,cAAI;AACA,kBAAM,UAAU,IAAI,YAAY,EAAE,OAAO,IAAI;AAC7C,kBAAM,SAAS,KAAK,MAAM,OAAO;AACjC,kBAAM,gBAAgB,WAAW,sBAAsB;AACvD,iCAAqB,sBAAsB,SAAS,OAAO,OAAO,aAAa,KAAK,EAAE,CAAC;AACvF,kBAAM,eAAe,yBAAyB,oBAAoB,KAAK,IAAI,CAAC;AAC5E,gBAAI,eAAe,GAAG;AAClB;AAAA,gBACI;AAAA,gBACA,8BAA8B,mBAAmB,OAAO,uBAAkB,KAAK,KAAK,eAAe,GAAI,CAAC;AAAA,cAC5G;AACA,qBAAO,wBAAwB,SAAS,6CAA6C,KAAK;AAAA,gBACtF,eAAe,OAAO,KAAK,KAAK,eAAe,GAAI,CAAC;AAAA,cACxD,CAAC;AAAA,YACL;AAAA,UACJ,QAAQ;AACJ,iCAAqB,sBAAsB,SAAS,EAAE;AAAA,UAC1D;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAGA,UAAM,eAAkD;AAAA,MACpD;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,QAAQ,YAAY,QAAQ,mBAAmB,EAAE,KAAK,aAAa,GAAM;AAAA,IAC7E;AACA,QAAI,gBAAgB,eAAgB,cAAa,SAAS;AAE1D,UAAM,WAAW,MAAM,MAAM,UAAU,SAAS,GAAG,YAAY;AAG/D,UAAM,kBAAkB,IAAI,QAAQ;AACpC,UAAM,2BAA2B,4BAA4B,SAAS,OAAO;AAC7E,aAAS,QAAQ,QAAQ,CAAC,OAAO,QAAQ;AACrC,YAAM,QAAQ,IAAI,YAAY;AAC9B,UAAI,UAAU,sBAAsB,UAAU,iBAAkB;AAChE,UAAI,yBAAyB,OAAO,wBAAwB,EAAG;AAC/D,sBAAgB,IAAI,KAAK,KAAK;AAAA,IAClC,CAAC;AAKD,QAAI,eAA6D;AACjE,QAAI,aAAa;AACjB,QAAI,cAAc,SAAS;AAE3B,QAAI,cAAc;AACd,YAAM,eAAe,SAAS,QAAQ,IAAI,cAAc,KAAK,IAAI,YAAY;AAC7E,UAAI,YAAY,SAAS,kBAAkB,GAAG;AAC1C,YAAI;AACA,gBAAM,UAAU,MAAM,SAAS,KAAK;AACpC,gBAAM,UAAU,KAAK,MAAM,OAAO;AAClC,gBAAM,cAAc,WAAW,uBAAuB;AAAA,YAClD,aAAa;AAAA,YACb,cAAc;AAAA,UAClB;AACA,gBAAM,mBAAmB,QAAQ,YAAY,WAAW;AACxD,gBAAM,cAAc,OAAO,qBAAqB,WAAW,iBAAiB,KAAK,IAAI;AACrF,cAAI,aAAa;AACb,yBAAa;AAAA,UACjB;AAGA,gBAAM,mBAAmB,EAAE,GAAG,QAAQ;AACtC,iBAAO,iBAAiB,YAAY,WAAW;AAC/C,iBAAO,iBAAiB,YAAY,YAAY;AAChD,yBAAe,KAAK,UAAU,gBAAgB;AAC9C,0BAAgB,IAAI,gBAAgB,iCAAiC;AACrE,0BAAgB,OAAO,gBAAgB;AAAA,QAC3C,QAAQ;AAEJ,yBAAe,KAAK,UAAU,EAAE,OAAO,+CAA+C,CAAC;AAGvF,wBAAc;AACd,0BAAgB,IAAI,gBAAgB,iCAAiC;AACrE,0BAAgB,OAAO,gBAAgB;AAAA,QAC3C;AAAA,MACJ;AAAA,IACJ;AAGA,UAAM,WAAW,mBAAmB,OAAO;AAC3C,qBAAiB,iBAAiB,SAAS,QAAQ;AACnD,QAAI,aAAa;AACb,sBAAgB,IAAI,iBAAiB,uDAAuD;AAC5F,sBAAgB,IAAI,UAAU,UAAU;AACxC,sBAAgB,IAAI,WAAW,GAAG;AAAA,IACtC,WAAW,CAAC,gBAAgB,IAAI,eAAe,GAAG;AAI9C,sBAAgB,IAAI,iBAAiB,mBAAmB;AAAA,IAC5D;AACA,QAAI,iBAAiB,SAAS,UAAU,OAAO,SAAS,SAAS,OAAO,CAAC,mBAAmB,OAAO,GAAG;AAClG,sBAAgB,IAAI,mBAAmB,oBAAoB;AAAA,IAC/D;AAEA,UAAM,wBAAwB,MAAM,gCAAgC,QAAQ;AAI5E,QAAI,CAAC,aAAc,gBAAe,SAAS;AAE3C,UAAM,eAAe,IAAI,aAAa,cAAc;AAAA,MAChD,QAAQ;AAAA,MACR,SAAS;AAAA,IACb,CAAC;AAGD,QAAI,cAAc,SAAS,UAAU,OAAO,SAAS,SAAS,KAAK;AAC/D,uBAAiB,cAAc,SAAS,UAAU;AAClD,UAAI,oBAAoB;AACpB,+BAAuB,kBAAkB;AAAA,MAC7C;AAAA,IACJ,WAAW,gBAAgB,sBAAsB,uBAAuB,IAAI,WAAW,GAAG;AACtF,iCAA2B,oBAAoB,KAAK,IAAI,CAAC;AAAA,IAC7D,WACI,iBACA,SAAS,WAAW,OACnB,sBAAsB,YAAY,SAAS,WAAW,OACvD,uBACF;AACE,gBAAU,wBAAwB;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,QAAQ,SAAS;AAAA,QACjB,QAAQ,gBACF,WACA,SAAS,WAAW,MAClB,eACA,sBAAsB,YAAY,SAAS,WAAW,MACpD,kBACA,wBACE,2BACA;AAAA,QACZ,iBAAiB,CAAC,CAAC;AAAA,MACvB,CAAC;AACD,yBAAmB,cAAc,OAAO;AAAA,IAC5C;AAEA,WAAO;AAAA,EACX,SAAS,OAAgB;AACrB,QAAI,uBAAuB,KAAK,GAAG;AAC/B,aAAO,wBAAwB,SAAS,qBAAqB,GAAG;AAAA,IACpE;AACA,UAAM,QAAQ,QAAQ,IAAI,aAAa;AACvC,UAAM,WAAW,aAAa;AAAA,MAC1B;AAAA,QACI,OAAO;AAAA,QACP,GAAI,QAAQ,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,IAAI,CAAC;AAAA,MACvF;AAAA,MACA,EAAE,QAAQ,IAAI;AAAA,IAClB;AAEA,UAAM,yBAAyB,cAAc,YAAY,EAAE,QAAQ,uBAAuB,EAAE;AAC5F,UAAM,mBAAmB,mBAAmB,EAAE,KAAK,cAAc,eAAe,YAAY;AAC5F,qBAAiB,SAAS,SAAS,SAAS,mBAAmB,OAAO,CAAC;AACvE,QAAI,2BAA2B,iBAAiB;AAC5C,yBAAmB,UAAU,OAAO;AACpC,eAAS,QAAQ,IAAI,iBAAiB,uDAAuD;AAC7F,eAAS,QAAQ,IAAI,UAAU,UAAU;AACzC,eAAS,QAAQ,IAAI,WAAW,GAAG;AACnC,gBAAU,wBAAwB;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ;AAAA,MACZ,CAAC;AAAA,IACL;AAEA,WAAO;AAAA,EACX;AACJ;AAGA,eAAsB,QAAQ,SAAsB;AAChD,QAAM,UAAU,IAAI,QAAQ;AAC5B,QAAM,WAAW,mBAAmB,OAAO;AAC3C,mBAAiB,SAAS,SAAS,QAAQ;AAC3C,SAAO,IAAI,aAAa,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAC1D;","names":[]}
|