@stacksjs/rpx 0.11.19 → 0.11.21

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.
Files changed (49) hide show
  1. package/dist/acme-challenge.d.ts +13 -0
  2. package/dist/auth.d.ts +21 -0
  3. package/dist/bin/cli.js +252 -200
  4. package/dist/cert-inspect.d.ts +5 -3
  5. package/dist/{chunk-83dqq28c.js → chunk-1108y1dk.js} +1 -1
  6. package/dist/{chunk-0f32jmrb.js → chunk-c9brkawa.js} +1 -1
  7. package/dist/{chunk-w888yhnp.js → chunk-cqkz06bg.js} +1 -1
  8. package/dist/chunk-qs36t2rf.js +224 -0
  9. package/dist/daemon.d.ts +4 -1
  10. package/dist/https.d.ts +1 -0
  11. package/dist/index.d.ts +33 -1
  12. package/dist/index.js +7 -7
  13. package/dist/proxy-handler.d.ts +18 -1
  14. package/dist/proxy-pool.d.ts +5 -0
  15. package/dist/redirect.d.ts +40 -0
  16. package/dist/registry.d.ts +2 -1
  17. package/dist/site-resolver.d.ts +106 -0
  18. package/dist/site-splash.d.ts +19 -0
  19. package/dist/site-supervisor.d.ts +56 -0
  20. package/dist/start.d.ts +31 -8
  21. package/dist/types.d.ts +64 -0
  22. package/package.json +6 -7
  23. package/dist/chunk-rs8gqpax.js +0 -174
  24. package/src/cert-inspect.ts +0 -69
  25. package/src/colors.ts +0 -13
  26. package/src/config.ts +0 -45
  27. package/src/daemon-runner.ts +0 -180
  28. package/src/daemon.ts +0 -1209
  29. package/src/dns-state.ts +0 -116
  30. package/src/dns.ts +0 -568
  31. package/src/host-match.ts +0 -52
  32. package/src/host-routes.ts +0 -147
  33. package/src/hosts.ts +0 -283
  34. package/src/https.ts +0 -905
  35. package/src/index.ts +0 -161
  36. package/src/logger.ts +0 -19
  37. package/src/macos-trust.ts +0 -175
  38. package/src/on-demand.ts +0 -264
  39. package/src/origin-guard.ts +0 -127
  40. package/src/port-manager.ts +0 -183
  41. package/src/process-manager.ts +0 -164
  42. package/src/proxy-handler.ts +0 -387
  43. package/src/proxy-pool.ts +0 -1003
  44. package/src/registry.ts +0 -366
  45. package/src/sni.ts +0 -93
  46. package/src/start.ts +0 -1421
  47. package/src/static-files.ts +0 -201
  48. package/src/types.ts +0 -267
  49. package/src/utils.ts +0 -243
package/dist/daemon.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { OnDemandTlsConfig, ProductionTlsConfig, TlsOption } from './types';
1
+ import type { OnDemandSitesConfig, OnDemandTlsConfig, ProductionTlsConfig, TlsOption } from './types';
2
+ import type { SiteSnapshot } from './site-supervisor';
2
3
  export declare function getDaemonRpxDir(): string;
3
4
  export declare function getDaemonPidPath(rpxDir?: string): string;
