@stacksjs/rpx 0.11.13 → 0.11.14

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,4 +1,4 @@
1
- import type { PathRewrite } from './types';
1
+ import type { PathRewrite, StaticRouteConfig } from './types';
2
2
  /**
3
3
  * Sanitize an arbitrary `to` host into a valid registry id. Drops anything
4
4
  * that isn't `[a-zA-Z0-9._-]`, collapses runs to a single dash, and trims
@@ -13,11 +13,12 @@ export declare function deriveIdFromTarget(to: string): string;
13
13
  export declare function runViaDaemon(opts: DaemonRunnerOptions): Promise<void>;
14
14
  export declare interface DaemonRunnerProxy {
15
15
  id?: string
16
- from: string
16
+ from?: string
17
17
  to: string
18
18
  cleanUrls?: boolean
19
19
  changeOrigin?: boolean
20
20
  pathRewrites?: PathRewrite[]
21
+ static?: string | StaticRouteConfig
21
22
  }
22
23
  export declare interface DaemonRunnerOptions {
23
24
  proxies: DaemonRunnerProxy[]
package/dist/daemon.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TlsOption } from './types';
1
+ import type { ProductionTlsConfig, TlsOption } from './types';
2
2
  export declare function getDaemonRpxDir(): string;
3
3
  export declare function getDaemonPidPath(rpxDir?: string): string;
4
4
  /**
@@ -67,6 +67,7 @@ export declare interface DaemonOptions {
67
67
  httpPort?: number
68
68
  hostname?: string
69
69
  https?: TlsOption
70
+ productionCerts?: ProductionTlsConfig
70
71
  gcIntervalMs?: number
71
72
  }
72
73
  export declare interface DaemonHandle {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Host-based route matching with wildcard support.
3
+ *
4
+ * The routing table is keyed by host pattern. A pattern is either an exact
5
+ * hostname (`api.example.com`) or a wildcard (`*.example.com`). Lookup prefers
6
+ * an exact match, then the most-specific (deepest-suffix) wildcard.
7
+ *
8
+ * Kept dependency-free and pure so it's reusable from both the daemon and the
9
+ * in-process multi-proxy path, and trivially unit-testable.
10
+ */
11
+ export declare function isWildcardPattern(pattern: string): boolean;
12
+ /**
13
+ * True if `hostname` matches the wildcard `pattern` (`*.suffix`). A wildcard
14
+ * matches exactly one or more leading labels — `*.example.com` matches
15
+ * `a.example.com` and `a.b.example.com`, but NOT the bare apex `example.com`.
16
+ */
17
+ export declare function matchesWildcard(hostname: string, pattern: string): boolean;
18
+ /**
19
+ * Find the route value for `hostname` in a host-keyed map. Exact match wins;
20
+ * otherwise the matching wildcard with the longest (most-specific) suffix wins.
21
+ * Returns `undefined` when nothing matches.
22
+ */
23
+ export declare function matchHost<T>(table: Map<string, T>, hostname: string): T | undefined;
package/dist/index.d.ts CHANGED
@@ -9,7 +9,9 @@ export type {
9
9
  StopDaemonOptions,
10
10
  StopDaemonResult,
11
11
  } from './daemon';
