@stacksjs/rpx 0.11.17 → 0.11.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.d.ts CHANGED
@@ -19,18 +19,17 @@ export declare function isDaemonRunning(rpxDir?: string): Promise<boolean>;
19
19
  */
20
20
  export declare function acquireDaemonLock(rpxDir?: string): Promise<string>;
21
21
  export declare function releaseDaemonLock(rpxDir?: string): Promise<void>;
22
- /**
23
- * Start the daemon. Returns a handle that resolves `done` once the daemon has
24
- * cleanly shut down (signal received and listeners closed).
25
- *
26
- * The promise itself resolves as soon as the daemon is *ready* — i.e. both
27
- * listeners are bound and the initial routing table is populated. Use
28
- * `handle.done` for the lifetime promise.
29
- */
30
22
  // `opts` IS used throughout; pickier's no-unused-vars mis-fires on this fn after
31
23
  // the on-demand serve refactor (its --fix would wrongly rename to `_opts`).
32
24
  // eslint-disable-next-line pickier/no-unused-vars
33
25
  export declare function runDaemon(opts?: DaemonOptions): Promise<DaemonHandle>;
26
+ /**
27
+ * A cluster worker: binds :443 with `reusePort`, serves the proxy handler, keeps
28
+ * its routing table in sync with the registry, and reloads its TLS certs from
29
+ * the coordinator-published file on `SIGHUP`. It owns none of the singletons
30
+ * (lock, DNS, hosts, :80, ACME issuance) — the coordinator does.
31
+ */
32
+ export declare function runDaemonWorker(ctx: WorkerCtx): Promise<DaemonHandle>;
34
33
  /**
35
34
  * Best-effort default for the spawn command used by lazy-spawn. Compiled
36
35
  * binaries (`bun build --compile`) self-invoke; source-mode executions invoke
@@ -73,6 +72,7 @@ export declare interface DaemonOptions {
73
72
  productionCerts?: ProductionTlsConfig
74
73
  onDemandTls?: OnDemandTlsConfig
75
74
  gcIntervalMs?: number
75
+ workers?: number
76
76
  }
77
77
  export declare interface DaemonHandle {
78
78
  stop: () => Promise<void>
@@ -82,6 +82,14 @@ export declare interface DaemonHandle {
82
82
  pidPath: string
83
83
  ensureCert: (host: string) => Promise<boolean>
84
84
  }
85
+ // ───────────────────────── cluster: coordinator + workers ─────────────────────
86
+ declare interface WorkerCtx {
87
+ rpxDir: string
88
+ registryDir: string
89
+ httpsPort: number
90
+ hostname: string
91
+ verbose: boolean
92
+ }
85
93
  export declare interface EnsureDaemonOptions {
86
94
  rpxDir?: string
87
95
  spawnCommand?: string[]
@@ -107,3 +115,4 @@ export declare interface StopDaemonResult {
107
115
  pid: number | null
108
116
  forced: boolean
109
117
  }
118
+ declare type SniEntry = { serverName: string, cert: string, key: string }
package/dist/dns.d.ts CHANGED
@@ -2,6 +2,8 @@ import type { RegistryEntry } from './registry';
2
2
  export declare function startDnsServer(domains: string[], verbose?: boolean): Promise<boolean>;
3
3
  export declare function stopDnsServer(verbose?: boolean): void;
4
4
  export declare function isDnsServerRunning(): boolean;
5
+ /** True when something is answering A queries for `host` on the rpx dev DNS port. */
6
+ export declare function isRpxDevelopmentDnsAnswering(host: string, timeoutMs?: number): Promise<boolean>;
5
7
  export declare function resolverFilePath(basename: string): string;
6
8
  /** True when a resolver file points at the rpx local DNS port. */
7
9
  export declare function contentLooksLikeRpxResolver(content: string): boolean;
package/dist/https.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { config } from './config';
2
2
  import { MACOS_CA_TRUST_FLAGS, MACOS_SYSTEM_KEYCHAIN, getMacosLoginKeychainPath, isRootCaFingerprintInKeychains, isRootCaTrustedForSsl, pruneStaleRootCas, trustRootCaForBrowsers } from './macos-trust';
3
3
  import type { ProxyConfigs, ProxyOption, ProxyOptions, SSLConfig, TlsConfig } from './types';
4
+ /**
5
+ * Bun needs one `tls[]` entry per SNI name even when a single PEM covers every SAN.
6
+ * Without this, :443 serves the default cert (wrong CN → ERR_CERT_COMMON_NAME_INVALID).
7
+ */
8
+ export declare function devSslToSniEntries(hosts: string[], ssl: SSLConfig): Array<{ serverName: string, cert: string, key: string }>;
9
+ /** ProxyOptions for the shared multi-app dev certificate (all registry hosts as SANs). */
10
+ export declare function buildRegistryTlsProxyOptions(registryHosts: string[], primary: string, verbose?: boolean): ProxyOptions;
4
11
  /**
5
12
  * Returns the canonical Root CA cert + key paths inside `basePath`.
6
13
  */
@@ -51,6 +58,8 @@ export declare function cleanupCertificates(domain: string, verbose?: boolean):
51
58
  * If options.regenerateUntrustedCerts is false, always returns true (skips trust check)
52
59
  */
53
60
  export declare function isCertTrusted(certPath: string, options?: { verbose?: boolean, regenerateUntrustedCerts?: boolean, serverName?: string }): Promise<boolean>;
