@spring-systems/server 0.8.6 → 0.8.9
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 +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,19 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
DELETE,
|
|
3
|
-
GET,
|
|
4
|
-
OPTIONS,
|
|
5
|
-
PATCH,
|
|
6
|
-
POST,
|
|
7
|
-
PUT
|
|
8
|
-
} from "./chunk-NFJ25NQQ.js";
|
|
9
|
-
import "./chunk-PZWKMIA4.js";
|
|
10
|
-
import "./chunk-7IUSTA5W.js";
|
|
11
|
-
export {
|
|
12
|
-
DELETE,
|
|
13
|
-
GET,
|
|
14
|
-
OPTIONS,
|
|
15
|
-
PATCH,
|
|
16
|
-
POST,
|
|
17
|
-
PUT
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=api-route-handler.js.map
|
|
1
|
+
import{a,b,c,d,e,f}from"./chunk-VB7DEUGT.js";import"./chunk-M23YQYLU.js";import"./chunk-E4AX7MID.js";export{d as DELETE,a as GET,f as OPTIONS,e as PATCH,b as POST,c as PUT};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var s=class{ipStore=new Map;accountStore=new Map;getStore(t){return t==="ip"?this.ipStore:this.accountStore}get(t,e){return this.getStore(t).get(e)??null}set(t,e,r){this.getStore(t).set(e,r)}delete(t,e){this.getStore(t).delete(e)}size(t){return this.getStore(t).size}evictOldest(t,e){let r=this.getStore(t),o=[...r.entries()].sort((i,c)=>i[1].windowStartedAt-c[1].windowStartedAt);for(let i=0;i<e&&i<o.length;i++){let c=o[i];if(!c)break;r.delete(c[0])}}sweepExpired(t,e,r){let o=this.getStore(t);for(let[i,c]of o.entries()){let u=e-c.windowStartedAt>r,p=c.blockedUntil<=e;u&&p&&o.delete(i)}}},a=new s,l=!1;function g(n){a=n}function f(){return a}function w(n,t,e){!l&&a instanceof s&&process.env.NODE_ENV==="production"&&(l=!0,console.warn("[server] Rate limiter is using in-memory storage in production. For multi-instance deployments, call setRateLimiterAdapter() with a shared-storage adapter."));let r=Date.now(),o=d(a,"ip",n,e.maxAttemptsByIpAndAccount,e,r);if(o)return o;if(t){let i=d(a,"account",t,e.maxAttemptsByAccount,e,r);if(i)return i}return null}function A(n,t,e){let r=Date.now();m(a,"ip",n,e,r),t&&m(a,"account",t,e,r)}function b(n,t){a.delete("ip",n),t&&a.delete("account",t)}function d(n,t,e,r,o,i){let c=n.get(t,e);if(!c)return null;if(c.blockedUntil>i){let u=Math.ceil((c.blockedUntil-i)/1e3);return`Rate limited (${t}). Try again in ${u}s.`}return i-c.windowStartedAt>o.windowMs?(n.delete(t,e),null):c.count>=r?(c.blockedUntil=i+o.blockMs,n.set(t,e,c),`Too many attempts (${t}). Blocked for ${Math.ceil(o.blockMs/1e3)}s.`):null}function m(n,t,e,r,o){n.size(t)>=r.maxKeys&&n.evictOldest(t,Math.floor(r.maxKeys*.1));let i=n.get(t,e);!i||o-i.windowStartedAt>r.windowMs?n.set(t,e,{count:1,windowStartedAt:o,blockedUntil:0}):(i.count++,n.set(t,e,i))}export{g as a,f as b,w as c,A as d,b as e};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var r="accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()",i=Object.freeze({"Referrer-Policy":"strict-origin-when-cross-origin","X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","Cross-Origin-Opener-Policy":"same-origin","Cross-Origin-Resource-Policy":"same-origin","X-DNS-Prefetch-Control":"off","X-Permitted-Cross-Domain-Policies":"none","Origin-Agent-Cluster":"?1","Permissions-Policy":r}),n=Object.entries(i).map(([e,o])=>({key:e,value:o}));export{r as a,i as b,n as c};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{b as R}from"./chunk-FXUI75TW.js";import{getFrameworkConfig as S}from"@spring-systems/core/config";import{NextResponse as g}from"next/server.js";function O(r){let s=r.trim();if(!s)return[];try{let t=new URL(s),n=[t.origin];return t.protocol==="https:"?n.push(`wss://${t.host}`):t.protocol==="http:"&&n.push(`ws://${t.host}`),n}catch{return[]}}var j=new Set(["javascript:","data:","blob:","vbscript:","filesystem:"]);function l(r){let s=r.trim();if(!s||/[\s;,\r\n]/.test(s))return null;if(/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(s)){let t=s.toLowerCase();return j.has(t)?null:t}try{let t=new URL(s);return!["http:","https:","ws:","wss:"].includes(t.protocol)||t.username||t.password||t.pathname!=="/"||t.search||t.hash?null:`${t.protocol}//${t.host}`}catch{return null}}function D(r){let s=r.trim();if(!s||/[\s;,\r\n"]/.test(s))return null;try{let t=new URL(s);return!["http:","https:"].includes(t.protocol)||t.username||t.password?null:t.toString()}catch{return null}}function u(r){let s=r.split(",").map(t=>l(t)).filter(t=>!!t);return[...new Set(s)]}function M(r){let s=r.nextUrl.clone(),t=process.env.TARGET_ENV||"test",n=t==="prod"||process.env.NODE_ENV==="production",o=t==="prod",i=s.hostname==="localhost"||s.hostname==="127.0.0.1",c=crypto.randomUUID().replace(/-/g,""),f=process.env.CSP_DENY_STYLE_ATTR==="true",E=!o,_=u(process.env.CSP_CONNECT_HOSTS||""),C=O(process.env.API_URL||""),P=u(process.env.CSP_IMG_HOSTS||""),w=u(process.env.CSP_SCRIPT_HOSTS||""),x=u(process.env.CSP_FRAME_HOSTS||""),p=S().csp,T=p.thirdPartyConnectSources.map(e=>l(e)).filter(e=>!!e),H=p.thirdPartyImageSources.map(e=>l(e)).filter(e=>!!e),U=p.thirdPartyScriptSources.map(e=>l(e)).filter(e=>!!e),b=p.thirdPartyFrameSources.map(e=>l(e)).filter(e=>!!e),L=["'self'",...T,...C,..._,...n?[]:["http://localhost:*","http://127.0.0.1:*","ws://localhost:*","ws://127.0.0.1:*"]].join(" "),$=["'self'","data:","blob:",...H,...P].join(" "),y=[...U,...w].join(" "),N=[...b,...x].join(" "),a=D(process.env.CSP_REPORT_URI||""),d=["default-src 'self'","base-uri 'self'","object-src 'none'",`script-src 'self' 'nonce-${c}' 'strict-dynamic' ${y}`,`script-src-elem 'self' 'nonce-${c}' 'strict-dynamic' ${y}`,"script-src-attr 'none'",E?"style-src 'self' 'unsafe-inline'":`style-src 'self' 'nonce-${c}'`,f?"style-src-attr 'none'":"style-src-attr 'unsafe-inline'",`img-src ${$}`,"media-src 'self' blob: data:","manifest-src 'self'",`connect-src ${L}`,`frame-src ${N||"'none'"}`,"frame-ancestors 'none'","form-action 'self'",...o?["upgrade-insecure-requests"]:[],...a?[`report-uri ${a}`,"report-to csp-violations"]:[]].join("; "),A=S().proxy.blockedPathPrefixesInProd??[];if(n){for(let e of A)if(s.pathname.startsWith(e)){let m=new g("Not Found",{status:404});return h(m,{isLocalHost:i,isTargetProd:o,csp:d,nonce:c,reportUri:a})}}if(s.pathname==="/"){let e=new URL(s.toString());!i&&o&&(e.protocol="https:"),e.pathname=S().app.defaultRoute,e.search="";let m=k(e,308);return h(m,{isLocalHost:i,isTargetProd:o,csp:d,nonce:c,reportUri:a})}let v=new Headers(r.headers);v.set("x-nonce",c);let I=g.next({request:{headers:v}});return h(I,{isLocalHost:i,isTargetProd:o,csp:d,nonce:c,reportUri:a})}function h(r,s){let{isLocalHost:t,csp:n,nonce:o,reportUri:i}=s;n&&r.headers.set("Content-Security-Policy",n);for(let[c,f]of Object.entries(R))r.headers.set(c,f);return s.isTargetProd&&!t&&r.headers.set("Strict-Transport-Security","max-age=63072000; includeSubDomains; preload"),i&&r.headers.set("Reporting-Endpoints",`csp-violations="${i}"`),o&&r.headers.set("x-nonce",o),r}function k(r,s=308){let t=new g(null,{status:s});return t.headers.set("Location",typeof r=="string"?r:r.toString()),t}var Y={matcher:["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)"]};export{M as a,Y as b};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{b as g,c as T,d as h,e as E}from"./chunk-E4AX7MID.js";import{getFrameworkConfig as x,getSecureSessionCookieName as I,getSessionCookieName as H}from"@spring-systems/core/config";var U=604800;function M(){return Number(process.env.AUTH_COOKIE_MAX_AGE_SECONDS||"28800")}function F(){return(process.env.TARGET_ENV||"")==="prod"}function c(){return H(x().auth.sessionCookiePrefix)}function l(){return I(x().auth.sessionCookiePrefix)}function v(){return new Set(x().auth.sessionExpiredCodes)}function K(e){return e.nextUrl.hostname==="localhost"||e.nextUrl.hostname==="127.0.0.1"||e.nextUrl.hostname==="::1"||e.nextUrl.hostname==="[::1]"}function A(e){return K(e)?!1:F()?!0:e.nextUrl.protocol==="https:"}function D(e){return A(e)?l():c()}function S(e,t,r,n,o){let i=A(t);e.cookies.set({name:r,value:n,httpOnly:!0,secure:i,sameSite:"lax",path:"/",maxAge:o,priority:"high"})}function ne(e,t,r){let n=M(),o=Number.isFinite(n)&&n>0?Math.floor(n):28800,i=Math.min(o,U),s=D(t),u=s===c()?l():c();S(e,t,s,r,i),S(e,t,u,"",0)}function w(e,t,r){let n=A(t);e.cookies.set({name:r,value:"",httpOnly:!0,secure:n,sameSite:"lax",path:"/",maxAge:0,priority:"high"});let o=!n,i=[`${r}=`,"Path=/","Max-Age=0","HttpOnly","SameSite=Lax","Priority=High"];o&&i.push("Secure"),e.headers.append("Set-Cookie",i.join("; "))}function oe(e,t){w(e,t,c()),w(e,t,l())}function ie(e){let t=[c(),l()];for(let o of t){let i=(e.cookies.get(o)?.value||"").trim();if(i)return i}let r=e.headers.get("cookie")||"";if(!r)return"";let n=r.split(";").map(o=>o.trim());for(let o of n){if(!o)continue;let i=o.indexOf("=");if(i<=0)continue;let s=o.slice(0,i).trim();if(s!==c()&&s!==l())continue;let u=o.slice(i+1).trim();if(u)try{return decodeURIComponent(u)}catch{return u}}return""}function z(e){return typeof e=="string"?e.trim().toLowerCase():""}function L(e){let t=z(e);return t!==""&&v().has(t)}async function se(e){if(e.status!==403)return!1;try{let t=e.clone();try{let r=await t.json();return L(r.code)||L(r.error_code)}catch{return!1}}catch{return!1}}import{getFrameworkConfig as V}from"@spring-systems/core/config";function m(e){let t=e;if(typeof t.ip=="string")return t.ip.trim()||void 0}var G=new Set(["connection","keep-alive","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade","proxy-connection"]);function a(e,t){return Number.isFinite(e)&&e>0?Math.floor(e):t}function ce(e){if(!e)return null;let t=Number(e);return!Number.isFinite(t)||t<0?null:Math.floor(t)}function ae(e){return e.replace(/^\/+|\/+$/g,"").toLowerCase()}function le(e){return!(!e||e==="."||e===".."||e.includes("\\")||e.includes("/")||/[\u0000-\u001F\u007F]/.test(e))}function fe(e){return e.match(/\/(v\d+(?:\.\d+)?)\/?$/i)?.[1]??null}function pe(e){return e.match(/^(v\d+(?:\.\d+)?)(?:\/|$)/i)?.[1]??null}function me(e){let t=(e.get("connection")||"").trim();return t?new Set(t.split(",").map(r=>r.trim().toLowerCase()).filter(Boolean)):new Set}function de(e,t){let r=e.toLowerCase();return G.has(r)||t.has(r)}var p=class extends Error{code="PAYLOAD_TOO_LARGE";constructor(t){super(t),this.name="PayloadTooLargeError"}};function ge(e,t){let r=0;return e.pipeThrough(new TransformStream({transform(n,o){if(r+=n.byteLength,r>t){o.error(new p("Payload too large"));return}o.enqueue(n)}}))}function xe(e){let t=[e],r=new Set;for(;t.length>0;){let n=t.shift();if(!(!n||r.has(n))){if(r.add(n),n instanceof p)return!0;if(n instanceof Error){if(n.name==="PayloadTooLargeError"||n.code==="PAYLOAD_TOO_LARGE")return!0;let i=n.cause;i&&t.push(i)}else if(typeof n=="object"){let o=n;if(o.code==="PAYLOAD_TOO_LARGE"||typeof o.name=="string"&&o.name==="PayloadTooLargeError")return!0;o.cause&&t.push(o.cause)}}}return!1}var B=new Set(["POST","PUT","PATCH","DELETE"]);function q(){return(process.env.FRONTEND_URL||"").trim()}function _(){return(process.env.FRONTEND_IP||"").trim()}function X(){return process.env.AUTH_TRUST_PROXY_HEADERS==="true"}function O(){return new Set((process.env.CSRF_EXEMPT_PATHS||"").split(",").map(e=>e.trim().replace(/^\/+|\/+$/g,"").toLowerCase()).filter(Boolean))}function P(){return(process.env.NODE_ENV||"")==="production"||(process.env.TARGET_ENV||"")==="prod"}function d(e){try{return new URL(e).origin}catch{return null}}function Ce(e){let t=_();if(!t)return!1;let r=m(e);if(r)return r===t;if(!X())return!1;let o=(e.headers.get("x-forwarded-for")||"").trim().split(",")[0]?.trim();if(o)return o===t;let i=(e.headers.get("x-real-ip")||"").trim();return i?i===t:!1}function y(e){let t=(e.headers.get("origin")||"").trim();if(!t)return null;let r=d(t);if(!r)return null;let n=q(),o=n?d(n):null;if(o&&r===o)return r;let i=_();if(!i)return null;try{let s=new URL(r);return s.hostname!==i||!["http:","https:"].includes(s.protocol)||P()&&s.protocol!=="https:"||s.protocol==="http:"&&s.port!==""&&s.port!=="80"||s.protocol==="https:"&&s.port!==""&&s.port!=="443"?null:r}catch{return null}}function $(e){return B.has(e.toUpperCase())}function N(e,t){let r=(e.get("Vary")||"").trim();if(!r){e.set("Vary",t);return}let n=r.split(",").map(i=>i.trim()).filter(Boolean);if(n.some(i=>i==="*")){e.set("Vary","*");return}if(n.some(i=>i.toLowerCase()===t.toLowerCase())){e.set("Vary",n.join(", "));return}e.set("Vary",[...n,t].join(", "))}function j(e){let t=(e.headers.get("sec-fetch-site")||"").trim().toLowerCase();return t?!(t==="same-origin"||t==="same-site"||t==="none"):!1}function Y(e){let t=(e.headers.get("origin")||"").trim();if(!t)return!1;let r=d(t);if(!r)return!1;let n=e.nextUrl.origin;if(r===n)return!0;let o=y(e);return!!o&&o===r}function W(e){let t=(e.headers.get("referer")||"").trim();return t?d(t):null}function J(e){let t=W(e);if(!t)return!1;if(t===e.nextUrl.origin)return!0;let r=y(e);return!!r&&r===t}function Te(e,t,r){let n=O();return!$(t)||n.has(r.toLowerCase())?!1:j(e)?!0:!(Y(e)||J(e))}function he(e,t,r){let n=y(t),o=V().proxy.corsAllowedHeaders;if(r){n&&e.set("Access-Control-Allow-Origin",n),e.set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS"),e.set("Access-Control-Allow-Headers",o),e.set("Access-Control-Max-Age","86400"),n?e.set("Access-Control-Allow-Credentials","true"):e.delete("Access-Control-Allow-Credentials"),N(e,"Origin");return}n?(e.set("Access-Control-Allow-Origin",n),e.set("Access-Control-Allow-Credentials","true")):e.delete("Access-Control-Allow-Credentials"),N(e,"Origin"),e.set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, PATCH, OPTIONS"),e.set("Access-Control-Allow-Headers",o),e.set("Access-Control-Max-Age","86400")}function Q(){return O().size>0}function Ee(){return P()&&Q()?"CSRF_EXEMPT_PATHS is not allowed in production":null}function R(){return process.env.AUTH_LOGIN_RATE_LIMIT_ENABLED!=="false"}function Z(){return process.env.AUTH_TRUST_PROXY_HEADERS==="true"}function f(e,t){return(e.headers.get(t)||"").trim()}function ee(e){let t=f(e,"user-agent").toLowerCase().slice(0,64),r=f(e,"accept-language").toLowerCase().slice(0,32),n=f(e,"sec-ch-ua").toLowerCase().slice(0,64),o=[t,r,n].filter(Boolean).join("|");return o?`fp:${o}`.slice(0,128):"unknown"}function C(){return{windowMs:a(Number(process.env.AUTH_LOGIN_RATE_LIMIT_WINDOW_MS||"900000"),9e5),blockMs:a(Number(process.env.AUTH_LOGIN_RATE_LIMIT_BLOCK_MS||"900000"),9e5),maxAttemptsByIpAndAccount:a(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_ATTEMPTS||"8"),8),maxAttemptsByAccount:a(Number(process.env.AUTH_LOGIN_RATE_LIMIT_ACCOUNT_MAX_ATTEMPTS||"20"),20),maxKeys:a(Number(process.env.AUTH_LOGIN_RATE_LIMIT_MAX_KEYS||"10000"),1e4)}}function te(e){if(Z()){let n=f(e,"x-forwarded-for").split(",")[0]?.trim();if(n)return n;let o=f(e,"x-real-ip");if(o)return o}let t=m(e);return t||ee(e)}function b(e){let t=e.trim().toLowerCase();return t?t.slice(0,128):"unknown"}function Ne(e,t){let r=b(t);return{pairKey:`${b(te(e))}:${r}`,accountKey:r!=="unknown"?r:null}}function _e(e){let t=C(),r=g();for(let n of["ip","account"]){typeof r.sweepExpired=="function"&&r.sweepExpired(n,e,t.windowMs);let o=r.size(n);o>t.maxKeys&&r.evictOldest(n,o-t.maxKeys)}}function Oe(e,t){if(!R())return 0;let r=C();if(!T(e.pairKey,e.accountKey,r))return 0;let o=g(),i=o.get("ip",e.pairKey),s=e.accountKey?o.get("account",e.accountKey):null,u=i&&i.blockedUntil>t?i.blockedUntil-t:0,k=s&&s.blockedUntil>t?s.blockedUntil-t:0;return Math.max(u,k)}function Pe(e,t){if(!R())return;let r=C();h(e.pairKey,e.accountKey,r)}function be(e){R()&&E(e.pairKey,e.accountKey)}export{a,ce as b,ae as c,le as d,fe as e,pe as f,me as g,de as h,ge as i,xe as j,K as k,A as l,D as m,ne as n,oe as o,ie as p,L as q,se as r,d as s,Ce as t,y as u,Te as v,he as w,Q as x,Ee as y,te as z,Ne as A,_e as B,Oe as C,Pe as D,be as E};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getRuntimeEnvKeys as i}from"@spring-systems/core/config";import{NextResponse as s}from"next/server.js";async function p(){let e=(process.env.NODE_ENV||"")==="production"||(process.env.TARGET_ENV||"")==="prod",n=(process.env.RUNTIME_ENV_PUBLIC_IN_PROD||"").toLowerCase()==="true";if(e&&!n)return s.json({error:"Not found"},{status:404});let t=Object.fromEntries(i().map(r=>[r,process.env[r]]).filter(([,r])=>typeof r=="string"));return s.json(t,{status:200,headers:{"Cache-Control":"no-store, no-cache, must-revalidate, proxy-revalidate",Pragma:"no-cache",Expires:"0"}})}import{getRuntimeEnvKeys as c}from"@spring-systems/core/config";import{jsx as m}from"react/jsx-runtime";function u(e){return JSON.stringify(e).replace(/</g,"\\u003C").replace(/>/g,"\\u003E").replace(/&/g,"\\u0026").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")}function a({nonce:e}){let n={};for(let r of c()){let o=process.env[r];typeof o=="string"&&(n[r]=o)}let t=`window.__RUNTIME_ENV__ = ${u(n)};`;return m("script",{id:"runtime-env",nonce:e,suppressHydrationWarning:!0,dangerouslySetInnerHTML:{__html:t}})}export{p as a,a as b};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import o from"next/dynamic";import p from"next/image";import u from"next/link";import{useParams as m,usePathname as i,useRouter as c,useSearchParams as d}from"next/navigation";var t=!1;function N(){return{usePathname:i,useSearchParams:()=>d()??new URLSearchParams,useParams:()=>m()??{},useRouter:c}}function r(e){let a=e?.defaultDynamicSsr??!0;return e?.defaultDynamicSsr===void 0&&!t&&process.env.NODE_ENV!=="production"&&(t=!0,console.warn("[server] createNextUIAdapter() defaults dynamic imports to ssr:true. For client-only apps use createClientOnlyNextUIAdapter() or pass { defaultDynamicSsr: false }.")),{Image:p,Link:u,dynamic:(s,n)=>o(s,{ssr:n?.ssr??a})}}function P(){return r({defaultDynamicSsr:!1})}function I(){return r({defaultDynamicSsr:!0})}export{N as a,r as b,P as c,I as d};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{A as J,B as fe,C as ge,D as me,E as ye,a as Y,b as ne,c as V,d as oe,e as se,f as ae,g as G,h as K,i as re,j as ie,k as ce,n as le,o as v,p as he,r as ue,t as P,v as pe,w as R,y as de}from"./chunk-M23YQYLU.js";import{getFrameworkConfig as L}from"@spring-systems/core/config";import{logInfo as Ce,logWarn as Pe}from"@spring-systems/core/logger";import{NextResponse as C}from"next/server.js";var W;function Ee(){if(W!==void 0)return W;let e=(process.env.API_URL||"").trim();if(!e)throw new Error("API_URL environment variable is not set");try{new URL(e)}catch{throw new Error(`API_URL is not a valid URL: "${e}"`)}return W=e,e}function Re(e,n){let t=process.env[e];if(!t)return n;let l=Number(t);if(!Number.isFinite(l)||l<=0)throw new Error(`${e} must be a positive number, got: "${t}"`);return l}var Ue=Re("API_PROXY_NON_MULTIPART_MAX_BYTES",10485760),ke=Re("API_PROXY_MULTIPART_MAX_BYTES",52428800),be=new Set([400,401,403,404,429,502]),Be=process.env.AUTH_DEBUG==="true";function He(){return new Set(L().auth.publicAuthPaths)}function ve(){return L().proxy.safeHashPattern}function Ie(){return L().proxy.maxProxyPathLength}function Ne(){return de()}function Oe(e){return e?Y(ke,52428800):Y(Ue,10485760)}function I(e,n){Be&&Ce(`AuthProxy.${e}`,n)}function $e(e){let n=e.trim();if(!n)return"";let t=ve();return!t||!t.test(n)?"":n}function u(e,n,t,l){let h=new Headers(l);return R(h,e,P(e)),C.json({error:n},{status:t,headers:h})}async function je(e,n){let{path:t}=await n.params;return E(e,t,"GET")}async function Ye(e,n){let{path:t}=await n.params;return E(e,t,"POST")}async function Ve(e,n){let{path:t}=await n.params;return E(e,t,"PUT")}async function Ge(e,n){let{path:t}=await n.params;return E(e,t,"DELETE")}async function Ke(e,n){let{path:t}=await n.params;return E(e,t,"PATCH")}async function E(e,n,t){let l=n.join("/");try{fe(Date.now());let h=Ne();if(h)return u(e,h,500);if(!n.every(o=>oe(o)))return u(e,"Invalid path",400);if(l.length>Ie())return u(e,"Path too long",414);let w=Ee().replace(/\/+$/,"");if(!w)return u(e,"API_URL is not configured",500);let U=new URL(w).pathname,k=se(U),N=ae(l),O=l;k&&N&&k.toLowerCase()===N.toLowerCase()&&(O=l.replace(new RegExp(`^${N}/?`,"i"),""));let Le=`${w}/${O.replace(/^\/+/,"")}`,r=V(O),T=L().auth,Q=(T.loginPath??"auth/login").toLowerCase(),Z=(T.logoutPath??"auth/logout").toLowerCase(),b=(T.infoPath??"auth/info").toLowerCase(),$=r===Q,D=r===Z,we=r===Q||r===Z||r===b,Te=new URL(e.url),g=new URL(Le);Te.searchParams.forEach((o,c)=>{g.searchParams.has(c)||g.searchParams.set(c,o)});let p=new Headers,xe=G(e.headers);e.headers.forEach((o,c)=>{let a=c.toLowerCase();a==="host"||a==="content-length"||a==="cookie"||a==="authorization"||K(a,xe)||p.set(c,o)}),p.has("X-Requested-With")||p.set("X-Requested-With","XMLHttpRequest");let x=he(e),q=(e.headers.get("authorization")||"").trim(),M=/^Bearer\s+\S+$/i.test(q);if(I("incoming-auth",{path:r,method:t,hasSessionToken:!!x,hasBearerAuthorization:M,host:e.nextUrl.host,protocol:e.nextUrl.protocol}),r===b&&!x&&!M){let o=new C(null,{status:204});return R(o.headers,e,P(e)),o.headers.set("Cache-Control","no-store, no-cache, must-revalidate, proxy-revalidate"),o.headers.set("Pragma","no-cache"),o.headers.set("Expires","0"),v(o,e),I("short-circuit-auth-info-no-token",{host:e.nextUrl.host,protocol:e.nextUrl.protocol}),o}if(pe(e,t,r))return Pe("ApiProxy.CSRF",`Blocked ${t} ${r} \u2014 origin: ${e.headers.get("origin")??"none"}, referer: ${e.headers.get("referer")??"none"}, sec-fetch-site: ${e.headers.get("sec-fetch-site")??"none"}`),u(e,"Forbidden",403);x&&!He().has(r)?p.set("Authorization",`Bearer ${x}`):M&&p.set("Authorization",q);let Ae=e.headers.get("x-hash")||"",A=$e(Ae);A&&(p.set("X-hash",A),p.set("Hash",A),g.searchParams.has("hash")||g.searchParams.set("hash",A),g.searchParams.has("Hash")||g.searchParams.set("Hash",A));let y,d=null;if(t!=="GET"&&t!=="HEAD"){let o=e.headers.get("content-type")||"",c=o.includes("multipart/form-data"),a=Oe(c),m=ne(e.headers.get("content-length"));if(m!==null&&m>a)return u(e,"Payload too large",413);if(c){if(m===null)return u(e,"Content-Length required for multipart payload",411);y=e.body?re(e.body,a):e.body}else{if(y=await e.arrayBuffer(),y.byteLength>a)return u(e,"Payload too large",413);if($&&o.includes("application/json"))try{let B=new TextDecoder().decode(y),H=JSON.parse(B),S=T.loginUsernameField??"username";d=J(e,String(H[S]||""));let j=ge(d,Date.now());if(j>0)return Pe("ApiProxy.RateLimit",`Login rate-limited for key ${d.pairKey} \u2014 retry after ${Math.ceil(j/1e3)}s`),u(e,"Too many login attempts. Try again later.",429,{"Retry-After":String(Math.ceil(j/1e3))})}catch{d=J(e,"")}}}let ee={method:t,headers:p,body:y,redirect:"manual",signal:AbortSignal.timeout(L().api?.timeoutMs??3e4)};y instanceof ReadableStream&&(ee.duplex="half");let s=await fetch(g.toString(),ee),i=new Headers,_e=G(s.headers);s.headers.forEach((o,c)=>{let a=c.toLowerCase();a==="content-encoding"||a==="content-length"||K(a,_e)||i.set(c,o)});let _=null,z="",F=s.status;if($&&(s.headers.get("content-type")||"").toLowerCase().includes("application/json"))try{let c=await s.text(),a=JSON.parse(c),m=T.tokenResponseFields??{accessToken:"access_token",refreshToken:"refresh_token"},B=a[m.accessToken],H=typeof B=="string"?B.trim():"";H&&(z=H);let S={...a};delete S[m.accessToken],delete S[m.refreshToken],_=JSON.stringify(S),i.set("content-type","application/json; charset=utf-8"),i.delete("content-length")}catch{_=JSON.stringify({error:"Invalid response from authentication service"}),F=502,i.set("content-type","application/json; charset=utf-8"),i.delete("content-length")}let Se=P(e);R(i,e,Se),we?(i.set("Cache-Control","no-store, no-cache, must-revalidate, proxy-revalidate"),i.set("Pragma","no-cache"),i.set("Expires","0")):i.has("Cache-Control")||i.set("Cache-Control","private, no-store"),D&&s.status>=200&&s.status<300&&!ce(e)&&i.set("Clear-Site-Data",'"cache", "storage"');let te=await ue(s);_||(_=s.body);let X=new C(_,{status:F,headers:i});return z&&s.status>=200&&s.status<300?(le(X,e,z),d&&ye(d)):$&&d&&be.has(F)?me(d,Date.now()):(D||s.status===401||r===b&&s.status===403||te)&&(I("clear-session-cookie",{path:r,method:t,status:s.status,reason:D?"logout":s.status===401?"status_401":r===b&&s.status===403?"auth_info_403":te?"forbidden_session_text":"unknown",hadSessionToken:!!x}),v(X,e)),X}catch(h){if(ie(h))return u(e,"Payload too large",413);let w=process.env.NODE_ENV==="development",f=C.json({error:"Proxy request failed",...w?{details:h instanceof Error?h.message:String(h)}:{}},{status:500}),U=V(l).replace(/^v\d+(?:\.\d+)?\/?/i,""),k=(L().auth.logoutPath??"auth/logout").toLowerCase();return R(f.headers,e,P(e)),U===k&&(v(f,e),f.headers.set("Cache-Control","no-store, no-cache, must-revalidate, proxy-revalidate"),f.headers.set("Pragma","no-cache"),f.headers.set("Expires","0"),I("clear-session-cookie",{path:U,method:t,status:500,reason:"logout_proxy_failure"})),f}}async function Je(e){let n=new Headers,t=P(e);return R(n,e,t),new C(null,{status:204,headers:n})}export{je as a,Ye as b,Ve as c,Ge as d,Ke as e,Je as f};
|
package/dist/client.js
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import {
|
|
3
|
-
createClientOnlyNextUIAdapter,
|
|
4
|
-
createNextRouteAdapter
|
|
5
|
-
} from "./chunk-CP33WQ5Q.js";
|
|
6
|
-
|
|
7
|
-
// src/client.ts
|
|
8
|
-
var SPRING_SERVER_VERSION = true ? "0.8.6" : "0.0.0-dev";
|
|
9
|
-
export {
|
|
10
|
-
SPRING_SERVER_VERSION,
|
|
11
|
-
createClientOnlyNextUIAdapter,
|
|
12
|
-
createNextRouteAdapter
|
|
13
|
-
};
|
|
14
|
-
//# sourceMappingURL=client.js.map
|
|
1
|
+
"use client";import{a as e,c as t}from"./chunk-UDRRRHFI.js";var _="0.8.9";export{_ as SPRING_SERVER_VERSION,t as createClientOnlyNextUIAdapter,e as createNextRouteAdapter};
|
package/dist/handlers/index.js
CHANGED
|
@@ -1,48 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
applyCorsHeaders,
|
|
3
|
-
cleanupExpiredLoginLimits,
|
|
4
|
-
clearLoginAttemptState,
|
|
5
|
-
clearSessionCookie,
|
|
6
|
-
getClientAddress,
|
|
7
|
-
getLoginRateLimitKeys,
|
|
8
|
-
getRateLimitRetryAfterMs,
|
|
9
|
-
getSessionToken,
|
|
10
|
-
hasCsrfExemptPaths,
|
|
11
|
-
isInternalIpAccess,
|
|
12
|
-
isLocalHostRequest,
|
|
13
|
-
isSessionExpiredCode,
|
|
14
|
-
normalizeOrigin,
|
|
15
|
-
registerFailedLoginAttempt,
|
|
16
|
-
resolveAllowedOrigin,
|
|
17
|
-
resolveProdSecurityConfigError,
|
|
18
|
-
resolveSessionCookieName,
|
|
19
|
-
setSessionCookie,
|
|
20
|
-
shouldClearSessionFromForbidden,
|
|
21
|
-
shouldRejectByCsrfProtection,
|
|
22
|
-
useSecureCookies
|
|
23
|
-
} from "../chunk-PZWKMIA4.js";
|
|
24
|
-
import "../chunk-7IUSTA5W.js";
|
|
25
|
-
export {
|
|
26
|
-
applyCorsHeaders,
|
|
27
|
-
cleanupExpiredLoginLimits,
|
|
28
|
-
clearLoginAttemptState,
|
|
29
|
-
clearSessionCookie,
|
|
30
|
-
getClientAddress,
|
|
31
|
-
getLoginRateLimitKeys,
|
|
32
|
-
getRateLimitRetryAfterMs,
|
|
33
|
-
getSessionToken,
|
|
34
|
-
hasCsrfExemptPaths,
|
|
35
|
-
isInternalIpAccess,
|
|
36
|
-
isLocalHostRequest,
|
|
37
|
-
isSessionExpiredCode,
|
|
38
|
-
normalizeOrigin,
|
|
39
|
-
registerFailedLoginAttempt,
|
|
40
|
-
resolveAllowedOrigin,
|
|
41
|
-
resolveProdSecurityConfigError,
|
|
42
|
-
resolveSessionCookieName,
|
|
43
|
-
setSessionCookie,
|
|
44
|
-
shouldClearSessionFromForbidden,
|
|
45
|
-
shouldRejectByCsrfProtection,
|
|
46
|
-
useSecureCookies
|
|
47
|
-
};
|
|
48
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import{A as q,B as r,C as s,D as t,E as u,k as a,l as b,m as c,n as d,o as e,p as f,q as g,r as h,s as i,t as j,u as k,v as l,w as m,x as n,y as o,z as p}from"../chunk-M23YQYLU.js";import"../chunk-E4AX7MID.js";export{m as applyCorsHeaders,r as cleanupExpiredLoginLimits,u as clearLoginAttemptState,e as clearSessionCookie,p as getClientAddress,q as getLoginRateLimitKeys,s as getRateLimitRetryAfterMs,f as getSessionToken,n as hasCsrfExemptPaths,j as isInternalIpAccess,a as isLocalHostRequest,g as isSessionExpiredCode,i as normalizeOrigin,t as registerFailedLoginAttempt,k as resolveAllowedOrigin,o as resolveProdSecurityConfigError,c as resolveSessionCookieName,d as setSessionCookie,h as shouldClearSessionFromForbidden,l as shouldRejectByCsrfProtection,b as useSecureCookies};
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
proxy,
|
|
3
|
-
proxyConfig
|
|
4
|
-
} from "./chunk-4SUIIQDW.js";
|
|
5
|
-
import {
|
|
6
|
-
DELETE,
|
|
7
|
-
GET,
|
|
8
|
-
OPTIONS,
|
|
9
|
-
PATCH,
|
|
10
|
-
POST,
|
|
11
|
-
PUT
|
|
12
|
-
} from "./chunk-NFJ25NQQ.js";
|
|
13
|
-
import "./chunk-PZWKMIA4.js";
|
|
14
|
-
import {
|
|
15
|
-
GET as GET2,
|
|
16
|
-
RuntimeEnvScript
|
|
17
|
-
} from "./chunk-YV6DZVPI.js";
|
|
18
|
-
import "./chunk-7IUSTA5W.js";
|
|
19
|
-
import {
|
|
20
|
-
BASE_SECURITY_HEADERS,
|
|
21
|
-
BASE_SECURITY_HEADER_VALUES,
|
|
22
|
-
PERMISSIONS_POLICY_VALUE
|
|
23
|
-
} from "./chunk-KA7RJCWA.js";
|
|
24
|
-
|
|
25
|
-
// src/index.ts
|
|
26
|
-
import "server-only";
|
|
27
|
-
var SPRING_SERVER_VERSION = true ? "0.8.6" : "0.0.0-dev";
|
|
28
|
-
export {
|
|
29
|
-
BASE_SECURITY_HEADERS,
|
|
30
|
-
BASE_SECURITY_HEADER_VALUES,
|
|
31
|
-
DELETE,
|
|
32
|
-
GET,
|
|
33
|
-
OPTIONS,
|
|
34
|
-
PATCH,
|
|
35
|
-
PERMISSIONS_POLICY_VALUE,
|
|
36
|
-
POST,
|
|
37
|
-
PUT,
|
|
38
|
-
RuntimeEnvScript,
|
|
39
|
-
SPRING_SERVER_VERSION,
|
|
40
|
-
proxy,
|
|
41
|
-
proxyConfig,
|
|
42
|
-
GET2 as runtimeEnvGET
|
|
43
|
-
};
|
|
44
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
import{a as I,b as p}from"./chunk-KEEIJ5EV.js";import{a as E,b as _,c as S,d as r,e as o,f as t}from"./chunk-VB7DEUGT.js";import"./chunk-M23YQYLU.js";import{a as O,b as T}from"./chunk-TVAJDSYB.js";import"./chunk-E4AX7MID.js";import{a as e,b as R,c as n}from"./chunk-FXUI75TW.js";import"server-only";var f="0.8.9";export{n as BASE_SECURITY_HEADERS,R as BASE_SECURITY_HEADER_VALUES,r as DELETE,E as GET,t as OPTIONS,o as PATCH,e as PERMISSIONS_POLICY_VALUE,_ as POST,S as PUT,T as RuntimeEnvScript,f as SPRING_SERVER_VERSION,I as proxy,p as proxyConfig,O as runtimeEnvGET};
|
package/dist/next-adapters.js
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import {
|
|
3
|
-
createClientOnlyNextUIAdapter,
|
|
4
|
-
createNextRouteAdapter,
|
|
5
|
-
createNextUIAdapter,
|
|
6
|
-
createSsrNextUIAdapter
|
|
7
|
-
} from "./chunk-CP33WQ5Q.js";
|
|
8
|
-
export {
|
|
9
|
-
createClientOnlyNextUIAdapter,
|
|
10
|
-
createNextRouteAdapter,
|
|
11
|
-
createNextUIAdapter,
|
|
12
|
-
createSsrNextUIAdapter
|
|
13
|
-
};
|
|
14
|
-
//# sourceMappingURL=next-adapters.js.map
|
|
1
|
+
"use client";import{a,b,c,d}from"./chunk-UDRRRHFI.js";export{c as createClientOnlyNextUIAdapter,a as createNextRouteAdapter,b as createNextUIAdapter,d as createSsrNextUIAdapter};
|
package/dist/proxy-middleware.js
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
proxy,
|
|
3
|
-
proxyConfig
|
|
4
|
-
} from "./chunk-4SUIIQDW.js";
|
|
5
|
-
import "./chunk-KA7RJCWA.js";
|
|
6
|
-
export {
|
|
7
|
-
proxy,
|
|
8
|
-
proxyConfig
|
|
9
|
-
};
|
|
10
|
-
//# sourceMappingURL=proxy-middleware.js.map
|
|
1
|
+
import{a,b}from"./chunk-KEEIJ5EV.js";import"./chunk-FXUI75TW.js";export{a as proxy,b as proxyConfig};
|
package/dist/rate-limiter.js
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
checkRateLimit,
|
|
3
|
-
clearRateLimitEntries,
|
|
4
|
-
getRateLimiterAdapter,
|
|
5
|
-
recordFailedAttempt,
|
|
6
|
-
setRateLimiterAdapter
|
|
7
|
-
} from "./chunk-7IUSTA5W.js";
|
|
8
|
-
export {
|
|
9
|
-
checkRateLimit,
|
|
10
|
-
clearRateLimitEntries,
|
|
11
|
-
getRateLimiterAdapter,
|
|
12
|
-
recordFailedAttempt,
|
|
13
|
-
setRateLimiterAdapter
|
|
14
|
-
};
|
|
15
|
-
//# sourceMappingURL=rate-limiter.js.map
|
|
1
|
+
import{a,b,c,d,e}from"./chunk-E4AX7MID.js";export{c as checkRateLimit,e as clearRateLimitEntries,b as getRateLimiterAdapter,d as recordFailedAttempt,a as setRateLimiterAdapter};
|
package/dist/runtime-env.js
CHANGED
package/dist/security-headers.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
BASE_SECURITY_HEADERS,
|
|
3
|
-
BASE_SECURITY_HEADER_VALUES,
|
|
4
|
-
PERMISSIONS_POLICY_VALUE
|
|
5
|
-
} from "./chunk-KA7RJCWA.js";
|
|
6
|
-
export {
|
|
7
|
-
BASE_SECURITY_HEADERS,
|
|
8
|
-
BASE_SECURITY_HEADER_VALUES,
|
|
9
|
-
PERMISSIONS_POLICY_VALUE
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=security-headers.js.map
|
|
1
|
+
import{a,b,c}from"./chunk-FXUI75TW.js";export{c as BASE_SECURITY_HEADERS,b as BASE_SECURITY_HEADER_VALUES,a as PERMISSIONS_POLICY_VALUE};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spring-systems/server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"description": "Next.js server-only code for the Spring Systems framework (proxy, API routes, runtime env)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
"tsup": "^8.5.1",
|
|
103
103
|
"typescript": "^5.9.3",
|
|
104
104
|
"vitest": "^4.0.18",
|
|
105
|
-
"@spring-systems/core": "^0.8.
|
|
106
|
-
"@spring-systems/ui": "^0.8.
|
|
105
|
+
"@spring-systems/core": "^0.8.9",
|
|
106
|
+
"@spring-systems/ui": "^0.8.9"
|
|
107
107
|
},
|
|
108
108
|
"scripts": {
|
|
109
109
|
"build": "tsup",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/chunk-4SUIIQDW.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BASE_SECURITY_HEADER_VALUES
|
|
3
|
-
} from "./chunk-KA7RJCWA.js";
|
|
4
|
-
|
|
5
|
-
// src/proxy-middleware.ts
|
|
6
|
-
import { getFrameworkConfig } from "@spring-systems/core/config";
|
|
7
|
-
import { NextResponse } from "next/server.js";
|
|
8
|
-
function toOriginSource(value) {
|
|
9
|
-
const raw = value.trim();
|
|
10
|
-
if (!raw) return [];
|
|
11
|
-
try {
|
|
12
|
-
const parsed = new URL(raw);
|
|
13
|
-
const out = [parsed.origin];
|
|
14
|
-
if (parsed.protocol === "https:") {
|
|
15
|
-
out.push(`wss://${parsed.host}`);
|
|
16
|
-
} else if (parsed.protocol === "http:") {
|
|
17
|
-
out.push(`ws://${parsed.host}`);
|
|
18
|
-
}
|
|
19
|
-
return out;
|
|
20
|
-
} catch {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
var BLOCKED_CSP_SCHEMES = /* @__PURE__ */ new Set(["javascript:", "data:", "blob:", "vbscript:", "filesystem:"]);
|
|
25
|
-
function parseCspSource(token) {
|
|
26
|
-
const value = token.trim();
|
|
27
|
-
if (!value) return null;
|
|
28
|
-
if (/[\s;,\r\n]/.test(value)) return null;
|
|
29
|
-
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {
|
|
30
|
-
const lower = value.toLowerCase();
|
|
31
|
-
if (BLOCKED_CSP_SCHEMES.has(lower)) return null;
|
|
32
|
-
return lower;
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const parsed = new URL(value);
|
|
36
|
-
if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) return null;
|
|
37
|
-
if (parsed.username || parsed.password) return null;
|
|
38
|
-
if (parsed.pathname !== "/" || parsed.search || parsed.hash) return null;
|
|
39
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function parseReportUri(value) {
|
|
45
|
-
const trimmed = value.trim();
|
|
46
|
-
if (!trimmed) return null;
|
|
47
|
-
if (/[\s;,\r\n"]/.test(trimmed)) return null;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(trimmed);
|
|
50
|
-
if (!["http:", "https:"].includes(parsed.protocol)) return null;
|
|
51
|
-
if (parsed.username || parsed.password) return null;
|
|
52
|
-
return parsed.toString();
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function parseCspList(value) {
|
|
58
|
-
const parsed = value.split(",").map((s) => parseCspSource(s)).filter((s) => !!s);
|
|
59
|
-
return [...new Set(parsed)];
|
|
60
|
-
}
|
|
61
|
-
function proxy(req) {
|
|
62
|
-
const url = req.nextUrl.clone();
|
|
63
|
-
const TARGET_ENV = process.env.TARGET_ENV || "test";
|
|
64
|
-
const isProdRuntime = TARGET_ENV === "prod" || process.env.NODE_ENV === "production";
|
|
65
|
-
const isTargetProd = TARGET_ENV === "prod";
|
|
66
|
-
const isLocalHost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
67
|
-
const nonce = crypto.randomUUID().replace(/-/g, "");
|
|
68
|
-
const denyStyleAttr = process.env.CSP_DENY_STYLE_ATTR === "true";
|
|
69
|
-
const allowUnsafeInlineStyle = !isTargetProd;
|
|
70
|
-
const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || "");
|
|
71
|
-
const apiConnectSources = toOriginSource(process.env.API_URL || "");
|
|
72
|
-
const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || "");
|
|
73
|
-
const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || "");
|
|
74
|
-
const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || "");
|
|
75
|
-
const cspConfig = getFrameworkConfig().csp;
|
|
76
|
-
const validatedConnectSources = cspConfig.thirdPartyConnectSources.map((s) => parseCspSource(s)).filter((s) => !!s);
|
|
77
|
-
const validatedImgSources = cspConfig.thirdPartyImageSources.map((s) => parseCspSource(s)).filter((s) => !!s);
|
|
78
|
-
const validatedScriptSources = cspConfig.thirdPartyScriptSources.map((s) => parseCspSource(s)).filter((s) => !!s);
|
|
79
|
-
const validatedFrameSources = cspConfig.thirdPartyFrameSources.map((s) => parseCspSource(s)).filter((s) => !!s);
|
|
80
|
-
const connectSrc = [
|
|
81
|
-
"'self'",
|
|
82
|
-
...validatedConnectSources,
|
|
83
|
-
...apiConnectSources,
|
|
84
|
-
...connectHostsEnv,
|
|
85
|
-
...!isProdRuntime ? ["http://localhost:*", "http://127.0.0.1:*", "ws://localhost:*", "ws://127.0.0.1:*"] : []
|
|
86
|
-
].join(" ");
|
|
87
|
-
const imgSrc = ["'self'", "data:", "blob:", ...validatedImgSources, ...imgHostsEnv].join(" ");
|
|
88
|
-
const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(" ");
|
|
89
|
-
const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(" ");
|
|
90
|
-
const reportUri = parseReportUri(process.env.CSP_REPORT_URI || "");
|
|
91
|
-
const cspDirectives = [
|
|
92
|
-
"default-src 'self'",
|
|
93
|
-
"base-uri 'self'",
|
|
94
|
-
"object-src 'none'",
|
|
95
|
-
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,
|
|
96
|
-
`script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,
|
|
97
|
-
"script-src-attr 'none'",
|
|
98
|
-
allowUnsafeInlineStyle ? "style-src 'self' 'unsafe-inline'" : `style-src 'self' 'nonce-${nonce}'`,
|
|
99
|
-
denyStyleAttr ? "style-src-attr 'none'" : "style-src-attr 'unsafe-inline'",
|
|
100
|
-
`img-src ${imgSrc}`,
|
|
101
|
-
"media-src 'self' blob: data:",
|
|
102
|
-
"manifest-src 'self'",
|
|
103
|
-
`connect-src ${connectSrc}`,
|
|
104
|
-
`frame-src ${frameSrcHosts || "'none'"}`,
|
|
105
|
-
"frame-ancestors 'none'",
|
|
106
|
-
"form-action 'self'",
|
|
107
|
-
...isTargetProd ? ["upgrade-insecure-requests"] : [],
|
|
108
|
-
...reportUri ? [`report-uri ${reportUri}`, "report-to csp-violations"] : []
|
|
109
|
-
];
|
|
110
|
-
const csp = cspDirectives.join("; ");
|
|
111
|
-
const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];
|
|
112
|
-
if (isProdRuntime) {
|
|
113
|
-
for (const prefix of blockedPrefixes) {
|
|
114
|
-
if (url.pathname.startsWith(prefix)) {
|
|
115
|
-
const res2 = new NextResponse("Not Found", { status: 404 });
|
|
116
|
-
return setSecurity(res2, { isLocalHost, isTargetProd, csp, nonce, reportUri });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (url.pathname === "/") {
|
|
121
|
-
const dest = new URL(url.toString());
|
|
122
|
-
if (!isLocalHost && isTargetProd) dest.protocol = "https:";
|
|
123
|
-
dest.pathname = getFrameworkConfig().app.defaultRoute;
|
|
124
|
-
dest.search = "";
|
|
125
|
-
const res2 = makeRedirect(dest, 308);
|
|
126
|
-
return setSecurity(res2, { isLocalHost, isTargetProd, csp, nonce, reportUri });
|
|
127
|
-
}
|
|
128
|
-
const requestHeaders = new Headers(req.headers);
|
|
129
|
-
requestHeaders.set("x-nonce", nonce);
|
|
130
|
-
const res = NextResponse.next({ request: { headers: requestHeaders } });
|
|
131
|
-
return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });
|
|
132
|
-
}
|
|
133
|
-
function setSecurity(res, opts) {
|
|
134
|
-
const { isLocalHost, csp, nonce, reportUri } = opts;
|
|
135
|
-
if (csp) res.headers.set("Content-Security-Policy", csp);
|
|
136
|
-
for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {
|
|
137
|
-
res.headers.set(key, value);
|
|
138
|
-
}
|
|
139
|
-
if (opts.isTargetProd && !isLocalHost)
|
|
140
|
-
res.headers.set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload");
|
|
141
|
-
if (reportUri) res.headers.set("Reporting-Endpoints", `csp-violations="${reportUri}"`);
|
|
142
|
-
if (nonce) res.headers.set("x-nonce", nonce);
|
|
143
|
-
return res;
|
|
144
|
-
}
|
|
145
|
-
function makeRedirect(to, status = 308) {
|
|
146
|
-
const res = new NextResponse(null, { status });
|
|
147
|
-
res.headers.set("Location", typeof to === "string" ? to : to.toString());
|
|
148
|
-
return res;
|
|
149
|
-
}
|
|
150
|
-
var proxyConfig = {
|
|
151
|
-
matcher: ["/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)"]
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
export {
|
|
155
|
-
proxy,
|
|
156
|
-
proxyConfig
|
|
157
|
-
};
|
|
158
|
-
//# sourceMappingURL=chunk-4SUIIQDW.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/proxy-middleware.ts"],"sourcesContent":["import { getFrameworkConfig } from \"@spring-systems/core/config\";\nimport { type NextRequest, NextResponse } from \"next/server.js\";\n\nimport { BASE_SECURITY_HEADER_VALUES } from \"./security-headers\";\n\nfunction toOriginSource(value: string): string[] {\n const raw = value.trim();\n if (!raw) return [];\n\n try {\n const parsed = new URL(raw);\n const out = [parsed.origin];\n\n if (parsed.protocol === \"https:\") {\n out.push(`wss://${parsed.host}`);\n } else if (parsed.protocol === \"http:\") {\n out.push(`ws://${parsed.host}`);\n }\n\n return out;\n } catch {\n return [];\n }\n}\n\nconst BLOCKED_CSP_SCHEMES = new Set([\"javascript:\", \"data:\", \"blob:\", \"vbscript:\", \"filesystem:\"]);\n\nfunction parseCspSource(token: string): string | null {\n const value = token.trim();\n if (!value) return null;\n\n // Prevent header/CSP injection via env values.\n if (/[\\s;,\\r\\n]/.test(value)) return null;\n\n // Allow safe scheme sources (e.g. https:, wss:). Block dangerous schemes\n // that enable XSS or data exfiltration when used in CSP directives.\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:$/.test(value)) {\n const lower = value.toLowerCase();\n if (BLOCKED_CSP_SCHEMES.has(lower)) return null;\n return lower;\n }\n\n try {\n const parsed = new URL(value);\n if (![\"http:\", \"https:\", \"ws:\", \"wss:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n if (parsed.pathname !== \"/\" || parsed.search || parsed.hash) return null;\n return `${parsed.protocol}//${parsed.host}`;\n } catch {\n return null;\n }\n}\n\nfunction parseReportUri(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n // Block header injection characters and double-quotes (used in Reporting-Endpoints structured header).\n if (/[\\s;,\\r\\n\"]/.test(trimmed)) return null;\n try {\n const parsed = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(parsed.protocol)) return null;\n if (parsed.username || parsed.password) return null;\n return parsed.toString();\n } catch {\n return null;\n }\n}\n\nfunction parseCspList(value: string): string[] {\n const parsed = value\n .split(\",\")\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n return [...new Set(parsed)];\n}\n\nexport function proxy(req: NextRequest) {\n const url = req.nextUrl.clone();\n\n const TARGET_ENV = process.env.TARGET_ENV || \"test\";\n const isProdRuntime = TARGET_ENV === \"prod\" || process.env.NODE_ENV === \"production\";\n const isTargetProd = TARGET_ENV === \"prod\";\n\n const isLocalHost = url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\";\n\n const nonce = crypto.randomUUID().replace(/-/g, \"\");\n const denyStyleAttr = process.env.CSP_DENY_STYLE_ATTR === \"true\";\n const allowUnsafeInlineStyle = !isTargetProd;\n\n const connectHostsEnv = parseCspList(process.env.CSP_CONNECT_HOSTS || \"\");\n const apiConnectSources = toOriginSource(process.env.API_URL || \"\");\n const imgHostsEnv = parseCspList(process.env.CSP_IMG_HOSTS || \"\");\n const scriptHostsEnv = parseCspList(process.env.CSP_SCRIPT_HOSTS || \"\");\n const frameHostsEnv = parseCspList(process.env.CSP_FRAME_HOSTS || \"\");\n\n const cspConfig = getFrameworkConfig().csp;\n\n // Validate framework config CSP sources through parseCspSource to prevent injection\n const validatedConnectSources = cspConfig.thirdPartyConnectSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedImgSources = cspConfig.thirdPartyImageSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedScriptSources = cspConfig.thirdPartyScriptSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n const validatedFrameSources = cspConfig.thirdPartyFrameSources\n .map((s) => parseCspSource(s))\n .filter((s): s is string => !!s);\n\n const connectSrc = [\n \"'self'\",\n ...validatedConnectSources,\n ...apiConnectSources,\n ...connectHostsEnv,\n ...(!isProdRuntime ? [\"http://localhost:*\", \"http://127.0.0.1:*\", \"ws://localhost:*\", \"ws://127.0.0.1:*\"] : []),\n ].join(\" \");\n\n const imgSrc = [\"'self'\", \"data:\", \"blob:\", ...validatedImgSources, ...imgHostsEnv].join(\" \");\n\n const scriptSrcHosts = [...validatedScriptSources, ...scriptHostsEnv].join(\" \");\n const frameSrcHosts = [...validatedFrameSources, ...frameHostsEnv].join(\" \");\n\n const reportUri = parseReportUri(process.env.CSP_REPORT_URI || \"\");\n\n const cspDirectives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n `script-src-elem 'self' 'nonce-${nonce}' 'strict-dynamic' ${scriptSrcHosts}`,\n \"script-src-attr 'none'\",\n allowUnsafeInlineStyle ? \"style-src 'self' 'unsafe-inline'\" : `style-src 'self' 'nonce-${nonce}'`,\n denyStyleAttr ? \"style-src-attr 'none'\" : \"style-src-attr 'unsafe-inline'\",\n `img-src ${imgSrc}`,\n \"media-src 'self' blob: data:\",\n \"manifest-src 'self'\",\n `connect-src ${connectSrc}`,\n `frame-src ${frameSrcHosts || \"'none'\"}`,\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n ...(isTargetProd ? [\"upgrade-insecure-requests\"] : []),\n ...(reportUri ? [`report-uri ${reportUri}`, \"report-to csp-violations\"] : []),\n ];\n const csp = cspDirectives.join(\"; \");\n\n const blockedPrefixes = getFrameworkConfig().proxy.blockedPathPrefixesInProd ?? [];\n if (isProdRuntime) {\n for (const prefix of blockedPrefixes) {\n if (url.pathname.startsWith(prefix)) {\n const res = new NextResponse(\"Not Found\", { status: 404 });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n }\n }\n\n if (url.pathname === \"/\") {\n const dest = new URL(url.toString());\n if (!isLocalHost && isTargetProd) dest.protocol = \"https:\";\n dest.pathname = getFrameworkConfig().app.defaultRoute;\n dest.search = \"\";\n\n const res = makeRedirect(dest, 308);\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n }\n\n const requestHeaders = new Headers(req.headers);\n requestHeaders.set(\"x-nonce\", nonce);\n\n const res = NextResponse.next({ request: { headers: requestHeaders } });\n return setSecurity(res, { isLocalHost, isTargetProd, csp, nonce, reportUri });\n}\n\nfunction setSecurity(\n res: NextResponse,\n opts: { isLocalHost: boolean; isTargetProd: boolean; csp?: string; nonce?: string; reportUri?: string | null },\n) {\n const { isLocalHost, csp, nonce, reportUri } = opts;\n if (csp) res.headers.set(\"Content-Security-Policy\", csp);\n for (const [key, value] of Object.entries(BASE_SECURITY_HEADER_VALUES)) {\n res.headers.set(key, value);\n }\n if (opts.isTargetProd && !isLocalHost)\n res.headers.set(\"Strict-Transport-Security\", \"max-age=63072000; includeSubDomains; preload\");\n if (reportUri) res.headers.set(\"Reporting-Endpoints\", `csp-violations=\"${reportUri}\"`);\n if (nonce) res.headers.set(\"x-nonce\", nonce);\n return res;\n}\n\nfunction makeRedirect(to: URL | string, status = 308) {\n const res = new NextResponse(null, { status });\n res.headers.set(\"Location\", typeof to === \"string\" ? to : to.toString());\n return res;\n}\n\nexport const proxyConfig = {\n matcher: [\"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)\"],\n};\n"],"mappings":"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","names":["res"]}
|
package/dist/chunk-7IUSTA5W.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// src/rate-limiter.ts
|
|
2
|
-
var InMemoryRateLimiter = class {
|
|
3
|
-
ipStore = /* @__PURE__ */ new Map();
|
|
4
|
-
accountStore = /* @__PURE__ */ new Map();
|
|
5
|
-
getStore(store) {
|
|
6
|
-
return store === "ip" ? this.ipStore : this.accountStore;
|
|
7
|
-
}
|
|
8
|
-
get(store, key) {
|
|
9
|
-
return this.getStore(store).get(key) ?? null;
|
|
10
|
-
}
|
|
11
|
-
set(store, key, entry) {
|
|
12
|
-
this.getStore(store).set(key, entry);
|
|
13
|
-
}
|
|
14
|
-
delete(store, key) {
|
|
15
|
-
this.getStore(store).delete(key);
|
|
16
|
-
}
|
|
17
|
-
size(store) {
|
|
18
|
-
return this.getStore(store).size;
|
|
19
|
-
}
|
|
20
|
-
evictOldest(store, count) {
|
|
21
|
-
const map = this.getStore(store);
|
|
22
|
-
const entries = [...map.entries()].sort((a, b) => a[1].windowStartedAt - b[1].windowStartedAt);
|
|
23
|
-
for (let i = 0; i < count && i < entries.length; i++) {
|
|
24
|
-
const candidate = entries[i];
|
|
25
|
-
if (!candidate) break;
|
|
26
|
-
map.delete(candidate[0]);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
sweepExpired(store, now, windowMs) {
|
|
30
|
-
const map = this.getStore(store);
|
|
31
|
-
for (const [key, entry] of map.entries()) {
|
|
32
|
-
const windowExpired = now - entry.windowStartedAt > windowMs;
|
|
33
|
-
const noActiveBlock = entry.blockedUntil <= now;
|
|
34
|
-
if (windowExpired && noActiveBlock) {
|
|
35
|
-
map.delete(key);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var adapter = new InMemoryRateLimiter();
|
|
41
|
-
var hasWarnedInMemory = false;
|
|
42
|
-
function setRateLimiterAdapter(custom) {
|
|
43
|
-
adapter = custom;
|
|
44
|
-
}
|
|
45
|
-
function getRateLimiterAdapter() {
|
|
46
|
-
return adapter;
|
|
47
|
-
}
|
|
48
|
-
function checkRateLimit(ipKey, accountKey, policy) {
|
|
49
|
-
if (!hasWarnedInMemory && adapter instanceof InMemoryRateLimiter && process.env.NODE_ENV === "production") {
|
|
50
|
-
hasWarnedInMemory = true;
|
|
51
|
-
console.warn("[server] Rate limiter is using in-memory storage in production. For multi-instance deployments, call setRateLimiterAdapter() with a shared-storage adapter.");
|
|
52
|
-
}
|
|
53
|
-
const now = Date.now();
|
|
54
|
-
const ipResult = checkSingleLimit(adapter, "ip", ipKey, policy.maxAttemptsByIpAndAccount, policy, now);
|
|
55
|
-
if (ipResult) return ipResult;
|
|
56
|
-
if (accountKey) {
|
|
57
|
-
const accountResult = checkSingleLimit(adapter, "account", accountKey, policy.maxAttemptsByAccount, policy, now);
|
|
58
|
-
if (accountResult) return accountResult;
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
function recordFailedAttempt(ipKey, accountKey, policy) {
|
|
63
|
-
const now = Date.now();
|
|
64
|
-
recordAttempt(adapter, "ip", ipKey, policy, now);
|
|
65
|
-
if (accountKey) {
|
|
66
|
-
recordAttempt(adapter, "account", accountKey, policy, now);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function clearRateLimitEntries(ipKey, accountKey) {
|
|
70
|
-
adapter.delete("ip", ipKey);
|
|
71
|
-
if (accountKey) {
|
|
72
|
-
adapter.delete("account", accountKey);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function checkSingleLimit(adap, store, key, maxAttempts, policy, now) {
|
|
76
|
-
const entry = adap.get(store, key);
|
|
77
|
-
if (!entry) return null;
|
|
78
|
-
if (entry.blockedUntil > now) {
|
|
79
|
-
const remainSec = Math.ceil((entry.blockedUntil - now) / 1e3);
|
|
80
|
-
return `Rate limited (${store}). Try again in ${remainSec}s.`;
|
|
81
|
-
}
|
|
82
|
-
if (now - entry.windowStartedAt > policy.windowMs) {
|
|
83
|
-
adap.delete(store, key);
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
if (entry.count >= maxAttempts) {
|
|
87
|
-
entry.blockedUntil = now + policy.blockMs;
|
|
88
|
-
adap.set(store, key, entry);
|
|
89
|
-
return `Too many attempts (${store}). Blocked for ${Math.ceil(policy.blockMs / 1e3)}s.`;
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
function recordAttempt(adap, store, key, policy, now) {
|
|
94
|
-
if (adap.size(store) >= policy.maxKeys) {
|
|
95
|
-
adap.evictOldest(store, Math.floor(policy.maxKeys * 0.1));
|
|
96
|
-
}
|
|
97
|
-
const entry = adap.get(store, key);
|
|
98
|
-
if (!entry || now - entry.windowStartedAt > policy.windowMs) {
|
|
99
|
-
adap.set(store, key, { count: 1, windowStartedAt: now, blockedUntil: 0 });
|
|
100
|
-
} else {
|
|
101
|
-
entry.count++;
|
|
102
|
-
adap.set(store, key, entry);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export {
|
|
107
|
-
setRateLimiterAdapter,
|
|
108
|
-
getRateLimiterAdapter,
|
|
109
|
-
checkRateLimit,
|
|
110
|
-
recordFailedAttempt,
|
|
111
|
-
clearRateLimitEntries
|
|
112
|
-
};
|
|
113
|
-
//# sourceMappingURL=chunk-7IUSTA5W.js.map
|
|
@@ -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":[]}
|