@stacksjs/rpx 0.11.45 → 0.11.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{$ as Tx,$a as P6,A as vR,Aa as $x,B as c,Ba as B6,C as wx,Ca as Bx,D as Ix,Da as Jx,E as mR,Ea as J6,F as u,Fa as V6,G as zx,Ga as D6,H as lR,Ha as Q6,I as bR,Ia as Z6,J as aR,Ja as k6,K as Ax,Ka as H6,L as Nx,La as I6,M as pR,Ma as F6,N as jx,Na as M6,O as iR,Oa as W6,P as Fx,Pa as E6,Q as dR,Qa as f6,R as Mx,Ra as P,S as nR,Sa as Px,T as gR,Ta as L,U as tR,Ua as T6,V as sR,Va as y6,W as rR,Wa as q6,X as Wx,Xa as C6,Y as Ex,Ya as U6,Z as eR,Za as c6,_ as fx,_a as u6,a as j,aa as m,ab as L6,b as Sx,ba as oR,bb as O6,c as VR,ca as yx,cb as h6,d as DR,da as x6,db as v6,e as QR,ea as qx,eb as m6,f as ZR,fa as R6,g as kR,ga as Cx,gb as l6,h as HR,ha as gx,hb as b6,i as IR,ia as S6,ib as a6,j as FR,ja as Ux,jb as p6,k as MR,ka as tx,kb as i6,l as WR,la as _6,lb as d6,m as ER,ma as w6,mb as n6,n as fR,na as cx,nb as g6,o as TR,oa as z6,ob as t6,p as yR,pa as A6,pb as s6,q as qR,qa as N6,qb as r6,r as CR,ra as j6,rb as e6,s as UR,sa as K6,sb as o6,t as cR,ta as G6,tb as x4,u as uR,ua as X6,ub as R4,v as PR,va as C,vb as S4,w as LR,wa as Y6,wb as _4,x as OR,xa as ux,xb as Vx,y as hR,ya as $6,yb as w4,z as _x,za as Yx,zb as z4}from"./chunk-t6pzd7ec.js";import{Ab as Hx,Bb as dx,Cb as qS,Db as CS,Eb as US,Fb as _,Gb as nx,Hb as cS,Ib as q,Jb as uS,Kb as PS,Lb as LS,Mb as OS,Nb as hS,Ob as vS,Pb as mS,Qb as lS,Rb as bS}from"./chunk-w4t1nar9.js";import{execSync as KR}from"node:child_process";import*as Kx from"node:http";import*as sx from"node:net";import*as Gx from"node:os";import*as rx from"node:path";import*as Q from"node:process";var n=(R,x)=>(S)=>`\x1B[${R}m${S}\x1B[${x}m`,F={bold:n(1,22),dim:n(2,22),green:n(32,39),cyan:n(36,39)};import*as mx from"node:fs";import*as lx from"node:path";import*as E from"node:process";function bx(R,x){let w=(x&&x!=="/"?`${R}${x}`:R).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return w.length>0?w:"rpx"}async function g(R){if(R.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let x=R.verbose??!1,S=R.registryDir,w=new Set,K=R.proxies.map((X)=>{let V=X.id??bx(X.to,X.path);if(!$x(V))throw Error(`invalid registry id "${V}" derived from to="${X.to}"`);if(w.has(V))throw Error(`duplicate registry id "${V}" — set an explicit \`id\` on one of the proxies`);return w.add(V),{...X,id:V}}),N=new Date().toISOString();for(let X of K)await Bx({id:X.id,from:X.from,to:X.to,path:X.path,pid:R.persistent?void 0:E.pid,cwd:E.cwd(),createdAt:N,cleanUrls:X.cleanUrls,changeOrigin:X.changeOrigin,pathRewrites:X.pathRewrites,static:X.static,loadBalancer:X.loadBalancer},S,x);let z=await Vx({rpxDir:R.rpxDir,verbose:x,spawnCommand:R.spawnCommand,startupTimeoutMs:R.startupTimeoutMs,spawnEnv:R.spawnEnv});for(let X of K){let V=X.static?`static ${typeof X.static==="string"?X.static:X.static.dir}`:X.from;j.success(`https://${X.to} → ${V}`)}if(j.info(`(via rpx daemon pid=${z.pid}; \`rpx daemon:status\` to inspect)`),R.detached)return;let $=!1,G=S??Yx(),Y=K.map((X)=>X.id),J=async()=>{if($)return;$=!0;for(let X of Y)await Jx(X,S,x).catch((V)=>{_("runner",`removeEntry(${X}) failed: ${V}`,x)})},B=(X)=>{_("runner",`received ${X}, unregistering ${Y.length} entries`,x),J().finally(()=>E.exit(0))};E.once("SIGINT",B),E.once("SIGTERM",B),E.once("exit",()=>{if($)return;for(let X of Y)try{mx.unlinkSync(lx.join(G,`${X}.json`))}catch{}}),await new Promise(()=>{})}import{spawn as _R}from"node:child_process";import*as t from"node:process";class s{processes=new Map;isShuttingDown=!1;async startProcess(R,x,S){if(this.processes.has(R)){_("start",`Process ${R} is already running`,S);return}let[w,...K]=x.command.split(" "),N=x.cwd||t.cwd();_("start",`Starting process ${R}:`,S),_("start",` Command: ${w} ${K.join(" ")}`,S),_("start",` Working directory: ${N}`,S),_("start",` Environment variables: ${q(x.env)}`,S);let z=_R(w,K,{cwd:N,env:{...t.env,...x.env},shell:!0,stdio:"inherit"});return this.processes.set(R,{command:x.command,cwd:N,process:z,env:x.env}),new Promise(($,G)=>{if(z.on("error",(Y)=>{if(!this.isShuttingDown)_("start",`Process ${R} failed to start: ${Y}`,S),this.processes.delete(R),G(Y)}),z.on("exit",(Y)=>{if(!this.isShuttingDown&&Y!==null&&Y!==0)_("start",`Process ${R} exited with code ${Y}; leaving the proxy running`,S),this.processes.delete(R),G(Error(`Process ${R} exited with code ${Y}`))}),S)z.stdout?.on("data",(Y)=>{_("process",`[${R}] ${Y.toString().trim()}`,!0)}),z.stderr?.on("data",(Y)=>{_("process",`[${R}] ERR: ${Y.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&z.killed)this.processes.delete(R),G(Error(`Process ${R} was killed during startup`));else _("start",`Process ${R} started successfully`,S),$()},1000)})}async stopProcess(R,x){let S=this.processes.get(R);if(!S?.process){_("start",`No process found for ${R}`,x);return}return _("start",`Stopping process ${R}`,x),new Promise((w)=>{if(!S.process){w();return}S.process.once("exit",()=>{this.processes.delete(R),_("start",`Process ${R} stopped`,x),w()});try{S.process.kill("SIGTERM"),setTimeout(()=>{if(S.process){_("start",`Force killing process ${R}`,x);try{S.process.kill("SIGKILL")}catch(K){}}},3000)}catch(K){_("start",`Error stopping process ${R}: ${K}`,x),this.processes.delete(R),w()}})}async stopAll(R){if(this.isShuttingDown){_("start","Already shutting down, skipping duplicate stopAll call",R);return}this.isShuttingDown=!0,_("start","Stopping all processes",R);let x=Array.from(this.processes.keys()).map((S)=>this.stopProcess(S,R).catch((w)=>{j.error(`Failed to stop process ${S}:`,w)}));await Promise.allSettled(x),this.processes.clear(),this.isShuttingDown=!1}isRunning(R){let x=this.processes.get(R);return!!x?.process&&!x.process.killed}}var D4=new s;import{timingSafeEqual as wR}from"node:crypto";function zR(R,x){if(R==null)return!1;let S=Buffer.from(R),w=Buffer.from(x);if(S.length!==w.length)return!1;return wR(S,w)}function r(R){return R.toLowerCase().replace(/\.$/,"")}var AR=["/.well-known/acme-challenge/"];function NR(R){let x=R.headers.get("host");if(x)return r(x.split(":")[0]);try{return r(new URL(R.url).hostname)}catch{return""}}function Dx(R){let x=R.header.toLowerCase(),S=new Set,w=[];for(let G of R.hosts){let Y=r(G);if(Y.startsWith("*."))w.push(Y);else S.add(Y)}let K=R.exemptPaths??AR,N=R.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
2
- `,z=(G)=>{let Y=r(G);return S.has(Y)||w.some((J)=>m(Y,J))},$=(G)=>{let Y=NR(G);if(!z(Y))return;let J="/";try{J=new URL(G.url).pathname}catch{}if(K.some((B)=>J.startsWith(B)))return;if(zR(G.headers.get(x),R.value))return;return new Response(N,{status:403,headers:{"content-type":"text/plain"}})};return $.protects=z,$}var ax={name:"@stacksjs/rpx",type:"module",version:"0.11.45",description:"A modern and smart reverse proxy.",author:"Chris Breuer <chris@stacksjs.org>",license:"MIT",homepage:"https://github.com/stacksjs/rpx",repository:{type:"git",url:"git+https://github.com/stacksjs/rpx.git"},bugs:{url:"https://github.com/stacksjs/rpx/issues"},keywords:["reverse proxy","ssl","development","environment","proxy","bun","stacks","typescript","javascript"],exports:{".":{types:"./dist/index.d.ts",bun:"./dist/index.js",import:"./dist/index.js"}},module:"./dist/index.js",types:"./dist/index.d.ts",bin:{rpx:"./dist/bin/cli.js","reverse-proxy":"./dist/bin/cli.js"},files:["README.md","dist"],scripts:{build:"bun build.ts && bun build --production ./bin/cli.ts --compile --outfile bin/rpx",compile:"bun build --production ./bin/cli.ts --compile --outfile bin/rpx","compile:all":"bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64","compile:linux-x64":"bun build --production ./bin/cli.ts --compile --target=bun-linux-x64 --outfile bin/rpx-linux-x64","compile:linux-arm64":"bun build --production ./bin/cli.ts --compile --target=bun-linux-arm64 --outfile bin/rpx-linux-arm64","compile:windows-x64":"bun build --production ./bin/cli.ts --compile --target=bun-windows-x64 --outfile bin/rpx-windows-x64.exe","compile:darwin-x64":"bun build --production ./bin/cli.ts --compile --target=bun-darwin-x64 --outfile bin/rpx-darwin-x64","compile:darwin-arm64":"bun build --production ./bin/cli.ts --compile --target=bun-darwin-arm64 --outfile bin/rpx-darwin-arm64",bench:"bun run bench/run.ts","bench:html":"bun run bench/run.ts --html","bench:latency":"bun run bench/run.ts --latency","bench:throughput":"bun run bench/run.ts --throughput",lint:"bunx --bun pickier .","lint:fix":"bunx --bun pickier . --fix",fresh:"bunx rimraf node_modules/ bun.lock && bun i",changelog:"changelogen --output CHANGELOG.md",prepublishOnly:"bun build.ts","release:binaries":"bun run compile:all && bun run zip",test:"bun test",typecheck:"bunx tsc --noEmit",zip:"bun run zip:all","zip:all":"bun run zip:linux-x64 && bun run zip:linux-arm64 && bun run zip:windows-x64 && bun run zip:darwin-x64 && bun run zip:darwin-arm64","zip:linux-x64":"zip -j bin/rpx-linux-x64.zip bin/rpx-linux-x64","zip:linux-arm64":"zip -j bin/rpx-linux-arm64.zip bin/rpx-linux-arm64","zip:windows-x64":"zip -j bin/rpx-windows-x64.zip bin/rpx-windows-x64.exe","zip:darwin-x64":"zip -j bin/rpx-darwin-x64.zip bin/rpx-darwin-x64","zip:darwin-arm64":"zip -j bin/rpx-darwin-arm64.zip bin/rpx-darwin-arm64"},dependencies:{"@stacksjs/clapp":"^0.2.10","@stacksjs/tlsx":"^0.13.13"},devDependencies:{bunfig:"^0.15.6",mitata:"^1.0.34",typescript:"^7.0.2"},"simple-git-hooks":{"pre-commit":"bunx lint-staged"},"lint-staged":{"*.{js,ts}":"bunx --bun pickier . --fix"}};var px=ax.version;var o=new s,GR=new ux("0.0.0.0"),b=new Set,Rx=new Set,Qx=!1,e=null,Zx=null;async function p(R){if(Qx)return _("cleanup","Cleanup already in progress, skipping",R?.verbose),Zx||Promise.resolve();Qx=!0,_("cleanup","Starting cleanup process",R?.verbose),Zx=new Promise((x)=>{e=x});try{await o.stopAll(R?.verbose),j.info("Shutting down proxy servers...");let x=[],S=Array.from(b).map((w)=>new Promise((K)=>{let N=w;try{if(typeof N.stop==="function")N.stop(!0),_("cleanup","Bun server stopped",R?.verbose),K();else if(typeof N.close==="function")N.close(()=>{_("cleanup","Server closed successfully",R?.verbose),K()});else K()}catch(z){_("cleanup",`Error stopping server: ${z}`,R?.verbose),K()}}));x.push(...S),b.clear();for(let w of Rx)Nx(w);if(Rx.clear(),R?.hosts&&R.domains?.length){_("cleanup","Cleaning up hosts file entries",R?.verbose),_("cleanup",`Original domains for cleanup: ${JSON.stringify(R.domains)}`,R?.verbose);let w=R.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(w)}`,R?.verbose),w.length>0)j.info("Cleaning up hosts file entries..."),x.push(Px(w,R?.verbose).then(()=>{_("cleanup",`Removed hosts entries for ${w.join(", ")}`,R?.verbose)}).catch((K)=>{_("cleanup",`Failed to remove hosts entries: ${K}`,R?.verbose),j.warn(`Failed to clean up hosts file entries for ${w.join(", ")}:`,K)}))}if(R?.certs&&R.domains?.length){_("cleanup","Cleaning up SSL certificates",R?.verbose),j.info("Cleaning up SSL certificates...");let w=R.domains.map(async(K)=>{try{await Ix(K,R?.verbose),_("cleanup",`Removed certificates for ${K}`,R?.verbose)}catch(N){_("cleanup",`Failed to remove certificates for ${K}: ${N}`,R?.verbose),j.warn(`Failed to clean up certificates for ${K}:`,N)}});x.push(...w)}await Promise.allSettled(x),_("cleanup","All cleanup tasks completed successfully",R?.verbose),j.success("All cleanup tasks completed successfully")}catch(x){_("cleanup",`Error during cleanup: ${x}`,R?.verbose),j.error("Error during cleanup:",x)}finally{if(e)e();e=null,Qx=!1;let x=R&&"vitePluginUsage"in R&&R.vitePluginUsage===!0;if(Q.env.NODE_ENV!=="test"&&Q.env.BUN_ENV!=="test"&&!x)Q.exit(0)}return Zx}var kx=!1;function ex(R){if(kx){_("signal",`Received second ${R} signal, forcing exit`,!0),Q.exit(1);return}kx=!0,_("signal",`Received ${R} signal, initiating cleanup`,!0),p().catch((x)=>{_("signal",`Cleanup failed after ${R}: ${x}`,!0),Q.exit(1)}).finally(()=>{kx=!1})}Q.once("SIGINT",()=>ex("SIGINT"));Q.once("SIGTERM",()=>ex("SIGTERM"));Q.on("uncaughtException",(R)=>{j.error("Uncaught exception (continuing):",R)});Q.on("unhandledRejection",(R)=>{j.error("Unhandled rejection (continuing):",R)});async function a(R,x,S,w=5){_("connection",`Testing connection to ${R}:${x} (retries left: ${w})`,S);let K=15000,N=Date.now();if(Q.env.RPX_BYPASS_CONNECTION_TEST==="true"){_("connection",`Bypassing connection test for ${R}:${x} due to RPX_BYPASS_CONNECTION_TEST flag`,S);return}let z=()=>new Promise(($,G)=>{let Y=sx.connect({host:R,port:x,timeout:3000});Y.once("connect",()=>{_("connection",`Successfully connected to ${R}:${x}`,S),Y.end(),$()}),Y.once("timeout",()=>{_("connection",`Connection to ${R}:${x} timed out`,S),Y.destroy(),G(Error("Connection timed out"))}),Y.once("error",(J)=>{_("connection",`Failed to connect to ${R}:${x}: ${J}`,S),Y.destroy(),G(J)})});try{await z()}catch($){let G=$;if(Date.now()-N>K){_("connection",`Connection test timed out after ${K}ms, but continuing anyway`,S),j.warn(`Connection test to ${R}:${x} timed out, but RPX will try to proceed anyway.`);return}if(G.code==="ECONNREFUSED"&&w>0)return _("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${w} retries left)`,S),await new Promise((J)=>setTimeout(J,2000)),a(R,x,S,w-1);if(w>0)try{_("connection",`Trying HTTP request to ${R}:${x}`,S),await new Promise((J,B)=>{let X=Kx.request({hostname:R,port:x,path:"/",method:"HEAD",timeout:5000},(V)=>{_("connection",`Received HTTP response with status: ${V.statusCode}`,S),J()});X.on("error",(V)=>B(V)),X.on("timeout",()=>{X.destroy(),B(Error("HTTP request timed out"))}),X.end()}),_("connection",`HTTP request to ${R}:${x} succeeded`,S);return}catch(J){return _("connection",`HTTP request to ${R}:${x} failed: ${J}`,S),_("connection",`Retrying socket connection in 2 seconds... (${w} retries left)`,S),await new Promise((B)=>setTimeout(B,2000)),a(R,x,S,w-1)}let Y=`Failed to connect to ${R}:${x} after ${5-w} attempts: ${G.message}`;_("connection",`${Y}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,S),j.warn(Y),j.warn("RPX will try to continue anyway. If you're sure this is correct, you can set RPX_BYPASS_CONNECTION_TEST=true to skip this check.")}}async function Lx(R){_("server",`Starting server with options: ${q(R)}`,R.verbose);let x=u(R.from),S=new URL(x.startsWith("http")?x:`http://${x}`),w=new URL((R.to?.startsWith("http")?R.to:`http://${R.to}`)||"rpx.localhost"),K=Number.parseInt(S.port)||(S.protocol.includes("https:")?443:80),N=[w.hostname];if(Ox(R)&&!w.hostname.includes("localhost")&&!w.hostname.includes("127.0.0.1")){_("hosts",`Checking if hosts file entry exists for: ${w.hostname}`,R?.verbose);try{if(!(await L(N,R.verbose))[0]){j.info(`Adding ${w.hostname} to hosts file...`),j.info("This may require sudo/administrator privileges");try{await P(N,R.verbose)}catch(G){if(j.error("Failed to add hosts entry:",G.message),j.warn("You can manually add this entry to your hosts file:"),j.warn(`127.0.0.1 ${w.hostname}`),j.warn(`::1 ${w.hostname}`),Q.platform==="win32")j.warn("On Windows:"),j.warn("1. Run notepad as administrator"),j.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else j.warn("On Unix systems:"),j.warn("sudo nano /etc/hosts")}}else _("hosts",`Host entry already exists for ${w.hostname}`,R.verbose)}catch($){j.error("Failed to check hosts file:",$.message)}}try{await a(S.hostname,K,R.verbose)}catch($){_("server",`Connection test failed: ${$}`,R.verbose),j.error($.message),j.warn("Continuing with proxy setup despite connection test failure..."),j.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let z=R._cachedSSLConfig||null;if(R.https)try{if(R.https===!0)R.https=wx({...R,to:w.hostname});if(z=await c({...R,to:w.hostname,https:R.https}),!z){if(_("ssl",`Generating new certificates for ${w.hostname}`,R.verbose),await _x({...R,from:S.toString(),to:w.hostname,https:R.https}),z=await c({...R,to:w.hostname,https:R.https}),!z)throw Error(`Failed to load SSL configuration after generating certificates for ${w.hostname}`)}}catch($){throw _("server",`SSL setup failed: ${$}`,R.verbose),$}_("server",`Setting up reverse proxy with SSL config for ${w.hostname}`,R.verbose),await YR({...R,from:x,originalFrom:R.from||x,to:w.hostname,fromPort:K,sourceUrl:{hostname:S.hostname,host:S.host},ssl:z})}async function XR(R,x,S,w,K,N,z,$,G,Y,J,B){_("proxy",`Creating proxy server ${R} -> ${x} with cleanUrls: ${$}`,z);let X=zx(J??w.host,B);Ax(X);let V=[{host:x,route:{sourceHost:w.host,upstreamPool:X,cleanUrls:$||!1,changeOrigin:G||!1,basePath:"/",auth:Y}}];if(!xx({routeEntries:V,listenPort:S,sslConfig:K,originGuard:null,verbose:z??!1}))throw Nx(X),Error(`Failed to start proxy server for ${x} on port ${S}`);Rx.add(X),JR({from:R,to:x,vitePluginUsage:N,listenPort:S,ssl:!!K,cleanUrls:$,verbose:z})}async function YR(R){_("setup",`Setting up reverse proxy: ${q(R)}`,R.verbose);let{from:x,originalFrom:S,to:w,sourceUrl:K,ssl:N,verbose:z,cleanup:$,vitePluginUsage:G,changeOrigin:Y,cleanUrls:J}=R,B=80,X=443,V=xR(),f=R.portManager||GR,O=Ox(R);try{if(O&&w&&!w.includes("localhost")&&!w.includes("127.0.0.1")){if(!(await L([w],z))[0]){j.warn(`The hostname ${w} isn't in your hosts file. Adding it now...`);try{await P([w],z),j.success(`Added ${w} to your hosts file.`)}catch(h){j.error(`Failed to add ${w} to your hosts file: ${h}`),j.info(`You may need to manually add '127.0.0.1 ${w}' to your /etc/hosts file.`)}}}else if(O&&Q.platform!=="darwin"&&w&&w.includes("localhost")&&!w.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await L([w],z))[0]){_("hosts",`${w} not found in hosts file, adding...`,z);try{await P([w],z)}catch(h){_("hosts",`Failed to add ${w} to hosts file: ${h}`,z)}}}if(N&&!f.usedPorts.has(B)){if(!await C(B,V,z))_("setup","Starting HTTP redirect server",z),ox(z),f.usedPorts.add(B);else if(_("setup","Port 80 is in use, skipping HTTP redirect",z),z)j.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let Z=N?X:B,M=await C(Z,V,z),I;if(M){if(_("setup",`Port ${Z} is already in use`,z),z)j.warn(`Port ${Z} is already in use. This may be another instance of rpx or another service.`);if(Z===443){if(I=await f.getNextAvailablePort(3443,!0),_("setup",`Using port ${I} instead of ${Z}`,z),z)j.info(`Using port ${I} instead. Access your site at https://${w}:${I}`)}else if(I=await f.getNextAvailablePort(Z+1000,!0),_("setup",`Using port ${I} instead of ${Z}`,z),z)j.info(`Using port ${I} instead. Access your site at http://${w}:${I}`)}else I=Z,f.usedPorts.add(I),_("setup",`Using standard ${Z===443?"HTTPS":"HTTP"} port ${Z} for ${w}`,z);await XR(x,w,I,K,N,G,z,J,Y,jx(R.auth),S,R.loadBalancer)}catch(Z){_("setup",`Setup failed: ${Z}`,z),j.error(`Failed to setup reverse proxy: ${Z.message}`),p({domains:[w],hosts:typeof $==="boolean"?$:$?.hosts,certs:typeof $==="boolean"?$:$?.certs,verbose:z,vitePluginUsage:G})}}function ox(R,x=80,S=443,w,K,N){_("redirect",`Starting HTTP redirect server on port ${x}`,R);let z=Kx.createServer(($,G)=>{let Y=$.url?$.url.split("?",1)[0]:"";if(Y.startsWith("/.well-known/acme-challenge/")){if(K){let V=K.challengeStore.handlePath(Y);if(V!==void 0){_("redirect",`Serving on-demand ACME challenge ${Y}`,R),G.writeHead(200,{"content-type":"text/plain"}),G.end(V);return}}if(w){let V=fx(w,Y);if(V!=null){_("redirect",`Serving ACME challenge ${Y}`,R),G.writeHead(200,{"content-type":"text/plain"}),G.end(V);return}}if(K){G.writeHead(404,{"content-type":"text/plain"}),G.end("challenge not found");return}}let J=$.headers.host||"",B=J.includes(":")?J.slice(0,J.indexOf(":")):J;if(K&&B&&!K.hasCert(B)&&(!N||N(B)))K.ensureCert(B).catch(()=>{});let X=S===443?B:`${B}:${S}`;_("redirect",`Redirecting request from ${J}${$.url} to https://${X}`,R),G.writeHead(301,{Location:`https://${X}${$.url}`}),G.end()}).listen(x);b.add(z),_("redirect","HTTP redirect server started",R)}function $R(R){let x={...Sx,...R};if(_("proxy",`Starting proxy with options: ${q(x)}`,x?.verbose),x.viaDaemon){if(!x.from||!x.to){j.error("viaDaemon mode requires both `from` and `to`");return}g({proxies:[{id:x.id,from:x.from,to:x.to,path:x.path,cleanUrls:x.cleanUrls,changeOrigin:x.changeOrigin,pathRewrites:x.pathRewrites}],verbose:x.verbose}).catch((G)=>{j.error(`Failed to register with rpx daemon: ${G.message}`),Q.exit(1)});return}let S=x.to||"",w=S.split(".").pop()?.toLowerCase()||"",K=Q.platform==="darwin"&&S&&!S.includes("localhost")&&!S.includes("127.0.0.1"),N=["dev","app","page","new","day","foo"],z=["test","localhost","local","example","invalid"];if(K&&N.includes(w)&&x?.verbose)j.warn(`The .${w} TLD may not work reliably for local development`),j.info(` Google owns .${w} with HSTS preloading, which can bypass local DNS`),j.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(K)import("./chunk-vce43wgr.js").then(({setupDevelopmentDns:G})=>{G({domains:[S],verbose:x.verbose}).then((Y)=>{if(Y)Promise.resolve().then(()=>{if(x.verbose)if(z.includes(w))j.success(`DNS server started for .${w} domains`);else j.success(`DNS server started for .${w} domains (hosts file entry also added)`)});else _("dns",`Could not start DNS server - ${S} may not resolve in browser`,x.verbose)})}).catch((G)=>{_("dns",`Failed to start DNS server: ${G}`,x.verbose)});let $={from:x.from,to:x.to,cleanUrls:x.cleanUrls,https:wx(x),cleanup:x.cleanup,vitePluginUsage:x.vitePluginUsage,changeOrigin:x.changeOrigin,verbose:x.verbose,regenerateUntrustedCerts:x.regenerateUntrustedCerts};_("proxy",`Server options: ${q($)}`,x.verbose),Lx($).catch((G)=>{_("proxy",`Failed to start proxy: ${G}`,x.verbose),j.error(`Failed to start proxy: ${G.message}`),p({domains:[x.to],hosts:typeof x.cleanup==="boolean"?x.cleanup:x.cleanup?.hosts,certs:typeof x.cleanup==="boolean"?x.cleanup:x.cleanup?.certs,verbose:x.verbose})})}function BR(R){return R?.verbose||!1}function Ox(R){if(R?.hostsManagement===!1)return!1;let x=R?.cleanup;if(x===!1)return!1;if(x&&typeof x==="object"&&x.hosts===!1)return!1;return!0}async function hx(R){let x={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(R)x={...x,...R};let S=BR(x),w=Ox(x);if(_("config",`Starting with config: ${q(x,2)}`,S),_("config",`Is multi-proxy? ${"proxies"in x}`,S),_("config",`Hosts management enabled? ${w}`,S),x.viaDaemon){let D="proxies"in x&&Array.isArray(x.proxies)?x.proxies.map((k)=>({id:k.id,from:k.from,to:k.to,path:k.path,cleanUrls:k.cleanUrls??x.cleanUrls,changeOrigin:k.changeOrigin??x.changeOrigin,pathRewrites:k.pathRewrites})):[{id:x.id,from:x.from,to:x.to??"rpx.localhost",path:x.path,cleanUrls:x.cleanUrls,changeOrigin:x.changeOrigin,pathRewrites:x.pathRewrites}];await g({proxies:D,verbose:S});return}if("proxies"in x&&Array.isArray(x.proxies)){_("servers",`Found ${x.proxies.length} proxies in config`,S);for(let A of x.proxies)if(A.start){let D=`${A.from}-${A.to}`;try{_("watch",`Starting command for ${D} with command: ${A.start.command}`,S),j.info(`Starting command for ${D}...`),await o.startProcess(D,A.start,S);let k=u(A.from),H=new URL(k.startsWith("http")?k:`http://${k}`),W=H.hostname||"localhost",T=Number(H.port)||80;try{await a(W,T,S),_("watch",`Dev server is ready at ${W}:${T}`,S)}catch(Xx){_("watch",`Connection check failed, but continuing with proxy setup: ${Xx}`,S),j.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(k){throw _("watch",`Failed to start command for ${D}: ${k}`,S),Error(`Failed to start command for ${D}: ${k}`)}}else _("watch",`No start command for proxy ${A.from} -> ${A.to}`,S)}else if("start"in x&&x.start){_("watch","Found start command in single proxy config",S);let A=`${x.from}-${x.to}`;try{if(x.start)_("watch",`Starting command: ${x.start.command}`,S),await o.startProcess(A,x.start,S);let D=u(x.from),k=new URL(D.startsWith("http")?D:`http://${D}`),H=k.hostname||"localhost",W=Number(k.port)||80;try{await a(H,W,S),_("watch",`Dev server is ready at ${H}:${W}`,S)}catch(T){_("watch",`Connection check failed, but continuing with proxy setup: ${T}`,S),j.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(D){throw _("watch",`Failed to run start command: ${D}`,S),Error(`Failed to run start command: ${D}`)}}else _("watch","No start command found in config",S);let K="proxies"in x&&Array.isArray(x.proxies)?x.proxies[0]?.to:("to"in x)?x.to:"rpx.localhost";if(Q.platform!=="win32"&&(x.https||w)){if(!dx())try{_("sudo","Pre-acquiring sudo credentials for privileged operations",S),KR("sudo -v",{stdio:"inherit"})}catch{_("sudo","Could not pre-acquire sudo credentials",S)}}let N=[];if(x.productionCerts){if(N=await Ux(x.productionCerts,S),N.length>0)_("ssl",`Using ${N.length} production SNI cert(s): ${N.map((A)=>A.serverName).join(", ")}`,S)}if(x.https){let A=N.length>0?null:await c(x);if(!A&&N.length===0){if(_("ssl",`No valid or trusted certificates found for ${K}, generating new ones`,x.verbose),await _x(x),A=await c(x),!A)throw Error(`Failed to load SSL certificates after generation for ${K}`)}else _("ssl",`Using existing and trusted certificates for ${K}`,x.verbose);x._cachedSSLConfig=A}let z="proxies"in x&&Array.isArray(x.proxies)?x.proxies.map((A)=>({...A,https:x.https,cleanup:x.cleanup,cleanUrls:A.cleanUrls??("cleanUrls"in x?x.cleanUrls:!1),vitePluginUsage:x.vitePluginUsage,changeOrigin:A.changeOrigin??x.changeOrigin,verbose:S,_cachedSSLConfig:x._cachedSSLConfig})):[{from:"from"in x?x.from:"localhost:5173",to:"to"in x?x.to:"rpx.localhost",cleanUrls:"cleanUrls"in x?x.cleanUrls:!1,https:x.https,cleanup:x.cleanup,vitePluginUsage:x.vitePluginUsage,start:"start"in x?x.start:void 0,changeOrigin:x.changeOrigin,auth:"auth"in x?x.auth:void 0,verbose:S,_cachedSSLConfig:x._cachedSSLConfig}],$=z.map((A)=>A.to||"rpx.localhost"),G=N.length>0?N:x._cachedSSLConfig??null,Y=$.filter((A)=>A&&!A.includes("localhost")&&!A.includes("127.0.0.1")),J=["dev","app","page","new","day","foo"],B=["test","localhost","local","example","invalid"],X=[...new Set(Y.map((A)=>A.split(".").pop()?.toLowerCase()))],V=X.filter((A)=>!!A&&J.includes(A));if(V.length>0&&S)j.warn(`The following TLDs may not work reliably for local development: ${V.map((A)=>`.${A}`).join(", ")}`),j.info(" These TLDs have HSTS preloading which can bypass local DNS"),j.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(w&&Q.platform==="darwin"&&Y.length>0){let{setupDevelopmentDns:A}=await import("./chunk-vce43wgr.js");if(await A({domains:Y,verbose:S})){if(S)if(X.every((H)=>!!H&&B.includes(H)))j.success(`DNS server started for ${X.map((H)=>`.${H}`).join(", ")} domains`);else j.success(`DNS server started for ${X.map((H)=>`.${H}`).join(", ")} domains (hosts file entries also added)`)}else _("dns","Could not start DNS server - custom domains may not resolve",S)}let f=async()=>{_("cleanup","Starting cleanup handler",x.verbose);try{let{tearDownDevelopmentDns:A}=await import("./chunk-vce43wgr.js");await A({verbose:x.verbose})}catch(A){_("cleanup",`Error stopping DNS server: ${A}`,x.verbose)}try{await o.stopAll(x.verbose)}catch(A){_("cleanup",`Error stopping processes: ${A}`,x.verbose)}await p({domains:$,hosts:typeof x.cleanup==="boolean"?x.cleanup:x.cleanup?.hosts,certs:typeof x.cleanup==="boolean"?x.cleanup:x.cleanup?.certs,verbose:x.verbose||!1})};Q.on("SIGINT",f),Q.on("SIGTERM",f);let O=x.singlePortMode===!0,Z=x.httpsPort??443,M=x.httpPort??80,I=x.originGuard?Dx(x.originGuard):null,i=!!G&&(z.length>1||O||N.length>0),h=!G&&O&&z.length>0;if(i&&G){_("proxies",`Creating shared HTTPS server for ${z.length} domains on port ${Z}`,S);let A=await ix(z,w,S),D=null,k=x.onDemandTls,H=k?.enabled?new cx({config:k,certsDir:k.certsDir??x.productionCerts?.certsDir??rx.join(Gx.homedir(),".stacks","rpx","on-demand-certs"),initial:N,verbose:S,onCertAdded:(U)=>{if(tx()==="restart")_("on-demand","certificate installed; restarting supervised gateway to reload TLS",S),setTimeout(()=>Q.kill(Q.pid,"SIGTERM"),10).unref();else Xx(U)}}):null,W=null,T=!1;async function Xx(U){if(W=U,T)return;T=!0;try{while(W){let d=W;if(W=null,_("proxies",`rebuilding :${Z} with ${d.length} SNI cert(s)`,S),D)b.delete(D),D.stop(!1);let y=!1;for(let v=0;!y&&v<60;v++){let vx=xx({routeEntries:A,listenPort:Z,sslConfig:d,originGuard:I,verbose:S});if(vx){D=vx,y=!0;break}await new Promise((SR)=>setTimeout(SR,Math.min(25*2**Math.min(v,4),500)))}if(!y)j.error(`rpx: CRITICAL — could not rebind :${Z} after cert issuance; HTTPS unbound until the next cert event or a gateway restart`)}}finally{T=!1}}if(!await C(M,"0.0.0.0",S)){let U=new Set(A.map((y)=>y.host)),d=(y)=>U.has(y)||[...U].some((v)=>m(y,v));ox(S,M,Z,x.acmeChallengeWebroot,H,d)}if(await C(Z,"0.0.0.0",S)){if(_("proxies",`Port ${Z} is already in use, cannot start shared proxy`,S),S)j.warn(`Port ${Z} is in use. Shared HTTPS proxy cannot start.`);return}let RR=H&&H.sniEntries().length>0?H.sniEntries():G;if(D=xx({routeEntries:A,listenPort:Z,sslConfig:RR,originGuard:I,verbose:S}),!D){j.error(`Shared HTTPS proxy failed to bind :${Z}; not exiting`);return}}else if(h){_("proxies",`Creating shared HTTP server for ${z.length} domains on port ${M}`,S);let A=await ix(z,w,S);if(await C(M,"0.0.0.0",S)){if(_("proxies",`Port ${M} is already in use, cannot start shared proxy`,S),S)j.warn(`Port ${M} is in use. Shared HTTP proxy cannot start.`);return}if(!xx({routeEntries:A,listenPort:M,sslConfig:null,originGuard:I,verbose:S})){j.error(`Shared HTTP proxy failed to bind :${M}; not exiting`);return}}else for(let A of z)try{let D=A.to||"rpx.localhost";_("proxy",`Starting proxy for ${D} with SSL config: ${!!G}`,A.verbose),await Lx({from:A.from||"localhost:5173",to:D,cleanUrls:A.cleanUrls||!1,https:A.https||!1,cleanup:A.cleanup||!1,vitePluginUsage:A.vitePluginUsage||!1,verbose:A.verbose||!1,_cachedSSLConfig:x._cachedSSLConfig,changeOrigin:A.changeOrigin||!1,loadBalancer:A.loadBalancer,auth:A.auth,path:A.path,pathRewrites:A.pathRewrites})}catch(D){_("proxies",`Failed to start proxy for ${A.to}: ${D}`,A.verbose),j.error(`Failed to start proxy for ${A.to}:`,D)}}async function ix(R,x,S){let w=[],K=new Set;for(let N of R){let z=N.to||"rpx.localhost",$=N.cleanUrls||!1,G=N.path,Y=yx(G),J=jx(N.auth);if(N.redirect){let B=Fx(N.redirect);w.push({host:z,path:G,route:{redirect:B,basePath:Y,auth:J}}),_("proxies",`Route: ${z}${G??""} → redirect ${B.status} ${B.to}${J?" (auth)":""}`,S)}else if(N.static)w.push({host:z,path:G,route:{static:Mx(N.static,$),cleanUrls:$,basePath:Y,auth:J}}),_("proxies",`Route: ${z}${G??""} → static ${typeof N.static==="string"?N.static:N.static.dir}${J?" (auth)":""}`,S);else{let B=u(N.from),X=new URL(B.startsWith("http")?B:`http://${B}`),V=zx(N.from??X.host,N.loadBalancer);Ax(V),Rx.add(V),w.push({host:z,path:G,route:{sourceHost:X.host,upstreamPool:V,cleanUrls:$,changeOrigin:N.changeOrigin||!1,pathRewrites:N.pathRewrites,basePath:Y,auth:J}}),_("proxies",`Route: ${z}${G??""} → ${X.host}${J?" (auth)":""}`,S)}if(K.has(z))continue;if(K.add(z),x&&!Tx(z)&&!z.includes("localhost")&&!z.includes("127.0.0.1"))try{if(!(await L([z],S))[0])await P([z],S)}catch{_("hosts",`Could not add hosts entry for ${z}`,S)}}return w}var l=null;function xR(){if(Q.env.RPX_BIND_HOSTNAME)return Q.env.RPX_BIND_HOSTNAME;if(l)return l;try{l=Object.values(Gx.networkInterfaces()).flat().some((S)=>S&&S.family==="IPv6")?"::":"0.0.0.0"}catch{l="0.0.0.0"}return l}function xx(R){let{routeEntries:x,listenPort:S,sslConfig:w,originGuard:K,verbose:N}=R,z=qx(x),$=Wx((J,B)=>Cx(z,J,B),N),G=K?(J,B)=>K(J)??$(J,B):$,Y=Ex(N);try{let J=Bun.serve({port:S,hostname:xR(),reusePort:nx(),...w?{tls:gx(Array.isArray(w)?w.map((B)=>({serverName:B.serverName,key:B.key,cert:B.cert})):{key:w.key,cert:w.cert,ca:w.ca,requestCert:!1,rejectUnauthorized:!1})}:{},fetch(B,X){return G(B,X)},websocket:Y,error(B){return _("server",`Shared proxy server error: ${B}`,N),new Response(`Server Error: ${B.message}`,{status:500})}});return b.add(J),_("proxies",`Shared ${w?"HTTPS":"HTTP"} proxy listening on port ${S} for ${z.size} domains`,N),J}catch(J){return _("proxies",`Failed to start shared proxy: ${J}`,N),console.error("Failed to start shared proxy:",J),null}}function JR(R){if(R?.vitePluginUsage||!R?.verbose)return;if(console.log(""),console.log(` ${F.green(F.bold("rpx"))} ${F.green(`v${px}`)}`),console.log(` ${F.green("➜")} ${F.dim(R?.from??"")} ${F.dim("➜")} ${F.cyan(R?.ssl?`https://${R?.to}`:`http://${R?.to}`)}`),R?.listenPort!==(R?.ssl?443:80))console.log(` ${F.green("➜")} Listening on port ${R?.listenPort}`);if(R?.cleanUrls)console.log(` ${F.green("➜")} Clean URLs enabled`)}var yS=hx;export{Bx as writeEntry,Q6 as watchRegistry,kR as verifyHttpsChain,CR as trustRootCaForBrowsers,n6 as tearDownDevelopmentDns,d6 as syncDevelopmentDnsFromRegistry,rR as stripBasePath,Nx as stopHealthChecks,v6 as stopDnsServer,w4 as stopDaemon,Lx as startServer,$R as startProxy,hx as startProxies,Ax as startHealthChecks,h6 as startDnsServer,A6 as siteIdForHost,nx as shouldReusePort,a6 as setupResolver,i6 as setupDevelopmentDns,S6 as serverNameFromCertFilename,sR as serveStaticFile,lR as selectUpstream,q as safeStringify,gR as safeRelativePath,bS as safeDeleteFile,g as runViaDaemon,S4 as runDaemon,l6 as resolverFilePath,u6 as resolverBasenamesForDomains,c6 as resolverBasenameForDomain,Mx as resolveStaticRoute,tR as resolveStaticFile,Fx as resolveRedirect,lS as resolvePathRewrite,jx as resolveAuth,H6 as renderStartingPage,I6 as renderFailedPage,y6 as removeStaleRpxHosts,g6 as removeResolver,p6 as removeLegacyTldResolvers,Px as removeHosts,Jx as removeEntry,R4 as releaseDaemonLock,cS as redactSensitive,t6 as reconcileStaleDevelopmentDns,z4 as reconcileDevelopmentDnsOnIdle,j6 as readSiteManifest,J6 as readEntry,e6 as readDaemonPid,DR as readCertSha256Fingerprint,QR as readCertCommonName,V6 as readAll,fx as readAcmeChallenge,TR as pruneStaleRootCas,N6 as projectNameFromHost,u as primaryUpstreamUrl,$6 as portManager,x6 as pathPrefixMatches,HR as parseSha256HashesFromSecurityListing,pR as parseHtpasswd,M6 as parseHostsLine,VR as normalizeSha256Fingerprint,yx as normalizePathPrefix,U6 as normalizeDevDomain,m as matchesWildcard,_6 as matchesAllowedSuffix,Cx as matchHostRoute,R6 as matchHostList,oR as matchHost,bR as markSuccess,aR as markFailure,OR as loadSSLConfig,X6 as listDiscoverableSites,fR as listCertSha256HashesByCommonName,Tx as isWildcardPattern,PS as isValidRootCA,$x as isValidId,vS as isSingleProxyOptions,mS as isSingleProxyConfig,yR as isRootCaTrustedForSsl,qR as isRootCaFingerprintInKeychains,qS as isProcessElevated,C as isPortInUse,B6 as isPidAlive,hS as isMultiProxyOptions,OS as isMultiProxyConfig,w6 as isLikelyHostname,m6 as isDnsServerRunning,o6 as isDaemonRunning,mR as isCertTrusted,wx as httpsConfig,W6 as hostsLineMapsHost,dx as getSudoPassword,LR as getSharedDaemonCertPaths,PR as getRootCAPaths,Yx as getRegistryDir,LS as getPrimaryDomain,ER as getMacosTrustKeychains,WR as getMacosLoginKeychainPath,s6 as getDaemonRpxDir,r6 as getDaemonPidPath,_x as generateCertificate,D6 as gcStaleEntries,hR as forceTrustCertificate,T6 as findStaleRpxHosts,Y6 as findAvailablePort,E6 as filterRpxHostsEntries,uS as extractHostname,z6 as expandHome,US as execSudoSync,k6 as escapeHtml,Vx as ensureDaemonRunning,iR as enforceBasicAuth,f6 as dropStaleRpxHostsLines,cR as devSslToSniEntries,P6 as devDomainsFromHosts,K6 as detectProjectPreset,bx as deriveIdFromTarget,_4 as defaultDaemonSpawnCommand,Sx as defaultConfig,yS as default,_ as debugLog,zx as createUpstreamPool,G6 as createSiteResolver,Ex as createProxyWebSocketHandler,Wx as createProxyFetchHandler,Dx as createOriginGuard,nR as contentTypeFor,b6 as contentLooksLikeRpxResolver,Sx as config,F as colors,vR as clearSslConfigCache,Ix as cleanupCertificates,p as cleanup,L as checkHosts,c as checkExistingCertificates,ZR as certIncludesSanHostnames,Ux as buildSniTlsConfig,uR as buildRegistryTlsProxyOptions,dR as buildRedirectLocation,qx as buildHostRoutes,CS as authorizeSystemAccess,P as addHosts,x4 as acquireDaemonLock,Z6 as SiteSupervisor,UR as SHARED_DEV_HOST_CERT_PATH,MR as RPX_ROOT_CA_COMMON_NAME,O6 as RPX_RESOLVER_MARKER,F6 as RPX_HOSTS_MARKER,cx as OnDemandCertManager,FR as MACOS_SYSTEM_KEYCHAIN,IR as MACOS_CA_TRUST_FLAGS,C6 as LEGACY_TLD_RESOLVER_LABELS,ux as DefaultPortManager,q6 as DNS_STATE_VERSION,L6 as DNS_PORT,eR as ACME_CHALLENGE_PREFIX};
1
+ import{$ as De,$a as Qr,A as se,Aa as Fr,Ab as vo,B as nr,Ba as Hr,Bb as Co,C as k,Ca as Ne,Cb as $o,D as ie,Da as Ur,Db as To,E as Ce,Ea as _r,Eb as Ro,F as sr,Fa as kr,Fb as Oo,G as M,Ga as Mr,Gb as Do,H as ae,Ha as Nr,Hb as Eo,I as ir,Ia as Gr,Ib as Lo,J as ar,Ja as Br,Jb as Ao,K as lr,Ka as U,Kb as Fo,L as le,La as Ir,Lb as Ho,M as ce,Ma as Ge,Mb as ye,N as cr,Na as jr,Nb as Uo,O as pe,Oa as me,Ob as _o,P as pr,Pa as ge,Q as $e,Qa as Wr,R as ur,Ra as he,S as Te,Sa as xe,T as dr,Ta as zr,U as fr,Ua as Xr,V as mr,Va as Yr,W as gr,Wa as qr,X as hr,Xa as Vr,Y as Re,Ya as Kr,Z as Oe,Za as Jr,_ as xr,_a as Zr,a as u,aa as Ee,ab as eo,b as ne,ba as I,bb as to,c as Ht,ca as yr,cb as ro,d as Ut,da as Le,db as oo,e as _t,ea as wr,eb as N,f as kt,fa as Ae,fb as Be,g as Mt,ga as Pr,gb as G,h as Nt,ha as Fe,hb as no,i as Gt,ia as Sr,ib as so,j as Bt,ja as br,jb as io,k as It,ka as He,kb as ao,l as jt,la as Ue,lb as lo,m as Wt,ma as vr,mb as co,n as zt,na as _e,nb as po,o as Xt,oa as tt,ob as uo,p as Yt,pa as Cr,pb as fo,q as qt,qa as $r,qb as mo,r as Vt,ra as ke,rb as go,s as Kt,sa as Tr,sb as ho,t as Jt,ta as Rr,tb as xo,u as Zt,ua as Or,v as Qt,va as Dr,vb as yo,w as er,wa as Er,wb as wo,x as tr,xa as Lr,xb as Po,y as rr,ya as Me,yb as So,z as or,za as Ar,zb as bo}from"./chunk-3tg7caak.js";import{$b as Ss,Qb as ve,Rb as Qe,Sb as gs,Tb as hs,Ub as xs,Vb as o,Wb as et,Xb as ys,Yb as F,Zb as ws,_b as Ps,ac as bs,bc as vs,cc as Cs,dc as $s,ec as Ts,fc as Rs}from"./chunk-pcyc92vj.js";import{execSync as vt}from"node:child_process";import*as ue from"node:http";import*as rt from"node:net";import*as de from"node:os";import*as ot from"node:path";import*as y from"node:process";var V=(t,e)=>(r)=>`\x1B[${t}m${r}\x1B[${e}m`,R={bold:V(1,22),dim:V(2,22),green:V(32,39),cyan:V(36,39)};import*as Ye from"node:fs";import*as qe from"node:path";import*as E from"node:process";function Ve(t,e){let n=(e&&e!=="/"?`${t}${e}`:t).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return n.length>0?n:"rpx"}async function K(t){if(t.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let e=t.verbose??!1,r=t.registryDir,n=new Set,l=t.proxies.map((p)=>{let g=p.id??Ve(p.to,p.path);if(!ge(g))throw Error(`invalid registry id "${g}" derived from to="${p.to}"`);if(n.has(g))throw Error(`duplicate registry id "${g}" — set an explicit \`id\` on one of the proxies`);return n.add(g),{...p,id:g}}),a=new Date().toISOString();for(let p of l)await he({id:p.id,from:p.from,to:p.to,path:p.path,pid:t.persistent?void 0:E.pid,cwd:E.cwd(),createdAt:a,cleanUrls:p.cleanUrls,changeOrigin:p.changeOrigin,pathRewrites:p.pathRewrites,static:p.static,loadBalancer:p.loadBalancer},r,e);let s=await ye({rpxDir:t.rpxDir,verbose:e,spawnCommand:t.spawnCommand,startupTimeoutMs:t.startupTimeoutMs,spawnEnv:t.spawnEnv});for(let p of l){let g=p.static?`static ${typeof p.static==="string"?p.static:p.static.dir}`:p.from;u.success(`https://${p.to} → ${g}`)}if(u.info(`(via rpx daemon pid=${s.pid}; \`rpx daemon:status\` to inspect)`),t.detached)return;let f=!1,c=r??me(),d=l.map((p)=>p.id),h=async()=>{if(f)return;f=!0;for(let p of d)await xe(p,r,e).catch((g)=>{o("runner",`removeEntry(${p}) failed: ${g}`,e)})},m=(p)=>{o("runner",`received ${p}, unregistering ${d.length} entries`,e),h().finally(()=>E.exit(0))};E.once("SIGINT",m),E.once("SIGTERM",m),E.once("exit",()=>{if(f)return;for(let p of d)try{Ye.unlinkSync(qe.join(c,`${p}.json`))}catch{}}),await new Promise(()=>{})}import{spawn as xt}from"node:child_process";import*as J from"node:process";class Z{processes=new Map;isShuttingDown=!1;async startProcess(t,e,r){if(this.processes.has(t)){o("start",`Process ${t} is already running`,r);return}let[n,...l]=e.command.split(" "),a=e.cwd||J.cwd();o("start",`Starting process ${t}:`,r),o("start",` Command: ${n} ${l.join(" ")}`,r),o("start",` Working directory: ${a}`,r),o("start",` Environment variables: ${F(e.env)}`,r);let s=xt(n,l,{cwd:a,env:{...J.env,...e.env},shell:!0,stdio:"inherit"});return this.processes.set(t,{command:e.command,cwd:a,process:s,env:e.env}),new Promise((f,c)=>{if(s.on("error",(d)=>{if(!this.isShuttingDown)o("start",`Process ${t} failed to start: ${d}`,r),this.processes.delete(t),c(d)}),s.on("exit",(d)=>{if(!this.isShuttingDown&&d!==null&&d!==0)o("start",`Process ${t} exited with code ${d}; leaving the proxy running`,r),this.processes.delete(t),c(Error(`Process ${t} exited with code ${d}`))}),r)s.stdout?.on("data",(d)=>{o("process",`[${t}] ${d.toString().trim()}`,!0)}),s.stderr?.on("data",(d)=>{o("process",`[${t}] ERR: ${d.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&s.killed)this.processes.delete(t),c(Error(`Process ${t} was killed during startup`));else o("start",`Process ${t} started successfully`,r),f()},1000)})}async stopProcess(t,e){let r=this.processes.get(t);if(!r?.process){o("start",`No process found for ${t}`,e);return}return o("start",`Stopping process ${t}`,e),new Promise((n)=>{if(!r.process){n();return}r.process.once("exit",()=>{this.processes.delete(t),o("start",`Process ${t} stopped`,e),n()});try{r.process.kill("SIGTERM"),setTimeout(()=>{if(r.process){o("start",`Force killing process ${t}`,e);try{r.process.kill("SIGKILL")}catch(l){}}},3000)}catch(l){o("start",`Error stopping process ${t}: ${l}`,e),this.processes.delete(t),n()}})}async stopAll(t){if(this.isShuttingDown){o("start","Already shutting down, skipping duplicate stopAll call",t);return}this.isShuttingDown=!0,o("start","Stopping all processes",t);let e=Array.from(this.processes.keys()).map((r)=>this.stopProcess(r,t).catch((n)=>{u.error(`Failed to stop process ${r}:`,n)}));await Promise.allSettled(e),this.processes.clear(),this.isShuttingDown=!1}isRunning(t){let e=this.processes.get(t);return!!e?.process&&!e.process.killed}}var qo=new Z;import{timingSafeEqual as yt}from"node:crypto";function wt(t,e){if(t==null)return!1;let r=Buffer.from(t),n=Buffer.from(e);if(r.length!==n.length)return!1;return yt(r,n)}function Q(t){return t.toLowerCase().replace(/\.$/,"")}var Pt=["/.well-known/acme-challenge/"];function St(t){let e=t.headers.get("host");if(e)return Q(e.split(":")[0]);try{return Q(new URL(t.url).hostname)}catch{return""}}function we(t){let e=t.header.toLowerCase(),r=new Set,n=[];for(let c of t.hosts){let d=Q(c);if(d.startsWith("*."))n.push(d);else r.add(d)}let l=t.exemptPaths??Pt,a=t.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
2
+ `,s=(c)=>{let d=Q(c);return r.has(d)||n.some((h)=>I(d,h))},f=(c)=>{let d=St(c);if(!s(d))return;let h="/";try{h=new URL(c.url).pathname}catch{}if(l.some((m)=>h.startsWith(m)))return;if(wt(c.headers.get(e),t.value))return;return new Response(a,{status:403,headers:{"content-type":"text/plain"}})};return f.protects=s,f}var Ke={name:"@stacksjs/rpx",type:"module",version:"0.11.47",description:"A modern and smart reverse proxy.",author:"Chris Breuer <chris@stacksjs.org>",license:"MIT",homepage:"https://github.com/stacksjs/rpx",repository:{type:"git",url:"git+https://github.com/stacksjs/rpx.git"},bugs:{url:"https://github.com/stacksjs/rpx/issues"},keywords:["reverse proxy","ssl","development","environment","proxy","bun","stacks","typescript","javascript"],exports:{".":{types:"./dist/index.d.ts",bun:"./dist/index.js",import:"./dist/index.js"}},module:"./dist/index.js",types:"./dist/index.d.ts",bin:{rpx:"./dist/bin/cli.js","reverse-proxy":"./dist/bin/cli.js"},files:["README.md","dist"],scripts:{build:"bun build.ts && bun build --production ./bin/cli.ts --compile --outfile bin/rpx",compile:"bun build --production ./bin/cli.ts --compile --outfile bin/rpx","compile:all":"bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:windows-x64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64","compile:linux-x64":"bun build --production ./bin/cli.ts --compile --target=bun-linux-x64 --outfile bin/rpx-linux-x64","compile:linux-arm64":"bun build --production ./bin/cli.ts --compile --target=bun-linux-arm64 --outfile bin/rpx-linux-arm64","compile:windows-x64":"bun build --production ./bin/cli.ts --compile --target=bun-windows-x64 --outfile bin/rpx-windows-x64.exe","compile:darwin-x64":"bun build --production ./bin/cli.ts --compile --target=bun-darwin-x64 --outfile bin/rpx-darwin-x64","compile:darwin-arm64":"bun build --production ./bin/cli.ts --compile --target=bun-darwin-arm64 --outfile bin/rpx-darwin-arm64",bench:"bun run bench/run.ts","bench:html":"bun run bench/run.ts --html","bench:latency":"bun run bench/run.ts --latency","bench:throughput":"bun run bench/run.ts --throughput",lint:"bunx --bun pickier .","lint:fix":"bunx --bun pickier . --fix",fresh:"bunx rimraf node_modules/ bun.lock && bun i",changelog:"changelogen --output CHANGELOG.md",prepublishOnly:"bun build.ts","release:binaries":"bun run compile:all && bun run zip",test:"bun test",typecheck:"bunx tsc --noEmit",zip:"bun run zip:all","zip:all":"bun run zip:linux-x64 && bun run zip:linux-arm64 && bun run zip:windows-x64 && bun run zip:darwin-x64 && bun run zip:darwin-arm64","zip:linux-x64":"zip -j bin/rpx-linux-x64.zip bin/rpx-linux-x64","zip:linux-arm64":"zip -j bin/rpx-linux-arm64.zip bin/rpx-linux-arm64","zip:windows-x64":"zip -j bin/rpx-windows-x64.zip bin/rpx-windows-x64.exe","zip:darwin-x64":"zip -j bin/rpx-darwin-x64.zip bin/rpx-darwin-x64","zip:darwin-arm64":"zip -j bin/rpx-darwin-arm64.zip bin/rpx-darwin-arm64"},dependencies:{"@stacksjs/clapp":"^0.2.10","@stacksjs/tlsx":"^0.13.19"},devDependencies:{bunfig:"^0.15.6",mitata:"^1.0.34",typescript:"^7.0.2"},"simple-git-hooks":{"pre-commit":"bunx lint-staged"},"lint-staged":{"*.{js,ts}":"bunx --bun pickier . --fix"}};var Je=Ke.version;var te=new Z,Ct=new Ge("0.0.0.0"),W=new Set,oe=new Set,Pe=!1,ee=null,Se=null;async function X(t){if(Pe)return o("cleanup","Cleanup already in progress, skipping",t?.verbose),Se||Promise.resolve();Pe=!0,o("cleanup","Starting cleanup process",t?.verbose),Se=new Promise((e)=>{ee=e});try{await te.stopAll(t?.verbose),u.info("Shutting down proxy servers...");let e=[],r=Array.from(W).map((n)=>new Promise((l)=>{let a=n;try{if(typeof a.stop==="function")a.stop(!0),o("cleanup","Bun server stopped",t?.verbose),l();else if(typeof a.close==="function")a.close(()=>{o("cleanup","Server closed successfully",t?.verbose),l()});else l()}catch(s){o("cleanup",`Error stopping server: ${s}`,t?.verbose),l()}}));e.push(...r),W.clear();for(let n of oe)ce(n);if(oe.clear(),t?.hosts&&t.domains?.length){o("cleanup","Cleaning up hosts file entries",t?.verbose),o("cleanup",`Original domains for cleanup: ${JSON.stringify(t.domains)}`,t?.verbose);let n=t.domains.filter((l)=>{if(l==="test.local")return!0;return l!=="localhost"&&!l.startsWith("localhost.")&&l!=="127.0.0.1"});if(o("cleanup",`Filtered domains for cleanup: ${JSON.stringify(n)}`,t?.verbose),n.length>0)u.info("Cleaning up hosts file entries..."),e.push(Be(n,t?.verbose).then(()=>{o("cleanup",`Removed hosts entries for ${n.join(", ")}`,t?.verbose)}).catch((l)=>{o("cleanup",`Failed to remove hosts entries: ${l}`,t?.verbose),u.warn(`Failed to clean up hosts file entries for ${n.join(", ")}:`,l)}))}if(t?.certs&&t.domains?.length){o("cleanup","Cleaning up SSL certificates",t?.verbose),u.info("Cleaning up SSL certificates...");let n=t.domains.map(async(l)=>{try{await Ce(l,t?.verbose),o("cleanup",`Removed certificates for ${l}`,t?.verbose)}catch(a){o("cleanup",`Failed to remove certificates for ${l}: ${a}`,t?.verbose),u.warn(`Failed to clean up certificates for ${l}:`,a)}});e.push(...n)}await Promise.allSettled(e),o("cleanup","All cleanup tasks completed successfully",t?.verbose),u.success("All cleanup tasks completed successfully")}catch(e){o("cleanup",`Error during cleanup: ${e}`,t?.verbose),u.error("Error during cleanup:",e)}finally{if(ee)ee();ee=null,Pe=!1;let e=t&&"vitePluginUsage"in t&&t.vitePluginUsage===!0;if(y.env.NODE_ENV!=="test"&&y.env.BUN_ENV!=="test"&&!e)y.exit(0)}return Se}var be=!1;function nt(t){if(be){o("signal",`Received second ${t} signal, forcing exit`,!0),y.exit(1);return}be=!0,o("signal",`Received ${t} signal, initiating cleanup`,!0),X().catch((e)=>{o("signal",`Cleanup failed after ${t}: ${e}`,!0),y.exit(1)}).finally(()=>{be=!1})}y.once("SIGINT",()=>nt("SIGINT"));y.once("SIGTERM",()=>nt("SIGTERM"));y.on("uncaughtException",(t)=>{u.error("Uncaught exception (continuing):",t)});y.on("unhandledRejection",(t)=>{u.error("Unhandled rejection (continuing):",t)});async function z(t,e,r,n=5){o("connection",`Testing connection to ${t}:${e} (retries left: ${n})`,r);let l=15000,a=Date.now();if(y.env.RPX_BYPASS_CONNECTION_TEST==="true"){o("connection",`Bypassing connection test for ${t}:${e} due to RPX_BYPASS_CONNECTION_TEST flag`,r);return}let s=()=>new Promise((f,c)=>{let d=rt.connect({host:t,port:e,timeout:3000});d.once("connect",()=>{o("connection",`Successfully connected to ${t}:${e}`,r),d.end(),f()}),d.once("timeout",()=>{o("connection",`Connection to ${t}:${e} timed out`,r),d.destroy(),c(Error("Connection timed out"))}),d.once("error",(h)=>{o("connection",`Failed to connect to ${t}:${e}: ${h}`,r),d.destroy(),c(h)})});try{await s()}catch(f){let c=f;if(Date.now()-a>l){o("connection",`Connection test timed out after ${l}ms, but continuing anyway`,r),u.warn(`Connection test to ${t}:${e} timed out, but RPX will try to proceed anyway.`);return}if(c.code==="ECONNREFUSED"&&n>0)return o("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${n} retries left)`,r),await new Promise((h)=>setTimeout(h,2000)),z(t,e,r,n-1);if(n>0)try{o("connection",`Trying HTTP request to ${t}:${e}`,r),await new Promise((h,m)=>{let p=ue.request({hostname:t,port:e,path:"/",method:"HEAD",timeout:5000},(g)=>{o("connection",`Received HTTP response with status: ${g.statusCode}`,r),h()});p.on("error",(g)=>m(g)),p.on("timeout",()=>{p.destroy(),m(Error("HTTP request timed out"))}),p.end()}),o("connection",`HTTP request to ${t}:${e} succeeded`,r);return}catch(h){return o("connection",`HTTP request to ${t}:${e} failed: ${h}`,r),o("connection",`Retrying socket connection in 2 seconds... (${n} retries left)`,r),await new Promise((m)=>setTimeout(m,2000)),z(t,e,r,n-1)}let d=`Failed to connect to ${t}:${e} after ${5-n} attempts: ${c.message}`;o("connection",`${d}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,r),u.warn(d),u.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 Ie(t){o("server",`Starting server with options: ${F(t)}`,t.verbose);let e=M(t.from),r=new URL(e.startsWith("http")?e:`http://${e}`),n=new URL((t.to?.startsWith("http")?t.to:`http://${t.to}`)||"rpx.localhost"),l=Number.parseInt(r.port)||(r.protocol.includes("https:")?443:80),a=[n.hostname];if(je(t)&&!n.hostname.includes("localhost")&&!n.hostname.includes("127.0.0.1")){o("hosts",`Checking if hosts file entry exists for: ${n.hostname}`,t?.verbose);try{if(!(await G(a,t.verbose))[0]){u.info(`Adding ${n.hostname} to hosts file...`),u.info("This may require sudo/administrator privileges");try{await N(a,t.verbose)}catch(c){if(u.error("Failed to add hosts entry:",c.message),u.warn("You can manually add this entry to your hosts file:"),u.warn(`127.0.0.1 ${n.hostname}`),u.warn(`::1 ${n.hostname}`),y.platform==="win32")u.warn("On Windows:"),u.warn("1. Run notepad as administrator"),u.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else u.warn("On Unix systems:"),u.warn("sudo nano /etc/hosts")}}else o("hosts",`Host entry already exists for ${n.hostname}`,t.verbose)}catch(f){u.error("Failed to check hosts file:",f.message)}}try{await z(r.hostname,l,t.verbose)}catch(f){o("server",`Connection test failed: ${f}`,t.verbose),u.error(f.message),u.warn("Continuing with proxy setup despite connection test failure..."),u.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let s=t._cachedSSLConfig||null;if(t.https)try{if(t.https===!0)t.https=ie({...t,to:n.hostname});if(s=await k({...t,to:n.hostname,https:t.https}),!s){if(o("ssl",`Generating new certificates for ${n.hostname}`,t.verbose),await se({...t,from:r.toString(),to:n.hostname,https:t.https}),s=await k({...t,to:n.hostname,https:t.https}),!s)throw Error(`Failed to load SSL configuration after generating certificates for ${n.hostname}`)}}catch(f){throw o("server",`SSL setup failed: ${f}`,t.verbose),f}o("server",`Setting up reverse proxy with SSL config for ${n.hostname}`,t.verbose),await Tt({...t,from:e,originalFrom:t.from||e,to:n.hostname,fromPort:l,sourceUrl:{hostname:r.hostname,host:r.host},ssl:s})}async function $t(t,e,r,n,l,a,s,f,c,d,h,m){o("proxy",`Creating proxy server ${t} -> ${e} with cleanUrls: ${f}`,s);let p=ae(h??n.host,m);le(p);let g=[{host:e,route:{sourceHost:n.host,upstreamPool:p,cleanUrls:f||!1,changeOrigin:c||!1,basePath:"/",auth:d}}];if(!re({routeEntries:g,listenPort:r,sslConfig:l,originGuard:null,verbose:s??!1}))throw ce(p),Error(`Failed to start proxy server for ${e} on port ${r}`);oe.add(p),Dt({from:t,to:e,vitePluginUsage:a,listenPort:r,ssl:!!l,cleanUrls:f,verbose:s})}async function Tt(t){o("setup",`Setting up reverse proxy: ${F(t)}`,t.verbose);let{from:e,originalFrom:r,to:n,sourceUrl:l,ssl:a,verbose:s,cleanup:f,vitePluginUsage:c,changeOrigin:d,cleanUrls:h}=t,m=t.httpPort??80,p=t.httpsPort??443,g=it(),v=t.portManager||Ct,S=je(t);try{if(S&&n&&!n.includes("localhost")&&!n.includes("127.0.0.1")){if(!(await G([n],s))[0]){u.warn(`The hostname ${n} isn't in your hosts file. Adding it now...`);try{await N([n],s),u.success(`Added ${n} to your hosts file.`)}catch(H){u.error(`Failed to add ${n} to your hosts file: ${H}`),u.info(`You may need to manually add '127.0.0.1 ${n}' to your /etc/hosts file.`)}}}else if(S&&y.platform!=="darwin"&&n&&n.includes("localhost")&&!n.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await G([n],s))[0]){o("hosts",`${n} not found in hosts file, adding...`,s);try{await N([n],s)}catch(H){o("hosts",`Failed to add ${n} to hosts file: ${H}`,s)}}}if(a&&!v.usedPorts.has(m)){if(!await U(m,g,s))o("setup","Starting HTTP redirect server",s),st(s,m,p),v.usedPorts.add(m);else if(o("setup","Port 80 is in use, skipping HTTP redirect",s),s)u.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let P=a?p:m,O=await U(P,g,s),b;if(O){if(o("setup",`Port ${P} is already in use`,s),s)u.warn(`Port ${P} is already in use. This may be another instance of rpx or another service.`);if(P===443){if(b=await v.getNextAvailablePort(3443,!0),o("setup",`Using port ${b} instead of ${P}`,s),s)u.info(`Using port ${b} instead. Access your site at https://${n}:${b}`)}else if(b=await v.getNextAvailablePort(P+1000,!0),o("setup",`Using port ${b} instead of ${P}`,s),s)u.info(`Using port ${b} instead. Access your site at http://${n}:${b}`)}else b=P,v.usedPorts.add(b),o("setup",`Using standard ${P===443?"HTTPS":"HTTP"} port ${P} for ${n}`,s);await $t(e,n,b,l,a,c,s,h,d,pe(t.auth),r,t.loadBalancer)}catch(P){o("setup",`Setup failed: ${P}`,s),u.error(`Failed to setup reverse proxy: ${P.message}`),X({domains:[n],hosts:typeof f==="boolean"?f:f?.hosts,certs:typeof f==="boolean"?f:f?.certs,verbose:s,vitePluginUsage:c})}}function st(t,e=80,r=443,n,l,a){o("redirect",`Starting HTTP redirect server on port ${e}`,t);let s=ue.createServer((f,c)=>{let d=f.url?f.url.split("?",1)[0]:"";if(d.startsWith("/.well-known/acme-challenge/")){if(l){let g=l.challengeStore.handlePath(d);if(g!==void 0){o("redirect",`Serving on-demand ACME challenge ${d}`,t),c.writeHead(200,{"content-type":"text/plain"}),c.end(g);return}}if(n){let g=De(n,d);if(g!=null){o("redirect",`Serving ACME challenge ${d}`,t),c.writeHead(200,{"content-type":"text/plain"}),c.end(g);return}}if(l){c.writeHead(404,{"content-type":"text/plain"}),c.end("challenge not found");return}}let h=f.headers.host||"",m=h.includes(":")?h.slice(0,h.indexOf(":")):h;if(l&&m&&!l.hasCert(m)&&(!a||a(m)))l.ensureCert(m).catch(()=>{});let p=r===443?m:`${m}:${r}`;o("redirect",`Redirecting request from ${h}${f.url} to https://${p}`,t),c.writeHead(301,{Location:`https://${p}${f.url}`}),c.end()}).listen(e);W.add(s),o("redirect","HTTP redirect server started",t)}function Rt(t){let e={...ne,...t};if(o("proxy",`Starting proxy with options: ${F(e)}`,e?.verbose),e.viaDaemon){if(!e.from||!e.to){u.error("viaDaemon mode requires both `from` and `to`");return}K({proxies:[{id:e.id,from:e.from,to:e.to,path:e.path,cleanUrls:e.cleanUrls,changeOrigin:e.changeOrigin,pathRewrites:e.pathRewrites}],verbose:e.verbose}).catch((c)=>{u.error(`Failed to register with rpx daemon: ${c.message}`),y.exit(1)});return}let r=e.to||"",n=r.split(".").pop()?.toLowerCase()||"",l=y.platform==="darwin"&&r&&!r.includes("localhost")&&!r.includes("127.0.0.1"),a=["dev","app","page","new","day","foo"],s=["test","localhost","local","example","invalid"];if(l&&a.includes(n)&&e?.verbose)u.warn(`The .${n} TLD may not work reliably for local development`),u.info(` Google owns .${n} with HSTS preloading, which can bypass local DNS`),u.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(l)import("./chunk-handw2xw.js").then(({setupDevelopmentDns:c})=>{c({domains:[r],verbose:e.verbose}).then((d)=>{if(d)Promise.resolve().then(()=>{if(e.verbose)if(s.includes(n))u.success(`DNS server started for .${n} domains`);else u.success(`DNS server started for .${n} domains (hosts file entry also added)`)});else o("dns",`Could not start DNS server - ${r} may not resolve in browser`,e.verbose)})}).catch((c)=>{o("dns",`Failed to start DNS server: ${c}`,e.verbose)});let f={from:e.from,to:e.to,cleanUrls:e.cleanUrls,https:ie(e),cleanup:e.cleanup,vitePluginUsage:e.vitePluginUsage,changeOrigin:e.changeOrigin,verbose:e.verbose,regenerateUntrustedCerts:e.regenerateUntrustedCerts};o("proxy",`Server options: ${F(f)}`,e.verbose),Ie(f).catch((c)=>{o("proxy",`Failed to start proxy: ${c}`,e.verbose),u.error(`Failed to start proxy: ${c.message}`),X({domains:[e.to],hosts:typeof e.cleanup==="boolean"?e.cleanup:e.cleanup?.hosts,certs:typeof e.cleanup==="boolean"?e.cleanup:e.cleanup?.certs,verbose:e.verbose})})}function Ot(t){return t?.verbose||!1}function je(t){if(t?.hostsManagement===!1)return!1;let e=t?.cleanup;if(e===!1)return!1;if(e&&typeof e==="object"&&e.hosts===!1)return!1;return!0}async function Y(t){let e={from:"localhost:5173",to:"rpx.localhost",https:!1,cleanup:{hosts:!0,certs:!1},vitePluginUsage:!1,verbose:!1,cleanUrls:!1,changeOrigin:!1,regenerateUntrustedCerts:!0};if(t)e={...e,...t};let r=Ot(e),n=e.https===!1;if(e.localCa)Me(e.localCa,e.onDemandTls);let l=je(e);if(o("config",`Starting with config: ${F(e,2)}`,r),o("config",`Is multi-proxy? ${"proxies"in e}`,r),o("config",`Hosts management enabled? ${l}`,r),e.viaDaemon){let x="proxies"in e&&Array.isArray(e.proxies)?e.proxies.map((w)=>({id:w.id,from:w.from,to:w.to,path:w.path,cleanUrls:w.cleanUrls??e.cleanUrls,changeOrigin:w.changeOrigin??e.changeOrigin,pathRewrites:w.pathRewrites})):[{id:e.id,from:e.from,to:e.to??"rpx.localhost",path:e.path,cleanUrls:e.cleanUrls,changeOrigin:e.changeOrigin,pathRewrites:e.pathRewrites}];await K({proxies:x,verbose:r});return}if("proxies"in e&&Array.isArray(e.proxies)){o("servers",`Found ${e.proxies.length} proxies in config`,r);for(let i of e.proxies)if(i.start){let x=`${i.from}-${i.to}`;try{o("watch",`Starting command for ${x} with command: ${i.start.command}`,r),u.info(`Starting command for ${x}...`),await te.startProcess(x,i.start,r);let w=M(i.from),C=new URL(w.startsWith("http")?w:`http://${w}`),D=C.hostname||"localhost",L=Number(C.port)||80;try{await z(D,L,r),o("watch",`Dev server is ready at ${D}:${L}`,r)}catch(fe){o("watch",`Connection check failed, but continuing with proxy setup: ${fe}`,r),u.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(w){throw o("watch",`Failed to start command for ${x}: ${w}`,r),Error(`Failed to start command for ${x}: ${w}`)}}else o("watch",`No start command for proxy ${i.from} -> ${i.to}`,r)}else if("start"in e&&e.start){o("watch","Found start command in single proxy config",r);let i=`${e.from}-${e.to}`;try{if(e.start)o("watch",`Starting command: ${e.start.command}`,r),await te.startProcess(i,e.start,r);let x=M(e.from),w=new URL(x.startsWith("http")?x:`http://${x}`),C=w.hostname||"localhost",D=Number(w.port)||80;try{await z(C,D,r),o("watch",`Dev server is ready at ${C}:${D}`,r)}catch(L){o("watch",`Connection check failed, but continuing with proxy setup: ${L}`,r),u.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(x){throw o("watch",`Failed to run start command: ${x}`,r),Error(`Failed to run start command: ${x}`)}}else o("watch","No start command found in config",r);let a="proxies"in e&&Array.isArray(e.proxies)?e.proxies[0]?.to:("to"in e)?e.to:"rpx.localhost";if(y.platform!=="win32"&&(e.https||l)){if(!Qe())try{o("sudo","Pre-acquiring sudo credentials for privileged operations",r),vt("sudo -v",{stdio:"inherit"})}catch{o("sudo","Could not pre-acquire sudo credentials",r)}}let s=[],f=null;if(e.productionCerts&&!n){if(s=await _e(e.productionCerts,r),s.length>0)o("ssl",`Using ${s.length} production SNI cert(s): ${s.map((i)=>i.serverName).join(", ")}`,r)}if(e.localCa&&!n){let i=await Ne(e.localCa,{verbose:r,onDemandTls:e.onDemandTls}),x=new Set(i.entries.map((w)=>w.serverName));s=[...i.entries,...s.filter((w)=>!x.has(w.serverName))],f=i.defaultTls,o("ssl",`Local CA: ${i.leafMinted?`minted leaf (${i.renewalReason})`:"reusing leaf"} for ${[...x].join(", ")}; valid until ${i.notAfter.toISOString()}`,r)}if(e.https){let i=s.length>0?null:await k(e);if(!i&&s.length===0){if(o("ssl",`No valid or trusted certificates found for ${a}, generating new ones`,e.verbose),await se(e),i=await k(e),!i)throw Error(`Failed to load SSL certificates after generation for ${a}`)}else o("ssl",`Using existing and trusted certificates for ${a}`,e.verbose);e._cachedSSLConfig=i}let c="proxies"in e&&Array.isArray(e.proxies)?e.proxies.map((i)=>({...i,https:e.https,cleanup:e.cleanup,cleanUrls:i.cleanUrls??("cleanUrls"in e?e.cleanUrls:!1),vitePluginUsage:e.vitePluginUsage,changeOrigin:i.changeOrigin??e.changeOrigin,verbose:r,_cachedSSLConfig:e._cachedSSLConfig})):[{from:"from"in e?e.from:"localhost:5173",to:"to"in e?e.to:"rpx.localhost",cleanUrls:"cleanUrls"in e?e.cleanUrls:!1,https:e.https,cleanup:e.cleanup,vitePluginUsage:e.vitePluginUsage,start:"start"in e?e.start:void 0,changeOrigin:e.changeOrigin,auth:"auth"in e?e.auth:void 0,verbose:r,_cachedSSLConfig:e._cachedSSLConfig}],d=c.map((i)=>i.to||"rpx.localhost"),h=n?null:s.length>0?s:e._cachedSSLConfig??null,m=d.filter((i)=>i&&!i.includes("localhost")&&!i.includes("127.0.0.1")),p=["dev","app","page","new","day","foo"],g=["test","localhost","local","example","invalid"],v=[...new Set(m.map((i)=>i.split(".").pop()?.toLowerCase()))],S=v.filter((i)=>!!i&&p.includes(i));if(S.length>0&&r)u.warn(`The following TLDs may not work reliably for local development: ${S.map((i)=>`.${i}`).join(", ")}`),u.info(" These TLDs have HSTS preloading which can bypass local DNS"),u.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(l&&y.platform==="darwin"&&m.length>0){let{setupDevelopmentDns:i}=await import("./chunk-handw2xw.js");if(await i({domains:m,verbose:r})){if(r)if(v.every((C)=>!!C&&g.includes(C)))u.success(`DNS server started for ${v.map((C)=>`.${C}`).join(", ")} domains`);else u.success(`DNS server started for ${v.map((C)=>`.${C}`).join(", ")} domains (hosts file entries also added)`)}else o("dns","Could not start DNS server - custom domains may not resolve",r)}let P=async()=>{o("cleanup","Starting cleanup handler",e.verbose);try{let{tearDownDevelopmentDns:i}=await import("./chunk-handw2xw.js");await i({verbose:e.verbose})}catch(i){o("cleanup",`Error stopping DNS server: ${i}`,e.verbose)}try{await te.stopAll(e.verbose)}catch(i){o("cleanup",`Error stopping processes: ${i}`,e.verbose)}await X({domains:d,hosts:typeof e.cleanup==="boolean"?e.cleanup:e.cleanup?.hosts,certs:typeof e.cleanup==="boolean"?e.cleanup:e.cleanup?.certs,verbose:e.verbose||!1})};y.on("SIGINT",P),y.on("SIGTERM",P);let O=e.singlePortMode===!0,b=e.httpsPort??443,T=e.httpPort??80,H=e.originGuard?we(e.originGuard):null,ft=!!h&&(c.length>1||O||s.length>0),mt=!h&&c.length>0&&(O||c.length>1),ze=e.maxTlsContexts;if(ft&&h){o("proxies",`Creating shared HTTPS server for ${c.length} domains on port ${b}`,r);let i=await Ze(c,l,r),x=null,w=e.onDemandTls,C=w?.enabled?new ke({config:w,certsDir:w.certsDir??e.productionCerts?.certsDir??ot.join(de.homedir(),".stacks","rpx","on-demand-certs"),initial:s,verbose:r,onCertAdded:(_)=>{if(tt()==="restart")o("on-demand","certificate installed; restarting supervised gateway to reload TLS",r),setTimeout(()=>y.kill(y.pid,"SIGTERM"),10).unref();else fe(_)}}):null,D=null,L=!1;async function fe(_){if(D=_,L)return;L=!0;try{while(D){let q=D;if(D=null,o("proxies",`rebuilding :${b} with ${q.length} SNI cert(s)`,r),x)W.delete(x),x.stop(!1);let A=!1;for(let B=0;!A&&B<60;B++){let Xe=re({routeEntries:i,listenPort:b,sslConfig:q,defaultTls:f,maxTlsContexts:ze,originGuard:H,verbose:r});if(Xe){x=Xe,A=!0;break}await new Promise((ht)=>setTimeout(ht,Math.min(25*2**Math.min(B,4),500)))}if(!A)u.error(`rpx: CRITICAL — could not rebind :${b} after cert issuance; HTTPS unbound until the next cert event or a gateway restart`)}}finally{L=!1}}if(!await U(T,"0.0.0.0",r)){let _=new Set(i.map((A)=>A.host)),q=(A)=>_.has(A)||[..._].some((B)=>I(A,B));st(r,T,b,e.acmeChallengeWebroot,C,q)}if(await U(b,"0.0.0.0",r)){if(o("proxies",`Port ${b} is already in use, cannot start shared proxy`,r),r)u.warn(`Port ${b} is in use. Shared HTTPS proxy cannot start.`);return}let gt=C&&C.sniEntries().length>0?C.sniEntries():h;if(x=re({routeEntries:i,listenPort:b,sslConfig:gt,defaultTls:f,maxTlsContexts:ze,originGuard:H,verbose:r}),!x){u.error(`Shared HTTPS proxy failed to bind :${b}; not exiting`);return}}else if(mt){o("proxies",`Creating shared HTTP server for ${c.length} domains on port ${T}`,r);let i=await Ze(c,l,r);if(await U(T,"0.0.0.0",r)){if(o("proxies",`Port ${T} is already in use, cannot start shared proxy`,r),r)u.warn(`Port ${T} is in use. Shared HTTP proxy cannot start.`);return}if(!re({routeEntries:i,listenPort:T,sslConfig:null,originGuard:H,verbose:r})){u.error(`Shared HTTP proxy failed to bind :${T}; not exiting`);return}}else for(let i of c)try{let x=i.to||"rpx.localhost";o("proxy",`Starting proxy for ${x} with SSL config: ${!!h}`,i.verbose),await Ie({from:i.from||"localhost:5173",to:x,cleanUrls:i.cleanUrls||!1,https:i.https||!1,cleanup:i.cleanup||!1,vitePluginUsage:i.vitePluginUsage||!1,verbose:i.verbose||!1,_cachedSSLConfig:e._cachedSSLConfig,changeOrigin:i.changeOrigin||!1,loadBalancer:i.loadBalancer,auth:i.auth,path:i.path,pathRewrites:i.pathRewrites,httpPort:e.httpPort,httpsPort:e.httpsPort})}catch(x){o("proxies",`Failed to start proxy for ${i.to}: ${x}`,i.verbose),u.error(`Failed to start proxy for ${i.to}:`,x)}}async function Ze(t,e,r){let n=[],l=new Set;for(let a of t){let s=a.to||"rpx.localhost",f=a.cleanUrls||!1,c=a.path,d=Le(c),h=pe(a.auth);if(a.redirect){let m=$e(a.redirect);n.push({host:s,path:c,route:{redirect:m,basePath:d,auth:h}}),o("proxies",`Route: ${s}${c??""} → redirect ${m.status} ${m.to}${h?" (auth)":""}`,r)}else if(a.static)n.push({host:s,path:c,route:{static:Te(a.static,f),cleanUrls:f,basePath:d,auth:h}}),o("proxies",`Route: ${s}${c??""} → static ${typeof a.static==="string"?a.static:a.static.dir}${h?" (auth)":""}`,r);else{let m=M(a.from),p=new URL(m.startsWith("http")?m:`http://${m}`),g=ae(a.from??p.host,a.loadBalancer);le(g),oe.add(g),n.push({host:s,path:c,route:{sourceHost:p.host,upstreamPool:g,cleanUrls:f,changeOrigin:a.changeOrigin||!1,pathRewrites:a.pathRewrites,basePath:d,auth:h}}),o("proxies",`Route: ${s}${c??""} → ${p.host}${h?" (auth)":""}`,r)}if(l.has(s))continue;if(l.add(s),e&&!Ee(s)&&!s.includes("localhost")&&!s.includes("127.0.0.1"))try{if(!(await G([s],r))[0])await N([s],r)}catch{o("hosts",`Could not add hosts entry for ${s}`,r)}}return n}var j=null;function it(){if(y.env.RPX_BIND_HOSTNAME)return y.env.RPX_BIND_HOSTNAME;if(j)return j;try{j=Object.values(de.networkInterfaces()).flat().some((r)=>r&&r.family==="IPv6")?"::":"0.0.0.0"}catch{j="0.0.0.0"}return j}function re(t){let{routeEntries:e,listenPort:r,sslConfig:n,defaultTls:l,maxTlsContexts:a,originGuard:s,verbose:f}=t,c=Ae(e),d=Re((p,g)=>Fe(c,p,g),f),h=s?(p,g)=>s(p)??d(p,g):d,m=Oe(f);try{let p=Bun.serve({port:r,hostname:it(),reusePort:et(),...n?{tls:Array.isArray(n)?He({sni:n,defaultTls:l,maxTlsContexts:a,verbose:f}):Ue({key:n.key,cert:n.cert,ca:n.ca,requestCert:!1,rejectUnauthorized:!1})}:{},fetch(g,v){return h(g,v)},websocket:m,error(g){return o("server",`Shared proxy server error: ${g}`,f),new Response(`Server Error: ${g.message}`,{status:500})}});return W.add(p),o("proxies",`Shared ${n?"HTTPS":"HTTP"} proxy listening on port ${r} for ${c.size} domains`,f),p}catch(p){return o("proxies",`Failed to start shared proxy: ${p}`,f),console.error("Failed to start shared proxy:",p),null}}function Dt(t){if(t?.vitePluginUsage||!t?.verbose)return;if(console.log(""),console.log(` ${R.green(R.bold("rpx"))} ${R.green(`v${Je}`)}`),console.log(` ${R.green("➜")} ${R.dim(t?.from??"")} ${R.dim("➜")} ${R.cyan(t?.ssl?`https://${t?.to}`:`http://${t?.to}`)}`),t?.listenPort!==(t?.ssl?443:80))console.log(` ${R.green("➜")} Listening on port ${t?.listenPort}`);if(t?.cleanUrls)console.log(` ${R.green("➜")} Clean URLs enabled`)}import{readdir as Et,readFile as Lt}from"node:fs/promises";import*as at from"node:path";import*as lt from"node:process";var ct="/etc/rpx/certs",We="/etc/rpx/sites.d";function At(t,e){console.error(`[rpx gateway] SKIPPING malformed fragment ${t}; its host(s) will 404 until fixed: ${e.message}`)}async function pt(t,e=At){let r=[];try{r=(await Et(t)).filter((l)=>l.endsWith(".json")).sort()}catch{return[]}let n=[];for(let l of r)try{let a=JSON.parse(await Lt(at.join(t,l),"utf8"));if(!a||typeof a!=="object"||Array.isArray(a))throw TypeError("fragment is not a JSON object");n.push({file:l,fragment:a})}catch(a){e(l,a instanceof Error?a:Error(String(a)))}return n}function ut(t,e={}){let r=e.onWarning??((v)=>console.warn(`[rpx gateway] ${v}`)),n=[],l=new Set,a=new Map,s=new Set,f=new Set,c,d=e.certsDir??ct,h,m,p=!1;for(let{file:v,fragment:S}of t){for(let P of Array.isArray(S.proxies)?S.proxies:[]){let O=P.id||`${P.to}${P.path??""}`;if(l.has(O)){r(`duplicate route ${O} in ${v} ignored; first declared by ${a.get(O)}`);continue}l.add(O),a.set(O,v),n.push(P)}for(let P of S.onDemandTls?.allowedSuffixes??[])s.add(P);if(c??=S.onDemandTls?.email,S.onDemandTls&&S.onDemandTls.staging!==!0)p=!0;if(S.productionCerts?.certsDir)d=S.productionCerts.certsDir;if(h??=S.acmeChallengeWebroot,S.originGuard&&S.originGuard.header&&S.originGuard.value)if(m??={header:S.originGuard.header,value:S.originGuard.value},S.originGuard.header===m.header&&S.originGuard.value===m.value)for(let P of S.originGuard.hosts??[])f.add(P);else r(`origin-guard secret in ${v} differs from the one already in force; its hosts stay unguarded rather than rejecting all traffic`)}let g={proxies:n,productionCerts:{certsDir:d,certsDirServerNames:[...new Set(n.map((v)=>v.to).filter(Boolean))]},https:!0,hostsManagement:!1,cleanup:{hosts:!1,certs:!1}};if(s.size>0)g.onDemandTls={enabled:!0,allowedSuffixes:[...s],email:c,certsDir:d,staging:!p};if(h)g.acmeChallengeWebroot=h;if(m)g.originGuard={header:m.header,value:m.value,hosts:[...f]};return g}async function dt(t={}){let e=t.sitesDir??We,r=t.verbose??lt.env.RPX_VERBOSE!=="false",n=await pt(e,t.onFragmentError);o("gateway",`merged ${n.length} fragment(s) from ${e}: ${n.map((s)=>s.file).join(", ")||"<none>"}`,r);let a={...ut(n,{certsDir:t.certsDir}),verbose:r,singlePortMode:!0};if(t.https===!1)a.https=!1;if(t.httpPort!==void 0)a.httpPort=t.httpPort;if(t.httpsPort!==void 0)a.httpsPort=t.httpsPort;if(t.localCa)a.localCa=t.localCa;if(t.maxTlsContexts!==void 0)a.maxTlsContexts=t.maxTlsContexts;return a}async function Ft(t={}){let e=await dt(t);if(("proxies"in e&&Array.isArray(e.proxies)?e.proxies:[]).length===0&&!t.localCa)console.warn(`[rpx gateway] no routes found under ${t.sitesDir??We}; every request will answer 404 until a fragment is deployed`);await Y(e)}var ms=Y;export{xr as ACME_CHALLENGE_PREFIX,ct as DEFAULT_GATEWAY_CERTS_DIR,We as DEFAULT_GATEWAY_SITES_DIR,Er as DEFAULT_LOCAL_CA_RENEW_BEFORE_DAYS,Dr as DEFAULT_LOCAL_CA_VALIDITY_DAYS,Sr as DEFAULT_MAX_TLS_CONTEXTS,fo as DNS_PORT,io as DNS_STATE_VERSION,Ge as DefaultPortManager,ao as LEGACY_TLD_RESOLVER_LABELS,Or as LOCAL_CA_COMMON_NAME,Tr as LOCAL_CA_LEAF_CERT_FILENAME,Rr as LOCAL_CA_LEAF_KEY_FILENAME,Gt as MACOS_CA_TRUST_FLAGS,Bt as MACOS_SYSTEM_KEYCHAIN,ke as OnDemandCertManager,Qr as RPX_HOSTS_MARKER,mo as RPX_RESOLVER_MARKER,It as RPX_ROOT_CA_COMMON_NAME,Kt as SHARED_DEV_HOST_CERT_PATH,Vr as SiteSupervisor,Lo as acquireDaemonLock,N as addHosts,hs as authorizeSystemAccess,Ae as buildHostRoutes,He as buildListenerTls,ur as buildRedirectLocation,Zt as buildRegistryTlsProxyOptions,_e as buildSniTlsConfig,br as capTlsContexts,kt as certIncludesSanHostnames,k as checkExistingCertificates,G as checkHosts,X as cleanup,Ce as cleanupCertificates,nr as clearSslConfigCache,R as colors,ne as config,wo as contentLooksLikeRpxResolver,dr as contentTypeFor,we as createOriginGuard,Re as createProxyFetchHandler,Oe as createProxyWebSocketHandler,Gr as createSiteResolver,ae as createUpstreamPool,o as debugLog,ms as default,ne as defaultConfig,Ho as defaultDaemonSpawnCommand,Ve as deriveIdFromTarget,Nr as detectProjectPreset,uo as devDomainsFromHosts,Jt as devSslToSniEntries,oo as dropStaleRpxHostsLines,pr as enforceBasicAuth,ye as ensureDaemonRunning,Ne as ensureLocalCa,tr as ensureRootCA,Kr as escapeHtml,xs as execSudoSync,Ur as expandHome,ws as extractHostname,ro as filterRpxHostsEntries,Ir as findAvailablePort,no as findStaleRpxHosts,or as forceTrustCertificate,Yr as gcStaleEntries,se as generateCertificate,Oo as getDaemonPidPath,Ro as getDaemonRpxDir,jt as getMacosLoginKeychainPath,Wt as getMacosTrustKeychains,Ss as getPrimaryDomain,me as getRegistryDir,Qt as getRootCAPaths,er as getSharedDaemonCertPaths,Qe as getSudoPassword,to as hostsLineMapsHost,ie as httpsConfig,Hr as installLocalCaTrust,sr as isCertTrusted,Eo as isDaemonRunning,xo as isDnsServerRunning,$r as isLikelyHostname,bs as isMultiProxyConfig,vs as isMultiProxyOptions,Wr as isPidAlive,U as isPortInUse,gs as isProcessElevated,qt as isRootCaFingerprintInKeychains,Yt as isRootCaTrustedForSsl,$s as isSingleProxyConfig,Cs as isSingleProxyOptions,ge as isValidId,Ps as isValidRootCA,Ee as isWildcardPattern,Fr as leafRenewalReason,zt as listCertSha256HashesByCommonName,Br as listDiscoverableSites,rr as loadSSLConfig,Lr as localCaPaths,lr as markFailure,ar as markSuccess,yr as matchHost,Pr as matchHostList,Fe as matchHostRoute,Cr as matchesAllowedSuffix,I as matchesWildcard,ut as mergeGatewayFragments,lo as normalizeDevDomain,Le as normalizePathPrefix,Ht as normalizeSha256Fingerprint,eo as parseHostsLine,cr as parseHtpasswd,Ar as parseSanNames,Nt as parseSha256HashesFromSecurityListing,wr as pathPrefixMatches,jr as portManager,M as primaryUpstreamUrl,kr as projectNameFromHost,Xt as pruneStaleRootCas,De as readAcmeChallenge,Xr as readAll,_t as readCertCommonName,Ut as readCertSha256Fingerprint,Do as readDaemonPid,zr as readEntry,pt as readGatewayFragments,Mr as readSiteManifest,_o as reconcileDevelopmentDnsOnIdle,To as reconcileStaleDevelopmentDns,ys as redactSensitive,Ao as releaseDaemonLock,xe as removeEntry,Be as removeHosts,So as removeLegacyTldResolvers,$o as removeResolver,so as removeStaleRpxHosts,Zr as renderFailedPage,Jr as renderStartingPage,pe as resolveAuth,dt as resolveGatewayOptions,Me as resolveLocalCaConfig,Ts as resolvePathRewrite,$e as resolveRedirect,mr as resolveStaticFile,Te as resolveStaticRoute,co as resolverBasenameForDomain,po as resolverBasenamesForDomains,yo as resolverFilePath,Fo as runDaemon,K as runViaDaemon,Rs as safeDeleteFile,fr as safeRelativePath,F as safeStringify,ir as selectUpstream,gr as serveStaticFile,vr as serverNameFromCertFilename,bo as setupDevelopmentDns,Po as setupResolver,et as shouldReusePort,_r as siteIdForHost,go as startDnsServer,Ft as startGateway,le as startHealthChecks,Y as startProxies,Rt as startProxy,Ie as startServer,Uo as stopDaemon,ho as stopDnsServer,ce as stopHealthChecks,hr as stripBasePath,vo as syncDevelopmentDnsFromRegistry,Co as tearDownDevelopmentDns,Vt as trustRootCaForBrowsers,Mt as verifyHttpsChain,qr as watchRegistry,Ue as withLowMemoryTls,he as writeEntry};
@@ -0,0 +1,77 @@
1
+ import type { DefaultTlsContext, SniTlsEntry } from './sni';
2
+ import type { LocalCaConfig, OnDemandTlsConfig } from './types';
3
+ /** Where the CA and the leaf live inside `dir`. */
4
+ export declare function localCaPaths(dir: string): LocalCaPaths;
5
+ /**
6
+ * Normalize and validate a {@link LocalCaConfig}. Throws on an empty host
7
+ * list, a malformed host or IP, and on any host that a public on-demand set
8
+ * also claims (the two flows would fight over one SNI name, and ACME would be
9
+ * asked for a name it can never issue).
10
+ */
11
+ export declare function resolveLocalCaConfig(cfg: LocalCaConfig, onDemandTls?: OnDemandTlsConfig): ResolvedLocalCaConfig;
12
+ /** Parse Node's `subjectAltName` string into the dNSName and iPAddress sets. */
13
+ export declare function parseSanNames(subjectAltName: string | undefined): { dns: Set<string>, ips: Set<string> };
14
+ /**
15
+ * Why an on-disk leaf must be re-minted, or `null` when it can be reused.
16
+ * Checked on every start: SAN coverage (a host or IP added to the config),
17
+ * the signing CA (a rotated CA orphans its leaves), the key pair, and the
18
+ * expiry window (`renewBeforeDays`).
19
+ */
20
+ export declare function leafRenewalReason(material: { cert: string, key: string, caCert: string }, cfg: Pick<ResolvedLocalCaConfig, 'hosts' | 'ips' | 'renewBeforeDays'>, now?: Date): string | null;
21
+ /**
22
+ * Install the local Root CA into the system trust store (tlsx `installCA`),
23
+ * skipped when it is already trusted. Never throws: on a box where the trust
24
+ * store cannot be written rpx must still serve; the operator sees a warning
25
+ * and can trust the CA by hand.
26
+ */
27
+ export declare function installLocalCaTrust(paths: LocalCaPaths, verbose?: boolean): Promise<{ alreadyTrusted: boolean, installed: boolean }>;
28
+ /**
29
+ * Load-or-create the Root CA under `cfg.dir`, then reuse or (re)mint the one
30
+ * LAN leaf. Idempotent: a second start with the same config touches nothing.
31
+ */
32
+ export declare function ensureLocalCa(cfg: LocalCaConfig, options?: EnsureLocalCaOptions): Promise<LocalCaMaterial>;
33
+ export declare const LOCAL_CA_LEAF_CERT_FILENAME: 'rpx-local-host.crt';
34
+ export declare const LOCAL_CA_LEAF_KEY_FILENAME: 'rpx-local-host.key';
35
+ /** Common name of the Root CA rpx mints for a LAN gateway. */
36
+ export declare const LOCAL_CA_COMMON_NAME: 'rpx Local CA';
37
+ export declare const DEFAULT_LOCAL_CA_VALIDITY_DAYS: 825;
38
+ export declare const DEFAULT_LOCAL_CA_RENEW_BEFORE_DAYS: 30;
39
+ export declare interface LocalCaPaths {
40
+ caCertPath: string
41
+ caKeyPath: string
42
+ certPath: string
43
+ keyPath: string
44
+ }
45
+ export declare interface ResolvedLocalCaConfig {
46
+ dir: string
47
+ hosts: string[]
48
+ ips: string[]
49
+ installTrust: boolean
50
+ validityDays: number
51
+ renewBeforeDays: number
52
+ }
53
+ export declare interface LocalCaMaterial {
54
+ paths: LocalCaPaths
55
+ caCert: string
56
+ cert: string
57
+ key: string
58
+ caCreated: boolean
59
+ leafMinted: boolean
60
+ renewalReason: string | null
61
+ notAfter: Date
62
+ entries: SniTlsEntry[]
63
+ defaultTls: DefaultTlsContext
64
+ trust?: { alreadyTrusted: boolean, installed: boolean }
65
+ }
66
+ export declare interface EnsureLocalCaOptions {
67
+ verbose?: boolean
68
+ onDemandTls?: OnDemandTlsConfig
69
+ now?: () => Date
70
+ }
71
+ /**
72
+ * Newer tlsx releases ship their own `isCertTrusted` (with a Linux trust-store
73
+ * check). Prefer it when the installed tlsx has one; fall back to rpx's own
74
+ * fingerprint check otherwise. Resolved at call time so rpx keeps working
75
+ * against both older and newer tlsx builds.
76
+ */
77
+ declare type TrustCheck = (certPath: string, options?: { verbose?: boolean }) => Promise<boolean> | boolean;
@@ -55,9 +55,10 @@ export declare interface ProxyRoute {
55
55
  stripBasePathPrefix?: boolean
56
56
  auth?: ResolvedAuth
57
57
  }
58
- /** Minimal shape of the Bun server needed for WebSocket upgrades. */
58
+ /** Minimal shape of the Bun server needed for WebSocket upgrades and peer lookup. */
59
59
  export declare interface ProxyServer {
60
60
  upgrade: (req: Request, options?: { data?: unknown, headers?: Bun.HeadersInit }) => boolean
61
+ requestIP?: (req: Request) => { address: string } | null
61
62
  }
62
63
  /*` → app, `/docs*` → static dir). Callers
63
64
  * that only route by host can ignore the second argument — it's optional so
@@ -23,6 +23,7 @@ export declare interface PoolRequest {
23
23
  path: string
24
24
  reqHeaders: Headers
25
25
  forwardedHost: string
26
+ clientIp?: string
26
27
  originOverride?: string
27
28
  body: ReadableStream<Uint8Array> | null
28
29
  signal?: AbortSignal
package/dist/sni.d.ts CHANGED
@@ -1,4 +1,23 @@
1
1
  import type { ProductionTlsConfig } from './types';
2
+ /**
3
+ * Memory guard: cap the live SNI set at `max` entries. Keeps the FIRST `max`
4
+ * (callers order the set so the hosts that matter most, e.g. a LAN local-CA
5
+ * leaf, come first) and logs ONE warning naming every dropped host, so a
6
+ * silently missing cert is never a mystery. Returns the input untouched when
7
+ * it fits.
8
+ */
9
+ export declare function capTlsContexts(entries: SniTlsEntry[], max?: number, verbose?: boolean): SniTlsEntry[];
10
+ /**
11
+ * Assemble the `Bun.serve({ tls })` array for a shared listener: the optional
12
+ * default context first (no `serverName`), then the SNI entries capped at
13
+ * `maxTlsContexts`, every entry in low-memory mode.
14
+ */
15
+ export declare function buildListenerTls(opts: {
16
+ sni: SniTlsEntry[]
17
+ defaultTls?: DefaultTlsContext | null
18
+ maxTlsContexts?: number
19
+ verbose?: boolean
20
+ }): Bun.TLSOptions[];
2
21
  /**
3
22
  * Production gateways keep many TLS contexts alive and may serve large,
4
23
  * concurrent responses. Ask OpenSSL to release per-connection read and write
@@ -21,9 +40,27 @@ export declare function serverNameFromCertFilename(filename: string): string | n
21
40
  * usable is found so the caller can fall back to the dev cert flow.
22
41
  */
23
42
  export declare function buildSniTlsConfig(cfg: ProductionTlsConfig, verbose?: boolean): Promise<SniTlsEntry[]>;
43
+ /**
44
+ * Default for {@link import('./types').SharedProxyConfig.maxTlsContexts}. A
45
+ * parsed cert + key per SNI entry lives for the life of the listener; 256 is
46
+ * far beyond any one box's routed hosts while staying small on a 4 GB Pi.
47
+ */
48
+ export declare const DEFAULT_MAX_TLS_CONTEXTS: 256;
24
49
  /** One entry of the Bun.serve `tls` array. */
25
50
  export declare interface SniTlsEntry {
26
51
  serverName: string
27
52
  cert: string
28
53
  key: string
29
54
  }
55
+ /**
56
+ * The cert a listener presents when the client sends no SNI at all (an
57
+ * IP-literal URL such as `https://192.168.1.20/`) or an SNI name no entry
58
+ * matches. Bun has no separate "default context" knob: the FIRST element of
59
+ * the `tls` array is the default, and it is the only element allowed to omit
60
+ * `serverName` (verified on Bun 1.3.14: an unnamed entry anywhere but first
61
+ * throws "SNI tls object must have a serverName"). See {@link buildListenerTls}.
62
+ */
63
+ export declare interface DefaultTlsContext {
64
+ cert: string
65
+ key: string
66
+ }
package/dist/start.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { createOriginGuard } from './origin-guard';
2
2
  import { OnDemandCertManager } from './on-demand';
3
3
  import * as path from 'node:path';
4
+ import * as tls from 'node:tls';
4
5
  import type { CleanupOptions, ProxyOption, ProxyOptions, ProxySetupOptions, SingleProxyConfig, SSLConfig } from './types';
6
+ import type { DefaultTlsContext, SniTlsEntry } from './sni';
5
7
  import type { ProxyRoute } from './proxy-handler';
6
- import type { SniTlsEntry } from './sni';
7
8
  export declare function cleanup(options?: CleanupOptions): Promise<void>;
8
9
  export declare function startServer(options: SingleProxyConfig): Promise<void>;
9
10
  export declare function setupProxy(options: ProxySetupOptions): Promise<void>;
@@ -36,6 +37,13 @@ export declare function createSharedProxyServer(opts: {
36
37
  routeEntries: Array<{ host: string, path?: string, route: ProxyRoute }>
37
38
  listenPort: number
38
39
  sslConfig: SharedTlsConfig | null
40
+ /**
41
+ * Cert for connections with no SNI / an unknown SNI name (only meaningful
42
+ * with an SNI array `sslConfig`). Becomes the first `tls[]` entry.
43
+ */
44
+ defaultTls?: DefaultTlsContext | null
45
+ /** Memory guard for the SNI array; see `SharedProxyConfig.maxTlsContexts`. */
46
+ maxTlsContexts?: number
39
47
  originGuard: ReturnType<typeof createOriginGuard> | null
40
48
  verbose: boolean
41
49
  }): ReturnType<typeof Bun.serve> | null;
package/dist/types.d.ts CHANGED
@@ -165,6 +165,27 @@ export declare interface OnDemandSitesConfig {
165
165
  idleTimeoutMs?: number
166
166
  startupTimeoutMs?: number
167
167
  }
168
+ /**
169
+ * LAN production mode: serve HTTPS for hosts that public ACME can never
170
+ * certify (`pi-stacks.local`, a private IP) from a Root CA rpx owns.
171
+ *
172
+ * On start rpx loads or creates the CA under {@link dir}, mints ONE leaf whose
173
+ * SANs name every entry of {@link hosts} (dNSName) and {@link ips} (iPAddress),
174
+ * registers it under each host's SNI name AND as the listener's default TLS
175
+ * context, so a connection that sends no SNI at all (an IP-literal URL) still
176
+ * gets it. The leaf is re-minted when its SAN set no longer matches or fewer
177
+ * than {@link renewBeforeDays} remain. Public on-demand ACME and
178
+ * `productionCerts` keep working alongside it; a host may not appear in both
179
+ * `hosts` and `onDemandTls.allowedSuffixes` (that is a config error).
180
+ */
181
+ export declare interface LocalCaConfig {
182
+ dir: string
183
+ hosts: string[]
184
+ ips?: string[]
185
+ installTrust?: boolean
186
+ validityDays?: number
187
+ renewBeforeDays?: number
188
+ }
168
189
  export declare interface SharedProxyConfig {
169
190
  https: boolean | TlsOption
170
191
  cleanup: boolean | CleanupOptions
@@ -183,6 +204,8 @@ export declare interface SharedProxyConfig {
183
204
  hostsManagement?: boolean
184
205
  productionCerts?: ProductionTlsConfig
185
206
  onDemandTls?: OnDemandTlsConfig
207
+ localCa?: LocalCaConfig
208
+ maxTlsContexts?: number
186
209
  onDemand?: OnDemandSitesConfig
187
210
  originGuard?: OriginGuardOptions
188
211
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/rpx",
3
3
  "type": "module",
4
- "version": "0.11.45",
4
+ "version": "0.11.47",
5
5
  "description": "A modern and smart reverse proxy.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@stacksjs/clapp": "^0.2.10",
75
- "@stacksjs/tlsx": "^0.13.13"
75
+ "@stacksjs/tlsx": "^0.13.19"
76
76
  },
77
77
  "devDependencies": {
78
78
  "bunfig": "^0.15.6",
@@ -1 +0,0 @@
1
- import{Bb as a,Cb as b,Db as c,Eb as d,Fb as e,Gb as f,Hb as g,Ib as h,Jb as i,Kb as j,Lb as k,Mb as l,Nb as m,Ob as n,Pb as o,Qb as p,Rb as q}from"./chunk-w4t1nar9.js";export{f as shouldReusePort,h as safeStringify,q as safeDeleteFile,p as resolvePathRewrite,g as redactSensitive,j as isValidRootCA,n as isSingleProxyOptions,o as isSingleProxyConfig,b as isProcessElevated,m as isMultiProxyOptions,l as isMultiProxyConfig,a as getSudoPassword,k as getPrimaryDomain,i as extractHostname,d as execSudoSync,e as debugLog,c as authorizeSystemAccess};