@stacksjs/rpx 0.11.46 → 0.11.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OnDemandCertManager } from './on-demand';
2
- import type { OnDemandSitesConfig, OnDemandTlsConfig, ProductionTlsConfig, TlsOption } from './types';
2
+ import type { LocalCaConfig, OnDemandSitesConfig, OnDemandTlsConfig, ProductionTlsConfig, TlsOption } from './types';
3
3
  import type { ProxyRoute } from './proxy-handler';
4
4
  import type { RegistryEntry } from './registry';
5
5
  import type { SiteSnapshot } from './site-supervisor';
@@ -89,9 +89,11 @@ export declare interface DaemonOptions {
89
89
  httpsPort?: number
90
90
  httpPort?: number
91
91
  hostname?: string
92
- https?: TlsOption
92
+ https?: boolean | TlsOption
93
93
  productionCerts?: ProductionTlsConfig
94
94
  onDemandTls?: OnDemandTlsConfig
95
+ localCa?: LocalCaConfig
96
+ maxTlsContexts?: number
95
97
  onDemandSites?: OnDemandSitesConfig
96
98
  acmeChallengeWebroot?: string
97
99
  gcIntervalMs?: number
@@ -0,0 +1,65 @@
1
+ import type { LocalCaConfig, OnDemandTlsConfig, ProductionTlsConfig, ProxyOptions, ResolvedProxyOptions } from './types';
2
+ import type { OriginGuardOptions } from './origin-guard';
3
+ /**
4
+ * Read every `*.json` fragment in `sitesDir`, in filename order. A missing
5
+ * directory yields no fragments (a box with nothing deployed yet). A fragment
6
+ * that fails to read or parse is passed to `onFragmentError` and skipped.
7
+ */
8
+ export declare function readGatewayFragments(sitesDir: string, onFragmentError?: (file: string, err: Error) => void): Promise<GatewayFragmentFile[]>;
9
+ /**
10
+ * Merge fragments into the options `startProxies` takes, with ts-cloud's
11
+ * assembler semantics (see the module header). Pure: no I/O, no env reads.
12
+ */
13
+ export declare function mergeGatewayFragments(fragments: GatewayFragmentFile[], options?: MergeGatewayOptions): ProxyOptions;
14
+ /**
15
+ * Resolve the full `startProxies` options for a gateway: read + merge the
16
+ * fragments, then apply the gateway-level switches (`https`, ports, local CA,
17
+ * memory guard, verbosity). Exported so the CLI and tests see exactly what
18
+ * {@link startGateway} starts.
19
+ */
20
+ export declare function resolveGatewayOptions(options?: GatewayOptions): Promise<ProxyOptions>;
21
+ /**
22
+ * Start the gateway: merge every fragment under `sitesDir` and hand the result
23
+ * to {@link startProxies}. Resolves once the listeners are bound (or a bind was
24
+ * refused and logged); the process then serves until SIGINT / SIGTERM.
25
+ */
26
+ export declare function startGateway(options?: GatewayOptions): Promise<void>;
27
+ /** Default directory on the box that holds real per-domain TLS certs. */
28
+ export declare const DEFAULT_GATEWAY_CERTS_DIR: '/etc/rpx/certs';
29
+ /** Default per-app fragment registry. */
30
+ export declare const DEFAULT_GATEWAY_SITES_DIR: '/etc/rpx/sites.d';
31
+ /** A per-app fragment: ts-cloud's `RpxGatewayConfig` plus its `slug`. */
32
+ export declare interface GatewayFragment {
33
+ slug?: string
34
+ proxies?: GatewayRoute[]
35
+ productionCerts?: Partial<ProductionTlsConfig>
36
+ onDemandTls?: Partial<OnDemandTlsConfig> & { staging?: boolean }
37
+ acmeChallengeWebroot?: string
38
+ originGuard?: Partial<OriginGuardOptions>
39
+ [key: string]: unknown
40
+ }
41
+ export declare interface GatewayFragmentFile {
42
+ file: string
43
+ fragment: GatewayFragment
44
+ }
45
+ export declare interface GatewayOptions {
46
+ sitesDir?: string
47
+ certsDir?: string
48
+ localCa?: LocalCaConfig
49
+ https?: boolean
50
+ httpPort?: number
51
+ httpsPort?: number
52
+ maxTlsContexts?: number
53
+ verbose?: boolean
54
+ onFragmentError?: (file: string, err: Error) => void
55
+ }
56
+ export declare interface MergeGatewayOptions {
57
+ certsDir?: string
58
+ onWarning?: (message: string) => void
59
+ }
60
+ /**
61
+ * One route inside a fragment. Structurally the rpx `BaseProxyConfig` (ts-cloud
62
+ * emits exactly these keys), typed loosely enough that a fragment written by
63
+ * an older or newer ts-cloud still loads.
64
+ */
65
+ export type GatewayRoute = NonNullable<ResolvedProxyOptions['proxies']>[number];
package/dist/https.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { config } from './config';
2
2
  import { MACOS_CA_TRUST_FLAGS, MACOS_SYSTEM_KEYCHAIN, getMacosLoginKeychainPath, isRootCaFingerprintInKeychains, isRootCaTrustedForSsl, pruneStaleRootCas, trustRootCaForBrowsers } from './macos-trust';
3
3
  import { readCertCommonName, readCertSha256Fingerprint } from './cert-inspect';
4
+ import type { CAOptions } from '@stacksjs/tlsx';
4
5
  import type { ProxyConfigs, ProxyOption, ProxyOptions, SSLConfig, TlsConfig } from './types';
5
6
  /**
6
7
  * Bun needs one `tls[]` entry per SNI name even when a single PEM covers every SAN.
@@ -20,6 +21,13 @@ export declare function getSharedDaemonCertPaths(sslDir: string): {
20
21
  caCertPath: string
21
22
  rootCA: RootCAPaths
22
23
  };
24
+ /**
25
+ * Load the persisted Root CA under `sslDir`, or create and persist a fresh one
26
+ * (cert 0644, private key 0600). The one load-or-create used for the dev cert
27
+ * flow (`~/.stacks/ssl`) and the LAN local-CA flow alike, so a CA is only ever
28
+ * minted once per directory and browsers trust it once.
29
+ */
30
+ export declare function ensureRootCA(sslDir: string, options?: { caOptions?: CAOptions, verbose?: boolean }): Promise<{ certificate: string, privateKey: string, created: boolean, paths: RootCAPaths }>;
23
31
  /**
24
32
  * Resolves SSL paths based on configuration
25
33
  */
package/dist/index.d.ts CHANGED
@@ -17,7 +17,9 @@ export type { OriginGuard, OriginGuardOptions } from './origin-guard';
17
17
  export type { ResolvedAuth } from './auth';
18
18
  export type { HostRoutes, PathRoute } from './host-routes';
19
19
  export type { ResolvedStaticRoute, StaticResolution } from './static-files';
20
- export type { SniTlsEntry } from './sni';
20
+ export type { DefaultTlsContext, SniTlsEntry } from './sni';
21
+ export type { EnsureLocalCaOptions, LocalCaMaterial, LocalCaPaths, ResolvedLocalCaConfig } from './local-ca';
22
+ export type { GatewayFragment, GatewayFragmentFile, GatewayOptions, GatewayRoute, MergeGatewayOptions } from './gateway';
21
23
  export type { CertIssuer, OnDemandCertManagerOptions } from './on-demand';
22
24
  export type {
23
25
  ResolvedSite,
@@ -57,6 +59,7 @@ export {
57
59
  cleanupCertificates,
58
60
  clearSslConfigCache,
59
61
  devSslToSniEntries,
62
+ ensureRootCA,
60
63
  forceTrustCertificate,
61
64
  generateCertificate,
62
65
  getRootCAPaths,
@@ -163,7 +166,28 @@ export {
163
166
  safeRelativePath,
164
167
  serveStaticFile,
165
168
  } from './static-files';
166
- export { buildSniTlsConfig, serverNameFromCertFilename } from './sni';
169
+ export { buildListenerTls, buildSniTlsConfig, capTlsContexts, DEFAULT_MAX_TLS_CONTEXTS, serverNameFromCertFilename, withLowMemoryTls } from './sni';
170
+ export {
171
+ DEFAULT_LOCAL_CA_RENEW_BEFORE_DAYS,
172
+ DEFAULT_LOCAL_CA_VALIDITY_DAYS,
173
+ ensureLocalCa,
174
+ installLocalCaTrust,
175
+ leafRenewalReason,
176
+ LOCAL_CA_COMMON_NAME,
177
+ LOCAL_CA_LEAF_CERT_FILENAME,
178
+ LOCAL_CA_LEAF_KEY_FILENAME,
179
+ localCaPaths,
180
+ parseSanNames,
181
+ resolveLocalCaConfig,
182
+ } from './local-ca';
183
+ export {
184
+ DEFAULT_GATEWAY_CERTS_DIR,
185
+ DEFAULT_GATEWAY_SITES_DIR,
186
+ mergeGatewayFragments,
187
+ readGatewayFragments,
188
+ resolveGatewayOptions,
189
+ startGateway,
190
+ } from './gateway';
167
191
  export { isLikelyHostname, matchesAllowedSuffix, OnDemandCertManager } from './on-demand';
168
192
  export {
169
193
  createSiteResolver,
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{$ as De,$a as Fr,A as _t,Aa as me,B as k,Ba as gr,C as ne,Ca as ge,D as ve,Da as he,E as It,Ea as hr,F as A,Fa as xr,G as ie,Ga as yr,H as Gt,Ha as Pr,I as jt,Ia as Sr,J as zt,Ja as wr,K as ae,Ka as br,L as ce,La as vr,M as Wt,Ma as $r,N as le,Na as Cr,O as Xt,Oa as Rr,P as $e,Pa as Tr,Q as qt,Qa as Or,R as Ce,Ra as F,S as Vt,Sa as Fe,T as Yt,Ta as M,U as Kt,Ua as Dr,V as Jt,Va as Er,W as Zt,Wa as Hr,X as Re,Xa as Ur,Y as Te,Ya as Lr,Z as Qt,Za as kr,_ as Oe,_a as Ar,a as c,aa as I,ab as Mr,b as oe,ba as er,bb as Nr,c as xt,ca as Ee,cb as Br,d as yt,da as tr,db as _r,e as Pt,ea as He,eb as Ir,f as St,fa as rr,g as wt,ga as Ue,gb as Gr,h as bt,ha as Ye,hb as jr,i as vt,ia as or,ib as zr,j as $t,ja as Le,jb as Wr,k as Ct,ka as Ke,kb as Xr,l as Rt,la as sr,lb as qr,m as Tt,ma as nr,mb as Vr,n as Ot,na as ke,nb as Yr,o as Dt,oa as ir,ob as Kr,p as Et,pa as ar,pb as Jr,q as Ht,qa as cr,qb as Zr,r as Ut,ra as lr,rb as Qr,s as Lt,sa as pr,sb as eo,t as kt,ta as ur,tb as to,u as At,ua as dr,ub as ro,v as Ft,va as U,vb as oo,w as Mt,wa as fr,wb as so,x as Nt,xa as Ae,xb as xe,y as Bt,ya as mr,yb as no,z as se,za as fe,zb as io}from"./chunk-z0swphpg.js";import{Ab as be,Bb as qe,Cb as Us,Db as Ls,Eb as ks,Fb as o,Gb as Ve,Hb as As,Ib as H,Jb as Fs,Kb as Ms,Lb as Ns,Mb as Bs,Nb as _s,Ob as Is,Pb as Gs,Qb as js,Rb as zs}from"./chunk-nnnptjfw.js";import{execSync as pt}from"node:child_process";import*as pe from"node:http";import*as Je from"node:net";import*as ue from"node:os";import*as Ze from"node:path";import*as y from"node:process";var V=(t,e)=>(r)=>`\x1B[${t}m${r}\x1B[${e}m`,v={bold:V(1,22),dim:V(2,22),green:V(32,39),cyan:V(36,39)};import*as Ie from"node:fs";import*as Ge from"node:path";import*as T from"node:process";function je(t,e){let s=(e&&e!=="/"?`${t}${e}`:t).replace(/[^a-zA-Z0-9._-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,128);return s.length>0?s:"rpx"}async function Y(t){if(t.proxies.length===0)throw Error("runViaDaemon: no proxies provided");let e=t.verbose??!1,r=t.registryDir,s=new Set,l=t.proxies.map((u)=>{let h=u.id??je(u.to,u.path);if(!me(h))throw Error(`invalid registry id "${h}" derived from to="${u.to}"`);if(s.has(h))throw Error(`duplicate registry id "${h}" — set an explicit \`id\` on one of the proxies`);return s.add(h),{...u,id:h}}),a=new Date().toISOString();for(let u of l)await ge({id:u.id,from:u.from,to:u.to,path:u.path,pid:t.persistent?void 0:T.pid,cwd:T.cwd(),createdAt:a,cleanUrls:u.cleanUrls,changeOrigin:u.changeOrigin,pathRewrites:u.pathRewrites,static:u.static,loadBalancer:u.loadBalancer},r,e);let n=await xe({rpxDir:t.rpxDir,verbose:e,spawnCommand:t.spawnCommand,startupTimeoutMs:t.startupTimeoutMs,spawnEnv:t.spawnEnv});for(let u of l){let h=u.static?`static ${typeof u.static==="string"?u.static:u.static.dir}`:u.from;c.success(`https://${u.to} → ${h}`)}if(c.info(`(via rpx daemon pid=${n.pid}; \`rpx daemon:status\` to inspect)`),t.detached)return;let f=!1,p=r??fe(),d=l.map((u)=>u.id),g=async()=>{if(f)return;f=!0;for(let u of d)await he(u,r,e).catch((h)=>{o("runner",`removeEntry(${u}) failed: ${h}`,e)})},m=(u)=>{o("runner",`received ${u}, unregistering ${d.length} entries`,e),g().finally(()=>T.exit(0))};T.once("SIGINT",m),T.once("SIGTERM",m),T.once("exit",()=>{if(f)return;for(let u of d)try{Ie.unlinkSync(Ge.join(p,`${u}.json`))}catch{}}),await new Promise(()=>{})}import{spawn as st}from"node:child_process";import*as K from"node:process";class J{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[s,...l]=e.command.split(" "),a=e.cwd||K.cwd();o("start",`Starting process ${t}:`,r),o("start",` Command: ${s} ${l.join(" ")}`,r),o("start",` Working directory: ${a}`,r),o("start",` Environment variables: ${H(e.env)}`,r);let n=st(s,l,{cwd:a,env:{...K.env,...e.env},shell:!0,stdio:"inherit"});return this.processes.set(t,{command:e.command,cwd:a,process:n,env:e.env}),new Promise((f,p)=>{if(n.on("error",(d)=>{if(!this.isShuttingDown)o("start",`Process ${t} failed to start: ${d}`,r),this.processes.delete(t),p(d)}),n.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),p(Error(`Process ${t} exited with code ${d}`))}),r)n.stdout?.on("data",(d)=>{o("process",`[${t}] ${d.toString().trim()}`,!0)}),n.stderr?.on("data",(d)=>{o("process",`[${t}] ERR: ${d.toString().trim()}`,!0)});setTimeout(()=>{if(!this.isShuttingDown&&n.killed)this.processes.delete(t),p(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((s)=>{if(!r.process){s();return}r.process.once("exit",()=>{this.processes.delete(t),o("start",`Process ${t} stopped`,e),s()});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),s()}})}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((s)=>{c.error(`Failed to stop process ${r}:`,s)}));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 Po=new J;import{timingSafeEqual as nt}from"node:crypto";function it(t,e){if(t==null)return!1;let r=Buffer.from(t),s=Buffer.from(e);if(r.length!==s.length)return!1;return nt(r,s)}function Z(t){return t.toLowerCase().replace(/\.$/,"")}var at=["/.well-known/acme-challenge/"];function ct(t){let e=t.headers.get("host");if(e)return Z(e.split(":")[0]);try{return Z(new URL(t.url).hostname)}catch{return""}}function ye(t){let e=t.header.toLowerCase(),r=new Set,s=[];for(let p of t.hosts){let d=Z(p);if(d.startsWith("*."))s.push(d);else r.add(d)}let l=t.exemptPaths??at,a=t.forbiddenMessage??`Forbidden: direct origin access is not allowed; requests must arrive via the CDN.
2
- `,n=(p)=>{let d=Z(p);return r.has(d)||s.some((g)=>I(d,g))},f=(p)=>{let d=ct(p);if(!n(d))return;let g="/";try{g=new URL(p.url).pathname}catch{}if(l.some((m)=>g.startsWith(m)))return;if(it(p.headers.get(e),t.value))return;return new Response(a,{status:403,headers:{"content-type":"text/plain"}})};return f.protects=n,f}var ze={name:"@stacksjs/rpx",type:"module",version:"0.11.46",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 We=ze.version;var ee=new J,ut=new Ae("0.0.0.0"),j=new Set,re=new Set,Pe=!1,Q=null,Se=null;async function W(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)=>{Q=e});try{await ee.stopAll(t?.verbose),c.info("Shutting down proxy servers...");let e=[],r=Array.from(j).map((s)=>new Promise((l)=>{let a=s;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(n){o("cleanup",`Error stopping server: ${n}`,t?.verbose),l()}}));e.push(...r),j.clear();for(let s of re)ce(s);if(re.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 s=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(s)}`,t?.verbose),s.length>0)c.info("Cleaning up hosts file entries..."),e.push(Fe(s,t?.verbose).then(()=>{o("cleanup",`Removed hosts entries for ${s.join(", ")}`,t?.verbose)}).catch((l)=>{o("cleanup",`Failed to remove hosts entries: ${l}`,t?.verbose),c.warn(`Failed to clean up hosts file entries for ${s.join(", ")}:`,l)}))}if(t?.certs&&t.domains?.length){o("cleanup","Cleaning up SSL certificates",t?.verbose),c.info("Cleaning up SSL certificates...");let s=t.domains.map(async(l)=>{try{await ve(l,t?.verbose),o("cleanup",`Removed certificates for ${l}`,t?.verbose)}catch(a){o("cleanup",`Failed to remove certificates for ${l}: ${a}`,t?.verbose),c.warn(`Failed to clean up certificates for ${l}:`,a)}});e.push(...s)}await Promise.allSettled(e),o("cleanup","All cleanup tasks completed successfully",t?.verbose),c.success("All cleanup tasks completed successfully")}catch(e){o("cleanup",`Error during cleanup: ${e}`,t?.verbose),c.error("Error during cleanup:",e)}finally{if(Q)Q();Q=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 we=!1;function Qe(t){if(we){o("signal",`Received second ${t} signal, forcing exit`,!0),y.exit(1);return}we=!0,o("signal",`Received ${t} signal, initiating cleanup`,!0),W().catch((e)=>{o("signal",`Cleanup failed after ${t}: ${e}`,!0),y.exit(1)}).finally(()=>{we=!1})}y.once("SIGINT",()=>Qe("SIGINT"));y.once("SIGTERM",()=>Qe("SIGTERM"));y.on("uncaughtException",(t)=>{c.error("Uncaught exception (continuing):",t)});y.on("unhandledRejection",(t)=>{c.error("Unhandled rejection (continuing):",t)});async function z(t,e,r,s=5){o("connection",`Testing connection to ${t}:${e} (retries left: ${s})`,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 n=()=>new Promise((f,p)=>{let d=Je.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(),p(Error("Connection timed out"))}),d.once("error",(g)=>{o("connection",`Failed to connect to ${t}:${e}: ${g}`,r),d.destroy(),p(g)})});try{await n()}catch(f){let p=f;if(Date.now()-a>l){o("connection",`Connection test timed out after ${l}ms, but continuing anyway`,r),c.warn(`Connection test to ${t}:${e} timed out, but RPX will try to proceed anyway.`);return}if(p.code==="ECONNREFUSED"&&s>0)return o("connection",`Connection refused, server might be starting up. Retrying in 2 seconds... (${s} retries left)`,r),await new Promise((g)=>setTimeout(g,2000)),z(t,e,r,s-1);if(s>0)try{o("connection",`Trying HTTP request to ${t}:${e}`,r),await new Promise((g,m)=>{let u=pe.request({hostname:t,port:e,path:"/",method:"HEAD",timeout:5000},(h)=>{o("connection",`Received HTTP response with status: ${h.statusCode}`,r),g()});u.on("error",(h)=>m(h)),u.on("timeout",()=>{u.destroy(),m(Error("HTTP request timed out"))}),u.end()}),o("connection",`HTTP request to ${t}:${e} succeeded`,r);return}catch(g){return o("connection",`HTTP request to ${t}:${e} failed: ${g}`,r),o("connection",`Retrying socket connection in 2 seconds... (${s} retries left)`,r),await new Promise((m)=>setTimeout(m,2000)),z(t,e,r,s-1)}let d=`Failed to connect to ${t}:${e} after ${5-s} attempts: ${p.message}`;o("connection",`${d}. To bypass this check set RPX_BYPASS_CONNECTION_TEST=true`,r),c.warn(d),c.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 Me(t){o("server",`Starting server with options: ${H(t)}`,t.verbose);let e=A(t.from),r=new URL(e.startsWith("http")?e:`http://${e}`),s=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=[s.hostname];if(Ne(t)&&!s.hostname.includes("localhost")&&!s.hostname.includes("127.0.0.1")){o("hosts",`Checking if hosts file entry exists for: ${s.hostname}`,t?.verbose);try{if(!(await M(a,t.verbose))[0]){c.info(`Adding ${s.hostname} to hosts file...`),c.info("This may require sudo/administrator privileges");try{await F(a,t.verbose)}catch(p){if(c.error("Failed to add hosts entry:",p.message),c.warn("You can manually add this entry to your hosts file:"),c.warn(`127.0.0.1 ${s.hostname}`),c.warn(`::1 ${s.hostname}`),y.platform==="win32")c.warn("On Windows:"),c.warn("1. Run notepad as administrator"),c.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");else c.warn("On Unix systems:"),c.warn("sudo nano /etc/hosts")}}else o("hosts",`Host entry already exists for ${s.hostname}`,t.verbose)}catch(f){c.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),c.error(f.message),c.warn("Continuing with proxy setup despite connection test failure..."),c.info("If you need to bypass connection testing, set environment variable RPX_BYPASS_CONNECTION_TEST=true")}let n=t._cachedSSLConfig||null;if(t.https)try{if(t.https===!0)t.https=ne({...t,to:s.hostname});if(n=await k({...t,to:s.hostname,https:t.https}),!n){if(o("ssl",`Generating new certificates for ${s.hostname}`,t.verbose),await se({...t,from:r.toString(),to:s.hostname,https:t.https}),n=await k({...t,to:s.hostname,https:t.https}),!n)throw Error(`Failed to load SSL configuration after generating certificates for ${s.hostname}`)}}catch(f){throw o("server",`SSL setup failed: ${f}`,t.verbose),f}o("server",`Setting up reverse proxy with SSL config for ${s.hostname}`,t.verbose),await ft({...t,from:e,originalFrom:t.from||e,to:s.hostname,fromPort:l,sourceUrl:{hostname:r.hostname,host:r.host},ssl:n})}async function dt(t,e,r,s,l,a,n,f,p,d,g,m){o("proxy",`Creating proxy server ${t} -> ${e} with cleanUrls: ${f}`,n);let u=ie(g??s.host,m);ae(u);let h=[{host:e,route:{sourceHost:s.host,upstreamPool:u,cleanUrls:f||!1,changeOrigin:p||!1,basePath:"/",auth:d}}];if(!te({routeEntries:h,listenPort:r,sslConfig:l,originGuard:null,verbose:n??!1}))throw ce(u),Error(`Failed to start proxy server for ${e} on port ${r}`);re.add(u),ht({from:t,to:e,vitePluginUsage:a,listenPort:r,ssl:!!l,cleanUrls:f,verbose:n})}async function ft(t){o("setup",`Setting up reverse proxy: ${H(t)}`,t.verbose);let{from:e,originalFrom:r,to:s,sourceUrl:l,ssl:a,verbose:n,cleanup:f,vitePluginUsage:p,changeOrigin:d,cleanUrls:g}=t,m=80,u=443,h=tt(),O=t.portManager||ut,N=Ne(t);try{if(N&&s&&!s.includes("localhost")&&!s.includes("127.0.0.1")){if(!(await M([s],n))[0]){c.warn(`The hostname ${s} isn't in your hosts file. Adding it now...`);try{await F([s],n),c.success(`Added ${s} to your hosts file.`)}catch(B){c.error(`Failed to add ${s} to your hosts file: ${B}`),c.info(`You may need to manually add '127.0.0.1 ${s}' to your /etc/hosts file.`)}}}else if(N&&y.platform!=="darwin"&&s&&s.includes("localhost")&&!s.match(/^(localhost|127\.0\.0\.1)$/)){if(!(await M([s],n))[0]){o("hosts",`${s} not found in hosts file, adding...`,n);try{await F([s],n)}catch(B){o("hosts",`Failed to add ${s} to hosts file: ${B}`,n)}}}if(a&&!O.usedPorts.has(m)){if(!await U(m,h,n))o("setup","Starting HTTP redirect server",n),et(n),O.usedPorts.add(m);else if(o("setup","Port 80 is in use, skipping HTTP redirect",n),n)c.warn("Port 80 is in use, HTTP to HTTPS redirect will not be available")}let P=a?u:m,C=await U(P,h,n),b;if(C){if(o("setup",`Port ${P} is already in use`,n),n)c.warn(`Port ${P} is already in use. This may be another instance of rpx or another service.`);if(P===443){if(b=await O.getNextAvailablePort(3443,!0),o("setup",`Using port ${b} instead of ${P}`,n),n)c.info(`Using port ${b} instead. Access your site at https://${s}:${b}`)}else if(b=await O.getNextAvailablePort(P+1000,!0),o("setup",`Using port ${b} instead of ${P}`,n),n)c.info(`Using port ${b} instead. Access your site at http://${s}:${b}`)}else b=P,O.usedPorts.add(b),o("setup",`Using standard ${P===443?"HTTPS":"HTTP"} port ${P} for ${s}`,n);await dt(e,s,b,l,a,p,n,g,d,le(t.auth),r,t.loadBalancer)}catch(P){o("setup",`Setup failed: ${P}`,n),c.error(`Failed to setup reverse proxy: ${P.message}`),W({domains:[s],hosts:typeof f==="boolean"?f:f?.hosts,certs:typeof f==="boolean"?f:f?.certs,verbose:n,vitePluginUsage:p})}}function et(t,e=80,r=443,s,l,a){o("redirect",`Starting HTTP redirect server on port ${e}`,t);let n=pe.createServer((f,p)=>{let d=f.url?f.url.split("?",1)[0]:"";if(d.startsWith("/.well-known/acme-challenge/")){if(l){let h=l.challengeStore.handlePath(d);if(h!==void 0){o("redirect",`Serving on-demand ACME challenge ${d}`,t),p.writeHead(200,{"content-type":"text/plain"}),p.end(h);return}}if(s){let h=Oe(s,d);if(h!=null){o("redirect",`Serving ACME challenge ${d}`,t),p.writeHead(200,{"content-type":"text/plain"}),p.end(h);return}}if(l){p.writeHead(404,{"content-type":"text/plain"}),p.end("challenge not found");return}}let g=f.headers.host||"",m=g.includes(":")?g.slice(0,g.indexOf(":")):g;if(l&&m&&!l.hasCert(m)&&(!a||a(m)))l.ensureCert(m).catch(()=>{});let u=r===443?m:`${m}:${r}`;o("redirect",`Redirecting request from ${g}${f.url} to https://${u}`,t),p.writeHead(301,{Location:`https://${u}${f.url}`}),p.end()}).listen(e);j.add(n),o("redirect","HTTP redirect server started",t)}function mt(t){let e={...oe,...t};if(o("proxy",`Starting proxy with options: ${H(e)}`,e?.verbose),e.viaDaemon){if(!e.from||!e.to){c.error("viaDaemon mode requires both `from` and `to`");return}Y({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((p)=>{c.error(`Failed to register with rpx daemon: ${p.message}`),y.exit(1)});return}let r=e.to||"",s=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"],n=["test","localhost","local","example","invalid"];if(l&&a.includes(s)&&e?.verbose)c.warn(`The .${s} TLD may not work reliably for local development`),c.info(` Google owns .${s} with HSTS preloading, which can bypass local DNS`),c.info(" Consider using a reserved TLD: .test, .localhost, or .local");if(l)import("./chunk-086aa2f7.js").then(({setupDevelopmentDns:p})=>{p({domains:[r],verbose:e.verbose}).then((d)=>{if(d)Promise.resolve().then(()=>{if(e.verbose)if(n.includes(s))c.success(`DNS server started for .${s} domains`);else c.success(`DNS server started for .${s} domains (hosts file entry also added)`)});else o("dns",`Could not start DNS server - ${r} may not resolve in browser`,e.verbose)})}).catch((p)=>{o("dns",`Failed to start DNS server: ${p}`,e.verbose)});let f={from:e.from,to:e.to,cleanUrls:e.cleanUrls,https:ne(e),cleanup:e.cleanup,vitePluginUsage:e.vitePluginUsage,changeOrigin:e.changeOrigin,verbose:e.verbose,regenerateUntrustedCerts:e.regenerateUntrustedCerts};o("proxy",`Server options: ${H(f)}`,e.verbose),Me(f).catch((p)=>{o("proxy",`Failed to start proxy: ${p}`,e.verbose),c.error(`Failed to start proxy: ${p.message}`),W({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 gt(t){return t?.verbose||!1}function Ne(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 Be(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=gt(e),s=Ne(e);if(o("config",`Starting with config: ${H(e,2)}`,r),o("config",`Is multi-proxy? ${"proxies"in e}`,r),o("config",`Hosts management enabled? ${s}`,r),e.viaDaemon){let x="proxies"in e&&Array.isArray(e.proxies)?e.proxies.map((S)=>({id:S.id,from:S.from,to:S.to,path:S.path,cleanUrls:S.cleanUrls??e.cleanUrls,changeOrigin:S.changeOrigin??e.changeOrigin,pathRewrites:S.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 Y({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),c.info(`Starting command for ${x}...`),await ee.startProcess(x,i.start,r);let S=A(i.from),w=new URL(S.startsWith("http")?S:`http://${S}`),R=w.hostname||"localhost",D=Number(w.port)||80;try{await z(R,D,r),o("watch",`Dev server is ready at ${R}:${D}`,r)}catch(de){o("watch",`Connection check failed, but continuing with proxy setup: ${de}`,r),c.warn("Dev server connection check failed. RPX will try to proceed anyway...")}}catch(S){throw o("watch",`Failed to start command for ${x}: ${S}`,r),Error(`Failed to start command for ${x}: ${S}`)}}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 ee.startProcess(i,e.start,r);let x=A(e.from),S=new URL(x.startsWith("http")?x:`http://${x}`),w=S.hostname||"localhost",R=Number(S.port)||80;try{await z(w,R,r),o("watch",`Dev server is ready at ${w}:${R}`,r)}catch(D){o("watch",`Connection check failed, but continuing with proxy setup: ${D}`,r),c.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 l="proxies"in e&&Array.isArray(e.proxies)?e.proxies[0]?.to:("to"in e)?e.to:"rpx.localhost";if(y.platform!=="win32"&&(e.https||s)){if(!qe())try{o("sudo","Pre-acquiring sudo credentials for privileged operations",r),pt("sudo -v",{stdio:"inherit"})}catch{o("sudo","Could not pre-acquire sudo credentials",r)}}let a=[];if(e.productionCerts){if(a=await Le(e.productionCerts,r),a.length>0)o("ssl",`Using ${a.length} production SNI cert(s): ${a.map((i)=>i.serverName).join(", ")}`,r)}if(e.https){let i=a.length>0?null:await k(e);if(!i&&a.length===0){if(o("ssl",`No valid or trusted certificates found for ${l}, generating new ones`,e.verbose),await se(e),i=await k(e),!i)throw Error(`Failed to load SSL certificates after generation for ${l}`)}else o("ssl",`Using existing and trusted certificates for ${l}`,e.verbose);e._cachedSSLConfig=i}let n="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}],f=n.map((i)=>i.to||"rpx.localhost"),p=a.length>0?a:e._cachedSSLConfig??null,d=f.filter((i)=>i&&!i.includes("localhost")&&!i.includes("127.0.0.1")),g=["dev","app","page","new","day","foo"],m=["test","localhost","local","example","invalid"],u=[...new Set(d.map((i)=>i.split(".").pop()?.toLowerCase()))],h=u.filter((i)=>!!i&&g.includes(i));if(h.length>0&&r)c.warn(`The following TLDs may not work reliably for local development: ${h.map((i)=>`.${i}`).join(", ")}`),c.info(" These TLDs have HSTS preloading which can bypass local DNS"),c.info(" Consider using reserved TLDs: .test, .localhost, or .local");if(s&&y.platform==="darwin"&&d.length>0){let{setupDevelopmentDns:i}=await import("./chunk-086aa2f7.js");if(await i({domains:d,verbose:r})){if(r)if(u.every((w)=>!!w&&m.includes(w)))c.success(`DNS server started for ${u.map((w)=>`.${w}`).join(", ")} domains`);else c.success(`DNS server started for ${u.map((w)=>`.${w}`).join(", ")} domains (hosts file entries also added)`)}else o("dns","Could not start DNS server - custom domains may not resolve",r)}let O=async()=>{o("cleanup","Starting cleanup handler",e.verbose);try{let{tearDownDevelopmentDns:i}=await import("./chunk-086aa2f7.js");await i({verbose:e.verbose})}catch(i){o("cleanup",`Error stopping DNS server: ${i}`,e.verbose)}try{await ee.stopAll(e.verbose)}catch(i){o("cleanup",`Error stopping processes: ${i}`,e.verbose)}await W({domains:f,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",O),y.on("SIGTERM",O);let N=e.singlePortMode===!0,P=e.httpsPort??443,C=e.httpPort??80,b=e.originGuard?ye(e.originGuard):null,X=!!p&&(n.length>1||N||a.length>0),B=!p&&N&&n.length>0;if(X&&p){o("proxies",`Creating shared HTTPS server for ${n.length} domains on port ${P}`,r);let i=await Xe(n,s,r),x=null,S=e.onDemandTls,w=S?.enabled?new ke({config:S,certsDir:S.certsDir??e.productionCerts?.certsDir??Ze.join(ue.homedir(),".stacks","rpx","on-demand-certs"),initial:a,verbose:r,onCertAdded:(L)=>{if(Ke()==="restart")o("on-demand","certificate installed; restarting supervised gateway to reload TLS",r),setTimeout(()=>y.kill(y.pid,"SIGTERM"),10).unref();else de(L)}}):null,R=null,D=!1;async function de(L){if(R=L,D)return;D=!0;try{while(R){let q=R;if(R=null,o("proxies",`rebuilding :${P} with ${q.length} SNI cert(s)`,r),x)j.delete(x),x.stop(!1);let E=!1;for(let _=0;!E&&_<60;_++){let _e=te({routeEntries:i,listenPort:P,sslConfig:q,originGuard:b,verbose:r});if(_e){x=_e,E=!0;break}await new Promise((ot)=>setTimeout(ot,Math.min(25*2**Math.min(_,4),500)))}if(!E)c.error(`rpx: CRITICAL — could not rebind :${P} after cert issuance; HTTPS unbound until the next cert event or a gateway restart`)}}finally{D=!1}}if(!await U(C,"0.0.0.0",r)){let L=new Set(i.map((E)=>E.host)),q=(E)=>L.has(E)||[...L].some((_)=>I(E,_));et(r,C,P,e.acmeChallengeWebroot,w,q)}if(await U(P,"0.0.0.0",r)){if(o("proxies",`Port ${P} is already in use, cannot start shared proxy`,r),r)c.warn(`Port ${P} is in use. Shared HTTPS proxy cannot start.`);return}let rt=w&&w.sniEntries().length>0?w.sniEntries():p;if(x=te({routeEntries:i,listenPort:P,sslConfig:rt,originGuard:b,verbose:r}),!x){c.error(`Shared HTTPS proxy failed to bind :${P}; not exiting`);return}}else if(B){o("proxies",`Creating shared HTTP server for ${n.length} domains on port ${C}`,r);let i=await Xe(n,s,r);if(await U(C,"0.0.0.0",r)){if(o("proxies",`Port ${C} is already in use, cannot start shared proxy`,r),r)c.warn(`Port ${C} is in use. Shared HTTP proxy cannot start.`);return}if(!te({routeEntries:i,listenPort:C,sslConfig:null,originGuard:b,verbose:r})){c.error(`Shared HTTP proxy failed to bind :${C}; not exiting`);return}}else for(let i of n)try{let x=i.to||"rpx.localhost";o("proxy",`Starting proxy for ${x} with SSL config: ${!!p}`,i.verbose),await Me({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})}catch(x){o("proxies",`Failed to start proxy for ${i.to}: ${x}`,i.verbose),c.error(`Failed to start proxy for ${i.to}:`,x)}}async function Xe(t,e,r){let s=[],l=new Set;for(let a of t){let n=a.to||"rpx.localhost",f=a.cleanUrls||!1,p=a.path,d=Ee(p),g=le(a.auth);if(a.redirect){let m=$e(a.redirect);s.push({host:n,path:p,route:{redirect:m,basePath:d,auth:g}}),o("proxies",`Route: ${n}${p??""} → redirect ${m.status} ${m.to}${g?" (auth)":""}`,r)}else if(a.static)s.push({host:n,path:p,route:{static:Ce(a.static,f),cleanUrls:f,basePath:d,auth:g}}),o("proxies",`Route: ${n}${p??""} → static ${typeof a.static==="string"?a.static:a.static.dir}${g?" (auth)":""}`,r);else{let m=A(a.from),u=new URL(m.startsWith("http")?m:`http://${m}`),h=ie(a.from??u.host,a.loadBalancer);ae(h),re.add(h),s.push({host:n,path:p,route:{sourceHost:u.host,upstreamPool:h,cleanUrls:f,changeOrigin:a.changeOrigin||!1,pathRewrites:a.pathRewrites,basePath:d,auth:g}}),o("proxies",`Route: ${n}${p??""} → ${u.host}${g?" (auth)":""}`,r)}if(l.has(n))continue;if(l.add(n),e&&!De(n)&&!n.includes("localhost")&&!n.includes("127.0.0.1"))try{if(!(await M([n],r))[0])await F([n],r)}catch{o("hosts",`Could not add hosts entry for ${n}`,r)}}return s}var G=null;function tt(){if(y.env.RPX_BIND_HOSTNAME)return y.env.RPX_BIND_HOSTNAME;if(G)return G;try{G=Object.values(ue.networkInterfaces()).flat().some((r)=>r&&r.family==="IPv6")?"::":"0.0.0.0"}catch{G="0.0.0.0"}return G}function te(t){let{routeEntries:e,listenPort:r,sslConfig:s,originGuard:l,verbose:a}=t,n=He(e),f=Re((g,m)=>Ue(n,g,m),a),p=l?(g,m)=>l(g)??f(g,m):f,d=Te(a);try{let g=Bun.serve({port:r,hostname:tt(),reusePort:Ve(),...s?{tls:Ye(Array.isArray(s)?s.map((m)=>({serverName:m.serverName,key:m.key,cert:m.cert})):{key:s.key,cert:s.cert,ca:s.ca,requestCert:!1,rejectUnauthorized:!1})}:{},fetch(m,u){return p(m,u)},websocket:d,error(m){return o("server",`Shared proxy server error: ${m}`,a),new Response(`Server Error: ${m.message}`,{status:500})}});return j.add(g),o("proxies",`Shared ${s?"HTTPS":"HTTP"} proxy listening on port ${r} for ${n.size} domains`,a),g}catch(g){return o("proxies",`Failed to start shared proxy: ${g}`,a),console.error("Failed to start shared proxy:",g),null}}function ht(t){if(t?.vitePluginUsage||!t?.verbose)return;if(console.log(""),console.log(` ${v.green(v.bold("rpx"))} ${v.green(`v${We}`)}`),console.log(` ${v.green("➜")} ${v.dim(t?.from??"")} ${v.dim("➜")} ${v.cyan(t?.ssl?`https://${t?.to}`:`http://${t?.to}`)}`),t?.listenPort!==(t?.ssl?443:80))console.log(` ${v.green("➜")} Listening on port ${t?.listenPort}`);if(t?.cleanUrls)console.log(` ${v.green("➜")} Clean URLs enabled`)}var Hs=Be;export{Qt as ACME_CHALLENGE_PREFIX,Mr as DNS_PORT,Hr as DNS_STATE_VERSION,Ae as DefaultPortManager,Ur as LEGACY_TLD_RESOLVER_LABELS,vt as MACOS_CA_TRUST_FLAGS,$t as MACOS_SYSTEM_KEYCHAIN,ke as OnDemandCertManager,$r as RPX_HOSTS_MARKER,Nr as RPX_RESOLVER_MARKER,Ct as RPX_ROOT_CA_COMMON_NAME,Lt as SHARED_DEV_HOST_CERT_PATH,Sr as SiteSupervisor,to as acquireDaemonLock,F as addHosts,Ls as authorizeSystemAccess,He as buildHostRoutes,qt as buildRedirectLocation,At as buildRegistryTlsProxyOptions,Le as buildSniTlsConfig,St as certIncludesSanHostnames,k as checkExistingCertificates,M as checkHosts,W as cleanup,ve as cleanupCertificates,_t as clearSslConfigCache,v as colors,oe as config,jr as contentLooksLikeRpxResolver,Vt as contentTypeFor,ye as createOriginGuard,Re as createProxyFetchHandler,Te as createProxyWebSocketHandler,ur as createSiteResolver,ie as createUpstreamPool,o as debugLog,Hs as default,oe as defaultConfig,so as defaultDaemonSpawnCommand,je as deriveIdFromTarget,pr as detectProjectPreset,Fr as devDomainsFromHosts,kt as devSslToSniEntries,Or as dropStaleRpxHostsLines,Xt as enforceBasicAuth,xe as ensureDaemonRunning,wr as escapeHtml,ks as execSudoSync,ir as expandHome,Fs as extractHostname,Tr as filterRpxHostsEntries,fr as findAvailablePort,Dr as findStaleRpxHosts,Bt as forceTrustCertificate,yr as gcStaleEntries,se as generateCertificate,Zr as getDaemonPidPath,Jr as getDaemonRpxDir,Rt as getMacosLoginKeychainPath,Tt as getMacosTrustKeychains,Ns as getPrimaryDomain,fe as getRegistryDir,Ft as getRootCAPaths,Mt as getSharedDaemonCertPaths,qe as getSudoPassword,Rr as hostsLineMapsHost,ne as httpsConfig,It as isCertTrusted,eo as isDaemonRunning,Ir as isDnsServerRunning,nr as isLikelyHostname,Bs as isMultiProxyConfig,_s as isMultiProxyOptions,gr as isPidAlive,U as isPortInUse,Us as isProcessElevated,Ht as isRootCaFingerprintInKeychains,Et as isRootCaTrustedForSsl,Gs as isSingleProxyConfig,Is as isSingleProxyOptions,me as isValidId,Ms as isValidRootCA,De as isWildcardPattern,Ot as listCertSha256HashesByCommonName,dr as listDiscoverableSites,Nt as loadSSLConfig,zt as markFailure,jt as markSuccess,er as matchHost,rr as matchHostList,Ue as matchHostRoute,sr as matchesAllowedSuffix,I as matchesWildcard,Lr as normalizeDevDomain,Ee as normalizePathPrefix,xt as normalizeSha256Fingerprint,Cr as parseHostsLine,Wt as parseHtpasswd,bt as parseSha256HashesFromSecurityListing,tr as pathPrefixMatches,mr as portManager,A as primaryUpstreamUrl,cr as projectNameFromHost,Dt as pruneStaleRootCas,Oe as readAcmeChallenge,xr as readAll,Pt as readCertCommonName,yt as readCertSha256Fingerprint,Qr as readDaemonPid,hr as readEntry,lr as readSiteManifest,io as reconcileDevelopmentDnsOnIdle,Kr as reconcileStaleDevelopmentDns,As as redactSensitive,ro as releaseDaemonLock,he as removeEntry,Fe as removeHosts,Wr as removeLegacyTldResolvers,Yr as removeResolver,Er as removeStaleRpxHosts,vr as renderFailedPage,br as renderStartingPage,le as resolveAuth,js as resolvePathRewrite,$e as resolveRedirect,Kt as resolveStaticFile,Ce as resolveStaticRoute,kr as resolverBasenameForDomain,Ar as resolverBasenamesForDomains,Gr as resolverFilePath,oo as runDaemon,Y as runViaDaemon,zs as safeDeleteFile,Yt as safeRelativePath,H as safeStringify,Gt as selectUpstream,Jt as serveStaticFile,or as serverNameFromCertFilename,Xr as setupDevelopmentDns,zr as setupResolver,Ve as shouldReusePort,ar as siteIdForHost,Br as startDnsServer,ae as startHealthChecks,Be as startProxies,mt as startProxy,Me as startServer,no as stopDaemon,_r as stopDnsServer,ce as stopHealthChecks,Zt as stripBasePath,qr as syncDevelopmentDnsFromRegistry,Vr as tearDownDevelopmentDns,Ut as trustRootCaForBrowsers,wt as verifyHttpsChain,Pr as watchRegistry,ge as writeEntry};
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.48",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;
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.46",
4
+ "version": "0.11.48",
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",