12
- export type { GetRoute, ProxyFetchHandler, ProxyRoute } from './proxy-handler';
12
+ export type { GetRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './proxy-handler';
13
+ export type { ResolvedStaticRoute, StaticResolution } from './static-files';
14
+ export type { SniTlsEntry } from './sni';
13
15
  export type { DaemonRunnerOptions, DaemonRunnerProxy } from './daemon-runner';
14
16
  export { colors } from './colors';
15
17
  export { config, config as defaultConfig } from './config';
@@ -99,7 +101,16 @@ export {
99
101
  runDaemon,
100
102
  stopDaemon,
101
103
  } from './daemon';
102
- export { createProxyFetchHandler } from './proxy-handler';
104
+ export { createProxyFetchHandler, createProxyWebSocketHandler } from './proxy-handler';
105
+ export { isWildcardPattern, matchesWildcard, matchHost } from './host-match';
106
+ export {
107
+ contentTypeFor,
108
+ resolveStaticFile,
109
+ resolveStaticRoute,
110
+ safeRelativePath,
111
+ serveStaticFile,
112
+ } from './static-files';
113
+ export { buildSniTlsConfig, serverNameFromCertFilename } from './sni';
103
114
  export { deriveIdFromTarget, runViaDaemon } from './daemon-runner';
104
115
  export { cleanup } from './start';
105
116
  export { startProxies, startProxy, startServer } from './start';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import{$ as L2,A as z_,B as V2,C as M_,D as Y_,E as $_,F as R2,G as G_,H as W_,I as E2,J as z2,K as M2,L as S2,M as T2,N as F2,O as I2,P as w2,Q as j2,R as H2,S as k2,T as C2,U as q2,V as x2,W as U2,X as f2,Y as y2,Z as O2,_ as P2,a as J,aa as h2,b as B_,ba as c2,c as i_,ca as u2,d as n_,da as m2,e as t_,ea as v2,f as r_,fa as b2,g as s_,ga as l2,h as o_,ha as a2,i as e_,ia as d2,j as _2,ja as p2,k as D2,ka as g2,l as B2,la as i2,m as K2,ma as X_,n as N2,na as n2,o as Y2,oa as t2,p as $2,q as G2,r as W2,s as X2,t as J2,u as Q2,v as Z2,w as K_,x as A2,y as c,z as N_}from"./chunk-pncxrxde.js";import{Aa as NB,Ba as YB,Ca as $B,Da as GB,pa as E_,qa as m,ra as sD,sa as K,ta as oD,ua as C,va as eD,wa as _B,xa as DB,ya as BB,za as KB}from"./chunk-zs1tyy8z.js";import{execSync as v_}from"node:child_process";import*as O from"node:http";import*as y_ from"node:http2";import*as O_ from"node:net";import*as F from"node:process";var n=(D,_)=>(B)=>`\x1B[${D}m${B}\x1B[${_}m`,H={bold:n(1,22),dim:n(2,22),green:n(32,39),cyan:n(36,39)};import*as j_ from"node:fs";import*as H_ from"node:path";import*as k from"node:process";function k_(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,N=new Set,Y=D.proxies.map((R)=>{let $=R.id??k_(R.to);if(!$_($))throw Error(`invalid registry id "${$}" derived from to="${R.to}"`);if(N.has($))throw Error(`duplicate registry id "${$}" — set an explicit \`id\` on one of the proxies`);return N.add($),{...R,id:$}}),W=new Date().toISOString();for(let R of Y)await G_({id:R.id,from:R.from,to:R.to,pid:D.persistent?void 0:k.pid,cwd:k.cwd(),createdAt:W,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites},B,_);let G=await X_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let R of Y)J.success(`https://${R.to} → ${R.from}`);if(J.info(`(via rpx daemon pid=${G.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let V=!1,X=B??Y_(),A=Y.map((R)=>R.id),T=async()=>{if(V)return;V=!0;for(let R of A)await W_(R,B,_).catch(($)=>{K("runner",`removeEntry(${R}) failed: ${$}`,_)})},I=(R)=>{K("runner",`received ${R}, unregistering ${A.length} entries`,_),T().finally(()=>k.exit(0))};k.once("SIGINT",I),k.once("SIGTERM",I),k.once("exit",()=>{if(V)return;for(let R of A)try{j_.unlinkSync(H_.join(X,`${R}.json`))}catch{}}),await new Promise(()=>{})}import{exec as h_}from"node:child_process";import f from"node:fs";import q_ from"node:os";import Q_ from"node:path";import*as v from"node:process";import{promisify as c_}from"node:util";var r=c_(h_);function C_(D){let _=D.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var j=v.platform==="win32"?Q_.join(v.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",J_=!1;async function s(D){if(v.platform==="win32")throw Error("Administrator privileges required on Windows");let _=m(),B=D.replace(/'/g,"'\\''");try{if(_){let{stdout:N}=await r(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return J_=!0,N}if(J_)try{let{stdout:N}=await r(`sudo -n sh -c '${B}'`);return N}catch(N){K("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:N}=await r(`sudo -n sh -c '${B}'`);return J_=!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 L(D,_){let B=D.filter((Y)=>!C_(Y)),N=D.filter((Y)=>C_(Y));if(N.length>0)K("hosts",`Skipping /etc/hosts for loopback dev names: ${N.join(", ")}`,_);if(B.length===0)return;K("hosts",`Adding hosts: ${B.join(", ")}`,_),K("hosts",`Using hosts file at: ${j}`,_);try{let Y;try{Y=await f.promises.readFile(j,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{Y=await s(`cat "${j}"`)}catch(X){throw console.log(" Could not read hosts file — skipping hosts setup"),K("hosts",`sudo read also failed: ${X}`,_),Error(`Cannot read hosts file: ${X}`)}}let W=B.filter((X)=>{let A=`127.0.0.1 ${X}`,T=`::1 ${X}`;return!Y.includes(A)&&!Y.includes(T)});if(W.length===0){K("hosts","All hosts already exist in hosts file",_);return}let G=W.map((X)=>`
1
+ import{$ as hD,A as z_,B as TD,C as F_,D as MD,E as wD,F as ED,G as ID,H as T_,I as M_,J as w_,K as jD,L as E_,M as HD,N as kD,O as Y_,P as $_,Q as CD,R as G_,S as R_,T as qD,U as xD,V as fD,W as yD,X as UD,Y as OD,Z as PD,_ as LD,a as W,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 KD,ha as dD,i as ND,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 XD,na as rD,o as WD,oa as sD,p as JD,pa as oD,q as QD,qa as eD,r as ZD,ra as _2,s as VD,sa as D2,t as AD,ta as B2,u as SD,ua as K2,v as zD,va as N2,w as K_,wa as Y2,x as FD,xa as X_,y as u,ya as $2,z as N_,za as G2}from"./chunk-tx5hnj92.js";import{Aa as S_,Ba as v,Ca as VB,Da as K,Ea as AB,Fa as f,Ga as SB,Ha as zB,Ia as FB,Ja as TB,Ka as MB,La as wB,Ma as EB,Na as IB,Oa as jB}from"./chunk-3pgh05pc.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 M 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 x_ from"node:fs";import*as f_ from"node:path";import*as x 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,N=new Set,Y=D.proxies.map((S)=>{let w=S.id??y_(S.to);if(!$_(w))throw Error(`invalid registry id "${w}" derived from to="${S.to}"`);if(N.has(w))throw Error(`duplicate registry id "${w}" — set an explicit \`id\` on one of the proxies`);return N.add(w),{...S,id:w}}),R=new Date().toISOString();for(let S of Y)await G_({id:S.id,from:S.from,to:S.to,pid:D.persistent?void 0:x.pid,cwd:x.cwd(),createdAt:R,cleanUrls:S.cleanUrls,changeOrigin:S.changeOrigin,pathRewrites:S.pathRewrites,static:S.static},B,_);let G=await X_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let S of Y){let w=S.static?`static ${typeof S.static==="string"?S.static:S.static.dir}`:S.from;W.success(`https://${S.to} → ${w}`)}if(W.info(`(via rpx daemon pid=${G.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let V=!1,X=B??Y_(),Q=Y.map((S)=>S.id),E=async()=>{if(V)return;V=!0;for(let S of Q)await R_(S,B,_).catch((w)=>{K("runner",`removeEntry(${S}) failed: ${w}`,_)})},j=(S)=>{K("runner",`received ${S}, unregistering ${Q.length} entries`,_),E().finally(()=>x.exit(0))};x.once("SIGINT",j),x.once("SIGTERM",j),x.once("exit",()=>{if(V)return;for(let S of Q)try{x_.unlinkSync(f_.join(X,`${S}.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",W_=!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:N}=await r(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return W_=!0,N}if(W_)try{let{stdout:N}=await r(`sudo -n sh -c '${B}'`);return N}catch(N){K("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:N}=await r(`sudo -n sh -c '${B}'`);return W_=!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 h(D,_){let B=D.filter((Y)=>!U_(Y)),N=D.filter((Y)=>U_(Y));if(N.length>0)K("hosts",`Skipping /etc/hosts for loopback dev names: ${N.join(", ")}`,_);if(B.length===0)return;K("hosts",`Adding hosts: ${B.join(", ")}`,_),K("hosts",`Using hosts file at: ${k}`,_);try{let Y;try{Y=await O.promises.readFile(k,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{Y=await s(`cat "${k}"`)}catch(X){throw console.log(" Could not read hosts file — skipping hosts setup"),K("hosts",`sudo read also failed: ${X}`,_),Error(`Cannot read hosts file: ${X}`)}}let R=B.filter((X)=>{let Q=`127.0.0.1 ${X}`,E=`::1 ${X}`;return!Y.includes(Q)&&!Y.includes(E)});if(R.length===0){K("hosts","All hosts already exist in hosts file",_);return}let G=R.map((X)=>`
2
2
  # Added by rpx
3
3
  127.0.0.1 ${X}
4
4
  ::1 ${X}`).join(`
5
- `),V=Q_.join(q_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await f.promises.writeFile(V,Y+G,"utf8"),await s(`cat "${V}" | tee "${j}" > /dev/null`),console.log(` Hosts updated: ${W.join(", ")}`)}catch(X){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),W.forEach((A)=>{console.log(` 127.0.0.1 ${A}`),console.log(` ::1 ${A}`)}),console.log(` Or run: sudo nano ${j}`)}finally{try{await f.promises.unlink(V)}catch{}}}catch(Y){K("hosts",`Failed to manage hosts file: ${Y.message}`,_)}}async function Z_(D,_){K("hosts",`Removing hosts: ${D.join(", ")}`,_);try{let B;try{B=await f.promises.readFile(j,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{B=await s(`cat "${j}"`)}catch(X){throw K("hosts",`sudo read also failed: ${X}`,_),Error(`Cannot read hosts file: ${X}`)}}let N=B.split(`
6
- `),Y=!1,W=N.filter((X)=>{if(D.some((T)=>X.includes(` ${T}`)&&(X.includes("127.0.0.1")||X.includes("::1"))))return Y=!0,!1;if(X.trim()==="# Added by rpx")return Y=!0,!1;return!0});if(!Y){K("hosts","No matching hosts found to remove",_);return}while(W[W.length-1]?.trim()==="")W.pop();let G=`${W.join(`
5
+ `),V=J_.join(O_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await O.promises.writeFile(V,Y+G,"utf8"),await s(`cat "${V}" | tee "${k}" > /dev/null`),console.log(` Hosts updated: ${R.join(", ")}`)}catch(X){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(V)}catch{}}}catch(Y){K("hosts",`Failed to manage hosts file: ${Y.message}`,_)}}async function Q_(D,_){K("hosts",`Removing hosts: ${D.join(", ")}`,_);try{let B;try{B=await O.promises.readFile(k,"utf-8")}catch{K("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{B=await s(`cat "${k}"`)}catch(X){throw K("hosts",`sudo read also failed: ${X}`,_),Error(`Cannot read hosts file: ${X}`)}}let N=B.split(`
6
+ `),Y=!1,R=N.filter((X)=>{if(D.some((E)=>X.includes(` ${E}`)&&(X.includes("127.0.0.1")||X.includes("::1"))))return Y=!0,!1;if(X.trim()==="# Added by rpx")return Y=!0,!1;return!0});if(!Y){K("hosts","No matching hosts found to remove",_);return}while(R[R.length-1]?.trim()==="")R.pop();let G=`${R.join(`
7
7
  `)}
8
- `,V=Q_.join(q_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await f.promises.writeFile(V,G,"utf8"),await s(`cat "${V}" | tee "${j}" > /dev/null`),K("hosts","Hosts removed successfully",_)}catch(X){K("hosts","Could not clean up hosts file automatically",_)}finally{try{await f.promises.unlink(V)}catch(X){K("hosts",`Failed to remove temporary file: ${X}`,_)}}}catch(B){K("hosts",`Failed to clean up hosts file: ${B.message}`,_)}}async function h(D,_){K("hosts",`Checking hosts: ${D}`,_);let B;try{B=await f.promises.readFile(j,"utf-8")}catch(N){K("hosts",`Error reading hosts file: ${N}`,_);try{let Y=m(),W;if(Y)W=`echo '${Y}' | sudo -S cat "${j}" 2>/dev/null`;else W=`sudo -n cat "${j}" 2>/dev/null || cat "${j}" 2>/dev/null || echo ""`;let{stdout:G}=await r(W);B=G}catch(Y){return K("hosts",`Cannot read hosts file, assuming entries don't exist: ${Y}`,_),D.map(()=>!1)}}return D.map((N)=>{let Y=`127.0.0.1 ${N}`,W=`::1 ${N}`;return B.includes(Y)||B.includes(W)})}import*as o from"node:net";function x(D,_,B){return K("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((N)=>{let Y=o.createServer(),W=setTimeout(()=>{K("port",`Checking port ${D} timed out, assuming it's in use`,B),Y.close(),N(!0)},3000);Y.once("error",(G)=>{if(clearTimeout(W),G.code==="EADDRINUSE")K("port",`Port ${D} is in use`,B),N(!0);else K("port",`Error checking port ${D}: ${G.message}`,B),N(!0)}),Y.once("listening",()=>{clearTimeout(W),K("port",`Port ${D} is available`,B),Y.close(),N(!1)});try{Y.listen(D,_)}catch(G){clearTimeout(W),K("port",`Exception checking port ${D}: ${G}`,B),N(!0)}})}async function U_(D,_,B,N=50){K("port",`Finding available port starting from ${D} (max attempts: ${N})`,B);let Y=D,W=0;while(W<N){if(W++,!await x(Y,_,B))return K("port",`Found available port: ${Y} after ${W} attempts`,B),Y;K("port",`Port ${Y} is in use, trying ${Y+1} (attempt ${W}/${N})`,B),Y++}throw Error(`Unable to find available port after ${N} attempts starting from ${D}`)}function x_(D,_,B=5000,N){return K("port",`Testing connection to ${_}:${D}`,N),new Promise((Y)=>{let W=o.connect({host:_,port:D,timeout:B});W.once("connect",()=>{K("port",`Successfully connected to ${_}:${D}`,N),W.end(),Y(!0)}),W.once("timeout",()=>{K("port",`Connection to ${_}:${D} timed out`,N),W.destroy(),Y(!1)}),W.once("error",(G)=>{K("port",`Failed to connect to ${_}:${D}: ${G.message}`,N),W.destroy(),Y(!1)})})}class b{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 x(D,this.hostname,this.verbose))return this.findNextAvailablePort(D+1,_);if(_){if(!await x_(D,this.hostname,3000,this.verbose))return K("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 U_(D,this.hostname,this.verbose,this.maxRetries);if(_){if(!await x_(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){K("port",`Releasing port ${D}`,this.verbose),this.usedPorts.delete(D)}}var u_=new b;import{spawn as m_}from"node:child_process";import*as y from"node:process";class e{processes=new Map;isShuttingDown=!1;async startProcess(D,_,B){if(this.processes.has(D)){K("start",`Process ${D} is already running`,B);return}let[N,...Y]=_.command.split(" "),W=_.cwd||y.cwd();K("start",`Starting process ${D}:`,B),K("start",` Command: ${N} ${Y.join(" ")}`,B),K("start",` Working directory: ${W}`,B),K("start",` Environment variables: ${C(_.env)}`,B);let G=m_(N,Y,{cwd:W,env:{...y.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(D,{command:_.command,cwd:W,process:G,env:_.env}),new Promise((V,X)=>{if(G.on("error",(A)=>{if(!this.isShuttingDown)K("start",`Process ${D} failed to start: ${A}`,B),this.processes.delete(D),X(A),y.emit("SIGINT")}),G.on("exit",(A)=>{if(!this.isShuttingDown&&A!==null&&A!==0)K("start",`Process ${D} exited with code ${A}`,B),this.processes.delete(D),X(Error(`Process ${D} exited with code ${A}`)),y.emit("SIGINT")}),B)G.stdout?.on("data",(A)=>{K("process",`[${D}] ${A.toString().trim()}`,!0)}),G.stderr?.on("data",(A)=>{K("process",`[${D}] ERR: ${A.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&G.killed)this.processes.delete(D),X(Error(`Process ${D} was killed during startup`));else K("start",`Process ${D} started successfully`,B),V()},1000)})}async stopProcess(D,_){let B=this.processes.get(D);if(!B?.process){K("start",`No process found for ${D}`,_);return}return K("start",`Stopping process ${D}`,_),new Promise((N)=>{if(!B.process){N();return}B.process.once("exit",()=>{this.processes.delete(D),K("start",`Process ${D} stopped`,_),N()});try{B.process.kill("SIGTERM"),setTimeout(()=>{if(B.process){K("start",`Force killing process ${D}`,_);try{B.process.kill("SIGKILL")}catch(Y){}}},3000)}catch(Y){K("start",`Error stopping process ${D}: ${Y}`,_),this.processes.delete(D),N()}})}async stopAll(D){if(this.isShuttingDown){K("start","Already shutting down, skipping duplicate stopAll call",D);return}this.isShuttingDown=!0,K("start","Stopping all processes",D);let _=Array.from(this.processes.keys()).map((B)=>this.stopProcess(B,D).catch((N)=>{J.error(`Failed to stop process ${B}:`,N)}));await Promise.allSettled(_),this.processes.clear(),this.isShuttingDown=!1}isRunning(D){let _=this.processes.get(D);return!!_?.process&&!_.process.killed}}var VD=new e;var D_=new e,b_="0.12.0",l_=new b("0.0.0.0"),l=new Set,A_=!1,__=null,V_=null;async function d(D){if(A_)return K("cleanup","Cleanup already in progress, skipping",D?.verbose),V_||Promise.resolve();A_=!0,K("cleanup","Starting cleanup process",D?.verbose),V_=new Promise((_)=>{__=_});try{await D_.stopAll(D?.verbose),J.info("Shutting down proxy servers...");let _=[],B=Array.from(l).map((N)=>new Promise((Y)=>{N.close(()=>{K("cleanup","Server closed successfully",D?.verbose),Y()})}));if(_.push(...B),D?.hosts&&D.domains?.length){K("cleanup","Cleaning up hosts file entries",D?.verbose),K("cleanup",`Original domains for cleanup: ${JSON.stringify(D.domains)}`,D?.verbose);let N=D.domains.filter((Y)=>{if(Y==="test.local")return!0;return Y!=="localhost"&&!Y.startsWith("localhost.")&&Y!=="127.0.0.1"});if(K("cleanup",`Filtered domains for cleanup: ${JSON.stringify(N)}`,D?.verbose),N.length>0)J.info("Cleaning up hosts file entries..."),_.push(Z_(N,D?.verbose).then(()=>{K("cleanup",`Removed hosts entries for ${N.join(", ")}`,D?.verbose)}).catch((Y)=>{K("cleanup",`Failed to remove hosts entries: ${Y}`,D?.verbose),J.warn(`Failed to clean up hosts file entries for ${N.join(", ")}:`,Y)}))}if(D?.certs&&D.domains?.length){K("cleanup","Cleaning up SSL certificates",D?.verbose),J.info("Cleaning up SSL certificates...");let N=D.domains.map(async(Y)=>{try{await z_(Y,D?.verbose),K("cleanup",`Removed certificates for ${Y}`,D?.verbose)}catch(W){K("cleanup",`Failed to remove certificates for ${Y}: ${W}`,D?.verbose),J.warn(`Failed to clean up certificates for ${Y}:`,W)}});_.push(...N)}await Promise.allSettled(_),K("cleanup","All cleanup tasks completed successfully",D?.verbose),J.success("All cleanup tasks completed successfully")}catch(_){K("cleanup",`Error during cleanup: ${_}`,D?.verbose),J.error("Error during cleanup:",_)}finally{if(__)__();__=null,A_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(F.env.NODE_ENV!=="test"&&F.env.BUN_ENV!=="test"&&!_)F.exit(0)}return V_}var R_=!1;function S_(D){if(R_){K("signal",`Received second ${D} signal, forcing exit`,!0),F.exit(1);return}R_=!0,K("signal",`Received ${D} signal, initiating cleanup`,!0),d().catch((_)=>{K("signal",`Cleanup failed after ${D}: ${_}`,!0),F.exit(1)}).finally(()=>{R_=!1})}F.once("SIGINT",()=>S_("SIGINT"));F.once("SIGTERM",()=>S_("SIGTERM"));F.on("uncaughtException",(D)=>{K("process",`Uncaught exception: ${D}`,!0),J.error("Uncaught exception:",D),S_("uncaughtException")});async function a(D,_,B,N=5){K("connection",`Testing connection to ${D}:${_} (retries left: ${N})`,B);let Y=15000,W=Date.now();if(F.env.RPX_BYPASS_CONNECTION_TEST==="true"){K("connection",`Bypassing connection test for ${D}:${_} due to RPX_BYPASS_CONNECTION_TEST flag`,B);return}let G=()=>new Promise((V,X)=>{let A=O_.connect({host:D,port:_,timeout:3000});A.once("connect",()=>{K("connection",`Successfully connected to ${D}:${_}`,B),A.end(),V()}),A.once("timeout",()=>{K("connection",`Connection to ${D}:${_} timed out`,B),A.destroy(),X(Error("Connection timed out"))}),A.once("error",(T)=>{K("connection",`Failed to connect to ${D}:${_}: ${T}`,B),A.destroy(),X(T)})});try{await G()}catch(V){if(Date.now()-W>Y){K("connection",`Connection test timed out after ${Y}ms, but continuing anyway`,B),J.warn(`Connection test to ${D}:${_} timed out, but RPX will try to proceed anyway.`);return}if(V.code==="ECONNREFUSED"&&N>0)return K("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${N} retries left)`,B),await new Promise((A)=>setTimeout(A,2000)),a(D,_,B,N-1);if(N>0)try{K("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((A,T)=>{let I=O.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(R)=>{K("connection",`Received HTTP response with status: ${R.statusCode}`,B),A()});I.on("error",(R)=>T(R)),I.on("timeout",()=>{I.destroy(),T(Error("HTTP request timed out"))}),I.end()}),K("connection",`HTTP request to ${D}:${_} succeeded`,B);return}catch(A){return K("connection",`HTTP request to ${D}:${_} failed: ${A}`,B),K("connection",`Retrying socket connection in 2 seconds... (${N} retries left)`,B),await new Promise((T)=>setTimeout(T,2000)),a(D,_,B,N-1)}let X=`Failed to connect to ${D}:${_} after ${5-N} attempts: ${V.message}`;K("connection",`${X}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,B),J.warn(X),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 T_(D){K("server",`Starting server with options: ${C(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"),N=Number.parseInt(_.port)||(_.protocol.includes("https:")?443:80),Y=[B.hostname];if(!B.hostname.includes("localhost")&&!B.hostname.includes("127.0.0.1")){K("hosts",`Checking if hosts file entry exists for: ${B.hostname}`,D?.verbose);try{if(!(await h(Y,D.verbose))[0]){J.info(`Adding ${B.hostname} to hosts file...`),J.info("This may require sudo/administrator privileges");try{await L(Y,D.verbose)}catch(V){if(J.error("Failed to add hosts entry:",V.message),J.warn("You can manually add this entry to your hosts file:"),J.warn(`127.0.0.1 ${B.hostname}`),J.warn(`::1 ${B.hostname}`),F.platform==="win32")J.warn("On Windows:"),J.warn("1. Run notepad as administrator"),J.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else J.warn("On Unix systems:"),J.warn("sudo nano /etc/hosts")}}else K("hosts",`Host entry already exists for ${B.hostname}`,D.verbose)}catch(G){J.error("Failed to check hosts file:",G.message)}}try{await a(_.hostname,N,D.verbose)}catch(G){K("server",`Connection test failed: ${G}`,D.verbose),J.error(G.message),J.warn("Continuing with proxy setup despite connection test failure..."),J.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let W=D._cachedSSLConfig||null;if(D.https)try{if(D.https===!0)D.https=N_({...D,to:B.hostname});if(W=await c({...D,to:B.hostname,https:D.https}),!W){if(K("ssl",`Generating new certificates for ${B.hostname}`,D.verbose),await K_({...D,from:_.toString(),to:B.hostname,https:D.https}),W=await c({...D,to:B.hostname,https:D.https}),!W)throw Error(`Failed to load SSL configuration after generating certificates for ${B.hostname}`)}}catch(G){throw K("server",`SSL setup failed: ${G}`,D.verbose),G}K("server",`Setting up reverse proxy with SSL config for ${B.hostname}`,D.verbose),await d_({...D,from:D.from||"localhost:5173",to:B.hostname,fromPort:N,sourceUrl:{hostname:_.hostname,host:_.host},ssl:W})}async function a_(D,_,B,N,Y,W,G,V,X,A,T){K("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${A}`,X);function I(Z){let z={};for(let[Q,E]of Object.entries(Z))if(!Q.startsWith(":"))z[Q]=E;return z}let R=(Z,z)=>{K("request",`Incoming request: ${Z.method} ${Z.url}`,X);let Q=Z.url||"/",E=Z.method||"GET";if(Z instanceof y_.Http2ServerRequest){let S=Z.headers;E=S[":method"]||E,Q=S[":path"]||Q}if(A){if(!Q.match(/\.[a-z0-9]+$/i))if(Q.endsWith("/"))Q=`${Q}index.html`;else Q=`${Q}.html`}let w=I(Z.headers);if(T)w.host=`${W.hostname}:${B}`,K("request",`Changed origin: setting host header to ${w.host}`,X);let U={hostname:W.hostname,port:B,path:Q,method:E,headers:w};K("request",`Proxy request options: ${C(U)}`,X);let p=O.request(U,(S)=>{if(K("response",`Proxy response received with status ${S.statusCode}`,X),A&&S.statusCode===404){let q=[];if(Q.endsWith(".html"))q.push(Q.slice(0,-5));else if(!Q.match(/\.[a-z0-9]+$/i))q.push(`${Q}.html`);if(!Q.endsWith("/"))q.push(`${Q}/index.html`);if(q.length>0){K("cleanUrls",`Trying alternative paths: ${q.join(", ")}`,X);let u=(g)=>{if(g.length===0){z.writeHead(S.statusCode||404,S.headers),S.pipe(z);return}let I_=g[0],L_={...U,path:I_},w_=O.request(L_,(i)=>{if(i.statusCode===200)K("cleanUrls",`Found matching path: ${I_}`,X),z.writeHead(i.statusCode,i.headers),i.pipe(z);else u(g.slice(1))});w_.on("error",()=>u(g.slice(1))),w_.end()};u(q);return}}let P={...S.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};z.writeHead(S.statusCode||500,P),S.pipe(z)});p.on("error",(S)=>{K("request",`Proxy request failed: ${S}`,X),J.error("Proxy request failed:",S),z.writeHead(502),z.end(`Proxy Error: ${S.message}`)}),Z.pipe(p)};if(K("server",`Creating server with SSL config: ${!!G}`,X),G)return new Promise((Z,z)=>{try{let Q=Bun.serve({port:N,hostname:Y,tls:{key:G.key,cert:G.cert,ca:G.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(E){let w=new URL(E.url);K("request",`Bun.serve received: ${E.method} ${w.pathname}`,X);let U=`http://${W.host}`,p=new URL(w.pathname+w.search,U);try{let S=new Headers(E.headers);if(S.set("host",W.host),T)S.set("origin",U);S.set("x-forwarded-for","127.0.0.1"),S.set("x-forwarded-proto","https"),S.set("x-forwarded-host",_);let P=await fetch(p.toString(),{method:E.method,headers:S,body:E.body,redirect:"manual"}),q=new Headers(P.headers);if(A&&w.pathname.endsWith(".html")){let u=w.pathname.replace(/\.html$/,"");return new Response(null,{status:301,headers:{Location:u}})}return new Response(P.body,{status:P.status,statusText:P.statusText,headers:q})}catch(S){return K("request",`Proxy error: ${S}`,X),new Response(`Proxy Error: ${S}`,{status:502})}},error(E){return K("server",`Bun.serve error: ${E}`,X),new Response(`Server Error: ${E.message}`,{status:500})}});l.add(Q),f_({from:D,to:_,vitePluginUsage:V,listenPort:N,ssl:!0,cleanUrls:A,verbose:X}),Z()}catch(Q){z(Q)}});let $=O.createServer(R);function M(Z){return l.add(Z),new Promise((z,Q)=>{Z.listen(N,Y,()=>{K("server",`Server listening on port ${N}`,X),f_({from:D,to:_,vitePluginUsage:V,listenPort:N,ssl:!!G,cleanUrls:A,verbose:X}),z()}),Z.on("error",(E)=>{K("server",`Server error: ${E}`,X),Q(E)})})}return M($)}async function d_(D){K("setup",`Setting up reverse proxy: ${C(D)}`,D.verbose);let{from:_,to:B,fromPort:N,sourceUrl:Y,ssl:W,verbose:G,cleanup:V,vitePluginUsage:X,changeOrigin:A,cleanUrls:T}=D,I=80,R=443,$="0.0.0.0",M=D.portManager||l_;try{if(B&&!B.includes("localhost")&&!B.includes("127.0.0.1")){if(!(await h([B],G))[0]){J.warn(`The hostname ${B} isn't in your hosts file. Adding it now...`);try{await L([B],G),J.success(`Added ${B} to your hosts file.`)}catch(w){J.error(`Failed to add ${B} to your hosts file: ${w}`),J.info(`You may need to manually add '127.0.0.1 ${B}' to your /etc/hosts file.`)}}}else if(F.platform!=="darwin"&&B&&B.includes("localhost")&&!B.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await h([B],G))[0]){K("hosts",`${B} not found in hosts file, adding...`,G);try{await L([B],G)}catch(w){K("hosts",`Failed to add ${B} to hosts file: ${w}`,G)}}}if(W&&!M.usedPorts.has(I)){if(!await x(I,$,G))K("setup","Starting HTTP redirect server",G),P_(G),M.usedPorts.add(I);else if(K("setup","Port 80 is in use, skipping HTTP redirect",G),G)J.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let Z=W?R:I,z=await x(Z,$,G),Q;if(z){if(K("setup",`Port ${Z} is already in use`,G),G)J.warn(`Port ${Z} is already in use. This may be another instance of rpx or another service.`);if(Z===443){if(Q=await M.getNextAvailablePort(3443,!0),K("setup",`Using port ${Q} instead of ${Z}`,G),G)J.info(`Using port ${Q} instead. Access your site at https://${B}:${Q}`)}else if(Q=await M.getNextAvailablePort(Z+1000,!0),K("setup",`Using port ${Q} instead of ${Z}`,G),G)J.info(`Using port ${Q} instead. Access your site at http://${B}:${Q}`)}else Q=Z,M.usedPorts.add(Q),K("setup",`Using standard ${Z===443?"HTTPS":"HTTP"} port ${Z} for ${B}`,G);await a_(_,B,N,Q,$,Y,W,X,G,T,A)}catch(Z){K("setup",`Setup failed: ${Z}`,G),J.error(`Failed to setup reverse proxy: ${Z.message}`),d({domains:[B],hosts:typeof V==="boolean"?V:V?.hosts,certs:typeof V==="boolean"?V:V?.certs,verbose:G,vitePluginUsage:X})}}function P_(D){K("redirect","Starting HTTP redirect server",D);let _=O.createServer((B,N)=>{let Y=B.headers.host||"";K("redirect",`Redirecting request from ${Y}${B.url} to HTTPS`,D),N.writeHead(301,{Location:`https://${Y}${B.url}`}),N.end()}).listen(80);l.add(_),K("redirect","HTTP redirect server started",D)}function p_(D){let _={...B_,...D};if(K("proxy",`Starting proxy with options: ${C(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){J.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((X)=>{J.error(`Failed to register with rpx daemon: ${X.message}`),F.exit(1)});return}let B=_.to||"",N=B.split(".").pop()?.toLowerCase()||"",Y=F.platform==="darwin"&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1"),W=["dev","app","page","new","day","foo"],G=["test","localhost","local","example","invalid"];if(Y&&W.includes(N)&&_?.verbose)J.warn(`The .${N} TLD may not work reliably for local development`),J.info(` Google owns .${N} with HSTS preloading, which can bypass local DNS`),J.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(Y)import("./chunk-kbnzcycw.js").then(({setupDevelopmentDns:X})=>{X({domains:[B],verbose:_.verbose}).then((A)=>{if(A)Promise.resolve().then(()=>{if(_.verbose)if(G.includes(N))J.success(`DNS server started for .${N} domains`);else J.success(`DNS server started for .${N} domains (hosts file entry also added)`)});else K("dns",`Could not start DNS server - ${B} may not resolve in browser`,_.verbose)})}).catch((X)=>{K("dns",`Failed to start DNS server: ${X}`,_.verbose)});let V={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:N_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};K("proxy",`Server options: ${C(V)}`,_.verbose),T_(V).catch((X)=>{K("proxy",`Failed to start proxy: ${X}`,_.verbose),J.error(`Failed to start proxy: ${X.message}`),d({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function g_(D){return D?.verbose||!1}async function F_(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=g_(_);if(K("config",`Starting with config: ${C(_,2)}`,B),K("config",`Is multi-proxy? ${"proxies"in _}`,B),_.viaDaemon){let M="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((Z)=>({id:Z.id,from:Z.from,to:Z.to,cleanUrls:Z.cleanUrls??_.cleanUrls,changeOrigin:Z.changeOrigin??_.changeOrigin,pathRewrites:Z.pathRewrites})):[{id:_.id,from:_.from,to:_.to,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await t({proxies:M,verbose:B});return}if("proxies"in _&&Array.isArray(_.proxies)){K("servers",`Found ${_.proxies.length} proxies in config`,B);for(let $ of _.proxies)if($.start){let M=`${$.from}-${$.to}`;try{K("watch",`Starting command for ${M} with command: ${$.start.command}`,B),J.info(`Starting command for ${M}...`),await D_.startProcess(M,$.start,B);let Z=new URL($.from.startsWith("http")?$.from:`http://${$.from}`),z=Z.hostname||"localhost",Q=Number(Z.port)||80;try{await a(z,Q,B),K("watch",`Dev server is ready at ${z}:${Q}`,B)}catch(E){K("watch",`Connection check failed, but continuing with proxy setup: ${E}`,B),J.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(Z){throw K("watch",`Failed to start command for ${M}: ${Z}`,B),Error(`Failed to start command for ${M}: ${Z}`)}}else K("watch",`No start command for proxy ${$.from} -> ${$.to}`,B)}else if("start"in _&&_.start){K("watch","Found start command in single proxy config",B);let $=`${_.from}-${_.to}`;try{if(_.start)K("watch",`Starting command: ${_.start.command}`,B),await D_.startProcess($,_.start,B);let M=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),Z=M.hostname||"localhost",z=Number(M.port)||80;try{await a(Z,z,B),K("watch",`Dev server is ready at ${Z}:${z}`,B)}catch(Q){K("watch",`Connection check failed, but continuing with proxy setup: ${Q}`,B),J.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(M){throw K("watch",`Failed to run start command: ${M}`,B),Error(`Failed to run start command: ${M}`)}}else K("watch","No start command found in config",B);let N="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(F.platform!=="win32"&&(_.https||_.cleanup?.hosts!==!1)){if(!m())try{K("sudo","Pre-acquiring sudo credentials for privileged operations",B),v_("sudo -v",{stdio:"inherit"})}catch{K("sudo","Could not pre-acquire sudo credentials",B)}}if(_.https){let $=await c(_);if(!$){if(K("ssl",`No valid or trusted certificates found for ${N}, generating new ones`,_.verbose),await K_(_),$=await c(_),!$)throw Error(`Failed to load SSL certificates after generation for ${N}`)}else K("ssl",`Using existing and trusted certificates for ${N}`,_.verbose);_._cachedSSLConfig=$}let Y="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}],W=Y.map(($)=>$.to||"rpx.localhost"),G=_._cachedSSLConfig,V=W.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),X=["dev","app","page","new","day","foo"],A=["test","localhost","local","example","invalid"],T=[...new Set(V.map(($)=>$.split(".").pop()?.toLowerCase()))],I=T.filter(($)=>!!$&&X.includes($));if(I.length>0&&B)J.warn(`The following TLDs may not work reliably for local development: ${I.map(($)=>`.${$}`).join(", ")}`),J.info(" These TLDs have HSTS preloading which can bypass local DNS"),J.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(F.platform==="darwin"&&V.length>0){let{setupDevelopmentDns:$}=await import("./chunk-kbnzcycw.js");if(await $({domains:V,verbose:B})){if(B)if(T.every((z)=>!!z&&A.includes(z)))J.success(`DNS server started for ${T.map((z)=>`.${z}`).join(", ")} domains`);else J.success(`DNS server started for ${T.map((z)=>`.${z}`).join(", ")} domains (hosts file entries also added)`)}else K("dns","Could not start DNS server - custom domains may not resolve",B)}let R=async()=>{K("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-kbnzcycw.js");await $({verbose:_.verbose})}catch($){K("cleanup",`Error stopping DNS server: ${$}`,_.verbose)}try{await D_.stopAll(_.verbose)}catch($){K("cleanup",`Error stopping processes: ${$}`,_.verbose)}await d({domains:W,hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose||!1})};if(F.on("SIGINT",R),F.on("SIGTERM",R),F.on("uncaughtException",($)=>{K("process",`Uncaught exception: ${$}`,!0),console.error("Uncaught exception:",$),R()}),G&&Y.length>1){K("proxies",`Creating shared HTTPS server for ${Y.length} domains`,B);let $=new Map;for(let Q of Y){let E=Q.to||"rpx.localhost",w=new URL(Q.from?.startsWith("http")?Q.from:`http://${Q.from}`);if($.set(E,{sourceHost:w.host,cleanUrls:Q.cleanUrls||!1,changeOrigin:Q.changeOrigin||!1,pathRewrites:Q.pathRewrites}),K("proxies",`Route: ${E} → ${w.host}`,B),!E.includes("localhost")&&!E.includes("127.0.0.1"))try{if(!(await h([E],B))[0])await L([E],B)}catch{K("hosts",`Could not add hosts entry for ${E}`,B)}}if(!await x(80,"0.0.0.0",B))P_(B);let Z=443;if(await x(Z,"0.0.0.0",B)){if(K("proxies",`Port ${Z} is already in use, cannot start shared proxy`,B),B)J.warn(`Port ${Z} is in use. Shared HTTPS proxy cannot start.`);return}try{let Q=Bun.serve({port:Z,hostname:"0.0.0.0",tls:{key:G.key,cert:G.cert,ca:G.ca,requestCert:!1,rejectUnauthorized:!1},fetch:M_((E)=>$.get(E),B),error(E){return K("server",`Shared proxy server error: ${E}`,B),new Response(`Server Error: ${E.message}`,{status:500})}});l.add(Q),K("proxies",`Shared HTTPS proxy listening on port ${Z} for ${$.size} domains`,B)}catch(Q){K("proxies",`Failed to start shared proxy: ${Q}`,B),console.error("Failed to start shared HTTPS proxy:",Q),R()}}else for(let $ of Y)try{let M=$.to||"rpx.localhost";K("proxy",`Starting proxy for ${M} with SSL config: ${!!G}`,$.verbose),await T_({from:$.from||"localhost:5173",to:M,cleanUrls:$.cleanUrls||!1,https:$.https||!1,cleanup:$.cleanup||!1,vitePluginUsage:$.vitePluginUsage||!1,verbose:$.verbose||!1,_cachedSSLConfig:G,changeOrigin:$.changeOrigin||!1})}catch(M){K("proxies",`Failed to start proxy for ${$.to}: ${M}`,$.verbose),console.error(`Failed to start proxy for ${$.to}:`,M),R()}}function f_(D){if(D?.vitePluginUsage||!D?.verbose)return;if(console.log(""),console.log(` ${H.green(H.bold("rpx"))} ${H.green(`v${b_}`)}`),console.log(` ${H.green("➜")} ${H.dim(D?.from??"")} ${H.dim("➜")} ${H.cyan(D?.ssl?`https://${D?.to}`:`http://${D?.to}`)}`),D?.listenPort!==(D?.ssl?443:80))console.log(` ${H.green("➜")} Listening on port ${D?.listenPort}`);if(D?.cleanUrls)console.log(` ${H.green("➜")} Clean URLs enabled`)}var rD=F_;export{G_ as writeEntry,S2 as watchRegistry,s_ as verifyHttpsChain,W2 as trustRootCaForBrowsers,c2 as tearDownDevelopmentDns,h2 as syncDevelopmentDnsFromRegistry,x2 as stopDnsServer,n2 as stopDaemon,T_ as startServer,p_ as startProxy,F_ as startProxies,q2 as startDnsServer,O2 as setupResolver,L2 as setupDevelopmentDns,C as safeStringify,GB as safeDeleteFile,t as runViaDaemon,g2 as runDaemon,f2 as resolverFilePath,j2 as resolverBasenamesForDomains,w2 as resolverBasenameForDomain,$B as resolvePathRewrite,u2 as removeResolver,P2 as removeLegacyTldResolvers,Z_ as removeHosts,W_ as removeEntry,p2 as releaseDaemonLock,oD as redactSensitive,m2 as reconcileStaleDevelopmentDns,t2 as reconcileDevelopmentDnsOnIdle,E2 as readEntry,l2 as readDaemonPid,n_ as readCertSha256Fingerprint,t_ as readCertCommonName,z2 as readAll,Y2 as pruneStaleRootCas,u_ as portManager,o_ as parseSha256HashesFromSecurityListing,i_ as normalizeSha256Fingerprint,I2 as normalizeDevDomain,Q2 as loadSSLConfig,N2 as listCertSha256HashesByCommonName,_B as isValidRootCA,$_ as isValidId,NB as isSingleProxyOptions,YB as isSingleProxyConfig,$2 as isRootCaTrustedForSsl,G2 as isRootCaFingerprintInKeychains,x as isPortInUse,R2 as isPidAlive,KB as isMultiProxyOptions,BB as isMultiProxyConfig,U2 as isDnsServerRunning,a2 as isDaemonRunning,V2 as isCertTrusted,N_ as httpsConfig,m as getSudoPassword,J2 as getSharedDaemonCertPaths,X2 as getRootCAPaths,Y_ as getRegistryDir,DB as getPrimaryDomain,K2 as getMacosTrustKeychains,B2 as getMacosLoginKeychainPath,v2 as getDaemonRpxDir,b2 as getDaemonPidPath,K_ as generateCertificate,M2 as gcStaleEntries,Z2 as forceTrustCertificate,U_ as findAvailablePort,eD as extractHostname,sD as execSudoSync,X_ as ensureDaemonRunning,H2 as devDomainsFromHosts,k_ as deriveIdFromTarget,i2 as defaultDaemonSpawnCommand,B_ as defaultConfig,rD as default,K as debugLog,M_ as createProxyFetchHandler,y2 as contentLooksLikeRpxResolver,B_ as config,H as colors,A2 as clearSslConfigCache,z_ as cleanupCertificates,d as cleanup,h as checkHosts,c as checkExistingCertificates,r_ as certIncludesSanHostnames,L as addHosts,d2 as acquireDaemonLock,D2 as RPX_ROOT_CA_COMMON_NAME,C2 as RPX_RESOLVER_MARKER,_2 as MACOS_SYSTEM_KEYCHAIN,e_ as MACOS_CA_TRUST_FLAGS,F2 as LEGACY_TLD_RESOLVER_LABELS,b as DefaultPortManager,T2 as DNS_STATE_VERSION,k2 as DNS_PORT};
8
+ `,V=J_.join(O_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await O.promises.writeFile(V,G,"utf8"),await s(`cat "${V}" | tee "${k}" > /dev/null`),K("hosts","Hosts removed successfully",_)}catch(X){K("hosts","Could not clean up hosts file automatically",_)}finally{try{await O.promises.unlink(V)}catch(X){K("hosts",`Failed to remove temporary file: ${X}`,_)}}}catch(B){K("hosts",`Failed to clean up hosts file: ${B.message}`,_)}}async function c(D,_){K("hosts",`Checking hosts: ${D}`,_);let B;try{B=await O.promises.readFile(k,"utf-8")}catch(N){K("hosts",`Error reading hosts file: ${N}`,_);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 K("hosts",`Cannot read hosts file, assuming entries don't exist: ${Y}`,_),D.map(()=>!1)}}return D.map((N)=>{let Y=`127.0.0.1 ${N}`,R=`::1 ${N}`;return B.includes(Y)||B.includes(R)})}import*as o from"node:net";function U(D,_,B){return K("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((N)=>{let Y=o.createServer(),R=setTimeout(()=>{K("port",`Checking port ${D} timed out, assuming it's in use`,B),Y.close(),N(!0)},3000);Y.once("error",(G)=>{if(clearTimeout(R),G.code==="EADDRINUSE")K("port",`Port ${D} is in use`,B),N(!0);else K("port",`Error checking port ${D}: ${G.message}`,B),N(!0)}),Y.once("listening",()=>{clearTimeout(R),K("port",`Port ${D} is available`,B),Y.close(),N(!1)});try{Y.listen(D,_)}catch(G){clearTimeout(R),K("port",`Exception checking port ${D}: ${G}`,B),N(!0)}})}async function L_(D,_,B,N=50){K("port",`Finding available port starting from ${D} (max attempts: ${N})`,B);let Y=D,R=0;while(R<N){if(R++,!await U(Y,_,B))return K("port",`Found available port: ${Y} after ${R} attempts`,B),Y;K("port",`Port ${Y} is in use, trying ${Y+1} (attempt ${R}/${N})`,B),Y++}throw Error(`Unable to find available port after ${N} attempts starting from ${D}`)}function P_(D,_,B=5000,N){return K("port",`Testing connection to ${_}:${D}`,N),new Promise((Y)=>{let R=o.connect({host:_,port:D,timeout:B});R.once("connect",()=>{K("port",`Successfully connected to ${_}:${D}`,N),R.end(),Y(!0)}),R.once("timeout",()=>{K("port",`Connection to ${_}:${D} timed out`,N),R.destroy(),Y(!1)}),R.once("error",(G)=>{K("port",`Failed to connect to ${_}:${D}: ${G.message}`,N),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 K("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){K("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)){K("start",`Process ${D} is already running`,B);return}let[N,...Y]=_.command.split(" "),R=_.cwd||P.cwd();K("start",`Starting process ${D}:`,B),K("start",` Command: ${N} ${Y.join(" ")}`,B),K("start",` Working directory: ${R}`,B),K("start",` Environment variables: ${f(_.env)}`,B);let G=d_(N,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((V,X)=>{if(G.on("error",(Q)=>{if(!this.isShuttingDown)K("start",`Process ${D} failed to start: ${Q}`,B),this.processes.delete(D),X(Q),P.emit("SIGINT")}),G.on("exit",(Q)=>{if(!this.isShuttingDown&&Q!==null&&Q!==0)K("start",`Process ${D} exited with code ${Q}`,B),this.processes.delete(D),X(Error(`Process ${D} exited with code ${Q}`)),P.emit("SIGINT")}),B)G.stdout?.on("data",(Q)=>{K("process",`[${D}] ${Q.toString().trim()}`,!0)}),G.stderr?.on("data",(Q)=>{K("process",`[${D}] ERR: ${Q.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&G.killed)this.processes.delete(D),X(Error(`Process ${D} was killed during startup`));else K("start",`Process ${D} started successfully`,B),V()},1000)})}async stopProcess(D,_){let B=this.processes.get(D);if(!B?.process){K("start",`No process found for ${D}`,_);return}return K("start",`Stopping process ${D}`,_),new Promise((N)=>{if(!B.process){N();return}B.process.once("exit",()=>{this.processes.delete(D),K("start",`Process ${D} stopped`,_),N()});try{B.process.kill("SIGTERM"),setTimeout(()=>{if(B.process){K("start",`Force killing process ${D}`,_);try{B.process.kill("SIGKILL")}catch(Y){}}},3000)}catch(Y){K("start",`Error stopping process ${D}: ${Y}`,_),this.processes.delete(D),N()}})}async stopAll(D){if(this.isShuttingDown){K("start","Already shutting down, skipping duplicate stopAll call",D);return}this.isShuttingDown=!0,K("start","Stopping all processes",D);let _=Array.from(this.processes.keys()).map((B)=>this.stopProcess(B,D).catch((N)=>{W.error(`Failed to stop process ${B}:`,N)}));await Promise.allSettled(_),this.processes.clear(),this.isShuttingDown=!1}isRunning(D){let _=this.processes.get(D);return!!_?.process&&!_.process.killed}}var k2=new e;var D_=new e,g_="0.12.0",i_=new l("0.0.0.0"),a=new Set,Z_=!1,__=null,V_=null;async function p(D){if(Z_)return K("cleanup","Cleanup already in progress, skipping",D?.verbose),V_||Promise.resolve();Z_=!0,K("cleanup","Starting cleanup process",D?.verbose),V_=new Promise((_)=>{__=_});try{await D_.stopAll(D?.verbose),W.info("Shutting down proxy servers...");let _=[],B=Array.from(a).map((N)=>new Promise((Y)=>{N.close(()=>{K("cleanup","Server closed successfully",D?.verbose),Y()})}));if(_.push(...B),D?.hosts&&D.domains?.length){K("cleanup","Cleaning up hosts file entries",D?.verbose),K("cleanup",`Original domains for cleanup: ${JSON.stringify(D.domains)}`,D?.verbose);let N=D.domains.filter((Y)=>{if(Y==="test.local")return!0;return Y!=="localhost"&&!Y.startsWith("localhost.")&&Y!=="127.0.0.1"});if(K("cleanup",`Filtered domains for cleanup: ${JSON.stringify(N)}`,D?.verbose),N.length>0)W.info("Cleaning up hosts file entries..."),_.push(Q_(N,D?.verbose).then(()=>{K("cleanup",`Removed hosts entries for ${N.join(", ")}`,D?.verbose)}).catch((Y)=>{K("cleanup",`Failed to remove hosts entries: ${Y}`,D?.verbose),W.warn(`Failed to clean up hosts file entries for ${N.join(", ")}:`,Y)}))}if(D?.certs&&D.domains?.length){K("cleanup","Cleaning up SSL certificates",D?.verbose),W.info("Cleaning up SSL certificates...");let N=D.domains.map(async(Y)=>{try{await z_(Y,D?.verbose),K("cleanup",`Removed certificates for ${Y}`,D?.verbose)}catch(R){K("cleanup",`Failed to remove certificates for ${Y}: ${R}`,D?.verbose),W.warn(`Failed to clean up certificates for ${Y}:`,R)}});_.push(...N)}await Promise.allSettled(_),K("cleanup","All cleanup tasks completed successfully",D?.verbose),W.success("All cleanup tasks completed successfully")}catch(_){K("cleanup",`Error during cleanup: ${_}`,D?.verbose),W.error("Error during cleanup:",_)}finally{if(__)__();__=null,Z_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(M.env.NODE_ENV!=="test"&&M.env.BUN_ENV!=="test"&&!_)M.exit(0)}return V_}var A_=!1;function I_(D){if(A_){K("signal",`Received second ${D} signal, forcing exit`,!0),M.exit(1);return}A_=!0,K("signal",`Received ${D} signal, initiating cleanup`,!0),p().catch((_)=>{K("signal",`Cleanup failed after ${D}: ${_}`,!0),M.exit(1)}).finally(()=>{A_=!1})}M.once("SIGINT",()=>I_("SIGINT"));M.once("SIGTERM",()=>I_("SIGTERM"));M.on("uncaughtException",(D)=>{K("process",`Uncaught exception: ${D}`,!0),W.error("Uncaught exception:",D),I_("uncaughtException")});async function d(D,_,B,N=5){K("connection",`Testing connection to ${D}:${_} (retries left: ${N})`,B);let Y=15000,R=Date.now();if(M.env.RPX_BYPASS_CONNECTION_TEST==="true"){K("connection",`Bypassing connection test for ${D}:${_} due to RPX_BYPASS_CONNECTION_TEST flag`,B);return}let G=()=>new Promise((V,X)=>{let Q=u_.connect({host:D,port:_,timeout:3000});Q.once("connect",()=>{K("connection",`Successfully connected to ${D}:${_}`,B),Q.end(),V()}),Q.once("timeout",()=>{K("connection",`Connection to ${D}:${_} timed out`,B),Q.destroy(),X(Error("Connection timed out"))}),Q.once("error",(E)=>{K("connection",`Failed to connect to ${D}:${_}: ${E}`,B),Q.destroy(),X(E)})});try{await G()}catch(V){if(Date.now()-R>Y){K("connection",`Connection test timed out after ${Y}ms, but continuing anyway`,B),W.warn(`Connection test to ${D}:${_} timed out, but RPX will try to proceed anyway.`);return}if(V.code==="ECONNREFUSED"&&N>0)return K("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${N} retries left)`,B),await new Promise((Q)=>setTimeout(Q,2000)),d(D,_,B,N-1);if(N>0)try{K("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((Q,E)=>{let j=L.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(S)=>{K("connection",`Received HTTP response with status: ${S.statusCode}`,B),Q()});j.on("error",(S)=>E(S)),j.on("timeout",()=>{j.destroy(),E(Error("HTTP request timed out"))}),j.end()}),K("connection",`HTTP request to ${D}:${_} succeeded`,B);return}catch(Q){return K("connection",`HTTP request to ${D}:${_} failed: ${Q}`,B),K("connection",`Retrying socket connection in 2 seconds... (${N} retries left)`,B),await new Promise((E)=>setTimeout(E,2000)),d(D,_,B,N-1)}let X=`Failed to connect to ${D}:${_} after ${5-N} attempts: ${V.message}`;K("connection",`${X}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,B),W.warn(X),W.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){K("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"),N=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")){K("hosts",`Checking if hosts file entry exists for: ${B.hostname}`,D?.verbose);try{if(!(await c(Y,D.verbose))[0]){W.info(`Adding ${B.hostname} to hosts file...`),W.info("This may require sudo/administrator privileges");try{await h(Y,D.verbose)}catch(V){if(W.error("Failed to add hosts entry:",V.message),W.warn("You can manually add this entry to your hosts file:"),W.warn(`127.0.0.1 ${B.hostname}`),W.warn(`::1 ${B.hostname}`),M.platform==="win32")W.warn("On Windows:"),W.warn("1. Run notepad as administrator"),W.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else W.warn("On Unix systems:"),W.warn("sudo nano /etc/hosts")}}else K("hosts",`Host entry already exists for ${B.hostname}`,D.verbose)}catch(G){W.error("Failed to check hosts file:",G.message)}}try{await d(_.hostname,N,D.verbose)}catch(G){K("server",`Connection test failed: ${G}`,D.verbose),W.error(G.message),W.warn("Continuing with proxy setup despite connection test failure..."),W.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=N_({...D,to:B.hostname});if(R=await u({...D,to:B.hostname,https:D.https}),!R){if(K("ssl",`Generating new certificates for ${B.hostname}`,D.verbose),await K_({...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 K("server",`SSL setup failed: ${G}`,D.verbose),G}K("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:N,sourceUrl:{hostname:_.hostname,host:_.host},ssl:R})}async function n_(D,_,B,N,Y,R,G,V,X,Q,E){K("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${Q}`,X);function j(Z){let J={};for(let[A,z]of Object.entries(Z))if(!A.startsWith(":"))J[A]=z;return J}let S=(Z,J)=>{K("request",`Incoming request: ${Z.method} ${Z.url}`,X);let A=Z.url||"/",z=Z.method||"GET";if(Z instanceof c_.Http2ServerRequest){let F=Z.headers;z=F[":method"]||z,A=F[":path"]||A}if(Q){if(!A.match(/\.[a-z0-9]+$/i))if(A.endsWith("/"))A=`${A}index.html`;else A=`${A}.html`}let H=j(Z.headers);if(E)H.host=`${R.hostname}:${B}`,K("request",`Changed origin: setting host header to ${H.host}`,X);let T={hostname:R.hostname,port:B,path:A,method:z,headers:H};K("request",`Proxy request options: ${f(T)}`,X);let I=L.request(T,(F)=>{if(K("response",`Proxy response received with status ${F.statusCode}`,X),Q&&F.statusCode===404){let y=[];if(A.endsWith(".html"))y.push(A.slice(0,-5));else if(!A.match(/\.[a-z0-9]+$/i))y.push(`${A}.html`);if(!A.endsWith("/"))y.push(`${A}/index.html`);if(y.length>0){K("cleanUrls",`Trying alternative paths: ${y.join(", ")}`,X);let m=(g)=>{if(g.length===0){J.writeHead(F.statusCode||404,F.headers),F.pipe(J);return}let C_=g[0],v_={...T,path:C_},q_=L.request(v_,(i)=>{if(i.statusCode===200)K("cleanUrls",`Found matching path: ${C_}`,X),J.writeHead(i.statusCode,i.headers),i.pipe(J);else m(g.slice(1))});q_.on("error",()=>m(g.slice(1))),q_.end()};m(y);return}}let q={...F.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};J.writeHead(F.statusCode||500,q),F.pipe(J)});I.on("error",(F)=>{K("request",`Proxy request failed: ${F}`,X),W.error("Proxy request failed:",F),J.writeHead(502),J.end(`Proxy Error: ${F.message}`)}),Z.pipe(I)};if(K("server",`Creating server with SSL config: ${!!G}`,X),G)return new Promise((Z,J)=>{try{let A=Bun.serve({port:N,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);K("request",`Bun.serve received: ${z.method} ${H.pathname}`,X);let T=`http://${R.host}`,I=new URL(H.pathname+H.search,T);try{let F=new Headers(z.headers);if(F.set("host",R.host),E)F.set("origin",T);F.set("x-forwarded-for","127.0.0.1"),F.set("x-forwarded-proto","https"),F.set("x-forwarded-host",_);let q=await fetch(I.toString(),{method:z.method,headers:F,body:z.body,redirect:"manual"}),y=new Headers(q.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(q.body,{status:q.status,statusText:q.statusText,headers:y})}catch(F){return K("request",`Proxy error: ${F}`,X),new Response(`Proxy Error: ${F}`,{status:502})}},error(z){return K("server",`Bun.serve error: ${z}`,X),new Response(`Server Error: ${z.message}`,{status:500})}});a.add(A),h_({from:D,to:_,vitePluginUsage:V,listenPort:N,ssl:!0,cleanUrls:Q,verbose:X}),Z()}catch(A){J(A)}});let w=L.createServer(S);function $(Z){return a.add(Z),new Promise((J,A)=>{Z.listen(N,Y,()=>{K("server",`Server listening on port ${N}`,X),h_({from:D,to:_,vitePluginUsage:V,listenPort:N,ssl:!!G,cleanUrls:Q,verbose:X}),J()}),Z.on("error",(z)=>{K("server",`Server error: ${z}`,X),A(z)})})}return $(w)}async function t_(D){K("setup",`Setting up reverse proxy: ${f(D)}`,D.verbose);let{from:_,to:B,fromPort:N,sourceUrl:Y,ssl:R,verbose:G,cleanup:V,vitePluginUsage:X,changeOrigin:Q,cleanUrls:E}=D,j=80,S=443,w="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]){W.warn(`The hostname ${B} isn't in your hosts file. Adding it now...`);try{await h([B],G),W.success(`Added ${B} to your hosts file.`)}catch(T){W.error(`Failed to add ${B} to your hosts file: ${T}`),W.info(`You may need to manually add '127.0.0.1 ${B}' to your /etc/hosts file.`)}}}else if(Z&&M.platform!=="darwin"&&B&&B.includes("localhost")&&!B.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await c([B],G))[0]){K("hosts",`${B} not found in hosts file, adding...`,G);try{await h([B],G)}catch(T){K("hosts",`Failed to add ${B} to hosts file: ${T}`,G)}}}if(R&&!$.usedPorts.has(j)){if(!await U(j,w,G))K("setup","Starting HTTP redirect server",G),m_(G),$.usedPorts.add(j);else if(K("setup","Port 80 is in use, skipping HTTP redirect",G),G)W.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let J=R?S:j,A=await U(J,w,G),z;if(A){if(K("setup",`Port ${J} is already in use`,G),G)W.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),K("setup",`Using port ${z} instead of ${J}`,G),G)W.info(`Using port ${z} instead. Access your site at https://${B}:${z}`)}else if(z=await $.getNextAvailablePort(J+1000,!0),K("setup",`Using port ${z} instead of ${J}`,G),G)W.info(`Using port ${z} instead. Access your site at http://${B}:${z}`)}else z=J,$.usedPorts.add(z),K("setup",`Using standard ${J===443?"HTTPS":"HTTP"} port ${J} for ${B}`,G);await n_(_,B,N,z,w,Y,R,X,G,E,Q)}catch(J){K("setup",`Setup failed: ${J}`,G),W.error(`Failed to setup reverse proxy: ${J.message}`),p({domains:[B],hosts:typeof V==="boolean"?V:V?.hosts,certs:typeof V==="boolean"?V:V?.certs,verbose:G,vitePluginUsage:X})}}function m_(D){K("redirect","Starting HTTP redirect server",D);let _=L.createServer((B,N)=>{let Y=B.headers.host||"";K("redirect",`Redirecting request from ${Y}${B.url} to HTTPS`,D),N.writeHead(301,{Location:`https://${Y}${B.url}`}),N.end()}).listen(80);a.add(_),K("redirect","HTTP redirect server started",D)}function r_(D){let _={...B_,...D};if(K("proxy",`Starting proxy with options: ${f(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){W.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((X)=>{W.error(`Failed to register with rpx daemon: ${X.message}`),M.exit(1)});return}let B=_.to||"",N=B.split(".").pop()?.toLowerCase()||"",Y=M.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(N)&&_?.verbose)W.warn(`The .${N} TLD may not work reliably for local development`),W.info(` Google owns .${N} with HSTS preloading, which can bypass local DNS`),W.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(Y)import("./chunk-5ygwd93k.js").then(({setupDevelopmentDns:X})=>{X({domains:[B],verbose:_.verbose}).then((Q)=>{if(Q)Promise.resolve().then(()=>{if(_.verbose)if(G.includes(N))W.success(`DNS server started for .${N} domains`);else W.success(`DNS server started for .${N} domains (hosts file entry also added)`)});else K("dns",`Could not start DNS server - ${B} may not resolve in browser`,_.verbose)})}).catch((X)=>{K("dns",`Failed to start DNS server: ${X}`,_.verbose)});let V={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:N_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};K("proxy",`Server options: ${f(V)}`,_.verbose),j_(V).catch((X)=>{K("proxy",`Failed to start proxy: ${X}`,_.verbose),W.error(`Failed to start proxy: ${X.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_(_),N=H_(_);if(K("config",`Starting with config: ${f(_,2)}`,B),K("config",`Is multi-proxy? ${"proxies"in _}`,B),K("config",`Hosts management enabled? ${N}`,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)){K("servers",`Found ${_.proxies.length} proxies in config`,B);for(let $ of _.proxies)if($.start){let Z=`${$.from}-${$.to}`;try{K("watch",`Starting command for ${Z} with command: ${$.start.command}`,B),W.info(`Starting command for ${Z}...`),await D_.startProcess(Z,$.start,B);let J=new URL($.from.startsWith("http")?$.from:`http://${$.from}`),A=J.hostname||"localhost",z=Number(J.port)||80;try{await d(A,z,B),K("watch",`Dev server is ready at ${A}:${z}`,B)}catch(H){K("watch",`Connection check failed, but continuing with proxy setup: ${H}`,B),W.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(J){throw K("watch",`Failed to start command for ${Z}: ${J}`,B),Error(`Failed to start command for ${Z}: ${J}`)}}else K("watch",`No start command for proxy ${$.from} -> ${$.to}`,B)}else if("start"in _&&_.start){K("watch","Found start command in single proxy config",B);let $=`${_.from}-${_.to}`;try{if(_.start)K("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",A=Number(Z.port)||80;try{await d(J,A,B),K("watch",`Dev server is ready at ${J}:${A}`,B)}catch(z){K("watch",`Connection check failed, but continuing with proxy setup: ${z}`,B),W.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(Z){throw K("watch",`Failed to run start command: ${Z}`,B),Error(`Failed to run start command: ${Z}`)}}else K("watch","No start command found in config",B);let Y="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(M.platform!=="win32"&&(_.https||N)){if(!v())try{K("sudo","Pre-acquiring sudo credentials for privileged operations",B),p_("sudo -v",{stdio:"inherit"})}catch{K("sudo","Could not pre-acquire sudo credentials",B)}}if(_.https){let $=await u(_);if(!$){if(K("ssl",`No valid or trusted certificates found for ${Y}, generating new ones`,_.verbose),await K_(_),$=await u(_),!$)throw Error(`Failed to load SSL certificates after generation for ${Y}`)}else K("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"),V=_._cachedSSLConfig,X=G.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],E=["test","localhost","local","example","invalid"],j=[...new Set(X.map(($)=>$.split(".").pop()?.toLowerCase()))],S=j.filter(($)=>!!$&&Q.includes($));if(S.length>0&&B)W.warn(`The following TLDs may not work reliably for local development: ${S.map(($)=>`.${$}`).join(", ")}`),W.info(" These TLDs have HSTS preloading which can bypass local DNS"),W.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(N&&M.platform==="darwin"&&X.length>0){let{setupDevelopmentDns:$}=await import("./chunk-5ygwd93k.js");if(await $({domains:X,verbose:B})){if(B)if(j.every((A)=>!!A&&E.includes(A)))W.success(`DNS server started for ${j.map((A)=>`.${A}`).join(", ")} domains`);else W.success(`DNS server started for ${j.map((A)=>`.${A}`).join(", ")} domains (hosts file entries also added)`)}else K("dns","Could not start DNS server - custom domains may not resolve",B)}let w=async()=>{K("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-5ygwd93k.js");await $({verbose:_.verbose})}catch($){K("cleanup",`Error stopping DNS server: ${$}`,_.verbose)}try{await D_.stopAll(_.verbose)}catch($){K("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(M.on("SIGINT",w),M.on("SIGTERM",w),M.on("uncaughtException",($)=>{K("process",`Uncaught exception: ${$}`,!0),console.error("Uncaught exception:",$),w()}),V&&R.length>1){K("proxies",`Creating shared HTTPS server for ${R.length} domains`,B);let $=new Map;for(let T of R){let I=T.to||"rpx.localhost",F=T.cleanUrls||!1;if(T.static)$.set(I,{static:F_(T.static,F),cleanUrls:F}),K("proxies",`Route: ${I} → static ${typeof T.static==="string"?T.static:T.static.dir}`,B);else{let q=new URL(T.from?.startsWith("http")?T.from:`http://${T.from}`);$.set(I,{sourceHost:q.host,cleanUrls:F,changeOrigin:T.changeOrigin||!1,pathRewrites:T.pathRewrites}),K("proxies",`Route: ${I} → ${q.host}`,B)}if(N&&!w_(I)&&!I.includes("localhost")&&!I.includes("127.0.0.1"))try{if(!(await c([I],B))[0])await h([I],B)}catch{K("hosts",`Could not add hosts entry for ${I}`,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(K("proxies",`Port ${J} is already in use, cannot start shared proxy`,B),B)W.warn(`Port ${J} is in use. Shared HTTPS proxy cannot start.`);return}let z=T_((T)=>E_($,T),B),H=M_(B);try{let T=Bun.serve({port:J,hostname:"0.0.0.0",tls:{key:V.key,cert:V.cert,ca:V.ca,requestCert:!1,rejectUnauthorized:!1},fetch(I,F){return z(I,F)},websocket:H,error(I){return K("server",`Shared proxy server error: ${I}`,B),new Response(`Server Error: ${I.message}`,{status:500})}});a.add(T),K("proxies",`Shared HTTPS proxy listening on port ${J} for ${$.size} domains`,B)}catch(T){K("proxies",`Failed to start shared proxy: ${T}`,B),console.error("Failed to start shared HTTPS proxy:",T),w()}}else for(let $ of R)try{let Z=$.to||"rpx.localhost";K("proxy",`Starting proxy for ${Z} with SSL config: ${!!V}`,$.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:V,changeOrigin:$.changeOrigin||!1})}catch(Z){K("proxies",`Failed to start proxy for ${$.to}: ${Z}`,$.verbose),console.error(`Failed to start proxy for ${$.to}:`,Z),w()}}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,yD as watchRegistry,BD as verifyHttpsChain,ZD as trustRootCaForBrowsers,tD as tearDownDevelopmentDns,nD as syncDevelopmentDnsFromRegistry,bD as stopDnsServer,$2 as stopDaemon,j_ as startServer,r_ as startProxy,k_ as startProxies,vD as startDnsServer,pD as setupResolver,iD as setupDevelopmentDns,HD as serverNameFromCertFilename,ID as serveStaticFile,f as safeStringify,wD as safeRelativePath,jB as safeDeleteFile,t as runViaDaemon,N2 as runDaemon,aD as resolverFilePath,hD as resolverBasenamesForDomains,LD as resolverBasenameForDomain,F_ as resolveStaticRoute,ED as resolveStaticFile,IB as resolvePathRewrite,rD as removeResolver,gD as removeLegacyTldResolvers,Q_ as removeHosts,R_ as removeEntry,K2 as releaseDaemonLock,AB as redactSensitive,sD as reconcileStaleDevelopmentDns,G2 as reconcileDevelopmentDnsOnIdle,qD as readEntry,_2 as readDaemonPid,e_ as readCertSha256Fingerprint,_D as readCertCommonName,xD as readAll,WD as pruneStaleRootCas,a_ as portManager,KD as parseSha256HashesFromSecurityListing,o_ as normalizeSha256Fingerprint,PD as normalizeDevDomain,jD as matchesWildcard,E_ as matchHost,SD as loadSSLConfig,XD as listCertSha256HashesByCommonName,w_ as isWildcardPattern,zB as isValidRootCA,$_ as isValidId,wB as isSingleProxyOptions,EB as isSingleProxyConfig,JD as isRootCaTrustedForSsl,QD as isRootCaFingerprintInKeychains,U as isPortInUse,CD as isPidAlive,MB as isMultiProxyOptions,TB as isMultiProxyConfig,lD as isDnsServerRunning,D2 as isDaemonRunning,TD as isCertTrusted,N_ as httpsConfig,v as getSudoPassword,AD as getSharedDaemonCertPaths,VD as getRootCAPaths,Y_ as getRegistryDir,FB as getPrimaryDomain,RD as getMacosTrustKeychains,GD as getMacosLoginKeychainPath,oD as getDaemonRpxDir,eD as getDaemonPidPath,K_ as generateCertificate,fD as gcStaleEntries,zD as forceTrustCertificate,L_ as findAvailablePort,SB as extractHostname,VB as execSudoSync,X_ as ensureDaemonRunning,cD as devDomainsFromHosts,y_ as deriveIdFromTarget,Y2 as defaultDaemonSpawnCommand,B_ as defaultConfig,ZB as default,K as debugLog,M_ as createProxyWebSocketHandler,T_ as createProxyFetchHandler,MD as contentTypeFor,dD as contentLooksLikeRpxResolver,B_ as config,C as colors,FD as clearSslConfigCache,z_ as cleanupCertificates,p as cleanup,c as checkHosts,u as checkExistingCertificates,DD as certIncludesSanHostnames,kD as buildSniTlsConfig,h as addHosts,B2 as acquireDaemonLock,$D as RPX_ROOT_CA_COMMON_NAME,mD as RPX_RESOLVER_MARKER,YD as MACOS_SYSTEM_KEYCHAIN,ND as MACOS_CA_TRUST_FLAGS,OD as LEGACY_TLD_RESOLVER_LABELS,l as DefaultPortManager,UD as DNS_STATE_VERSION,uD as DNS_PORT};
@@ -1,15 +1,30 @@
1
1
  import type { PathRewrite } from './types';
2
+ import type { ResolvedStaticRoute } from './static-files';
2
3
  /**
3
4
  * Build a Bun.serve-compatible `fetch` handler that routes requests based on
4
5
  * the `Host` header. Returns 404 when no route matches and 502 on upstream
5
- * failures.
6
+ * failures. When a request is a WebSocket upgrade and `server` is supplied, it
7
+ * is upgraded (returns `undefined` so Bun completes the handshake) and the
8
+ * traffic is handled by the `websocket` handler from {@link createProxyWebSocketHandler}.
6
9
  */
7
10
  export declare function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean): ProxyFetchHandler;
11
+ /**
12
+ * Build the `websocket` handler block for Bun.serve. It opens an upstream
13
+ * `WebSocket` per client socket, buffers client→upstream frames until the
14
+ * upstream connection is open, and pipes messages, closes and errors in both
15
+ * directions with a clean teardown.
16
+ */
17
+ export declare function createProxyWebSocketHandler(verbose?: boolean): void;
8
18
  export declare interface ProxyRoute {
9
- sourceHost: string
19
+ sourceHost?: string
10
20
  cleanUrls?: boolean
11
21
  changeOrigin?: boolean
12
22
  pathRewrites?: PathRewrite[]
23
+ static?: ResolvedStaticRoute
24
+ }
25
+ /** Minimal shape of the Bun server needed for WebSocket upgrades. */
26
+ export declare interface ProxyServer {
27
+ upgrade: (req: Request, options?: { data?: any, headers?: any }) => boolean
13
28
  }
14
29
  export type GetRoute = (hostname: string) => ProxyRoute | undefined;
15
- export type ProxyFetchHandler = (req: Request) => Promise<Response>;
30
+ export type ProxyFetchHandler = (req: Request, server?: ProxyServer) => Promise<Response | undefined>;
@@ -1,4 +1,4 @@
1
- import type { PathRewrite } from './types';
1
+ import type { PathRewrite, StaticRouteConfig } from './types';
2
2
  /**
3
3
  * Default location for the registry directory. The daemon's PID file and log
4
4
  * sit alongside it under `~/.stacks/rpx/`.
@@ -55,7 +55,7 @@ export declare function gcStaleEntries(dir?: string, verbose?: boolean): Promise
55
55
  export declare function watchRegistry(onChange: (entries: RegistryEntry[]) => void | Promise<void>, opts?: WatchOptions & { dir?: string }): WatchHandle;
56
56
  export declare interface RegistryEntry {
57
57
  id: string
58
- from: string
58
+ from?: string
59
59
  to: string
60
60
  pid?: number
61
61
  cwd?: string
@@ -63,6 +63,7 @@ export declare interface RegistryEntry {
63
63
  pathRewrites?: PathRewrite[]
64
64
  cleanUrls?: boolean
65
65
  changeOrigin?: boolean
66
+ static?: string | StaticRouteConfig
66
67
  }
67
68
  export declare interface WatchHandle {
68
69
  close: () => void
package/dist/sni.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ProductionTlsConfig } from './types';
2
+ /**
3
+ * Map a PEM filename under a `certsDir` to its SNI server name. Returns `null`
4
+ * for files that aren't `<name>.crt`. The wildcard convention
5
+ * `_wildcard.<apex>.crt` maps to server name `*.<apex>`.
6
+ */
7
+ export declare function serverNameFromCertFilename(filename: string): string | null;
8
+ /**
9
+ * Build the SNI TLS array from a {@link ProductionTlsConfig}. Reads PEM files
10
+ * from an explicit `domains` map and/or a `certsDir` convention. Files that
11
+ * can't be read are skipped (logged in verbose mode). Returns `[]` when nothing
12
+ * usable is found so the caller can fall back to the dev cert flow.
13
+ */
14
+ export declare function buildSniTlsConfig(cfg: ProductionTlsConfig, verbose?: boolean): Promise<SniTlsEntry[]>;
15
+ /** One entry of the Bun.serve `tls` array. */
16
+ export declare interface SniTlsEntry {
17
+ serverName: string
18
+ cert: string
19
+ key: string
20
+ }
@@ -0,0 +1,46 @@
1
+ import type { PathRewriteStyle, StaticRouteConfig } from './types';
2
+ export declare function resolveStaticRoute(cfg: string | StaticRouteConfig, cleanUrls: boolean): ResolvedStaticRoute;
3
+ export declare function contentTypeFor(filePath: string): string;
4
+ /**
5
+ * Decode + normalize a URL pathname into a safe relative path.
6
+ *
7
+ * Traversal safety: normalizing against a leading `/` collapses every `..`
8
+ * segment and clamps at the root, so the returned relative path never contains
9
+ * `..` and `path.join(root, rel)` can't escape `root`. Backslash, NUL and
10
+ * malformed percent-encoding are rejected outright (return `null`); the
11
+ * residual `..` guard is belt-and-suspenders.
12
+ */
13
+ export declare function safeRelativePath(pathname: string): string | null;
14
+ /**
15
+ * Pure resolution of an incoming request pathname to a candidate file path on
16
+ * disk. Does no I/O; the caller checks existence and may fall back (SPA).
17
+ *
18
+ * Rules:
19
+ * - A trailing `/` (or root) resolves to `index.html` in that directory.
20
+ * - `cleanUrls` + a `.html` request → 301 to the extensionless URL.
21
+ * - Extensionless paths resolve per `pathRewriteStyle`:
22
+ * - `directory`: `/about` → `about/index.html`
23
+ * - `flat`: `/about` → `about.html`
24
+ * - Paths with a real extension (`.css`, `.png`, …) map straight through.
25
+ *
26
+ * Returns `null` when the path is unsafe (traversal attempt).
27
+ */
28
+ export declare function resolveStaticFile(pathname: string, route: ResolvedStaticRoute): StaticResolution | null;
29
+ /**
30
+ * Serve a static file for the matched route. Returns a 301 for clean-URL
31
+ * redirects, the file with the right `Content-Type`/`Cache-Control` when it
32
+ * exists, the SPA `index.html` fallback when configured, or 404.
33
+ */
34
+ export declare function serveStaticFile(pathname: string, route: ResolvedStaticRoute): Promise<Response>;
35
+ /** Normalized static-route config (shorthand string already expanded). */
36
+ export declare interface ResolvedStaticRoute {
37
+ dir: string
38
+ spa: boolean
39
+ pathRewriteStyle: PathRewriteStyle
40
+ maxAge: number
41
+ cleanUrls: boolean
42
+ }
43
+ export declare interface StaticResolution {
44
+ filePath: string
45
+ redirectTo?: string
46
+ }
package/dist/types.d.ts CHANGED
@@ -10,11 +10,18 @@ export declare interface PathRewrite {
10
10
  to: string
11
11
  stripPrefix?: boolean
12
12
  }
13
+ export declare interface StaticRouteConfig {
14
+ dir: string
15
+ spa?: boolean
16
+ pathRewriteStyle?: PathRewriteStyle
17
+ maxAge?: number
18
+ }
13
19
  export declare interface BaseProxyConfig {
14
- from: string
20
+ from?: string
15
21
  to: string
16
22
  start?: StartOptions
17
23
  pathRewrites?: PathRewrite[]
24
+ static?: string | StaticRouteConfig
18
25
  id?: string
19
26
  }
20
27
  export declare interface CleanupConfig {
@@ -24,6 +31,22 @@ export declare interface CleanupConfig {
24
31
  verbose: boolean
25
32
  vitePluginUsage?: boolean
26
33
  }
34
+ /**
35
+ * A real PEM cert+key pair on disk for one SNI server name.
36
+ */
37
+ export declare interface DomainCert {
38
+ certPath: string
39
+ keyPath: string
40
+ }
41
+ /**
42
+ * Production TLS using real certs (e.g. Let's Encrypt) served per-domain via
43
+ * SNI on a single listener. Provide either an explicit `domains` map or a
44
+ * `certsDir` convention.
45
+ */
46
+ export declare interface ProductionTlsConfig {
47
+ domains?: Record<string, DomainCert>
48
+ certsDir?: string
49
+ }
27
50
  export declare interface SharedProxyConfig {
28
51
  https: boolean | TlsOption
29
52
  cleanup: boolean | CleanupOptions
@@ -35,6 +58,8 @@ export declare interface SharedProxyConfig {
35
58
  changeOrigin?: boolean
36
59
  regenerateUntrustedCerts?: boolean
37
60
  viaDaemon?: boolean
61
+ hostsManagement?: boolean
62
+ productionCerts?: ProductionTlsConfig
38
63
  }
39
64
  export declare interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
40
65
  export declare interface MultiProxyConfig extends SharedProxyConfig {
@@ -57,6 +82,13 @@ export declare interface PortManager {
57
82
  usedPorts: Set<number>
58
83
  getNextAvailablePort: (startPort: number) => Promise<number>
59
84
  }
85
+ /**
86
+ * How a static-file route maps request paths to files on disk.
87
+ *
88
+ * - `directory` (default): `/about` → `<root>/about/index.html` (SSG dir style).
89
+ * - `flat`: `/about` → `<root>/about.html` (flat-file style).
90
+ */
91
+ export type PathRewriteStyle = 'directory' | 'flat';
60
92
  export type BaseProxyOptions = Partial<BaseProxyConfig>;
61
93
  export type CleanupOptions = Partial<CleanupConfig>;
62
94
  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.13",
4
+ "version": "0.11.14",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The daemon's PID-GC reaps anything we miss if this process dies `kill -9`.
12
12
  */
13
- import type { PathRewrite } from './types'
13
+ import type { PathRewrite, StaticRouteConfig } from './types'
14
14
  import * as fs from 'node:fs'
15
15
  import * as path from 'node:path'
16
16
  import * as process from 'node:process'
@@ -21,11 +21,14 @@ import { debugLog } from './utils'
21
21
 
22
22
  export interface DaemonRunnerProxy {
23
23
  id?: string
24
- from: string
24
+ /** Upstream `host:port`. Optional when `static` is set. */
25
+ from?: string
25
26
  to: string
26
27
  cleanUrls?: boolean
27
28
  changeOrigin?: boolean
28
29
  pathRewrites?: PathRewrite[]
30
+ /** Serve a local directory for this route instead of proxying. */
31
+ static?: string | StaticRouteConfig
29
32
  }
30
33
 
31
34
  export interface DaemonRunnerOptions {
@@ -100,6 +103,7 @@ export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
100
103
  cleanUrls: p.cleanUrls,
101
104
  changeOrigin: p.changeOrigin,
102
105
  pathRewrites: p.pathRewrites,
106
+ static: p.static,
103
107
  }, registryDir, verbose)
104
108
  }
105
109
 
@@ -111,8 +115,12 @@ export async function runViaDaemon(opts: DaemonRunnerOptions): Promise<void> {
111
115
  spawnEnv: opts.spawnEnv,
112
116
  })
113
117
 
114
- for (const p of resolved)
115
- log.success(`https://${p.to} → ${p.from}`)
118
+ for (const p of resolved) {
119
+ const target = p.static
120
+ ? `static ${typeof p.static === 'string' ? p.static : p.static.dir}`
121
+ : p.from
122
+ log.success(`https://${p.to} → ${target}`)
123
+ }
116
124
  log.info(`(via rpx daemon pid=${result.pid}; \`rpx daemon:status\` to inspect)`)
117
125
 
118
126
  if (opts.detached)