@stacksjs/rpx 0.11.23 → 0.11.24

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.
@@ -1 +1 @@
1
- import{ib as a,jb as b,kb as c,lb as d,mb as e,nb as f,ob as g,pb as h,qb as i,rb as j,sb as k,tb as l,ub as m,vb as n,wb as o,xb as p}from"./chunk-prvgnh82.js";export{e as shouldReusePort,g as safeStringify,p as safeDeleteFile,o as resolvePathRewrite,f as redactSensitive,i as isValidRootCA,m as isSingleProxyOptions,n as isSingleProxyConfig,b as isProcessElevated,l as isMultiProxyOptions,k as isMultiProxyConfig,a as getSudoPassword,j as getPrimaryDomain,h as extractHostname,c as execSudoSync,d as debugLog};
1
+ import{Ab as l,Bb as m,Cb as n,Db as o,Eb as p,pb as a,qb as b,rb as c,sb as d,tb as e,ub as f,vb as g,wb as h,xb as i,yb as j,zb as k}from"./chunk-2yk7jygq.js";export{e as shouldReusePort,g as safeStringify,p as safeDeleteFile,o as resolvePathRewrite,f as redactSensitive,i as isValidRootCA,m as isSingleProxyOptions,n as isSingleProxyConfig,b as isProcessElevated,l as isMultiProxyOptions,k as isMultiProxyConfig,a as getSudoPassword,j as getPrimaryDomain,h as extractHostname,c as execSudoSync,d as debugLog};
@@ -1 +1 @@
1
- import{Ja as a,Ka as b,La as c,Ma as d,Na as e,Oa as f,Pa as g,Qa as h,Ra as i,Sa as j,Ta as k,Ua as l,Va as m,Wa as n,Xa as o}from"./chunk-vy2pj4at.js";import"./chunk-prvgnh82.js";export{m as tearDownDevelopmentDns,l as syncDevelopmentDnsFromRegistry,d as stopDnsServer,c as startDnsServer,i as setupResolver,k as setupDevelopmentDns,g as resolverFilePath,n as removeResolver,j as removeLegacyTldResolvers,o as reconcileStaleDevelopmentDns,f as isRpxDevelopmentDnsAnswering,e as isDnsServerRunning,h as contentLooksLikeRpxResolver,b as RPX_RESOLVER_MARKER,a as DNS_PORT};
1
+ import{$a as l,Qa as a,Ra as b,Sa as c,Ta as d,Ua as e,Va as f,Wa as g,Xa as h,Ya as i,Za as j,_a as k,ab as m,bb as n,cb as o}from"./chunk-6m0f5m3q.js";import"./chunk-2yk7jygq.js";export{m as tearDownDevelopmentDns,l as syncDevelopmentDnsFromRegistry,d as stopDnsServer,c as startDnsServer,i as setupResolver,k as setupDevelopmentDns,g as resolverFilePath,n as removeResolver,j as removeLegacyTldResolvers,o as reconcileStaleDevelopmentDns,f as isRpxDevelopmentDnsAnswering,e as isDnsServerRunning,h as contentLooksLikeRpxResolver,b as RPX_RESOLVER_MARKER,a as DNS_PORT};
@@ -1,5 +1,5 @@
1
1
  import * as path from 'node:path';