61
+ /** Shared dev host cert path used by the rpx daemon and `./buddy dev`. */
62
+ export declare const SHARED_DEV_HOST_CERT_PATH: string;
54
63
  export declare interface RootCAPaths {
55
64
  caCertPath: string
56
65
  caKeyPath: string
package/dist/index.d.ts CHANGED
@@ -24,9 +24,12 @@ export {
24
24
  removeHosts,
25
25
  } from './hosts';
26
26
  export {
27
+ SHARED_DEV_HOST_CERT_PATH,
28
+ buildRegistryTlsProxyOptions,
27
29
  checkExistingCertificates,
28
30
  cleanupCertificates,
29
31
  clearSslConfigCache,
32
+ devSslToSniEntries,
30
33
  forceTrustCertificate,
31
34
  generateCertificate,
32
35
  getRootCAPaths,
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import{$ as X_,A as M_,Aa as WD,B as H2,Ba as ZD,C as F_,Ca as SD,D as k2,Da as VD,E as C2,Ea as AD,F as x2,Fa as zD,G as f2,Ga as J_,H as q2,Ha as MD,I as T_,Ia as FD,J as I_,K as j_,L as q_,M as y2,N as w_,O as U2,P as E_,Q as O2,R as H_,S as P2,T as L2,U as c2,V as h2,W as u2,X as R_,Y as Y_,Z as m2,_ as $_,a as J,aa as v2,b as N_,ba as b2,c as G2,ca as l2,d as K2,da as a2,e as R2,ea as d2,f as Y2,fa as g2,g as $2,ga as p2,h as X2,ha as i2,i as J2,ia as n2,j as Q2,ja as t2,k as W2,ka as r2,l as Z2,la as s2,m as S2,ma as o2,n as V2,na as e2,o as A2,oa as _D,p as z2,pa as DD,q as M2,qa as BD,r as F2,ra as ND,s as T2,sa as GD,t as I2,ta as KD,u as j2,ua as RD,v as w2,va as YD,w as G_,wa as $D,x as E2,xa as XD,y as b,ya as JD,z as K_,za as QD}from"./chunk-zx2ghrc1.js";import{Ja as z_,Ka as l,La as UB,Ma as N,Na as OB,Oa as y,Pa as PB,Qa as LB,Ra as cB,Sa as hB,Ta as uB,Ua as mB,Va as vB,Wa as bB,Xa as lB}from"./chunk-rbgb5fyg.js";import{execSync as n_}from"node:child_process";import*as c from"node:http";import*as v_ from"node:http2";import*as b_ from"node:net";import*as I from"node:process";var t=(D,_)=>(B)=>`\x1B[${D}m${B}\x1B[${_}m`,f={bold:t(1,22),dim:t(2,22),green:t(32,39),cyan:t(36,39)};import*as U_ from"node:fs";import*as O_ from"node:path";import*as q from"node:process";function P_(D,_){let G=(_&&_!=="/"?`${D}${_}`:D).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return G.length>0?G:"rpx"}async function r(D){if(D.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let _=D.verbose??!1,B=D.registryDir,G=new Set,K=D.proxies.map((z)=>{let j=z.id??P_(z.to,z.path);if(!Y_(j))throw Error(`invalid registry id "${j}" derived from to="${z.to}"`);if(G.has(j))throw Error(`duplicate registry id "${j}" — set an explicit \`id\` on one of the proxies`);return G.add(j),{...z,id:j}}),X=new Date().toISOString();for(let z of K)await $_({id:z.id,from:z.from,to:z.to,path:z.path,pid:D.persistent?void 0:q.pid,cwd:q.cwd(),createdAt:X,cleanUrls:z.cleanUrls,changeOrigin:z.changeOrigin,pathRewrites:z.pathRewrites,static:z.static},B,_);let $=await J_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let z of K){let j=z.static?`static ${typeof z.static==="string"?z.static:z.static.dir}`:z.from;J.success(`https://${z.to} → ${j}`)}if(J.info(`(via rpx daemon pid=${$.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let V=!1,R=B??R_(),Q=K.map((z)=>z.id),T=async()=>{if(V)return;V=!0;for(let z of Q)await X_(z,B,_).catch((j)=>{N("runner",`removeEntry(${z}) failed: ${j}`,_)})},w=(z)=>{N("runner",`received ${z}, unregistering ${Q.length} entries`,_),T().finally(()=>q.exit(0))};q.once("SIGINT",w),q.once("SIGTERM",w),q.once("exit",()=>{if(V)return;for(let z of Q)try{U_.unlinkSync(O_.join(R,`${z}.json`))}catch{}}),await new Promise(()=>{})}import{exec as d_}from"node:child_process";import P from"node:fs";import c_ from"node:os";import W_ from"node:path";import*as a from"node:process";import{promisify as g_}from"node:util";var s=g_(d_);function L_(D){let _=D.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var C=a.platform==="win32"?W_.join(a.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",Q_=!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:G}=await s(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return Q_=!0,G}if(Q_)try{let{stdout:G}=await s(`sudo -n sh -c '${B}'`);return G}catch(G){N("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:G}=await s(`sudo -n sh -c '${B}'`);return Q_=!0,G}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(G){throw Error(`Failed to execute sudo command: ${G.message}`)}}async function m(D,_){let B=D.filter((K)=>!L_(K)),G=D.filter((K)=>L_(K));if(G.length>0)N("hosts",`Skipping /etc/hosts for loopback dev names: ${G.join(", ")}`,_);if(B.length===0)return;N("hosts",`Adding hosts: ${B.join(", ")}`,_),N("hosts",`Using hosts file at: ${C}`,_);try{let K;try{K=await P.promises.readFile(C,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{K=await o(`cat "${C}"`)}catch(R){throw console.log(" Could not read hosts file — skipping hosts setup"),N("hosts",`sudo read also failed: ${R}`,_),Error(`Cannot read hosts file: ${R}`)}}let X=B.filter((R)=>{let Q=`127.0.0.1 ${R}`,T=`::1 ${R}`;return!K.includes(Q)&&!K.includes(T)});if(X.length===0){N("hosts","All hosts already exist in hosts file",_);return}let $=X.map((R)=>`
1
+ import{$ as X_,A as UD,Aa as A2,B as b,Ba as T2,C as Y_,Ca as M2,D as w_,Da as z2,E as OD,Ea as F2,F as H_,Fa as I2,G as PD,Ga as E2,H as LD,Ha as j2,I as cD,Ia as w2,J as hD,Ja as H2,K as uD,Ka as V_,L as k_,La as k2,M as C_,Ma as C2,N as x_,O as T_,P as mD,Q as f_,R as vD,S as q_,T as bD,U as y_,V as lD,W as aD,X as dD,Y as gD,Z as pD,_ as G_,a as X,aa as iD,b as R_,ba as J_,c as GD,ca as Q_,d as XD,da as nD,e as JD,ea as tD,f as QD,fa as rD,g as VD,ga as sD,h as WD,ha as oD,i as ZD,ia as eD,j as AD,ja as _2,k as TD,ka as D2,l as MD,la as B2,m as zD,ma as N2,n as FD,na as K2,o as ID,oa as R2,p as ED,pa as S2,q as jD,qa as Y2,r as wD,ra as $2,s as HD,t as kD,ta as G2,u as CD,ua as X2,v as xD,va as J2,w as fD,wa as Q2,x as qD,xa as V2,y as yD,ya as W2,z as S_,za as Z2}from"./chunk-rs8gqpax.js";import{$a as rB,Na as E_,Oa as a,Pa as v_,Qa as lB,Ra as N,Sa as j_,Ta as aB,Ua as y,Va as dB,Wa as gB,Xa as pB,Ya as iB,Za as nB,_a as tB,ab as sB,bb as oB}from"./chunk-w888yhnp.js";import{execSync as BD}from"node:child_process";import*as h from"node:http";import*as g_ from"node:http2";import*as p_ from"node:net";import*as j from"node:process";var s=(D,_)=>(B)=>`\x1B[${D}m${B}\x1B[${_}m`,x={bold:s(1,22),dim:s(2,22),green:s(32,39),cyan:s(36,39)};import*as c_ from"node:fs";import*as h_ from"node:path";import*as q from"node:process";function u_(D,_){let K=(_&&_!=="/"?`${D}${_}`:D).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return K.length>0?K:"rpx"}async function o(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((A)=>{let I=A.id??u_(A.to,A.path);if(!X_(I))throw Error(`invalid registry id "${I}" derived from to="${A.to}"`);if(K.has(I))throw Error(`duplicate registry id "${I}" — set an explicit \`id\` on one of the proxies`);return K.add(I),{...A,id:I}}),G=new Date().toISOString();for(let A of R)await J_({id:A.id,from:A.from,to:A.to,path:A.path,pid:D.persistent?void 0:q.pid,cwd:q.cwd(),createdAt:G,cleanUrls:A.cleanUrls,changeOrigin:A.changeOrigin,pathRewrites:A.pathRewrites,static:A.static},B,_);let $=await V_({rpxDir:D.rpxDir,verbose:_,spawnCommand:D.spawnCommand,startupTimeoutMs:D.startupTimeoutMs,spawnEnv:D.spawnEnv});for(let A of R){let I=A.static?`static ${typeof A.static==="string"?A.static:A.static.dir}`:A.from;X.success(`https://${A.to} → ${I}`)}if(X.info(`(via rpx daemon pid=${$.pid}; \`rpx daemon:status\` to inspect)`),D.detached)return;let Z=!1,S=B??G_(),J=R.map((A)=>A.id),F=async()=>{if(Z)return;Z=!0;for(let A of J)await Q_(A,B,_).catch((I)=>{N("runner",`removeEntry(${A}) failed: ${I}`,_)})},w=(A)=>{N("runner",`received ${A}, unregistering ${J.length} entries`,_),F().finally(()=>q.exit(0))};q.once("SIGINT",w),q.once("SIGTERM",w),q.once("exit",()=>{if(Z)return;for(let A of J)try{c_.unlinkSync(h_.join(S,`${A}.json`))}catch{}}),await new Promise(()=>{})}import{exec as n_}from"node:child_process";import L from"node:fs";import b_ from"node:os";import Z_ from"node:path";import*as d from"node:process";import{promisify as t_}from"node:util";var l=t_(n_);function m_(D){let _=D.trim().toLowerCase();return _==="localhost"||_.endsWith(".localhost")||_.endsWith(".localhost.")}var C=d.platform==="win32"?Z_.join(d.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",W_=!1;async function e(D){if(d.platform==="win32")throw Error("Administrator privileges required on Windows");if(v_()){let{stdout:K}=await l(D);return K}let _=a(),B=D.replace(/'/g,"'\\''");try{if(_){let{stdout:K}=await l(`echo '${_}' | sudo -S sh -c '${B}' 2>/dev/null`);return W_=!0,K}if(W_)try{let{stdout:K}=await l(`sudo -n sh -c '${B}'`);return K}catch(K){N("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:K}=await l(`sudo -n sh -c '${B}'`);return W_=!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)=>!m_(R)),K=D.filter((R)=>m_(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 L.promises.readFile(C,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{R=await e(`cat "${C}"`)}catch(S){throw console.log(" Could not read hosts file — skipping hosts setup"),N("hosts",`sudo read also failed: ${S}`,_),Error(`Cannot read hosts file: ${S}`)}}let G=B.filter((S)=>{let J=`127.0.0.1 ${S}`,F=`::1 ${S}`;return!R.includes(J)&&!R.includes(F)});if(G.length===0){N("hosts","All hosts already exist in hosts file",_);return}let $=G.map((S)=>`
2
2
  # Added by rpx
3
- 127.0.0.1 ${R}
4
- ::1 ${R}`).join(`
5
- `),V=W_.join(c_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await P.promises.writeFile(V,K+$,"utf8"),await o(`cat "${V}" | tee "${C}" > /dev/null`),console.log(` Hosts updated: ${X.join(", ")}`)}catch(R){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),X.forEach((Q)=>{console.log(` 127.0.0.1 ${Q}`),console.log(` ::1 ${Q}`)}),console.log(` Or run: sudo nano ${C}`)}finally{try{await P.promises.unlink(V)}catch{}}}catch(K){N("hosts",`Failed to manage hosts file: ${K.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(R){throw N("hosts",`sudo read also failed: ${R}`,_),Error(`Cannot read hosts file: ${R}`)}}let G=B.split(`
6
- `),K=!1,X=G.filter((R)=>{if(D.some((T)=>R.includes(` ${T}`)&&(R.includes("127.0.0.1")||R.includes("::1"))))return K=!0,!1;if(R.trim()==="# Added by rpx")return K=!0,!1;return!0});if(!K){N("hosts","No matching hosts found to remove",_);return}while(X[X.length-1]?.trim()==="")X.pop();let $=`${X.join(`
3
+ 127.0.0.1 ${S}
4
+ ::1 ${S}`).join(`
5
+ `),Z=Z_.join(b_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await L.promises.writeFile(Z,R+$,"utf8"),await e(`cat "${Z}" | tee "${C}" > /dev/null`),console.log(` Hosts updated: ${G.join(", ")}`)}catch(S){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),G.forEach((J)=>{console.log(` 127.0.0.1 ${J}`),console.log(` ::1 ${J}`)}),console.log(` Or run: sudo nano ${C}`)}finally{try{await L.promises.unlink(Z)}catch{}}}catch(R){N("hosts",`Failed to manage hosts file: ${R.message}`,_)}}async function A_(D,_){N("hosts",`Removing hosts: ${D.join(", ")}`,_);try{let B;try{B=await L.promises.readFile(C,"utf-8")}catch{N("hosts","Reading hosts file requires elevated permissions, using sudo",_);try{B=await e(`cat "${C}"`)}catch(S){throw N("hosts",`sudo read also failed: ${S}`,_),Error(`Cannot read hosts file: ${S}`)}}let K=B.split(`
6
+ `),R=!1,G=K.filter((S)=>{if(D.some((F)=>S.includes(` ${F}`)&&(S.includes("127.0.0.1")||S.includes("::1"))))return R=!0,!1;if(S.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
- `,V=W_.join(c_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await P.promises.writeFile(V,$,"utf8"),await o(`cat "${V}" | tee "${C}" > /dev/null`),N("hosts","Hosts removed successfully",_)}catch(R){N("hosts","Could not clean up hosts file automatically",_)}finally{try{await P.promises.unlink(V)}catch(R){N("hosts",`Failed to remove temporary file: ${R}`,_)}}}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(G){N("hosts",`Error reading hosts file: ${G}`,_);try{let K=l(),X;if(K)X=`echo '${K}' | sudo -S cat "${C}" 2>/dev/null`;else X=`sudo -n cat "${C}" 2>/dev/null || cat "${C}" 2>/dev/null || echo ""`;let{stdout:$}=await s(X);B=$}catch(K){return N("hosts",`Cannot read hosts file, assuming entries don't exist: ${K}`,_),D.map(()=>!1)}}return D.map((G)=>{let K=`127.0.0.1 ${G}`,X=`::1 ${G}`;return B.includes(K)||B.includes(X)})}import*as e from"node:net";function U(D,_,B){return N("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((G)=>{let K=e.createServer(),X=setTimeout(()=>{N("port",`Checking port ${D} timed out, assuming it's in use`,B),K.close(),G(!0)},3000);K.once("error",($)=>{if(clearTimeout(X),$.code==="EADDRINUSE")N("port",`Port ${D} is in use`,B),G(!0);else N("port",`Error checking port ${D}: ${$.message}`,B),G(!0)}),K.once("listening",()=>{clearTimeout(X),N("port",`Port ${D} is available`,B),K.close(),G(!1)});try{K.listen(D,_)}catch($){clearTimeout(X),N("port",`Exception checking port ${D}: ${$}`,B),G(!0)}})}async function u_(D,_,B,G=50){N("port",`Finding available port starting from ${D} (max attempts: ${G})`,B);let K=D,X=0;while(X<G){if(X++,!await U(K,_,B))return N("port",`Found available port: ${K} after ${X} attempts`,B),K;N("port",`Port ${K} is in use, trying ${K+1} (attempt ${X}/${G})`,B),K++}throw Error(`Unable to find available port after ${G} attempts starting from ${D}`)}function h_(D,_,B=5000,G){return N("port",`Testing connection to ${_}:${D}`,G),new Promise((K)=>{let X=e.connect({host:_,port:D,timeout:B});X.once("connect",()=>{N("port",`Successfully connected to ${_}:${D}`,G),X.end(),K(!0)}),X.once("timeout",()=>{N("port",`Connection to ${_}:${D} timed out`,G),X.destroy(),K(!1)}),X.once("error",($)=>{N("port",`Failed to connect to ${_}:${D}: ${$.message}`,G),X.destroy(),K(!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 h_(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 u_(D,this.hostname,this.verbose,this.maxRetries);if(_){if(!await h_(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 i_}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[G,...K]=_.command.split(" "),X=_.cwd||L.cwd();N("start",`Starting process ${D}:`,B),N("start",` Command: ${G} ${K.join(" ")}`,B),N("start",` Working directory: ${X}`,B),N("start",` Environment variables: ${y(_.env)}`,B);let $=i_(G,K,{cwd:X,env:{...L.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(D,{command:_.command,cwd:X,process:$,env:_.env}),new Promise((V,R)=>{if($.on("error",(Q)=>{if(!this.isShuttingDown)N("start",`Process ${D} failed to start: ${Q}`,B),this.processes.delete(D),R(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),R(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),R(Error(`Process ${D} was killed during startup`));else N("start",`Process ${D} started successfully`,B),V()},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((G)=>{if(!B.process){G();return}B.process.once("exit",()=>{this.processes.delete(D),N("start",`Process ${D} stopped`,_),G()});try{B.process.kill("SIGTERM"),setTimeout(()=>{if(B.process){N("start",`Force killing process ${D}`,_);try{B.process.kill("SIGKILL")}catch(K){}}},3000)}catch(K){N("start",`Error stopping process ${D}: ${K}`,_),this.processes.delete(D),G()}})}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((G)=>{J.error(`Failed to stop process ${B}:`,G)}));await Promise.allSettled(_),this.processes.clear(),this.isShuttingDown=!1}isRunning(D){let _=this.processes.get(D);return!!_?.process&&!_.process.killed}}var uD=new __;var B_=new __,t_="0.12.0",r_=new d("0.0.0.0"),g=new Set,S_=!1,D_=null,V_=null;async function i(D){if(S_)return N("cleanup","Cleanup already in progress, skipping",D?.verbose),V_||Promise.resolve();S_=!0,N("cleanup","Starting cleanup process",D?.verbose),V_=new Promise((_)=>{D_=_});try{await B_.stopAll(D?.verbose),J.info("Shutting down proxy servers...");let _=[],B=Array.from(g).map((G)=>new Promise((K)=>{G.close(()=>{N("cleanup","Server closed successfully",D?.verbose),K()})}));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 G=D.domains.filter((K)=>{if(K==="test.local")return!0;return K!=="localhost"&&!K.startsWith("localhost.")&&K!=="127.0.0.1"});if(N("cleanup",`Filtered domains for cleanup: ${JSON.stringify(G)}`,D?.verbose),G.length>0)J.info("Cleaning up hosts file entries..."),_.push(Z_(G,D?.verbose).then(()=>{N("cleanup",`Removed hosts entries for ${G.join(", ")}`,D?.verbose)}).catch((K)=>{N("cleanup",`Failed to remove hosts entries: ${K}`,D?.verbose),J.warn(`Failed to clean up hosts file entries for ${G.join(", ")}:`,K)}))}if(D?.certs&&D.domains?.length){N("cleanup","Cleaning up SSL certificates",D?.verbose),J.info("Cleaning up SSL certificates...");let G=D.domains.map(async(K)=>{try{await M_(K,D?.verbose),N("cleanup",`Removed certificates for ${K}`,D?.verbose)}catch(X){N("cleanup",`Failed to remove certificates for ${K}: ${X}`,D?.verbose),J.warn(`Failed to clean up certificates for ${K}:`,X)}});_.push(...G)}await Promise.allSettled(_),N("cleanup","All cleanup tasks completed successfully",D?.verbose),J.success("All cleanup tasks completed successfully")}catch(_){N("cleanup",`Error during cleanup: ${_}`,D?.verbose),J.error("Error during cleanup:",_)}finally{if(D_)D_();D_=null,S_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(I.env.NODE_ENV!=="test"&&I.env.BUN_ENV!=="test"&&!_)I.exit(0)}return V_}var A_=!1;function k_(D){if(A_){N("signal",`Received second ${D} signal, forcing exit`,!0),I.exit(1);return}A_=!0,N("signal",`Received ${D} signal, initiating cleanup`,!0),i().catch((_)=>{N("signal",`Cleanup failed after ${D}: ${_}`,!0),I.exit(1)}).finally(()=>{A_=!1})}I.once("SIGINT",()=>k_("SIGINT"));I.once("SIGTERM",()=>k_("SIGTERM"));I.on("uncaughtException",(D)=>{N("process",`Uncaught exception: ${D}`,!0),J.error("Uncaught exception:",D),k_("uncaughtException")});async function p(D,_,B,G=5){N("connection",`Testing connection to ${D}:${_} (retries left: ${G})`,B);let K=15000,X=Date.now();if(I.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((V,R)=>{let Q=b_.connect({host:D,port:_,timeout:3000});Q.once("connect",()=>{N("connection",`Successfully connected to ${D}:${_}`,B),Q.end(),V()}),Q.once("timeout",()=>{N("connection",`Connection to ${D}:${_} timed out`,B),Q.destroy(),R(Error("Connection timed out"))}),Q.once("error",(T)=>{N("connection",`Failed to connect to ${D}:${_}: ${T}`,B),Q.destroy(),R(T)})});try{await $()}catch(V){if(Date.now()-X>K){N("connection",`Connection test timed out after ${K}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"&&G>0)return N("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${G} retries left)`,B),await new Promise((Q)=>setTimeout(Q,2000)),p(D,_,B,G-1);if(G>0)try{N("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((Q,T)=>{let w=c.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(z)=>{N("connection",`Received HTTP response with status: ${z.statusCode}`,B),Q()});w.on("error",(z)=>T(z)),w.on("timeout",()=>{w.destroy(),T(Error("HTTP request timed out"))}),w.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... (${G} retries left)`,B),await new Promise((T)=>setTimeout(T,2000)),p(D,_,B,G-1)}let R=`Failed to connect to ${D}:${_} after ${5-G} attempts: ${V.message}`;N("connection",`${R}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,B),J.warn(R),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 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"),G=Number.parseInt(_.port)||(_.protocol.includes("https:")?443:80),K=[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(K,D.verbose))[0]){J.info(`Adding ${B.hostname} to hosts file...`),J.info("This may require sudo/administrator privileges");try{await m(K,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}`),I.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 N("hosts",`Host entry already exists for ${B.hostname}`,D.verbose)}catch($){J.error("Failed to check hosts file:",$.message)}}try{await p(_.hostname,G,D.verbose)}catch($){N("server",`Connection test failed: ${$}`,D.verbose),J.error($.message),J.warn("Continuing with proxy setup despite connection test failure..."),J.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let X=D._cachedSSLConfig||null;if(D.https)try{if(D.https===!0)D.https=K_({...D,to:B.hostname});if(X=await b({...D,to:B.hostname,https:D.https}),!X){if(N("ssl",`Generating new certificates for ${B.hostname}`,D.verbose),await G_({...D,from:_.toString(),to:B.hostname,https:D.https}),X=await b({...D,to:B.hostname,https:D.https}),!X)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 o_({...D,from:D.from||"localhost:5173",to:B.hostname,fromPort:G,sourceUrl:{hostname:_.hostname,host:_.host},ssl:X})}async function s_(D,_,B,G,K,X,$,V,R,Q,T){N("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${Q}`,R);function w(A){let W={};for(let[S,M]of Object.entries(A))if(!S.startsWith(":"))W[S]=M;return W}let z=(A,W)=>{N("request",`Incoming request: ${A.method} ${A.url}`,R);let S=A.url||"/",M=A.method||"GET";if(A instanceof v_.Http2ServerRequest){let Z=A.headers;M=Z[":method"]||M,S=Z[":path"]||S}if(Q){if(!S.match(/\.[a-z0-9]+$/i))if(S.endsWith("/"))S=`${S}index.html`;else S=`${S}.html`}let E=w(A.headers);if(T)E.host=`${X.hostname}:${B}`,N("request",`Changed origin: setting host header to ${E.host}`,R);let k={hostname:X.hostname,port:B,path:S,method:M,headers:E};N("request",`Proxy request options: ${y(k)}`,R);let h=c.request(k,(Z)=>{if(N("response",`Proxy response received with status ${Z.statusCode}`,R),Q&&Z.statusCode===404){let H=[];if(S.endsWith(".html"))H.push(S.slice(0,-5));else if(!S.match(/\.[a-z0-9]+$/i))H.push(`${S}.html`);if(!S.endsWith("/"))H.push(`${S}/index.html`);if(H.length>0){N("cleanUrls",`Trying alternative paths: ${H.join(", ")}`,R);let x=(O)=>{if(O.length===0){W.writeHead(Z.statusCode||404,Z.headers),Z.pipe(W);return}let u=O[0],a_={...k,path:u},y_=c.request(a_,(n)=>{if(n.statusCode===200)N("cleanUrls",`Found matching path: ${u}`,R),W.writeHead(n.statusCode,n.headers),n.pipe(W);else x(O.slice(1))});y_.on("error",()=>x(O.slice(1))),y_.end()};x(H);return}}let F={...Z.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};W.writeHead(Z.statusCode||500,F),Z.pipe(W)});h.on("error",(Z)=>{N("request",`Proxy request failed: ${Z}`,R),J.error("Proxy request failed:",Z),W.writeHead(502),W.end(`Proxy Error: ${Z.message}`)}),A.pipe(h)};if(N("server",`Creating server with SSL config: ${!!$}`,R),$)return new Promise((A,W)=>{try{let S=Bun.serve({port:G,hostname:K,tls:{key:$.key,cert:$.cert,ca:$.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(M){let E=new URL(M.url);N("request",`Bun.serve received: ${M.method} ${E.pathname}`,R);let k=`http://${X.host}`,h=new URL(E.pathname+E.search,k);try{let Z=new Headers(M.headers);if(Z.set("host",X.host),T)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:M.method,headers:Z,body:M.body,redirect:"manual"}),H=new Headers(F.headers);if(Q&&E.pathname.endsWith(".html")){let x=E.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}`,R),new Response(`Proxy Error: ${Z}`,{status:502})}},error(M){return N("server",`Bun.serve error: ${M}`,R),new Response(`Server Error: ${M.message}`,{status:500})}});g.add(S),m_({from:D,to:_,vitePluginUsage:V,listenPort:G,ssl:!0,cleanUrls:Q,verbose:R}),A()}catch(S){W(S)}});let j=c.createServer(z);function Y(A){return g.add(A),new Promise((W,S)=>{A.listen(G,K,()=>{N("server",`Server listening on port ${G}`,R),m_({from:D,to:_,vitePluginUsage:V,listenPort:G,ssl:!!$,cleanUrls:Q,verbose:R}),W()}),A.on("error",(M)=>{N("server",`Server error: ${M}`,R),S(M)})})}return Y(j)}async function o_(D){N("setup",`Setting up reverse proxy: ${y(D)}`,D.verbose);let{from:_,to:B,fromPort:G,sourceUrl:K,ssl:X,verbose:$,cleanup:V,vitePluginUsage:R,changeOrigin:Q,cleanUrls:T}=D,w=80,z=443,j="0.0.0.0",Y=D.portManager||r_,A=x_(D);try{if(A&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1")){if(!(await v([B],$))[0]){J.warn(`The hostname ${B} isn't in your hosts file. Adding it now...`);try{await m([B],$),J.success(`Added ${B} to your hosts file.`)}catch(k){J.error(`Failed to add ${B} to your hosts file: ${k}`),J.info(`You may need to manually add '127.0.0.1 ${B}' to your /etc/hosts file.`)}}}else if(A&&I.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(X&&!Y.usedPorts.has(w)){if(!await U(w,j,$))N("setup","Starting HTTP redirect server",$),l_($),Y.usedPorts.add(w);else if(N("setup","Port 80 is in use, skipping HTTP redirect",$),$)J.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let W=X?z:w,S=await U(W,j,$),M;if(S){if(N("setup",`Port ${W} is already in use`,$),$)J.warn(`Port ${W} is already in use. This may be another instance of rpx or another service.`);if(W===443){if(M=await Y.getNextAvailablePort(3443,!0),N("setup",`Using port ${M} instead of ${W}`,$),$)J.info(`Using port ${M} instead. Access your site at https://${B}:${M}`)}else if(M=await Y.getNextAvailablePort(W+1000,!0),N("setup",`Using port ${M} instead of ${W}`,$),$)J.info(`Using port ${M} instead. Access your site at http://${B}:${M}`)}else M=W,Y.usedPorts.add(M),N("setup",`Using standard ${W===443?"HTTPS":"HTTP"} port ${W} for ${B}`,$);await s_(_,B,G,M,j,K,X,R,$,T,Q)}catch(W){N("setup",`Setup failed: ${W}`,$),J.error(`Failed to setup reverse proxy: ${W.message}`),i({domains:[B],hosts:typeof V==="boolean"?V:V?.hosts,certs:typeof V==="boolean"?V:V?.certs,verbose:$,vitePluginUsage:R})}}function l_(D){N("redirect","Starting HTTP redirect server",D);let _=c.createServer((B,G)=>{let K=B.headers.host||"";N("redirect",`Redirecting request from ${K}${B.url} to HTTPS`,D),G.writeHead(301,{Location:`https://${K}${B.url}`}),G.end()}).listen(80);g.add(_),N("redirect","HTTP redirect server started",D)}function e_(D){let _={...N_,...D};if(N("proxy",`Starting proxy with options: ${y(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){J.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((R)=>{J.error(`Failed to register with rpx daemon: ${R.message}`),I.exit(1)});return}let B=_.to||"",G=B.split(".").pop()?.toLowerCase()||"",K=I.platform==="darwin"&&B&&!B.includes("localhost")&&!B.includes("127.0.0.1"),X=["dev","app","page","new","day","foo"],$=["test","localhost","local","example","invalid"];if(K&&X.includes(G)&&_?.verbose)J.warn(`The .${G} TLD may not work reliably for local development`),J.info(` Google owns .${G} with HSTS preloading, which can bypass local DNS`),J.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(K)import("./chunk-fndafyac.js").then(({setupDevelopmentDns:R})=>{R({domains:[B],verbose:_.verbose}).then((Q)=>{if(Q)Promise.resolve().then(()=>{if(_.verbose)if($.includes(G))J.success(`DNS server started for .${G} domains`);else J.success(`DNS server started for .${G} domains (hosts file entry also added)`)});else N("dns",`Could not start DNS server - ${B} may not resolve in browser`,_.verbose)})}).catch((R)=>{N("dns",`Failed to start DNS server: ${R}`,_.verbose)});let V={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:K_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};N("proxy",`Server options: ${y(V)}`,_.verbose),C_(V).catch((R)=>{N("proxy",`Failed to start proxy: ${R}`,_.verbose),J.error(`Failed to start proxy: ${R.message}`),i({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function _2(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 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=_2(_),G=x_(_);if(N("config",`Starting with config: ${y(_,2)}`,B),N("config",`Is multi-proxy? ${"proxies"in _}`,B),N("config",`Hosts management enabled? ${G}`,B),_.viaDaemon){let A="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((W)=>({id:W.id,from:W.from,to:W.to,path:W.path,cleanUrls:W.cleanUrls??_.cleanUrls,changeOrigin:W.changeOrigin??_.changeOrigin,pathRewrites:W.pathRewrites})):[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await r({proxies:A,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 A=`${Y.from}-${Y.to}`;try{N("watch",`Starting command for ${A} with command: ${Y.start.command}`,B),J.info(`Starting command for ${A}...`),await B_.startProcess(A,Y.start,B);let W=new URL(Y.from.startsWith("http")?Y.from:`http://${Y.from}`),S=W.hostname||"localhost",M=Number(W.port)||80;try{await p(S,M,B),N("watch",`Dev server is ready at ${S}:${M}`,B)}catch(E){N("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(W){throw N("watch",`Failed to start command for ${A}: ${W}`,B),Error(`Failed to start command for ${A}: ${W}`)}}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 A=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),W=A.hostname||"localhost",S=Number(A.port)||80;try{await p(W,S,B),N("watch",`Dev server is ready at ${W}:${S}`,B)}catch(M){N("watch",`Connection check failed, but continuing with proxy setup: ${M}`,B),J.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(A){throw N("watch",`Failed to run start command: ${A}`,B),Error(`Failed to run start command: ${A}`)}}else N("watch","No start command found in config",B);let K="proxies"in _&&Array.isArray(_.proxies)?_.proxies[0]?.to:("to"in _)?_.to:"rpx.localhost";if(I.platform!=="win32"&&(_.https||G)){if(!l())try{N("sudo","Pre-acquiring sudo credentials for privileged operations",B),n_("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 ${K}, generating new ones`,_.verbose),await G_(_),Y=await b(_),!Y)throw Error(`Failed to load SSL certificates after generation for ${K}`)}else N("ssl",`Using existing and trusted certificates for ${K}`,_.verbose);_._cachedSSLConfig=Y}let X="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}],$=X.map((Y)=>Y.to||"rpx.localhost"),V=_._cachedSSLConfig,R=$.filter((Y)=>Y&&!Y.includes("localhost")&&!Y.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],T=["test","localhost","local","example","invalid"],w=[...new Set(R.map((Y)=>Y.split(".").pop()?.toLowerCase()))],z=w.filter((Y)=>!!Y&&Q.includes(Y));if(z.length>0&&B)J.warn(`The following TLDs may not work reliably for local development: ${z.map((Y)=>`.${Y}`).join(", ")}`),J.info(" These TLDs have HSTS preloading which can bypass local DNS"),J.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(G&&I.platform==="darwin"&&R.length>0){let{setupDevelopmentDns:Y}=await import("./chunk-fndafyac.js");if(await Y({domains:R,verbose:B})){if(B)if(w.every((S)=>!!S&&T.includes(S)))J.success(`DNS server started for ${w.map((S)=>`.${S}`).join(", ")} domains`);else J.success(`DNS server started for ${w.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 j=async()=>{N("cleanup","Starting cleanup handler",_.verbose);try{let{tearDownDevelopmentDns:Y}=await import("./chunk-fndafyac.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(I.on("SIGINT",j),I.on("SIGTERM",j),I.on("uncaughtException",(Y)=>{N("process",`Uncaught exception: ${Y}`,!0),console.error("Uncaught exception:",Y),j()}),V&&X.length>1){N("proxies",`Creating shared HTTPS server for ${X.length} domains`,B);let Y=[],A=new Set;for(let Z of X){let F=Z.to||"rpx.localhost",H=Z.cleanUrls||!1,x=Z.path,O=w_(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(A.has(F))continue;if(A.add(F),G&&!j_(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))l_(B);let S=443;if(await U(S,"0.0.0.0",B)){if(N("proxies",`Port ${S} is already in use, cannot start shared proxy`,B),B)J.warn(`Port ${S} is in use. Shared HTTPS proxy cannot start.`);return}let E=E_(Y),k=T_((Z,F)=>H_(E,Z,F),B),h=I_(B);try{let Z=Bun.serve({port:S,hostname:"0.0.0.0",tls:{key:V.key,cert:V.cert,ca:V.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})}});g.add(Z),N("proxies",`Shared HTTPS proxy listening on port ${S} for ${E.size} domains`,B)}catch(Z){N("proxies",`Failed to start shared proxy: ${Z}`,B),console.error("Failed to start shared HTTPS proxy:",Z),j()}}else for(let Y of X)try{let A=Y.to||"rpx.localhost";N("proxy",`Starting proxy for ${A} with SSL config: ${!!V}`,Y.verbose),await C_({from:Y.from||"localhost:5173",to:A,cleanUrls:Y.cleanUrls||!1,https:Y.https||!1,cleanup:Y.cleanup||!1,vitePluginUsage:Y.vitePluginUsage||!1,verbose:Y.verbose||!1,_cachedSSLConfig:V,changeOrigin:Y.changeOrigin||!1})}catch(A){N("proxies",`Failed to start proxy for ${Y.to}: ${A}`,Y.verbose),console.error(`Failed to start proxy for ${Y.to}:`,A),j()}}function m_(D){if(D?.vitePluginUsage||!D?.verbose)return;if(console.log(""),console.log(` ${f.green(f.bold("rpx"))} ${f.green(`v${t_}`)}`),console.log(` ${f.green("➜")} ${f.dim(D?.from??"")} ${f.dim("➜")} ${f.cyan(D?.ssl?`https://${D?.to}`:`http://${D?.to}`)}`),D?.listenPort!==(D?.ssl?443:80))console.log(` ${f.green("➜")} Listening on port ${D?.listenPort}`);if(D?.cleanUrls)console.log(` ${f.green("➜")} Clean URLs enabled`)}var D2=["/.well-known/acme-challenge/"];function B2(D){let _=D.headers.get("host");if(_)return _.split(":")[0].toLowerCase();try{return new URL(D.url).hostname.toLowerCase()}catch{return""}}function N2(D){let _=D.header.toLowerCase(),B=new Set,G=[];for(let R of D.hosts){let Q=R.toLowerCase();if(Q.startsWith("*."))G.push(Q);else B.add(Q)}let K=D.exemptPaths??D2,X=D.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
- `,$=(R)=>{let Q=R.toLowerCase();return B.has(Q)||G.some((T)=>q_(Q,T))},V=(R)=>{let Q=B2(R);if(!$(Q))return;let T="/";try{T=new URL(R.url).pathname}catch{}if(K.some((w)=>T.startsWith(w)))return;if(R.headers.get(_)===D.value)return;return new Response(X,{status:403,headers:{"content-type":"text/plain"}})};return V.protects=$,V}var yB=f_;export{$_ as writeEntry,a2 as watchRegistry,$2 as verifyHttpsChain,F2 as trustRootCaForBrowsers,YD as tearDownDevelopmentDns,RD as syncDevelopmentDnsFromRegistry,q2 as stripBasePath,e2 as stopDnsServer,MD as stopDaemon,C_ as startServer,e_ as startProxy,f_ as startProxies,o2 as startDnsServer,ND as setupResolver,KD as setupDevelopmentDns,P2 as serverNameFromCertFilename,f2 as serveStaticFile,y as safeStringify,C2 as safeRelativePath,lB as safeDeleteFile,r as runViaDaemon,AD as runDaemon,DD as resolverFilePath,n2 as resolverBasenamesForDomains,i2 as resolverBasenameForDomain,F_ as resolveStaticRoute,x2 as resolveStaticFile,bB as resolvePathRewrite,$D as removeResolver,GD as removeLegacyTldResolvers,Z_ as removeHosts,X_ as removeEntry,VD as releaseDaemonLock,OB as redactSensitive,XD as reconcileStaleDevelopmentDns,FD as reconcileDevelopmentDnsOnIdle,v2 as readEntry,WD as readDaemonPid,K2 as readCertSha256Fingerprint,R2 as readCertCommonName,b2 as readAll,A2 as pruneStaleRootCas,p_ as portManager,U2 as pathPrefixMatches,X2 as parseSha256HashesFromSecurityListing,G2 as normalizeSha256Fingerprint,w_ as normalizePathPrefix,p2 as normalizeDevDomain,q_ as matchesWildcard,c2 as matchesAllowedSuffix,H_ as matchHostRoute,O2 as matchHostList,y2 as matchHost,j2 as loadSSLConfig,V2 as listCertSha256HashesByCommonName,j_ as isWildcardPattern,LB as isValidRootCA,Y_ as isValidId,mB as isSingleProxyOptions,vB as isSingleProxyConfig,z2 as isRootCaTrustedForSsl,M2 as isRootCaFingerprintInKeychains,U as isPortInUse,m2 as isPidAlive,uB as isMultiProxyOptions,hB as isMultiProxyConfig,h2 as isLikelyHostname,_D as isDnsServerRunning,ZD as isDaemonRunning,H2 as isCertTrusted,K_ as httpsConfig,l as getSudoPassword,I2 as getSharedDaemonCertPaths,T2 as getRootCAPaths,R_ as getRegistryDir,cB as getPrimaryDomain,S2 as getMacosTrustKeychains,Z2 as getMacosLoginKeychainPath,JD as getDaemonRpxDir,QD as getDaemonPidPath,G_ as generateCertificate,l2 as gcStaleEntries,w2 as forceTrustCertificate,u_ as findAvailablePort,PB as extractHostname,UB as execSudoSync,J_ as ensureDaemonRunning,t2 as devDomainsFromHosts,P_ as deriveIdFromTarget,zD as defaultDaemonSpawnCommand,N_ as defaultConfig,yB as default,N as debugLog,I_ as createProxyWebSocketHandler,T_ as createProxyFetchHandler,N2 as createOriginGuard,k2 as contentTypeFor,BD as contentLooksLikeRpxResolver,N_ as config,f as colors,E2 as clearSslConfigCache,M_ as cleanupCertificates,i as cleanup,v as checkHosts,b as checkExistingCertificates,Y2 as certIncludesSanHostnames,L2 as buildSniTlsConfig,E_ as buildHostRoutes,m as addHosts,SD as acquireDaemonLock,W2 as RPX_ROOT_CA_COMMON_NAME,s2 as RPX_RESOLVER_MARKER,u2 as OnDemandCertManager,Q2 as MACOS_SYSTEM_KEYCHAIN,J2 as MACOS_CA_TRUST_FLAGS,g2 as LEGACY_TLD_RESOLVER_LABELS,d as DefaultPortManager,d2 as DNS_STATE_VERSION,r2 as DNS_PORT};
8
+ `,Z=Z_.join(b_.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await L.promises.writeFile(Z,$,"utf8"),await e(`cat "${Z}" | tee "${C}" > /dev/null`),N("hosts","Hosts removed successfully",_)}catch(S){N("hosts","Could not clean up hosts file automatically",_)}finally{try{await L.promises.unlink(Z)}catch(S){N("hosts",`Failed to remove temporary file: ${S}`,_)}}}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 L.promises.readFile(C,"utf-8")}catch(K){N("hosts",`Error reading hosts file: ${K}`,_);try{let R=a(),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 l(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 __ from"node:net";function O(D,_,B){return N("port",`Checking if port ${D} is in use on ${_}`,B),new Promise((K)=>{let R=__.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 a_(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 O(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 l_(D,_,B=5000,K){return N("port",`Testing connection to ${_}:${D}`,K),new Promise((R)=>{let G=__.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 g{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 O(D,this.hostname,this.verbose))return this.findNextAvailablePort(D+1,_);if(_){if(!await l_(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 a_(D,this.hostname,this.verbose,this.maxRetries);if(_){if(!await l_(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 r_=new g;import{spawn as s_}from"node:child_process";import*as c from"node:process";class D_{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||c.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 $=s_(K,R,{cwd:G,env:{...c.env,..._.env},shell:!0,stdio:"inherit"});return this.processes.set(D,{command:_.command,cwd:G,process:$,env:_.env}),new Promise((Z,S)=>{if($.on("error",(J)=>{if(!this.isShuttingDown)N("start",`Process ${D} failed to start: ${J}`,B),this.processes.delete(D),S(J),c.emit("SIGINT")}),$.on("exit",(J)=>{if(!this.isShuttingDown&&J!==null&&J!==0)N("start",`Process ${D} exited with code ${J}`,B),this.processes.delete(D),S(Error(`Process ${D} exited with code ${J}`)),c.emit("SIGINT")}),B)$.stdout?.on("data",(J)=>{N("process",`[${D}] ${J.toString().trim()}`,!0)}),$.stderr?.on("data",(J)=>{N("process",`[${D}] ERR: ${J.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&$.killed)this.processes.delete(D),S(Error(`Process ${D} was killed during startup`));else N("start",`Process ${D} started successfully`,B),Z()},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 p2=new D_;import{timingSafeEqual as o_}from"node:crypto";function e_(D,_){if(D==null)return!1;let B=Buffer.from(D),K=Buffer.from(_);if(B.length!==K.length)return!1;return o_(B,K)}function B_(D){return D.toLowerCase().replace(/\.$/,"")}var _D=["/.well-known/acme-challenge/"];function DD(D){let _=D.headers.get("host");if(_)return B_(_.split(":")[0]);try{return B_(new URL(D.url).hostname)}catch{return""}}function M_(D){let _=D.header.toLowerCase(),B=new Set,K=[];for(let S of D.hosts){let J=B_(S);if(J.startsWith("*."))K.push(J);else B.add(J)}let R=D.exemptPaths??_D,G=D.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
+ `,$=(S)=>{let J=B_(S);return B.has(J)||K.some((F)=>T_(J,F))},Z=(S)=>{let J=DD(S);if(!$(J))return;let F="/";try{F=new URL(S.url).pathname}catch{}if(R.some((w)=>F.startsWith(w)))return;if(e_(S.headers.get(_),D.value))return;return new Response(G,{status:403,headers:{"content-type":"text/plain"}})};return Z.protects=$,Z}var K_=new D_,ND="0.12.0",KD=new g("0.0.0.0"),p=new Set,z_=!1,N_=null,F_=null;async function n(D){if(z_)return N("cleanup","Cleanup already in progress, skipping",D?.verbose),F_||Promise.resolve();z_=!0,N("cleanup","Starting cleanup process",D?.verbose),F_=new Promise((_)=>{N_=_});try{await K_.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(A_(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 w_(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(N_)N_();N_=null,z_=!1;let _=D&&"vitePluginUsage"in D&&D.vitePluginUsage===!0;if(j.env.NODE_ENV!=="test"&&j.env.BUN_ENV!=="test"&&!_)j.exit(0)}return F_}var I_=!1;function U_(D){if(I_){N("signal",`Received second ${D} signal, forcing exit`,!0),j.exit(1);return}I_=!0,N("signal",`Received ${D} signal, initiating cleanup`,!0),n().catch((_)=>{N("signal",`Cleanup failed after ${D}: ${_}`,!0),j.exit(1)}).finally(()=>{I_=!1})}j.once("SIGINT",()=>U_("SIGINT"));j.once("SIGTERM",()=>U_("SIGTERM"));j.on("uncaughtException",(D)=>{N("process",`Uncaught exception: ${D}`,!0),X.error("Uncaught exception:",D),U_("uncaughtException")});async function i(D,_,B,K=5){N("connection",`Testing connection to ${D}:${_} (retries left: ${K})`,B);let R=15000,G=Date.now();if(j.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((Z,S)=>{let J=p_.connect({host:D,port:_,timeout:3000});J.once("connect",()=>{N("connection",`Successfully connected to ${D}:${_}`,B),J.end(),Z()}),J.once("timeout",()=>{N("connection",`Connection to ${D}:${_} timed out`,B),J.destroy(),S(Error("Connection timed out"))}),J.once("error",(F)=>{N("connection",`Failed to connect to ${D}:${_}: ${F}`,B),J.destroy(),S(F)})});try{await $()}catch(Z){let S=Z;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(S.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((F)=>setTimeout(F,2000)),i(D,_,B,K-1);if(K>0)try{N("connection",`Trying HTTP request to ${D}:${_}`,B),await new Promise((F,w)=>{let A=h.request({hostname:D,port:_,path:"/",method:"HEAD",timeout:5000},(I)=>{N("connection",`Received HTTP response with status: ${I.statusCode}`,B),F()});A.on("error",(I)=>w(I)),A.on("timeout",()=>{A.destroy(),w(Error("HTTP request timed out"))}),A.end()}),N("connection",`HTTP request to ${D}:${_} succeeded`,B);return}catch(F){return N("connection",`HTTP request to ${D}:${_} failed: ${F}`,B),N("connection",`Retrying socket connection in 2 seconds... (${K} retries left)`,B),await new Promise((w)=>setTimeout(w,2000)),i(D,_,B,K-1)}let J=`Failed to connect to ${D}:${_} after ${5-K} attempts: ${S.message}`;N("connection",`${J}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,B),X.warn(J),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 O_(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(P_(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(Z){if(X.error("Failed to add hosts entry:",Z.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}`),j.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 i(_.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=Y_({...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 S_({...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 SD({...D,from:D.from||"localhost:5173",to:B.hostname,fromPort:K,sourceUrl:{hostname:_.hostname,host:_.host},ssl:G})}async function RD(D,_,B,K,R,G,$,Z,S,J,F){N("proxy",`Creating proxy server ${D} -> ${_} with cleanUrls: ${J}`,S);function w(W){let Q={};for(let[V,T]of Object.entries(W))if(!V.startsWith(":"))Q[V]=T;return Q}let A=(W,Q)=>{N("request",`Incoming request: ${W.method} ${W.url}`,S);let V=W.url||"/",T=W.method||"GET";if(W instanceof g_.Http2ServerRequest){let z=W.headers;T=z[":method"]||T,V=z[":path"]||V}if(J){if(!V.match(/\.[a-z0-9]+$/i))if(V.endsWith("/"))V=`${V}index.html`;else V=`${V}.html`}let H=w(W.headers);if(F)H.host=`${G.hostname}:${B}`,N("request",`Changed origin: setting host header to ${H.host}`,S);let k={hostname:G.hostname,port:B,path:V,method:T,headers:H};N("request",`Proxy request options: ${y(k)}`,S);let P=h.request(k,(z)=>{if(N("response",`Proxy response received with status ${z.statusCode}`,S),J&&z.statusCode===404){let M=[];if(V.endsWith(".html"))M.push(V.slice(0,-5));else if(!V.match(/\.[a-z0-9]+$/i))M.push(`${V}.html`);if(!V.endsWith("/"))M.push(`${V}/index.html`);if(M.length>0){N("cleanUrls",`Trying alternative paths: ${M.join(", ")}`,S);let E=(f)=>{if(f.length===0){Q.writeHead(z.statusCode||404,z.headers),z.pipe(Q);return}let U=f[0],t={...k,path:U},u=h.request(t,(r)=>{if(r.statusCode===200)N("cleanUrls",`Found matching path: ${U}`,S),Q.writeHead(r.statusCode,r.headers),r.pipe(Q);else E(f.slice(1))});u.on("error",()=>E(f.slice(1))),u.end()};E(M);return}}let $_={...z.headers,"Strict-Transport-Security":"max-age=31536000; includeSubDomains; preload","X-Content-Type-Options":"nosniff"};Q.writeHead(z.statusCode||500,$_),z.pipe(Q)});P.on("error",(z)=>{N("request",`Proxy request failed: ${z}`,S),X.error("Proxy request failed:",z),Q.writeHead(502),Q.end(`Proxy Error: ${z.message}`)}),W.pipe(P)};if(N("server",`Creating server with SSL config: ${!!$}`,S),$)return new Promise((W,Q)=>{try{let V=Bun.serve({port:K,hostname:R,reusePort:j_(),tls:{key:$.key,cert:$.cert,ca:$.ca,requestCert:!1,rejectUnauthorized:!1},async fetch(T){let H=new URL(T.url);if(N("request",`Bun.serve received: ${T.method} ${H.pathname}`,S),J&&H.pathname.endsWith(".html")){let z=H.pathname.replace(/\.html$/,"");return new Response(null,{status:301,headers:{Location:z}})}let k=`http://${G.host}`,P=`${k}${H.pathname}${H.search}`;try{let z=new Headers(T.headers);if(z.set("host",G.host),F)z.set("origin",k);return z.set("x-forwarded-for","127.0.0.1"),z.set("x-forwarded-proto","https"),z.set("x-forwarded-host",_),await fetch(P,{method:T.method,headers:z,body:T.body,redirect:"manual"})}catch(z){return N("request",`Proxy error: ${z}`,S),new Response(`Proxy Error: ${z}`,{status:502})}},error(T){return N("server",`Bun.serve error: ${T}`,S),new Response(`Server Error: ${T.message}`,{status:500})}});p.add(V),d_({from:D,to:_,vitePluginUsage:Z,listenPort:K,ssl:!0,cleanUrls:J,verbose:S}),W()}catch(V){Q(V)}});let I=h.createServer(A);function Y(W){return p.add(W),new Promise((Q,V)=>{W.listen(K,R,()=>{N("server",`Server listening on port ${K}`,S),d_({from:D,to:_,vitePluginUsage:Z,listenPort:K,ssl:!!$,cleanUrls:J,verbose:S}),Q()}),W.on("error",(T)=>{N("server",`Server error: ${T}`,S),V(T)})})}return Y(I)}async function SD(D){N("setup",`Setting up reverse proxy: ${y(D)}`,D.verbose);let{from:_,to:B,fromPort:K,sourceUrl:R,ssl:G,verbose:$,cleanup:Z,vitePluginUsage:S,changeOrigin:J,cleanUrls:F}=D,w=80,A=443,I="0.0.0.0",Y=D.portManager||KD,W=P_(D);try{if(W&&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(W&&j.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(w)){if(!await O(w,I,$))N("setup","Starting HTTP redirect server",$),i_($),Y.usedPorts.add(w);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 Q=G?A:w,V=await O(Q,I,$),T;if(V){if(N("setup",`Port ${Q} is already in use`,$),$)X.warn(`Port ${Q} is already in use. This may be another instance of rpx or another service.`);if(Q===443){if(T=await Y.getNextAvailablePort(3443,!0),N("setup",`Using port ${T} instead of ${Q}`,$),$)X.info(`Using port ${T} instead. Access your site at https://${B}:${T}`)}else if(T=await Y.getNextAvailablePort(Q+1000,!0),N("setup",`Using port ${T} instead of ${Q}`,$),$)X.info(`Using port ${T} instead. Access your site at http://${B}:${T}`)}else T=Q,Y.usedPorts.add(T),N("setup",`Using standard ${Q===443?"HTTPS":"HTTP"} port ${Q} for ${B}`,$);await RD(_,B,K,T,I,R,G,S,$,F,J)}catch(Q){N("setup",`Setup failed: ${Q}`,$),X.error(`Failed to setup reverse proxy: ${Q.message}`),n({domains:[B],hosts:typeof Z==="boolean"?Z:Z?.hosts,certs:typeof Z==="boolean"?Z:Z?.certs,verbose:$,vitePluginUsage:S})}}function i_(D){N("redirect","Starting HTTP redirect server",D);let _=h.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 YD(D){let _={...R_,...D};if(N("proxy",`Starting proxy with options: ${y(_)}`,_?.verbose),_.viaDaemon){if(!_.from||!_.to){X.error("viaDaemon mode requires both `from` and `to`");return}o({proxies:[{id:_.id,from:_.from,to:_.to,path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}],verbose:_.verbose}).catch((S)=>{X.error(`Failed to register with rpx daemon: ${S.message}`),j.exit(1)});return}let B=_.to||"",K=B.split(".").pop()?.toLowerCase()||"",R=j.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-83dqq28c.js").then(({setupDevelopmentDns:S})=>{S({domains:[B],verbose:_.verbose}).then((J)=>{if(J)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((S)=>{N("dns",`Failed to start DNS server: ${S}`,_.verbose)});let Z={from:_.from,to:_.to,cleanUrls:_.cleanUrls,https:Y_(_),cleanup:_.cleanup,vitePluginUsage:_.vitePluginUsage,changeOrigin:_.changeOrigin,verbose:_.verbose,regenerateUntrustedCerts:_.regenerateUntrustedCerts};N("proxy",`Server options: ${y(Z)}`,_.verbose),O_(Z).catch((S)=>{N("proxy",`Failed to start proxy: ${S}`,_.verbose),X.error(`Failed to start proxy: ${S.message}`),n({domains:[_.to],hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose})})}function $D(D){return D?.verbose||!1}function P_(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 L_(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=$D(_),K=P_(_);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 W="proxies"in _&&Array.isArray(_.proxies)?_.proxies.map((Q)=>({id:Q.id,from:Q.from,to:Q.to,path:Q.path,cleanUrls:Q.cleanUrls??_.cleanUrls,changeOrigin:Q.changeOrigin??_.changeOrigin,pathRewrites:Q.pathRewrites})):[{id:_.id,from:_.from,to:_.to??"rpx.localhost",path:_.path,cleanUrls:_.cleanUrls,changeOrigin:_.changeOrigin,pathRewrites:_.pathRewrites}];await o({proxies:W,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 W=`${Y.from}-${Y.to}`;try{N("watch",`Starting command for ${W} with command: ${Y.start.command}`,B),X.info(`Starting command for ${W}...`),await K_.startProcess(W,Y.start,B);let Q=new URL(Y.from?.startsWith("http")?Y.from:`http://${Y.from}`),V=Q.hostname||"localhost",T=Number(Q.port)||80;try{await i(V,T,B),N("watch",`Dev server is ready at ${V}:${T}`,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(Q){throw N("watch",`Failed to start command for ${W}: ${Q}`,B),Error(`Failed to start command for ${W}: ${Q}`)}}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 K_.startProcess(Y,_.start,B);let W=new URL(_.from?.startsWith("http")?_.from:`http://${_.from}`),Q=W.hostname||"localhost",V=Number(W.port)||80;try{await i(Q,V,B),N("watch",`Dev server is ready at ${Q}:${V}`,B)}catch(T){N("watch",`Connection check failed, but continuing with proxy setup: ${T}`,B),X.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(W){throw N("watch",`Failed to run start command: ${W}`,B),Error(`Failed to run start command: ${W}`)}}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(j.platform!=="win32"&&(_.https||K)){if(!a())try{N("sudo","Pre-acquiring sudo credentials for privileged operations",B),BD("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 S_(_),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"),Z=_._cachedSSLConfig,S=$.filter((Y)=>Y&&!Y.includes("localhost")&&!Y.includes("127.0.0.1")),J=["dev","app","page","new","day","foo"],F=["test","localhost","local","example","invalid"],w=[...new Set(S.map((Y)=>Y.split(".").pop()?.toLowerCase()))],A=w.filter((Y)=>!!Y&&J.includes(Y));if(A.length>0&&B)X.warn(`The following TLDs may not work reliably for local development: ${A.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&&j.platform==="darwin"&&S.length>0){let{setupDevelopmentDns:Y}=await import("./chunk-83dqq28c.js");if(await Y({domains:S,verbose:B})){if(B)if(w.every((V)=>!!V&&F.includes(V)))X.success(`DNS server started for ${w.map((V)=>`.${V}`).join(", ")} domains`);else X.success(`DNS server started for ${w.map((V)=>`.${V}`).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-83dqq28c.js");await Y({verbose:_.verbose})}catch(Y){N("cleanup",`Error stopping DNS server: ${Y}`,_.verbose)}try{await K_.stopAll(_.verbose)}catch(Y){N("cleanup",`Error stopping processes: ${Y}`,_.verbose)}await n({domains:$,hosts:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.hosts,certs:typeof _.cleanup==="boolean"?_.cleanup:_.cleanup?.certs,verbose:_.verbose||!1})};if(j.on("SIGINT",I),j.on("SIGTERM",I),j.on("uncaughtException",(Y)=>{N("process",`Uncaught exception: ${Y}`,!0),console.error("Uncaught exception:",Y),I()}),Z&&G.length>1){N("proxies",`Creating shared HTTPS server for ${G.length} domains`,B);let Y=[],W=new Set;for(let M of G){let E=M.to||"rpx.localhost",f=M.cleanUrls||!1,U=M.path,t=f_(U);if(M.static)Y.push({host:E,path:U,route:{static:H_(M.static,f),cleanUrls:f,basePath:t}}),N("proxies",`Route: ${E}${U??""} → static ${typeof M.static==="string"?M.static:M.static.dir}`,B);else{let u=new URL(M.from?.startsWith("http")?M.from:`http://${M.from}`);Y.push({host:E,path:U,route:{sourceHost:u.host,cleanUrls:f,changeOrigin:M.changeOrigin||!1,pathRewrites:M.pathRewrites,basePath:t}}),N("proxies",`Route: ${E}${U??""} → ${u.host}`,B)}if(W.has(E))continue;if(W.add(E),K&&!x_(E)&&!E.includes("localhost")&&!E.includes("127.0.0.1"))try{if(!(await v([E],B))[0])await m([E],B)}catch{N("hosts",`Could not add hosts entry for ${E}`,B)}}if(!await O(80,"0.0.0.0",B))i_(B);let V=443;if(await O(V,"0.0.0.0",B)){if(N("proxies",`Port ${V} is already in use, cannot start shared proxy`,B),B)X.warn(`Port ${V} is in use. Shared HTTPS proxy cannot start.`);return}let H=q_(Y),k=k_((M,E)=>y_(H,M,E),B),P=_.originGuard?M_(_.originGuard):null,z=P?(M,E)=>P(M)??k(M,E):k,$_=C_(B);try{let M=Bun.serve({port:V,hostname:"0.0.0.0",reusePort:j_(),tls:{key:Z.key,cert:Z.cert,ca:Z.ca,requestCert:!1,rejectUnauthorized:!1},fetch(E,f){return z(E,f)},websocket:$_,error(E){return N("server",`Shared proxy server error: ${E}`,B),new Response(`Server Error: ${E.message}`,{status:500})}});p.add(M),N("proxies",`Shared HTTPS proxy listening on port ${V} for ${H.size} domains`,B)}catch(M){N("proxies",`Failed to start shared proxy: ${M}`,B),console.error("Failed to start shared HTTPS proxy:",M),I()}}else for(let Y of G)try{let W=Y.to||"rpx.localhost";N("proxy",`Starting proxy for ${W} with SSL config: ${!!Z}`,Y.verbose),await O_({from:Y.from||"localhost:5173",to:W,cleanUrls:Y.cleanUrls||!1,https:Y.https||!1,cleanup:Y.cleanup||!1,vitePluginUsage:Y.vitePluginUsage||!1,verbose:Y.verbose||!1,_cachedSSLConfig:Z,changeOrigin:Y.changeOrigin||!1})}catch(W){N("proxies",`Failed to start proxy for ${Y.to}: ${W}`,Y.verbose),console.error(`Failed to start proxy for ${Y.to}:`,W),I()}}function d_(D){if(D?.vitePluginUsage||!D?.verbose)return;if(console.log(""),console.log(` ${x.green(x.bold("rpx"))} ${x.green(`v${ND}`)}`),console.log(` ${x.green("➜")} ${x.dim(D?.from??"")} ${x.dim("➜")} ${x.cyan(D?.ssl?`https://${D?.to}`:`http://${D?.to}`)}`),D?.listenPort!==(D?.ssl?443:80))console.log(` ${x.green("➜")} Listening on port ${D?.listenPort}`);if(D?.cleanUrls)console.log(` ${x.green("➜")} Clean URLs enabled`)}var bB=L_;export{J_ as writeEntry,sD as watchRegistry,VD as verifyHttpsChain,wD as trustRootCaForBrowsers,Z2 as tearDownDevelopmentDns,W2 as syncDevelopmentDnsFromRegistry,uD as stripBasePath,Y2 as stopDnsServer,k2 as stopDaemon,O_ as startServer,YD as startProxy,L_ as startProxies,S2 as startDnsServer,j_ as shouldReusePort,J2 as setupResolver,V2 as setupDevelopmentDns,lD as serverNameFromCertFilename,hD as serveStaticFile,y as safeStringify,LD as safeRelativePath,oB as safeDeleteFile,o as runViaDaemon,w2 as runDaemon,G2 as resolverFilePath,B2 as resolverBasenamesForDomains,D2 as resolverBasenameForDomain,H_ as resolveStaticRoute,cD as resolveStaticFile,sB as resolvePathRewrite,A2 as removeResolver,Q2 as removeLegacyTldResolvers,A_ as removeHosts,Q_ as removeEntry,j2 as releaseDaemonLock,aB as redactSensitive,T2 as reconcileStaleDevelopmentDns,C2 as reconcileDevelopmentDnsOnIdle,nD as readEntry,F2 as readDaemonPid,XD as readCertSha256Fingerprint,JD as readCertCommonName,tD as readAll,ID as pruneStaleRootCas,r_ as portManager,vD as pathPrefixMatches,WD as parseSha256HashesFromSecurityListing,GD as normalizeSha256Fingerprint,f_ as normalizePathPrefix,_2 as normalizeDevDomain,T_ as matchesWildcard,dD as matchesAllowedSuffix,y_ as matchHostRoute,bD as matchHostList,mD as matchHost,qD as loadSSLConfig,FD as listCertSha256HashesByCommonName,x_ as isWildcardPattern,gB as isValidRootCA,X_ as isValidId,tB as isSingleProxyOptions,rB as isSingleProxyConfig,ED as isRootCaTrustedForSsl,jD as isRootCaFingerprintInKeychains,v_ as isProcessElevated,O as isPortInUse,iD as isPidAlive,nB as isMultiProxyOptions,iB as isMultiProxyConfig,gD as isLikelyHostname,$2 as isDnsServerRunning,I2 as isDaemonRunning,OD as isCertTrusted,Y_ as httpsConfig,a as getSudoPassword,fD as getSharedDaemonCertPaths,xD as getRootCAPaths,G_ as getRegistryDir,pB as getPrimaryDomain,zD as getMacosTrustKeychains,MD as getMacosLoginKeychainPath,M2 as getDaemonRpxDir,z2 as getDaemonPidPath,S_ as generateCertificate,rD as gcStaleEntries,yD as forceTrustCertificate,a_ as findAvailablePort,dB as extractHostname,lB as execSudoSync,V_ as ensureDaemonRunning,kD as devSslToSniEntries,N2 as devDomainsFromHosts,u_ as deriveIdFromTarget,H2 as defaultDaemonSpawnCommand,R_ as defaultConfig,bB as default,N as debugLog,C_ as createProxyWebSocketHandler,k_ as createProxyFetchHandler,M_ as createOriginGuard,PD as contentTypeFor,X2 as contentLooksLikeRpxResolver,R_ as config,x as colors,UD as clearSslConfigCache,w_ as cleanupCertificates,n as cleanup,v as checkHosts,b as checkExistingCertificates,QD as certIncludesSanHostnames,aD as buildSniTlsConfig,CD as buildRegistryTlsProxyOptions,q_ as buildHostRoutes,m as addHosts,E2 as acquireDaemonLock,HD as SHARED_DEV_HOST_CERT_PATH,TD as RPX_ROOT_CA_COMMON_NAME,R2 as RPX_RESOLVER_MARKER,pD as OnDemandCertManager,AD as MACOS_SYSTEM_KEYCHAIN,ZD as MACOS_CA_TRUST_FLAGS,eD as LEGACY_TLD_RESOLVER_LABELS,g as DefaultPortManager,oD as DNS_STATE_VERSION,K2 as DNS_PORT};
package/dist/logger.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export declare const log: {
2
- info: (...args: any[]) => void
3
- success: (...args: any[]) => void
4
- warn: (...args: any[]) => void
5
- error: (...args: any[]) => void
6
- debug: (...args: any[]) => void
7
- log: (...args: any[]) => void
8
- start: (...args: any[]) => void
9
- box: (...args: any[]) => void
2
+ info: (...args: unknown[]) => void
3
+ success: (...args: unknown[]) => void
4
+ warn: (...args: unknown[]) => void
5
+ error: (...args: unknown[]) => void
6
+ debug: (...args: unknown[]) => void
7
+ log: (...args: unknown[]) => void
8
+ start: (...args: unknown[]) => void
9
+ box: (...args: unknown[]) => void
10
10
  };
@@ -33,7 +33,7 @@ export declare interface ProxyRoute {
33
33
  }
34
34
  /** Minimal shape of the Bun server needed for WebSocket upgrades. */
35
35
  export declare interface ProxyServer {
36
- upgrade: (req: Request, options?: { data?: any, headers?: any }) => boolean
36
+ upgrade: (req: Request, options?: { data?: unknown, headers?: Bun.HeadersInit }) => boolean
37
37
  }
38
38
  /*` → app, `/docs*` → static dir). Callers
39
39
  * that only route by host can ignore the second argument — it's optional so
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Forward a request through the pooled transport and return the upstream
3
+ * {@link Response}. Throws {@link FALLBACK} for cases it intentionally declines
4
+ * (large/streaming uploads, `Expect`, upgrades) so the caller can use `fetch()`.
5
+ */
6
+ export declare function proxyViaPool(reqOpts: PoolRequest): Promise<Response>;
7
+ /** Sentinel thrown when the pooled path declines a request; caller uses fetch(). */
8
+ export declare const FALLBACK: unique symbol;
9
+ /** Thrown when the upstream stalls past the configured timeout; caller maps to 504. */
10
+ export declare const TIMEOUT: unique symbol;
11
+ /**
12
+ * Thrown when every connection to an upstream is busy and the wait for a free
13
+ * slot exceeded {@link queueWaitMs}, or the waiter queue is already at its cap.
14
+ * The caller maps it to a 503. This is the backstop that keeps a saturated or
15
+ * stalled upstream from making the *listener* appear wedged: instead of parking
16
+ * a request forever with no response (the production incident), rpx fails it
17
+ * fast and loud so the listener keeps answering every other request.
18
+ */
19
+ export declare const POOL_BUSY: unique symbol;
20
+ export declare interface PoolRequest {
21
+ hostPort: string
22
+ method: string
23
+ path: string
24
+ reqHeaders: Headers
25
+ forwardedHost: string
26
+ originOverride?: string
27
+ body: ReadableStream<Uint8Array> | null
28
+ maxPerHost?: number
29
+ }
30
+ /**
31
+ * One pooled upstream socket. A connection serves a single request at a time
32
+ * (HTTP/1.1, no pipelining); it is checked out of the pool for the duration of a
33
+ * request and returned once the response body is fully read.
34
+ */
35
+ declare class Conn {
36
+ socket: import('bun').Socket<undefined> | null;
37
+ buf: Uint8Array;
38
+ len: unknown;
39
+ fresh: boolean;
40
+ timedOut: boolean;
41
+ idleSince: number;
42
+ bodyQueue: Uint8Array[] | null;
43
+ bodyRemaining: number;
44
+ drainWaiter: (() => void) | null;
45
+ writeAll(bytes: Uint8Array): Promise<void>;
46
+ wakeDrain(): void;
47
+ push(chunk: Uint8Array): void;
48
+ markClosed(): void;
49
+ markTimedOut(): void;
50
+ waitForData(seen: number): Promise<void>;
51
+ waitForBody(): Promise<void>;
52
+ compact(): void;
53
+ destroy(): void;
54
+ }
55
+ /**
56
+ * A bounded keepalive connection pool for a single upstream `host:port`.
57
+ *
58
+ * It caps the *total* number of open connections at `maxTotal` and **queues**
59
+ * requests that arrive while every connection is busy, handing each released
60
+ * connection straight to the next waiter. Connections are only ever closed on
61
+ * error or after sitting idle past the timeout — never on the hot release path.
62
+ *
63
+ * Bounding the total (rather than just the idle set) is what makes it
64
+ * collapse-safe: a fixed set of connections is reused indefinitely, so there is
65
+ * no per-request churn to pile sockets into TIME_WAIT and exhaust ephemeral
66
+ * ports under a flood. The price is that at concurrency above `maxTotal`,
67
+ * throughput is bounded by the pool size instead of growing unbounded — exactly
68
+ * the trade nginx makes, and the reason it stays up under load.
69
+ */
70
+ declare class UpstreamPool {
71
+ constructor(host: string, port: number, maxTotal: number);
72
+ dial(): Promise<Conn>;
73
+ acquireIdleSync(): Conn | null;
74
+ acquireOrDial(): Promise<Conn>;
75
+ release(conn: Conn): void;
76
+ destroy(conn: Conn): void;
77
+ }
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OriginGuardOptions } from './origin-guard';
1
2
  import type { TlsConfig, TlsOption } from '@stacksjs/tlsx';
2
3
  export type { TlsConfig, TlsOption };
3
4
  export declare interface StartOptions {
@@ -92,6 +93,7 @@ export declare interface SharedProxyConfig {
92
93
  hostsManagement?: boolean
93
94
  productionCerts?: ProductionTlsConfig
94
95
  onDemandTls?: OnDemandTlsConfig
96
+ originGuard?: OriginGuardOptions
95
97
  }
96
98
  export declare interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
97
99
  export declare interface MultiProxyConfig extends SharedProxyConfig {
@@ -129,3 +131,13 @@ export type ProxyConfigs = SingleProxyConfig | MultiProxyConfig;
129
131
  export type BaseProxyOption = Partial<BaseProxyConfig>;
130
132
  export type ProxyOption = Partial<SingleProxyConfig>;
131
133
  export type ProxyOptions = Partial<SingleProxyConfig> | Partial<MultiProxyConfig>;
134
+ /**
135
+ * Internal shape used by `startProxies` after merging the built-in defaults with
136
+ * the caller's single- or multi-proxy options. Every field is optional, and the
137
+ * `proxies` array elements tolerate the per-proxy `cleanUrls`/`changeOrigin`
138
+ * overrides the runtime reads — so the merged object can be accessed across both
139
+ * single and multi shapes without falling back to `any`.
140
+ */
141
+ export type ResolvedProxyOptions = Partial<SingleProxyConfig> & {
142
+ proxies?: Array<BaseProxyConfig & { cleanUrls?: boolean, changeOrigin?: boolean, pathRewrites?: PathRewrite[] }>
143
+ }
package/dist/utils.d.ts CHANGED
@@ -3,11 +3,26 @@ import type { MultiProxyConfig, PathRewrite, ProxyConfigs, ProxyOption, ProxyOpt
3
3
  * Get sudo password from environment variable if set
4
4
  */
5
5
  export declare function getSudoPassword(): string | undefined;
6
+ /** True when the process can write system paths without `sudo` (e.g. rpx daemon started via sudo). */
7
+ export declare function isProcessElevated(): boolean;
6
8
  /**
7
9
  * Execute a command with sudo, using SUDO_PASSWORD if available
8
10
  */
9
11
  export declare function execSudoSync(command: string): string;
10
12
  export declare function debugLog(category: string, message: string, verbose?: boolean): void;
13
+ /**
14
+ * Whether the proxy's listeners should set `reusePort`. Off by default: rpx is a
15
+ * single-instance local-dev proxy, and one instance gains nothing from
16
+ * `SO_REUSEPORT` while losing the helpful "port 443 already in use" guard.
17
+ *
18
+ * Opt in with `RPX_REUSE_PORT=1` when running **multiple** rpx instances behind a
19
+ * process manager (systemd/pm2/k8s) on **Linux**, where the kernel load-balances
20
+ * accepted connections across them for multi-core scaling. It is a no-op on
21
+ * macOS/BSD, whose `SO_REUSEPORT` does not load-balance across processes — so
22
+ * rpx never spawns a worker cluster itself; scaling is left to the OS + your
23
+ * supervisor, which is where it belongs for a dev proxy.
24
+ */
25
+ export declare function shouldReusePort(): boolean;
11
26
  export declare function redactSensitive(value: unknown): unknown;
12
27
  export declare function safeStringify(value: unknown, space?: number): string;
13
28
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/rpx",
3
3
  "type": "module",
4
- "version": "0.11.17",
4
+ "version": "0.11.19",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -51,6 +51,10 @@
51
51
  "compile:windows-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-windows-x64 --outfile bin/rpx-windows-x64.exe",
52
52
  "compile:darwin-x64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-x64 --outfile bin/rpx-darwin-x64",
53
53
  "compile:darwin-arm64": "bun build ./bin/cli.ts --compile --minify --target=bun-darwin-arm64 --outfile bin/rpx-darwin-arm64",
54
+ "bench": "bun run bench/run.ts",
55
+ "bench:html": "bun run bench/run.ts --html",
56
+ "bench:latency": "bun run bench/run.ts --latency",
57
+ "bench:throughput": "bun run bench/run.ts --throughput",
54
58
  "lint": "bunx --bun pickier .",
55
59
  "lint:fix": "bunx --bun pickier . --fix",
56
60
  "fresh": "bunx rimraf node_modules/ bun.lock && bun i",
@@ -73,6 +77,7 @@
73
77
  },
74
78
  "devDependencies": {
75
79
  "bunfig": "^0.15.6",
80
+ "mitata": "^1.0.34",
76
81
  "typescript": "^5.9.3"
77
82
  },
78
83
  "simple-git-hooks": {