4
5
  /**
@@ -71,6 +72,7 @@ export declare interface DaemonOptions {
71
72
  https?: TlsOption
72
73
  productionCerts?: ProductionTlsConfig
73
74
  onDemandTls?: OnDemandTlsConfig
75
+ onDemandSites?: OnDemandSitesConfig
74
76
  gcIntervalMs?: number
75
77
  workers?: number
76
78
  }
@@ -81,6 +83,7 @@ export declare interface DaemonHandle {
81
83
  httpPort: number
82
84
  pidPath: string
83
85
  ensureCert: (host: string) => Promise<boolean>
86
+ listSites: () => SiteSnapshot[]
84
87
  }
85
88
  // ───────────────────────── cluster: coordinator + workers ─────────────────────
86
89
  declare interface WorkerCtx {
package/dist/https.d.ts CHANGED
@@ -1,5 +1,6 @@
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
+ import { readCertCommonName, readCertSha256Fingerprint } from './cert-inspect';
3
4
  import type { ProxyConfigs, ProxyOption, ProxyOptions, SSLConfig, TlsConfig } from './types';
4
5
  /**
5
6
  * Bun needs one `tls[]` entry per SNI name even when a single PEM covers every SAN.
package/dist/index.d.ts CHANGED
@@ -9,12 +9,30 @@ export type {
9
9
  StopDaemonOptions,
10
10
  StopDaemonResult,
11
11
  } from './daemon';
12
- export type { GetRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './proxy-handler';
12
+ export type { GetRoute, NoRouteOutcome, OnNoRoute, ProxyFetchHandler, ProxyRoute, ProxyServer } from './proxy-handler';
13
+ export type { RedirectRouteConfig, ResolvedRedirect } from './redirect';
13
14
  export type { OriginGuard, OriginGuardOptions } from './origin-guard';
15
+ export type { ResolvedAuth } from './auth';
14
16
  export type { HostRoutes, PathRoute } from './host-routes';
15
17
  export type { ResolvedStaticRoute, StaticResolution } from './static-files';
16
18
  export type { SniTlsEntry } from './sni';
17
19
  export type { CertIssuer, OnDemandCertManagerOptions } from './on-demand';
20
+ export type {
21
+ ResolvedSite,
22
+ ResolverProbes,
23
+ SiteDetector,
24
+ SiteManifest,
25
+ SitePreset,
26
+ SiteResolver,
27
+ SiteResolverDeps,
28
+ } from './site-resolver';
29
+ export type {
30
+ SiteLauncher,
31
+ SiteProcessHandle,
32
+ SiteRequestStatus,
33
+ SiteSnapshot,
34
+ SiteSupervisorOptions,
35
+ } from './site-supervisor';
18
36
  export type { DaemonRunnerOptions, DaemonRunnerProxy } from './daemon-runner';
19
37
  export { colors } from './colors';
20
38
  export { config, config as defaultConfig } from './config';
@@ -108,8 +126,11 @@ export {
108
126
  stopDaemon,
109
127
  } from './daemon';
110
128
  export { createProxyFetchHandler, createProxyWebSocketHandler, stripBasePath } from './proxy-handler';
129
+ export { buildRedirectLocation, resolveRedirect } from './redirect';
130
+ export { ACME_CHALLENGE_PREFIX, readAcmeChallenge } from './acme-challenge';
111
131
  export { isWildcardPattern, matchesWildcard, matchHost } from './host-match';
112
132
  export { createOriginGuard } from './origin-guard';
133
+ export { enforceBasicAuth, parseHtpasswd, resolveAuth } from './auth';
113
134
  export {
114
135
  buildHostRoutes,
115
136
  matchHostList,
@@ -126,6 +147,17 @@ export {
126
147
  } from './static-files';
127
148
  export { buildSniTlsConfig, serverNameFromCertFilename } from './sni';
128
149
  export { isLikelyHostname, matchesAllowedSuffix, OnDemandCertManager } from './on-demand';
150
+ export {
151
+ createSiteResolver,
152
+ detectProjectPreset,
153
+ expandHome,
154
+ listDiscoverableSites,
155
+ projectNameFromHost,
156
+ readSiteManifest,
157
+ siteIdForHost,
158
+ } from './site-resolver';
159
+ export { SiteSupervisor } from './site-supervisor';
160
+ export { escapeHtml, renderFailedPage, renderStartingPage } from './site-splash';
129
161
  export { deriveIdFromTarget, runViaDaemon } from './daemon-runner';
130
162
  export { cleanup } from './start';
131
163
  export { startProxies, startProxy, startServer } from './start';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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)=>`
1
+ import{$ as qR,$a as vD,A as US,Aa as QD,B as U,Ba as VD,C as o,Ca as XD,D as FR,Da as ZD,E as PS,Ea as WD,F as OS,Fa as zD,G as RR,Ga as ID,H as LS,Ha as FD,I as MR,Ia as MD,J as cS,Ja as xD,K as xR,Ka as TD,L as mS,La as jD,M as vS,N as hS,Na as HD,O as uS,Oa as kD,P as bS,Pa as wD,Q as TR,Qa as ED,R as jR,Ra as qD,S as HR,Sa as CD,T as GR,Ta as yD,U as lS,Ua as fD,V as kR,Va as UD,W as aS,Wa as PD,X as wR,Xa as OD,Y as pS,Ya as LD,Z as ER,Za as cD,_ as dS,_a as mD,a as G,aa as gS,ab as hD,b as s,ba as iS,bb as uD,c as $S,ca as nS,cb as KR,d as GS,da as rS,db as bD,e as JS,ea as tS,eb as lD,f as QS,fa as sS,g as VS,ga as eS,h as XS,ha as oS,i as ZS,ia as RD,j as WS,ja as SD,k as zS,ka as C,l as IS,la as DD,m as FS,ma as CR,n as MS,na as _D,o as xS,oa as DR,p as TS,pa as _R,q as jS,qa as ND,r as HS,ra as NR,s as kS,sa as BR,t as wS,ta as BD,u as ES,ua as KD,v as qS,va as AD,w as CS,wa as YD,x as yS,xa as $D,y as fS,ya as GD,z as e,za as JD}from"./chunk-qs36t2rf.js";import{fb as zR,gb as m,hb as mR,ib as QN,jb as _,kb as lR,lb as VN,mb as E,nb as XN,ob as ZN,pb as WN,qb as zN,rb as IN,sb as FN,tb as MN,ub as xN,vb as TN}from"./chunk-cqkz06bg.js";import{execSync as SS}from"node:child_process";import*as SR from"node:http";import*as aR from"node:net";import*as z from"node:process";var l=(S,R)=>(D)=>`\x1B[${S}m${D}\x1B[${R}m`,H={bold:l(1,22),dim:l(2,22),green:l(32,39),cyan:l(36,39)};import*as PR from"node:fs";import*as OR from"node:path";import*as k from"node:process";function LR(S,R){let B=(R&&R!=="/"?`${S}${R}`:S).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return B.length>0?B:"rpx"}async function a(S){if(S.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let R=S.verbose??!1,D=S.registryDir,B=new Set,K=S.proxies.map((X)=>{let W=X.id??LR(X.to,X.path);if(!_R(W))throw Error(`invalid registry id "${W}" derived from to="${X.to}"`);if(B.has(W))throw Error(`duplicate registry id "${W}" — set an explicit \`id\` on one of the proxies`);return B.add(W),{...X,id:W}}),N=new Date().toISOString();for(let X of K)await NR({id:X.id,from:X.from,to:X.to,path:X.path,pid:S.persistent?void 0:k.pid,cwd:k.cwd(),createdAt:N,cleanUrls:X.cleanUrls,changeOrigin:X.changeOrigin,pathRewrites:X.pathRewrites,static:X.static},D,R);let A=await KR({rpxDir:S.rpxDir,verbose:R,spawnCommand:S.spawnCommand,startupTimeoutMs:S.startupTimeoutMs,spawnEnv:S.spawnEnv});for(let X of K){let W=X.static?`static ${typeof X.static==="string"?X.static:X.static.dir}`:X.from;G.success(`https://${X.to} → ${W}`)}if(G.info(`(via rpx daemon pid=${A.pid}; \`rpx daemon:status\` to inspect)`),S.detached)return;let V=!1,Y=D??DR(),J=K.map((X)=>X.id),Q=async()=>{if(V)return;V=!0;for(let X of J)await BR(X,D,R).catch((W)=>{_("runner",`removeEntry(${X}) failed: ${W}`,R)})},Z=(X)=>{_("runner",`received ${X}, unregistering ${J.length} entries`,R),Q().finally(()=>k.exit(0))};k.once("SIGINT",Z),k.once("SIGTERM",Z),k.once("exit",()=>{if(V)return;for(let X of J)try{PR.unlinkSync(OR.join(Y,`${X}.json`))}catch{}}),await new Promise(()=>{})}import{exec as nR}from"node:child_process";import q from"node:fs";import vR from"node:os";import YR from"node:path";import*as v from"node:process";import{promisify as rR}from"node:util";var c=rR(nR);function cR(S){let R=S.trim().toLowerCase();return R==="localhost"||R.endsWith(".localhost")||R.endsWith(".localhost.")}var j=v.platform==="win32"?YR.join(v.env.windir||"C:\\Windows","System32","drivers","etc","hosts"):"/etc/hosts",AR=!1;async function p(S){if(v.platform==="win32")throw Error("Administrator privileges required on Windows");if(mR()){let{stdout:B}=await c(S);return B}let R=m(),D=S.replace(/'/g,"'\\''");try{if(R){let{stdout:B}=await c(`echo '${R}' | sudo -S sh -c '${D}' 2>/dev/null`);return AR=!0,B}if(AR)try{let{stdout:B}=await c(`sudo -n sh -c '${D}'`);return B}catch(B){_("hosts","Cached sudo privileges expired, requesting again",!0)}try{let{stdout:B}=await c(`sudo -n sh -c '${D}'`);return AR=!0,B}catch{throw Error("sudo required but no cached credentials (set SUDO_PASSWORD in .env or run sudo -v)")}}catch(B){throw Error(`Failed to execute sudo command: ${B.message}`)}}async function y(S,R){let D=S.filter((K)=>!cR(K)),B=S.filter((K)=>cR(K));if(B.length>0)_("hosts",`Skipping /etc/hosts for loopback dev names: ${B.join(", ")}`,R);if(D.length===0)return;_("hosts",`Adding hosts: ${D.join(", ")}`,R),_("hosts",`Using hosts file at: ${j}`,R);try{let K;try{K=await q.promises.readFile(j,"utf-8")}catch{_("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{K=await p(`cat "${j}"`)}catch(Y){throw console.log(" Could not read hosts file — skipping hosts setup"),_("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let N=D.filter((Y)=>{let J=`127.0.0.1 ${Y}`,Q=`::1 ${Y}`;return!K.includes(J)&&!K.includes(Q)});if(N.length===0){_("hosts","All hosts already exist in hosts file",R);return}let A=N.map((Y)=>`
2
2
  # Added by rpx
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(`
3
+ 127.0.0.1 ${Y}
4
+ ::1 ${Y}`).join(`
5
+ `),V=YR.join(vR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await q.promises.writeFile(V,K+A,"utf8"),await p(`cat "${V}" | tee "${j}" > /dev/null`),console.log(` Hosts updated: ${N.join(", ")}`)}catch(Y){console.log(" Could not update hosts file automatically"),console.log(" Add these entries to /etc/hosts:"),N.forEach((J)=>{console.log(` 127.0.0.1 ${J}`),console.log(` ::1 ${J}`)}),console.log(` Or run: sudo nano ${j}`)}finally{try{await q.promises.unlink(V)}catch{}}}catch(K){_("hosts",`Failed to manage hosts file: ${K.message}`,R)}}async function $R(S,R){_("hosts",`Removing hosts: ${S.join(", ")}`,R);try{let D;try{D=await q.promises.readFile(j,"utf-8")}catch{_("hosts","Reading hosts file requires elevated permissions, using sudo",R);try{D=await p(`cat "${j}"`)}catch(Y){throw _("hosts",`sudo read also failed: ${Y}`,R),Error(`Cannot read hosts file: ${Y}`)}}let B=D.split(`
6
+ `),K=!1,N=B.filter((Y)=>{if(S.some((Q)=>Y.includes(` ${Q}`)&&(Y.includes("127.0.0.1")||Y.includes("::1"))))return K=!0,!1;if(Y.trim()==="# Added by rpx")return K=!0,!1;return!0});if(!K){_("hosts","No matching hosts found to remove",R);return}while(N[N.length-1]?.trim()==="")N.pop();let A=`${N.join(`
7
7
  `)}
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};
8
+ `,V=YR.join(vR.tmpdir(),`rpx-hosts-${Date.now()}.tmp`);try{await q.promises.writeFile(V,A,"utf8"),await p(`cat "${V}" | tee "${j}" > /dev/null`),_("hosts","Hosts removed successfully",R)}catch(Y){_("hosts","Could not clean up hosts file automatically",R)}finally{try{await q.promises.unlink(V)}catch(Y){_("hosts",`Failed to remove temporary file: ${Y}`,R)}}}catch(D){_("hosts",`Failed to clean up hosts file: ${D.message}`,R)}}async function f(S,R){_("hosts",`Checking hosts: ${S}`,R);let D;try{D=await q.promises.readFile(j,"utf-8")}catch(B){_("hosts",`Error reading hosts file: ${B}`,R);try{let K=m(),N;if(K)N=`echo '${K}' | sudo -S cat "${j}" 2>/dev/null`;else N=`sudo -n cat "${j}" 2>/dev/null || cat "${j}" 2>/dev/null || echo ""`;let{stdout:A}=await c(N);D=A}catch(K){return _("hosts",`Cannot read hosts file, assuming entries don't exist: ${K}`,R),S.map(()=>!1)}}return S.map((B)=>{let K=`127.0.0.1 ${B}`,N=`::1 ${B}`;return D.includes(K)||D.includes(N)})}import{spawn as tR}from"node:child_process";import*as d from"node:process";class g{processes=new Map;isShuttingDown=!1;async startProcess(S,R,D){if(this.processes.has(S)){_("start",`Process ${S} is already running`,D);return}let[B,...K]=R.command.split(" "),N=R.cwd||d.cwd();_("start",`Starting process ${S}:`,D),_("start",` Command: ${B} ${K.join(" ")}`,D),_("start",` Working directory: ${N}`,D),_("start",` Environment variables: ${E(R.env)}`,D);let A=tR(B,K,{cwd:N,env:{...d.env,...R.env},shell:!0,stdio:"inherit"});return this.processes.set(S,{command:R.command,cwd:N,process:A,env:R.env}),new Promise((V,Y)=>{if(A.on("error",(J)=>{if(!this.isShuttingDown)_("start",`Process ${S} failed to start: ${J}`,D),this.processes.delete(S),Y(J)}),A.on("exit",(J)=>{if(!this.isShuttingDown&&J!==null&&J!==0)_("start",`Process ${S} exited with code ${J}; leaving the proxy running`,D),this.processes.delete(S),Y(Error(`Process ${S} exited with code ${J}`))}),D)A.stdout?.on("data",(J)=>{_("process",`[${S}] ${J.toString().trim()}`,!0)}),A.stderr?.on("data",(J)=>{_("process",`[${S}] ERR: ${J.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&A.killed)this.processes.delete(S),Y(Error(`Process ${S} was killed during startup`));else _("start",`Process ${S} started successfully`,D),V()},1000)})}async stopProcess(S,R){let D=this.processes.get(S);if(!D?.process){_("start",`No process found for ${S}`,R);return}return _("start",`Stopping process ${S}`,R),new Promise((B)=>{if(!D.process){B();return}D.process.once("exit",()=>{this.processes.delete(S),_("start",`Process ${S} stopped`,R),B()});try{D.process.kill("SIGTERM"),setTimeout(()=>{if(D.process){_("start",`Force killing process ${S}`,R);try{D.process.kill("SIGKILL")}catch(K){}}},3000)}catch(K){_("start",`Error stopping process ${S}: ${K}`,R),this.processes.delete(S),B()}})}async stopAll(S){if(this.isShuttingDown){_("start","Already shutting down, skipping duplicate stopAll call",S);return}this.isShuttingDown=!0,_("start","Stopping all processes",S);let R=Array.from(this.processes.keys()).map((D)=>this.stopProcess(D,S).catch((B)=>{G.error(`Failed to stop process ${D}:`,B)}));await Promise.allSettled(R),this.processes.clear(),this.isShuttingDown=!1}isRunning(S){let R=this.processes.get(S);return!!R?.process&&!R.process.killed}}var B_=new g;import{timingSafeEqual as sR}from"node:crypto";function eR(S,R){if(S==null)return!1;let D=Buffer.from(S),B=Buffer.from(R);if(D.length!==B.length)return!1;return sR(D,B)}function i(S){return S.toLowerCase().replace(/\.$/,"")}var oR=["/.well-known/acme-challenge/"];function RS(S){let R=S.headers.get("host");if(R)return i(R.split(":")[0]);try{return i(new URL(S.url).hostname)}catch{return""}}function JR(S){let R=S.header.toLowerCase(),D=new Set,B=[];for(let Y of S.hosts){let J=i(Y);if(J.startsWith("*."))B.push(J);else D.add(J)}let K=S.exemptPaths??oR,N=S.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
9
+ `,A=(Y)=>{let J=i(Y);return D.has(J)||B.some((Q)=>GR(J,Q))},V=(Y)=>{let J=RS(Y);if(!A(J))return;let Q="/";try{Q=new URL(Y.url).pathname}catch{}if(K.some((Z)=>Q.startsWith(Z)))return;if(eR(Y.headers.get(R),S.value))return;return new Response(N,{status:403,headers:{"content-type":"text/plain"}})};return V.protects=A,V}import*as hR from"node:fs";import*as uR from"node:path";var QR="/.well-known/acme-challenge/";function VR(S,R){if(!S||!R.startsWith(QR))return null;let D=R.slice(QR.length);if(!D||!/^[A-Za-z0-9_-]+$/.test(D))return null;try{return hR.readFileSync(uR.join(S,D),"utf8")}catch{return null}}var r=new g,DS="0.12.0",_S=new CR("0.0.0.0"),t=new Set,XR=!1,n=null,ZR=null;async function u(S){if(XR)return _("cleanup","Cleanup already in progress, skipping",S?.verbose),ZR||Promise.resolve();XR=!0,_("cleanup","Starting cleanup process",S?.verbose),ZR=new Promise((R)=>{n=R});try{await r.stopAll(S?.verbose),G.info("Shutting down proxy servers...");let R=[],D=Array.from(t).map((B)=>new Promise((K)=>{let N=B;try{if(typeof N.stop==="function")N.stop(!0),_("cleanup","Bun server stopped",S?.verbose),K();else if(typeof N.close==="function")N.close(()=>{_("cleanup","Server closed successfully",S?.verbose),K()});else K()}catch(A){_("cleanup",`Error stopping server: ${A}`,S?.verbose),K()}}));if(R.push(...D),t.clear(),S?.hosts&&S.domains?.length){_("cleanup","Cleaning up hosts file entries",S?.verbose),_("cleanup",`Original domains for cleanup: ${JSON.stringify(S.domains)}`,S?.verbose);let B=S.domains.filter((K)=>{if(K==="test.local")return!0;return K!=="localhost"&&!K.startsWith("localhost.")&&K!=="127.0.0.1"});if(_("cleanup",`Filtered domains for cleanup: ${JSON.stringify(B)}`,S?.verbose),B.length>0)G.info("Cleaning up hosts file entries..."),R.push($R(B,S?.verbose).then(()=>{_("cleanup",`Removed hosts entries for ${B.join(", ")}`,S?.verbose)}).catch((K)=>{_("cleanup",`Failed to remove hosts entries: ${K}`,S?.verbose),G.warn(`Failed to clean up hosts file entries for ${B.join(", ")}:`,K)}))}if(S?.certs&&S.domains?.length){_("cleanup","Cleaning up SSL certificates",S?.verbose),G.info("Cleaning up SSL certificates...");let B=S.domains.map(async(K)=>{try{await FR(K,S?.verbose),_("cleanup",`Removed certificates for ${K}`,S?.verbose)}catch(N){_("cleanup",`Failed to remove certificates for ${K}: ${N}`,S?.verbose),G.warn(`Failed to clean up certificates for ${K}:`,N)}});R.push(...B)}await Promise.allSettled(R),_("cleanup","All cleanup tasks completed successfully",S?.verbose),G.success("All cleanup tasks completed successfully")}catch(R){_("cleanup",`Error during cleanup: ${R}`,S?.verbose),G.error("Error during cleanup:",R)}finally{if(n)n();n=null,XR=!1;let R=S&&"vitePluginUsage"in S&&S.vitePluginUsage===!0;if(z.env.NODE_ENV!=="test"&&z.env.BUN_ENV!=="test"&&!R)z.exit(0)}return ZR}var WR=!1;function pR(S){if(WR){_("signal",`Received second ${S} signal, forcing exit`,!0),z.exit(1);return}WR=!0,_("signal",`Received ${S} signal, initiating cleanup`,!0),u().catch((R)=>{_("signal",`Cleanup failed after ${S}: ${R}`,!0),z.exit(1)}).finally(()=>{WR=!1})}z.once("SIGINT",()=>pR("SIGINT"));z.once("SIGTERM",()=>pR("SIGTERM"));z.on("uncaughtException",(S)=>{G.error("Uncaught exception (continuing):",S)});z.on("unhandledRejection",(S)=>{G.error("Unhandled rejection (continuing):",S)});async function h(S,R,D,B=5){_("connection",`Testing connection to ${S}:${R} (retries left: ${B})`,D);let K=15000,N=Date.now();if(z.env.RPX_BYPASS_CONNECTION_TEST==="true"){_("connection",`Bypassing connection test for ${S}:${R} due to RPX_BYPASS_CONNECTION_TEST flag`,D);return}let A=()=>new Promise((V,Y)=>{let J=aR.connect({host:S,port:R,timeout:3000});J.once("connect",()=>{_("connection",`Successfully connected to ${S}:${R}`,D),J.end(),V()}),J.once("timeout",()=>{_("connection",`Connection to ${S}:${R} timed out`,D),J.destroy(),Y(Error("Connection timed out"))}),J.once("error",(Q)=>{_("connection",`Failed to connect to ${S}:${R}: ${Q}`,D),J.destroy(),Y(Q)})});try{await A()}catch(V){let Y=V;if(Date.now()-N>K){_("connection",`Connection test timed out after ${K}ms, but continuing anyway`,D),G.warn(`Connection test to ${S}:${R} timed out, but RPX will try to proceed anyway.`);return}if(Y.code==="ECONNREFUSED"&&B>0)return _("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${B} retries left)`,D),await new Promise((Q)=>setTimeout(Q,2000)),h(S,R,D,B-1);if(B>0)try{_("connection",`Trying HTTP request to ${S}:${R}`,D),await new Promise((Q,Z)=>{let X=SR.request({hostname:S,port:R,path:"/",method:"HEAD",timeout:5000},(W)=>{_("connection",`Received HTTP response with status: ${W.statusCode}`,D),Q()});X.on("error",(W)=>Z(W)),X.on("timeout",()=>{X.destroy(),Z(Error("HTTP request timed out"))}),X.end()}),_("connection",`HTTP request to ${S}:${R} succeeded`,D);return}catch(Q){return _("connection",`HTTP request to ${S}:${R} failed: ${Q}`,D),_("connection",`Retrying socket connection in 2 seconds... (${B} retries left)`,D),await new Promise((Z)=>setTimeout(Z,2000)),h(S,R,D,B-1)}let J=`Failed to connect to ${S}:${R} after ${5-B} attempts: ${Y.message}`;_("connection",`${J}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,D),G.warn(J),G.warn("RPX will try to continue anyway. If you're sure this is correct, you can set RPX_BYPASS_CONNECTION_TEST=true to skip this check.")}}async function yR(S){_("server",`Starting server with options: ${E(S)}`,S.verbose);let R=new URL((S.from?.startsWith("http")?S.from:`http://${S.from}`)||"localhost:5173"),D=new URL((S.to?.startsWith("http")?S.to:`http://${S.to}`)||"rpx.localhost"),B=Number.parseInt(R.port)||(R.protocol.includes("https:")?443:80),K=[D.hostname];if(fR(S)&&!D.hostname.includes("localhost")&&!D.hostname.includes("127.0.0.1")){_("hosts",`Checking if hosts file entry exists for: ${D.hostname}`,S?.verbose);try{if(!(await f(K,S.verbose))[0]){G.info(`Adding ${D.hostname} to hosts file...`),G.info("This may require sudo/administrator privileges");try{await y(K,S.verbose)}catch(V){if(G.error("Failed to add hosts entry:",V.message),G.warn("You can manually add this entry to your hosts file:"),G.warn(`127.0.0.1 ${D.hostname}`),G.warn(`::1 ${D.hostname}`),z.platform==="win32")G.warn("On Windows:"),G.warn("1. Run notepad as administrator"),G.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else G.warn("On Unix systems:"),G.warn("sudo nano /etc/hosts")}}else _("hosts",`Host entry already exists for ${D.hostname}`,S.verbose)}catch(A){G.error("Failed to check hosts file:",A.message)}}try{await h(R.hostname,B,S.verbose)}catch(A){_("server",`Connection test failed: ${A}`,S.verbose),G.error(A.message),G.warn("Continuing with proxy setup despite connection test failure..."),G.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let N=S._cachedSSLConfig||null;if(S.https)try{if(S.https===!0)S.https=o({...S,to:D.hostname});if(N=await U({...S,to:D.hostname,https:S.https}),!N){if(_("ssl",`Generating new certificates for ${D.hostname}`,S.verbose),await e({...S,from:R.toString(),to:D.hostname,https:S.https}),N=await U({...S,to:D.hostname,https:S.https}),!N)throw Error(`Failed to load SSL configuration after generating certificates for ${D.hostname}`)}}catch(A){throw _("server",`SSL setup failed: ${A}`,S.verbose),A}_("server",`Setting up reverse proxy with SSL config for ${D.hostname}`,S.verbose),await BS({...S,from:S.from||"localhost:5173",to:D.hostname,fromPort:B,sourceUrl:{hostname:R.hostname,host:R.host},ssl:N})}async function NS(S,R,D,B,K,N,A,V,Y,J){_("proxy",`Creating proxy server ${S} -> ${R} with cleanUrls: ${V}`,A);let Q=[{host:R,route:{sourceHost:B.host,cleanUrls:V||!1,changeOrigin:Y||!1,basePath:"/",auth:J}}];if(!IR({routeEntries:Q,listenPort:D,sslConfig:K,originGuard:null,verbose:A??!1}))throw Error(`Failed to start proxy server for ${R} on port ${D}`);YS({from:S,to:R,vitePluginUsage:N,listenPort:D,ssl:!!K,cleanUrls:V,verbose:A})}async function BS(S){_("setup",`Setting up reverse proxy: ${E(S)}`,S.verbose);let{from:R,to:D,sourceUrl:B,ssl:K,verbose:N,cleanup:A,vitePluginUsage:V,changeOrigin:Y,cleanUrls:J}=S,Q=80,Z=443,X="0.0.0.0",W=S.portManager||_S,P=fR(S);try{if(P&&D&&!D.includes("localhost")&&!D.includes("127.0.0.1")){if(!(await f([D],N))[0]){G.warn(`The hostname ${D} isn't in your hosts file. Adding it now...`);try{await y([D],N),G.success(`Added ${D} to your hosts file.`)}catch(L){G.error(`Failed to add ${D} to your hosts file: ${L}`),G.info(`You may need to manually add '127.0.0.1 ${D}' to your /etc/hosts file.`)}}}else if(P&&z.platform!=="darwin"&&D&&D.includes("localhost")&&!D.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await f([D],N))[0]){_("hosts",`${D} not found in hosts file, adding...`,N);try{await y([D],N)}catch(L){_("hosts",`Failed to add ${D} to hosts file: ${L}`,N)}}}if(K&&!W.usedPorts.has(Q)){if(!await C(Q,X,N))_("setup","Starting HTTP redirect server",N),dR(N),W.usedPorts.add(Q);else if(_("setup","Port 80 is in use, skipping HTTP redirect",N),N)G.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let x=K?Z:Q,w=await C(x,X,N),F;if(w){if(_("setup",`Port ${x} is already in use`,N),N)G.warn(`Port ${x} is already in use. This may be another instance of rpx or another service.`);if(x===443){if(F=await W.getNextAvailablePort(3443,!0),_("setup",`Using port ${F} instead of ${x}`,N),N)G.info(`Using port ${F} instead. Access your site at https://${D}:${F}`)}else if(F=await W.getNextAvailablePort(x+1000,!0),_("setup",`Using port ${F} instead of ${x}`,N),N)G.info(`Using port ${F} instead. Access your site at http://${D}:${F}`)}else F=x,W.usedPorts.add(F),_("setup",`Using standard ${x===443?"HTTPS":"HTTP"} port ${x} for ${D}`,N);await NS(R,D,F,B,K,V,N,J,Y,RR(S.auth))}catch(x){_("setup",`Setup failed: ${x}`,N),G.error(`Failed to setup reverse proxy: ${x.message}`),u({domains:[D],hosts:typeof A==="boolean"?A:A?.hosts,certs:typeof A==="boolean"?A:A?.certs,verbose:N,vitePluginUsage:V})}}function dR(S,R=80,D=443,B){_("redirect",`Starting HTTP redirect server on port ${R}`,S);let K=SR.createServer((N,A)=>{if(B&&N.url){let Q=N.url.split("?",1)[0],Z=VR(B,Q);if(Z!=null){_("redirect",`Serving ACME challenge ${Q}`,S),A.writeHead(200,{"content-type":"text/plain"}),A.end(Z);return}}let V=N.headers.host||"",Y=V.includes(":")?V.slice(0,V.indexOf(":")):V,J=D===443?Y:`${Y}:${D}`;_("redirect",`Redirecting request from ${V}${N.url} to https://${J}`,S),A.writeHead(301,{Location:`https://${J}${N.url}`}),A.end()}).listen(R);t.add(K),_("redirect","HTTP redirect server started",S)}function KS(S){let R={...s,...S};if(_("proxy",`Starting proxy with options: ${E(R)}`,R?.verbose),R.viaDaemon){if(!R.from||!R.to){G.error("viaDaemon mode requires both `from` and `to`");return}a({proxies:[{id:R.id,from:R.from,to:R.to,path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}],verbose:R.verbose}).catch((Y)=>{G.error(`Failed to register with rpx daemon: ${Y.message}`),z.exit(1)});return}let D=R.to||"",B=D.split(".").pop()?.toLowerCase()||"",K=z.platform==="darwin"&&D&&!D.includes("localhost")&&!D.includes("127.0.0.1"),N=["dev","app","page","new","day","foo"],A=["test","localhost","local","example","invalid"];if(K&&N.includes(B)&&R?.verbose)G.warn(`The .${B} TLD may not work reliably for local development`),G.info(` Google owns .${B} with HSTS preloading, which can bypass local DNS`),G.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(K)import("./chunk-1108y1dk.js").then(({setupDevelopmentDns:Y})=>{Y({domains:[D],verbose:R.verbose}).then((J)=>{if(J)Promise.resolve().then(()=>{if(R.verbose)if(A.includes(B))G.success(`DNS server started for .${B} domains`);else G.success(`DNS server started for .${B} domains (hosts file entry also added)`)});else _("dns",`Could not start DNS server - ${D} may not resolve in browser`,R.verbose)})}).catch((Y)=>{_("dns",`Failed to start DNS server: ${Y}`,R.verbose)});let V={from:R.from,to:R.to,cleanUrls:R.cleanUrls,https:o(R),cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,changeOrigin:R.changeOrigin,verbose:R.verbose,regenerateUntrustedCerts:R.regenerateUntrustedCerts};_("proxy",`Server options: ${E(V)}`,R.verbose),yR(V).catch((Y)=>{_("proxy",`Failed to start proxy: ${Y}`,R.verbose),G.error(`Failed to start proxy: ${Y.message}`),u({domains:[R.to],hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose})})}function AS(S){return S?.verbose||!1}function fR(S){if(S?.hostsManagement===!1)return!1;let R=S?.cleanup;if(R===!1)return!1;if(R&&typeof R==="object"&&R.hosts===!1)return!1;return!0}async function UR(S){let R={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(S)R={...R,...S};let D=AS(R),B=fR(R);if(_("config",`Starting with config: ${E(R,2)}`,D),_("config",`Is multi-proxy? ${"proxies"in R}`,D),_("config",`Hosts management enabled? ${B}`,D),R.viaDaemon){let I="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map((M)=>({id:M.id,from:M.from,to:M.to,path:M.path,cleanUrls:M.cleanUrls??R.cleanUrls,changeOrigin:M.changeOrigin??R.changeOrigin,pathRewrites:M.pathRewrites})):[{id:R.id,from:R.from,to:R.to??"rpx.localhost",path:R.path,cleanUrls:R.cleanUrls,changeOrigin:R.changeOrigin,pathRewrites:R.pathRewrites}];await a({proxies:I,verbose:D});return}if("proxies"in R&&Array.isArray(R.proxies)){_("servers",`Found ${R.proxies.length} proxies in config`,D);for(let $ of R.proxies)if($.start){let I=`${$.from}-${$.to}`;try{_("watch",`Starting command for ${I} with command: ${$.start.command}`,D),G.info(`Starting command for ${I}...`),await r.startProcess(I,$.start,D);let M=new URL($.from?.startsWith("http")?$.from:`http://${$.from}`),T=M.hostname||"localhost",b=Number(M.port)||80;try{await h(T,b,D),_("watch",`Dev server is ready at ${T}:${b}`,D)}catch(iR){_("watch",`Connection check failed, but continuing with proxy setup: ${iR}`,D),G.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(M){throw _("watch",`Failed to start command for ${I}: ${M}`,D),Error(`Failed to start command for ${I}: ${M}`)}}else _("watch",`No start command for proxy ${$.from} -> ${$.to}`,D)}else if("start"in R&&R.start){_("watch","Found start command in single proxy config",D);let $=`${R.from}-${R.to}`;try{if(R.start)_("watch",`Starting command: ${R.start.command}`,D),await r.startProcess($,R.start,D);let I=new URL(R.from?.startsWith("http")?R.from:`http://${R.from}`),M=I.hostname||"localhost",T=Number(I.port)||80;try{await h(M,T,D),_("watch",`Dev server is ready at ${M}:${T}`,D)}catch(b){_("watch",`Connection check failed, but continuing with proxy setup: ${b}`,D),G.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(I){throw _("watch",`Failed to run start command: ${I}`,D),Error(`Failed to run start command: ${I}`)}}else _("watch","No start command found in config",D);let K="proxies"in R&&Array.isArray(R.proxies)?R.proxies[0]?.to:("to"in R)?R.to:"rpx.localhost";if(z.platform!=="win32"&&(R.https||B)){if(!m())try{_("sudo","Pre-acquiring sudo credentials for privileged operations",D),SS("sudo -v",{stdio:"inherit"})}catch{_("sudo","Could not pre-acquire sudo credentials",D)}}let N=[];if(R.productionCerts){if(N=await qR(R.productionCerts,D),N.length>0)_("ssl",`Using ${N.length} production SNI cert(s): ${N.map(($)=>$.serverName).join(", ")}`,D)}if(R.https){let $=N.length>0?null:await U(R);if(!$&&N.length===0){if(_("ssl",`No valid or trusted certificates found for ${K}, generating new ones`,R.verbose),await e(R),$=await U(R),!$)throw Error(`Failed to load SSL certificates after generation for ${K}`)}else _("ssl",`Using existing and trusted certificates for ${K}`,R.verbose);R._cachedSSLConfig=$}let A="proxies"in R&&Array.isArray(R.proxies)?R.proxies.map(($)=>({...$,https:R.https,cleanup:R.cleanup,cleanUrls:$.cleanUrls??("cleanUrls"in R?R.cleanUrls:!1),vitePluginUsage:R.vitePluginUsage,changeOrigin:$.changeOrigin??R.changeOrigin,verbose:D,_cachedSSLConfig:R._cachedSSLConfig})):[{from:"from"in R?R.from:"localhost:5173",to:"to"in R?R.to:"rpx.localhost",cleanUrls:"cleanUrls"in R?R.cleanUrls:!1,https:R.https,cleanup:R.cleanup,vitePluginUsage:R.vitePluginUsage,start:"start"in R?R.start:void 0,changeOrigin:R.changeOrigin,auth:"auth"in R?R.auth:void 0,verbose:D,_cachedSSLConfig:R._cachedSSLConfig}],V=A.map(($)=>$.to||"rpx.localhost"),Y=N.length>0?N:R._cachedSSLConfig??null,J=V.filter(($)=>$&&!$.includes("localhost")&&!$.includes("127.0.0.1")),Q=["dev","app","page","new","day","foo"],Z=["test","localhost","local","example","invalid"],X=[...new Set(J.map(($)=>$.split(".").pop()?.toLowerCase()))],W=X.filter(($)=>!!$&&Q.includes($));if(W.length>0&&D)G.warn(`The following TLDs may not work reliably for local development: ${W.map(($)=>`.${$}`).join(", ")}`),G.info(" These TLDs have HSTS preloading which can bypass local DNS"),G.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(B&&z.platform==="darwin"&&J.length>0){let{setupDevelopmentDns:$}=await import("./chunk-1108y1dk.js");if(await $({domains:J,verbose:D})){if(D)if(X.every((T)=>!!T&&Z.includes(T)))G.success(`DNS server started for ${X.map((T)=>`.${T}`).join(", ")} domains`);else G.success(`DNS server started for ${X.map((T)=>`.${T}`).join(", ")} domains (hosts file entries also added)`)}else _("dns","Could not start DNS server - custom domains may not resolve",D)}let P=async()=>{_("cleanup","Starting cleanup handler",R.verbose);try{let{tearDownDevelopmentDns:$}=await import("./chunk-1108y1dk.js");await $({verbose:R.verbose})}catch($){_("cleanup",`Error stopping DNS server: ${$}`,R.verbose)}try{await r.stopAll(R.verbose)}catch($){_("cleanup",`Error stopping processes: ${$}`,R.verbose)}await u({domains:V,hosts:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.hosts,certs:typeof R.cleanup==="boolean"?R.cleanup:R.cleanup?.certs,verbose:R.verbose||!1})};z.on("SIGINT",P),z.on("SIGTERM",P);let x=R.singlePortMode===!0,w=R.httpsPort??443,F=R.httpPort??80,O=R.originGuard?JR(R.originGuard):null,L=!!Y&&(A.length>1||x),gR=!Y&&x&&A.length>0;if(L&&Y){_("proxies",`Creating shared HTTPS server for ${A.length} domains on port ${w}`,D);let $=await bR(A,B,D);if(!await C(F,"0.0.0.0",D))dR(D,F,w,R.acmeChallengeWebroot);if(await C(w,"0.0.0.0",D)){if(_("proxies",`Port ${w} is already in use, cannot start shared proxy`,D),D)G.warn(`Port ${w} is in use. Shared HTTPS proxy cannot start.`);return}if(!IR({routeEntries:$,listenPort:w,sslConfig:Y,originGuard:O,verbose:D})){G.error(`Shared HTTPS proxy failed to bind :${w}; not exiting`);return}}else if(gR){_("proxies",`Creating shared HTTP server for ${A.length} domains on port ${F}`,D);let $=await bR(A,B,D);if(await C(F,"0.0.0.0",D)){if(_("proxies",`Port ${F} is already in use, cannot start shared proxy`,D),D)G.warn(`Port ${F} is in use. Shared HTTP proxy cannot start.`);return}if(!IR({routeEntries:$,listenPort:F,sslConfig:null,originGuard:O,verbose:D})){G.error(`Shared HTTP proxy failed to bind :${F}; not exiting`);return}}else for(let $ of A)try{let I=$.to||"rpx.localhost";_("proxy",`Starting proxy for ${I} with SSL config: ${!!Y}`,$.verbose),await yR({from:$.from||"localhost:5173",to:I,cleanUrls:$.cleanUrls||!1,https:$.https||!1,cleanup:$.cleanup||!1,vitePluginUsage:$.vitePluginUsage||!1,verbose:$.verbose||!1,_cachedSSLConfig:R._cachedSSLConfig,changeOrigin:$.changeOrigin||!1})}catch(I){_("proxies",`Failed to start proxy for ${$.to}: ${I}`,$.verbose),G.error(`Failed to start proxy for ${$.to}:`,I)}}async function bR(S,R,D){let B=[],K=new Set;for(let N of S){let A=N.to||"rpx.localhost",V=N.cleanUrls||!1,Y=N.path,J=kR(Y),Q=RR(N.auth);if(N.redirect){let Z=MR(N.redirect);B.push({host:A,path:Y,route:{redirect:Z,basePath:J,auth:Q}}),_("proxies",`Route: ${A}${Y??""} → redirect ${Z.status} ${Z.to}${Q?" (auth)":""}`,D)}else if(N.static)B.push({host:A,path:Y,route:{static:xR(N.static,V),cleanUrls:V,basePath:J,auth:Q}}),_("proxies",`Route: ${A}${Y??""} → static ${typeof N.static==="string"?N.static:N.static.dir}${Q?" (auth)":""}`,D);else{let Z=new URL(N.from?.startsWith("http")?N.from:`http://${N.from}`);B.push({host:A,path:Y,route:{sourceHost:Z.host,cleanUrls:V,changeOrigin:N.changeOrigin||!1,pathRewrites:N.pathRewrites,basePath:J,auth:Q}}),_("proxies",`Route: ${A}${Y??""} → ${Z.host}${Q?" (auth)":""}`,D)}if(K.has(A))continue;if(K.add(A),R&&!HR(A)&&!A.includes("localhost")&&!A.includes("127.0.0.1"))try{if(!(await f([A],D))[0])await y([A],D)}catch{_("hosts",`Could not add hosts entry for ${A}`,D)}}return B}function IR(S){let{routeEntries:R,listenPort:D,sslConfig:B,originGuard:K,verbose:N}=S,A=wR(R),V=TR((Q,Z)=>ER(A,Q,Z),N),Y=K?(Q,Z)=>K(Q)??V(Q,Z):V,J=jR(N);try{let Q=Bun.serve({port:D,hostname:"0.0.0.0",reusePort:lR(),...B?{tls:Array.isArray(B)?B.map((Z)=>({serverName:Z.serverName,key:Z.key,cert:Z.cert})):{key:B.key,cert:B.cert,ca:B.ca,requestCert:!1,rejectUnauthorized:!1}}:{},fetch(Z,X){return Y(Z,X)},websocket:J,error(Z){return _("server",`Shared proxy server error: ${Z}`,N),new Response(`Server Error: ${Z.message}`,{status:500})}});return t.add(Q),_("proxies",`Shared ${B?"HTTPS":"HTTP"} proxy listening on port ${D} for ${A.size} domains`,N),Q}catch(Q){return _("proxies",`Failed to start shared proxy: ${Q}`,N),console.error("Failed to start shared proxy:",Q),null}}function YS(S){if(S?.vitePluginUsage||!S?.verbose)return;if(console.log(""),console.log(` ${H.green(H.bold("rpx"))} ${H.green(`v${DS}`)}`),console.log(` ${H.green("➜")} ${H.dim(S?.from??"")} ${H.dim("➜")} ${H.cyan(S?.ssl?`https://${S?.to}`:`http://${S?.to}`)}`),S?.listenPort!==(S?.ssl?443:80))console.log(` ${H.green("➜")} Listening on port ${S?.listenPort}`);if(S?.cleanUrls)console.log(` ${H.green("➜")} Clean URLs enabled`)}var JN=UR;export{NR as writeEntry,YD as watchRegistry,VS as verifyHttpsChain,HS as trustRootCaForBrowsers,yD as tearDownDevelopmentDns,CD as syncDevelopmentDnsFromRegistry,bS as stripBasePath,TD as stopDnsServer,bD as stopDaemon,yR as startServer,KS as startProxy,UR as startProxies,xD as startDnsServer,tS as siteIdForHost,lR as shouldReusePort,wD as setupResolver,qD as setupDevelopmentDns,dS as serverNameFromCertFilename,uS as serveStaticFile,E as safeStringify,vS as safeRelativePath,TN as safeDeleteFile,a as runViaDaemon,hD as runDaemon,HD as resolverFilePath,zD as resolverBasenamesForDomains,WD as resolverBasenameForDomain,xR as resolveStaticRoute,hS as resolveStaticFile,MR as resolveRedirect,xN as resolvePathRewrite,RR as resolveAuth,JD as renderStartingPage,QD as renderFailedPage,fD as removeResolver,ED as removeLegacyTldResolvers,$R as removeHosts,BR as removeEntry,vD as releaseDaemonLock,VN as redactSensitive,UD as reconcileStaleDevelopmentDns,lD as reconcileDevelopmentDnsOnIdle,eS as readSiteManifest,BD as readEntry,LD as readDaemonPid,GS as readCertSha256Fingerprint,JS as readCertCommonName,KD as readAll,VR as readAcmeChallenge,xS as pruneStaleRootCas,sS as projectNameFromHost,_D as portManager,aS as pathPrefixMatches,XS as parseSha256HashesFromSecurityListing,OS as parseHtpasswd,$S as normalizeSha256Fingerprint,kR as normalizePathPrefix,ZD as normalizeDevDomain,GR as matchesWildcard,gS as matchesAllowedSuffix,ER as matchHostRoute,pS as matchHostList,lS as matchHost,yS as loadSSLConfig,SD as listDiscoverableSites,MS as listCertSha256HashesByCommonName,HR as isWildcardPattern,ZN as isValidRootCA,_R as isValidId,FN as isSingleProxyOptions,MN as isSingleProxyConfig,TS as isRootCaTrustedForSsl,jS as isRootCaFingerprintInKeychains,mR as isProcessElevated,C as isPortInUse,ND as isPidAlive,IN as isMultiProxyOptions,zN as isMultiProxyConfig,iS as isLikelyHostname,jD as isDnsServerRunning,cD as isDaemonRunning,PS as isCertTrusted,o as httpsConfig,m as getSudoPassword,CS as getSharedDaemonCertPaths,qS as getRootCAPaths,DR as getRegistryDir,WN as getPrimaryDomain,FS as getMacosTrustKeychains,IS as getMacosLoginKeychainPath,PD as getDaemonRpxDir,OD as getDaemonPidPath,e as generateCertificate,AD as gcStaleEntries,fS as forceTrustCertificate,DD as findAvailablePort,XN as extractHostname,rS as expandHome,QN as execSudoSync,GD as escapeHtml,KR as ensureDaemonRunning,LS as enforceBasicAuth,wS as devSslToSniEntries,ID as devDomainsFromHosts,oS as detectProjectPreset,LR as deriveIdFromTarget,uD as defaultDaemonSpawnCommand,s as defaultConfig,JN as default,_ as debugLog,RD as createSiteResolver,jR as createProxyWebSocketHandler,TR as createProxyFetchHandler,JR as createOriginGuard,mS as contentTypeFor,kD as contentLooksLikeRpxResolver,s as config,H as colors,US as clearSslConfigCache,FR as cleanupCertificates,u as cleanup,f as checkHosts,U as checkExistingCertificates,QS as certIncludesSanHostnames,qR as buildSniTlsConfig,ES as buildRegistryTlsProxyOptions,cS as buildRedirectLocation,wR as buildHostRoutes,y as addHosts,mD as acquireDaemonLock,$D as SiteSupervisor,kS as SHARED_DEV_HOST_CERT_PATH,zS as RPX_ROOT_CA_COMMON_NAME,MD as RPX_RESOLVER_MARKER,nS as OnDemandCertManager,WS as MACOS_SYSTEM_KEYCHAIN,ZS as MACOS_CA_TRUST_FLAGS,XD as LEGACY_TLD_RESOLVER_LABELS,CR as DefaultPortManager,VD as DNS_STATE_VERSION,FD as DNS_PORT,QR as ACME_CHALLENGE_PREFIX};
@@ -1,4 +1,6 @@
1
1
  import type { PathRewrite } from './types';
2
+ import type { ResolvedAuth } from './auth';
3
+ import type { ResolvedRedirect } from './redirect';
2
4
  import type { ResolvedStaticRoute } from './static-files';
3
5
  /**
4
6
  * Strip the route's mount prefix (`basePath`) from a request pathname so a
@@ -14,7 +16,7 @@ export declare function stripBasePath(pathname: string, basePath?: string): stri
14
16
  * is upgraded (returns `undefined` so Bun completes the handshake) and the
15
17
  * traffic is handled by the `websocket` handler from {@link createProxyWebSocketHandler}.
16
18
  */