2
- import type { PathRewrite, StaticRouteConfig } from './types';
2
+ import type { LoadBalancerConfig, PathRewrite, ProxyFrom, StaticRouteConfig } from './types';
3
3
  /**
4
4
  * Sanitize an arbitrary `to` host into a valid registry id. Drops anything
5
5
  * that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
@@ -14,13 +14,14 @@ export declare function deriveIdFromTarget(to: string, routePath?: string): stri
14
14
  export declare function runViaDaemon(opts: DaemonRunnerOptions): Promise<void>;
15
15
  export declare interface DaemonRunnerProxy {
16
16
  id?: string
17
- from?: string
17
+ from?: ProxyFrom
18
18
  to: string
19
19
  path?: string
20
20
  cleanUrls?: boolean
21
21
  changeOrigin?: boolean
22
22
  pathRewrites?: PathRewrite[]
23
23
  static?: string | StaticRouteConfig
24
+ loadBalancer?: LoadBalancerConfig
24
25
  }
25
26
  export declare interface DaemonRunnerOptions {
26
27
  proxies: DaemonRunnerProxy[]
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type {
10
10
  StopDaemonResult,
11
11
  } from './daemon';
12
12
  export type { GetRoute, NoRouteOutcome, OnNoRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './proxy-handler';
13
+ export type { UpstreamPool, UpstreamState } from './load-balancer';
13
14
  export type { RedirectRouteConfig, ResolvedRedirect } from './redirect';
14
15
  export type { OriginGuard, OriginGuardOptions } from './origin-guard';
15
16
  export type { ResolvedAuth } from './auth';
@@ -126,6 +127,15 @@ export {
126
127
  stopDaemon,
127
128
  } from './daemon';
128
129
  export { createProxyFetchHandler, createProxyWebSocketHandler, stripBasePath } from './proxy-handler';
130
+ export {
131
+ createUpstreamPool,
132
+ markFailure,
133
+ markSuccess,
134
+ primaryUpstreamUrl,
135
+ selectUpstream,
136
+ startHealthChecks,
137
+ stopHealthChecks,
138
+ } from './load-balancer';
129
139
  export { buildRedirectLocation, resolveRedirect } from './redirect';
130
140
  export { ACME_CHALLENGE_PREFIX, readAcmeChallenge } from './acme-challenge';
131
141
  export { isWildcardPattern, matchesWildcard, matchHost } from './host-match';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import{$ as wR,$a as LS,A as C_,Aa as YS,B as U,Ba as $S,C as o,Ca as GS,D as WR,Da as XS,E as y_,Ea as JS,F as f_,Fa as QS,G as RR,Ga as VS,H as U_,Ha as ZS,I as zR,Ia as IS,J as P_,Ja as WS,K as MR,Ka as zS,L as L_,La as MS,M as O_,Ma as FS,N as c_,Na as TS,O as h_,P as m_,Pa as jS,Q as FR,Qa as xS,R as TR,Ra as HS,S as v_,Sa as kS,T as jR,Ta as wS,U as xR,Ua as ES,V as GR,Va as qS,W as u_,Wa as CS,X as HR,Xa as yS,Y as a_,Ya as fS,Z as kR,Za as US,_ as l_,_a as PS,a as G,aa as b_,ab as OS,b as s,ba as ER,bb as cS,c as B_,ca as p_,cb as hS,d as K_,da as d_,db as mS,e as Y_,ea as g_,eb as BR,f as $_,fa as i_,fb as vS,g as G_,ga as n_,gb as uS,h as X_,ha as r_,i as J_,ia as t_,j as Q_,ja as s_,k as V_,ka as e_,l as Z_,la as o_,m as I_,ma as C,n as W_,na as RS,o as z_,oa as qR,p as M_,pa as _S,q as F_,qa as SR,r as T_,ra as DR,s as j_,sa as SS,t as x_,ta as NR,u as H_,ua as AR,v as k_,va as DS,w as w_,wa as NS,x as E_,xa as AS,y as q_,ya as BS,z as e,za as KS}from"./chunk-vy2pj4at.js";import{hb as ZR,ib as h,jb as cR,kb as $N,lb as D,mb as vR,nb as GN,ob as E,pb as XN,qb as JN,rb as QN,sb as VN,tb as ZN,ub as IN,vb as WN,wb as zN,xb as MN}from"./chunk-prvgnh82.js";import{execSync as eR}from"node:child_process";import*as _R from"node:http";import*as uR from"node:net";import*as W from"node:process";var l=(_,R)=>(S)=>`\x1B[${_}m${S}\x1B[${R}m`,H={bold:l(1,22),dim:l(2,22),green:l(32,39),cyan:l(36,39)};import*as UR from"node:fs";import*as PR from"node:path";import*as k from"node:process";function LR(_,R){let A=(R&&R!=="/"?`${_}${R}`:_).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return A.length>0?A:"rpx"}async function b(_){if(_.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let R=_.verbose??!1,S=_.registryDir,A=new Set,B=_.proxies.map((V)=>{let I=V.id??LR(V.to,V.path);if(!DR(I))throw Error(`invalid registry id "${I}" derived from to="${V.to}"`);if(A.has(I))throw Error(`duplicate registry id "${I}" — set an explicit \`id\` on one of the proxies`);return A.add(I),{...V,id:I}}),N=new Date().toISOString();for(let V of B)await NR({id:V.id,from:V.from,to:V.to,path:V.path,pid:_.persistent?void 0:k.pid,cwd:k.cwd(),createdAt:N,cleanUrls:V.cleanUrls,changeOrigin:V.changeOrigin,pathRewrites:V.pathRewrites,static:V.static},S,R);let K=await BR({rpxDir:_.rpxDir,verbose:R,spawnCommand:_.spawnCommand,startupTimeoutMs:_.startupTimeoutMs,spawnEnv:_.spawnEnv});for(let V of B){let I=V.static?`static ${typeof V.static==="string"?V.static:V.static.dir}`:V.from;G.success(`https://${V.to} → ${I}`)}if(G.info(`(via rpx daemon pid=${K.pid}; \`rpx daemon:status\` to inspect)`),_.detached)return;let Q=!1,Y=S??SR(),X=B.map((V)=>V.id),J=async()=>{if(Q)return;Q=!0;for(let V of X)await AR(V,S,R).catch((I)=>{D("runner",`removeEntry(${V}) failed: ${I}`,R)})},Z=(V)=>{D("runner",`received ${V}, unregistering ${X.length} entries`,R),J().finally(()=>k.exit(0))};k.once("SIGINT",Z),k.once("SIGTERM",Z),k.once("exit",()=>{if(Q)return;for(let V of X)try{UR.unlinkSync(PR.join(Y,`${V}.json`))}catch{}}),await new Promise(()=>{})}import{exec as dR}from"node:child_process";import q from"node:fs";import hR from"node:os";import YR from"node:path";import*as m from"node:process";import{promisify as gR}from"node:util";var c=gR(dR);function OR(_){let R=_.trim().toLowerCase();return R==="localhost"||R.endsWith(".localhost")||R.endsWith(".localhost.")}var x=m.platform==="win32"?YR.join(m.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",KR=!1;async function p(_){if(m.platform==="win32")throw Error("Administrator privileges required on Windows");if(cR()){let{stdout:A}=await c(_);return A}let R=h(),S=_.replace(/'/g,"'\\''");try{if(R){let{stdout:A}=await c(`echo '${R}' | sudo -S sh -c '${S}' 2>/dev/null`);return KR=!0,A}if(KR)try{let{stdout:A}=await c(`sudo -n sh -c '${S}'`);return A}catch(A){D("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:A}=await c(`sudo -n sh -c '${S}'`);return KR=!0,A}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(A){throw Error(`Failed to execute sudo command: ${A.message}`)}}async function y(_,R){let S=_.filter((B)=>!OR(B)),A=_.filter((B)=>OR(B));if(A.length>0)D("hosts",`Skipping /etc/hosts for loopback dev names: ${A.join(", ")}`,R);if(S.length===0)return;D("hosts",`Adding hosts: ${S.join(", ")}`,R),D("hosts",`Using hosts file at: ${x}`,R);try{let B;try{B=await q.promises.readFile(x,"utf-8")}catch{D("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{B=await p(`cat "${x}"`)}catch(Y){throw console.log(" Could not read hosts file — skipping hosts setup"),D("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let N=S.filter((Y)=>{let X=`127.0.0.1 ${Y}`,J=`::1 ${Y}`;return!B.includes(X)&&!B.includes(J)});if(N.length===0){D("hosts","All hosts already exist in hosts file",R);return}let K=N.map((Y)=>`
1
+ import{$ as fR,$a as c_,A as OS,Aa as XR,B as P,Ba as JR,C as DR,Ca as J_,D as kR,Da as Q_,E as cS,Ea as V_,F as L,Fa as Z_,G as NR,Ga as I_,H as hS,Ha as W_,I as uS,Ia as z_,J as vS,Ja as M_,K as AR,Ka as x_,L as BR,La as j_,M as mS,Ma as T_,N as KR,Na as H_,O as bS,Oa as k_,P as wR,Pa as w_,Q as aS,Qa as E_,R as ER,Ra as F_,S as lS,Sa as q_,T as dS,Ta as C_,U as pS,Ua as f_,V as gS,W as nS,Wa as y_,X as FR,Xa as U_,Y as qR,Ya as P_,Z as iS,Za as L_,_ as CR,_a as O_,a as $,aa as WR,ab as h_,b as SR,ba as tS,bb as u_,c as JS,ca as yR,cb as v_,d as QS,da as rS,db as m_,e as VS,ea as UR,eb as b_,f as ZS,fa as sS,fb as a_,g as IS,ga as PR,gb as l_,h as WS,ha as eS,hb as d_,i as zS,ia as LR,ib as p_,j as MS,ja as oS,jb as g_,k as xS,ka as R_,kb as n_,l as jS,la as S_,lb as QR,m as TS,ma as __,mb as i_,n as HS,na as D_,nb as t_,o as kS,oa as N_,p as wS,pa as A_,q as ES,qa as B_,r as FS,ra as K_,s as qS,sa as Y_,t as CS,ta as f,u as fS,ua as $_,v as yS,va as OR,w as US,wa as G_,x as PS,xa as $R,y as LS,ya as GR,z as _R,za as X_}from"./chunk-6m0f5m3q.js";import{Ab as EN,Bb as FN,Cb as qN,Db as CN,Eb as fN,ob as TR,pb as v,qb as lR,rb as xN,sb as D,tb as gR,ub as jN,vb as q,wb as TN,xb as HN,yb as kN,zb as wN}from"./chunk-2yk7jygq.js";import{execSync as NS}from"node:child_process";import*as YR from"node:http";import*as nR from"node:net";import*as z from"node:process";var p=(S,R)=>(_)=>`\x1B[${S}m${_}\x1B[${R}m`,k={bold:p(1,22),dim:p(2,22),green:p(32,39),cyan:p(36,39)};import*as vR from"node:fs";import*as mR from"node:path";import*as E from"node:process";function bR(S,R){let N=(R&&R!=="/"?`${S}${R}`:S).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return N.length>0?N:"rpx"}async function g(S){if(S.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let R=S.verbose??!1,_=S.registryDir,N=new Set,K=S.proxies.map((Q)=>{let I=Q.id??bR(Q.to,Q.path);if(!GR(I))throw Error(`invalid registry id "${I}" derived from to="${Q.to}"`);if(N.has(I))throw Error(`duplicate registry id "${I}" — set an explicit \`id\` on one of the proxies`);return N.add(I),{...Q,id:I}}),B=new Date().toISOString();for(let Q of K)await XR({id:Q.id,from:Q.from,to:Q.to,path:Q.path,pid:S.persistent?void 0:E.pid,cwd:E.cwd(),createdAt:B,cleanUrls:Q.cleanUrls,changeOrigin:Q.changeOrigin,pathRewrites:Q.pathRewrites,static:Q.static,loadBalancer:Q.loadBalancer},_,R);let A=await QR({rpxDir:S.rpxDir,verbose:R,spawnCommand:S.spawnCommand,startupTimeoutMs:S.startupTimeoutMs,spawnEnv:S.spawnEnv});for(let Q of K){let I=Q.static?`static ${typeof Q.static==="string"?Q.static:Q.static.dir}`:Q.from;$.success(`https://${Q.to} → ${I}`)}if($.info(`(via rpx daemon pid=${A.pid}; \`rpx daemon:status\` to inspect)`),S.detached)return;let X=!1,Y=_??$R(),J=K.map((Q)=>Q.id),V=async()=>{if(X)return;X=!0;for(let Q of J)await JR(Q,_,R).catch((I)=>{D("runner",`removeEntry(${Q}) failed: ${I}`,R)})},Z=(Q)=>{D("runner",`received ${Q}, unregistering ${J.length} entries`,R),V().finally(()=>E.exit(0))};E.once("SIGINT",Z),E.once("SIGTERM",Z),E.once("exit",()=>{if(X)return;for(let Q of J)try{vR.unlinkSync(mR.join(Y,`${Q}.json`))}catch{}}),await new Promise(()=>{})}import{exec as sR}from"node:child_process";import C from"node:fs";import dR from"node:os";import ZR from"node:path";import*as m from"node:process";import{promisify as eR}from"node:util";var u=eR(sR);function aR(S){let R=S.trim().toLowerCase();return R==="localhost"||R.endsWith(".localhost")||R.endsWith(".localhost.")}var H=m.platform==="win32"?ZR.join(m.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",VR=!1;async function n(S){if(m.platform==="win32")throw Error("Administrator privileges required on Windows");if(lR()){let{stdout:N}=await u(S);return N}let R=v(),_=S.replace(/'/g,"'\\''");try{if(R){let{stdout:N}=await u(`echo '${R}' | sudo -S sh -c '${_}' 2>/dev/null`);return VR=!0,N}if(VR)try{let{stdout:N}=await u(`sudo -n sh -c '${_}'`);return N}catch(N){D("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:N}=await u(`sudo -n sh -c '${_}'`);return VR=!0,N}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(N){throw Error(`Failed to execute sudo command: ${N.message}`)}}async function y(S,R){let _=S.filter((K)=>!aR(K)),N=S.filter((K)=>aR(K));if(N.length>0)D("hosts",`Skipping /etc/hosts for loopback dev names: ${N.join(", ")}`,R);if(_.length===0)return;D("hosts",`Adding hosts: ${_.join(", ")}`,R),D("hosts",`Using hosts file at: ${H}`,R);try{let K;try{K=await C.promises.readFile(H,"utf-8")}catch{D("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{K=await n(`cat "${H}"`)}catch(Y){throw console.log(" Could not read hosts file — skipping hosts setup"),D("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let B=_.filter((Y)=>{let J=`127.0.0.1 ${Y}`,V=`::1 ${Y}`;return!K.includes(J)&&!K.includes(V)});if(B.length===0){D("hosts","All hosts already exist in hosts file",R);return}let A=B.map((Y)=>`
2
2
  # Added by rpx
3
3
  127.0.0.1 ${Y}
4
4
  ::1 ${Y}`).join(`
5
- `),Q=YR.join(hR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await q.promises.writeFile(Q,B+K,"utf8"),await p(`cat "${Q}" | tee "${x}" > /dev/null`),console.log(` Hosts updated: ${N.join(", ")}`)}catch(Y){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),N.forEach((X)=>{console.log(` 127.0.0.1 ${X}`),console.log(` ::1 ${X}`)}),console.log(` Or run: sudo nano ${x}`)}finally{try{await q.promises.unlink(Q)}catch{}}}catch(B){D("hosts",`Failed to manage hosts file: ${B.message}`,R)}}async function $R(_,R){D("hosts",`Removing hosts: ${_.join(", ")}`,R);try{let S;try{S=await q.promises.readFile(x,"utf-8")}catch{D("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{S=await p(`cat "${x}"`)}catch(Y){throw D("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let A=S.split(`
6
- `),B=!1,N=A.filter((Y)=>{if(_.some((J)=>Y.includes(` ${J}`)&&(Y.includes("127.0.0.1")||Y.includes("::1"))))return B=!0,!1;if(Y.trim()==="# Added by rpx")return B=!0,!1;return!0});if(!B){D("hosts","No matching hosts found to remove",R);return}while(N[N.length-1]?.trim()==="")N.pop();let K=`${N.join(`
5
+ `),X=ZR.join(dR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await C.promises.writeFile(X,K+A,"utf8"),await n(`cat "${X}" | tee "${H}" > /dev/null`),console.log(` Hosts updated: ${B.join(", ")}`)}catch(Y){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),B.forEach((J)=>{console.log(` 127.0.0.1 ${J}`),console.log(` ::1 ${J}`)}),console.log(` Or run: sudo nano ${H}`)}finally{try{await C.promises.unlink(X)}catch{}}}catch(K){D("hosts",`Failed to manage hosts file: ${K.message}`,R)}}async function IR(S,R){D("hosts",`Removing hosts: ${S.join(", ")}`,R);try{let _;try{_=await C.promises.readFile(H,"utf-8")}catch{D("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{_=await n(`cat "${H}"`)}catch(Y){throw D("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let N=_.split(`
6
+ `),K=!1,B=N.filter((Y)=>{if(S.some((V)=>Y.includes(` ${V}`)&&(Y.includes("127.0.0.1")||Y.includes("::1"))))return K=!0,!1;if(Y.trim()==="# Added by rpx")return K=!0,!1;return!0});if(!K){D("hosts","No matching hosts found to remove",R);return}while(B[B.length-1]?.trim()==="")B.pop();let A=`${B.join(`
7
7
  `)}
8
- `,Q=YR.join(hR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await q.promises.writeFile(Q,K,"utf8"),await p(`cat "${Q}" | tee "${x}" > /dev/null`),D("hosts","Hosts removed successfully",R)}catch(Y){D("hosts","Could not clean up hosts file automatically",R)}finally{try{await q.promises.unlink(Q)}catch(Y){D("hosts",`Failed to remove temporary file: ${Y}`,R)}}}catch(S){D("hosts",`Failed to clean up hosts file: ${S.message}`,R)}}async function f(_,R){D("hosts",`Checking hosts: ${_}`,R);let S;try{S=await q.promises.readFile(x,"utf-8")}catch(A){D("hosts",`Error reading hosts file: ${A}`,R);try{let B=h(),N;if(B)N=`echo '${B}' | sudo -S cat "${x}" 2>/dev/null`;else N=`sudo -n cat "${x}" 2>/dev/null || cat "${x}" 2>/dev/null || echo ""`;let{stdout:K}=await c(N);S=K}catch(B){return D("hosts",`Cannot read hosts file, assuming entries don't exist: ${B}`,R),_.map(()=>!1)}}return _.map((A)=>{let B=`127.0.0.1 ${A}`,N=`::1 ${A}`;return S.includes(B)||S.includes(N)})}import{spawn as iR}from"node:child_process";import*as d from"node:process";class g{processes=new Map;isShuttingDown=!1;async startProcess(_,R,S){if(this.processes.has(_)){D("start",`Process ${_} is already running`,S);return}let[A,...B]=R.command.split(" "),N=R.cwd||d.cwd();D("start",`Starting process ${_}:`,S),D("start",` Command: ${A} ${B.join(" ")}`,S),D("start",` Working directory: ${N}`,S),D("start",` Environment variables: ${E(R.env)}`,S);let K=iR(A,B,{cwd:N,env:{...d.env,...R.env},shell:!0,stdio:"inherit"});return this.processes.set(_,{command:R.command,cwd:N,process:K,env:R.env}),new Promise((Q,Y)=>{if(K.on("error",(X)=>{if(!this.isShuttingDown)D("start",`Process ${_} failed to start: ${X}`,S),this.processes.delete(_),Y(X)}),K.on("exit",(X)=>{if(!this.isShuttingDown&&X!==null&&X!==0)D("start",`Process ${_} exited with code ${X}; leaving the proxy running`,S),this.processes.delete(_),Y(Error(`Process ${_} exited with code ${X}`))}),S)K.stdout?.on("data",(X)=>{D("process",`[${_}] ${X.toString().trim()}`,!0)}),K.stderr?.on("data",(X)=>{D("process",`[${_}] ERR: ${X.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&K.killed)this.processes.delete(_),Y(Error(`Process ${_} was killed during startup`));else D("start",`Process ${_} started successfully`,S),Q()},1000)})}async stopProcess(_,R){let S=this.processes.get(_);if(!S?.process){D("start",`No process found for ${_}`,R);return}return D("start",`Stopping process ${_}`,R),new Promise((A)=>{if(!S.process){A();return}S.process.once("exit",()=>{this.processes.delete(_),D("start",`Process ${_} stopped`,R),A()});try{S.process.kill("SIGTERM"),setTimeout(()=>{if(S.process){D("start",`Force killing process ${_}`,R);try{S.process.kill("SIGKILL")}catch(B){}}},3000)}catch(B){D("start",`Error stopping process ${_}: ${B}`,R),this.processes.delete(_),A()}})}async stopAll(_){if(this.isShuttingDown){D("start","Already shutting down, skipping duplicate stopAll call",_);return}this.isShuttingDown=!0,D("start","Stopping all processes",_);let R=Array.from(this.processes.keys()).map((S)=>this.stopProcess(S,_).catch((A)=>{G.error(`Failed to stop process ${S}:`,A)}));await Promise.allSettled(R),this.processes.clear(),this.isShuttingDown=!1}isRunning(_){let R=this.processes.get(_);return!!R?.process&&!R.process.killed}}var DD=new g;import{timingSafeEqual as nR}from"node:crypto";function rR(_,R){if(_==null)return!1;let S=Buffer.from(_),A=Buffer.from(R);if(S.length!==A.length)return!1;return nR(S,A)}function i(_){return _.toLowerCase().replace(/\.$/,"")}var tR=["/.well-known/acme-challenge/"];function sR(_){let R=_.headers.get("host");if(R)return i(R.split(":")[0]);try{return i(new URL(_.url).hostname)}catch{return""}}function XR(_){let R=_.header.toLowerCase(),S=new Set,A=[];for(let Y of _.hosts){let X=i(Y);if(X.startsWith("*."))A.push(X);else S.add(X)}let B=_.exemptPaths??tR,N=_.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
- `,K=(Y)=>{let X=i(Y);return S.has(X)||A.some((J)=>GR(X,J))},Q=(Y)=>{let X=sR(Y);if(!K(X))return;let J="/";try{J=new URL(Y.url).pathname}catch{}if(B.some((Z)=>J.startsWith(Z)))return;if(rR(Y.headers.get(R),_.value))return;return new Response(N,{status:403,headers:{"content-type":"text/plain"}})};return Q.protects=K,Q}var r=new g,oR="0.12.0",R_=new qR("0.0.0.0"),t=new Set,JR=!1,n=null,QR=null;async function u(_){if(JR)return D("cleanup","Cleanup already in progress, skipping",_?.verbose),QR||Promise.resolve();JR=!0,D("cleanup","Starting cleanup process",_?.verbose),QR=new Promise((R)=>{n=R});try{await r.stopAll(_?.verbose),G.info("Shutting down proxy servers...");let R=[],S=Array.from(t).map((A)=>new Promise((B)=>{let N=A;try{if(typeof N.stop==="function")N.stop(!0),D("cleanup","Bun server stopped",_?.verbose),B();else if(typeof N.close==="function")N.close(()=>{D("cleanup","Server closed successfully",_?.verbose),B()});else B()}catch(K){D("cleanup",`Error stopping server: ${K}`,_?.verbose),B()}}));if(R.push(...S),t.clear(),_?.hosts&&_.domains?.length){D("cleanup","Cleaning up hosts file entries",_?.verbose),D("cleanup",`Original domains for cleanup: ${JSON.stringify(_.domains)}`,_?.verbose);let A=_.domains.filter((B)=>{if(B==="test.local")return!0;return B!=="localhost"&&!B.startsWith("localhost.")&&B!=="127.0.0.1"});if(D("cleanup",`Filtered domains for cleanup: ${JSON.stringify(A)}`,_?.verbose),A.length>0)G.info("Cleaning up hosts file entries..."),R.push($R(A,_?.verbose).then(()=>{D("cleanup",`Removed hosts entries for ${A.join(", ")}`,_?.verbose)}).catch((B)=>{D("cleanup",`Failed to remove hosts entries: ${B}`,_?.verbose),G.warn(`Failed to clean up hosts file entries for ${A.join(", ")}:`,B)}))}if(_?.certs&&_.domains?.length){D("cleanup","Cleaning up SSL certificates",_?.verbose),G.info("Cleaning up SSL certificates...");let A=_.domains.map(async(B)=>{try{await WR(B,_?.verbose),D("cleanup",`Removed certificates for ${B}`,_?.verbose)}catch(N){D("cleanup",`Failed to remove certificates for ${B}: ${N}`,_?.verbose),G.warn(`Failed to clean up certificates for ${B}:`,N)}});R.push(...A)}await Promise.allSettled(R),D("cleanup","All cleanup tasks completed successfully",_?.verbose),G.success("All cleanup tasks completed successfully")}catch(R){D("cleanup",`Error during cleanup: ${R}`,_?.verbose),G.error("Error during cleanup:",R)}finally{if(n)n();n=null,JR=!1;let R=_&&"vitePluginUsage"in _&&_.vitePluginUsage===!0;if(W.env.NODE_ENV!=="test"&&W.env.BUN_ENV!=="test"&&!R)W.exit(0)}return QR}var VR=!1;function aR(_){if(VR){D("signal",`Received second ${_} signal, forcing exit`,!0),W.exit(1);return}VR=!0,D("signal",`Received ${_} signal, initiating cleanup`,!0),u().catch((R)=>{D("signal",`Cleanup failed after ${_}: ${R}`,!0),W.exit(1)}).finally(()=>{VR=!1})}W.once("SIGINT",()=>aR("SIGINT"));W.once("SIGTERM",()=>aR("SIGTERM"));W.on("uncaughtException",(_)=>{G.error("Uncaught exception (continuing):",_)});W.on("unhandledRejection",(_)=>{G.error("Unhandled rejection (continuing):",_)});async function v(_,R,S,A=5){D("connection",`Testing connection to ${_}:${R} (retries left: ${A})`,S);let B=15000,N=Date.now();if(W.env.RPX_BYPASS_CONNECTION_TEST==="true"){D("connection",`Bypassing connection test for ${_}:${R} due to RPX_BYPASS_CONNECTION_TEST flag`,S);return}let K=()=>new Promise((Q,Y)=>{let X=uR.connect({host:_,port:R,timeout:3000});X.once("connect",()=>{D("connection",`Successfully connected to ${_}:${R}`,S),X.end(),Q()}),X.once("timeout",()=>{D("connection",`Connection to ${_}:${R} timed out`,S),X.destroy(),Y(Error("Connection timed out"))}),X.once("error",(J)=>{D("connection",`Failed to connect to ${_}:${R}: ${J}`,S),X.destroy(),Y(J)})});try{await K()}catch(Q){let Y=Q;if(Date.now()-N>B){D("connection",`Connection test timed out after ${B}ms, but continuing anyway`,S),G.warn(`Connection test to ${_}:${R} timed out, but RPX will try to proceed anyway.`);return}if(Y.code==="ECONNREFUSED"&&A>0)return D("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${A} retries left)`,S),await new Promise((J)=>setTimeout(J,2000)),v(_,R,S,A-1);if(A>0)try{D("connection",`Trying HTTP request to ${_}:${R}`,S),await new Promise((J,Z)=>{let V=_R.request({hostname:_,port:R,path:"/",method:"HEAD",timeout:5000},(I)=>{D("connection",`Received HTTP response with status: ${I.statusCode}`,S),J()});V.on("error",(I)=>Z(I)),V.on("timeout",()=>{V.destroy(),Z(Error("HTTP request timed out"))}),V.end()}),D("connection",`HTTP request to ${_}:${R} succeeded`,S);return}catch(J){return D("connection",`HTTP request to ${_}:${R} failed: ${J}`,S),D("connection",`Retrying socket connection in 2 seconds... (${A} retries left)`,S),await new Promise((Z)=>setTimeout(Z,2000)),v(_,R,S,A-1)}let X=`Failed to connect to ${_}:${R} after ${5-A} attempts: ${Y.message}`;D("connection",`${X}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,S),G.warn(X),G.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 CR(_){D("server",`Starting server with options: ${E(_)}`,_.verbose);let R=new URL((_.from?.startsWith("http")?_.from:`http://${_.from}`)||"localhost:5173"),S=new URL((_.to?.startsWith("http")?_.to:`http://${_.to}`)||"rpx.localhost"),A=Number.parseInt(R.port)||(R.protocol.includes("https:")?443:80),B=[S.hostname];if(yR(_)&&!S.hostname.includes("localhost")&&!S.hostname.includes("127.0.0.1")){D("hosts",`Checking if hosts file entry exists for: ${S.hostname}`,_?.verbose);try{if(!(await f(B,_.verbose))[0]){G.info(`Adding ${S.hostname} to hosts file...`),G.info("This may require sudo/administrator privileges");try{await y(B,_.verbose)}catch(Q){if(G.error("Failed to add hosts entry:",Q.message),G.warn("You can manually add this entry to your hosts file:"),G.warn(`127.0.0.1 ${S.hostname}`),G.warn(`::1 ${S.hostname}`),W.platform==="win32")G.warn("On Windows:"),G.warn("1. Run notepad as administrator"),G.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else G.warn("On Unix systems:"),G.warn("sudo nano /etc/hosts")}}else D("hosts",`Host entry already exists for ${S.hostname}`,_.verbose)}catch(K){G.error("Failed to check hosts file:",K.message)}}try{await v(R.hostname,A,_.verbose)}catch(K){D("server",`Connection test failed: ${K}`,_.verbose),G.error(K.message),G.warn("Continuing with proxy setup despite connection test failure..."),G.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let N=_._cachedSSLConfig||null;if(_.https)try{if(_.https===!0)_.https=o({..._,to:S.hostname});if(N=await U({..._,to:S.hostname,https:_.https}),!N){if(D("ssl",`Generating new certificates for ${S.hostname}`,_.verbose),await e({..._,from:R.toString(),to:S.hostname,https:_.https}),N=await U({..._,to:S.hostname,https:_.https}),!N)throw Error(`Failed to load SSL configuration after generating certificates for ${S.hostname}`)}}catch(K){throw D("server",`SSL setup failed: ${K}`,_.verbose),K}D("server",`Setting up reverse proxy with SSL config for ${S.hostname}`,_.verbose),await S_({..._,from:_.from||"localhost:5173",to:S.hostname,fromPort:A,sourceUrl:{hostname:R.hostname,host:R.host},ssl:N})}async function __(_,R,S,A,B,N,K,Q,Y,X){D("proxy",`Creating proxy server ${_} -> ${R} with cleanUrls: ${Q}`,K);let J=[{host:R,route:{sourceHost:A.host,cleanUrls:Q||!1,changeOrigin:Y||!1,basePath:"/",auth:X}}];if(!IR({routeEntries:J,listenPort:S,sslConfig:B,originGuard:null,verbose:K??!1}))throw Error(`Failed to start proxy server for ${R} on port ${S}`);A_({from:_,to:R,vitePluginUsage:N,listenPort:S,ssl:!!B,cleanUrls:Q,verbose:K})}async function S_(_){D("setup",`Setting up reverse proxy: ${E(_)}`,_.verbose);let{from:R,to:S,sourceUrl:A,ssl:B,verbose:N,cleanup:K,vitePluginUsage:Q,changeOrigin:Y,cleanUrls:X}=_,J=80,Z=443,V="0.0.0.0",I=_.portManager||R_,P=yR(_);try{if(P&&S&&!S.includes("localhost")&&!S.includes("127.0.0.1")){if(!(await f([S],N))[0]){G.warn(`The hostname ${S} isn't in your hosts file. Adding it now...`);try{await y([S],N),G.success(`Added ${S} to your hosts file.`)}catch(O){G.error(`Failed to add ${S} to your hosts file: ${O}`),G.info(`You may need to manually add '127.0.0.1 ${S}' to your /etc/hosts file.`)}}}else if(P&&W.platform!=="darwin"&&S&&S.includes("localhost")&&!S.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await f([S],N))[0]){D("hosts",`${S} not found in hosts file, adding...`,N);try{await y([S],N)}catch(O){D("hosts",`Failed to add ${S} to hosts file: ${O}`,N)}}}if(B&&!I.usedPorts.has(J)){if(!await C(J,V,N))D("setup","Starting HTTP redirect server",N),lR(N),I.usedPorts.add(J);else if(D("setup","Port 80 is in use, skipping HTTP redirect",N),N)G.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let T=B?Z:J,w=await C(T,V,N),M;if(w){if(D("setup",`Port ${T} is already in use`,N),N)G.warn(`Port ${T} is already in use. This may be another instance of rpx or another service.`);if(T===443){if(M=await I.getNextAvailablePort(3443,!0),D("setup",`Using port ${M} instead of ${T}`,N),N)G.info(`Using port ${M} instead. Access your site at https://${S}:${M}`)}else if(M=await I.getNextAvailablePort(T+1000,!0),D("setup",`Using port ${M} instead of ${T}`,N),N)G.info(`Using port ${M} instead. Access your site at http://${S}:${M}`)}else M=T,I.usedPorts.add(M),D("setup",`Using standard ${T===443?"HTTPS":"HTTP"} port ${T} for ${S}`,N);await __(R,S,M,A,B,Q,N,X,Y,RR(_.auth))}catch(T){D("setup",`Setup failed: ${T}`,N),G.error(`Failed to setup reverse proxy: ${T.message}`),u({domains:[S],hosts:typeof K==="boolean"?K:K?.hosts,certs:typeof K==="boolean"?K:K?.certs,verbose:N,vitePluginUsage:Q})}}function lR(_,R=80,S=443,A){D("redirect",`Starting HTTP redirect server on port ${R}`,_);let B=_R.createServer((N,K)=>{if(A&&N.url){let J=N.url.split("?",1)[0],Z=jR(A,J);if(Z!=null){D("redirect",`Serving ACME challenge ${J}`,_),K.writeHead(200,{"content-type":"text/plain"}),K.end(Z);return}}let Q=N.headers.host||"",Y=Q.includes(":")?Q.slice(0,Q.indexOf(":")):Q,X=S===443?Y:`${Y}:${S}`;D("redirect",`Redirecting request from ${Q}${N.url} to https://${X}`,_),K.writeHead(301,{Location:`https://${X}${N.url}`}),K.end()}).listen(R);t.add(B),D("redirect","HTTP redirect server started",_)}function D_(_){let R={...s,..._};if(D("proxy",`Starting proxy with options: ${E(R)}`,R?.verbose),R.viaDaemon){if(!R.from||!R.to){G.error("viaDaemon mode requires both `from` and `to`");return}b({proxies:[{id:R.id,from:R.from,to:R.to,path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}],verbose:R.verbose}).catch((Y)=>{G.error(`Failed to register with rpx daemon: ${Y.message}`),W.exit(1)});return}let S=R.to||"",A=S.split(".").pop()?.toLowerCase()||"",B=W.platform==="darwin"&&S&&!S.includes("localhost")&&!S.includes("127.0.0.1"),N=["dev","app","page","new","day","foo"],K=["test","localhost","local","example","invalid"];if(B&&N.includes(A)&&R?.verbose)G.warn(`The .${A} TLD may not work reliably for local development`),G.info(` Google owns .${A} with HSTS preloading, which can bypass local DNS`),G.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(B)import("./chunk-c5scw35a.js").then(({setupDevelopmentDns:Y})=>{Y({domains:[S],verbose:R.verbose}).then((X)=>{if(X)Promise.resolve().then(()=>{if(R.verbose)if(K.includes(A))G.success(`DNS server started for .${A} domains`);else G.success(`DNS server started for .${A} domains (hosts file entry also added)`)});else D("dns",`Could not start DNS server - ${S} may not resolve in browser`,R.verbose)})}).catch((Y)=>{D("dns",`Failed to start DNS server: ${Y}`,R.verbose)});let Q={from:R.from,to:R.to,cleanUrls:R.cleanUrls,https:o(R),cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,changeOrigin:R.changeOrigin,verbose:R.verbose,regenerateUntrustedCerts:R.regenerateUntrustedCerts};D("proxy",`Server options: ${E(Q)}`,R.verbose),CR(Q).catch((Y)=>{D("proxy",`Failed to start proxy: ${Y}`,R.verbose),G.error(`Failed to start proxy: ${Y.message}`),u({domains:[R.to],hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose})})}function N_(_){return _?.verbose||!1}function yR(_){if(_?.hostsManagement===!1)return!1;let R=_?.cleanup;if(R===!1)return!1;if(R&&typeof R==="object"&&R.hosts===!1)return!1;return!0}async function fR(_){let R={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(_)R={...R,..._};let S=N_(R),A=yR(R);if(D("config",`Starting with config: ${E(R,2)}`,S),D("config",`Is multi-proxy? ${"proxies"in R}`,S),D("config",`Hosts management enabled? ${A}`,S),R.viaDaemon){let z="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map((F)=>({id:F.id,from:F.from,to:F.to,path:F.path,cleanUrls:F.cleanUrls??R.cleanUrls,changeOrigin:F.changeOrigin??R.changeOrigin,pathRewrites:F.pathRewrites})):[{id:R.id,from:R.from,to:R.to??"rpx.localhost",path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}];await b({proxies:z,verbose:S});return}if("proxies"in R&&Array.isArray(R.proxies)){D("servers",`Found ${R.proxies.length} proxies in config`,S);for(let $ of R.proxies)if($.start){let z=`${$.from}-${$.to}`;try{D("watch",`Starting command for ${z} with command: ${$.start.command}`,S),G.info(`Starting command for ${z}...`),await r.startProcess(z,$.start,S);let F=new URL($.from?.startsWith("http")?$.from:`http://${$.from}`),j=F.hostname||"localhost",a=Number(F.port)||80;try{await v(j,a,S),D("watch",`Dev server is ready at ${j}:${a}`,S)}catch(pR){D("watch",`Connection check failed, but continuing with proxy setup: ${pR}`,S),G.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(F){throw D("watch",`Failed to start command for ${z}: ${F}`,S),Error(`Failed to start command for ${z}: ${F}`)}}else D("watch",`No start command for proxy ${$.from} -> ${$.to}`,S)}else if("start"in R&&R.start){D("watch","Found start command in single proxy config",S);let $=`${R.from}-${R.to}`;try{if(R.start)D("watch",`Starting command: ${R.start.command}`,S),await r.startProcess($,R.start,S);let z=new URL(R.from?.startsWith("http")?R.from:`http://${R.from}`),F=z.hostname||"localhost",j=Number(z.port)||80;try{await v(F,j,S),D("watch",`Dev server is ready at ${F}:${j}`,S)}catch(a){D("watch",`Connection check failed, but continuing with proxy setup: ${a}`,S),G.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(z){throw D("watch",`Failed to run start command: ${z}`,S),Error(`Failed to run start command: ${z}`)}}else D("watch","No start command found in config",S);let B="proxies"in R&&Array.isArray(R.proxies)?R.proxies[0]?.to:("to"in R)?R.to:"rpx.localhost";if(W.platform!=="win32"&&(R.https||A)){if(!h())try{D("sudo","Pre-acquiring sudo credentials for privileged operations",S),eR("sudo -v",{stdio:"inherit"})}catch{D("sudo","Could not pre-acquire sudo credentials",S)}}let N=[];if(R.productionCerts){if(N=await ER(R.productionCerts,S),N.length>0)D("ssl",`Using ${N.length} production SNI cert(s): ${N.map(($)=>$.serverName).join(", ")}`,S)}if(R.https){let $=N.length>0?null:await U(R);if(!$&&N.length===0){if(D("ssl",`No valid or trusted certificates found for ${B}, generating new ones`,R.verbose),await e(R),$=await U(R),!$)throw Error(`Failed to load SSL certificates after generation for ${B}`)}else D("ssl",`Using existing and trusted certificates for ${B}`,R.verbose);R._cachedSSLConfig=$}let K="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map(($)=>({...$,https:R.https,cleanup:R.cleanup,cleanUrls:$.cleanUrls??("cleanUrls"in R?R.cleanUrls:!1),vitePluginUsage:R.vitePluginUsage,changeOrigin:$.changeOrigin??R.changeOrigin,verbose:S,_cachedSSLConfig:R._cachedSSLConfig})):[{from:"from"in R?R.from:"localhost:5173",to:"to"in R?R.to:"rpx.localhost",cleanUrls:"cleanUrls"in R?R.cleanUrls:!1,https:R.https,cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,start:"start"in R?R.start:void 0,changeOrigin:R.changeOrigin,auth:"auth"in R?R.auth:void 0,verbose:S,_cachedSSLConfig:R._cachedSSLConfig}],Q=K.map(($)=>$.to||"rpx.localhost"),Y=N.length>0?N:R._cachedSSLConfig??null,X=Q.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),J=["dev","app","page","new","day","foo"],Z=["test","localhost","local","example","invalid"],V=[...new Set(X.map(($)=>$.split(".").pop()?.toLowerCase()))],I=V.filter(($)=>!!$&&J.includes($));if(I.length>0&&S)G.warn(`The following TLDs may not work reliably for local development: ${I.map(($)=>`.${$}`).join(", ")}`),G.info(" These TLDs have HSTS preloading which can bypass local DNS"),G.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(A&&W.platform==="darwin"&&X.length>0){let{setupDevelopmentDns:$}=await import("./chunk-c5scw35a.js");if(await $({domains:X,verbose:S})){if(S)if(V.every((j)=>!!j&&Z.includes(j)))G.success(`DNS server started for ${V.map((j)=>`.${j}`).join(", ")} domains`);else G.success(`DNS server started for ${V.map((j)=>`.${j}`).join(", ")} domains (hosts file entries also added)`)}else D("dns","Could not start DNS server - custom domains may not resolve",S)}let P=async()=>{D("cleanup","Starting cleanup handler",R.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-c5scw35a.js");await $({verbose:R.verbose})}catch($){D("cleanup",`Error stopping DNS server: ${$}`,R.verbose)}try{await r.stopAll(R.verbose)}catch($){D("cleanup",`Error stopping processes: ${$}`,R.verbose)}await u({domains:Q,hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose||!1})};W.on("SIGINT",P),W.on("SIGTERM",P);let T=R.singlePortMode===!0,w=R.httpsPort??443,M=R.httpPort??80,L=R.originGuard?XR(R.originGuard):null,O=!!Y&&(K.length>1||T),bR=!Y&&T&&K.length>0;if(O&&Y){D("proxies",`Creating shared HTTPS server for ${K.length} domains on port ${w}`,S);let $=await mR(K,A,S);if(!await C(M,"0.0.0.0",S))lR(S,M,w,R.acmeChallengeWebroot);if(await C(w,"0.0.0.0",S)){if(D("proxies",`Port ${w} is already in use, cannot start shared proxy`,S),S)G.warn(`Port ${w} is in use. Shared HTTPS proxy cannot start.`);return}if(!IR({routeEntries:$,listenPort:w,sslConfig:Y,originGuard:L,verbose:S})){G.error(`Shared HTTPS proxy failed to bind :${w}; not exiting`);return}}else if(bR){D("proxies",`Creating shared HTTP server for ${K.length} domains on port ${M}`,S);let $=await mR(K,A,S);if(await C(M,"0.0.0.0",S)){if(D("proxies",`Port ${M} is already in use, cannot start shared proxy`,S),S)G.warn(`Port ${M} is in use. Shared HTTP proxy cannot start.`);return}if(!IR({routeEntries:$,listenPort:M,sslConfig:null,originGuard:L,verbose:S})){G.error(`Shared HTTP proxy failed to bind :${M}; not exiting`);return}}else for(let $ of K)try{let z=$.to||"rpx.localhost";D("proxy",`Starting proxy for ${z} with SSL config: ${!!Y}`,$.verbose),await CR({from:$.from||"localhost:5173",to:z,cleanUrls:$.cleanUrls||!1,https:$.https||!1,cleanup:$.cleanup||!1,vitePluginUsage:$.vitePluginUsage||!1,verbose:$.verbose||!1,_cachedSSLConfig:R._cachedSSLConfig,changeOrigin:$.changeOrigin||!1})}catch(z){D("proxies",`Failed to start proxy for ${$.to}: ${z}`,$.verbose),G.error(`Failed to start proxy for ${$.to}:`,z)}}async function mR(_,R,S){let A=[],B=new Set;for(let N of _){let K=N.to||"rpx.localhost",Q=N.cleanUrls||!1,Y=N.path,X=HR(Y),J=RR(N.auth);if(N.redirect){let Z=zR(N.redirect);A.push({host:K,path:Y,route:{redirect:Z,basePath:X,auth:J}}),D("proxies",`Route: ${K}${Y??""} → redirect ${Z.status} ${Z.to}${J?" (auth)":""}`,S)}else if(N.static)A.push({host:K,path:Y,route:{static:MR(N.static,Q),cleanUrls:Q,basePath:X,auth:J}}),D("proxies",`Route: ${K}${Y??""} → static ${typeof N.static==="string"?N.static:N.static.dir}${J?" (auth)":""}`,S);else{let Z=new URL(N.from?.startsWith("http")?N.from:`http://${N.from}`);A.push({host:K,path:Y,route:{sourceHost:Z.host,cleanUrls:Q,changeOrigin:N.changeOrigin||!1,pathRewrites:N.pathRewrites,basePath:X,auth:J}}),D("proxies",`Route: ${K}${Y??""} → ${Z.host}${J?" (auth)":""}`,S)}if(B.has(K))continue;if(B.add(K),R&&!xR(K)&&!K.includes("localhost")&&!K.includes("127.0.0.1"))try{if(!(await f([K],S))[0])await y([K],S)}catch{D("hosts",`Could not add hosts entry for ${K}`,S)}}return A}function IR(_){let{routeEntries:R,listenPort:S,sslConfig:A,originGuard:B,verbose:N}=_,K=kR(R),Q=FR((J,Z)=>wR(K,J,Z),N),Y=B?(J,Z)=>B(J)??Q(J,Z):Q,X=TR(N);try{let J=Bun.serve({port:S,hostname:"0.0.0.0",reusePort:vR(),...A?{tls:Array.isArray(A)?A.map((Z)=>({serverName:Z.serverName,key:Z.key,cert:Z.cert})):{key:A.key,cert:A.cert,ca:A.ca,requestCert:!1,rejectUnauthorized:!1}}:{},fetch(Z,V){return Y(Z,V)},websocket:X,error(Z){return D("server",`Shared proxy server error: ${Z}`,N),new Response(`Server Error: ${Z.message}`,{status:500})}});return t.add(J),D("proxies",`Shared ${A?"HTTPS":"HTTP"} proxy listening on port ${S} for ${K.size} domains`,N),J}catch(J){return D("proxies",`Failed to start shared proxy: ${J}`,N),console.error("Failed to start shared proxy:",J),null}}function A_(_){if(_?.vitePluginUsage||!_?.verbose)return;if(console.log(""),console.log(` ${H.green(H.bold("rpx"))} ${H.green(`v${oR}`)}`),console.log(` ${H.green("➜")} ${H.dim(_?.from??"")} ${H.dim("➜")} ${H.cyan(_?.ssl?`https://${_?.to}`:`http://${_?.to}`)}`),_?.listenPort!==(_?.ssl?443:80))console.log(` ${H.green("➜")} Listening on port ${_?.listenPort}`);if(_?.cleanUrls)console.log(` ${H.green("➜")} Clean URLs enabled`)}var YN=fR;export{NR as writeEntry,BS as watchRegistry,G_ as verifyHttpsChain,T_ as trustRootCaForBrowsers,qS as tearDownDevelopmentDns,ES as syncDevelopmentDnsFromRegistry,m_ as stripBasePath,FS as stopDnsServer,vS as stopDaemon,CR as startServer,D_ as startProxy,fR as startProxies,MS as startDnsServer,n_ as siteIdForHost,vR as shouldReusePort,HS as setupResolver,wS as setupDevelopmentDns,b_ as serverNameFromCertFilename,h_ as serveStaticFile,E as safeStringify,O_ as safeRelativePath,MN as safeDeleteFile,b as runViaDaemon,hS as runDaemon,jS as resolverFilePath,ZS as resolverBasenamesForDomains,VS as resolverBasenameForDomain,MR as resolveStaticRoute,c_ as resolveStaticFile,zR as resolveRedirect,zN as resolvePathRewrite,RR as resolveAuth,$S as renderStartingPage,GS as renderFailedPage,CS as removeResolver,kS as removeLegacyTldResolvers,$R as removeHosts,AR as removeEntry,cS as releaseDaemonLock,GN as redactSensitive,yS as reconcileStaleDevelopmentDns,uS as reconcileDevelopmentDnsOnIdle,t_ as readSiteManifest,DS as readEntry,PS as readDaemonPid,K_ as readCertSha256Fingerprint,Y_ as readCertCommonName,NS as readAll,jR as readAcmeChallenge,z_ as pruneStaleRootCas,r_ as projectNameFromHost,_S as portManager,a_ as pathPrefixMatches,X_ as parseSha256HashesFromSecurityListing,f_ as parseHtpasswd,B_ as normalizeSha256Fingerprint,HR as normalizePathPrefix,QS as normalizeDevDomain,GR as matchesWildcard,p_ as matchesAllowedSuffix,wR as matchHostRoute,l_ as matchHostList,u_ as matchHost,E_ as loadSSLConfig,o_ as listDiscoverableSites,W_ as listCertSha256HashesByCommonName,xR as isWildcardPattern,JN as isValidRootCA,DR as isValidId,IN as isSingleProxyOptions,WN as isSingleProxyConfig,M_ as isRootCaTrustedForSsl,F_ as isRootCaFingerprintInKeychains,cR as isProcessElevated,C as isPortInUse,SS as isPidAlive,ZN as isMultiProxyOptions,VN as isMultiProxyConfig,d_ as isLikelyHostname,TS as isDnsServerRunning,LS as isDaemonRunning,y_ as isCertTrusted,o as httpsConfig,h as getSudoPassword,w_ as getSharedDaemonCertPaths,k_ as getRootCAPaths,SR as getRegistryDir,QN as getPrimaryDomain,I_ as getMacosTrustKeychains,Z_ as getMacosLoginKeychainPath,fS as getDaemonRpxDir,US as getDaemonPidPath,e as generateCertificate,AS as gcStaleEntries,q_ as forceTrustCertificate,RS as findAvailablePort,XN as extractHostname,i_ as expandHome,$N as execSudoSync,YS as escapeHtml,BR as ensureDaemonRunning,U_ as enforceBasicAuth,x_ as devSslToSniEntries,IS as devDomainsFromHosts,s_ as detectProjectPreset,LR as deriveIdFromTarget,mS as defaultDaemonSpawnCommand,s as defaultConfig,YN as default,D as debugLog,e_ as createSiteResolver,TR as createProxyWebSocketHandler,FR as createProxyFetchHandler,XR as createOriginGuard,L_ as contentTypeFor,xS as contentLooksLikeRpxResolver,s as config,H as colors,C_ as clearSslConfigCache,WR as cleanupCertificates,u as cleanup,f as checkHosts,U as checkExistingCertificates,$_ as certIncludesSanHostnames,ER as buildSniTlsConfig,H_ as buildRegistryTlsProxyOptions,P_ as buildRedirectLocation,kR as buildHostRoutes,y as addHosts,OS as acquireDaemonLock,KS as SiteSupervisor,j_ as SHARED_DEV_HOST_CERT_PATH,V_ as RPX_ROOT_CA_COMMON_NAME,zS as RPX_RESOLVER_MARKER,g_ as OnDemandCertManager,Q_ as MACOS_SYSTEM_KEYCHAIN,J_ as MACOS_CA_TRUST_FLAGS,JS as LEGACY_TLD_RESOLVER_LABELS,qR as DefaultPortManager,XS as DNS_STATE_VERSION,WS as DNS_PORT,v_ as ACME_CHALLENGE_PREFIX};
8
+ `,X=ZR.join(dR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await C.promises.writeFile(X,A,"utf8"),await n(`cat "${X}" | tee "${H}" > /dev/null`),D("hosts","Hosts removed successfully",R)}catch(Y){D("hosts","Could not clean up hosts file automatically",R)}finally{try{await C.promises.unlink(X)}catch(Y){D("hosts",`Failed to remove temporary file: ${Y}`,R)}}}catch(_){D("hosts",`Failed to clean up hosts file: ${_.message}`,R)}}async function U(S,R){D("hosts",`Checking hosts: ${S}`,R);let _;try{_=await C.promises.readFile(H,"utf-8")}catch(N){D("hosts",`Error reading hosts file: ${N}`,R);try{let K=v(),B;if(K)B=`echo '${K}' | sudo -S cat "${H}" 2>/dev/null`;else B=`sudo -n cat "${H}" 2>/dev/null || cat "${H}" 2>/dev/null || echo ""`;let{stdout:A}=await u(B);_=A}catch(K){return D("hosts",`Cannot read hosts file, assuming entries don't exist: ${K}`,R),S.map(()=>!1)}}return S.map((N)=>{let K=`127.0.0.1 ${N}`,B=`::1 ${N}`;return _.includes(K)||_.includes(B)})}import{spawn as oR}from"node:child_process";import*as i from"node:process";class t{processes=new Map;isShuttingDown=!1;async startProcess(S,R,_){if(this.processes.has(S)){D("start",`Process ${S} is already running`,_);return}let[N,...K]=R.command.split(" "),B=R.cwd||i.cwd();D("start",`Starting process ${S}:`,_),D("start",` Command: ${N} ${K.join(" ")}`,_),D("start",` Working directory: ${B}`,_),D("start",` Environment variables: ${q(R.env)}`,_);let A=oR(N,K,{cwd:B,env:{...i.env,...R.env},shell:!0,stdio:"inherit"});return this.processes.set(S,{command:R.command,cwd:B,process:A,env:R.env}),new Promise((X,Y)=>{if(A.on("error",(J)=>{if(!this.isShuttingDown)D("start",`Process ${S} failed to start: ${J}`,_),this.processes.delete(S),Y(J)}),A.on("exit",(J)=>{if(!this.isShuttingDown&&J!==null&&J!==0)D("start",`Process ${S} exited with code ${J}; leaving the proxy running`,_),this.processes.delete(S),Y(Error(`Process ${S} exited with code ${J}`))}),_)A.stdout?.on("data",(J)=>{D("process",`[${S}] ${J.toString().trim()}`,!0)}),A.stderr?.on("data",(J)=>{D("process",`[${S}] ERR: ${J.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&A.killed)this.processes.delete(S),Y(Error(`Process ${S} was killed during startup`));else D("start",`Process ${S} started successfully`,_),X()},1000)})}async stopProcess(S,R){let _=this.processes.get(S);if(!_?.process){D("start",`No process found for ${S}`,R);return}return D("start",`Stopping process ${S}`,R),new Promise((N)=>{if(!_.process){N();return}_.process.once("exit",()=>{this.processes.delete(S),D("start",`Process ${S} stopped`,R),N()});try{_.process.kill("SIGTERM"),setTimeout(()=>{if(_.process){D("start",`Force killing process ${S}`,R);try{_.process.kill("SIGKILL")}catch(K){}}},3000)}catch(K){D("start",`Error stopping process ${S}: ${K}`,R),this.processes.delete(S),N()}})}async stopAll(S){if(this.isShuttingDown){D("start","Already shutting down, skipping duplicate stopAll call",S);return}this.isShuttingDown=!0,D("start","Stopping all processes",S);let R=Array.from(this.processes.keys()).map((_)=>this.stopProcess(_,S).catch((N)=>{$.error(`Failed to stop process ${_}:`,N)}));await Promise.allSettled(R),this.processes.clear(),this.isShuttingDown=!1}isRunning(S){let R=this.processes.get(S);return!!R?.process&&!R.process.killed}}var JD=new t;import{timingSafeEqual as RS}from"node:crypto";function SS(S,R){if(S==null)return!1;let _=Buffer.from(S),N=Buffer.from(R);if(_.length!==N.length)return!1;return RS(_,N)}function r(S){return S.toLowerCase().replace(/\.$/,"")}var _S=["/.well-known/acme-challenge/"];function DS(S){let R=S.headers.get("host");if(R)return r(R.split(":")[0]);try{return r(new URL(S.url).hostname)}catch{return""}}function zR(S){let R=S.header.toLowerCase(),_=new Set,N=[];for(let Y of S.hosts){let J=r(Y);if(J.startsWith("*."))N.push(J);else _.add(J)}let K=S.exemptPaths??_S,B=S.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
+ `,A=(Y)=>{let J=r(Y);return _.has(J)||N.some((V)=>WR(J,V))},X=(Y)=>{let J=DS(Y);if(!A(J))return;let V="/";try{V=new URL(Y.url).pathname}catch{}if(K.some((Z)=>V.startsWith(Z)))return;if(SS(Y.headers.get(R),S.value))return;return new Response(B,{status:403,headers:{"content-type":"text/plain"}})};return X.protects=A,X}var e=new t,AS="0.12.0",BS=new OR("0.0.0.0"),o=new Set,RR=new Set,MR=!1,s=null,xR=null;async function a(S){if(MR)return D("cleanup","Cleanup already in progress, skipping",S?.verbose),xR||Promise.resolve();MR=!0,D("cleanup","Starting cleanup process",S?.verbose),xR=new Promise((R)=>{s=R});try{await e.stopAll(S?.verbose),$.info("Shutting down proxy servers...");let R=[],_=Array.from(o).map((N)=>new Promise((K)=>{let B=N;try{if(typeof B.stop==="function")B.stop(!0),D("cleanup","Bun server stopped",S?.verbose),K();else if(typeof B.close==="function")B.close(()=>{D("cleanup","Server closed successfully",S?.verbose),K()});else K()}catch(A){D("cleanup",`Error stopping server: ${A}`,S?.verbose),K()}}));R.push(..._),o.clear();for(let N of RR)BR(N);if(RR.clear(),S?.hosts&&S.domains?.length){D("cleanup","Cleaning up hosts file entries",S?.verbose),D("cleanup",`Original domains for cleanup: ${JSON.stringify(S.domains)}`,S?.verbose);let N=S.domains.filter((K)=>{if(K==="test.local")return!0;return K!=="localhost"&&!K.startsWith("localhost.")&&K!=="127.0.0.1"});if(D("cleanup",`Filtered domains for cleanup: ${JSON.stringify(N)}`,S?.verbose),N.length>0)$.info("Cleaning up hosts file entries..."),R.push(IR(N,S?.verbose).then(()=>{D("cleanup",`Removed hosts entries for ${N.join(", ")}`,S?.verbose)}).catch((K)=>{D("cleanup",`Failed to remove hosts entries: ${K}`,S?.verbose),$.warn(`Failed to clean up hosts file entries for ${N.join(", ")}:`,K)}))}if(S?.certs&&S.domains?.length){D("cleanup","Cleaning up SSL certificates",S?.verbose),$.info("Cleaning up SSL certificates...");let N=S.domains.map(async(K)=>{try{await kR(K,S?.verbose),D("cleanup",`Removed certificates for ${K}`,S?.verbose)}catch(B){D("cleanup",`Failed to remove certificates for ${K}: ${B}`,S?.verbose),$.warn(`Failed to clean up certificates for ${K}:`,B)}});R.push(...N)}await Promise.allSettled(R),D("cleanup","All cleanup tasks completed successfully",S?.verbose),$.success("All cleanup tasks completed successfully")}catch(R){D("cleanup",`Error during cleanup: ${R}`,S?.verbose),$.error("Error during cleanup:",R)}finally{if(s)s();s=null,MR=!1;let R=S&&"vitePluginUsage"in S&&S.vitePluginUsage===!0;if(z.env.NODE_ENV!=="test"&&z.env.BUN_ENV!=="test"&&!R)z.exit(0)}return xR}var jR=!1;function iR(S){if(jR){D("signal",`Received second ${S} signal, forcing exit`,!0),z.exit(1);return}jR=!0,D("signal",`Received ${S} signal, initiating cleanup`,!0),a().catch((R)=>{D("signal",`Cleanup failed after ${S}: ${R}`,!0),z.exit(1)}).finally(()=>{jR=!1})}z.once("SIGINT",()=>iR("SIGINT"));z.once("SIGTERM",()=>iR("SIGTERM"));z.on("uncaughtException",(S)=>{$.error("Uncaught exception (continuing):",S)});z.on("unhandledRejection",(S)=>{$.error("Unhandled rejection (continuing):",S)});async function b(S,R,_,N=5){D("connection",`Testing connection to ${S}:${R} (retries left: ${N})`,_);let K=15000,B=Date.now();if(z.env.RPX_BYPASS_CONNECTION_TEST==="true"){D("connection",`Bypassing connection test for ${S}:${R} due to RPX_BYPASS_CONNECTION_TEST flag`,_);return}let A=()=>new Promise((X,Y)=>{let J=nR.connect({host:S,port:R,timeout:3000});J.once("connect",()=>{D("connection",`Successfully connected to ${S}:${R}`,_),J.end(),X()}),J.once("timeout",()=>{D("connection",`Connection to ${S}:${R} timed out`,_),J.destroy(),Y(Error("Connection timed out"))}),J.once("error",(V)=>{D("connection",`Failed to connect to ${S}:${R}: ${V}`,_),J.destroy(),Y(V)})});try{await A()}catch(X){let Y=X;if(Date.now()-B>K){D("connection",`Connection test timed out after ${K}ms, but continuing anyway`,_),$.warn(`Connection test to ${S}:${R} timed out, but RPX will try to proceed anyway.`);return}if(Y.code==="ECONNREFUSED"&&N>0)return D("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${N} retries left)`,_),await new Promise((V)=>setTimeout(V,2000)),b(S,R,_,N-1);if(N>0)try{D("connection",`Trying HTTP request to ${S}:${R}`,_),await new Promise((V,Z)=>{let Q=YR.request({hostname:S,port:R,path:"/",method:"HEAD",timeout:5000},(I)=>{D("connection",`Received HTTP response with status: ${I.statusCode}`,_),V()});Q.on("error",(I)=>Z(I)),Q.on("timeout",()=>{Q.destroy(),Z(Error("HTTP request timed out"))}),Q.end()}),D("connection",`HTTP request to ${S}:${R} succeeded`,_);return}catch(V){return D("connection",`HTTP request to ${S}:${R} failed: ${V}`,_),D("connection",`Retrying socket connection in 2 seconds... (${N} retries left)`,_),await new Promise((Z)=>setTimeout(Z,2000)),b(S,R,_,N-1)}let J=`Failed to connect to ${S}:${R} after ${5-N} attempts: ${Y.message}`;D("connection",`${J}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,_),$.warn(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 cR(S){D("server",`Starting server with options: ${q(S)}`,S.verbose);let R=L(S.from),_=new URL(R.startsWith("http")?R:`http://${R}`),N=new URL((S.to?.startsWith("http")?S.to:`http://${S.to}`)||"rpx.localhost"),K=Number.parseInt(_.port)||(_.protocol.includes("https:")?443:80),B=[N.hostname];if(hR(S)&&!N.hostname.includes("localhost")&&!N.hostname.includes("127.0.0.1")){D("hosts",`Checking if hosts file entry exists for: ${N.hostname}`,S?.verbose);try{if(!(await U(B,S.verbose))[0]){$.info(`Adding ${N.hostname} to hosts file...`),$.info("This may require sudo/administrator privileges");try{await y(B,S.verbose)}catch(Y){if($.error("Failed to add hosts entry:",Y.message),$.warn("You can manually add this entry to your hosts file:"),$.warn(`127.0.0.1 ${N.hostname}`),$.warn(`::1 ${N.hostname}`),z.platform==="win32")$.warn("On Windows:"),$.warn("1. Run notepad as administrator"),$.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else $.warn("On Unix systems:"),$.warn("sudo nano /etc/hosts")}}else D("hosts",`Host entry already exists for ${N.hostname}`,S.verbose)}catch(X){$.error("Failed to check hosts file:",X.message)}}try{await b(_.hostname,K,S.verbose)}catch(X){D("server",`Connection test failed: ${X}`,S.verbose),$.error(X.message),$.warn("Continuing with proxy setup despite connection test failure..."),$.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let A=S._cachedSSLConfig||null;if(S.https)try{if(S.https===!0)S.https=DR({...S,to:N.hostname});if(A=await P({...S,to:N.hostname,https:S.https}),!A){if(D("ssl",`Generating new certificates for ${N.hostname}`,S.verbose),await _R({...S,from:_.toString(),to:N.hostname,https:S.https}),A=await P({...S,to:N.hostname,https:S.https}),!A)throw Error(`Failed to load SSL configuration after generating certificates for ${N.hostname}`)}}catch(X){throw D("server",`SSL setup failed: ${X}`,S.verbose),X}D("server",`Setting up reverse proxy with SSL config for ${N.hostname}`,S.verbose),await YS({...S,from:R,originalFrom:S.from||R,to:N.hostname,fromPort:K,sourceUrl:{hostname:_.hostname,host:_.host},ssl:A})}async function KS(S,R,_,N,K,B,A,X,Y,J,V,Z){D("proxy",`Creating proxy server ${S} -> ${R} with cleanUrls: ${X}`,A);let Q=NR(V??N.host,Z);AR(Q);let I=[{host:R,route:{sourceHost:N.host,upstreamPool:Q,cleanUrls:X||!1,changeOrigin:Y||!1,basePath:"/",auth:J}}];if(!HR({routeEntries:I,listenPort:_,sslConfig:K,originGuard:null,verbose:A??!1}))throw BR(Q),Error(`Failed to start proxy server for ${R} on port ${_}`);RR.add(Q),XS({from:S,to:R,vitePluginUsage:B,listenPort:_,ssl:!!K,cleanUrls:X,verbose:A})}async function YS(S){D("setup",`Setting up reverse proxy: ${q(S)}`,S.verbose);let{from:R,originalFrom:_,to:N,sourceUrl:K,ssl:B,verbose:A,cleanup:X,vitePluginUsage:Y,changeOrigin:J,cleanUrls:V}=S,Z=80,Q=443,I="0.0.0.0",F=S.portManager||BS,O=hR(S);try{if(O&&N&&!N.includes("localhost")&&!N.includes("127.0.0.1")){if(!(await U([N],A))[0]){$.warn(`The hostname ${N} isn't in your hosts file. Adding it now...`);try{await y([N],A),$.success(`Added ${N} to your hosts file.`)}catch(c){$.error(`Failed to add ${N} to your hosts file: ${c}`),$.info(`You may need to manually add '127.0.0.1 ${N}' to your /etc/hosts file.`)}}}else if(O&&z.platform!=="darwin"&&N&&N.includes("localhost")&&!N.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await U([N],A))[0]){D("hosts",`${N} not found in hosts file, adding...`,A);try{await y([N],A)}catch(c){D("hosts",`Failed to add ${N} to hosts file: ${c}`,A)}}}if(B&&!F.usedPorts.has(Z)){if(!await f(Z,I,A))D("setup","Starting HTTP redirect server",A),tR(A),F.usedPorts.add(Z);else if(D("setup","Port 80 is in use, skipping HTTP redirect",A),A)$.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let M=B?Q:Z,w=await f(M,I,A),j;if(w){if(D("setup",`Port ${M} is already in use`,A),A)$.warn(`Port ${M} is already in use. This may be another instance of rpx or another service.`);if(M===443){if(j=await F.getNextAvailablePort(3443,!0),D("setup",`Using port ${j} instead of ${M}`,A),A)$.info(`Using port ${j} instead. Access your site at https://${N}:${j}`)}else if(j=await F.getNextAvailablePort(M+1000,!0),D("setup",`Using port ${j} instead of ${M}`,A),A)$.info(`Using port ${j} instead. Access your site at http://${N}:${j}`)}else j=M,F.usedPorts.add(j),D("setup",`Using standard ${M===443?"HTTPS":"HTTP"} port ${M} for ${N}`,A);await KS(R,N,j,K,B,Y,A,V,J,KR(S.auth),_,S.loadBalancer)}catch(M){D("setup",`Setup failed: ${M}`,A),$.error(`Failed to setup reverse proxy: ${M.message}`),a({domains:[N],hosts:typeof X==="boolean"?X:X?.hosts,certs:typeof X==="boolean"?X:X?.certs,verbose:A,vitePluginUsage:Y})}}function tR(S,R=80,_=443,N){D("redirect",`Starting HTTP redirect server on port ${R}`,S);let K=YR.createServer((B,A)=>{if(N&&B.url){let V=B.url.split("?",1)[0],Z=CR(N,V);if(Z!=null){D("redirect",`Serving ACME challenge ${V}`,S),A.writeHead(200,{"content-type":"text/plain"}),A.end(Z);return}}let X=B.headers.host||"",Y=X.includes(":")?X.slice(0,X.indexOf(":")):X,J=_===443?Y:`${Y}:${_}`;D("redirect",`Redirecting request from ${X}${B.url} to https://${J}`,S),A.writeHead(301,{Location:`https://${J}${B.url}`}),A.end()}).listen(R);o.add(K),D("redirect","HTTP redirect server started",S)}function $S(S){let R={...SR,...S};if(D("proxy",`Starting proxy with options: ${q(R)}`,R?.verbose),R.viaDaemon){if(!R.from||!R.to){$.error("viaDaemon mode requires both `from` and `to`");return}g({proxies:[{id:R.id,from:R.from,to:R.to,path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}],verbose:R.verbose}).catch((Y)=>{$.error(`Failed to register with rpx daemon: ${Y.message}`),z.exit(1)});return}let _=R.to||"",N=_.split(".").pop()?.toLowerCase()||"",K=z.platform==="darwin"&&_&&!_.includes("localhost")&&!_.includes("127.0.0.1"),B=["dev","app","page","new","day","foo"],A=["test","localhost","local","example","invalid"];if(K&&B.includes(N)&&R?.verbose)$.warn(`The .${N} TLD may not work reliably for local development`),$.info(` Google owns .${N} with HSTS preloading, which can bypass local DNS`),$.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(K)import("./chunk-r1k4wgag.js").then(({setupDevelopmentDns:Y})=>{Y({domains:[_],verbose:R.verbose}).then((J)=>{if(J)Promise.resolve().then(()=>{if(R.verbose)if(A.includes(N))$.success(`DNS server started for .${N} domains`);else $.success(`DNS server started for .${N} domains (hosts file entry also added)`)});else D("dns",`Could not start DNS server - ${_} may not resolve in browser`,R.verbose)})}).catch((Y)=>{D("dns",`Failed to start DNS server: ${Y}`,R.verbose)});let X={from:R.from,to:R.to,cleanUrls:R.cleanUrls,https:DR(R),cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,changeOrigin:R.changeOrigin,verbose:R.verbose,regenerateUntrustedCerts:R.regenerateUntrustedCerts};D("proxy",`Server options: ${q(X)}`,R.verbose),cR(X).catch((Y)=>{D("proxy",`Failed to start proxy: ${Y}`,R.verbose),$.error(`Failed to start proxy: ${Y.message}`),a({domains:[R.to],hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose})})}function GS(S){return S?.verbose||!1}function hR(S){if(S?.hostsManagement===!1)return!1;let R=S?.cleanup;if(R===!1)return!1;if(R&&typeof R==="object"&&R.hosts===!1)return!1;return!0}async function uR(S){let R={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(S)R={...R,...S};let _=GS(R),N=hR(R);if(D("config",`Starting with config: ${q(R,2)}`,_),D("config",`Is multi-proxy? ${"proxies"in R}`,_),D("config",`Hosts management enabled? ${N}`,_),R.viaDaemon){let W="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map((x)=>({id:x.id,from:x.from,to:x.to,path:x.path,cleanUrls:x.cleanUrls??R.cleanUrls,changeOrigin:x.changeOrigin??R.changeOrigin,pathRewrites:x.pathRewrites})):[{id:R.id,from:R.from,to:R.to??"rpx.localhost",path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}];await g({proxies:W,verbose:_});return}if("proxies"in R&&Array.isArray(R.proxies)){D("servers",`Found ${R.proxies.length} proxies in config`,_);for(let G of R.proxies)if(G.start){let W=`${G.from}-${G.to}`;try{D("watch",`Starting command for ${W} with command: ${G.start.command}`,_),$.info(`Starting command for ${W}...`),await e.startProcess(W,G.start,_);let x=L(G.from),T=new URL(x.startsWith("http")?x:`http://${x}`),h=T.hostname||"localhost",d=Number(T.port)||80;try{await b(h,d,_),D("watch",`Dev server is ready at ${h}:${d}`,_)}catch(rR){D("watch",`Connection check failed, but continuing with proxy setup: ${rR}`,_),$.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(x){throw D("watch",`Failed to start command for ${W}: ${x}`,_),Error(`Failed to start command for ${W}: ${x}`)}}else D("watch",`No start command for proxy ${G.from} -> ${G.to}`,_)}else if("start"in R&&R.start){D("watch","Found start command in single proxy config",_);let G=`${R.from}-${R.to}`;try{if(R.start)D("watch",`Starting command: ${R.start.command}`,_),await e.startProcess(G,R.start,_);let W=L(R.from),x=new URL(W.startsWith("http")?W:`http://${W}`),T=x.hostname||"localhost",h=Number(x.port)||80;try{await b(T,h,_),D("watch",`Dev server is ready at ${T}:${h}`,_)}catch(d){D("watch",`Connection check failed, but continuing with proxy setup: ${d}`,_),$.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(W){throw D("watch",`Failed to run start command: ${W}`,_),Error(`Failed to run start command: ${W}`)}}else D("watch","No start command found in config",_);let K="proxies"in R&&Array.isArray(R.proxies)?R.proxies[0]?.to:("to"in R)?R.to:"rpx.localhost";if(z.platform!=="win32"&&(R.https||N)){if(!v())try{D("sudo","Pre-acquiring sudo credentials for privileged operations",_),NS("sudo -v",{stdio:"inherit"})}catch{D("sudo","Could not pre-acquire sudo credentials",_)}}let B=[];if(R.productionCerts){if(B=await LR(R.productionCerts,_),B.length>0)D("ssl",`Using ${B.length} production SNI cert(s): ${B.map((G)=>G.serverName).join(", ")}`,_)}if(R.https){let G=B.length>0?null:await P(R);if(!G&&B.length===0){if(D("ssl",`No valid or trusted certificates found for ${K}, generating new ones`,R.verbose),await _R(R),G=await P(R),!G)throw Error(`Failed to load SSL certificates after generation for ${K}`)}else D("ssl",`Using existing and trusted certificates for ${K}`,R.verbose);R._cachedSSLConfig=G}let A="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map((G)=>({...G,https:R.https,cleanup:R.cleanup,cleanUrls:G.cleanUrls??("cleanUrls"in R?R.cleanUrls:!1),vitePluginUsage:R.vitePluginUsage,changeOrigin:G.changeOrigin??R.changeOrigin,verbose:_,_cachedSSLConfig:R._cachedSSLConfig})):[{from:"from"in R?R.from:"localhost:5173",to:"to"in R?R.to:"rpx.localhost",cleanUrls:"cleanUrls"in R?R.cleanUrls:!1,https:R.https,cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,start:"start"in R?R.start:void 0,changeOrigin:R.changeOrigin,auth:"auth"in R?R.auth:void 0,verbose:_,_cachedSSLConfig:R._cachedSSLConfig}],X=A.map((G)=>G.to||"rpx.localhost"),Y=B.length>0?B:R._cachedSSLConfig??null,J=X.filter((G)=>G&&!G.includes("localhost")&&!G.includes("127.0.0.1")),V=["dev","app","page","new","day","foo"],Z=["test","localhost","local","example","invalid"],Q=[...new Set(J.map((G)=>G.split(".").pop()?.toLowerCase()))],I=Q.filter((G)=>!!G&&V.includes(G));if(I.length>0&&_)$.warn(`The following TLDs may not work reliably for local development: ${I.map((G)=>`.${G}`).join(", ")}`),$.info(" These TLDs have HSTS preloading which can bypass local DNS"),$.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(N&&z.platform==="darwin"&&J.length>0){let{setupDevelopmentDns:G}=await import("./chunk-r1k4wgag.js");if(await G({domains:J,verbose:_})){if(_)if(Q.every((T)=>!!T&&Z.includes(T)))$.success(`DNS server started for ${Q.map((T)=>`.${T}`).join(", ")} domains`);else $.success(`DNS server started for ${Q.map((T)=>`.${T}`).join(", ")} domains (hosts file entries also added)`)}else D("dns","Could not start DNS server - custom domains may not resolve",_)}let F=async()=>{D("cleanup","Starting cleanup handler",R.verbose);try{let{tearDownDevelopmentDns:G}=await import("./chunk-r1k4wgag.js");await G({verbose:R.verbose})}catch(G){D("cleanup",`Error stopping DNS server: ${G}`,R.verbose)}try{await e.stopAll(R.verbose)}catch(G){D("cleanup",`Error stopping processes: ${G}`,R.verbose)}await a({domains:X,hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose||!1})};z.on("SIGINT",F),z.on("SIGTERM",F);let O=R.singlePortMode===!0,M=R.httpsPort??443,w=R.httpPort??80,j=R.originGuard?zR(R.originGuard):null,l=!!Y&&(A.length>1||O),c=!Y&&O&&A.length>0;if(l&&Y){D("proxies",`Creating shared HTTPS server for ${A.length} domains on port ${M}`,_);let G=await pR(A,N,_);if(!await f(w,"0.0.0.0",_))tR(_,w,M,R.acmeChallengeWebroot);if(await f(M,"0.0.0.0",_)){if(D("proxies",`Port ${M} is already in use, cannot start shared proxy`,_),_)$.warn(`Port ${M} is in use. Shared HTTPS proxy cannot start.`);return}if(!HR({routeEntries:G,listenPort:M,sslConfig:Y,originGuard:j,verbose:_})){$.error(`Shared HTTPS proxy failed to bind :${M}; not exiting`);return}}else if(c){D("proxies",`Creating shared HTTP server for ${A.length} domains on port ${w}`,_);let G=await pR(A,N,_);if(await f(w,"0.0.0.0",_)){if(D("proxies",`Port ${w} is already in use, cannot start shared proxy`,_),_)$.warn(`Port ${w} is in use. Shared HTTP proxy cannot start.`);return}if(!HR({routeEntries:G,listenPort:w,sslConfig:null,originGuard:j,verbose:_})){$.error(`Shared HTTP proxy failed to bind :${w}; not exiting`);return}}else for(let G of A)try{let W=G.to||"rpx.localhost";D("proxy",`Starting proxy for ${W} with SSL config: ${!!Y}`,G.verbose),await cR({from:G.from||"localhost:5173",to:W,cleanUrls:G.cleanUrls||!1,https:G.https||!1,cleanup:G.cleanup||!1,vitePluginUsage:G.vitePluginUsage||!1,verbose:G.verbose||!1,_cachedSSLConfig:R._cachedSSLConfig,changeOrigin:G.changeOrigin||!1})}catch(W){D("proxies",`Failed to start proxy for ${G.to}: ${W}`,G.verbose),$.error(`Failed to start proxy for ${G.to}:`,W)}}async function pR(S,R,_){let N=[],K=new Set;for(let B of S){let A=B.to||"rpx.localhost",X=B.cleanUrls||!1,Y=B.path,J=yR(Y),V=KR(B.auth);if(B.redirect){let Z=wR(B.redirect);N.push({host:A,path:Y,route:{redirect:Z,basePath:J,auth:V}}),D("proxies",`Route: ${A}${Y??""} → redirect ${Z.status} ${Z.to}${V?" (auth)":""}`,_)}else if(B.static)N.push({host:A,path:Y,route:{static:ER(B.static,X),cleanUrls:X,basePath:J,auth:V}}),D("proxies",`Route: ${A}${Y??""} → static ${typeof B.static==="string"?B.static:B.static.dir}${V?" (auth)":""}`,_);else{let Z=L(B.from),Q=new URL(Z.startsWith("http")?Z:`http://${Z}`),I=NR(B.from??Q.host,B.loadBalancer);AR(I),RR.add(I),N.push({host:A,path:Y,route:{sourceHost:Q.host,upstreamPool:I,cleanUrls:X,changeOrigin:B.changeOrigin||!1,pathRewrites:B.pathRewrites,basePath:J,auth:V}}),D("proxies",`Route: ${A}${Y??""} → ${Q.host}${V?" (auth)":""}`,_)}if(K.has(A))continue;if(K.add(A),R&&!fR(A)&&!A.includes("localhost")&&!A.includes("127.0.0.1"))try{if(!(await U([A],_))[0])await y([A],_)}catch{D("hosts",`Could not add hosts entry for ${A}`,_)}}return N}function HR(S){let{routeEntries:R,listenPort:_,sslConfig:N,originGuard:K,verbose:B}=S,A=UR(R),X=FR((V,Z)=>PR(A,V,Z),B),Y=K?(V,Z)=>K(V)??X(V,Z):X,J=qR(B);try{let V=Bun.serve({port:_,hostname:"0.0.0.0",reusePort:gR(),...N?{tls:Array.isArray(N)?N.map((Z)=>({serverName:Z.serverName,key:Z.key,cert:Z.cert})):{key:N.key,cert:N.cert,ca:N.ca,requestCert:!1,rejectUnauthorized:!1}}:{},fetch(Z,Q){return Y(Z,Q)},websocket:J,error(Z){return D("server",`Shared proxy server error: ${Z}`,B),new Response(`Server Error: ${Z.message}`,{status:500})}});return o.add(V),D("proxies",`Shared ${N?"HTTPS":"HTTP"} proxy listening on port ${_} for ${A.size} domains`,B),V}catch(V){return D("proxies",`Failed to start shared proxy: ${V}`,B),console.error("Failed to start shared proxy:",V),null}}function XS(S){if(S?.vitePluginUsage||!S?.verbose)return;if(console.log(""),console.log(` ${k.green(k.bold("rpx"))} ${k.green(`v${AS}`)}`),console.log(` ${k.green("➜")} ${k.dim(S?.from??"")} ${k.dim("➜")} ${k.cyan(S?.ssl?`https://${S?.to}`:`http://${S?.to}`)}`),S?.listenPort!==(S?.ssl?443:80))console.log(` ${k.green("➜")} Listening on port ${S?.listenPort}`);if(S?.cleanUrls)console.log(` ${k.green("➜")} Clean URLs enabled`)}var MN=uR;export{XR as writeEntry,Z_ as watchRegistry,IS as verifyHttpsChain,FS as trustRootCaForBrowsers,h_ as tearDownDevelopmentDns,c_ as syncDevelopmentDnsFromRegistry,nS as stripBasePath,BR as stopHealthChecks,C_ as stopDnsServer,i_ as stopDaemon,cR as startServer,$S as startProxy,uR as startProxies,AR as startHealthChecks,q_ as startDnsServer,D_ as siteIdForHost,gR as shouldReusePort,P_ as setupResolver,O_ as setupDevelopmentDns,eS as serverNameFromCertFilename,gS as serveStaticFile,hS as selectUpstream,q as safeStringify,dS as safeRelativePath,fN as safeDeleteFile,g as runViaDaemon,g_ as runDaemon,y_ as resolverFilePath,k_ as resolverBasenamesForDomains,H_ as resolverBasenameForDomain,ER as resolveStaticRoute,pS as resolveStaticFile,wR as resolveRedirect,CN as resolvePathRewrite,KR as resolveAuth,z_ as renderStartingPage,M_ as renderFailedPage,u_ as removeResolver,L_ as removeLegacyTldResolvers,IR as removeHosts,JR as removeEntry,p_ as releaseDaemonLock,jN as redactSensitive,v_ as reconcileStaleDevelopmentDns,t_ as reconcileDevelopmentDnsOnIdle,A_ as readSiteManifest,J_ as readEntry,a_ as readDaemonPid,QS as readCertSha256Fingerprint,VS as readCertCommonName,Q_ as readAll,CR as readAcmeChallenge,kS as pruneStaleRootCas,N_ as projectNameFromHost,L as primaryUpstreamUrl,G_ as portManager,rS as pathPrefixMatches,WS as parseSha256HashesFromSecurityListing,mS as parseHtpasswd,JS as normalizeSha256Fingerprint,yR as normalizePathPrefix,T_ as normalizeDevDomain,WR as matchesWildcard,oS as matchesAllowedSuffix,PR as matchHostRoute,sS as matchHostList,tS as matchHost,uS as markSuccess,vS as markFailure,PS as loadSSLConfig,Y_ as listDiscoverableSites,HS as listCertSha256HashesByCommonName,fR as isWildcardPattern,HN as isValidRootCA,GR as isValidId,FN as isSingleProxyOptions,qN as isSingleProxyConfig,wS as isRootCaTrustedForSsl,ES as isRootCaFingerprintInKeychains,lR as isProcessElevated,f as isPortInUse,X_ as isPidAlive,EN as isMultiProxyOptions,wN as isMultiProxyConfig,R_ as isLikelyHostname,f_ as isDnsServerRunning,l_ as isDaemonRunning,cS as isCertTrusted,DR as httpsConfig,v as getSudoPassword,US as getSharedDaemonCertPaths,yS as getRootCAPaths,$R as getRegistryDir,kN as getPrimaryDomain,TS as getMacosTrustKeychains,jS as getMacosLoginKeychainPath,m_ as getDaemonRpxDir,b_ as getDaemonPidPath,_R as generateCertificate,V_ as gcStaleEntries,LS as forceTrustCertificate,$_ as findAvailablePort,TN as extractHostname,__ as expandHome,xN as execSudoSync,W_ as escapeHtml,QR as ensureDaemonRunning,bS as enforceBasicAuth,CS as devSslToSniEntries,w_ as devDomainsFromHosts,B_ as detectProjectPreset,bR as deriveIdFromTarget,n_ as defaultDaemonSpawnCommand,SR as defaultConfig,MN as default,D as debugLog,NR as createUpstreamPool,K_ as createSiteResolver,qR as createProxyWebSocketHandler,FR as createProxyFetchHandler,zR as createOriginGuard,lS as contentTypeFor,U_ as contentLooksLikeRpxResolver,SR as config,k as colors,OS as clearSslConfigCache,kR as cleanupCertificates,a as cleanup,U as checkHosts,P as checkExistingCertificates,ZS as certIncludesSanHostnames,LR as buildSniTlsConfig,fS as buildRegistryTlsProxyOptions,aS as buildRedirectLocation,UR as buildHostRoutes,y as addHosts,d_ as acquireDaemonLock,I_ as SiteSupervisor,qS as SHARED_DEV_HOST_CERT_PATH,xS as RPX_ROOT_CA_COMMON_NAME,F_ as RPX_RESOLVER_MARKER,S_ as OnDemandCertManager,MS as MACOS_SYSTEM_KEYCHAIN,zS as MACOS_CA_TRUST_FLAGS,j_ as LEGACY_TLD_RESOLVER_LABELS,OR as DefaultPortManager,x_ as DNS_STATE_VERSION,E_ as DNS_PORT,iS as ACME_CHALLENGE_PREFIX};
@@ -0,0 +1,65 @@
1
+ import type { LoadBalancerConfig, LoadBalancerStrategy, ProxyFrom } from './types';
2
+ /**
3
+ * The first upstream's `host:port` from a {@link ProxyFrom}, for call sites
4
+ * that need a single representative address (connection testing before the
5
+ * pool exists, hosts-file checks, id derivation, log lines) rather than the
6
+ * full pool. Defaults to `'localhost:5173'` to match rpx's historical
7
+ * single-upstream default when `from` is unset.
8
+ */
9
+ export declare function primaryUpstreamUrl(from?: ProxyFrom): string;
10
+ /**
11
+ * Build a pool from a route's `from` (single string or array) plus optional
12
+ * load-balancer config. Always returns at least one upstream when `from` is
13
+ * set to a non-empty string/array; callers pass `''`/`[]` only for
14
+ * static/redirect routes that carry no upstream at all (in which case use of
15
+ * the pool is skipped entirely).
16
+ */
17
+ export declare function createUpstreamPool(from: ProxyFrom, lbConfig?: LoadBalancerConfig): UpstreamPool;
18
+ /**
19
+ * Pick the next upstream per the pool's configured strategy, considering only
20
+ * currently-healthy upstreams (unless the pool is a degenerate single-upstream
21
+ * pool — see {@link healthyUpstreams}). Returns `undefined` when the pool has
22
+ * no upstreams at all, or every upstream (in an N>1 pool) is unhealthy — the
23
+ * caller should respond with a 502 in that case.
24
+ */
25
+ export declare function selectUpstream(pool: UpstreamPool): UpstreamState | undefined;
26
+ /** Record a successful outcome for `upstream` — passive health-check bookkeeping. */
27
+ export declare function markSuccess(pool: UpstreamPool, upstream: UpstreamState): void;
28
+ /** Record a failed outcome for `upstream` — passive health-check bookkeeping. */
29
+ export declare function markFailure(pool: UpstreamPool, upstream: UpstreamState): void;
30
+ /**
31
+ * Start active health checking for `pool` when `healthCheck.enabled` — probes
32
+ * every upstream on `healthCheck.interval`, independent of live traffic. No-op
33
+ * (and idempotent) when active checks are disabled or already running. Always
34
+ * pair with {@link stopHealthChecks} on server shutdown so the interval
35
+ * doesn't leak.
36
+ */
37
+ export declare function startHealthChecks(pool: UpstreamPool): void;
38
+ /** Stop active health checking for `pool`, if running. Safe to call repeatedly. */
39
+ export declare function stopHealthChecks(pool: UpstreamPool): void;
40
+ /** Per-upstream runtime state tracked across requests. */
41
+ export declare interface UpstreamState {
42
+ url: string
43
+ weight: number
44
+ healthy: boolean
45
+ activeConnections: number
46
+ consecutiveFailures: number
47
+ consecutiveSuccesses: number
48
+ }
49
+ declare interface ResolvedHealthCheckConfig {
50
+ enabled: boolean
51
+ path: string
52
+ interval: number
53
+ timeout: number
54
+ healthyThreshold: number
55
+ unhealthyThreshold: number
56
+ }
57
+ /** A route's normalized upstream pool + selection/health state. */
58
+ export declare interface UpstreamPool {
59
+ upstreams: UpstreamState[]
60
+ strategy: LoadBalancerStrategy
61
+ healthCheck: ResolvedHealthCheckConfig
62
+ cursor: number
63
+ wrrCurrentWeights: number[]
64
+ healthCheckTimer: ReturnType<typeof setInterval> | null
65
+ }
@@ -2,6 +2,7 @@ import type { PathRewrite } from './types';
2
2
  import type { ResolvedAuth } from './auth';
