@stacksjs/rpx 0.11.15 → 0.11.16

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.
@@ -46,4 +46,4 @@ export type ConfigOf = Config
46
46
  `.replace(this.ANSI_PATTERN,"");if(this.shouldWriteToFile())await this.writeToFile(G)}async prompt(Q){if(O())return Promise.resolve(!0);return new Promise((U)=>{console.error(`${K.cyan("?")} ${Q} (y/n) `);let X=(Z)=>{let $=Z.toString().trim().toLowerCase();L.stdin.removeListener("data",X);try{if(typeof L.stdin.setRawMode==="function")L.stdin.setRawMode(!1)}catch{}L.stdin.pause(),console.error(""),U($==="y"||$==="yes")};try{if(typeof L.stdin.setRawMode==="function")L.stdin.setRawMode(!0)}catch{}L.stdin.resume(),L.stdin.once("data",X)})}setFancy(Q){this.fancy=Q}isFancy(){return this.fancy}pause(){this.enabled=!1}resume(){this.enabled=!0}async start(Q,...U){if(!this.enabled)return;let X=Q;if(U&&U.length>0){let H=/%([sdijfo%])/g,q=0;if(X=Q.replace(H,(_,A)=>{if(A==="%")return"%";if(q>=U.length)return _;let z=U[q++];switch(A){case"s":return String(z);case"d":case"i":return Number(z).toString();case"j":case"o":return JSON.stringify(z,null,2);default:return _}}),q<U.length)X+=` ${U.slice(q).map((_)=>typeof _==="object"?JSON.stringify(_,null,2):String(_)).join(" ")}`}let{consoleText:Z,fileText:$}=this.buildOutputTexts(X);if(this.shouldStyleConsole()){let H=this.options.showTags!==!1&&this.name?K.gray(this.formatTag(this.name)):"",q=this.options.showIcons===!1?"":`${K.blue("◐")} `;console.error(`${q}${H} ${K.cyan(Z)}`)}let J=`[${new Date().toISOString()}] ${this.environment}.INFO: [START] ${$}
47
47
  `.replace(this.ANSI_PATTERN,"");if(this.shouldWriteToFile())await this.writeToFile(J)}renderProgressBar(Q,U=!1){if(!this.enabled||!this.shouldStyleConsole()||!L.stdout.isTTY)return;let X=Math.min(100,Math.max(0,Math.round(Q.current/Q.total*100))),Z=Math.round(Q.barLength*X/100),$=Q.barLength-Z,Y=K.green("━".repeat(Z)),G=K.gray("━".repeat($)),J=`[${Y}${G}]`,H=`${X}%`.padStart(4),q=Q.message?` ${Q.message}`:"",_=this.options.showIcons===!1?"":U||X===100?K.green("✓"):K.blue("▶"),A=this.options.showTags!==!1&&this.name?` ${K.gray(this.formatTag(this.name))}`:"",z=`\r${_}${A} ${J} ${H}${q}`,W=L.stdout.columns||80,V=" ".repeat(Math.max(0,W-z.replace(this.ANSI_PATTERN,"").length));if(Q.lastRenderedLine=`${z}${V}`,L.stdout.write(Q.lastRenderedLine),U)L.stdout.write(`