17
- export declare function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean): ProxyFetchHandler;
19
+ export declare function createProxyFetchHandler(getRoute: GetRoute, verbose?: boolean, onNoRoute?: OnNoRoute): ProxyFetchHandler;
18
20
  /**
19
21
  * Build the `websocket` handler block for Bun.serve. It opens an upstream
20
22
  * `WebSocket` per client socket, buffers client→upstream frames until the
@@ -28,8 +30,10 @@ export declare interface ProxyRoute {
28
30
  changeOrigin?: boolean
29
31
  pathRewrites?: PathRewrite[]
30
32
  static?: ResolvedStaticRoute
33
+ redirect?: ResolvedRedirect
31
34
  basePath?: string
32
35
  stripBasePathPrefix?: boolean
36
+ auth?: ResolvedAuth
33
37
  }
34
38
  /** Minimal shape of the Bun server needed for WebSocket upgrades. */
35
39
  export declare interface ProxyServer {
@@ -40,4 +44,17 @@ export declare interface ProxyServer {
40
44
  * existing host-only `getRoute` callbacks remain valid.
41
45
  */
42
46
  export type GetRoute = (hostname: string, pathname: string) => ProxyRoute | undefined;
47
+ /**
48
+ * Outcome of the no-route fallback ({@link OnNoRoute}):
49
+ * - a `Response` — serve it as-is (e.g. a "starting…" splash);
50
+ * - `{ retry: true }` — a route was just published, so re-resolve and proxy;
51
+ * - `undefined` — no fallback applies, fall through to 404.
52
+ */
53
+ export type NoRouteOutcome = Response | { retry: true } | undefined;
54
+ /**
55
+ * Called when `getRoute` finds nothing for a request. Lets the daemon lazily
56
+ * boot an on-demand site (see {@link import('./site-supervisor').SiteSupervisor})
57
+ * and either hold the request behind a splash or signal that the route is now live.
58
+ */
59
+ export type OnNoRoute = (hostname: string, pathname: string, req: Request) => Promise<NoRouteOutcome>;
43
60
  export type ProxyFetchHandler = (req: Request, server?: ProxyServer) => Promise<Response | undefined>;
@@ -41,10 +41,15 @@ declare class Conn {
41
41
  idleSince: number;
42
42
  bodyQueue: Uint8Array[] | null;
43
43
  bodyRemaining: number;
44
+ queuedBytes: number;
45
+ streamingBody: boolean;
46
+ lastActivityAt: number;
44
47
  drainWaiter: (() => void) | null;
45
48
  writeAll(bytes: Uint8Array): Promise<void>;
46
49
  wakeDrain(): void;
47
50
  push(chunk: Uint8Array): void;
51
+ resumeIfDrained(): void;
52
+ clearStreaming(): void;
48
53
  markClosed(): void;
49
54
  markTimedOut(): void;
50
55
  waitForData(seen: number): Promise<void>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Normalize a `redirect` config (string shorthand or object) into a
3
+ * {@link ResolvedRedirect}. A `to` without a scheme is treated as `https://<to>`
4
+ * (a public redirect should land on TLS), and any trailing slash is trimmed so
5
+ * appending the request path joins cleanly.
6
+ */
7
+ export declare function resolveRedirect(input: string | RedirectRouteConfig): ResolvedRedirect;
8
+ /**
9
+ * Build the `Location` value for a redirect route. With `preservePath` the
10
+ * request `pathname` + `search` (e.g. `/a/b?x=1`) is appended to the target
11
+ * base; otherwise the bare target is used. `pathname` always starts with `/`
12
+ * and `search` carries its own leading `?` (or is empty).
13
+ */
14
+ export declare function buildRedirectLocation(redirect: ResolvedRedirect, pathname: string, search: string): string;
15
+ /**
16
+ * Redirect routes: a gateway host whose requests are answered with an HTTP
17
+ * redirect (a `Location` response) instead of being proxied to an upstream or
18
+ * served from disk. The canonical use is pointing an alternate/parked domain at
19
+ * its primary host — e.g. `very-good-adblock.org` → `https://verygoodadblock.org`
20
+ * — while preserving deep links: the request path + query are appended to the
21
+ * target by default.
22
+ *
23
+ * Kept transport-agnostic and dependency-free so both the shared `:443` handler
24
+ * and tests can use it directly.
25
+ */
26
+ /**
27
+ * Public redirect config for a route. A bare string is shorthand for a
28
+ * permanent (301) redirect to that URL, path-preserving.
29
+ */
30
+ export declare interface RedirectRouteConfig {
31
+ to: string
32
+ status?: 301 | 302 | 307 | 308
33
+ preservePath?: boolean
34
+ }
35
+ /** A normalized redirect, ready to serve. */
36
+ export declare interface ResolvedRedirect {
37
+ to: string
38
+ status: 301 | 302 | 307 | 308
39
+ preservePath: boolean
40
+ }
@@ -1,5 +1,5 @@
1
1
  import * as path from 'node:path';
2
- import type { PathRewrite, StaticRouteConfig } from './types';
2
+ import type { BasicAuthConfig, PathRewrite, StaticRouteConfig } from './types';
3
3
  /**
4
4
  * Default location for the registry directory. The daemon's PID file and log
5
5
  * sit alongside it under `~/.stacks/rpx/`.
@@ -66,6 +66,7 @@ export declare interface RegistryEntry {
66
66
  cleanUrls?: boolean
67
67
  changeOrigin?: boolean
68
68
  static?: string | StaticRouteConfig
69
+ auth?: BasicAuthConfig
69
70
  }
70
71
  export declare interface WatchHandle {
71
72
  close: () => void
@@ -0,0 +1,106 @@
1
+ import type { OnDemandSitesConfig, SiteRouteTemplate } from './types';
2
+ /** Expand a leading `~` (or `~/…`) to the resolved home directory. */
3
+ export declare function expandHome(p: string, home: string): string;
4
+ /** A registry-safe id derived from a host (`a.localhost` → `a.localhost`). */
5
+ export declare function siteIdForHost(host: string): string;
6
+ /**
7
+ * The host's single project label for convention discovery: `<name>.<tld>` →
8
+ * `name`. Multi-label hosts (`docs.app.localhost`) and bare TLDs don't discover —
9
+ * point those at an explicit `sites` entry. Returns `null` when no TLD matches.
10
+ */
11
+ export declare function projectNameFromHost(host: string, tlds: string[]): string | null;
12
+ /**
13
+ * Read a per-project {@link SiteManifest} so users can define the dev startup
14
+ * manually. Checked sources, in order: a `rpx.site.json` file in the project,
15
+ * then a `"rpx"` key in its `package.json`. Returns `null` when neither exists
16
+ * (or both are malformed).
17
+ */
18
+ export declare function readSiteManifest(dir: string, deps: ResolverProbes): SiteManifest | null;
19
+ /*` dependency in
20
+ * `package.json`. Boots frontend (`/`), API (`/api`) and docs (`/docs`) with
21
+ * the conventional `PORT`/`PORT_API`/`PORT_DOCS` env, deferring proxy + TLS to
22
+ * rpx (`STACKS_PROXY_MANAGED=1`) and taking its public origin via `APP_URL`.
23
+ * - **Generic** — any `package.json` with a `dev` script: a single `bun run dev`
24
+ * backend on `PORT`.
25
+ * - Otherwise `null`.
26
+ */
27
+ export declare function detectProjectPreset(dir: string, deps: ResolverProbes): SitePreset | null;
28
+ /**
29
+ * Build a {@link SiteResolver} over an {@link OnDemandSitesConfig}. Explicit
30
+ * `sites` are matched first (exact host, then wildcard), then convention
31
+ * discovery under `roots`.
32
+ */
33
+ export declare function createSiteResolver(config: OnDemandSitesConfig, deps?: SiteResolverDeps): SiteResolver;
34
+ /**
35
+ * Enumerate the sites rpx can currently boot — explicit non-wildcard
36
+ * {@link SiteConfig}s plus every project discovered by scanning the configured
37
+ * roots. Powers `rpx sites`. Each entry is a fully-resolved {@link ResolvedSite}
38
+ * (so the caller sees the dir, command, and host it would serve).
39
+ */
40
+ export declare function listDiscoverableSites(config: OnDemandSitesConfig, deps?: SiteResolverDeps): ResolvedSite[];
41
+ /** A fully-resolved site, ready for the supervisor to boot and route. */
42
+ export declare interface ResolvedSite {
43
+ host: string
44
+ id: string
45
+ dir: string
46
+ command: string
47
+ env: Record<string, string>
48
+ routes: SiteRouteTemplate[]
49
+ selfRegisters: boolean
50
+ idleTimeoutMs: number
51
+ source: 'config' | 'discovered'
52
+ }
53
+ /**
54
+ * The dev-command shape for a recognized project kind. Returned by a
55
+ * {@link SiteDetector}; the resolver folds it into a {@link ResolvedSite}.
56
+ */
57
+ export declare interface SitePreset {
58
+ command: string
59
+ env?: Record<string, string>
60
+ routes?: SiteRouteTemplate[]
61
+ selfRegisters?: boolean
62
+ urlEnv?: string[]
63
+ }
64
+ /**
65
+ * A per-project override of how rpx boots a site — so the dev startup can be
66
+ * defined **manually** instead of relying on auto-detection. Read from a
67
+ * `rpx.site.json` file in the project, or a `"rpx"` key in its `package.json`.
68
+ * A manifest with a `command` fully defines the preset (and makes even a
69
+ * directory that isn't otherwise a recognized project bootable).
70
+ *
71
+ * ```jsonc
72
+ * // rpx.site.json
73
+ * {
74
+ * "command": "pnpm dev",
75
+ * "env": { "NODE_ENV": "development" },
76
+ * "routes": [
77
+ * { "path": "/", "portEnv": "PORT", "defaultPort": 5173, "readyGate": true },
78
+ * { "path": "/api", "portEnv": "API_PORT", "defaultPort": 4000 }
79
+ * ]
80
+ * }
81
+ * ```
82
+ */
83
+ export declare interface SiteManifest {
84
+ command?: string
85
+ env?: Record<string, string>
86
+ routes?: SiteRouteTemplate[]
87
+ selfRegisters?: boolean
88
+ urlEnv?: string[]
89
+ }
90
+ /** Filesystem probes the resolver and detector use — injected for tests. */
91
+ export declare interface ResolverProbes {
92
+ dirExists: (p: string) => boolean
93
+ fileExists: (p: string) => boolean
94
+ readText: (p: string) => string | null
95
+ }
96
+ export declare interface SiteResolverDeps extends Partial<ResolverProbes> {
97
+ detect?: SiteDetector
98
+ homeDir?: string
99
+ readdir?: (p: string) => string[]
100
+ }
101
+ /** Resolve a host to a site (or `null`). Construct once, call per request. */
102
+ export declare interface SiteResolver {
103
+ resolve: (host: string) => ResolvedSite | null
104
+ }
105
+ /** Detect a project kind from its directory. Returns `null` for "not a dev project". */
106
+ export type SiteDetector = (dir: string, deps: ResolverProbes) => SitePreset | null;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The interstitial pages rpx serves while an on-demand site boots (or after it
3
+ * fails). Plain server-rendered HTML with a `<meta http-equiv="refresh">` so the
4
+ * browser re-requests on its own — no client JS — and the moment the site's
5
+ * routes are published the refresh lands on the real app instead.
6
+ */
7
+ /** Escape the five HTML-significant characters for safe interpolation. */
8
+ export declare function escapeHtml(value: string): string;
9
+ /**
10
+ * The "starting…" splash (HTTP 503 + `Retry-After`). Auto-refreshes until the
11
+ * site's routes go live, at which point the refresh hits the real app. Shows the
12
+ * tail of the boot log so progress is visible while waiting.
13
+ */
14
+ export declare function renderStartingPage(opts: { host: string, sinceMs: number, logTail?: string }): Response;
15
+ /**
16
+ * The failure page (HTTP 502). Shows the tail of the site's log and keeps a slow
17
+ * refresh so a fix + restart is picked up without a manual reload.
18
+ */
19
+ export declare function renderFailedPage(opts: { host: string, error: string, logTail: string }): Response;
@@ -0,0 +1,56 @@
1
+ import type { RegistryEntry } from './registry';
2
+ import type { SiteResolver } from './site-resolver';
3
+ /** A launched site process, abstracted so tests can inject a fake. */
4
+ export declare interface SiteProcessHandle {
5
+ pid: number
6
+ exited: Promise<number | null>
7
+ stop: (signal?: NodeJS.Signals) => void
8
+ }
9
+ /** A point-in-time view of one supervised site, for `rpx sites` / status. */
10
+ export declare interface SiteSnapshot {
11
+ host: string
12
+ dir: string
13
+ status: 'starting' | 'ready' | 'failed'
14
+ pid: number | null
15
+ ports: Record<string, number>
16
+ uptimeMs: number
17
+ idleMs: number
18
+ error?: string
19
+ }
20
+ export declare interface SiteSupervisorOptions {
21
+ resolver: SiteResolver
22
+ registryDir?: string
23
+ rpxDir?: string
24
+ verbose?: boolean
25
+ startupTimeoutMs?: number
26
+ pollIntervalMs?: number
27
+ reapIntervalMs?: number
28
+ restartDelayMs?: number
29
+ killGraceMs?: number
30
+ launcher?: SiteLauncher
31
+ probePort?: (port: number) => Promise<boolean>
32
+ pickPort?: (preferred: number) => Promise<number>
33
+ isHostRoutable?: (host: string) => boolean
34
+ onSiteActivating?: (host: string) => void
35
+ now?: () => number
36
+ writeEntry?: (entry: RegistryEntry, dir?: string, verbose?: boolean) => Promise<void>
37
+ removeEntry?: (id: string, dir?: string, verbose?: boolean) => Promise<void>
38
+ }
39
+ /** Spawn a site's dev command. Injected for tests; default spawns a shell. */
40
+ export type SiteLauncher = (spec: {
41
+ command: string
42
+ cwd: string
43
+ env: Record<string, string>
44
+ logPath: string
45
+ }) => SiteProcessHandle;
46
+ export type SiteRequestStatus = | { kind: 'unknown' }
47
+ | { kind: 'starting', host: string, sinceMs: number, source: 'config' | 'discovered', logTail: string }
48
+ | { kind: 'ready', host: string }
49
+ | { kind: 'failed', host: string, error: string, logTail: string }
50
+ export declare class SiteSupervisor {
51
+ constructor(opts: SiteSupervisorOptions);
52
+ onRequest(host: string): Promise<SiteRequestStatus>;
53
+ stop(host: string): Promise<void>;
54
+ list(): SiteSnapshot[];
55
+ stopAll(): Promise<void>;
56
+ }