@stacksjs/rpx 0.11.16 → 0.11.18

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/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type {
10
10
  StopDaemonResult,
11
11
  } from './daemon';
12
12
  export type { GetRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './proxy-handler';
13
+ export type { OriginGuard, OriginGuardOptions } from './origin-guard';
13
14
  export type { HostRoutes, PathRoute } from './host-routes';
14
15
  export type { ResolvedStaticRoute, StaticResolution } from './static-files';
15
16
  export type { SniTlsEntry } from './sni';
@@ -105,6 +106,7 @@ export {
105
106
  } from './daemon';
106
107
  export { createProxyFetchHandler, createProxyWebSocketHandler, stripBasePath } from './proxy-handler';
107
108
  export { isWildcardPattern, matchesWildcard, matchHost } from './host-match';
109
+ export { createOriginGuard } from './origin-guard';
108
110
  export {
109
111
  buildHostRoutes,
110
112
  matchHostList,
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
- import{$ as W_,A as z_,Aa as WD,B as I2,Ba as XD,C as F_,Ca as JD,D as w2,Da as QD,E as E2,Ea as ZD,F as j2,Fa as AD,G as H2,Ga as X_,H as k2,Ha as SD,I as T_,Ia as VD,J as I_,K as w_,L as C2,M as x2,N as E_,O as q2,P as j_,Q as f2,R as H_,S as y2,T as U2,U as O2,V as P2,W as L2,X as Y_,Y as $_,Z as c2,_ as G_,a as X,aa as h2,b as N_,ba as u2,c as _2,ca as m2,d as D2,da as v2,e as B2,ea as b2,f as N2,fa as l2,g as K2,ga as a2,h as R2,ha as d2,i as Y2,ia as p2,j as $2,ja as g2,k as G2,ka as i2,l as W2,la as n2,m as X2,ma as t2,n as J2,na as r2,o as Q2,oa as s2,p as Z2,pa as o2,q as A2,qa as e2,r as S2,ra as _D,s as V2,sa as DD,t as M2,ta as BD,u as z2,ua as ND,v as F2,va as KD,w as K_,wa as RD,x as T2,xa as YD,y as b,ya as $D,z as R_,za as GD}from"./chunk-n3etse5z.js";import{Ja as M_,Ka as l,La as kB,Ma as N,Na as CB,Oa as y,Pa as xB,Qa as qB,Ra as fB,Sa as yB,Ta as UB,Ua as OB,Va as PB,Wa as LB,Xa as cB}from"./chunk-rbgb5fyg.js";import{execSync as i_}from"node:child_process";import*as c from"node:http";import*as m_ from"node:http2";import*as v_ from"node:net";import*as T from"node:process";var t=(D,_)=>(B)=>`\x1B[${D}m${B}\x1B[${_}m`,q={bold:t(1,22),dim:t(2,22),green:t(32,39),cyan:t(36,39)};import*as y_ from"node:fs";import*as U_ from"node:path";import*as f from"node:process";function O_(D,_){let K=(_&&_!=="/"?`${D}${_}`:D).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return K.length>0?K:"rpx"}async function r(D){if(D.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let _=D.verbose??!1,B=D.registryDir,K=new Set,R=D.proxies.map((V)=>{let I=V.id??O_(V.to,V.path);if(!$_(I))throw Error(`invalid registry id "${I}" derived from to="${V.to}"`);if(K.has(I))throw Error(`duplicate registry id "${I}" — set an explicit \`id\` on one of the proxies`);return K.add(I),{...V,id:I}}),G=new Date().toISOString();for(let V of R)await G_({id:V.id,from:V.from,to:V.to,path:V.path,pid:D.persistent?void 0:f.pid,cwd:f.cwd(),createdAt:G,cleanUrls:V.cleanUrls,changeOrigin:V.changeOrigin,pathRewrites:V.pathRewrites,static:V.static},B,_);let $=await X_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let V of R){let I=V.static?`static ${typeof V.static==="string"?V.static:V.static.dir}`:V.from;X.success(`https://${V.to} → ${I}`)}if(X.info(`(via rpx daemon pid=${$.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let M=!1,W=B??Y_(),Q=R.map((V)=>V.id),w=async()=>{if(M)return;M=!0;for(let V of Q)await W_(V,B,_).catch((I)=>{N("runner",`removeEntry(${V}) failed: ${I}`,_)})},E=(V)=>{N("runner",`received ${V}, unregistering ${Q.length} entries`,_),w().finally(()=>f.exit(0))};f.once("SIGINT",E),f.once("SIGTERM",E),f.once("exit",()=>{if(M)return;for(let V of Q)try{y_.unlinkSync(U_.join(W,`${V}.json`))}catch{}}),await new Promise(()=>{})}import{exec as a_}from"node:child_process";import P from"node:fs";import L_ from"node:os";import Q_ from"node:path";import*as a from"node:process";import{promisify as d_}from"node:util";var s=d_(a_);function P_(D){let _=D.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var C=a.platform==="win32"?Q_.join(a.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",J_=!1;async function o(D){if(a.platform==="win32")throw Error("Administrator privileges required on Windows");let _=l(),B=D.replace(/'/g,"'\\''");try{if(_){let{stdout:K}=await s(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return J_=!0,K}if(J_)try{let{stdout:K}=await s(`sudo -n sh -c '${B}'`);return K}catch(K){N("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:K}=await s(`sudo -n sh -c '${B}'`);return J_=!0,K}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(K){throw Error(`Failed to execute sudo command: ${K.message}`)}}async function m(D,_){let B=D.filter((R)=>!P_(R)),K=D.filter((R)=>P_(R));if(K.length>0)N("hosts",`Skipping /etc/hosts for loopback dev names: ${K.join(", ")}`,_);if(B.length===0)return;N("hosts",`Adding hosts: ${B.join(", ")}`,_),N("hosts",`Using hosts file at: ${C}`,_);try{let R;try{R=await P.promises.readFile(C,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{R=await o(`cat "${C}"`)}catch(W){throw console.log(" Could not read hosts file — skipping hosts setup"),N("hosts",`sudo read also failed: ${W}`,_),Error(`Cannot read hosts file: ${W}`)}}let G=B.filter((W)=>{let Q=`127.0.0.1 ${W}`,w=`::1 ${W}`;return!R.includes(Q)&&!R.includes(w)});if(G.length===0){N("hosts","All hosts already exist in hosts file",_);return}let $=G.map((W)=>`
1
+ import{$ as Q_,A as j_,Aa as ZB,B as H2,Ba as VB,C as w_,Ca as WB,D as k2,Da as AB,E as C2,Ea as SB,F as x2,Fa as zB,G as f2,Ga as Z_,H as q2,Ha as MB,I as E_,Ia as TB,J as H_,K as k_,L as S_,M as y2,N as C_,O as U2,P as x_,Q as O2,R as f_,S as P2,T as L2,U as c2,V as h2,W as u2,X as G_,Y as X_,Z as m2,_ as J_,a as J,aa as v2,b as N_,ba as b2,c as R2,ca as l2,d as N2,da as a2,e as Y2,ea as d2,f as $2,fa as g2,g as G2,ga as p2,h as X2,ha as i2,i as J2,ia as n2,j as Q2,ja as t2,k as Z2,ka as r2,l as V2,la as s2,m as W2,ma as o2,n as A2,na as e2,o as S2,oa as _B,p as z2,pa as BB,q as M2,qa as DB,r as T2,ra as KB,s as F2,sa as RB,t as I2,ta as NB,u as j2,ua as YB,v as w2,va as $B,w as Y_,wa as GB,x as E2,xa as XB,y as l,ya as JB,z as $_,za as QB}from"./chunk-s4tdtq37.js";import{Ja as I_,Ka as a,La as OD,Ma as K,Na as PD,Oa as y,Pa as LD,Qa as cD,Ra as hD,Sa as uD,Ta as mD,Ua as vD,Va as bD,Wa as lD,Xa as aD}from"./chunk-rbgb5fyg.js";import{execSync as s_}from"node:child_process";import*as u from"node:http";import*as l_ from"node:http2";import*as a_ from"node:net";import*as j from"node:process";var s=(B,_)=>(D)=>`\x1B[${B}m${D}\x1B[${_}m`,x={bold:s(1,22),dim:s(2,22),green:s(32,39),cyan:s(36,39)};import*as P_ from"node:fs";import*as L_ from"node:path";import*as q from"node:process";function c_(B,_){let R=(_&&_!=="/"?`${B}${_}`:B).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return R.length>0?R:"rpx"}async function o(B){if(B.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let _=B.verbose??!1,D=B.registryDir,R=new Set,N=B.proxies.map((S)=>{let w=S.id??c_(S.to,S.path);if(!X_(w))throw Error(`invalid registry id "${w}" derived from to="${S.to}"`);if(R.has(w))throw Error(`duplicate registry id "${w}" — set an explicit \`id\` on one of the proxies`);return R.add(w),{...S,id:w}}),X=new Date().toISOString();for(let S of N)await J_({id:S.id,from:S.from,to:S.to,path:S.path,pid:B.persistent?void 0:q.pid,cwd:q.cwd(),createdAt:X,cleanUrls:S.cleanUrls,changeOrigin:S.changeOrigin,pathRewrites:S.pathRewrites,static:S.static},D,_);let G=await Z_({rpxDir:B.rpxDir,verbose:_,spawnCommand:B.spawnCommand,startupTimeoutMs:B.startupTimeoutMs,spawnEnv:B.spawnEnv});for(let S of N){let w=S.static?`static ${typeof S.static==="string"?S.static:S.static.dir}`:S.from;J.success(`https://${S.to} → ${w}`)}if(J.info(`(via rpx daemon pid=${G.pid}; \`rpx daemon:status\` to inspect)`),B.detached)return;let W=!1,Y=D??G_(),Q=N.map((S)=>S.id),I=async()=>{if(W)return;W=!0;for(let S of Q)await Q_(S,D,_).catch((w)=>{K("runner",`removeEntry(${S}) failed: ${w}`,_)})},E=(S)=>{K("runner",`received ${S}, unregistering ${Q.length} entries`,_),I().finally(()=>q.exit(0))};q.once("SIGINT",E),q.once("SIGTERM",E),q.once("exit",()=>{if(W)return;for(let S of Q)try{P_.unlinkSync(L_.join(Y,`${S}.json`))}catch{}}),await new Promise(()=>{})}import{exec as g_}from"node:child_process";import c from"node:fs";import u_ from"node:os";import W_ from"node:path";import*as d from"node:process";import{promisify as p_}from"node:util";var e=p_(g_);function h_(B){let _=B.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var C=d.platform==="win32"?W_.join(d.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",V_=!1;async function __(B){if(d.platform==="win32")throw Error("Administrator privileges required on Windows");let _=a(),D=B.replace(/'/g,"'\\''");try{if(_){let{stdout:R}=await e(`echo '${_}' | sudo -S sh -c '${D}' 2>/dev/null`);return V_=!0,R}if(V_)try{let{stdout:R}=await e(`sudo -n sh -c '${D}'`);return R}catch(R){K("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:R}=await e(`sudo -n sh -c '${D}'`);return V_=!0,R}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(R){throw Error(`Failed to execute sudo command: ${R.message}`)}}async function v(B,_){let D=B.filter((N)=>!h_(N)),R=B.filter((N)=>h_(N));if(R.length>0)K("hosts",`Skipping /etc/hosts for loopback dev names: ${R.join(", ")}`,_);if(D.length===0)return;K("hosts",`Adding hosts: ${D.join(", ")}`,_),K("hosts",`Using hosts file at: ${C}`,_);try{let N;try{N=await c.promises.readFile(C,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{N=await __(`cat "${C}"`)}catch(Y){throw console.log(" Could not read hosts file — skipping hosts setup"),K("hosts",`sudo read also failed: ${Y}`,_),Error(`Cannot read hosts file: ${Y}`)}}let X=D.filter((Y)=>{let Q=`127.0.0.1 ${Y}`,I=`::1 ${Y}`;return!N.includes(Q)&&!N.includes(I)});if(X.length===0){K("hosts","All hosts already exist in hosts file",_);return}let G=X.map((Y)=>`
2
2
  # Added by rpx
3
- 127.0.0.1 ${W}
4
- ::1 ${W}`).join(`
5
- `),M=Q_.join(L_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await P.promises.writeFile(M,R+$,"utf8"),await o(`cat "${M}" | tee "${C}" > /dev/null`),console.log(` Hosts updated: ${G.join(", ")}`)}catch(W){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),G.forEach((Q)=>{console.log(` 127.0.0.1 ${Q}`),console.log(` ::1 ${Q}`)}),console.log(` Or run: sudo nano ${C}`)}finally{try{await P.promises.unlink(M)}catch{}}}catch(R){N("hosts",`Failed to manage hosts file: ${R.message}`,_)}}async function Z_(D,_){N("hosts",`Removing hosts: ${D.join(", ")}`,_);try{let B;try{B=await P.promises.readFile(C,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{B=await o(`cat "${C}"`)}catch(W){throw N("hosts",`sudo read also failed: ${W}`,_),Error(`Cannot read hosts file: ${W}`)}}let K=B.split(`
6
- `),R=!1,G=K.filter((W)=>{if(D.some((w)=>W.includes(` ${w}`)&&(W.includes("127.0.0.1")||W.includes("::1"))))return R=!0,!1;if(W.trim()==="# Added by rpx")return R=!0,!1;return!0});if(!R){N("hosts","No matching hosts found to remove",_);return}while(G[G.length-1]?.trim()==="")G.pop();let $=`${G.join(`
3
+ 127.0.0.1 ${Y}
4
+ ::1 ${Y}`).join(`
5
+ `),W=W_.join(u_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await c.promises.writeFile(W,N+G,"utf8"),await __(`cat "${W}" | tee "${C}" > /dev/null`),console.log(` Hosts updated: ${X.join(", ")}`)}catch(Y){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),X.forEach((Q)=>{console.log(` 127.0.0.1 ${Q}`),console.log(` ::1 ${Q}`)}),console.log(` Or run: sudo nano ${C}`)}finally{try{await c.promises.unlink(W)}catch{}}}catch(N){K("hosts",`Failed to manage hosts file: ${N.message}`,_)}}async function A_(B,_){K("hosts",`Removing hosts: ${B.join(", ")}`,_);try{let D;try{D=await c.promises.readFile(C,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{D=await __(`cat "${C}"`)}catch(Y){throw K("hosts",`sudo read also failed: ${Y}`,_),Error(`Cannot read hosts file: ${Y}`)}}let R=D.split(`
6
+ `),N=!1,X=R.filter((Y)=>{if(B.some((I)=>Y.includes(` ${I}`)&&(Y.includes("127.0.0.1")||Y.includes("::1"))))return N=!0,!1;if(Y.trim()==="# Added by rpx")return N=!0,!1;return!0});if(!N){K("hosts","No matching hosts found to remove",_);return}while(X[X.length-1]?.trim()==="")X.pop();let G=`${X.join(`
7
7
  `)}
8
- `,M=Q_.join(L_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await P.promises.writeFile(M,$,"utf8"),await o(`cat "${M}" | tee "${C}" > /dev/null`),N("hosts","Hosts removed successfully",_)}catch(W){N("hosts","Could not clean up hosts file automatically",_)}finally{try{await P.promises.unlink(M)}catch(W){N("hosts",`Failed to remove temporary file: ${W}`,_)}}}catch(B){N("hosts",`Failed to clean up hosts file: ${B.message}`,_)}}async function v(D,_){N("hosts",`Checking hosts: ${D}`,_);let B;try{B=await P.promises.readFile(C,"utf-8")}catch(K){N("hosts",`Error reading hosts file: ${K}`,_);try{let R=l(),G;if(R)G=`echo '${R}' | sudo -S cat "${C}" 2>/dev/null`;else G=`sudo -n cat "${C}" 2>/dev/null || cat "${C}" 2>/dev/null || echo ""`;let{stdout:$}=await s(G);B=$}catch(R){return N("hosts",`Cannot read hosts file, assuming entries don't exist: ${R}`,_),D.map(()=>!1)}}return D.map((K)=>{let R=`127.0.0.1 ${K}`,G=`::1 ${K}`;return B.includes(R)||B.includes(G)})}import*as e from"node:net";function U(D,_,B){return N("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((K)=>{let R=e.createServer(),G=setTimeout(()=>{N("port",`Checking port ${D} timed out, assuming it's in use`,B),R.close(),K(!0)},3000);R.once("error",($)=>{if(clearTimeout(G),$.code==="EADDRINUSE")N("port",`Port ${D} is in use`,B),K(!0);else N("port",`Error checking port ${D}: ${$.message}`,B),K(!0)}),R.once("listening",()=>{clearTimeout(G),N("port",`Port ${D} is available`,B),R.close(),K(!1)});try{R.listen(D,_)}catch($){clearTimeout(G),N("port",`Exception checking port ${D}: ${$}`,B),K(!0)}})}async function h_(D,_,B,K=50){N("port",`Finding available port starting from ${D} (max attempts: ${K})`,B);let R=D,G=0;while(G<K){if(G++,!await U(R,_,B))return N("port",`Found available port: ${R} after ${G} attempts`,B),R;N("port",`Port ${R} is in use, trying ${R+1} (attempt ${G}/${K})`,B),R++}throw Error(`Unable to find available port after ${K} attempts starting from ${D}`)}function c_(D,_,B=5000,K){return N("port",`Testing connection to ${_}:${D}`,K),new Promise((R)=>{let G=e.connect({host:_,port:D,timeout:B});G.once("connect",()=>{N("port",`Successfully connected to ${_}:${D}`,K),G.end(),R(!0)}),G.once("timeout",()=>{N("port",`Connection to ${_}:${D} timed out`,K),G.destroy(),R(!1)}),G.once("error",($)=>{N("port",`Failed to connect to ${_}:${D}: ${$.message}`,K),G.destroy(),R(!1)})})}class d{usedPorts=new Set;hostname;verbose;maxRetries;constructor(D="0.0.0.0",_,B=50){this.hostname=D,this.verbose=_,this.maxRetries=B}async getNextAvailablePort(D,_=!1){if(this.usedPorts.has(D))return this.findNextAvailablePort(D+1,_);if(await U(D,this.hostname,this.verbose))return this.findNextAvailablePort(D+1,_);if(_){if(!await c_(D,this.hostname,3000,this.verbose))return N("port",`Port ${D} is available but not connectable, trying next port`,this.verbose),this.findNextAvailablePort(D+1,_)}return this.usedPorts.add(D),D}async findNextAvailablePort(D,_=!1){let B=await h_(D,this.hostname,this.verbose,this.maxRetries);if(_){if(!await c_(B,this.hostname,3000,this.verbose))if(B<D+this.maxRetries)return this.findNextAvailablePort(B+1,_);else throw Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)}return this.usedPorts.add(B),B}releasePort(D){N("port",`Releasing port ${D}`,this.verbose),this.usedPorts.delete(D)}}var p_=new d;import{spawn as g_}from"node:child_process";import*as L from"node:process";class __{processes=new Map;isShuttingDown=!1;async startProcess(D,_,B){if(this.processes.has(D)){N("start",`Process ${D} is already running`,B);return}let[K,...R]=_.command.split(" "),G=_.cwd||L.cwd();N("start",`Starting process ${D}:`,B),N("start",` Command: ${K} ${R.join(" ")}`,B),N("start",` Working directory: ${G}`,B),N("start",` Environment variables: ${y(_.env)}`,B);let $=g_(K,R,{cwd:G,env:{...L.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(D,{command:_.command,cwd:G,process:$,env:_.env}),new Promise((M,W)=>{if($.on("error",(Q)=>{if(!this.isShuttingDown)N("start",`Process ${D} failed to start: ${Q}`,B),this.processes.delete(D),W(Q),L.emit("SIGINT")}),$.on("exit",(Q)=>{if(!this.isShuttingDown&&Q!==null&&Q!==0)N("start",`Process ${D} exited with code ${Q}`,B),this.processes.delete(D),W(Error(`Process ${D} exited with code ${Q}`)),L.emit("SIGINT")}),B)$.stdout?.on("data",(Q)=>{N("process",`[${D}] ${Q.toString().trim()}`,!0)}),$.stderr?.on("data",(Q)=>{N("process",`[${D}] ERR: ${Q.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&$.killed)this.processes.delete(D),W(Error(`Process ${D} was killed during startup`));else N("start",`Process ${D} started successfully`,B),M()},1000)})}async stopProcess(D,_){let B=this.processes.get(D);if(!B?.process){N("start",`No process found for ${D}`,_);return}return N("start",`Stopping process ${D}`,_),new Promise((K)=>{if(!B.process){K();return}B.process.once("exit",()=>{this.processes.delete(D),N("start",`Process ${D} stopped`,_),K()});try{B.process.kill("SIGTERM"),setTimeout(()=>{if(B.process){N("start",`Force killing process ${D}`,_);try{B.process.kill("SIGKILL")}catch(R){}}},3000)}catch(R){N("start",`Error stopping process ${D}: ${R}`,_),this.processes.delete(D),K()}})}async stopAll(D){if(this.isShuttingDown){N("start","Already shutting down, skipping duplicate stopAll call",D);return}this.isShuttingDown=!0,N("start","Stopping all processes",D);let _=Array.from(this.processes.keys()).map((B)=>this.stopProcess(B,D).catch((K)=>{X.error(`Failed to stop process ${B}:`,K)}));await Promise.allSettled(_),this.processes.clear(),this.isShuttingDown=!1}isRunning(D){let _=this.processes.get(D);return!!_?.process&&!_.process.killed}}var LD=new __;var B_=new __,n_="0.12.0",t_=new d("0.0.0.0"),p=new Set,A_=!1,D_=null,S_=null;async function i(D){if(A_)return N("cleanup","Cleanup already in progress, skipping",D?.verbose),S_||Promise.resolve();A_=!0,N("cleanup","Starting cleanup process",D?.verbose),S_=new Promise((_)=>{D_=_});try{await B_.stopAll(D?.verbose),X.info("Shutting down proxy servers...");let _=[],B=Array.from(p).map((K)=>new Promise((R)=>{K.close(()=>{N("cleanup","Server closed successfully",D?.verbose),R()})}));if(_.push(...B),D?.hosts&&D.domains?.length){N("cleanup","Cleaning up hosts file entries",D?.verbose),N("cleanup",`Original domains for cleanup: ${JSON.stringify(D.domains)}`,D?.verbose);let K=D.domains.filter((R)=>{if(R==="test.local")return!0;return R!=="localhost"&&!R.startsWith("localhost.")&&R!=="127.0.0.1"});if(N("cleanup",`Filtered domains for cleanup: ${JSON.stringify(K)}`,D?.verbose),K.length>0)X.info("Cleaning up hosts file entries..."),_.push(Z_(K,D?.verbose).then(()=>{N("cleanup",`Removed hosts entries for ${K.join(", ")}`,D?.verbose)}).catch((R)=>{N("cleanup",`Failed to remove hosts entries: ${R}`,D?.verbose),X.warn(`Failed to clean up hosts file entries for ${K.join(", ")}:`,R)}))}if(D?.certs&&D.domains?.length){N("cleanup","Cleaning up SSL certificates",D?.verbose),X.info("Cleaning up SSL certificates...");let K=D.domains.map(async(R)=>{try{await z_(R,D?.verbose),N("cleanup",`Removed certificates for ${R}`,D?.verbose)}catch(G){N("cleanup",`Failed to remove certificates for ${R}: ${G}`,D?.verbose),X.warn(`Failed to clean up certificates for ${R}:`,G)}});_.push(...K)}await Promise.allSettled(_),N("cleanup","All cleanup tasks completed successfully",D?.verbose),X.success("All cleanup tasks completed successfully")}catch(_){N("cleanup",`Error during cleanup: ${_}`,D?.verbose),X.error("Error during cleanup:",_)}finally{if(D_)D_();D_=null,A_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(T.env.NODE_ENV!=="test"&&T.env.BUN_ENV!=="test"&&!_)T.exit(0)}return S_}var V_=!1;function k_(D){if(V_){N("signal",`Received second ${D} signal, forcing exit`,!0),T.exit(1);return}V_=!0,N("signal",`Received ${D} signal, initiating cleanup`,!0),i().catch((_)=>{N("signal",`Cleanup failed after ${D}: ${_}`,!0),T.exit(1)}).finally(()=>{V_=!1})}T.once("SIGINT",()=>k_("SIGINT"));T.once("SIGTERM",()=>k_("SIGTERM"));T.on("uncaughtException",(D)=>{N("process",`Uncaught exception: ${D}`,!0),X.error("Uncaught exception:",D),k_("uncaughtException")});async function g(D,_,B,K=5){N("connection",`Testing connection to ${D}:${_} (retries left: ${K})`,B);let R=15000,G=Date.now();if(T.env.RPX_BYPASS_CONNECTION_TEST==="true"){N("connection",`Bypassing connection test for ${D}:${_} due to RPX_BYPASS_CONNECTION_TEST flag`,B);return}let $=()=>new Promise((M,W)=>{let Q=v_.connect({host:D,port:_,timeout:3000});Q.once("connect",()=>{N("connection",`Successfully connected to ${D}:${_}`,B),Q.end(),M()}),Q.once("timeout",()=>{N("connection",`Connection to ${D}:${_} timed out`,B),Q.destroy(),W(Error("Connection timed out"))}),Q.once("error",(w)=>{N("connection",`Failed to connect to ${D}:${_}: ${w}`,B),Q.destroy(),W(w)})});try{await $()}catch(M){if(Date.now()-G>R){N("connection",`Connection test timed out after ${R}ms, but continuing anyway`,B),X.warn(`Connection test to ${D}:${_} timed out, but RPX will try to proceed anyway.`);return}if(M.code==="ECONNREFUSED"&&K>0)return N("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${K} retries left)`,B),await new Promise((Q)=>setTimeout(Q,2000)),g(D,_,B,K-1);if(K>0)try{N("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((Q,w)=>{let E=c.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(V)=>{N("connection",`Received HTTP response with status: ${V.statusCode}`,B),Q()});E.on("error",(V)=>w(V)),E.on("timeout",()=>{E.destroy(),w(Error("HTTP request timed out"))}),E.end()}),N("connection",`HTTP request to ${D}:${_} succeeded`,B);return}catch(Q){return N("connection",`HTTP request to ${D}:${_} failed: ${Q}`,B),N("connection",`Retrying socket connection in 2 seconds... (${K} retries left)`,B),await new Promise((w)=>setTimeout(w,2000)),g(D,_,B,K-1)}let W=`Failed to connect to ${D}:${_} after ${5-K} attempts: ${M.message}`;N("connection",`${W}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,B),X.warn(W),X.warn("RPX will try to continue anyway. If you're sure this is correct, you can set RPX_BYPASS_CONNECTION_TEST=true to skip this check.")}}async function C_(D){N("server",`Starting server with options: ${y(D)}`,D.verbose);let _=new URL((D.from?.startsWith("http")?D.from:`http://${D.from}`)||"localhost:5173"),B=new URL((D.to?.startsWith("http")?D.to:`http://${D.to}`)||"rpx.localhost"),K=Number.parseInt(_.port)||(_.protocol.includes("https:")?443:80),R=[B.hostname];if(x_(D)&&!B.hostname.includes("localhost")&&!B.hostname.includes("127.0.0.1")){N("hosts",`Checking if hosts file entry exists for: ${B.hostname}`,D?.verbose);try{if(!(await v(R,D.verbose))[0]){X.info(`Adding ${B.hostname} to hosts file...`),X.info("This may require sudo/administrator privileges");try{await m(R,D.verbose)}catch(M){if(X.error("Failed to add hosts entry:",M.message),X.warn("You can manually add this entry to your hosts file:"),X.warn(`127.0.0.1 ${B.hostname}`),X.warn(`::1 ${B.hostname}`),T.platform==="win32")X.warn("On Windows:"),X.warn("1. Run notepad as administrator"),X.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else X.warn("On Unix systems:"),X.warn("sudo nano /etc/hosts")}}else N("hosts",`Host entry already exists for ${B.hostname}`,D.verbose)}catch($){X.error("Failed to check hosts file:",$.message)}}try{await g(_.hostname,K,D.verbose)}catch($){N("server",`Connection test failed: ${$}`,D.verbose),X.error($.message),X.warn("Continuing with proxy setup despite connection test failure..."),X.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let G=D._cachedSSLConfig||null;if(D.https)try{if(D.https===!0)D.https=R_({...D,to:B.hostname});if(G=await b({...D,to:B.hostname,https:D.https}),!G){if(N("ssl",`Generating new certificates for ${B.hostname}`,D.verbose),await K_({...D,from:_.toString(),to:B.hostname,https:D.https}),G=await b({...D,to:B.hostname,https:D.https}),!G)throw Error(`Failed to load SSL configuration after generating certificates for ${B.hostname}`)}}catch($){throw N("server",`SSL setup failed: ${$}`,D.verbose),$}N("server",`Setting up reverse proxy with SSL config for ${B.hostname}`,D.verbose),await s_({...D,from:D.from||"localhost:5173",to:B.hostname,fromPort:K,sourceUrl:{hostname:_.hostname,host:_.host},ssl:G})}async function r_(D,_,B,K,R,G,$,M,W,Q,w){N("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${Q}`,W);function E(S){let J={};for(let[A,z]of Object.entries(S))if(!A.startsWith(":"))J[A]=z;return J}let V=(S,J)=>{N("request",`Incoming request: ${S.method} ${S.url}`,W);let A=S.url||"/",z=S.method||"GET";if(S instanceof m_.Http2ServerRequest){let Z=S.headers;z=Z[":method"]||z,A=Z[":path"]||A}if(Q){if(!A.match(/\.[a-z0-9]+$/i))if(A.endsWith("/"))A=`${A}index.html`;else A=`${A}.html`}let j=E(S.headers);if(w)j.host=`${G.hostname}:${B}`,N("request",`Changed origin: setting host header to ${j.host}`,W);let k={hostname:G.hostname,port:B,path:A,method:z,headers:j};N("request",`Proxy request options: ${y(k)}`,W);let h=c.request(k,(Z)=>{if(N("response",`Proxy response received with status ${Z.statusCode}`,W),Q&&Z.statusCode===404){let H=[];if(A.endsWith(".html"))H.push(A.slice(0,-5));else if(!A.match(/\.[a-z0-9]+$/i))H.push(`${A}.html`);if(!A.endsWith("/"))H.push(`${A}/index.html`);if(H.length>0){N("cleanUrls",`Trying alternative paths: ${H.join(", ")}`,W);let x=(O)=>{if(O.length===0){J.writeHead(Z.statusCode||404,Z.headers),Z.pipe(J);return}let u=O[0],l_={...k,path:u},f_=c.request(l_,(n)=>{if(n.statusCode===200)N("cleanUrls",`Found matching path: ${u}`,W),J.writeHead(n.statusCode,n.headers),n.pipe(J);else x(O.slice(1))});f_.on("error",()=>x(O.slice(1))),f_.end()};x(H);return}}let F={...Z.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};J.writeHead(Z.statusCode||500,F),Z.pipe(J)});h.on("error",(Z)=>{N("request",`Proxy request failed: ${Z}`,W),X.error("Proxy request failed:",Z),J.writeHead(502),J.end(`Proxy Error: ${Z.message}`)}),S.pipe(h)};if(N("server",`Creating server with SSL config: ${!!$}`,W),$)return new Promise((S,J)=>{try{let A=Bun.serve({port:K,hostname:R,tls:{key:$.key,cert:$.cert,ca:$.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(z){let j=new URL(z.url);N("request",`Bun.serve received: ${z.method} ${j.pathname}`,W);let k=`http://${G.host}`,h=new URL(j.pathname+j.search,k);try{let Z=new Headers(z.headers);if(Z.set("host",G.host),w)Z.set("origin",k);Z.set("x-forwarded-for","127.0.0.1"),Z.set("x-forwarded-proto","https"),Z.set("x-forwarded-host",_);let F=await fetch(h.toString(),{method:z.method,headers:Z,body:z.body,redirect:"manual"}),H=new Headers(F.headers);if(Q&&j.pathname.endsWith(".html")){let x=j.pathname.replace(/\.html$/,"");return new Response(null,{status:301,headers:{Location:x}})}return new Response(F.body,{status:F.status,statusText:F.statusText,headers:H})}catch(Z){return N("request",`Proxy error: ${Z}`,W),new Response(`Proxy Error: ${Z}`,{status:502})}},error(z){return N("server",`Bun.serve error: ${z}`,W),new Response(`Server Error: ${z.message}`,{status:500})}});p.add(A),u_({from:D,to:_,vitePluginUsage:M,listenPort:K,ssl:!0,cleanUrls:Q,verbose:W}),S()}catch(A){J(A)}});let I=c.createServer(V);function Y(S){return p.add(S),new Promise((J,A)=>{S.listen(K,R,()=>{N("server",`Server listening on port ${K}`,W),u_({from:D,to:_,vitePluginUsage:M,listenPort:K,ssl:!!$,cleanUrls:Q,verbose:W}),J()}),S.on("error",(z)=>{N("server",`Server error: ${z}`,W),A(z)})})}return Y(I)}async function s_(D){N("setup",`Setting up reverse proxy: ${y(D)}`,D.verbose);let{from:_,to:B,fromPort:K,sourceUrl:R,ssl:G,verbose:$,cleanup:M,vitePluginUsage:W,changeOrigin:Q,cleanUrls:w}=D,E=80,V=443,I="0.0.0.0",Y=D.portManager||t_,S=x_(D);try{if(S&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1")){if(!(await v([B],$))[0]){X.warn(`The hostname ${B} isn't in your hosts file. Adding it now...`);try{await m([B],$),X.success(`Added ${B} to your hosts file.`)}catch(k){X.error(`Failed to add ${B} to your hosts file: ${k}`),X.info(`You may need to manually add '127.0.0.1 ${B}' to your /etc/hosts file.`)}}}else if(S&&T.platform!=="darwin"&&B&&B.includes("localhost")&&!B.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await v([B],$))[0]){N("hosts",`${B} not found in hosts file, adding...`,$);try{await m([B],$)}catch(k){N("hosts",`Failed to add ${B} to hosts file: ${k}`,$)}}}if(G&&!Y.usedPorts.has(E)){if(!await U(E,I,$))N("setup","Starting HTTP redirect server",$),b_($),Y.usedPorts.add(E);else if(N("setup","Port 80 is in use, skipping HTTP redirect",$),$)X.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let J=G?V:E,A=await U(J,I,$),z;if(A){if(N("setup",`Port ${J} is already in use`,$),$)X.warn(`Port ${J} is already in use. This may be another instance of rpx or another service.`);if(J===443){if(z=await Y.getNextAvailablePort(3443,!0),N("setup",`Using port ${z} instead of ${J}`,$),$)X.info(`Using port ${z} instead. Access your site at https://${B}:${z}`)}else if(z=await Y.getNextAvailablePort(J+1000,!0),N("setup",`Using port ${z} instead of ${J}`,$),$)X.info(`Using port ${z} instead. Access your site at http://${B}:${z}`)}else z=J,Y.usedPorts.add(z),N("setup",`Using standard ${J===443?"HTTPS":"HTTP"} port ${J} for ${B}`,$);await r_(_,B,K,z,I,R,G,W,$,w,Q)}catch(J){N("setup",`Setup failed: ${J}`,$),X.error(`Failed to setup reverse proxy: ${J.message}`),i({domains:[B],hosts:typeof M==="boolean"?M:M?.hosts,certs:typeof M==="boolean"?M:M?.certs,verbose:$,vitePluginUsage:W})}}function b_(D){N("redirect","Starting HTTP redirect server",D);let _=c.createServer((B,K)=>{let R=B.headers.host||"";N("redirect",`Redirecting request from ${R}${B.url} to HTTPS`,D),K.writeHead(301,{Location:`https://${R}${B.url}`}),K.end()}).listen(80);p.add(_),N("redirect","HTTP redirect server started",D)}function o_(D){let _={...N_,...D};if(N("proxy",`Starting proxy with options: ${y(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){X.error("viaDaemon mode requires both `from` and `to`");return}r({proxies:[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}],verbose:_.verbose}).catch((W)=>{X.error(`Failed to register with rpx daemon: ${W.message}`),T.exit(1)});return}let B=_.to||"",K=B.split(".").pop()?.toLowerCase()||"",R=T.platform==="darwin"&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1"),G=["dev","app","page","new","day","foo"],$=["test","localhost","local","example","invalid"];if(R&&G.includes(K)&&_?.verbose)X.warn(`The .${K} TLD may not work reliably for local development`),X.info(` Google owns .${K} with HSTS preloading, which can bypass local DNS`),X.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(R)import("./chunk-1sg87m7v.js").then(({setupDevelopmentDns:W})=>{W({domains:[B],verbose:_.verbose}).then((Q)=>{if(Q)Promise.resolve().then(()=>{if(_.verbose)if($.includes(K))X.success(`DNS server started for .${K} domains`);else X.success(`DNS server started for .${K} domains (hosts file entry also added)`)});else N("dns",`Could not start DNS server - ${B} may not resolve in browser`,_.verbose)})}).catch((W)=>{N("dns",`Failed to start DNS server: ${W}`,_.verbose)});let M={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:R_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};N("proxy",`Server options: ${y(M)}`,_.verbose),C_(M).catch((W)=>{N("proxy",`Failed to start proxy: ${W}`,_.verbose),X.error(`Failed to start proxy: ${W.message}`),i({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function e_(D){return D?.verbose||!1}function x_(D){if(D?.hostsManagement===!1)return!1;let _=D?.cleanup;if(_===!1)return!1;if(_&&typeof _==="object"&&_.hosts===!1)return!1;return!0}async function q_(D){let _={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(D)_={..._,...D};let B=e_(_),K=x_(_);if(N("config",`Starting with config: ${y(_,2)}`,B),N("config",`Is multi-proxy? ${"proxies"in _}`,B),N("config",`Hosts management enabled? ${K}`,B),_.viaDaemon){let S="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((J)=>({id:J.id,from:J.from,to:J.to,path:J.path,cleanUrls:J.cleanUrls??_.cleanUrls,changeOrigin:J.changeOrigin??_.changeOrigin,pathRewrites:J.pathRewrites})):[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await r({proxies:S,verbose:B});return}if("proxies"in _&&Array.isArray(_.proxies)){N("servers",`Found ${_.proxies.length} proxies in config`,B);for(let Y of _.proxies)if(Y.start){let S=`${Y.from}-${Y.to}`;try{N("watch",`Starting command for ${S} with command: ${Y.start.command}`,B),X.info(`Starting command for ${S}...`),await B_.startProcess(S,Y.start,B);let J=new URL(Y.from.startsWith("http")?Y.from:`http://${Y.from}`),A=J.hostname||"localhost",z=Number(J.port)||80;try{await g(A,z,B),N("watch",`Dev server is ready at ${A}:${z}`,B)}catch(j){N("watch",`Connection check failed, but continuing with proxy setup: ${j}`,B),X.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(J){throw N("watch",`Failed to start command for ${S}: ${J}`,B),Error(`Failed to start command for ${S}: ${J}`)}}else N("watch",`No start command for proxy ${Y.from} -> ${Y.to}`,B)}else if("start"in _&&_.start){N("watch","Found start command in single proxy config",B);let Y=`${_.from}-${_.to}`;try{if(_.start)N("watch",`Starting command: ${_.start.command}`,B),await B_.startProcess(Y,_.start,B);let S=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),J=S.hostname||"localhost",A=Number(S.port)||80;try{await g(J,A,B),N("watch",`Dev server is ready at ${J}:${A}`,B)}catch(z){N("watch",`Connection check failed, but continuing with proxy setup: ${z}`,B),X.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(S){throw N("watch",`Failed to run start command: ${S}`,B),Error(`Failed to run start command: ${S}`)}}else N("watch","No start command found in config",B);let R="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(T.platform!=="win32"&&(_.https||K)){if(!l())try{N("sudo","Pre-acquiring sudo credentials for privileged operations",B),i_("sudo -v",{stdio:"inherit"})}catch{N("sudo","Could not pre-acquire sudo credentials",B)}}if(_.https){let Y=await b(_);if(!Y){if(N("ssl",`No valid or trusted certificates found for ${R}, generating new ones`,_.verbose),await K_(_),Y=await b(_),!Y)throw Error(`Failed to load SSL certificates after generation for ${R}`)}else N("ssl",`Using existing and trusted certificates for ${R}`,_.verbose);_._cachedSSLConfig=Y}let G="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((Y)=>({...Y,https:_.https,cleanup:_.cleanup,cleanUrls:Y.cleanUrls??("cleanUrls"in _?_.cleanUrls:!1),vitePluginUsage:_.vitePluginUsage,changeOrigin:Y.changeOrigin??_.changeOrigin,verbose:B,_cachedSSLConfig:_._cachedSSLConfig})):[{from:"from"in _?_.from:"localhost:5173",to:"to"in _?_.to:"rpx.localhost",cleanUrls:"cleanUrls"in _?_.cleanUrls:!1,https:_.https,cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,start:"start"in _?_.start:void 0,changeOrigin:_.changeOrigin,verbose:B,_cachedSSLConfig:_._cachedSSLConfig}],$=G.map((Y)=>Y.to||"rpx.localhost"),M=_._cachedSSLConfig,W=$.filter((Y)=>Y&&!Y.includes("localhost")&&!Y.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],w=["test","localhost","local","example","invalid"],E=[...new Set(W.map((Y)=>Y.split(".").pop()?.toLowerCase()))],V=E.filter((Y)=>!!Y&&Q.includes(Y));if(V.length>0&&B)X.warn(`The following TLDs may not work reliably for local development: ${V.map((Y)=>`.${Y}`).join(", ")}`),X.info(" These TLDs have HSTS preloading which can bypass local DNS"),X.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(K&&T.platform==="darwin"&&W.length>0){let{setupDevelopmentDns:Y}=await import("./chunk-1sg87m7v.js");if(await Y({domains:W,verbose:B})){if(B)if(E.every((A)=>!!A&&w.includes(A)))X.success(`DNS server started for ${E.map((A)=>`.${A}`).join(", ")} domains`);else X.success(`DNS server started for ${E.map((A)=>`.${A}`).join(", ")} domains (hosts file entries also added)`)}else N("dns","Could not start DNS server - custom domains may not resolve",B)}let I=async()=>{N("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:Y}=await import("./chunk-1sg87m7v.js");await Y({verbose:_.verbose})}catch(Y){N("cleanup",`Error stopping DNS server: ${Y}`,_.verbose)}try{await B_.stopAll(_.verbose)}catch(Y){N("cleanup",`Error stopping processes: ${Y}`,_.verbose)}await i({domains:$,hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose||!1})};if(T.on("SIGINT",I),T.on("SIGTERM",I),T.on("uncaughtException",(Y)=>{N("process",`Uncaught exception: ${Y}`,!0),console.error("Uncaught exception:",Y),I()}),M&&G.length>1){N("proxies",`Creating shared HTTPS server for ${G.length} domains`,B);let Y=[],S=new Set;for(let Z of G){let F=Z.to||"rpx.localhost",H=Z.cleanUrls||!1,x=Z.path,O=E_(x);if(Z.static)Y.push({host:F,path:x,route:{static:F_(Z.static,H),cleanUrls:H,basePath:O}}),N("proxies",`Route: ${F}${x??""} → static ${typeof Z.static==="string"?Z.static:Z.static.dir}`,B);else{let u=new URL(Z.from?.startsWith("http")?Z.from:`http://${Z.from}`);Y.push({host:F,path:x,route:{sourceHost:u.host,cleanUrls:H,changeOrigin:Z.changeOrigin||!1,pathRewrites:Z.pathRewrites,basePath:O}}),N("proxies",`Route: ${F}${x??""} → ${u.host}`,B)}if(S.has(F))continue;if(S.add(F),K&&!w_(F)&&!F.includes("localhost")&&!F.includes("127.0.0.1"))try{if(!(await v([F],B))[0])await m([F],B)}catch{N("hosts",`Could not add hosts entry for ${F}`,B)}}if(!await U(80,"0.0.0.0",B))b_(B);let A=443;if(await U(A,"0.0.0.0",B)){if(N("proxies",`Port ${A} is already in use, cannot start shared proxy`,B),B)X.warn(`Port ${A} is in use. Shared HTTPS proxy cannot start.`);return}let j=j_(Y),k=T_((Z,F)=>H_(j,Z,F),B),h=I_(B);try{let Z=Bun.serve({port:A,hostname:"0.0.0.0",tls:{key:M.key,cert:M.cert,ca:M.ca,requestCert:!1,rejectUnauthorized:!1},fetch(F,H){return k(F,H)},websocket:h,error(F){return N("server",`Shared proxy server error: ${F}`,B),new Response(`Server Error: ${F.message}`,{status:500})}});p.add(Z),N("proxies",`Shared HTTPS proxy listening on port ${A} for ${j.size} domains`,B)}catch(Z){N("proxies",`Failed to start shared proxy: ${Z}`,B),console.error("Failed to start shared HTTPS proxy:",Z),I()}}else for(let Y of G)try{let S=Y.to||"rpx.localhost";N("proxy",`Starting proxy for ${S} with SSL config: ${!!M}`,Y.verbose),await C_({from:Y.from||"localhost:5173",to:S,cleanUrls:Y.cleanUrls||!1,https:Y.https||!1,cleanup:Y.cleanup||!1,vitePluginUsage:Y.vitePluginUsage||!1,verbose:Y.verbose||!1,_cachedSSLConfig:M,changeOrigin:Y.changeOrigin||!1})}catch(S){N("proxies",`Failed to start proxy for ${Y.to}: ${S}`,Y.verbose),console.error(`Failed to start proxy for ${Y.to}:`,S),I()}}function u_(D){if(D?.vitePluginUsage||!D?.verbose)return;if(console.log(""),console.log(` ${q.green(q.bold("rpx"))} ${q.green(`v${n_}`)}`),console.log(` ${q.green("➜")} ${q.dim(D?.from??"")} ${q.dim("➜")} ${q.cyan(D?.ssl?`https://${D?.to}`:`http://${D?.to}`)}`),D?.listenPort!==(D?.ssl?443:80))console.log(` ${q.green("➜")} Listening on port ${D?.listenPort}`);if(D?.cleanUrls)console.log(` ${q.green("➜")} Clean URLs enabled`)}var HB=q_;export{G_ as writeEntry,v2 as watchRegistry,K2 as verifyHttpsChain,S2 as trustRootCaForBrowsers,KD as tearDownDevelopmentDns,ND as syncDevelopmentDnsFromRegistry,k2 as stripBasePath,r2 as stopDnsServer,SD as stopDaemon,C_ as startServer,o_ as startProxy,q_ as startProxies,t2 as startDnsServer,_D as setupResolver,BD as setupDevelopmentDns,y2 as serverNameFromCertFilename,H2 as serveStaticFile,y as safeStringify,E2 as safeRelativePath,cB as safeDeleteFile,r as runViaDaemon,ZD as runDaemon,o2 as resolverFilePath,p2 as resolverBasenamesForDomains,d2 as resolverBasenameForDomain,F_ as resolveStaticRoute,j2 as resolveStaticFile,LB as resolvePathRewrite,RD as removeResolver,DD as removeLegacyTldResolvers,Z_ as removeHosts,W_ as removeEntry,QD as releaseDaemonLock,CB as redactSensitive,YD as reconcileStaleDevelopmentDns,VD as reconcileDevelopmentDnsOnIdle,h2 as readEntry,WD as readDaemonPid,D2 as readCertSha256Fingerprint,B2 as readCertCommonName,u2 as readAll,Q2 as pruneStaleRootCas,p_ as portManager,q2 as pathPrefixMatches,R2 as parseSha256HashesFromSecurityListing,_2 as normalizeSha256Fingerprint,E_ as normalizePathPrefix,a2 as normalizeDevDomain,C2 as matchesWildcard,O2 as matchesAllowedSuffix,H_ as matchHostRoute,f2 as matchHostList,x2 as matchHost,z2 as loadSSLConfig,J2 as listCertSha256HashesByCommonName,w_ as isWildcardPattern,qB as isValidRootCA,$_ as isValidId,OB as isSingleProxyOptions,PB as isSingleProxyConfig,Z2 as isRootCaTrustedForSsl,A2 as isRootCaFingerprintInKeychains,U as isPortInUse,c2 as isPidAlive,UB as isMultiProxyOptions,yB as isMultiProxyConfig,P2 as isLikelyHostname,s2 as isDnsServerRunning,XD as isDaemonRunning,I2 as isCertTrusted,R_ as httpsConfig,l as getSudoPassword,M2 as getSharedDaemonCertPaths,V2 as getRootCAPaths,Y_ as getRegistryDir,fB as getPrimaryDomain,X2 as getMacosTrustKeychains,W2 as getMacosLoginKeychainPath,$D as getDaemonRpxDir,GD as getDaemonPidPath,K_ as generateCertificate,m2 as gcStaleEntries,F2 as forceTrustCertificate,h_ as findAvailablePort,xB as extractHostname,kB as execSudoSync,X_ as ensureDaemonRunning,g2 as devDomainsFromHosts,O_ as deriveIdFromTarget,AD as defaultDaemonSpawnCommand,N_ as defaultConfig,HB as default,N as debugLog,I_ as createProxyWebSocketHandler,T_ as createProxyFetchHandler,w2 as contentTypeFor,e2 as contentLooksLikeRpxResolver,N_ as config,q as colors,T2 as clearSslConfigCache,z_ as cleanupCertificates,i as cleanup,v as checkHosts,b as checkExistingCertificates,N2 as certIncludesSanHostnames,U2 as buildSniTlsConfig,j_ as buildHostRoutes,m as addHosts,JD as acquireDaemonLock,G2 as RPX_ROOT_CA_COMMON_NAME,n2 as RPX_RESOLVER_MARKER,L2 as OnDemandCertManager,$2 as MACOS_SYSTEM_KEYCHAIN,Y2 as MACOS_CA_TRUST_FLAGS,l2 as LEGACY_TLD_RESOLVER_LABELS,d as DefaultPortManager,b2 as DNS_STATE_VERSION,i2 as DNS_PORT};
8
+ `,W=W_.join(u_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await c.promises.writeFile(W,G,"utf8"),await __(`cat "${W}" | tee "${C}" > /dev/null`),K("hosts","Hosts removed successfully",_)}catch(Y){K("hosts","Could not clean up hosts file automatically",_)}finally{try{await c.promises.unlink(W)}catch(Y){K("hosts",`Failed to remove temporary file: ${Y}`,_)}}}catch(D){K("hosts",`Failed to clean up hosts file: ${D.message}`,_)}}async function b(B,_){K("hosts",`Checking hosts: ${B}`,_);let D;try{D=await c.promises.readFile(C,"utf-8")}catch(R){K("hosts",`Error reading hosts file: ${R}`,_);try{let N=a(),X;if(N)X=`echo '${N}' | sudo -S cat "${C}" 2>/dev/null`;else X=`sudo -n cat "${C}" 2>/dev/null || cat "${C}" 2>/dev/null || echo ""`;let{stdout:G}=await e(X);D=G}catch(N){return K("hosts",`Cannot read hosts file, assuming entries don't exist: ${N}`,_),B.map(()=>!1)}}return B.map((R)=>{let N=`127.0.0.1 ${R}`,X=`::1 ${R}`;return D.includes(N)||D.includes(X)})}import*as B_ from"node:net";function P(B,_,D){return K("port",`Checking if port ${B} is in use on ${_}`,D),new Promise((R)=>{let N=B_.createServer(),X=setTimeout(()=>{K("port",`Checking port ${B} timed out, assuming it's in use`,D),N.close(),R(!0)},3000);N.once("error",(G)=>{if(clearTimeout(X),G.code==="EADDRINUSE")K("port",`Port ${B} is in use`,D),R(!0);else K("port",`Error checking port ${B}: ${G.message}`,D),R(!0)}),N.once("listening",()=>{clearTimeout(X),K("port",`Port ${B} is available`,D),N.close(),R(!1)});try{N.listen(B,_)}catch(G){clearTimeout(X),K("port",`Exception checking port ${B}: ${G}`,D),R(!0)}})}async function v_(B,_,D,R=50){K("port",`Finding available port starting from ${B} (max attempts: ${R})`,D);let N=B,X=0;while(X<R){if(X++,!await P(N,_,D))return K("port",`Found available port: ${N} after ${X} attempts`,D),N;K("port",`Port ${N} is in use, trying ${N+1} (attempt ${X}/${R})`,D),N++}throw Error(`Unable to find available port after ${R} attempts starting from ${B}`)}function m_(B,_,D=5000,R){return K("port",`Testing connection to ${_}:${B}`,R),new Promise((N)=>{let X=B_.connect({host:_,port:B,timeout:D});X.once("connect",()=>{K("port",`Successfully connected to ${_}:${B}`,R),X.end(),N(!0)}),X.once("timeout",()=>{K("port",`Connection to ${_}:${B} timed out`,R),X.destroy(),N(!1)}),X.once("error",(G)=>{K("port",`Failed to connect to ${_}:${B}: ${G.message}`,R),X.destroy(),N(!1)})})}class g{usedPorts=new Set;hostname;verbose;maxRetries;constructor(B="0.0.0.0",_,D=50){this.hostname=B,this.verbose=_,this.maxRetries=D}async getNextAvailablePort(B,_=!1){if(this.usedPorts.has(B))return this.findNextAvailablePort(B+1,_);if(await P(B,this.hostname,this.verbose))return this.findNextAvailablePort(B+1,_);if(_){if(!await m_(B,this.hostname,3000,this.verbose))return K("port",`Port ${B} is available but not connectable, trying next port`,this.verbose),this.findNextAvailablePort(B+1,_)}return this.usedPorts.add(B),B}async findNextAvailablePort(B,_=!1){let D=await v_(B,this.hostname,this.verbose,this.maxRetries);if(_){if(!await m_(D,this.hostname,3000,this.verbose))if(D<B+this.maxRetries)return this.findNextAvailablePort(D+1,_);else throw Error(`Unable to find a connectable port after ${this.maxRetries} attempts`)}return this.usedPorts.add(D),D}releasePort(B){K("port",`Releasing port ${B}`,this.verbose),this.usedPorts.delete(B)}}var i_=new g;import{spawn as n_}from"node:child_process";import*as h from"node:process";class D_{processes=new Map;isShuttingDown=!1;async startProcess(B,_,D){if(this.processes.has(B)){K("start",`Process ${B} is already running`,D);return}let[R,...N]=_.command.split(" "),X=_.cwd||h.cwd();K("start",`Starting process ${B}:`,D),K("start",` Command: ${R} ${N.join(" ")}`,D),K("start",` Working directory: ${X}`,D),K("start",` Environment variables: ${y(_.env)}`,D);let G=n_(R,N,{cwd:X,env:{...h.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(B,{command:_.command,cwd:X,process:G,env:_.env}),new Promise((W,Y)=>{if(G.on("error",(Q)=>{if(!this.isShuttingDown)K("start",`Process ${B} failed to start: ${Q}`,D),this.processes.delete(B),Y(Q),h.emit("SIGINT")}),G.on("exit",(Q)=>{if(!this.isShuttingDown&&Q!==null&&Q!==0)K("start",`Process ${B} exited with code ${Q}`,D),this.processes.delete(B),Y(Error(`Process ${B} exited with code ${Q}`)),h.emit("SIGINT")}),D)G.stdout?.on("data",(Q)=>{K("process",`[${B}] ${Q.toString().trim()}`,!0)}),G.stderr?.on("data",(Q)=>{K("process",`[${B}] ERR: ${Q.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&G.killed)this.processes.delete(B),Y(Error(`Process ${B} was killed during startup`));else K("start",`Process ${B} started successfully`,D),W()},1000)})}async stopProcess(B,_){let D=this.processes.get(B);if(!D?.process){K("start",`No process found for ${B}`,_);return}return K("start",`Stopping process ${B}`,_),new Promise((R)=>{if(!D.process){R();return}D.process.once("exit",()=>{this.processes.delete(B),K("start",`Process ${B} stopped`,_),R()});try{D.process.kill("SIGTERM"),setTimeout(()=>{if(D.process){K("start",`Force killing process ${B}`,_);try{D.process.kill("SIGKILL")}catch(N){}}},3000)}catch(N){K("start",`Error stopping process ${B}: ${N}`,_),this.processes.delete(B),R()}})}async stopAll(B){if(this.isShuttingDown){K("start","Already shutting down, skipping duplicate stopAll call",B);return}this.isShuttingDown=!0,K("start","Stopping all processes",B);let _=Array.from(this.processes.keys()).map((D)=>this.stopProcess(D,B).catch((R)=>{J.error(`Failed to stop process ${D}:`,R)}));await Promise.allSettled(_),this.processes.clear(),this.isShuttingDown=!1}isRunning(B){let _=this.processes.get(B);return!!_?.process&&!_.process.killed}}var uB=new D_;var t_=["/.well-known/acme-challenge/"];function r_(B){let _=B.headers.get("host");if(_)return _.split(":")[0].toLowerCase();try{return new URL(B.url).hostname.toLowerCase()}catch{return""}}function z_(B){let _=B.header.toLowerCase(),D=new Set,R=[];for(let Y of B.hosts){let Q=Y.toLowerCase();if(Q.startsWith("*."))R.push(Q);else D.add(Q)}let N=B.exemptPaths??t_,X=B.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
+ `,G=(Y)=>{let Q=Y.toLowerCase();return D.has(Q)||R.some((I)=>S_(Q,I))},W=(Y)=>{let Q=r_(Y);if(!G(Q))return;let I="/";try{I=new URL(Y.url).pathname}catch{}if(N.some((E)=>I.startsWith(E)))return;if(Y.headers.get(_)===B.value)return;return new Response(X,{status:403,headers:{"content-type":"text/plain"}})};return W.protects=G,W}var R_=new D_,o_="0.12.0",e_=new g("0.0.0.0"),p=new Set,M_=!1,K_=null,T_=null;async function n(B){if(M_)return K("cleanup","Cleanup already in progress, skipping",B?.verbose),T_||Promise.resolve();M_=!0,K("cleanup","Starting cleanup process",B?.verbose),T_=new Promise((_)=>{K_=_});try{await R_.stopAll(B?.verbose),J.info("Shutting down proxy servers...");let _=[],D=Array.from(p).map((R)=>new Promise((N)=>{R.close(()=>{K("cleanup","Server closed successfully",B?.verbose),N()})}));if(_.push(...D),B?.hosts&&B.domains?.length){K("cleanup","Cleaning up hosts file entries",B?.verbose),K("cleanup",`Original domains for cleanup: ${JSON.stringify(B.domains)}`,B?.verbose);let R=B.domains.filter((N)=>{if(N==="test.local")return!0;return N!=="localhost"&&!N.startsWith("localhost.")&&N!=="127.0.0.1"});if(K("cleanup",`Filtered domains for cleanup: ${JSON.stringify(R)}`,B?.verbose),R.length>0)J.info("Cleaning up hosts file entries..."),_.push(A_(R,B?.verbose).then(()=>{K("cleanup",`Removed hosts entries for ${R.join(", ")}`,B?.verbose)}).catch((N)=>{K("cleanup",`Failed to remove hosts entries: ${N}`,B?.verbose),J.warn(`Failed to clean up hosts file entries for ${R.join(", ")}:`,N)}))}if(B?.certs&&B.domains?.length){K("cleanup","Cleaning up SSL certificates",B?.verbose),J.info("Cleaning up SSL certificates...");let R=B.domains.map(async(N)=>{try{await j_(N,B?.verbose),K("cleanup",`Removed certificates for ${N}`,B?.verbose)}catch(X){K("cleanup",`Failed to remove certificates for ${N}: ${X}`,B?.verbose),J.warn(`Failed to clean up certificates for ${N}:`,X)}});_.push(...R)}await Promise.allSettled(_),K("cleanup","All cleanup tasks completed successfully",B?.verbose),J.success("All cleanup tasks completed successfully")}catch(_){K("cleanup",`Error during cleanup: ${_}`,B?.verbose),J.error("Error during cleanup:",_)}finally{if(K_)K_();K_=null,M_=!1;let _=B&&"vitePluginUsage"in B&&B.vitePluginUsage===!0;if(j.env.NODE_ENV!=="test"&&j.env.BUN_ENV!=="test"&&!_)j.exit(0)}return T_}var F_=!1;function q_(B){if(F_){K("signal",`Received second ${B} signal, forcing exit`,!0),j.exit(1);return}F_=!0,K("signal",`Received ${B} signal, initiating cleanup`,!0),n().catch((_)=>{K("signal",`Cleanup failed after ${B}: ${_}`,!0),j.exit(1)}).finally(()=>{F_=!1})}j.once("SIGINT",()=>q_("SIGINT"));j.once("SIGTERM",()=>q_("SIGTERM"));j.on("uncaughtException",(B)=>{K("process",`Uncaught exception: ${B}`,!0),J.error("Uncaught exception:",B),q_("uncaughtException")});async function i(B,_,D,R=5){K("connection",`Testing connection to ${B}:${_} (retries left: ${R})`,D);let N=15000,X=Date.now();if(j.env.RPX_BYPASS_CONNECTION_TEST==="true"){K("connection",`Bypassing connection test for ${B}:${_} due to RPX_BYPASS_CONNECTION_TEST flag`,D);return}let G=()=>new Promise((W,Y)=>{let Q=a_.connect({host:B,port:_,timeout:3000});Q.once("connect",()=>{K("connection",`Successfully connected to ${B}:${_}`,D),Q.end(),W()}),Q.once("timeout",()=>{K("connection",`Connection to ${B}:${_} timed out`,D),Q.destroy(),Y(Error("Connection timed out"))}),Q.once("error",(I)=>{K("connection",`Failed to connect to ${B}:${_}: ${I}`,D),Q.destroy(),Y(I)})});try{await G()}catch(W){if(Date.now()-X>N){K("connection",`Connection test timed out after ${N}ms, but continuing anyway`,D),J.warn(`Connection test to ${B}:${_} timed out, but RPX will try to proceed anyway.`);return}if(W.code==="ECONNREFUSED"&&R>0)return K("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${R} retries left)`,D),await new Promise((Q)=>setTimeout(Q,2000)),i(B,_,D,R-1);if(R>0)try{K("connection",`Trying HTTP request to ${B}:${_}`,D),await new Promise((Q,I)=>{let E=u.request({hostname:B,port:_,path:"/",method:"HEAD",timeout:5000},(S)=>{K("connection",`Received HTTP response with status: ${S.statusCode}`,D),Q()});E.on("error",(S)=>I(S)),E.on("timeout",()=>{E.destroy(),I(Error("HTTP request timed out"))}),E.end()}),K("connection",`HTTP request to ${B}:${_} succeeded`,D);return}catch(Q){return K("connection",`HTTP request to ${B}:${_} failed: ${Q}`,D),K("connection",`Retrying socket connection in 2 seconds... (${R} retries left)`,D),await new Promise((I)=>setTimeout(I,2000)),i(B,_,D,R-1)}let Y=`Failed to connect to ${B}:${_} after ${5-R} attempts: ${W.message}`;K("connection",`${Y}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,D),J.warn(Y),J.warn("RPX will try to continue anyway. If you're sure this is correct, you can set RPX_BYPASS_CONNECTION_TEST=true to skip this check.")}}async function y_(B){K("server",`Starting server with options: ${y(B)}`,B.verbose);let _=new URL((B.from?.startsWith("http")?B.from:`http://${B.from}`)||"localhost:5173"),D=new URL((B.to?.startsWith("http")?B.to:`http://${B.to}`)||"rpx.localhost"),R=Number.parseInt(_.port)||(_.protocol.includes("https:")?443:80),N=[D.hostname];if(U_(B)&&!D.hostname.includes("localhost")&&!D.hostname.includes("127.0.0.1")){K("hosts",`Checking if hosts file entry exists for: ${D.hostname}`,B?.verbose);try{if(!(await b(N,B.verbose))[0]){J.info(`Adding ${D.hostname} to hosts file...`),J.info("This may require sudo/administrator privileges");try{await v(N,B.verbose)}catch(W){if(J.error("Failed to add hosts entry:",W.message),J.warn("You can manually add this entry to your hosts file:"),J.warn(`127.0.0.1 ${D.hostname}`),J.warn(`::1 ${D.hostname}`),j.platform==="win32")J.warn("On Windows:"),J.warn("1. Run notepad as administrator"),J.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else J.warn("On Unix systems:"),J.warn("sudo nano /etc/hosts")}}else K("hosts",`Host entry already exists for ${D.hostname}`,B.verbose)}catch(G){J.error("Failed to check hosts file:",G.message)}}try{await i(_.hostname,R,B.verbose)}catch(G){K("server",`Connection test failed: ${G}`,B.verbose),J.error(G.message),J.warn("Continuing with proxy setup despite connection test failure..."),J.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let X=B._cachedSSLConfig||null;if(B.https)try{if(B.https===!0)B.https=$_({...B,to:D.hostname});if(X=await l({...B,to:D.hostname,https:B.https}),!X){if(K("ssl",`Generating new certificates for ${D.hostname}`,B.verbose),await Y_({...B,from:_.toString(),to:D.hostname,https:B.https}),X=await l({...B,to:D.hostname,https:B.https}),!X)throw Error(`Failed to load SSL configuration after generating certificates for ${D.hostname}`)}}catch(G){throw K("server",`SSL setup failed: ${G}`,B.verbose),G}K("server",`Setting up reverse proxy with SSL config for ${D.hostname}`,B.verbose),await B2({...B,from:B.from||"localhost:5173",to:D.hostname,fromPort:R,sourceUrl:{hostname:_.hostname,host:_.host},ssl:X})}async function _2(B,_,D,R,N,X,G,W,Y,Q,I){K("proxy",`Creating proxy server ${B} -> ${_} with cleanUrls: ${Q}`,Y);function E(A){let Z={};for(let[V,z]of Object.entries(A))if(!V.startsWith(":"))Z[V]=z;return Z}let S=(A,Z)=>{K("request",`Incoming request: ${A.method} ${A.url}`,Y);let V=A.url||"/",z=A.method||"GET";if(A instanceof l_.Http2ServerRequest){let T=A.headers;z=T[":method"]||z,V=T[":path"]||V}if(Q){if(!V.match(/\.[a-z0-9]+$/i))if(V.endsWith("/"))V=`${V}index.html`;else V=`${V}.html`}let H=E(A.headers);if(I)H.host=`${X.hostname}:${D}`,K("request",`Changed origin: setting host header to ${H.host}`,Y);let k={hostname:X.hostname,port:D,path:V,method:z,headers:H};K("request",`Proxy request options: ${y(k)}`,Y);let L=u.request(k,(T)=>{if(K("response",`Proxy response received with status ${T.statusCode}`,Y),Q&&T.statusCode===404){let M=[];if(V.endsWith(".html"))M.push(V.slice(0,-5));else if(!V.match(/\.[a-z0-9]+$/i))M.push(`${V}.html`);if(!V.endsWith("/"))M.push(`${V}/index.html`);if(M.length>0){K("cleanUrls",`Trying alternative paths: ${M.join(", ")}`,Y);let F=(f)=>{if(f.length===0){Z.writeHead(T.statusCode||404,T.headers),T.pipe(Z);return}let O=f[0],t={...k,path:O},m=u.request(t,(r)=>{if(r.statusCode===200)K("cleanUrls",`Found matching path: ${O}`,Y),Z.writeHead(r.statusCode,r.headers),r.pipe(Z);else F(f.slice(1))});m.on("error",()=>F(f.slice(1))),m.end()};F(M);return}}let U={...T.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};Z.writeHead(T.statusCode||500,U),T.pipe(Z)});L.on("error",(T)=>{K("request",`Proxy request failed: ${T}`,Y),J.error("Proxy request failed:",T),Z.writeHead(502),Z.end(`Proxy Error: ${T.message}`)}),A.pipe(L)};if(K("server",`Creating server with SSL config: ${!!G}`,Y),G)return new Promise((A,Z)=>{try{let V=Bun.serve({port:R,hostname:N,tls:{key:G.key,cert:G.cert,ca:G.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(z){let H=new URL(z.url);K("request",`Bun.serve received: ${z.method} ${H.pathname}`,Y);let k=`http://${X.host}`,L=new URL(H.pathname+H.search,k);try{let T=new Headers(z.headers);if(T.set("host",X.host),I)T.set("origin",k);T.set("x-forwarded-for","127.0.0.1"),T.set("x-forwarded-proto","https"),T.set("x-forwarded-host",_);let U=await fetch(L.toString(),{method:z.method,headers:T,body:z.body,redirect:"manual"}),M=new Headers(U.headers);if(Q&&H.pathname.endsWith(".html")){let F=H.pathname.replace(/\.html$/,"");return new Response(null,{status:301,headers:{Location:F}})}return new Response(U.body,{status:U.status,statusText:U.statusText,headers:M})}catch(T){return K("request",`Proxy error: ${T}`,Y),new Response(`Proxy Error: ${T}`,{status:502})}},error(z){return K("server",`Bun.serve error: ${z}`,Y),new Response(`Server Error: ${z.message}`,{status:500})}});p.add(V),b_({from:B,to:_,vitePluginUsage:W,listenPort:R,ssl:!0,cleanUrls:Q,verbose:Y}),A()}catch(V){Z(V)}});let w=u.createServer(S);function $(A){return p.add(A),new Promise((Z,V)=>{A.listen(R,N,()=>{K("server",`Server listening on port ${R}`,Y),b_({from:B,to:_,vitePluginUsage:W,listenPort:R,ssl:!!G,cleanUrls:Q,verbose:Y}),Z()}),A.on("error",(z)=>{K("server",`Server error: ${z}`,Y),V(z)})})}return $(w)}async function B2(B){K("setup",`Setting up reverse proxy: ${y(B)}`,B.verbose);let{from:_,to:D,fromPort:R,sourceUrl:N,ssl:X,verbose:G,cleanup:W,vitePluginUsage:Y,changeOrigin:Q,cleanUrls:I}=B,E=80,S=443,w="0.0.0.0",$=B.portManager||e_,A=U_(B);try{if(A&&D&&!D.includes("localhost")&&!D.includes("127.0.0.1")){if(!(await b([D],G))[0]){J.warn(`The hostname ${D} isn't in your hosts file. Adding it now...`);try{await v([D],G),J.success(`Added ${D} to your hosts file.`)}catch(k){J.error(`Failed to add ${D} to your hosts file: ${k}`),J.info(`You may need to manually add '127.0.0.1 ${D}' to your /etc/hosts file.`)}}}else if(A&&j.platform!=="darwin"&&D&&D.includes("localhost")&&!D.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await b([D],G))[0]){K("hosts",`${D} not found in hosts file, adding...`,G);try{await v([D],G)}catch(k){K("hosts",`Failed to add ${D} to hosts file: ${k}`,G)}}}if(X&&!$.usedPorts.has(E)){if(!await P(E,w,G))K("setup","Starting HTTP redirect server",G),d_(G),$.usedPorts.add(E);else if(K("setup","Port 80 is in use, skipping HTTP redirect",G),G)J.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let Z=X?S:E,V=await P(Z,w,G),z;if(V){if(K("setup",`Port ${Z} is already in use`,G),G)J.warn(`Port ${Z} is already in use. This may be another instance of rpx or another service.`);if(Z===443){if(z=await $.getNextAvailablePort(3443,!0),K("setup",`Using port ${z} instead of ${Z}`,G),G)J.info(`Using port ${z} instead. Access your site at https://${D}:${z}`)}else if(z=await $.getNextAvailablePort(Z+1000,!0),K("setup",`Using port ${z} instead of ${Z}`,G),G)J.info(`Using port ${z} instead. Access your site at http://${D}:${z}`)}else z=Z,$.usedPorts.add(z),K("setup",`Using standard ${Z===443?"HTTPS":"HTTP"} port ${Z} for ${D}`,G);await _2(_,D,R,z,w,N,X,Y,G,I,Q)}catch(Z){K("setup",`Setup failed: ${Z}`,G),J.error(`Failed to setup reverse proxy: ${Z.message}`),n({domains:[D],hosts:typeof W==="boolean"?W:W?.hosts,certs:typeof W==="boolean"?W:W?.certs,verbose:G,vitePluginUsage:Y})}}function d_(B){K("redirect","Starting HTTP redirect server",B);let _=u.createServer((D,R)=>{let N=D.headers.host||"";K("redirect",`Redirecting request from ${N}${D.url} to HTTPS`,B),R.writeHead(301,{Location:`https://${N}${D.url}`}),R.end()}).listen(80);p.add(_),K("redirect","HTTP redirect server started",B)}function D2(B){let _={...N_,...B};if(K("proxy",`Starting proxy with options: ${y(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){J.error("viaDaemon mode requires both `from` and `to`");return}o({proxies:[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}],verbose:_.verbose}).catch((Y)=>{J.error(`Failed to register with rpx daemon: ${Y.message}`),j.exit(1)});return}let D=_.to||"",R=D.split(".").pop()?.toLowerCase()||"",N=j.platform==="darwin"&&D&&!D.includes("localhost")&&!D.includes("127.0.0.1"),X=["dev","app","page","new","day","foo"],G=["test","localhost","local","example","invalid"];if(N&&X.includes(R)&&_?.verbose)J.warn(`The .${R} TLD may not work reliably for local development`),J.info(` Google owns .${R} with HSTS preloading, which can bypass local DNS`),J.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(N)import("./chunk-9njrhbd4.js").then(({setupDevelopmentDns:Y})=>{Y({domains:[D],verbose:_.verbose}).then((Q)=>{if(Q)Promise.resolve().then(()=>{if(_.verbose)if(G.includes(R))J.success(`DNS server started for .${R} domains`);else J.success(`DNS server started for .${R} domains (hosts file entry also added)`)});else K("dns",`Could not start DNS server - ${D} may not resolve in browser`,_.verbose)})}).catch((Y)=>{K("dns",`Failed to start DNS server: ${Y}`,_.verbose)});let W={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:$_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};K("proxy",`Server options: ${y(W)}`,_.verbose),y_(W).catch((Y)=>{K("proxy",`Failed to start proxy: ${Y}`,_.verbose),J.error(`Failed to start proxy: ${Y.message}`),n({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function K2(B){return B?.verbose||!1}function U_(B){if(B?.hostsManagement===!1)return!1;let _=B?.cleanup;if(_===!1)return!1;if(_&&typeof _==="object"&&_.hosts===!1)return!1;return!0}async function O_(B){let _={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(B)_={..._,...B};let D=K2(_),R=U_(_);if(K("config",`Starting with config: ${y(_,2)}`,D),K("config",`Is multi-proxy? ${"proxies"in _}`,D),K("config",`Hosts management enabled? ${R}`,D),_.viaDaemon){let A="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((Z)=>({id:Z.id,from:Z.from,to:Z.to,path:Z.path,cleanUrls:Z.cleanUrls??_.cleanUrls,changeOrigin:Z.changeOrigin??_.changeOrigin,pathRewrites:Z.pathRewrites})):[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await o({proxies:A,verbose:D});return}if("proxies"in _&&Array.isArray(_.proxies)){K("servers",`Found ${_.proxies.length} proxies in config`,D);for(let $ of _.proxies)if($.start){let A=`${$.from}-${$.to}`;try{K("watch",`Starting command for ${A} with command: ${$.start.command}`,D),J.info(`Starting command for ${A}...`),await R_.startProcess(A,$.start,D);let Z=new URL($.from.startsWith("http")?$.from:`http://${$.from}`),V=Z.hostname||"localhost",z=Number(Z.port)||80;try{await i(V,z,D),K("watch",`Dev server is ready at ${V}:${z}`,D)}catch(H){K("watch",`Connection check failed, but continuing with proxy setup: ${H}`,D),J.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(Z){throw K("watch",`Failed to start command for ${A}: ${Z}`,D),Error(`Failed to start command for ${A}: ${Z}`)}}else K("watch",`No start command for proxy ${$.from} -> ${$.to}`,D)}else if("start"in _&&_.start){K("watch","Found start command in single proxy config",D);let $=`${_.from}-${_.to}`;try{if(_.start)K("watch",`Starting command: ${_.start.command}`,D),await R_.startProcess($,_.start,D);let A=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),Z=A.hostname||"localhost",V=Number(A.port)||80;try{await i(Z,V,D),K("watch",`Dev server is ready at ${Z}:${V}`,D)}catch(z){K("watch",`Connection check failed, but continuing with proxy setup: ${z}`,D),J.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(A){throw K("watch",`Failed to run start command: ${A}`,D),Error(`Failed to run start command: ${A}`)}}else K("watch","No start command found in config",D);let N="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(j.platform!=="win32"&&(_.https||R)){if(!a())try{K("sudo","Pre-acquiring sudo credentials for privileged operations",D),s_("sudo -v",{stdio:"inherit"})}catch{K("sudo","Could not pre-acquire sudo credentials",D)}}if(_.https){let $=await l(_);if(!$){if(K("ssl",`No valid or trusted certificates found for ${N}, generating new ones`,_.verbose),await Y_(_),$=await l(_),!$)throw Error(`Failed to load SSL certificates after generation for ${N}`)}else K("ssl",`Using existing and trusted certificates for ${N}`,_.verbose);_._cachedSSLConfig=$}let X="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map(($)=>({...$,https:_.https,cleanup:_.cleanup,cleanUrls:$.cleanUrls??("cleanUrls"in _?_.cleanUrls:!1),vitePluginUsage:_.vitePluginUsage,changeOrigin:$.changeOrigin??_.changeOrigin,verbose:D,_cachedSSLConfig:_._cachedSSLConfig})):[{from:"from"in _?_.from:"localhost:5173",to:"to"in _?_.to:"rpx.localhost",cleanUrls:"cleanUrls"in _?_.cleanUrls:!1,https:_.https,cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,start:"start"in _?_.start:void 0,changeOrigin:_.changeOrigin,verbose:D,_cachedSSLConfig:_._cachedSSLConfig}],G=X.map(($)=>$.to||"rpx.localhost"),W=_._cachedSSLConfig,Y=G.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],I=["test","localhost","local","example","invalid"],E=[...new Set(Y.map(($)=>$.split(".").pop()?.toLowerCase()))],S=E.filter(($)=>!!$&&Q.includes($));if(S.length>0&&D)J.warn(`The following TLDs may not work reliably for local development: ${S.map(($)=>`.${$}`).join(", ")}`),J.info(" These TLDs have HSTS preloading which can bypass local DNS"),J.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(R&&j.platform==="darwin"&&Y.length>0){let{setupDevelopmentDns:$}=await import("./chunk-9njrhbd4.js");if(await $({domains:Y,verbose:D})){if(D)if(E.every((V)=>!!V&&I.includes(V)))J.success(`DNS server started for ${E.map((V)=>`.${V}`).join(", ")} domains`);else J.success(`DNS server started for ${E.map((V)=>`.${V}`).join(", ")} domains (hosts file entries also added)`)}else K("dns","Could not start DNS server - custom domains may not resolve",D)}let w=async()=>{K("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-9njrhbd4.js");await $({verbose:_.verbose})}catch($){K("cleanup",`Error stopping DNS server: ${$}`,_.verbose)}try{await R_.stopAll(_.verbose)}catch($){K("cleanup",`Error stopping processes: ${$}`,_.verbose)}await n({domains:G,hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose||!1})};if(j.on("SIGINT",w),j.on("SIGTERM",w),j.on("uncaughtException",($)=>{K("process",`Uncaught exception: ${$}`,!0),console.error("Uncaught exception:",$),w()}),W&&X.length>1){K("proxies",`Creating shared HTTPS server for ${X.length} domains`,D);let $=[],A=new Set;for(let M of X){let F=M.to||"rpx.localhost",f=M.cleanUrls||!1,O=M.path,t=C_(O);if(M.static)$.push({host:F,path:O,route:{static:w_(M.static,f),cleanUrls:f,basePath:t}}),K("proxies",`Route: ${F}${O??""} → static ${typeof M.static==="string"?M.static:M.static.dir}`,D);else{let m=new URL(M.from?.startsWith("http")?M.from:`http://${M.from}`);$.push({host:F,path:O,route:{sourceHost:m.host,cleanUrls:f,changeOrigin:M.changeOrigin||!1,pathRewrites:M.pathRewrites,basePath:t}}),K("proxies",`Route: ${F}${O??""} → ${m.host}`,D)}if(A.has(F))continue;if(A.add(F),R&&!k_(F)&&!F.includes("localhost")&&!F.includes("127.0.0.1"))try{if(!(await b([F],D))[0])await v([F],D)}catch{K("hosts",`Could not add hosts entry for ${F}`,D)}}if(!await P(80,"0.0.0.0",D))d_(D);let V=443;if(await P(V,"0.0.0.0",D)){if(K("proxies",`Port ${V} is already in use, cannot start shared proxy`,D),D)J.warn(`Port ${V} is in use. Shared HTTPS proxy cannot start.`);return}let H=x_($),k=E_((M,F)=>f_(H,M,F),D),L=_.originGuard?z_(_.originGuard):null,T=L?(M,F)=>L(M)??k(M,F):k,U=H_(D);try{let M=Bun.serve({port:V,hostname:"0.0.0.0",tls:{key:W.key,cert:W.cert,ca:W.ca,requestCert:!1,rejectUnauthorized:!1},fetch(F,f){return T(F,f)},websocket:U,error(F){return K("server",`Shared proxy server error: ${F}`,D),new Response(`Server Error: ${F.message}`,{status:500})}});p.add(M),K("proxies",`Shared HTTPS proxy listening on port ${V} for ${H.size} domains`,D)}catch(M){K("proxies",`Failed to start shared proxy: ${M}`,D),console.error("Failed to start shared HTTPS proxy:",M),w()}}else for(let $ of X)try{let A=$.to||"rpx.localhost";K("proxy",`Starting proxy for ${A} with SSL config: ${!!W}`,$.verbose),await y_({from:$.from||"localhost:5173",to:A,cleanUrls:$.cleanUrls||!1,https:$.https||!1,cleanup:$.cleanup||!1,vitePluginUsage:$.vitePluginUsage||!1,verbose:$.verbose||!1,_cachedSSLConfig:W,changeOrigin:$.changeOrigin||!1})}catch(A){K("proxies",`Failed to start proxy for ${$.to}: ${A}`,$.verbose),console.error(`Failed to start proxy for ${$.to}:`,A),w()}}function b_(B){if(B?.vitePluginUsage||!B?.verbose)return;if(console.log(""),console.log(` ${x.green(x.bold("rpx"))} ${x.green(`v${o_}`)}`),console.log(` ${x.green("➜")} ${x.dim(B?.from??"")} ${x.dim("➜")} ${x.cyan(B?.ssl?`https://${B?.to}`:`http://${B?.to}`)}`),B?.listenPort!==(B?.ssl?443:80))console.log(` ${x.green("➜")} Listening on port ${B?.listenPort}`);if(B?.cleanUrls)console.log(` ${x.green("➜")} Clean URLs enabled`)}var UD=O_;export{J_ as writeEntry,a2 as watchRegistry,G2 as verifyHttpsChain,T2 as trustRootCaForBrowsers,$B as tearDownDevelopmentDns,YB as syncDevelopmentDnsFromRegistry,q2 as stripBasePath,e2 as stopDnsServer,MB as stopDaemon,y_ as startServer,D2 as startProxy,O_ as startProxies,o2 as startDnsServer,KB as setupResolver,NB as setupDevelopmentDns,P2 as serverNameFromCertFilename,f2 as serveStaticFile,y as safeStringify,C2 as safeRelativePath,aD as safeDeleteFile,o as runViaDaemon,SB as runDaemon,BB as resolverFilePath,n2 as resolverBasenamesForDomains,i2 as resolverBasenameForDomain,w_ as resolveStaticRoute,x2 as resolveStaticFile,lD as resolvePathRewrite,GB as removeResolver,RB as removeLegacyTldResolvers,A_ as removeHosts,Q_ as removeEntry,AB as releaseDaemonLock,PD as redactSensitive,XB as reconcileStaleDevelopmentDns,TB as reconcileDevelopmentDnsOnIdle,v2 as readEntry,ZB as readDaemonPid,N2 as readCertSha256Fingerprint,Y2 as readCertCommonName,b2 as readAll,S2 as pruneStaleRootCas,i_ as portManager,U2 as pathPrefixMatches,X2 as parseSha256HashesFromSecurityListing,R2 as normalizeSha256Fingerprint,C_ as normalizePathPrefix,p2 as normalizeDevDomain,S_ as matchesWildcard,c2 as matchesAllowedSuffix,f_ as matchHostRoute,O2 as matchHostList,y2 as matchHost,j2 as loadSSLConfig,A2 as listCertSha256HashesByCommonName,k_ as isWildcardPattern,cD as isValidRootCA,X_ as isValidId,vD as isSingleProxyOptions,bD as isSingleProxyConfig,z2 as isRootCaTrustedForSsl,M2 as isRootCaFingerprintInKeychains,P as isPortInUse,m2 as isPidAlive,mD as isMultiProxyOptions,uD as isMultiProxyConfig,h2 as isLikelyHostname,_B as isDnsServerRunning,VB as isDaemonRunning,H2 as isCertTrusted,$_ as httpsConfig,a as getSudoPassword,I2 as getSharedDaemonCertPaths,F2 as getRootCAPaths,G_ as getRegistryDir,hD as getPrimaryDomain,W2 as getMacosTrustKeychains,V2 as getMacosLoginKeychainPath,JB as getDaemonRpxDir,QB as getDaemonPidPath,Y_ as generateCertificate,l2 as gcStaleEntries,w2 as forceTrustCertificate,v_ as findAvailablePort,LD as extractHostname,OD as execSudoSync,Z_ as ensureDaemonRunning,t2 as devDomainsFromHosts,c_ as deriveIdFromTarget,zB as defaultDaemonSpawnCommand,N_ as defaultConfig,UD as default,K as debugLog,H_ as createProxyWebSocketHandler,E_ as createProxyFetchHandler,z_ as createOriginGuard,k2 as contentTypeFor,DB as contentLooksLikeRpxResolver,N_ as config,x as colors,E2 as clearSslConfigCache,j_ as cleanupCertificates,n as cleanup,b as checkHosts,l as checkExistingCertificates,$2 as certIncludesSanHostnames,L2 as buildSniTlsConfig,x_ as buildHostRoutes,v as addHosts,WB as acquireDaemonLock,Z2 as RPX_ROOT_CA_COMMON_NAME,s2 as RPX_RESOLVER_MARKER,u2 as OnDemandCertManager,Q2 as MACOS_SYSTEM_KEYCHAIN,J2 as MACOS_CA_TRUST_FLAGS,g2 as LEGACY_TLD_RESOLVER_LABELS,g as DefaultPortManager,d2 as DNS_STATE_VERSION,r2 as DNS_PORT};
@@ -0,0 +1,12 @@
1
+ export declare function createOriginGuard(options: OriginGuardOptions): OriginGuard;
2
+ export declare interface OriginGuardOptions {
3
+ header: string
4
+ value: string
5
+ hosts: string[]
6
+ exemptPaths?: string[]
7
+ forbiddenMessage?: string
8
+ }
9
+ export declare interface OriginGuard {
10
+ (req: Request): Response | undefined
11
+ protects: (hostname: string) => boolean
12
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OriginGuardOptions } from './origin-guard';
1
2
  import type { TlsConfig, TlsOption } from '@stacksjs/tlsx';
2
3
  export type { TlsConfig, TlsOption };
3
4
  export declare interface StartOptions {
@@ -92,6 +93,7 @@ export declare interface SharedProxyConfig {
92
93
  hostsManagement?: boolean
93
94
  productionCerts?: ProductionTlsConfig
94
95
  onDemandTls?: OnDemandTlsConfig
96
+ originGuard?: OriginGuardOptions
95
97
  }
96
98
  export declare interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
97
99
  export declare interface MultiProxyConfig extends SharedProxyConfig {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/rpx",
3
3
  "type": "module",
4
- "version": "0.11.16",
4
+ "version": "0.11.18",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
package/src/https.ts CHANGED
@@ -783,8 +783,8 @@ export async function isCertTrusted(
783
783
  // Different check methods per platform
784
784
  if (process.platform === 'darwin') {
785
785
  if (options?.serverName)
786
- return isRootCaTrustedForSsl(certPath, options.serverName, { verbose: options.verbose })
787
- return isRootCaFingerprintInKeychains(certPath, { verbose: options.verbose })
786
+ return isRootCaTrustedForSsl(certPath, options.serverName, { verbose: options?.verbose })
787
+ return isRootCaFingerprintInKeychains(certPath, { verbose: options?.verbose })
788
788
  }
789
789
  else if (process.platform === 'win32') {
790
790
  // On Windows, use PowerShell to check the certificate store
package/src/index.ts CHANGED
@@ -117,6 +117,9 @@ export type { GetRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './pro
117
117
 
118
118
  export { isWildcardPattern, matchesWildcard, matchHost } from './host-match'
119
119
 
120
+ export { createOriginGuard } from './origin-guard'
121
+ export type { OriginGuard, OriginGuardOptions } from './origin-guard'
122
+
120
123
  export {
121
124
  buildHostRoutes,
122
125
  matchHostList,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Origin verification guard for "CDN in front of rpx" topologies.
3
+ *
4
+ * When rpx is the origin behind a CDN (e.g. CloudFront → rpx), the origin host
5
+ * is publicly resolvable, so a client could resolve it and hit rpx directly,
6
+ * bypassing the CDN's caching/WAF. The standard mitigation is a shared secret:
7
+ * the CDN injects a secret request header on the origin fetch, and the origin
8
+ * rejects any request to the protected hosts that lacks it.
9
+ *
10
+ * `createOriginGuard` returns a tiny pre-router gate you place in front of your
11
+ * fetch handler. It only guards the listed hosts (exact or `*.wildcard`) — every
12
+ * other host (e.g. apps served directly, not via the CDN) passes through
13
+ * untouched. ACME HTTP-01 challenge paths are exempt by default so cert renewal
14
+ * keeps working on the open `:80` listener.
15
+ *
16
+ * @example
17
+ * const guard = createOriginGuard({
18
+ * header: 'x-origin-verify',
19
+ * value: process.env.ORIGIN_SECRET!,
20
+ * hosts: ['stacksjs.com', 'www.stacksjs.com', 'origin.stacksjs.com'],
21
+ * })
22
+ * Bun.serve({ fetch: req => guard(req) ?? handler(req, server) })
23
+ */
24
+ import { matchesWildcard } from './host-match'
25
+
26
+ export interface OriginGuardOptions {
27
+ /** Header the CDN injects on the origin hop (case-insensitive), e.g. `x-origin-verify`. */
28
+ header: string
29
+ /** Expected secret value. Requests to protected hosts must carry `header: value`. */
30
+ value: string
31
+ /** Hosts to protect — exact (`stacksjs.com`) or wildcard (`*.stacksjs.com`). Others pass through. */
32
+ hosts: string[]
33
+ /**
34
+ * Request paths exempt from the check (prefix match). Defaults to the ACME
35
+ * HTTP-01 challenge prefix so cert issuance/renewal is never blocked.
36
+ */
37
+ exemptPaths?: string[]
38
+ /** Body returned on rejection. Defaults to a short plain-text message. */
39
+ forbiddenMessage?: string
40
+ }
41
+
42
+ export interface OriginGuard {
43
+ /** Returns a 403 Response to short-circuit, or `undefined` to let the request proceed. */
44
+ (req: Request): Response | undefined
45
+ /** Whether a given hostname is in the protected set. */
46
+ protects: (hostname: string) => boolean
47
+ }
48
+
49
+ const DEFAULT_EXEMPT = ['/.well-known/acme-challenge/']
50
+
51
+ function hostnameOf(req: Request): string {
52
+ const hostHeader = req.headers.get('host')
53
+ if (hostHeader)
54
+ return hostHeader.split(':')[0].toLowerCase()
55
+ try {
56
+ return new URL(req.url).hostname.toLowerCase()
57
+ }
58
+ catch {
59
+ return ''
60
+ }
61
+ }
62
+
63
+ export function createOriginGuard(options: OriginGuardOptions): OriginGuard {
64
+ const header = options.header.toLowerCase()
65
+ const exact = new Set<string>()
66
+ const wildcards: string[] = []
67
+ for (const h of options.hosts) {
68
+ const host = h.toLowerCase()
69
+ if (host.startsWith('*.'))
70
+ wildcards.push(host)
71
+ else
72
+ exact.add(host)
73
+ }
74
+ const exemptPaths = options.exemptPaths ?? DEFAULT_EXEMPT
75
+ const forbidden = options.forbiddenMessage
76
+ ?? 'Forbidden: direct origin access is not allowed; requests must arrive via the CDN.\n'
77
+
78
+ const protects = (hostname: string): boolean => {
79
+ const h = hostname.toLowerCase()
80
+ return exact.has(h) || wildcards.some(w => matchesWildcard(h, w))
81
+ }
82
+
83
+ const guard: OriginGuard = ((req: Request): Response | undefined => {
84
+ const host = hostnameOf(req)
85
+ if (!protects(host))
86
+ return undefined
87
+
88
+ let pathname = '/'
89
+ try {
90
+ pathname = new URL(req.url).pathname
91
+ }
92
+ catch {
93
+ // fall through with '/'
94
+ }
95
+ if (exemptPaths.some(p => pathname.startsWith(p)))
96
+ return undefined
97
+
98
+ if (req.headers.get(header) === options.value)
99
+ return undefined
100
+
101
+ return new Response(forbidden, { status: 403, headers: { 'content-type': 'text/plain' } })
102
+ }) as OriginGuard
103
+ guard.protects = protects
104
+ return guard
105
+ }
package/src/start.ts CHANGED
@@ -20,6 +20,7 @@ import { addHosts, checkHosts, removeHosts } from './hosts'
20
20
  import { checkExistingCertificates, cleanupCertificates, generateCertificate, httpsConfig, loadSSLConfig } from './https'
21
21
  import { DefaultPortManager, findAvailablePort, isPortInUse } from './port-manager'
22
22
  import { ProcessManager } from './process-manager'
23
+ import { createOriginGuard } from './origin-guard'
23
24
  import { createProxyFetchHandler, createProxyWebSocketHandler } from './proxy-handler'
24
25
  import type { ProxyRoute, ProxyServer as ProxyServerLike } from './proxy-handler'
25
26
  import { isWildcardPattern } from './host-match'
@@ -1323,10 +1324,16 @@ export async function startProxies(options?: ProxyOptions): Promise<void> {
1323
1324
  }
1324
1325
 
1325
1326
  const routingTable = buildHostRoutes(routeEntries)
1326
- const sharedFetchHandler = createProxyFetchHandler(
1327
+ const baseFetchHandler = createProxyFetchHandler(
1327
1328
  (host, pathname) => matchHostRoute(routingTable, host, pathname),
1328
1329
  verbose,
1329
1330
  )
1331
+ // Origin lockdown: when a CDN fronts this gateway, reject direct hits to the
1332
+ // fronted hosts that lack the CDN's shared-secret header (the CDN injects it).
1333
+ const originGuard = mergedOptions.originGuard ? createOriginGuard(mergedOptions.originGuard) : null
1334
+ const sharedFetchHandler = originGuard
1335
+ ? (req: Request, server: ProxyServerLike) => originGuard(req) ?? baseFetchHandler(req, server)
1336
+ : baseFetchHandler
1330
1337
  const sharedWsHandler = createProxyWebSocketHandler(verbose)
1331
1338
 
1332
1339
  try {
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { TlsConfig, TlsOption } from '@stacksjs/tlsx'
2
+ import type { OriginGuardOptions } from './origin-guard'
2
3
 
3
4
  export interface StartOptions {
4
5
  command: string
@@ -208,6 +209,13 @@ export interface SharedProxyConfig {
208
209
  * the first time it's needed. Opt-in — see {@link OnDemandTlsConfig}.
209
210
  */
210
211
  onDemandTls?: OnDemandTlsConfig
212
+ /**
213
+ * Origin lockdown for "CDN in front of rpx" setups. When set, the shared
214
+ * HTTPS handler rejects requests to the listed hosts that lack the CDN's
215
+ * shared-secret header — so the publicly-resolvable origin can't be used to
216
+ * bypass the CDN. See {@link createOriginGuard}.
217
+ */
218
+ originGuard?: OriginGuardOptions
211
219
  }
212
220
 
213
221
  export type SharedProxyOptions = Partial<SharedProxyConfig>