48
48
  `)}finishProgressBar(Q,U){if(!this.enabled||!this.fancy||O()||!L.stdout.isTTY){this.activeProgressBar=null;return}if(Q.current<Q.total)Q.current=Q.total;if(U)Q.message=U;this.renderProgressBar(Q,!0),this.activeProgressBar=null}async clear(Q={}){if(O()){console.warn("Log clearing is not supported in browser environments.");return}try{console.warn("Clearing logs...",this.config.logDirectory);let U=await DQ(this.config.logDirectory),X=[];for(let Z of U){if(!(Q.name?new RegExp(Q.name.replace("*",".*")).test(Z):Z.startsWith(this.name))||!Z.endsWith(".log"))continue;let Y=GQ(this.config.logDirectory,Z);if(Q.before)try{if((await YQ(Y)).mtime>=Q.before)continue}catch(G){console.error(`Failed to get stats for file ${Y}:`,G);continue}X.push(Y)}if(X.length===0){console.warn("No log files matched the criteria for clearing.");return}console.warn(`Preparing to delete ${X.length} log file(s)...`);for(let Z of X)try{await NQ(Z),console.warn(`Deleted log file: ${Z}`)}catch($){console.error(`Failed to delete log file ${Z}:`,$)}console.warn("Log clearing process finished.")}catch(U){console.error("Error during log clearing process:",U)}}}var A8=new BQ("stacks");var h0=new BQ("rpx",{showTags:!1});function d0(){return process.env.SUDO_PASSWORD}function R8(Q){let U=d0(),X=Q.replace(/'/g,"'\\''");if(U)return PX(`echo '${U}' | sudo -S sh -c '${X}' 2>/dev/null`,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]});try{return PX(`sudo -n sh -c '${X}'`,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]})}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}function bX(Q,U,X){if(X)h0.debug(`[rpx:${Q}] ${U}`)}var yX="[redacted]",f0=new Set(["certificate","privatekey","key","cert","ca","rootca","password","sudo_password"]),p0=/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/;function u0(Q){let U=Q.toLowerCase();return f0.has(U)||U.endsWith("password")||U.includes("secret")||U.includes("token")}function EU(Q){if(Array.isArray(Q))return Q.map((X)=>EU(X));if(typeof Q==="string")return p0.test(Q)?yX:Q;if(!Q||typeof Q!=="object")return Q;let U={};for(let[X,Z]of Object.entries(Q)){if(u0(X)){U[X]=yX;continue}U[X]=EU(Z)}return U}function L8(Q,U){return JSON.stringify(EU(Q),null,U)}function w8(Q){if(hX(Q))return Q.proxies.map((U)=>{let X=U.to||"stacks.localhost";return X.startsWith("http")?new URL(X).hostname:X});if(dX(Q)){let U=Q.to||"stacks.localhost";return[U.startsWith("http")?new URL(U).hostname:U]}return["stacks.localhost"]}function K8(Q){return typeof Q==="object"&&Q!==null&&"certificate"in Q&&"privateKey"in Q&&typeof Q.certificate==="string"&&typeof Q.privateKey==="string"}function j8(Q){if(!Q)return"stacks.localhost";if(hX(Q)&&Q.proxies.length>0)return Q.proxies[0].to||"stacks.localhost";if(dX(Q))return Q.to||"stacks.localhost";return"stacks.localhost"}function F8(Q){return!!(Q&&("proxies"in Q)&&Array.isArray(Q.proxies))}function hX(Q){return"proxies"in Q&&Array.isArray(Q.proxies)}function dX(Q){return"to"in Q&&typeof Q.to==="string"}function I8(Q){return!!(Q&&("to"in Q)&&!("proxies"in Q))}function M8(Q,U){if(!U||U.length===0)return null;for(let X of U)if(Q===X.from||Q.startsWith(`${X.from}/`)){let Z=X.to.startsWith("http")?new URL(X.to).host:X.to,$=X.stripPrefix===!0?Q.slice(X.from.length)||"/":Q;return{targetHost:Z,targetPath:$}}return null}async function V8(Q,U){try{await vX.unlink(Q),bX("certificates",`Successfully deleted: ${Q}`,U)}catch(X){if(X.code!=="ENOENT")bX("certificates",`Warning: Could not delete ${Q}: ${X}`,U)}}
49
- export{r as Da,d0 as Ea,R8 as Fa,bX as Ga,EU as Ha,L8 as Ia,w8 as Ja,K8 as Ka,j8 as La,F8 as Ma,hX as Na,dX as Oa,I8 as Pa,M8 as Qa,V8 as Ra};
49
+ export{r as Ja,d0 as Ka,R8 as La,bX as Ma,EU as Na,L8 as Oa,w8 as Pa,K8 as Qa,j8 as Ra,F8 as Sa,hX as Ta,dX as Ua,I8 as Va,M8 as Wa,V8 as Xa};
@@ -1,10 +1,11 @@
1
+ import * as path from 'node:path';
1
2
  import type { PathRewrite, StaticRouteConfig } from './types';
2
3
  /**
3
4
  * Sanitize an arbitrary `to` host into a valid registry id. Drops anything
4
5
  * that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
5
6
  * leading/trailing dashes. Falls back to `'rpx'` if nothing's left.
6
7
  */
7
- export declare function deriveIdFromTarget(to: string): string;
8
+ export declare function deriveIdFromTarget(to: string, routePath?: string): string;
8
9
  /**
9
10
  * Register every proxy with the daemon and (unless `detached`) block until a
10
11
  * shutdown signal arrives. Throws if any id collides or the daemon fails to
@@ -15,6 +16,7 @@ export declare interface DaemonRunnerProxy {
15
16
  id?: string
16
17
  from?: string
17
18
  to: string
19
+ path?: string
18
20
  cleanUrls?: boolean
19
21
  changeOrigin?: boolean
20
22
  pathRewrites?: PathRewrite[]
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Normalize a path prefix to a leading-slash, no-trailing-slash form so prefix
3
+ * comparisons are predictable. `''`/`undefined`/`'/'` all normalize to `'/'`
4
+ * (the host default). `'/api/'` → `'/api'`, `'docs'` → `'/docs'`.
5
+ */
6
+ export declare function normalizePathPrefix(path: string | undefined): string;
7
+ /**
8
+ * True if `pathname` is matched by the prefix `prefix`. The root prefix `'/'`
9
+ * matches everything. A non-root prefix matches when the pathname equals it
10
+ * (`/api`), or continues with a `/` (`/api/x`) — so `/api` does NOT match
11
+ * `/apifoo`, only a real path-segment boundary.
12
+ */
13
+ export declare function pathPrefixMatches(pathname: string, prefix: string): boolean;
14
+ /**
15
+ * Build a {@link HostRoutes} table from a flat list of entries. Entries are
16
+ * grouped by host; within each host the path-routes are sorted longest-prefix
17
+ * first so {@link matchHostRoute} can take the first match. If two entries
18
+ * collide on the same (host, path) the later one wins (matching `Map.set`).
19
+ */
20
+ export declare function buildHostRoutes<T>(entries: Array<{ host: string, path?: string, route: T }>): HostRoutes<T>;
21
+ /**
22
+ * Find the path-route list for `hostname` in a {@link HostRoutes} table. Exact
23
+ * host match wins; otherwise the most-specific (deepest-suffix) wildcard wins —
24
+ * mirroring {@link import('./host-match').matchHost}.
25
+ */
26
+ export declare function matchHostList<T>(table: HostRoutes<T>, hostname: string): Array<PathRoute<T>> | undefined;
27
+ /**
28
+ * Resolve a (hostname, pathname) pair to a single route value. First the host
29
+ * is resolved ({@link matchHostList}); then the longest matching path prefix
30
+ * within that host wins. Returns `undefined` when no host matches, or a host
31
+ * matches but no path prefix (including the `'/'` default) covers the request.
32
+ */
33
+ export declare function matchHostRoute<T>(table: HostRoutes<T>, hostname: string, pathname: string): T | undefined;
34
+ /** One path-scoped route under a host. */
35
+ export declare interface PathRoute<T> {
36
+ path: string
37
+ route: T
38
+ }
39
+ /**
40
+ * A host-keyed routing table where each host owns an ordered set of
41
+ * path-scoped routes. Build it with {@link buildHostRoutes}.
42
+ */
43
+ export type HostRoutes<T> = Map<string, Array<PathRoute<T>>>;
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 { HostRoutes, PathRoute } from './host-routes';
13
14
  export type { ResolvedStaticRoute, StaticResolution } from './static-files';
14
15
  export type { SniTlsEntry } from './sni';
15
16
  export type { CertIssuer, OnDemandCertManagerOptions } from './on-demand';
@@ -102,8 +103,15 @@ export {
102
103
  runDaemon,
103
104
  stopDaemon,
104
105
  } from './daemon';
105
- export { createProxyFetchHandler, createProxyWebSocketHandler } from './proxy-handler';
106
+ export { createProxyFetchHandler, createProxyWebSocketHandler, stripBasePath } from './proxy-handler';
106
107
  export { isWildcardPattern, matchesWildcard, matchHost } from './host-match';
108
+ export {
109
+ buildHostRoutes,
110
+ matchHostList,
111
+ matchHostRoute,
112
+ normalizePathPrefix,
113
+ pathPrefixMatches,
114
+ } from './host-routes';
107
115
  export {
108
116
  contentTypeFor,
109
117
  resolveStaticFile,
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import{$ as hD,A as z_,Aa as W_,B as FD,Ba as W2,C as M_,Ca as X2,D as TD,E as ED,F as ID,G as wD,H as F_,I as T_,J as E_,K as jD,L as I_,M as HD,N as kD,O as CD,P as xD,Q as qD,R as Y_,S as $_,T as fD,U as G_,V as R_,W as yD,X as UD,Y as OD,Z as PD,_ as LD,a as X,aa as cD,b as B_,ba as uD,c as o_,ca as mD,d as e_,da as vD,e as _D,ea as bD,f as DD,fa as lD,g as BD,ga as aD,h as ND,ha as dD,i as KD,ia as pD,j as YD,ja as gD,k as $D,ka as iD,l as GD,la as nD,m as RD,ma as tD,n as WD,na as rD,o as XD,oa as sD,p as JD,pa as oD,q as QD,qa as eD,r as ZD,ra as _2,s as AD,sa as D2,t as SD,ta as B2,u as VD,ua as N2,v as zD,va as K2,w as N_,wa as Y2,x as MD,xa as $2,y as u,ya as G2,z as K_,za as R2}from"./chunk-0zdj72ps.js";import{Da as V_,Ea as v,Fa as MB,Ga as N,Ha as FB,Ia as f,Ja as TB,Ka as EB,La as IB,Ma as wB,Na as jB,Oa as HB,Pa as kB,Qa as CB,Ra as xB}from"./chunk-hf6e07v4.js";import{execSync as p_}from"node:child_process";import*as L from"node:http";import*as c_ from"node:http2";import*as u_ from"node:net";import*as T from"node:process";var n=(D,_)=>(B)=>`\x1B[${D}m${B}\x1B[${_}m`,C={bold:n(1,22),dim:n(2,22),green:n(32,39),cyan:n(36,39)};import*as q_ from"node:fs";import*as f_ from"node:path";import*as q from"node:process";function y_(D){let _=D.replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return _.length>0?_:"rpx"}async function t(D){if(D.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let _=D.verbose??!1,B=D.registryDir,K=new Set,Y=D.proxies.map((V)=>{let E=V.id??y_(V.to);if(!$_(E))throw Error(`invalid registry id "${E}" derived from to="${V.to}"`);if(K.has(E))throw Error(`duplicate registry id "${E}" — set an explicit \`id\` on one of the proxies`);return K.add(E),{...V,id:E}}),R=new Date().toISOString();for(let V of Y)await G_({id:V.id,from:V.from,to:V.to,pid:D.persistent?void 0:q.pid,cwd:q.cwd(),createdAt:R,cleanUrls:V.cleanUrls,changeOrigin:V.changeOrigin,pathRewrites:V.pathRewrites,static:V.static},B,_);let G=await W_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let V of Y){let E=V.static?`static ${typeof V.static==="string"?V.static:V.static.dir}`:V.from;X.success(`https://${V.to} → ${E}`)}if(X.info(`(via rpx daemon pid=${G.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let A=!1,W=B??Y_(),Q=Y.map((V)=>V.id),I=async()=>{if(A)return;A=!0;for(let V of Q)await R_(V,B,_).catch((E)=>{N("runner",`removeEntry(${V}) failed: ${E}`,_)})},j=(V)=>{N("runner",`received ${V}, unregistering ${Q.length} entries`,_),I().finally(()=>q.exit(0))};q.once("SIGINT",j),q.once("SIGTERM",j),q.once("exit",()=>{if(A)return;for(let V of Q)try{q_.unlinkSync(f_.join(W,`${V}.json`))}catch{}}),await new Promise(()=>{})}import{exec as b_}from"node:child_process";import O from"node:fs";import O_ from"node:os";import J_ from"node:path";import*as b from"node:process";import{promisify as l_}from"node:util";var r=l_(b_);function U_(D){let _=D.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var k=b.platform==="win32"?J_.join(b.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",X_=!1;async function s(D){if(b.platform==="win32")throw Error("Administrator privileges required on Windows");let _=v(),B=D.replace(/'/g,"'\\''");try{if(_){let{stdout:K}=await r(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return X_=!0,K}if(X_)try{let{stdout:K}=await r(`sudo -n sh -c '${B}'`);return K}catch(K){N("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:K}=await r(`sudo -n sh -c '${B}'`);return X_=!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 h(D,_){let B=D.filter((Y)=>!U_(Y)),K=D.filter((Y)=>U_(Y));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: ${k}`,_);try{let Y;try{Y=await O.promises.readFile(k,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{Y=await s(`cat "${k}"`)}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 R=B.filter((W)=>{let Q=`127.0.0.1 ${W}`,I=`::1 ${W}`;return!Y.includes(Q)&&!Y.includes(I)});if(R.length===0){N("hosts","All hosts already exist in hosts file",_);return}let G=R.map((W)=>`
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)=>`
2
2
  # Added by rpx
3
3
  127.0.0.1 ${W}
4
4
  ::1 ${W}`).join(`
5
- `),A=J_.join(O_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await O.promises.writeFile(A,Y+G,"utf8"),await s(`cat "${A}" | tee "${k}" > /dev/null`),console.log(` Hosts updated: ${R.join(", ")}`)}catch(W){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),R.forEach((Q)=>{console.log(` 127.0.0.1 ${Q}`),console.log(` ::1 ${Q}`)}),console.log(` Or run: sudo nano ${k}`)}finally{try{await O.promises.unlink(A)}catch{}}}catch(Y){N("hosts",`Failed to manage hosts file: ${Y.message}`,_)}}async function Q_(D,_){N("hosts",`Removing hosts: ${D.join(", ")}`,_);try{let B;try{B=await O.promises.readFile(k,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{B=await s(`cat "${k}"`)}catch(W){throw N("hosts",`sudo read also failed: ${W}`,_),Error(`Cannot read hosts file: ${W}`)}}let K=B.split(`
6
- `),Y=!1,R=K.filter((W)=>{if(D.some((I)=>W.includes(` ${I}`)&&(W.includes("127.0.0.1")||W.includes("::1"))))return Y=!0,!1;if(W.trim()==="# Added by rpx")return Y=!0,!1;return!0});if(!Y){N("hosts","No matching hosts found to remove",_);return}while(R[R.length-1]?.trim()==="")R.pop();let G=`${R.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(`
7
7
  `)}
8
- `,A=J_.join(O_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await O.promises.writeFile(A,G,"utf8"),await s(`cat "${A}" | tee "${k}" > /dev/null`),N("hosts","Hosts removed successfully",_)}catch(W){N("hosts","Could not clean up hosts file automatically",_)}finally{try{await O.promises.unlink(A)}catch(W){N("hosts",`Failed to remove temporary file: ${W}`,_)}}}catch(B){N("hosts",`Failed to clean up hosts file: ${B.message}`,_)}}async function c(D,_){N("hosts",`Checking hosts: ${D}`,_);let B;try{B=await O.promises.readFile(k,"utf-8")}catch(K){N("hosts",`Error reading hosts file: ${K}`,_);try{let Y=v(),R;if(Y)R=`echo '${Y}' | sudo -S cat "${k}" 2>/dev/null`;else R=`sudo -n cat "${k}" 2>/dev/null || cat "${k}" 2>/dev/null || echo ""`;let{stdout:G}=await r(R);B=G}catch(Y){return N("hosts",`Cannot read hosts file, assuming entries don't exist: ${Y}`,_),D.map(()=>!1)}}return D.map((K)=>{let Y=`127.0.0.1 ${K}`,R=`::1 ${K}`;return B.includes(Y)||B.includes(R)})}import*as o from"node:net";function U(D,_,B){return N("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((K)=>{let Y=o.createServer(),R=setTimeout(()=>{N("port",`Checking port ${D} timed out, assuming it's in use`,B),Y.close(),K(!0)},3000);Y.once("error",(G)=>{if(clearTimeout(R),G.code==="EADDRINUSE")N("port",`Port ${D} is in use`,B),K(!0);else N("port",`Error checking port ${D}: ${G.message}`,B),K(!0)}),Y.once("listening",()=>{clearTimeout(R),N("port",`Port ${D} is available`,B),Y.close(),K(!1)});try{Y.listen(D,_)}catch(G){clearTimeout(R),N("port",`Exception checking port ${D}: ${G}`,B),K(!0)}})}async function L_(D,_,B,K=50){N("port",`Finding available port starting from ${D} (max attempts: ${K})`,B);let Y=D,R=0;while(R<K){if(R++,!await U(Y,_,B))return N("port",`Found available port: ${Y} after ${R} attempts`,B),Y;N("port",`Port ${Y} is in use, trying ${Y+1} (attempt ${R}/${K})`,B),Y++}throw Error(`Unable to find available port after ${K} attempts starting from ${D}`)}function P_(D,_,B=5000,K){return N("port",`Testing connection to ${_}:${D}`,K),new Promise((Y)=>{let R=o.connect({host:_,port:D,timeout:B});R.once("connect",()=>{N("port",`Successfully connected to ${_}:${D}`,K),R.end(),Y(!0)}),R.once("timeout",()=>{N("port",`Connection to ${_}:${D} timed out`,K),R.destroy(),Y(!1)}),R.once("error",(G)=>{N("port",`Failed to connect to ${_}:${D}: ${G.message}`,K),R.destroy(),Y(!1)})})}class l{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 P_(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 L_(D,this.hostname,this.verbose,this.maxRetries);if(_){if(!await P_(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 a_=new l;import{spawn as d_}from"node:child_process";import*as P from"node:process";class e{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,...Y]=_.command.split(" "),R=_.cwd||P.cwd();N("start",`Starting process ${D}:`,B),N("start",` Command: ${K} ${Y.join(" ")}`,B),N("start",` Working directory: ${R}`,B),N("start",` Environment variables: ${f(_.env)}`,B);let G=d_(K,Y,{cwd:R,env:{...P.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(D,{command:_.command,cwd:R,process:G,env:_.env}),new Promise((A,W)=>{if(G.on("error",(Q)=>{if(!this.isShuttingDown)N("start",`Process ${D} failed to start: ${Q}`,B),this.processes.delete(D),W(Q),P.emit("SIGINT")}),G.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}`)),P.emit("SIGINT")}),B)G.stdout?.on("data",(Q)=>{N("process",`[${D}] ${Q.toString().trim()}`,!0)}),G.stderr?.on("data",(Q)=>{N("process",`[${D}] ERR: ${Q.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&G.killed)this.processes.delete(D),W(Error(`Process ${D} was killed during startup`));else N("start",`Process ${D} started successfully`,B),A()},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(Y){}}},3000)}catch(Y){N("start",`Error stopping process ${D}: ${Y}`,_),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 q2=new e;var D_=new e,g_="0.12.0",i_=new l("0.0.0.0"),a=new Set,Z_=!1,__=null,A_=null;async function p(D){if(Z_)return N("cleanup","Cleanup already in progress, skipping",D?.verbose),A_||Promise.resolve();Z_=!0,N("cleanup","Starting cleanup process",D?.verbose),A_=new Promise((_)=>{__=_});try{await D_.stopAll(D?.verbose),X.info("Shutting down proxy servers...");let _=[],B=Array.from(a).map((K)=>new Promise((Y)=>{K.close(()=>{N("cleanup","Server closed successfully",D?.verbose),Y()})}));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((Y)=>{if(Y==="test.local")return!0;return Y!=="localhost"&&!Y.startsWith("localhost.")&&Y!=="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(Q_(K,D?.verbose).then(()=>{N("cleanup",`Removed hosts entries for ${K.join(", ")}`,D?.verbose)}).catch((Y)=>{N("cleanup",`Failed to remove hosts entries: ${Y}`,D?.verbose),X.warn(`Failed to clean up hosts file entries for ${K.join(", ")}:`,Y)}))}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(Y)=>{try{await z_(Y,D?.verbose),N("cleanup",`Removed certificates for ${Y}`,D?.verbose)}catch(R){N("cleanup",`Failed to remove certificates for ${Y}: ${R}`,D?.verbose),X.warn(`Failed to clean up certificates for ${Y}:`,R)}});_.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(__)__();__=null,Z_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(T.env.NODE_ENV!=="test"&&T.env.BUN_ENV!=="test"&&!_)T.exit(0)}return A_}var S_=!1;function w_(D){if(S_){N("signal",`Received second ${D} signal, forcing exit`,!0),T.exit(1);return}S_=!0,N("signal",`Received ${D} signal, initiating cleanup`,!0),p().catch((_)=>{N("signal",`Cleanup failed after ${D}: ${_}`,!0),T.exit(1)}).finally(()=>{S_=!1})}T.once("SIGINT",()=>w_("SIGINT"));T.once("SIGTERM",()=>w_("SIGTERM"));T.on("uncaughtException",(D)=>{N("process",`Uncaught exception: ${D}`,!0),X.error("Uncaught exception:",D),w_("uncaughtException")});async function d(D,_,B,K=5){N("connection",`Testing connection to ${D}:${_} (retries left: ${K})`,B);let Y=15000,R=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 G=()=>new Promise((A,W)=>{let Q=u_.connect({host:D,port:_,timeout:3000});Q.once("connect",()=>{N("connection",`Successfully connected to ${D}:${_}`,B),Q.end(),A()}),Q.once("timeout",()=>{N("connection",`Connection to ${D}:${_} timed out`,B),Q.destroy(),W(Error("Connection timed out"))}),Q.once("error",(I)=>{N("connection",`Failed to connect to ${D}:${_}: ${I}`,B),Q.destroy(),W(I)})});try{await G()}catch(A){if(Date.now()-R>Y){N("connection",`Connection test timed out after ${Y}ms, but continuing anyway`,B),X.warn(`Connection test to ${D}:${_} timed out, but RPX will try to proceed anyway.`);return}if(A.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)),d(D,_,B,K-1);if(K>0)try{N("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((Q,I)=>{let j=L.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(V)=>{N("connection",`Received HTTP response with status: ${V.statusCode}`,B),Q()});j.on("error",(V)=>I(V)),j.on("timeout",()=>{j.destroy(),I(Error("HTTP request timed out"))}),j.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((I)=>setTimeout(I,2000)),d(D,_,B,K-1)}let W=`Failed to connect to ${D}:${_} after ${5-K} attempts: ${A.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 j_(D){N("server",`Starting server with options: ${f(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),Y=[B.hostname];if(H_(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 c(Y,D.verbose))[0]){X.info(`Adding ${B.hostname} to hosts file...`),X.info("This may require sudo/administrator privileges");try{await h(Y,D.verbose)}catch(A){if(X.error("Failed to add hosts entry:",A.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(G){X.error("Failed to check hosts file:",G.message)}}try{await d(_.hostname,K,D.verbose)}catch(G){N("server",`Connection test failed: ${G}`,D.verbose),X.error(G.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 R=D._cachedSSLConfig||null;if(D.https)try{if(D.https===!0)D.https=K_({...D,to:B.hostname});if(R=await u({...D,to:B.hostname,https:D.https}),!R){if(N("ssl",`Generating new certificates for ${B.hostname}`,D.verbose),await N_({...D,from:_.toString(),to:B.hostname,https:D.https}),R=await u({...D,to:B.hostname,https:D.https}),!R)throw Error(`Failed to load SSL configuration after generating certificates for ${B.hostname}`)}}catch(G){throw N("server",`SSL setup failed: ${G}`,D.verbose),G}N("server",`Setting up reverse proxy with SSL config for ${B.hostname}`,D.verbose),await t_({...D,from:D.from||"localhost:5173",to:B.hostname,fromPort:K,sourceUrl:{hostname:_.hostname,host:_.host},ssl:R})}async function n_(D,_,B,K,Y,R,G,A,W,Q,I){N("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${Q}`,W);function j(Z){let J={};for(let[S,z]of Object.entries(Z))if(!S.startsWith(":"))J[S]=z;return J}let V=(Z,J)=>{N("request",`Incoming request: ${Z.method} ${Z.url}`,W);let S=Z.url||"/",z=Z.method||"GET";if(Z instanceof c_.Http2ServerRequest){let M=Z.headers;z=M[":method"]||z,S=M[":path"]||S}if(Q){if(!S.match(/\.[a-z0-9]+$/i))if(S.endsWith("/"))S=`${S}index.html`;else S=`${S}.html`}let H=j(Z.headers);if(I)H.host=`${R.hostname}:${B}`,N("request",`Changed origin: setting host header to ${H.host}`,W);let F={hostname:R.hostname,port:B,path:S,method:z,headers:H};N("request",`Proxy request options: ${f(F)}`,W);let w=L.request(F,(M)=>{if(N("response",`Proxy response received with status ${M.statusCode}`,W),Q&&M.statusCode===404){let y=[];if(S.endsWith(".html"))y.push(S.slice(0,-5));else if(!S.match(/\.[a-z0-9]+$/i))y.push(`${S}.html`);if(!S.endsWith("/"))y.push(`${S}/index.html`);if(y.length>0){N("cleanUrls",`Trying alternative paths: ${y.join(", ")}`,W);let m=(g)=>{if(g.length===0){J.writeHead(M.statusCode||404,M.headers),M.pipe(J);return}let C_=g[0],v_={...F,path:C_},x_=L.request(v_,(i)=>{if(i.statusCode===200)N("cleanUrls",`Found matching path: ${C_}`,W),J.writeHead(i.statusCode,i.headers),i.pipe(J);else m(g.slice(1))});x_.on("error",()=>m(g.slice(1))),x_.end()};m(y);return}}let x={...M.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};J.writeHead(M.statusCode||500,x),M.pipe(J)});w.on("error",(M)=>{N("request",`Proxy request failed: ${M}`,W),X.error("Proxy request failed:",M),J.writeHead(502),J.end(`Proxy Error: ${M.message}`)}),Z.pipe(w)};if(N("server",`Creating server with SSL config: ${!!G}`,W),G)return new Promise((Z,J)=>{try{let S=Bun.serve({port:K,hostname:Y,tls:{key:G.key,cert:G.cert,ca:G.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(z){let H=new URL(z.url);N("request",`Bun.serve received: ${z.method} ${H.pathname}`,W);let F=`http://${R.host}`,w=new URL(H.pathname+H.search,F);try{let M=new Headers(z.headers);if(M.set("host",R.host),I)M.set("origin",F);M.set("x-forwarded-for","127.0.0.1"),M.set("x-forwarded-proto","https"),M.set("x-forwarded-host",_);let x=await fetch(w.toString(),{method:z.method,headers:M,body:z.body,redirect:"manual"}),y=new Headers(x.headers);if(Q&&H.pathname.endsWith(".html")){let m=H.pathname.replace(/\.html$/,"");return new Response(null,{status:301,headers:{Location:m}})}return new Response(x.body,{status:x.status,statusText:x.statusText,headers:y})}catch(M){return N("request",`Proxy error: ${M}`,W),new Response(`Proxy Error: ${M}`,{status:502})}},error(z){return N("server",`Bun.serve error: ${z}`,W),new Response(`Server Error: ${z.message}`,{status:500})}});a.add(S),h_({from:D,to:_,vitePluginUsage:A,listenPort:K,ssl:!0,cleanUrls:Q,verbose:W}),Z()}catch(S){J(S)}});let E=L.createServer(V);function $(Z){return a.add(Z),new Promise((J,S)=>{Z.listen(K,Y,()=>{N("server",`Server listening on port ${K}`,W),h_({from:D,to:_,vitePluginUsage:A,listenPort:K,ssl:!!G,cleanUrls:Q,verbose:W}),J()}),Z.on("error",(z)=>{N("server",`Server error: ${z}`,W),S(z)})})}return $(E)}async function t_(D){N("setup",`Setting up reverse proxy: ${f(D)}`,D.verbose);let{from:_,to:B,fromPort:K,sourceUrl:Y,ssl:R,verbose:G,cleanup:A,vitePluginUsage:W,changeOrigin:Q,cleanUrls:I}=D,j=80,V=443,E="0.0.0.0",$=D.portManager||i_,Z=H_(D);try{if(Z&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1")){if(!(await c([B],G))[0]){X.warn(`The hostname ${B} isn't in your hosts file. Adding it now...`);try{await h([B],G),X.success(`Added ${B} to your hosts file.`)}catch(F){X.error(`Failed to add ${B} to your hosts file: ${F}`),X.info(`You may need to manually add '127.0.0.1 ${B}' to your /etc/hosts file.`)}}}else if(Z&&T.platform!=="darwin"&&B&&B.includes("localhost")&&!B.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await c([B],G))[0]){N("hosts",`${B} not found in hosts file, adding...`,G);try{await h([B],G)}catch(F){N("hosts",`Failed to add ${B} to hosts file: ${F}`,G)}}}if(R&&!$.usedPorts.has(j)){if(!await U(j,E,G))N("setup","Starting HTTP redirect server",G),m_(G),$.usedPorts.add(j);else if(N("setup","Port 80 is in use, skipping HTTP redirect",G),G)X.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let J=R?V:j,S=await U(J,E,G),z;if(S){if(N("setup",`Port ${J} is already in use`,G),G)X.warn(`Port ${J} is already in use. This may be another instance of rpx or another service.`);if(J===443){if(z=await $.getNextAvailablePort(3443,!0),N("setup",`Using port ${z} instead of ${J}`,G),G)X.info(`Using port ${z} instead. Access your site at https://${B}:${z}`)}else if(z=await $.getNextAvailablePort(J+1000,!0),N("setup",`Using port ${z} instead of ${J}`,G),G)X.info(`Using port ${z} instead. Access your site at http://${B}:${z}`)}else z=J,$.usedPorts.add(z),N("setup",`Using standard ${J===443?"HTTPS":"HTTP"} port ${J} for ${B}`,G);await n_(_,B,K,z,E,Y,R,W,G,I,Q)}catch(J){N("setup",`Setup failed: ${J}`,G),X.error(`Failed to setup reverse proxy: ${J.message}`),p({domains:[B],hosts:typeof A==="boolean"?A:A?.hosts,certs:typeof A==="boolean"?A:A?.certs,verbose:G,vitePluginUsage:W})}}function m_(D){N("redirect","Starting HTTP redirect server",D);let _=L.createServer((B,K)=>{let Y=B.headers.host||"";N("redirect",`Redirecting request from ${Y}${B.url} to HTTPS`,D),K.writeHead(301,{Location:`https://${Y}${B.url}`}),K.end()}).listen(80);a.add(_),N("redirect","HTTP redirect server started",D)}function r_(D){let _={...B_,...D};if(N("proxy",`Starting proxy with options: ${f(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){X.error("viaDaemon mode requires both `from` and `to`");return}t({proxies:[{id:_.id,from:_.from,to:_.to,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()||"",Y=T.platform==="darwin"&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1"),R=["dev","app","page","new","day","foo"],G=["test","localhost","local","example","invalid"];if(Y&&R.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(Y)import("./chunk-pjwm8py7.js").then(({setupDevelopmentDns:W})=>{W({domains:[B],verbose:_.verbose}).then((Q)=>{if(Q)Promise.resolve().then(()=>{if(_.verbose)if(G.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 A={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:K_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};N("proxy",`Server options: ${f(A)}`,_.verbose),j_(A).catch((W)=>{N("proxy",`Failed to start proxy: ${W}`,_.verbose),X.error(`Failed to start proxy: ${W.message}`),p({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function s_(D){return D?.verbose||!1}function H_(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 k_(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=s_(_),K=H_(_);if(N("config",`Starting with config: ${f(_,2)}`,B),N("config",`Is multi-proxy? ${"proxies"in _}`,B),N("config",`Hosts management enabled? ${K}`,B),_.viaDaemon){let Z="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((J)=>({id:J.id,from:J.from,to:J.to,cleanUrls:J.cleanUrls??_.cleanUrls,changeOrigin:J.changeOrigin??_.changeOrigin,pathRewrites:J.pathRewrites})):[{id:_.id,from:_.from,to:_.to,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await t({proxies:Z,verbose:B});return}if("proxies"in _&&Array.isArray(_.proxies)){N("servers",`Found ${_.proxies.length} proxies in config`,B);for(let $ of _.proxies)if($.start){let Z=`${$.from}-${$.to}`;try{N("watch",`Starting command for ${Z} with command: ${$.start.command}`,B),X.info(`Starting command for ${Z}...`),await D_.startProcess(Z,$.start,B);let J=new URL($.from.startsWith("http")?$.from:`http://${$.from}`),S=J.hostname||"localhost",z=Number(J.port)||80;try{await d(S,z,B),N("watch",`Dev server is ready at ${S}:${z}`,B)}catch(H){N("watch",`Connection check failed, but continuing with proxy setup: ${H}`,B),X.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(J){throw N("watch",`Failed to start command for ${Z}: ${J}`,B),Error(`Failed to start command for ${Z}: ${J}`)}}else N("watch",`No start command for proxy ${$.from} -> ${$.to}`,B)}else if("start"in _&&_.start){N("watch","Found start command in single proxy config",B);let $=`${_.from}-${_.to}`;try{if(_.start)N("watch",`Starting command: ${_.start.command}`,B),await D_.startProcess($,_.start,B);let Z=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),J=Z.hostname||"localhost",S=Number(Z.port)||80;try{await d(J,S,B),N("watch",`Dev server is ready at ${J}:${S}`,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(Z){throw N("watch",`Failed to run start command: ${Z}`,B),Error(`Failed to run start command: ${Z}`)}}else N("watch","No start command found in config",B);let Y="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(T.platform!=="win32"&&(_.https||K)){if(!v())try{N("sudo","Pre-acquiring sudo credentials for privileged operations",B),p_("sudo -v",{stdio:"inherit"})}catch{N("sudo","Could not pre-acquire sudo credentials",B)}}if(_.https){let $=await u(_);if(!$){if(N("ssl",`No valid or trusted certificates found for ${Y}, generating new ones`,_.verbose),await N_(_),$=await u(_),!$)throw Error(`Failed to load SSL certificates after generation for ${Y}`)}else N("ssl",`Using existing and trusted certificates for ${Y}`,_.verbose);_._cachedSSLConfig=$}let R="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map(($)=>({...$,https:_.https,cleanup:_.cleanup,cleanUrls:$.cleanUrls??("cleanUrls"in _?_.cleanUrls:!1),vitePluginUsage:_.vitePluginUsage,changeOrigin:$.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=R.map(($)=>$.to||"rpx.localhost"),A=_._cachedSSLConfig,W=G.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],I=["test","localhost","local","example","invalid"],j=[...new Set(W.map(($)=>$.split(".").pop()?.toLowerCase()))],V=j.filter(($)=>!!$&&Q.includes($));if(V.length>0&&B)X.warn(`The following TLDs may not work reliably for local development: ${V.map(($)=>`.${$}`).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:$}=await import("./chunk-pjwm8py7.js");if(await $({domains:W,verbose:B})){if(B)if(j.every((S)=>!!S&&I.includes(S)))X.success(`DNS server started for ${j.map((S)=>`.${S}`).join(", ")} domains`);else X.success(`DNS server started for ${j.map((S)=>`.${S}`).join(", ")} domains (hosts file entries also added)`)}else N("dns","Could not start DNS server - custom domains may not resolve",B)}let E=async()=>{N("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-pjwm8py7.js");await $({verbose:_.verbose})}catch($){N("cleanup",`Error stopping DNS server: ${$}`,_.verbose)}try{await D_.stopAll(_.verbose)}catch($){N("cleanup",`Error stopping processes: ${$}`,_.verbose)}await p({domains:G,hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose||!1})};if(T.on("SIGINT",E),T.on("SIGTERM",E),T.on("uncaughtException",($)=>{N("process",`Uncaught exception: ${$}`,!0),console.error("Uncaught exception:",$),E()}),A&&R.length>1){N("proxies",`Creating shared HTTPS server for ${R.length} domains`,B);let $=new Map;for(let F of R){let w=F.to||"rpx.localhost",M=F.cleanUrls||!1;if(F.static)$.set(w,{static:M_(F.static,M),cleanUrls:M}),N("proxies",`Route: ${w} → static ${typeof F.static==="string"?F.static:F.static.dir}`,B);else{let x=new URL(F.from?.startsWith("http")?F.from:`http://${F.from}`);$.set(w,{sourceHost:x.host,cleanUrls:M,changeOrigin:F.changeOrigin||!1,pathRewrites:F.pathRewrites}),N("proxies",`Route: ${w} → ${x.host}`,B)}if(K&&!E_(w)&&!w.includes("localhost")&&!w.includes("127.0.0.1"))try{if(!(await c([w],B))[0])await h([w],B)}catch{N("hosts",`Could not add hosts entry for ${w}`,B)}}if(!await U(80,"0.0.0.0",B))m_(B);let J=443;if(await U(J,"0.0.0.0",B)){if(N("proxies",`Port ${J} is already in use, cannot start shared proxy`,B),B)X.warn(`Port ${J} is in use. Shared HTTPS proxy cannot start.`);return}let z=F_((F)=>I_($,F),B),H=T_(B);try{let F=Bun.serve({port:J,hostname:"0.0.0.0",tls:{key:A.key,cert:A.cert,ca:A.ca,requestCert:!1,rejectUnauthorized:!1},fetch(w,M){return z(w,M)},websocket:H,error(w){return N("server",`Shared proxy server error: ${w}`,B),new Response(`Server Error: ${w.message}`,{status:500})}});a.add(F),N("proxies",`Shared HTTPS proxy listening on port ${J} for ${$.size} domains`,B)}catch(F){N("proxies",`Failed to start shared proxy: ${F}`,B),console.error("Failed to start shared HTTPS proxy:",F),E()}}else for(let $ of R)try{let Z=$.to||"rpx.localhost";N("proxy",`Starting proxy for ${Z} with SSL config: ${!!A}`,$.verbose),await j_({from:$.from||"localhost:5173",to:Z,cleanUrls:$.cleanUrls||!1,https:$.https||!1,cleanup:$.cleanup||!1,vitePluginUsage:$.vitePluginUsage||!1,verbose:$.verbose||!1,_cachedSSLConfig:A,changeOrigin:$.changeOrigin||!1})}catch(Z){N("proxies",`Failed to start proxy for ${$.to}: ${Z}`,$.verbose),console.error(`Failed to start proxy for ${$.to}:`,Z),E()}}function h_(D){if(D?.vitePluginUsage||!D?.verbose)return;if(console.log(""),console.log(` ${C.green(C.bold("rpx"))} ${C.green(`v${g_}`)}`),console.log(` ${C.green("➜")} ${C.dim(D?.from??"")} ${C.dim("➜")} ${C.cyan(D?.ssl?`https://${D?.to}`:`http://${D?.to}`)}`),D?.listenPort!==(D?.ssl?443:80))console.log(` ${C.green("➜")} Listening on port ${D?.listenPort}`);if(D?.cleanUrls)console.log(` ${C.green("➜")} Clean URLs enabled`)}var zB=k_;export{G_ as writeEntry,PD as watchRegistry,BD as verifyHttpsChain,ZD as trustRootCaForBrowsers,oD as tearDownDevelopmentDns,sD as syncDevelopmentDnsFromRegistry,dD as stopDnsServer,W2 as stopDaemon,j_ as startServer,r_ as startProxy,k_ as startProxies,aD as startDnsServer,nD as setupResolver,rD as setupDevelopmentDns,HD as serverNameFromCertFilename,wD as serveStaticFile,f as safeStringify,ED as safeRelativePath,xB as safeDeleteFile,t as runViaDaemon,G2 as runDaemon,gD as resolverFilePath,mD as resolverBasenamesForDomains,uD as resolverBasenameForDomain,M_ as resolveStaticRoute,ID as resolveStaticFile,CB as resolvePathRewrite,eD as removeResolver,tD as removeLegacyTldResolvers,Q_ as removeHosts,R_ as removeEntry,$2 as releaseDaemonLock,FB as redactSensitive,_2 as reconcileStaleDevelopmentDns,X2 as reconcileDevelopmentDnsOnIdle,yD as readEntry,N2 as readDaemonPid,e_ as readCertSha256Fingerprint,_D as readCertCommonName,UD as readAll,XD as pruneStaleRootCas,a_ as portManager,ND as parseSha256HashesFromSecurityListing,o_ as normalizeSha256Fingerprint,cD as normalizeDevDomain,jD as matchesWildcard,CD as matchesAllowedSuffix,I_ as matchHost,VD as loadSSLConfig,WD as listCertSha256HashesByCommonName,E_ as isWildcardPattern,EB as isValidRootCA,$_ as isValidId,HB as isSingleProxyOptions,kB as isSingleProxyConfig,JD as isRootCaTrustedForSsl,QD as isRootCaFingerprintInKeychains,U as isPortInUse,fD as isPidAlive,jB as isMultiProxyOptions,wB as isMultiProxyConfig,xD as isLikelyHostname,pD as isDnsServerRunning,K2 as isDaemonRunning,FD as isCertTrusted,K_ as httpsConfig,v as getSudoPassword,SD as getSharedDaemonCertPaths,AD as getRootCAPaths,Y_ as getRegistryDir,IB as getPrimaryDomain,RD as getMacosTrustKeychains,GD as getMacosLoginKeychainPath,D2 as getDaemonRpxDir,B2 as getDaemonPidPath,N_ as generateCertificate,OD as gcStaleEntries,zD as forceTrustCertificate,L_ as findAvailablePort,TB as extractHostname,MB as execSudoSync,W_ as ensureDaemonRunning,vD as devDomainsFromHosts,y_ as deriveIdFromTarget,R2 as defaultDaemonSpawnCommand,B_ as defaultConfig,zB as default,N as debugLog,T_ as createProxyWebSocketHandler,F_ as createProxyFetchHandler,TD as contentTypeFor,iD as contentLooksLikeRpxResolver,B_ as config,C as colors,MD as clearSslConfigCache,z_ as cleanupCertificates,p as cleanup,c as checkHosts,u as checkExistingCertificates,DD as certIncludesSanHostnames,kD as buildSniTlsConfig,h as addHosts,Y2 as acquireDaemonLock,$D as RPX_ROOT_CA_COMMON_NAME,lD as RPX_RESOLVER_MARKER,qD as OnDemandCertManager,YD as MACOS_SYSTEM_KEYCHAIN,KD as MACOS_CA_TRUST_FLAGS,hD as LEGACY_TLD_RESOLVER_LABELS,l as DefaultPortManager,LD as DNS_STATE_VERSION,bD as DNS_PORT};
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};
@@ -1,5 +1,12 @@
1
1
  import type { PathRewrite } from './types';
2
2
  import type { ResolvedStaticRoute } from './static-files';
3
+ /**
4
+ * Strip the route's mount prefix (`basePath`) from a request pathname so a
5
+ * target mounted under `/docs` sees `/` for `/docs` and `/guide` for
6
+ * `/docs/guide`. A `/` (or empty) base strips nothing. The result always keeps
7
+ * a leading `/`.
8
+ */
9
+ export declare function stripBasePath(pathname: string, basePath?: string): string;
3
10
  /**
4
11
  * Build a Bun.serve-compatible `fetch` handler that routes requests based on
5
12
  * the `Host` header. Returns 404 when no route matches and 502 on upstream
@@ -21,10 +28,16 @@ export declare interface ProxyRoute {
21
28
  changeOrigin?: boolean
22
29
  pathRewrites?: PathRewrite[]
23
30
  static?: ResolvedStaticRoute
31
+ basePath?: string
32
+ stripBasePathPrefix?: boolean
24
33
  }
25
34
  /** Minimal shape of the Bun server needed for WebSocket upgrades. */
26
35
  export declare interface ProxyServer {
27
36
  upgrade: (req: Request, options?: { data?: any, headers?: any }) => boolean
28
37
  }
29
- export type GetRoute = (hostname: string) => ProxyRoute | undefined;
38
+ /*` app, `/docs*` static dir). Callers
39
+ * that only route by host can ignore the second argument — it's optional so
40
+ * existing host-only `getRoute` callbacks remain valid.
41
+ */
42
+ export type GetRoute = (hostname: string, pathname: string) => ProxyRoute | undefined;
30
43
  export type ProxyFetchHandler = (req: Request, server?: ProxyServer) => Promise<Response | undefined>;
@@ -1,3 +1,4 @@
1
+ import * as path from 'node:path';
1
2
  import type { PathRewrite, StaticRouteConfig } from './types';
2
3
  /**
3
4
  * Default location for the registry directory. The daemon's PID file and log
@@ -57,6 +58,7 @@ export declare interface RegistryEntry {
57
58
  id: string
58
59
  from?: string
59
60
  to: string
61
+ path?: string
60
62
  pid?: number
61
63
  cwd?: string
62
64
  createdAt: string
package/dist/types.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare interface StaticRouteConfig {
19
19
  export declare interface BaseProxyConfig {
20
20
  from?: string
21
21
  to: string
22
+ path?: string
22
23
  start?: StartOptions
23
24
  pathRewrites?: PathRewrite[]
24
25
  static?: string | StaticRouteConfig
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/rpx",
3
3
  "type": "module",
4
- "version": "0.11.15",
4
+ "version": "0.11.16",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -24,6 +24,13 @@ export interface DaemonRunnerProxy {
24
24
  /** Upstream `host:port`. Optional when `static` is set. */
25
25
  from?: string
26
26
  to: string
27
+ /**
28
+ * Optional path prefix this route owns under the host `to` (e.g. `'/api'`).
29
+ * Lets several routes share one host on different paths. When two proxies
30
+ * share `to` but differ by `path`, give each an explicit `id` (or rely on the
31
+ * path being folded into the derived id).
32
+ */
33
+ path?: string
27
34
  cleanUrls?: boolean
28
35
  changeOrigin?: boolean
29
36
  pathRewrites?: PathRewrite[]
@@ -62,8 +69,11 @@ export interface DaemonRunnerOptions {
62
69
  * that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
63
70
  * leading/trailing dashes. Falls back to `'rpx'` if nothing's left.
64
71
  */
65
- export function deriveIdFromTarget(to: string): string {
66
- const cleaned = to.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128)
72
+ export function deriveIdFromTarget(to: string, routePath?: string): string {
73
+ // Fold the path into the id so several routes on the same host don't collide
74
+ // (e.g. `stacksjs.com` + `/api` and `stacksjs.com` + `/docs`).
75
+ const base = routePath && routePath !== '/' ? `${to}${routePath}` : to
76
+ const cleaned = base.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 128)
67
77
  return cleaned.length > 0 ? cleaned : 'rpx'
68
78
  }
69
79
 
@@ -82,7 +92,7 @@ export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
82
92
 
83
93
  // Resolve and validate all ids up front so we don't half-register and bail.
84
94
  const resolved = opts.proxies.map((p) => {
85
- const id = p.id ?? deriveIdFromTarget(p.to)
95
+ const id = p.id ?? deriveIdFromTarget(p.to, p.path)
86
96
  if (!isValidId(id))
87
97
  throw new Error(`invalid registry id "${id}" derived from to="${p.to}"`)
88
98
  if (ids.has(id))
@@ -97,6 +107,7 @@ export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
97
107
  id: p.id,
98
108
  from: p.from,
99
109
  to: p.to,
110
+ path: p.path,
100
111
  pid: opts.persistent ? undefined : process.pid,
101
112
  cwd: process.cwd(),
102
113
  createdAt,
package/src/daemon.ts CHANGED
@@ -26,7 +26,8 @@ import * as process from 'node:process'
26
26
  import { log } from './logger'
27
27
  import { checkExistingCertificates, generateCertificate } from './https'
28
28
  import { createProxyFetchHandler, createProxyWebSocketHandler } from './proxy-handler'
29
- import { matchHost } from './host-match'
29
+ import { buildHostRoutes, matchHostRoute, normalizePathPrefix } from './host-routes'
30
+ import type { HostRoutes } from './host-routes'
30
31
  import { buildSniTlsConfig } from './sni'
31
32
  import { OnDemandCertManager } from './on-demand'
32
33
  import { resolveStaticRoute } from './static-files'
@@ -172,10 +173,12 @@ export async function releaseDaemonLock(rpxDir: string = getDaemonRpxDir()): Pro
172
173
  */
173
174
  function entryToRoute(entry: RegistryEntry): ProxyRoute {
174
175
  const cleanUrls = entry.cleanUrls ?? false
176
+ const basePath = normalizePathPrefix(entry.path)
175
177
  if (entry.static) {
176
178
  return {
177
179
  static: resolveStaticRoute(entry.static, cleanUrls),
178
180
  cleanUrls,
181
+ basePath,
179
182
  }
180
183
  }
181
184
  const from = entry.from ?? 'localhost:1'
@@ -185,6 +188,7 @@ function entryToRoute(entry: RegistryEntry): ProxyRoute {
185
188
  cleanUrls,
186
189
  changeOrigin: entry.changeOrigin ?? false,
187
190
  pathRewrites: entry.pathRewrites,
191
+ basePath,
188
192
  }
189
193
  }
190
194
 
@@ -352,17 +356,20 @@ export async function runDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle>
352
356
  const pidPath = await acquireDaemonLock(rpxDir)
353
357
 
354
358
  // Module-scoped state so the watcher and fetch handler share one routing view.
355
- // Routing table keyed by host pattern. Lookup prefers an exact match, then
356
- // the most-specific `*.suffix` wildcard (see `matchHost`).
357
- let routingTable = new Map<string, ProxyRoute>()
358
- const getRoute = (host: string): ProxyRoute | undefined => matchHost(routingTable, host)
359
+ // Routing table keyed by host pattern; each host owns an ordered list of
360
+ // path-scoped routes. Lookup prefers an exact host match, then the
361
+ // most-specific `*.suffix` wildcard (see `matchHostList`); within a host the
362
+ // longest matching path prefix wins (see `matchHostRoute`).
363
+ let routingTable: HostRoutes<ProxyRoute> = new Map()
364
+ const getRoute = (host: string, pathname: string): ProxyRoute | undefined =>
365
+ matchHostRoute(routingTable, host, pathname)
359
366
 
360
367
  function rebuild(entries: RegistryEntry[]): void {
361
- const next = new Map<string, ProxyRoute>()
362
- for (const e of entries)
363
- next.set(e.to, entryToRoute(e))
364
- routingTable = next
365
- debugLog('daemon', `routing table now covers ${next.size} host(s): ${Array.from(next.keys()).join(', ') || '<empty>'}`, verbose)
368
+ routingTable = buildHostRoutes(
369
+ entries.map(e => ({ host: e.to, path: e.path, route: entryToRoute(e) })),
370
+ )
371
+ const hosts = Array.from(routingTable.keys())
372
+ debugLog('daemon', `routing table now covers ${hosts.length} host(s): ${hosts.join(', ') || '<empty>'}`, verbose)
366
373
  }
367
374
 
368
375
  // Initial GC + load before binding so the very first request finds a route.