3
3
  import type { ResolvedRedirect } from './redirect';
4
4
  import type { ResolvedStaticRoute } from './static-files';
5
+ import type { UpstreamPool } from './load-balancer';
5
6
  /**
6
7
  * Cap on client→upstream frames buffered *before the upstream WebSocket opens*.
7
8
  * Without it, a fast client (or an upstream that never opens) grows `pending`
@@ -34,6 +35,7 @@ export declare function createProxyFetchHandler(getRoute: GetRoute, verbose?: bo
34
35
  export declare function createProxyWebSocketHandler(verbose?: boolean): void;
35
36
  export declare interface ProxyRoute {
36
37
  sourceHost?: string
38
+ upstreamPool?: UpstreamPool
37
39
  cleanUrls?: boolean
38
40
  changeOrigin?: boolean
39
41
  pathRewrites?: PathRewrite[]
@@ -1,5 +1,5 @@
1
1
  import * as path from 'node:path';
2
- import type { BasicAuthConfig, PathRewrite, StaticRouteConfig } from './types';
2
+ import type { BasicAuthConfig, LoadBalancerConfig, PathRewrite, ProxyFrom, StaticRouteConfig } from './types';
3
3
  /**
4
4
  * Default location for the registry directory. The daemon's PID file and log
5
5
  * sit alongside it under `~/.stacks/rpx/`.
@@ -56,7 +56,7 @@ export declare function gcStaleEntries(dir?: string, verbose?: boolean): Promise
56
56
  export declare function watchRegistry(onChange: (entries: RegistryEntry[]) => void | Promise<void>, opts?: WatchOptions & { dir?: string }): WatchHandle;
57
57
  export declare interface RegistryEntry {
58
58
  id: string
59
- from?: string
59
+ from?: ProxyFrom
60
60
  to: string
61
61
  path?: string
62
62
  pid?: number
@@ -67,6 +67,7 @@ export declare interface RegistryEntry {
67
67
  changeOrigin?: boolean
68
68
  static?: string | StaticRouteConfig
69
69
  auth?: BasicAuthConfig
70
+ loadBalancer?: LoadBalancerConfig
70
71
  }
71
72
  export declare interface WatchHandle {
72
73
  close: () => void
package/dist/types.d.ts CHANGED
@@ -31,9 +31,35 @@ export declare interface BasicAuthConfig {
31
31
  users?: Array<{ username: string, password: string }>
32
32
  htpasswdFile?: string
33
33
  }
34
+ /**
35
+ * One upstream in a load-balanced pool, with an optional relative weight.
36
+ */
37
+ export declare interface UpstreamTarget {
38
+ url: string
39
+ weight?: number
40
+ }
41
+ /**
42
+ * Health checking for a load-balanced pool. Passive checks (derived from live
43
+ * traffic outcomes) are always on; active checks (periodic out-of-band probes)
44
+ * are opt-in via {@link enabled}.
45
+ */
46
+ export declare interface HealthCheckConfig {
47
+ enabled?: boolean
48
+ path?: string
49
+ interval?: number
50
+ timeout?: number
51
+ healthyThreshold?: number
52
+ unhealthyThreshold?: number
53
+ }
54
+ /** Per-route load-balancing configuration. */
55
+ export declare interface LoadBalancerConfig {
56
+ strategy?: LoadBalancerStrategy
57
+ healthCheck?: HealthCheckConfig
58
+ }
34
59
  export declare interface BaseProxyConfig {
35
- from?: string
60
+ from?: ProxyFrom
36
61
  to: string
62
+ loadBalancer?: LoadBalancerConfig
37
63
  auth?: BasicAuthConfig
38
64
  path?: string
39
65
  start?: StartOptions
@@ -173,6 +199,7 @@ export declare interface ProxySetupOptions extends Omit<ProxyOption, 'from'> {
173
199
  sourceUrl: Pick<URL, 'hostname' | 'host'>
174
200
  ssl: SSLConfig | null
175
201
  from: string
202
+ originalFrom?: ProxyFrom
176
203
  to: string
177
204
  portManager?: PortManager
178
205
  }
@@ -187,6 +214,15 @@ export declare interface PortManager {
187
214
  * - `flat`: `/about` → `<root>/about.html` (flat-file style).
188
215
  */
189
216
  export type PathRewriteStyle = 'directory' | 'flat';
217
+ /**
218
+ * Upstream `host:port` to forward to. Either a single string (backward
219
+ * compatible, degenerate one-item pool) or an array of upstreams — either
220
+ * plain `host:port` strings or {@link UpstreamTarget} objects with a weight —
221
+ * to load-balance across.
222
+ */
223
+ export type ProxyFrom = string | Array<string | UpstreamTarget>;
224
+ /** How {@link selectUpstream} picks the next upstream from a healthy pool. */
225
+ export type LoadBalancerStrategy = 'round-robin' | 'weighted-round-robin' | 'least-connections';
190
226
  export type BaseProxyOptions = Partial<BaseProxyConfig>;
191
227
  export type CleanupOptions = Partial<CleanupConfig>;
192
228
  export type SharedProxyOptions = Partial<SharedProxyConfig>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/rpx",
3
3
  "type": "module",
4
- "version": "0.11.23",
4
+ "version": "0.11.24",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",